Skip to main content
Next-Level Reversing: Binary Ninja+TTD
  1. Posts/

Next-Level Reversing: Binary Ninja+TTD

·
seeinglogic
Author
seeinglogic
Dev/hacker exploring software and security through visualizations

I’m not sure why, but one of the most slept-on techniques I know of is “reversible debugging”, or time-travel debugging (TTD).

The idea has been around for quite a while, but it seems the best free way to experiment with it is using the venerable windbg.

I don’t have as much use for Windows-only tooling these days, but when Binary Ninja announced some improvements to their TTD integration, I thought it was about time to give it a spin!

This integration is somewhat new, so it seemed like it’d be worth going over the basic setup process and getting started with Binary Ninja TTD… and then we’ll talk about some of my favorite tricks to supercharge its effectiveness!

Setting Up Binary Ninja for TTD
#

Consulting the docs on the topic is always a good idea, especially as my experience is just a snapshot in time, using version the latest stable at the time of writing (4.1.5902).

If you already have things set up, please feel free to skip to the part of this post where we start using it.

The overall setup process looks like this:

  1. Install windbg through Binary Ninja
  2. Record our trace in Windbg proper (not through Binary Ninja)
  3. Open the target binary in Binary Ninja
  4. Open our trace in Binary Ninja using the Debugger dropdown menu:
    1. Target the trace with “Debug Adapter Settings”
    2. Start analyzing the trace with “Launch”
  5. Navigate our TTD trace by sending debugger commands via the Console > Debugger tab

Installing Windbg
#

You may already have windbg installed through the MS store, but in order for Binary Ninja to use it, we need to click the Install Windbg/TTD option in the Debugging menu.

Install Windbg button in Binary Ninja

If you don’t see that in the dropdown… are you on Windows? You kinda need to be on Windows for all of this.

Otherwise, watch the Python console and Log windows for errors during the install process, it worked without issue for me.

Recording a TTD Trace
#

If you’re just running a normal program and recording the full trace, you can use Binary Ninja’s Debugger > Record TTD Trace option and fill out the dialog like the below:

Record a TTD trace in Binary Ninja

It might not be obvious at first, but the trace files will be saved in the “Working Directory”.

For this post, I’m using the Windows’s ping.exe as my target, because I was curious how it worked under the hood. It also shows how to specify command line arguments nicely, because we need to specify a target host or domain to ping.

If you need other workflows, like attaching to a running process, then you need windbg, but luckily it’s pretty easy to use and there are good docs on how to record a trace.

You can install windbg via the Microsoft store, or use find the DbgX.Shell executable that Binary Ninja installed, likely somewhere like C:\Users\user\AppData\Roaming\Binary Ninja\windbg\DbgX.Shell.

Just go to: File > Start Debugging > Launch executable (advanced).

Record a trace in windbg

Just note where the trace is going to be saved, and run the target as normal while recording (in my case, ping exits on its own).

Opening the Target and Trace
#

Yup, we need to open the target binary in Binary Ninja (still on Windows, mind you).

Then go to Debugger > Debug Adapter Settings and select DBGENG_TTD from the dropdown, and then select a new file for the Executable Path, choosing the .run file from the TTD trace you just made (it’s the one with the windbg icon).

Pick the run file

The settings should look something like this:

TTD Debug Adapter seetings

Then go to the Debugger menu, pick Launch, and we should see the debugger stop at the entry point of our target.

Entry point of ping.exe

Now we’re ready to take this shiny new feature for a ride!

Using TTD in Binary Ninja
#

The basic idea is that we can run many of the same commands that we could in windbg via Binary Ninja’s Debugger tab in the Console pane.

For TTD, this means that in addition to g, p, t (for Go, steP over, Trace into), we can now do the reverse with g-, p-, and t-. Or we can use the fancy new buttons!

Binary Ninja’s new TTD buttons

If you aren’t familiar with windbg, when certain commands run it might seem like the debugger hangs, but it’s actually working in the background. Windbg has a helpful *BUSY* indicator, but unfortunately Binary Ninja also inherits some of this behavior, so just be patient if certain commands take a while.

windbg says it’s busy

Not everything is completely the same, for example in Windbg we get tab completion on known symbols, and certain commands won’t work in the debugger console (like .hh for looking up help on different commands).

But this way we get a fully capable reversing environment with Python scripting, various ILs, and sweet graph views.

It’ll be really interesting to see what will come out of the nexus of Binary Ninja and TTD in the future… it all seems very promising!

Binary Ninja TTD’s Killer App: Coverage Highlighting
#

Using a tool like bncov or lighthouse, we can use coverage to highlight basic blocks, which together with TTD will dramatically speed up our ability to understand traces.

If you have any problems with bncov, please open an issue and I’ll do my best to help you out!

This a two-step process:

  1. Extract coverage from the TTD trace
  2. Overlay coverage with bncov as covered in my previous post
Do this BEFORE starting the TTD debugging, otherwise you’ll probably run into issues due to rebasing.

Extracting coverage via windbg script
#

First download 0vercl0k’s codecov.js script: https://github.com/0vercl0k/windbg-scripts/blob/master/codecov/codecov.js

Just click the “Download raw file” button and save it where you like.

NOTE: running the script in Binary Ninja’s Debugger pane should work, but it didn’t for me… so I’m showing how to run it in windbg here, but YMMV.

Next, open up the TTD trace in windbg (should be in your File > Recent list, or do “Open a Trace”), and run the script like below, inserting your own script path and target module name:

.scriptload "C:\Users\user\Downloads\codecov.js"
!codecov "PING"

If you haven’t set up windbg before, you might be looking at a big empty window, but that’s all you need to type into the command box.

Coverage script ran successfully

Make sure there aren’t any errors in the windbg output window. On success the script should drop a file with coverage in the same directory as the trace.

Importing Coverage in Binary Ninja
#

To install bncov via the plugin manager, press CTRL+SHIFT+M on Windows and then type bncov in the search window. Right-click on the bncov entry and pick “Install”, accept the msgpack dependency, then wait for it to install (watch the Log window).

If bncov is installed correctly, we should be able to open the Plugins dropdown at the top of the window and see an entry for bncov.

If you don’t see it, go back to the bncov entry in the plugin manager, and make sure it’s enabled (right-click the entry and click enable), restarting Binary Ninja as needed.

bncov import file

Once bncov is installed, set your view to disassembly of the entry point or main function, then right-click and do “Plugins > bncov > Coverage Data > Import File” and find the coverage file that the codecov.js script generated.

Block highlighting for wmain

Boom, now we can see everything that got executed… which really enhances our TTD experience.

If you don’t see anything highlighted, check that 1) you haven’t already started the debugger and rebased the target binary, 2) you’re in a function that actually got executed, and 3) that you’re not in “High-Level IL” view, which is the only view that doesn’t automatically highlight (we’ll get to that).

Capitalizing on Coverage
#

Highlighting the blocks that actually ran is really helpful in and of itself, but there’s two other quick tricks I wanted to share.

One: leverage the Python console to help your exploration.

For example, the wmain function of ping.exe is massive, so if we want to see what non-library functions it calls, we can type up a quick snippet:

for f in set(current_function.callees):
    print(f.name, hex(f.start))

Then we can just click on the addresses to bounce between them, and we can see what got called by which ones start with a red block!

Scripting for great justice

Second trick: if you want high-level IL highlighting, drop the snippet below into the Python console. It’s not perfect, but it’s still really helpful:

# If you installed bncov from plugin manager:
import ForAllSecure_bncov as bncov
# If you installed from GitHub, instead do:
# import bncov
# Snippet to map coverage from disasm to HLIL
def transfer_highlights_to_instructions(covdb: bncov.CoverageDB):
    bv = covdb.bv
    for block_address in covdb.total_coverage:
        for block in bv.get_basic_blocks_starting_at(block_address):
            cur_highlight = block.highlight
            block.set_user_highlight(HighlightStandardColor.NoHighlightColor)
            cur_function = block.function
            cur_addr = block.start
            for cur_token, cur_inst_size in block:
                cur_function.set_user_instr_highlight(cur_addr, cur_highlight)
                cur_addr += cur_inst_size

# Get the coverage db for file currently open
covdb = bncov.get_covdb(bv)
# Apply highlights
transfer_highlights_to_instructions(covdb)

I was able to toss that entire blob into the Python console and get HLIL highlighting in both linear and graph view (just make sure to do this before launching the debugger).

HLIL highlighting in the graph view

Highlighted decompilation plus a debugger that can go forward and back? That makes reversing a TON easier.

Example: Deconstructing Ping
#

So how can we test out our new reversing superpowers?

Well I’m still curious how ping.exe works. My initial guesses were either A) there’s some esoteric Windows API function that does all the work, or B) they have some fancy internal logic (which I considered less likely).

So let’s start with the wmain function, though if you’ve never seen a wmainCRTStartup-style entrypoint for a Windows executable, it’s much easier to navigate with public symbols and block highlighting ;)

HLIL linear highlighting

wmain is a really long function (like almost 1,000 lines of linear HLIL), and for a long function with that much indentation, I find looking at the graph view easier… Especially because we can ignore the majority of the blocks since they’re uncovered.

Trawling through the graph looking at covered blocks, we eventually come across two functions that sound interesting: IcmpCreateFile and IcmpSendEcho2Ex.

ICMP function found

Just as we expected, oddball API functions do all the work.

Good ol’ IcmpSendEcho2Ex, am I right?

Let’s Go Faster
#

I know. I hear you.

Reading is hard, and we should do as little of it as possible.

Here’s faster ways to reach the same conclusion:

First: use the Binary Ninja’s Triage view, especially for Windows binaries. The Imports box doesn’t have too many entries for ping.exe, and the ICMP functions are right at the top. Plus, we can use the cross-references to jump to where they’re called.

Binary Ninja’s triage view

Second: once we have coverage imported, use bncov’s Coverage Report via Plugins > bncov > Reports > Show Coverage Report. There’s only like 20 functions with coverage, and most of them don’t sound network- or ICMP-related.

So after doing very little work, the “lots of custom logic” theory appears quite unlikely.

bncov’s Coverage Report

Third: use bncov to elevate your script-fu. There are 135 locations where wmain calls other functions.

But if we narrow down the list to just what’s in basic blocks that are covered, it drops to a manageable 39, and we can spot our interesting API calls pretty quickly:

covdb = bncov.get_covdb(bv)
for site in current_function.call_sites:
    blocks = bv.get_basic_blocks_at(site.address)
    if any(bb.start in covdb.total_coverage for bb in blocks):
        print(hex(site.address), site.hlil)
# Yields results like:
# 0x140001be6 SetThreadUILanguage(0)
# 0x140001bfc HeapSetInformation(nullptr, HeapEnableTerminationOnCorruption, nullptr, 0)
# 0x140001def GetDefaultTTL()
# 0x140001eb8 GetIpForwardTable(nullptr, &var_a70, 0)
# 0x140001f9e rax_26, address_1 = ResolveTarget(r14, Terminator_2, &address, 0xfffd, &var_858, var_af0)
# 0x14000265a IcmpHandle_1 = IcmpCreateFile()
# 0x140002931 uint32_t i_2 = IcmpSendEcho2Ex(IcmpHandle, nullptr, nullptr, nullptr, var_af8_2.d, data_1400076e4, var_ae8, var_a84.w, &RequestOptions, ReplyBuffer, rbx_5.d, Timeout)

For more tricks like this, check out the this post on doing more with coverage data from bncov.

Going Forward (and Back)
#

Personally I feel like this is a huge step for reversing because it helps us quickly focus on the code that got executed and debug to our heart’s content. We can see exactly where a function got called statically in HLIL, and we can time-travel to the beginning and end of every function call to help understand inputs and outputs.

For now, the Binary Ninja TTD experience is still maturing… whereas windbg is ancient and powerful. So I think of this as another tool in the arsenal rather than a complete replacement.

But if I have a Windows target that I don’t have source for and want to understand it quickly, TTD plus coverage highlighting is a pretty slick setup.

I hope this helped you, and if it did, follow my Twitter/Mastodon to keep up with cool stuff like this!