Skip to content

Instantly share code, notes, and snippets.

@vadim-a-yegorov
Created October 1, 2026 11:36
Show Gist options
  • Select an option

  • Save vadim-a-yegorov/14b29b4ce13594cafcae479a7e413709 to your computer and use it in GitHub Desktop.

Select an option

Save vadim-a-yegorov/14b29b4ce13594cafcae479a7e413709 to your computer and use it in GitHub Desktop.
unicorn CPU emulation, QEMU user-mode, qiling syscall+fs sandbox, Ghidra P-Code emulator, angr concolic (explore find/avoid + solver), Triton Dmiasm, single-function partial emulation, firmware MMIO rehosting. Cross-arch (x86/arm/arm64/mips), unicorn/qiling/angr/Triton snippets.
emulate
unicorn — CPU emulation (C) †unicorn-engine, map segments, set regs + stack, hook code/mem, stub imports/syscalls, run one function, read the output. Arch-portable.
QEMU user-mode runs a whole foreign-arch userland binary on the host (and backs AFL++ qemu-mode coverage). qiling (Python, on unicorn) emulates syscalls + a sandboxed rootfs with snapshot/restore and hooks — run firmware or a userland binary headless and script around it; cross-OS (Linux / Windows / macOUEFI loaders).
Ghidra P-Code emulator - arch-neutral single-function emulate from the decompiler's IR (EmulatorHelper) — portable transform execution with no arch backend of your own.
1 unicorn (CPU emulation, C/py)
map segments, set regs + stack, hook code/mem, stub imports/syscalls, run one function, read the output. Arch-portable.
#include <unicorn/unicorn.h>
uc_engine *uc;
uc_open(UC_ARCH_X86, UC_MODE_64, &uc);
uc_mem_map(uc, 0x100000, 0x1000, UC_PROT_ALL);
uc_mem_write(uc, 0x100000, code, code_len);
uint64_t rsp = 0x200000; uc_reg_write(uc, UC_X86_REG_R&rsp);
uc_hook h; uc_hook_add(uc, &h, UC_HOOK_MEM_UNMAPPED, map_cb, NULL, 1, 0); // lazy-map faults
uc_emu_start(uc, 0x100000, 0x100000 + code_len, 0, 0);
uc_reg_read(uc, UC_X86_REG_RAX, &out);
hooks: UC_HOOK_CODE (per-instruction), UC_HOOK_MEM_READ/WRITE, UC_HOOK_INTR (syscall/int). The unmapped-memory hook maps the page and returns true to continue — model only what the function touches.
open+map+run: uc_open(UC_ARCH_X86,UC_MODE_64,&uc); uc_mem_map(uc,0x100000,0x1000,UC_PROT_ALL); uc_mem_write(uc,0x100000,code,n); uc_emu_start(uc,0x100000,0x100000+n,0,0)
regs: uint64_t sp=0x200000; uc_reg_write(uc,UC_X86_REG_RSP,&sp); uc_reg_read(uc,UC_X86_REG_RAX,&out)
lazy-map faults: uc_hook_add(uc,&h,UC_HOOK_MEM_UNMAPPED,cb,0,1,0) -> in cb uc_mem_map the page, return true
per-insn trace: uc_hook_add(uc,&h,UC_HOOK_CODE,trace,0,1,0)
mem R/W hooks: UC_HOOK_MEM_READ / UC_HOOK_MEM_WRITE (log every access -> taint/coverage)
syscall/int: UC_HOOK_INTR (x86 int/syscall) -> emulate the syscall yourself
insn hook: UC_HOOK_INfor UC_X86_INS_SYSCALL / IN / OUT
snapshot: uc_context_alloc/uc_context_save/uc_context_restore -> fast re-run from a state
MMIO: uc_mmio_map(uc,base,sz,read_cb,0,write_cb,0) -> model a peripheral register
afl-unicorn: wrap as the fuzz loop (place_input writes the testcase at a buffer) -> coverage via the UC harness
py mirror: from unicorn import *; mu=Uc(UC_ARCH_ARM64,UC_MODE_ARM); mu.mem_map; mu.reg_write(UC_ARM64_REG_X0,v); mu.emu_start(...)
2 angr (concolic)
import angr, claripy
p = angr.Project("target", auto_load_libs=False)
st = p.factory.entry_state() # or blank_state(addr=fn) / call_state(fn, arg)
sm = p.factory.simulation_manager(st)
sm.explore(find=sink_addr, avoid=abort_addrs) # find/avoid = addr, list, or state->bool
if sm.found:
print(sm.found[0].solver.eval(sym_input, cast_to=bytes))
find/avoid may be a predicate on output: find=lambda s: b"win" in s.posix.dumps(1). Tame state explosion: constrain inputs, replace costly functions with SimProcedures, veritesting, concretize where symbolic is wasted (LAZY_SOLVEthe UNICORN engine option for concrete speedups).
load: import angr; p=angr.Project("t",auto_load_libs=False)
state: st=p.factory.entry_state() | blank_state(addr=fn) | call_state(fn, arg1, claripy.BVS('a',64))
explore: sm=p.factory.simulation_manager(st); sm.explore(find=sink, avoid=aborts); sm.found[0].solver.eval(sym,cast_to=bytes)
find/avoid predicate: find=lambda s: b"win" in s.posix.dumps(1)
symbolic stdin: st=p.factory.full_init_state(stdin=angr.SimFileStream('stdin')); then solver.eval the file
symbolic arg buffer: buf=claripy.BVS('buf',8*0x40); st.memory.store(addr,buf); st.add_constraints(...)
hook a function: p.hook(0x401000, myproc, length=5) | p.hook_symbol('strlen', angr.SIM_PROCEDURES['libc']['strlen']())
stub expensive fn: p.hook_symbol('check', angr.SIM_PROCEDURES['stubs']['ReturnUnconstrained']())
tame explosion: st.options.add(angr.options.LAZY_SOLVE; veritesting: p.factory.simgr(st, veritesting=True)
concrete speedups: add angr.options.unicorn (UNICORN engine) to run concrete regions natively
CFG: cfg=p.analyses.CFGFast() ; cfg.functions ; call graph for sink discovery
loop control: sm.use_technique(angr.exploration_techniques.LoopSeer(bound=10))
DFS/limits: sm.use_technique(angr.exploration_techniques.DFS()) ; sm.run(n=1000)
symbion (concrete+symbolic): drive the real process to a point, lift state into angr for the hard part
angrop: rop=p.analyses.ROP(); rop.find_gadgets(); chain=rop.execve(b"/bin/sh"); chain.payload_str()
solve a magic/length gate: constrain the branch condition, solver.eval the input that passes
3 triton (D+ taint + deobf)
dynamic symbolic execution + taint over a concrete trace (x86/arm64) — build a path predicate from a run, solve for a different branch. miasm: IR lift + symbolic execution + its own emulator; good for deobfuscation and SMT-solving a flattened/obfuscated function.
ctx: from triton import *; ctx=TritonContext(ARCH.X86_64)
feed a trace: for inst in trace: ctx.processing(inst) // build the path predicate
flip a branch: m=ctx.getModel(ctx.getPathPredicate() & (cond==1)) -> the input for the other side
symbolize: ctx.symbolizeMemory(MemoryAccess(addr,8)) ; ctx.symbolizeRegister(ctx.registers.rdi)
taint: ctx.taintMemory(addr) ; ctx.isRegisterTainted(reg) -> source->sink on a concrete run
VM-based obfuscation: symbolically execute the handler table to recover the bytecode semantics
MBA simplify: ctx.simplify(ast) ; or msynth/gamba on a lifted expression (deobfuscate mixed boolean-arithmetic)
4 qiling (headless OS/firmware)
run: from qiling import Qiling; ql=Qiling(["./bin"],"rootfs"); ql.run()
hook: ql.hook_address(cb,0x1234) ; ql.hook_code(cb) ; ql.hook_intno(cb,0x80)
mem: ql.mem.read(a,n) ; ql.mem.write(a,bytes) ; ql.arch.regs.rax
snapshot: ql.save(snapshot="s.bin") ; ql.restore(snapshot="s.bin") // fast re-run
firmware/uefi: Qiling with the vendor rootfs or a UEFI loader ; MMIO via ql.mem.map_mmio
fuzz: qiling + unicornafl -> ql.uc as the AFL executor
5 ghidra / miasm
EmulatorHelper: EmulatorHelper eh=new EmulatorHelper(program); eh.writeRegister("RDI",x); eh.setBreakpoint(end); eh.run(start); eh.readRegister("RAX")
PcodeEmulator: emulate a single function from P-Code (arch-neutral, headless)
miasm lift+symexec: Machine("x86_64"); ira; SymbolicExecutionEngine -> solve with z3 ; good for flattened/obfuscated fns
6 qemu
qemu-user: qemu-aarch64 -L /usr/aarch64-linux-gnu ./bin ; backs afl-fuzz -Q
qemu plugin / TCG: a TCG plugin for coverage or taint on whole-binary foreign-arch
fuzzware: auto-model MMIO so a Cortex-M firmware runs headless -> then fuzz
HALucinator: HAL-function hooking to skip hardware ; handler-table modeling
avatar2: hardware-in-the-loop (forward MMIO to the real device, run the rest emulated)
renode: full-emulation for IoT firmware (multi-peripheral)
MMIO modeling by hand: hook reads on the peripheral range -> return plausible/attacker values (unicorn / qiling S39)
7 unicorn (py) minimal:
uc=Uc(UC_ARCH_X86,UC_MODE_64); uc.mem_map(0x1000,0x1000); uc.mem_write(0x1000,code); uc.reg_write(UC_X86_REG_RSP,0x300000); uc.emu_start(0x1000,0x1000+len).
8 unicorn lazy-map:
uc.hook_add(UC_HOOK_MEM_UNMAPPED, lambda u,t,a,s,v,d:(u.mem_map(a&~0xfff,0x1000),True)[1]).
9 unicorn trace+disasm:
uc.hook_add(UC_HOOK_CODE, lambda u,a,s,d: print(hex(a), next(cs.disasm(u.mem_read(a,s),a)).mnemonic)).
10 unicorn stub a call:
in a HOOK_CODE at the call site, set the result reg + advance PC past the call — skip libc you did not map.
11 qiling run:
from qiling import Qiling; ql=Qiling(['./x'],'/rootfs',verbose=QL_VERBOSE.OFF); ql.run(); hook ql.hook_address(cb,addr); ql.mem.read/write.
12 qiling snapshot + syscall fake:
ql.save(); ql.restore(); ql.os.set_syscall('read', my_read) to feed a device/file from the rootfs.
13 angr find/avoid from CLI idea:
sm.explore(find=0xADDR, avoid=[0xBAD]); print(sm.found[0].posix.dumps(0)) → the stdin that reaches the target.
14 angr solve a buffer check:
arg=claripy.BVS('a',8*32); s=p.factory.call_state(fn,arg); sm.explore(find=win); sm.found[0].solver.eval(arg,cast_to=bytes).
15 angr printable constraint:
for b in arg.chop(8): s.add_constraints(b>=0x20, b<=0x7e).
16 angr speed:
s.options|={angr.options.LAZY_SOLVES}; p.hook_symbol('memcpy', angr.SIM_PROCEDURES['libc']['memcpy']()); sm.use_technique(angr.exploration_techniques.Veritesting()).
17 angr state-explosion guard:
sm.explore(find=…, num_find=1); DFS(); avoid the known abort block; hook expensive crypto with a SimProcedure.
18 triton D
ctx=TritonContext(ARCH.X86_64); ctx.setConcreteMemoryAreaValue(a,code); ctx.processing(Instruction(a,bytes)); ...; m=ctx.getModel(ctx.getPathPredicate()) → input for another branch.
19 ghidra p-code emulate:
EmulatorHelper(program) → set regs, setBreakpoint(end), run(), read a register — no arch backend needed.
20 qemu foreign arch + gdbstub:
qemu-aarch64 -L /usr/aarch64-linux-gnu -g 1234 ./arm_bin then target remote :1234 in pwndbg.
21 Partial transform:
map only the function + its data in unicorn, call with chosen inputs, read the output — recover a key/decoded blob without the program.
22 Firmware MMIO:
qiling with the firmware rootfs; or a unicorn UC_HOOK_MEM_READ on the peripheral range returning modeled values.
23 Seed a fuzzer from a solve:
angr/Triton cracks the magic/length gate → drop the input in the fuzzer corpus → resume coverage (fuzz-target-synth).
24 miasm for flattened/obfuscated:
lift to IR, run its symbolic engine to collapse the dispatcher, solve branch conditions with z3.
NOTES
- emulate the one transform (unpack stub, checksum, decode) instead of rehosting the system (unpack-runtime-protected)
- solver bridges fuzzing: a 4-byte magic / length equation / checksum -> angr/Triton produce the input, then resume (fuzz)
Sources: unicorn-engine/unicorn samples (samples/mem_apis.c, tests/unit/test_x86.c); angr docs (docs.angr.io core-concepts/pathgroups) — simulation_manager / explore / solver.eval, exploration_techniques; QEMU qemu.org; qiling qiling.io; JonathanSalwan/Triton (TritonContext / getModel); cea-sec/miasm; fuzzware / HALucinator. API + snippets verified against these.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment