Indirect jumps or calls (vtables, function pointers, switch tables) are a pain when reverse-engineering a binary statically.
IndyResolve is a quick tool I put together to bridge the gap between dynamic and static analysis. It uses a DynamoRIO client to trace indirect calls/jumps at runtime, and a Binary Ninja plugin to ingest that data. It patches the resolved targets straight into the Medium Level IL (MLIL), so your decompilation actually makes sense and cross-references work.
The project is split into two parts:
- The DynamoRIO Client (
indyresolve.c): You run your target binary under this. It hooks indirect branches, figures out where they're going, and dumps the mappings to a JSON file when the thread exits. It's smart enough to separate intra-module calls from external library calls. - The Binary Ninja Plugin (
indyresolve.py): You run this inside Binja. It reads the JSON, finds the unresolved MLIL instructions, and forces their destination variables to constant values (or sets of values, if the branch went to multiple places during runtime). It also adds comments and wires up indirect branches.
First, install DynamoRIO and set the DYNAMORIO_HOME environment variable to the DynamoRIO path.
export DYNAMORIO_HOME=/path/to/dynamorioThen, compile the project:
mkdir build
cd build
cmake ..
make
This builds libindyresolve.so (the DR client) and a demo executable you can test it on.
You'll also need to copy the indyresolve.py file into your Binary Ninja plugin folder ($HOME/.binaryninja/plugins/ by default on Linux)
Run your target executable under DynamoRIO using the compiled client:
$DYNAMORIO_HOME/bin64/drrun -c /path/to/libindyresolve.so -- ./your_targetWhen the program finishes, you should see new JSON files in your current directory (e.g., /full/path/to/your_target.json). The client also creates JSON files for each loaded libraries, in each library folder, if it has write access to them.
You can run your program more than one time (i.e. to provide different user inputs), the JSON results will be merged (not overwritten). To restart from scratch and "forget" previous runs, simply delete the JSON files manually.
- Open your target binary in Binja.
- Go to Plugin -> Import indirect call database
- Select the .json file generated by the DynamoRIO client.
The plugin will run through the binary and update the analysis. For each indirect call or jump, it will sets user-defined data-flows, comments, and user branches to wire up the jump/call site to their destination.
If the code jumps into an external library, right-click the instruction and select "Follow indirect external call/jump". If you have that library open in another Binja tab, it'll instantly switch tabs and jump straight to the target function.
There's a simple demo.c included in the repo. If you build the project, it'll compile demo. Run it under drrun, load it in Binja, and import the JSON to see how the indirect f() call gets resolved perfectly.
Nginx is notorious for having a lot of indirect transfer of control, including calls to external libraries.
Here is a full demonstration of tracing nginx and resolving a chain of indirect transfer of controls, landing in the ngx_http_image_filter_module.so module.
indyresolve.mp4
- Add support for other architectures or systems (only tested on x86_64 / Linux, for now)
- Add support for Ghidra, etc.
- Improve performances of the DR client by removing clean-calls
- Try other instrumentation framework (QBDI, etc)
