Event hooks

Welcome this second tutorial! Today we will learn how to instrument executed code using Maat's API to execute custom callbacks on events such as a register access, a memory access, a branch operation, etc.

Have fun!

Hooking events

Event hooks are one of Maat's most important features. They make it possible to control the execution, instrument code, and trigger certain actions when specific events occur.

Event hooks are managed through the engine's hooks attribute (see EventManager in the documentation). The add() method enables to add new hooks, for example:

from maat import *
m = MaatEngine(ARCH.X64, OS.LINUX)

# Hook code execution at addr 0xdeadbeef
m.hooks.add(EVENT.EXEC, WHEN.BEFORE, filter=0xdeadbeef)

# Hook register writes
m.hooks.add(EVENT.REG_W, WHEN.AFTER)

# Hook memory accesses between 0x1000 and 0x3000
m.hooks.add(EVENT.MEM_RW, WHEN.BEFORE, filter=(0x1000,0x3000))

As we can see in the example, each hook is set on a particular event, specified with a value of the EVENT enum. The complete list of events is available in the documentation, but the most common ones are listed below:

  • EXEC: executing an instruction at a given address
  • REG_R, REG_W, REG_RW: break when reading/writing register
  • MEM_R, MEM_W, MEM_RW: break when reading/writing memory
  • BRANCH: break on branch instructions

The second argument to add() indicates whether the hook must be triggered before (WHEN.BEFORE) or after (WHEN.AFTER) the event occurs.

The filter keyword argument restricts the range of memory addresses on whom the hook must be active. It can be used to hook only certain instructions for EVENT.EXEC events, and to monitor only a given memory range for EVENT.MEM_* events.

Last but not least: event hooks can also be given a unique name, and be assigned to a group of hooks. Both the unique name and the group can then be used to enable and disable arbitrary hooks or groups of hooks:

# Add two hooks, named "hook1" and "hook2", in the "reg_hooks" group
m.hooks.add(EVENT.REG_R, WHEN.BEFORE, name="hook1", group="reg_hooks")
m.hooks.add(EVENT.REG_W, WHEN.BEFORE, name="hook2", group="reg_hooks")

# Disable hook1

# Disable hook1 and hook2

# Enable hook1

# Enable hook1 and hook2

Defining custom callbacks

In order to perform arbitrary actions when a particular event occurs, hooks can be added along with event callbacks. Event callbacks are functions that are automatically executed when the hook is triggered. They must be registered when the hook is added to the engine's event manager, using the callbacks keyword argument:

m.hooks.add(EVENT.REG_R, WHEN.BEFORE, callbacks=[callback1, callback2])

Callbacks can be defined as regular functions that take a reference to the MaatEngine as single argument:

def print_rax_callback(engine):
    print(f"Current RAX: {engine.cpu.rax}")

Callbacks can optionally return an ACTION to the engine to control the execution. ACTION.CONTINUE causes execution to continue while ACTION.HALT causes the engine to stop once it finishes to execute the current instruction. For convenience, returning None is similar to returning ACTION.CONTINUE:

def print_rax_callback(engine):
    print(f"Current RAX: {engine.cpu.rax}")
    return ACTION.HALT # Stop after the current instruction

Note: hooks that have no callbacks will always cause execution to stop after the current instruction, just as though they had a callback returning ACTION.HALT. The only exception is hooks on EVENT.EXEC without callbacks, which will halt the execution before the current instruction is executed.

Event specific information

Remember the info attribute from the previous tutorial? We've learned how it gives contextual information about the engine, such as the reason it stopped running code, or the exit status of the program once it terminated. Well this info attribute will come in handy in event callbacks, because whenever a callback is executed info is set to contain useful event-specific information.

The Info documentation details all the attributes that can be used to get even info. The most common ones are addr that gives the address of the current executed instruction, and then reg_access for register accesses, mem_access for memory accesses, and branch for branch operations and path constraints. Below are a few examples showing how info can be used in callbacks:

def exec_callback(m: MaatEngine):
    print(f"Exec instructon at {m.info.addr}")                

def reg_written_callback(m: MaatEngine):
    print(f"Writing register {m.info.reg_access.reg}")
    print(f"Current value {m.info.reg_access.value}")
    print(f"New value {m.info.reg_access.new_value}")

def mem_written_callback(m: MaatEngine):
    print(f"Writing mem at {m.info.mem_access.addr}")
    print(f"Current value {m.mem.read(m.info.mem_access.addr, m.info.mem_access.size)}"
    print(f"New value {m.info.mem_access.value}")
def branch_callback(m: MaatEngine):
    if m.info.branch.taken:
        print(f"Branching to {m.info.branch.target}")
        print(f"Not branching, next inst at {m.info.branch.next}")