When you ask for a step of execution to happen in angr, something has to actually perform the step. angr uses a series of engines (subclasses of the
SimEngineclass) to emulate the effects that of a given section of code has on an input state. The execution core of angr simply tries all the available engines in sequence, taking the first one that is able to handle the step. The following is the default list of engines, in order:
- The failure engine kicks in when the previous step took us to some uncontinuable state
- The syscall engine kicks in when the previous step ended in a syscall
- The hook engine kicks in when the current address is hooked
- The unicorn engine kicks in when the
UNICORNstate option is enabled and there is no symbolic data in the state
- The VEX engine kicks in as the final fallback.
The code that actually tries all the engines in turn is
project.factory.successors(state, **kwargs), which passes its arguments onto each of the engines. This function is at the heart of
simulation_manager.step(). It returns a SimSuccessors object, which we discussed briefly before. The purpose of SimSuccessors is to perform a simple categorization of the successor states, stored in various list attributes. They are:
TODO: rewrite this to fix the narrative
Like any decent execution engine, angr supports breakpoints. This is pretty cool! A point is set as follows:
>>> import angr
>>> b = angr.Project('examples/fauxware/fauxware')
# get our state
>>> s = b.factory.entry_state()
# add a breakpoint. This breakpoint will drop into ipdb right before a memory write happens.
# on the other hand, we can have a breakpoint trigger right *after* a memory write happens.
# we can also have a callback function run instead of opening ipdb.
>>> def debug_func(state):
... print("State %s is about to do a memory write!")
>>> s.inspect.b('mem_write', when=angr.BP_AFTER, action=debug_func)
# or, you can have it drop you in an embedded IPython!
>>> s.inspect.b('mem_write', when=angr.BP_AFTER, action=angr.BP_IPYTHON)
There are many other places to break than a memory write. Here is the list. You can break at BP_BEFORE or BP_AFTER for each of these events.
These events expose different attributes:
These attributes can be accessed as members of
state.inspectduring the appropriate breakpoint callback to access the appropriate values. You can even modify these value to modify further uses of the values!
>>> def track_reads(state):
... print('Read', state.inspect.mem_read_expr, 'from', state.inspect.mem_read_address)
>>> s.inspect.b('mem_read', when=angr.BP_AFTER, action=track_reads)
Additionally, each of these properties can be used as a keyword argument to
inspect.bto make the breakpoint conditional:
# This will break before a memory write if 0x1000 is a possible value of its target expression
>>> s.inspect.b('mem_write', mem_write_address=0x1000)
# This will break before a memory write if 0x1000 is the *only* value of its target expression
>>> s.inspect.b('mem_write', mem_write_address=0x1000, mem_write_address_unique=True)
# This will break after instruction 0x8000, but only 0x1000 is a possible value of the last expression that was read from memory
>>> s.inspect.b('instruction', when=angr.BP_AFTER, instruction=0x8000, mem_read_expr=0x1000)
Cool stuff! In fact, we can even specify a function as a condition:
# this is a complex condition that could do anything! In this case, it makes sure that RAX is 0x41414141 and
# that the basic block starting at 0x8004 was executed sometime in this path's history
>>> def cond(state):
... return state.eval(state.regs.rax, cast_to=str) == 'AAAA' and 0x8004 in state.inspect.backtrace
>>> s.inspect.b('mem_write', condition=cond)
That is some cool stuff!
mem_readbreakpoint gets triggered anytime there are memory reads by either the executing program or the binary analysis. If you are using breakpoint on
mem_readand also using
state.memto load data from memory addresses, then know that the breakpoint will be fired as you are technically reading memory.
So if you want to load data from memory and not trigger any
mem_readbreakpoint you have had set up, then use
state.memory.loadwith the keyword arguments
This is also true for
state.findand you can use the same keyword arguments to prevent
mem_readbreakpoints from firing.