Created
October 1, 2026 11:36
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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