archiveAug 14, 2020
Using angr for Binary Analysis, Part 1
A working tour of angr's Project, CLE loader, factory helpers, SimState, simulation managers, analyses, and explore/find paths.
Choosing a project
# Pick the binary angr should run. (auto_load_libs=False -> unresolved)
# Set False when load cost is too high.
p = angr.Project('./Binary', auto_load_libs=False)
p.arch -> architecture
p.enrty -> entry point
p.filename -> binary name
Loader
- Mapping a binary into a virtual address space is messy.
- CLE handles that work.
- The loader result lives on the project; you can inspect shared libraries that came along and run basic queries against the mapped address space.
>>> p.loader
<Loaded RedVelvet, maps [0x400000:0xa08000]>
>>> p.loader.shared_objects
OrderedDict([
('RedVelvet', <ELF Object RedVelvet, maps [0x400000:0x60209f]>),
('extern-address space', <ExternObject Object cle##externs, maps [0x800000:0x808000]>),
('cle##tls', <ELFTLSObjectV2 Object cle##tls, maps [0x900000:0x915010]>)])
>>> p.loader.min_addr
4194304
>>> p.loader.max_addr
10518528
>>> p.loader.main_object
<ELF Object RedVelvet, maps [0x400000:0x60209f]>
>>> p.loader.main_object.execstack # executable stack?
False
>>> p.loader.main_object.pic # position-independent binary?
False
p.factory (constructors)
- angr has many classes, and most of them need a project instance.
- Passing the project everywhere gets old, so
project.factoryexposes convenient constructors for the objects you reach for often.
block
project.factory.block()pulls a basic block at a given address.
>>> block = p.factory.block(p.entry) # block at the program entry point
>>> block
<Block for 0x400890, 41 bytes>
>>> block.pp() # pretty-print disassembly to stdout
0x400890: xor ebp, ebp
0x400892: mov r9, rdx
0x400895: pop rsi
0x400896: mov rdx, rsp
0x400899: and rsp, 0xfffffffffffffff0
0x40089d: push rax
0x40089e: push rsp
0x40089f: mov r8, 0x4016b0
0x4008a6: mov rcx, 0x401640
0x4008ad: mov rdi, 0x4011a9
0x4008b4: call 0x4007e0
>>> hex(block.instructions) # instruction count
'0xb'
>>> block.instruction_addrs # instruction addresses
[4196496, 4196498, 4196501, 4196502, 4196505, 4196509,
4196510, 4196511, 4196518, 4196525, 4196532]
>>> block.capstone # Capstone disassembly
<CapstoneBlock for 0x400890>
>>> block.vex # VEX IRSB for this block
IRSB <0x29 bytes, 11 ins., <Arch AMD64 (LE)>> at 0x400890
state
- The project object is only the program's "initial image."
- When angr runs code, you work with a
SimState: the simulated program state at a point in time.
>>> state = p.factory.entry_state()
>>> state
<SimState @ 0x400890>- A
SimStateholds memory, registers, and filesystem-shaped data for the program.
<SimState @ 0x400890>
>>> state.regs.rip # current instruction pointer
<BV64 0x400890>
>>> state.regs.rax
<BV64 0x1c>
>>> state.mem[p.entry].int.resolved # entry memory as a C int
<BV32 0x8949ed31>- Those values are not Python ints. They are bitvectors.
- A Python int does not carry the same meaning as a CPU word.
>>> bv = state.solver.BVV(0x1234, 32) # 32-bit vector with value 0x1234
>>> bv
<BV32 0x1234>
>>> state.solver.eval(bv) # convert to a Python int
4660- You can write bitvectors back into registers and memory, or store Python ints directly.
- angr converts them into bitvectors of the right width.
>>> state.regs.rsi = state.solver.BVV(3, 64)
>>> state.regs.rsi
<BV64 0x3>
>>> state.regs.rsi
<BV64 0x3>
>>> state.mem[0x1000].long = 3
>>> state.mem[0x1000].long.resolved
<BV64 0x3>
>>> state.mem[0x1000].long = 4
>>> state.mem[0x1000].long.resolved- Address memory with
array[index]style access. <type>: char, short, int, long, size_t, uint8_t, uint16_t, …- Store either a bitvector or a Python int.
.resolvedreturns a bitvector;.concretereturns a Python int.
>>> state.regs.rdi
<BV64 reg_rdi_1_64{UNINITIALIZED}>- Still a 64-bit bitvector, but with a name instead of a numeric value. That is a symbolic variable, and it is the core of symbolic execution here.
1. p.factory.blank_state() : empty state
2. p.factory.entry_state() : entrypoint state (main start)
3. p.factory.full_init_state() : _init start
4. p.factory.call_state() : start at a selected function- You need exact addresses. If the program takes arguments, pass them through
entry_stateorfull_init_state.
Simulation managers
- A state is a snapshot; you still need a way to step the program from there.
p.factory.simgr (p.factory.simulation_manager)
sm = p.factory.simgr(state)
sm = p.factory.simulation_manager(state)
sm.active
[<SimState @ 0x400580>]- The simulation manager drives symbolic execution and the algorithms that walk state space.
>>> sm.step()
<SimulationManager with 1 active>- That stepped one basic block of symbolic execution.
- Check
activeagain and it has moved. - The original state was not mutated:
SimStateobjects are treated as immutable under execution. - You can safely reuse one state as a base for several runs.
>>> sm.active
[<SimState @ 0x400540>]
>>> simgr.active[0]
<SimState @ 0x400540>
>>> state.regs.rip
<BV64 0x400580>Analyses
- angr ships with built-in analyses that pull interesting structure out of a program.
>>> p.analyses.
p.analyses.BackwardSlice( p.analyses.Propagator(
p.analyses.BasePointerSaveSimplifier( p.analyses.ReachingDefinitions(
p.analyses.BinDiff( p.analyses.Reassembler(
p.analyses.BinaryOptimizer( p.analyses.RecursiveStructurer(
p.analyses.BoyScout( p.analyses.RegionIdentifier(
p.analyses.CDG( p.analyses.RegionSimplifier(
p.analyses.CFB( p.analyses.SootClassHierarchy(
p.analyses.CFBlanket( p.analyses.StackCanarySimplifier(
p.analyses.CFG( p.analyses.StackPointerTracker(
p.analyses.CFGEmulated( p.analyses.StaticHooker(
p.analyses.CFGFast( p.analyses.StructuredCodeGenerator(
p.analyses.CFGFastSoot( p.analyses.Structurer(
p.analyses.CalleeCleanupFinder( p.analyses.Typehoon(sm.explore() and printing found input
# find / avoid: addresses or strings to seek or skip
sm.explore(find=0x401546, avoid=(0x4007D0))
print(sm.found[0].posix.dumps(0))