Gravitational plate of three masses and a slashed discABC0Static engraved plate. Three-dimensional view is unavailable or reduced motion is requested.

← back to fieldarchive

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.factory exposes 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 SimState holds 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.
  • .resolved returns a bitvector; .concrete returns 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_state or full_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 active again and it has moved.
  • The original state was not mutated: SimState objects 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))

related

  1. Aug 14, 2020/archiveangr binary analysis notes 2: the loader
  2. Aug 14, 2020/articleUsing angr for Binary Analysis, Part 3
  3. Mar 1, 2021/articleLinked-List Attack Surface on Intel (Structure Notes)

graphfeed