Skip to content

Commit 1b07d76

Browse files
xusheng6claude
andcommitted
Add the BNIL (LLIL) emulator plugin
Add an experimental plugin under plugins/emulator that emulates Binary Ninja's Low Level IL with full register, flag, and memory state, structured like the debugger: a core engine (emulatorcore) exposing a C ABI, plus C++ (emulatorapi) and Python bindings. Supports cross-function emulation, breakpoints, arguments, built-in libc stubs, user hooks (call/syscall/memory/pre-instruction/intrinsic/ stdio), and JSON state serialization. Includes a self-contained Python unittest suite (plugins/emulator/test), a user guide (docs/guide/emulator.md) wired into the mkdocs nav, and the intx vendor library for wide register values. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 10bcb0a commit 1b07d76

27 files changed

Lines changed: 10024 additions & 0 deletions

binaryninjaapi.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,23 @@
3131
#include "binaryninjacore.h"
3232
#include "exceptions.h"
3333

34+
// intx provides the wide-integer type used by the emulator plugin's API. If a
35+
// translation unit includes <windows.h> before this header (as several debugger
36+
// adapters do), its min/max macros would break intx's numeric_limits<>::min()/max()
37+
// member definitions. Neutralize those macros just for the intx include, then
38+
// restore them so any later code that relies on windows.h min/max is unaffected.
39+
#if defined(_WIN32)
40+
#pragma push_macro("min")
41+
#pragma push_macro("max")
42+
#undef min
43+
#undef max
44+
#endif
45+
#include "vendor/intx/intx.hpp"
46+
#if defined(_WIN32)
47+
#pragma pop_macro("min")
48+
#pragma pop_macro("max")
49+
#endif
50+
3451
#include "json/json.h"
3552
#include "rapidjsonwrapper.h"
3653
#include "vendor/nlohmann/json.hpp"
@@ -23337,6 +23354,7 @@ namespace BinaryNinja {
2333723354
std::optional<DerivedString> RecognizeConstantData(
2333823355
const HighLevelILInstruction& instr) override;
2333923356
};
23357+
2334023358
} // namespace BinaryNinja
2334123359

2334223360

binaryninjacore.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9427,6 +9427,7 @@ extern "C"
94279427
BINARYNINJACOREAPI BNPossibleValueSet BNPossibleValueSetNegate(const BNPossibleValueSet* object, size_t size);
94289428
BINARYNINJACOREAPI BNPossibleValueSet BNPossibleValueSetNot(const BNPossibleValueSet* object, size_t size);
94299429

9430+
94309431
#ifdef __cplusplus
94319432
}
94329433
#endif

docs/guide/emulator.md

Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
# BNIL Emulator — Python API Guide
2+
3+
The emulator plugin executes Binary Ninja's Low Level IL (LLIL) with full register,
4+
flag, and memory state. It is aimed at focused, snippet-level tasks — decrypting
5+
strings, resolving API hashes, evaluating a slice of a function — rather than
6+
full-program or whole-system emulation. See the
7+
[plugin README](https://github.com/Vector35/binaryninja-api/blob/dev/plugins/emulator/README.md)
8+
for the scope, accuracy notes, and the list of supported LLIL instructions.
9+
10+
> **Experimental.** The API and behavior may change.
11+
12+
Today the emulator works on **LLIL**; we intend to expand it to **MLIL** and **HLIL**
13+
as well.
14+
15+
## Contents
16+
17+
- [Getting the class](#getting-the-class)
18+
- [Creating an emulator](#creating-an-emulator)
19+
- [Setting the entry point](#setting-the-entry-point)
20+
- [Running and stepping](#running-and-stepping)
21+
- [Stop reasons](#stop-reasons)
22+
- [Registers, flags, and temporaries](#registers-flags-and-temporaries)
23+
- [Memory](#memory)
24+
- [Function arguments](#function-arguments)
25+
- [Breakpoints](#breakpoints)
26+
- [Hooks](#hooks)
27+
- [Built-in libc stubs](#built-in-libc-stubs)
28+
- [Call stack](#call-stack)
29+
- [State serialization](#state-serialization)
30+
- [Full example: cross-function emulation](#full-example-cross-function-emulation)
31+
- [API reference](#api-reference)
32+
33+
## Getting the class
34+
35+
The emulator is a normal importable class — there is no auto-injected console
36+
variable. In the Python console or a headless script:
37+
38+
```python
39+
from binaryninja.emulator import LLILEmulator, ILEmulatorStopReason
40+
```
41+
42+
In the interactive console, `bv` (current view) and `here` (current address) are
43+
already available as built-in magic variables, so the two lines below are all you
44+
need to get going:
45+
46+
```python
47+
emu = LLILEmulator(bv)
48+
emu.set_entry_point(here)
49+
```
50+
51+
If you use the emulator often and want `LLILEmulator` available without importing
52+
each session, add the import to `~/.binaryninja/startup.py`.
53+
54+
## Creating an emulator
55+
56+
`LLILEmulator` can be constructed three ways:
57+
58+
```python
59+
# 1. For a whole view — resolve addresses to functions on demand (most common).
60+
emu = LLILEmulator(bv)
61+
62+
# 2. For a specific LLIL function.
63+
emu = LLILEmulator(bv, il=func.llil)
64+
65+
# 3. Wrap an existing core handle (advanced / internal).
66+
emu = LLILEmulator(bv, handle=raw_handle)
67+
```
68+
69+
A view can have as many independent emulators as you like; each keeps its own
70+
registers, memory, and breakpoints.
71+
72+
## Setting the entry point
73+
74+
`set_entry_point` accepts two forms:
75+
76+
```python
77+
# Address form: resolve an address to its function and start at its first LLIL
78+
# instruction. Returns False if the address is not inside an analyzed function.
79+
if not emu.set_entry_point(0x401000):
80+
raise ValueError("address is not in an analyzed function")
81+
82+
# IL form: start at a specific LLIL instruction index of a given LLIL function.
83+
emu.set_entry_point(func.llil, 5)
84+
```
85+
86+
## Running and stepping
87+
88+
```python
89+
emu.set_max_instructions(100000) # safety limit against runaway loops
90+
reason = emu.run() # run until a stop condition
91+
92+
emu.step() # execute a single instruction
93+
emu.step_n(10) # execute up to 10 instructions
94+
emu.step_over() # step over a call (run through the callee)
95+
96+
emu.request_stop() # ask a running emulator to stop (thread-safe)
97+
```
98+
99+
Progress and position:
100+
101+
```python
102+
emu.instructions_executed # count since the last reset
103+
emu.current_address # address of the current instruction
104+
emu.instruction_index # LLIL index within the current function (settable)
105+
```
106+
107+
## Stop reasons
108+
109+
`run`, `step`, `step_n`, and `step_over` all return an `ILEmulatorStopReason`, also
110+
available afterward via `emu.stop_reason` with a human-readable `emu.stop_message`:
111+
112+
| Reason | Meaning |
113+
| --- | --- |
114+
| `ILEmulatorRunning` | Still running (not a terminal state) |
115+
| `ILEmulatorBreakpoint` | Hit a breakpoint |
116+
| `ILEmulatorInstructionLimit` | Reached `set_max_instructions` |
117+
| `ILEmulatorHalt` | Returned from the top-level function / halted normally |
118+
| `ILEmulatorError` | Internal error |
119+
| `ILEmulatorCallHook` | Stopped by a call hook |
120+
| `ILEmulatorSyscallHook` | Stopped by a syscall hook |
121+
| `ILEmulatorUndefinedBehavior` | Executed undefined behavior |
122+
| `ILEmulatorUnimplemented` | Hit an unimplemented LLIL instruction |
123+
| `ILEmulatorUserRequestedStop` | Stopped via `request_stop` |
124+
125+
```python
126+
reason = emu.run()
127+
if reason != ILEmulatorStopReason.ILEmulatorHalt:
128+
print(f"stopped early: {reason.name}{emu.stop_message}")
129+
```
130+
131+
## Registers, flags, and temporaries
132+
133+
Registers accept either a name (`'rax'`) or a numeric register ID:
134+
135+
```python
136+
emu.set_register('rsp', 0x7fff0000)
137+
rax = emu.get_register('rax')
138+
139+
emu.regs # snapshot dict of every named register -> value
140+
141+
emu.set_flag('z', 1) # flag by name or ID
142+
emu.get_flag('z')
143+
144+
emu.set_temp_register(0, 0x1234) # LLIL temporary registers, by index
145+
emu.get_temp_register(0)
146+
```
147+
148+
## Memory
149+
150+
> **The emulator does not inherit memory from the BinaryView.** It starts with an
151+
> empty address space of its own. Execution works because the emulator runs on the
152+
> lifted LLIL, not by fetching bytes from its memory — but any data the code *reads*
153+
> (globals, `.rodata`, strings, tables, the stack) is **not** present unless you put
154+
> it there. Reading an address that holds data in the view returns zeroes in the
155+
> emulator. If you want the view's bytes, copy them in explicitly.
156+
157+
Map regions before accessing them, then read and write raw bytes:
158+
159+
```python
160+
emu.map_memory(0x1000, b'\x00' * 0x1000) # map with data
161+
emu.map_memory(0x2000, 0x1000) # map zero-filled
162+
emu.map_memory(0x3000, 0x1000, "stack") # map a named region
163+
164+
emu.write_memory(0x1000, b'hello') # returns bytes written
165+
emu.read_memory(0x1000, 5) # -> b'hello'
166+
167+
emu.get_mapped_regions() # [{'start':..., 'size':..., 'name':...}, ...]
168+
```
169+
170+
### Copying BinaryView memory into the emulator
171+
172+
To emulate code that reads existing program data, copy the relevant bytes from the
173+
view into the emulator at the same addresses. Copy whole segments:
174+
175+
```python
176+
for seg in bv.segments:
177+
data = bv.read(seg.start, seg.length) # bytes actually backed by the file
178+
if data:
179+
emu.map_memory(seg.start, data)
180+
```
181+
182+
...or just the region you need (cheaper for large binaries):
183+
184+
```python
185+
emu.map_memory(table_addr, bv.read(table_addr, table_size))
186+
```
187+
188+
Alternatively, serve reads on demand with a memory-read hook that pulls from the view
189+
(see [Hooks](#hooks)):
190+
191+
```python
192+
emu.set_memory_read_hook(
193+
lambda emu, addr, size: int.from_bytes(bv.read(addr, size), 'little')
194+
if bv.read(addr, size) else None)
195+
```
196+
197+
## Function arguments
198+
199+
Arguments are placed using the function's default calling convention:
200+
201+
```python
202+
emu.set_argument(0, 0x1000) # a single argument by index
203+
emu.set_arguments([0x1000, 16, 42]) # several at once
204+
```
205+
206+
## Breakpoints
207+
208+
```python
209+
emu.add_breakpoint(0x401234) # stops *before* executing that address
210+
emu.remove_breakpoint(0x401234)
211+
emu.clear_breakpoints()
212+
```
213+
214+
A breakpoint stops the emulator before the target instruction executes, so on stop
215+
`emu.current_address` equals the breakpoint address and its side effects have not yet
216+
occurred.
217+
218+
## Hooks
219+
220+
Hooks let embedding code intercept emulation. Pass a callable to install a hook and
221+
`None` to remove it. Exceptions raised inside a hook are swallowed and treated as
222+
"not handled".
223+
224+
```python
225+
# CALL: return True if handled (advance past the call), False to let the emulator
226+
# try cross-function emulation.
227+
emu.set_call_hook(lambda emu, target: True) # skip all calls
228+
229+
# SYSCALL: return True if handled, False to stop.
230+
emu.set_syscall_hook(lambda emu: True)
231+
232+
# Memory read: return the value to use, or None to fall through to real memory.
233+
emu.set_memory_read_hook(lambda emu, addr, size: 0 if addr in mmio else None)
234+
235+
# Memory write: return True if handled, False to let the write proceed.
236+
emu.set_memory_write_hook(lambda emu, addr, size, value: False)
237+
238+
# Before each instruction: return True to continue, False to stop.
239+
emu.set_pre_instruction_hook(lambda emu, index: True)
240+
241+
# INTRINSIC: return a list of (register_id, value) pairs if handled, else None.
242+
emu.set_intrinsic_hook(lambda emu, intrinsic, params: None)
243+
244+
# stdout from emulated printf/puts/putchar; data is bytes.
245+
emu.set_stdout_callback(lambda emu, data: sys.stdout.write(data.decode('latin1')))
246+
247+
# stdin for emulated getchar/fgets/fread; return up to max_len bytes, b'' for EOF.
248+
emu.set_stdin_callback(lambda emu, max_len: b'')
249+
```
250+
251+
The memory-read hook fires on **every** load, including implicit reads such as the
252+
stack pop performed by a `ret`, so filter by address when you only want to intercept
253+
specific regions.
254+
255+
## Built-in libc stubs
256+
257+
The emulator ships simple stubs for common libc functions so snippets that call
258+
`printf`, `malloc`, etc. can run without a real libc:
259+
260+
```python
261+
emu.builtin_libc_stubs # bool, default True — enable the built-in stubs
262+
emu.log_libc_calls # bool, default True — log stub calls to the console
263+
emu.nop_unknown_externals # bool, default False — treat unknown external calls
264+
# as no-ops returning 0 instead of stopping
265+
```
266+
267+
## Call stack
268+
269+
While stopped inside a called function:
270+
271+
```python
272+
emu.call_stack_depth # number of nested calls
273+
emu.get_call_stack() # [{'function_address':..., 'return_address':...}, ...]
274+
```
275+
276+
Frame 0 is the current function; later frames are its callers.
277+
278+
## State serialization
279+
280+
Emulator state (registers, flags, memory, call stack) can be snapshotted to JSON and
281+
restored — useful for save/restore points or reproducing a state across runs:
282+
283+
```python
284+
snapshot = emu.save_state() # -> JSON string
285+
emu.load_state(snapshot) # restore, returns True on success
286+
287+
emu.save_state_to_file("state.json")
288+
emu.load_state_from_file("state.json")
289+
290+
emu.reset() # clear all state back to initial
291+
```
292+
293+
## Full example: cross-function emulation
294+
295+
Decrypt a string by emulating a decryption routine, letting the emulator run through
296+
the called functions and skipping anything it can't resolve:
297+
298+
```python
299+
from binaryninja.emulator import LLILEmulator, ILEmulatorStopReason
300+
301+
emu = LLILEmulator(bv)
302+
emu.nop_unknown_externals = True # don't stop on unresolved externals
303+
emu.set_max_instructions(1_000_000)
304+
305+
# Give the routine a scratch output buffer and the encrypted input.
306+
emu.map_memory(0x100000, 0x1000, "out")
307+
emu.map_memory(0x101000, encrypted, "in")
308+
309+
emu.set_entry_point(decrypt_func.start)
310+
emu.set_arguments([0x100000, 0x101000, len(encrypted)])
311+
312+
reason = emu.run()
313+
if reason == ILEmulatorStopReason.ILEmulatorHalt:
314+
print(emu.read_memory(0x100000, 0x100).split(b'\x00', 1)[0])
315+
else:
316+
print(f"stopped: {reason.name}{emu.stop_message}")
317+
```
318+
319+
## API reference
320+
321+
Everything is on the `LLILEmulator` class.
322+
323+
**Construction:** `LLILEmulator(view, il=None, handle=None)`
324+
325+
**Execution:** `run`, `step`, `step_n`, `step_over`, `request_stop`,
326+
`set_max_instructions`, `reset`
327+
328+
**Entry / arguments:** `set_entry_point`, `set_argument`, `set_arguments`
329+
330+
**State (properties):** `instruction_index`, `current_address`, `stop_reason`,
331+
`stop_message`, `instructions_executed`, `call_stack_depth`, `regs`
332+
333+
**Registers / flags:** `get_register`, `set_register`, `get_temp_register`,
334+
`set_temp_register`, `get_flag`, `set_flag`
335+
336+
**Memory:** `map_memory`, `read_memory`, `write_memory`, `get_mapped_regions`
337+
338+
**Breakpoints:** `add_breakpoint`, `remove_breakpoint`, `clear_breakpoints`
339+
340+
**Hooks:** `set_call_hook`, `set_syscall_hook`, `set_memory_read_hook`,
341+
`set_memory_write_hook`, `set_pre_instruction_hook`, `set_intrinsic_hook`,
342+
`set_stdout_callback`, `set_stdin_callback`
343+
344+
**libc stubs (properties):** `builtin_libc_stubs`, `log_libc_calls`,
345+
`nop_unknown_externals`
346+
347+
**Call stack:** `get_call_stack`
348+
349+
**Serialization:** `save_state`, `load_state`, `save_state_to_file`,
350+
`load_state_from_file`
351+
352+
For runnable, self-contained examples of every feature above, see the
353+
[test suite](https://github.com/Vector35/binaryninja-api/blob/dev/plugins/emulator/test/emulator_test.py).

0 commit comments

Comments
 (0)