-
Notifications
You must be signed in to change notification settings - Fork 290
Add the framework for BNIL emulator and implement LLIL emulator #8314
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,6 +31,23 @@ | |
| #include "binaryninjacore.h" | ||
| #include "exceptions.h" | ||
|
|
||
| // intx provides the wide-integer type used by the emulator plugin's API. If a | ||
| // translation unit includes <windows.h> before this header (as several debugger | ||
| // adapters do), its min/max macros would break intx's numeric_limits<>::min()/max() | ||
| // member definitions. Neutralize those macros just for the intx include, then | ||
| // restore them so any later code that relies on windows.h min/max is unaffected. | ||
| #if defined(_WIN32) | ||
| #pragma push_macro("min") | ||
| #pragma push_macro("max") | ||
| #undef min | ||
| #undef max | ||
| #endif | ||
| #include "vendor/intx/intx.hpp" | ||
| #if defined(_WIN32) | ||
| #pragma pop_macro("min") | ||
| #pragma pop_macro("max") | ||
| #endif | ||
|
|
||
| #include "json/json.h" | ||
| #include "rapidjsonwrapper.h" | ||
| #include "vendor/nlohmann/json.hpp" | ||
|
|
@@ -23337,6 +23354,7 @@ namespace BinaryNinja { | |
| std::optional<DerivedString> RecognizeConstantData( | ||
| const HighLevelILInstruction& instr) override; | ||
| }; | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. erroneous formatting? |
||
| } // namespace BinaryNinja | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9427,6 +9427,7 @@ extern "C" | |
| BINARYNINJACOREAPI BNPossibleValueSet BNPossibleValueSetNegate(const BNPossibleValueSet* object, size_t size); | ||
| BINARYNINJACOREAPI BNPossibleValueSet BNPossibleValueSetNot(const BNPossibleValueSet* object, size_t size); | ||
|
|
||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems unnecessary.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. erroneous formatting? |
||
| #ifdef __cplusplus | ||
| } | ||
| #endif | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,353 @@ | ||
| # BNIL Emulator — Python API Guide | ||
|
|
||
| The emulator plugin executes Binary Ninja's Low Level IL (LLIL) with full register, | ||
| flag, and memory state. It is aimed at focused, snippet-level tasks — decrypting | ||
| strings, resolving API hashes, evaluating a slice of a function — rather than | ||
| full-program or whole-system emulation. See the | ||
| [plugin README](https://github.com/Vector35/binaryninja-api/blob/dev/plugins/emulator/README.md) | ||
| for the scope, accuracy notes, and the list of supported LLIL instructions. | ||
|
|
||
| > **Experimental.** The API and behavior may change. | ||
|
|
||
| Today the emulator works on **LLIL**; we intend to expand it to **MLIL** and **HLIL** | ||
| as well. | ||
|
|
||
| ## Contents | ||
|
|
||
| - [Getting the class](#getting-the-class) | ||
| - [Creating an emulator](#creating-an-emulator) | ||
| - [Setting the entry point](#setting-the-entry-point) | ||
| - [Running and stepping](#running-and-stepping) | ||
| - [Stop reasons](#stop-reasons) | ||
| - [Registers, flags, and temporaries](#registers-flags-and-temporaries) | ||
| - [Memory](#memory) | ||
| - [Function arguments](#function-arguments) | ||
| - [Breakpoints](#breakpoints) | ||
| - [Hooks](#hooks) | ||
| - [Built-in libc stubs](#built-in-libc-stubs) | ||
| - [Call stack](#call-stack) | ||
| - [State serialization](#state-serialization) | ||
| - [Full example: cross-function emulation](#full-example-cross-function-emulation) | ||
| - [API reference](#api-reference) | ||
|
|
||
| ## Getting the class | ||
|
|
||
| The emulator is a normal importable class — there is no auto-injected console | ||
| variable. In the Python console or a headless script: | ||
|
|
||
| ```python | ||
| from binaryninja.emulator import LLILEmulator, ILEmulatorStopReason | ||
| ``` | ||
|
|
||
| In the interactive console, `bv` (current view) and `here` (current address) are | ||
| already available as built-in magic variables, so the two lines below are all you | ||
| need to get going: | ||
|
|
||
| ```python | ||
| emu = LLILEmulator(bv) | ||
| emu.set_entry_point(here) | ||
| ``` | ||
|
|
||
| If you use the emulator often and want `LLILEmulator` available without importing | ||
| each session, add the import to `~/.binaryninja/startup.py`. | ||
|
|
||
| ## Creating an emulator | ||
|
|
||
| `LLILEmulator` can be constructed three ways: | ||
|
|
||
| ```python | ||
| # 1. For a whole view — resolve addresses to functions on demand (most common). | ||
| emu = LLILEmulator(bv) | ||
|
|
||
| # 2. For a specific LLIL function. | ||
| emu = LLILEmulator(bv, il=func.llil) | ||
|
|
||
| # 3. Wrap an existing core handle (advanced / internal). | ||
| emu = LLILEmulator(bv, handle=raw_handle) | ||
| ``` | ||
|
|
||
| A view can have as many independent emulators as you like; each keeps its own | ||
| registers, memory, and breakpoints. | ||
|
|
||
| ## Setting the entry point | ||
|
|
||
| `set_entry_point` accepts two forms: | ||
|
|
||
| ```python | ||
| # Address form: resolve an address to its function and start at its first LLIL | ||
| # instruction. Returns False if the address is not inside an analyzed function. | ||
| if not emu.set_entry_point(0x401000): | ||
| raise ValueError("address is not in an analyzed function") | ||
|
|
||
| # IL form: start at a specific LLIL instruction index of a given LLIL function. | ||
| emu.set_entry_point(func.llil, 5) | ||
| ``` | ||
|
|
||
| ## Running and stepping | ||
|
|
||
| ```python | ||
| emu.set_max_instructions(100000) # safety limit against runaway loops | ||
| reason = emu.run() # run until a stop condition | ||
|
|
||
| emu.step() # execute a single instruction | ||
| emu.step_n(10) # execute up to 10 instructions | ||
| emu.step_over() # step over a call (run through the callee) | ||
|
|
||
| emu.request_stop() # ask a running emulator to stop (thread-safe) | ||
| ``` | ||
|
|
||
| Progress and position: | ||
|
|
||
| ```python | ||
| emu.instructions_executed # count since the last reset | ||
| emu.current_address # address of the current instruction | ||
| emu.instruction_index # LLIL index within the current function (settable) | ||
| ``` | ||
|
|
||
| ## Stop reasons | ||
|
|
||
| `run`, `step`, `step_n`, and `step_over` all return an `ILEmulatorStopReason`, also | ||
| available afterward via `emu.stop_reason` with a human-readable `emu.stop_message`: | ||
|
|
||
| | Reason | Meaning | | ||
| | --- | --- | | ||
| | `ILEmulatorRunning` | Still running (not a terminal state) | | ||
| | `ILEmulatorBreakpoint` | Hit a breakpoint | | ||
| | `ILEmulatorInstructionLimit` | Reached `set_max_instructions` | | ||
| | `ILEmulatorHalt` | Returned from the top-level function / halted normally | | ||
| | `ILEmulatorError` | Internal error | | ||
| | `ILEmulatorCallHook` | Stopped by a call hook | | ||
| | `ILEmulatorSyscallHook` | Stopped by a syscall hook | | ||
| | `ILEmulatorUndefinedBehavior` | Executed undefined behavior | | ||
| | `ILEmulatorUnimplemented` | Hit an unimplemented LLIL instruction | | ||
| | `ILEmulatorUserRequestedStop` | Stopped via `request_stop` | | ||
|
|
||
| ```python | ||
| reason = emu.run() | ||
| if reason != ILEmulatorStopReason.ILEmulatorHalt: | ||
| print(f"stopped early: {reason.name} — {emu.stop_message}") | ||
| ``` | ||
|
|
||
| ## Registers, flags, and temporaries | ||
|
|
||
| Registers accept either a name (`'rax'`) or a numeric register ID: | ||
|
|
||
| ```python | ||
| emu.set_register('rsp', 0x7fff0000) | ||
| rax = emu.get_register('rax') | ||
|
|
||
| emu.regs # snapshot dict of every named register -> value | ||
|
|
||
| emu.set_flag('z', 1) # flag by name or ID | ||
| emu.get_flag('z') | ||
|
|
||
| emu.set_temp_register(0, 0x1234) # LLIL temporary registers, by index | ||
| emu.get_temp_register(0) | ||
| ``` | ||
|
|
||
| ## Memory | ||
|
|
||
| > **The emulator does not inherit memory from the BinaryView.** It starts with an | ||
| > empty address space of its own. Execution works because the emulator runs on the | ||
| > lifted LLIL, not by fetching bytes from its memory — but any data the code *reads* | ||
| > (globals, `.rodata`, strings, tables, the stack) is **not** present unless you put | ||
| > it there. Reading an address that holds data in the view returns zeroes in the | ||
| > emulator. If you want the view's bytes, copy them in explicitly. | ||
|
|
||
| Map regions before accessing them, then read and write raw bytes: | ||
|
|
||
| ```python | ||
| emu.map_memory(0x1000, b'\x00' * 0x1000) # map with data | ||
| emu.map_memory(0x2000, 0x1000) # map zero-filled | ||
| emu.map_memory(0x3000, 0x1000, "stack") # map a named region | ||
|
|
||
| emu.write_memory(0x1000, b'hello') # returns bytes written | ||
| emu.read_memory(0x1000, 5) # -> b'hello' | ||
|
|
||
| emu.get_mapped_regions() # [{'start':..., 'size':..., 'name':...}, ...] | ||
| ``` | ||
|
|
||
| ### Copying BinaryView memory into the emulator | ||
|
|
||
| To emulate code that reads existing program data, copy the relevant bytes from the | ||
| view into the emulator at the same addresses. Copy whole segments: | ||
|
|
||
| ```python | ||
| for seg in bv.segments: | ||
| data = bv.read(seg.start, seg.length) # bytes actually backed by the file | ||
| if data: | ||
| emu.map_memory(seg.start, data) | ||
| ``` | ||
|
|
||
| ...or just the region you need (cheaper for large binaries): | ||
|
|
||
| ```python | ||
| emu.map_memory(table_addr, bv.read(table_addr, table_size)) | ||
| ``` | ||
|
|
||
| Alternatively, serve reads on demand with a memory-read hook that pulls from the view | ||
| (see [Hooks](#hooks)): | ||
|
|
||
| ```python | ||
| emu.set_memory_read_hook( | ||
| lambda emu, addr, size: int.from_bytes(bv.read(addr, size), 'little') | ||
| if bv.read(addr, size) else None) | ||
| ``` | ||
|
|
||
| ## Function arguments | ||
|
|
||
| Arguments are placed using the function's default calling convention: | ||
|
|
||
| ```python | ||
| emu.set_argument(0, 0x1000) # a single argument by index | ||
| emu.set_arguments([0x1000, 16, 42]) # several at once | ||
| ``` | ||
|
|
||
| ## Breakpoints | ||
|
|
||
| ```python | ||
| emu.add_breakpoint(0x401234) # stops *before* executing that address | ||
| emu.remove_breakpoint(0x401234) | ||
| emu.clear_breakpoints() | ||
| ``` | ||
|
|
||
| A breakpoint stops the emulator before the target instruction executes, so on stop | ||
| `emu.current_address` equals the breakpoint address and its side effects have not yet | ||
| occurred. | ||
|
|
||
| ## Hooks | ||
|
|
||
| Hooks let embedding code intercept emulation. Pass a callable to install a hook and | ||
| `None` to remove it. Exceptions raised inside a hook are swallowed and treated as | ||
| "not handled". | ||
|
|
||
| ```python | ||
| # CALL: return True if handled (advance past the call), False to let the emulator | ||
| # try cross-function emulation. | ||
| emu.set_call_hook(lambda emu, target: True) # skip all calls | ||
|
|
||
| # SYSCALL: return True if handled, False to stop. | ||
| emu.set_syscall_hook(lambda emu: True) | ||
|
|
||
| # Memory read: return the value to use, or None to fall through to real memory. | ||
| emu.set_memory_read_hook(lambda emu, addr, size: 0 if addr in mmio else None) | ||
|
|
||
| # Memory write: return True if handled, False to let the write proceed. | ||
| emu.set_memory_write_hook(lambda emu, addr, size, value: False) | ||
|
|
||
| # Before each instruction: return True to continue, False to stop. | ||
| emu.set_pre_instruction_hook(lambda emu, index: True) | ||
|
|
||
| # INTRINSIC: return a list of (register_id, value) pairs if handled, else None. | ||
| emu.set_intrinsic_hook(lambda emu, intrinsic, params: None) | ||
|
|
||
| # stdout from emulated printf/puts/putchar; data is bytes. | ||
| emu.set_stdout_callback(lambda emu, data: sys.stdout.write(data.decode('latin1'))) | ||
|
|
||
| # stdin for emulated getchar/fgets/fread; return up to max_len bytes, b'' for EOF. | ||
| emu.set_stdin_callback(lambda emu, max_len: b'') | ||
| ``` | ||
|
|
||
| The memory-read hook fires on **every** load, including implicit reads such as the | ||
| stack pop performed by a `ret`, so filter by address when you only want to intercept | ||
| specific regions. | ||
|
|
||
| ## Built-in libc stubs | ||
|
|
||
| The emulator ships simple stubs for common libc functions so snippets that call | ||
| `printf`, `malloc`, etc. can run without a real libc: | ||
|
|
||
| ```python | ||
| emu.builtin_libc_stubs # bool, default True — enable the built-in stubs | ||
| emu.log_libc_calls # bool, default True — log stub calls to the console | ||
| emu.nop_unknown_externals # bool, default False — treat unknown external calls | ||
| # as no-ops returning 0 instead of stopping | ||
| ``` | ||
|
|
||
| ## Call stack | ||
|
|
||
| While stopped inside a called function: | ||
|
|
||
| ```python | ||
| emu.call_stack_depth # number of nested calls | ||
| emu.get_call_stack() # [{'function_address':..., 'return_address':...}, ...] | ||
| ``` | ||
|
|
||
| Frame 0 is the current function; later frames are its callers. | ||
|
|
||
| ## State serialization | ||
|
|
||
| Emulator state (registers, flags, memory, call stack) can be snapshotted to JSON and | ||
| restored — useful for save/restore points or reproducing a state across runs: | ||
|
|
||
| ```python | ||
| snapshot = emu.save_state() # -> JSON string | ||
| emu.load_state(snapshot) # restore, returns True on success | ||
|
|
||
| emu.save_state_to_file("state.json") | ||
| emu.load_state_from_file("state.json") | ||
|
|
||
| emu.reset() # clear all state back to initial | ||
| ``` | ||
|
|
||
| ## Full example: cross-function emulation | ||
|
|
||
| Decrypt a string by emulating a decryption routine, letting the emulator run through | ||
| the called functions and skipping anything it can't resolve: | ||
|
|
||
| ```python | ||
| from binaryninja.emulator import LLILEmulator, ILEmulatorStopReason | ||
|
|
||
| emu = LLILEmulator(bv) | ||
| emu.nop_unknown_externals = True # don't stop on unresolved externals | ||
| emu.set_max_instructions(1_000_000) | ||
|
|
||
| # Give the routine a scratch output buffer and the encrypted input. | ||
| emu.map_memory(0x100000, 0x1000, "out") | ||
| emu.map_memory(0x101000, encrypted, "in") | ||
|
|
||
| emu.set_entry_point(decrypt_func.start) | ||
| emu.set_arguments([0x100000, 0x101000, len(encrypted)]) | ||
|
|
||
| reason = emu.run() | ||
| if reason == ILEmulatorStopReason.ILEmulatorHalt: | ||
| print(emu.read_memory(0x100000, 0x100).split(b'\x00', 1)[0]) | ||
| else: | ||
| print(f"stopped: {reason.name} — {emu.stop_message}") | ||
| ``` | ||
|
|
||
| ## API reference | ||
|
|
||
| Everything is on the `LLILEmulator` class. | ||
|
|
||
| **Construction:** `LLILEmulator(view, il=None, handle=None)` | ||
|
|
||
| **Execution:** `run`, `step`, `step_n`, `step_over`, `request_stop`, | ||
| `set_max_instructions`, `reset` | ||
|
|
||
| **Entry / arguments:** `set_entry_point`, `set_argument`, `set_arguments` | ||
|
|
||
| **State (properties):** `instruction_index`, `current_address`, `stop_reason`, | ||
| `stop_message`, `instructions_executed`, `call_stack_depth`, `regs` | ||
|
|
||
| **Registers / flags:** `get_register`, `set_register`, `get_temp_register`, | ||
| `set_temp_register`, `get_flag`, `set_flag` | ||
|
|
||
| **Memory:** `map_memory`, `read_memory`, `write_memory`, `get_mapped_regions` | ||
|
|
||
| **Breakpoints:** `add_breakpoint`, `remove_breakpoint`, `clear_breakpoints` | ||
|
|
||
| **Hooks:** `set_call_hook`, `set_syscall_hook`, `set_memory_read_hook`, | ||
| `set_memory_write_hook`, `set_pre_instruction_hook`, `set_intrinsic_hook`, | ||
| `set_stdout_callback`, `set_stdin_callback` | ||
|
|
||
| **libc stubs (properties):** `builtin_libc_stubs`, `log_libc_calls`, | ||
| `nop_unknown_externals` | ||
|
|
||
| **Call stack:** `get_call_stack` | ||
|
|
||
| **Serialization:** `save_state`, `load_state`, `save_state_to_file`, | ||
| `load_state_from_file` | ||
|
|
||
| For runnable, self-contained examples of every feature above, see the | ||
| [test suite](https://github.com/Vector35/binaryninja-api/blob/dev/plugins/emulator/test/emulator_test.py). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused why this is necessary in binaryninjaapi.h when this header does not provide the emulator's API. Why isn't this in emulatorapi.h instead?