Using WinDbg to Analyse Kensington Verimark Fingerprint Scanner

Intro

Time for part 3! Previously we started reverse engineering the driver using Ghidra. While this works, it sometimes runs into problems when it's hard to understand how functions are called. To a large extent this is driven by the application being written in C++. The virtual function tables make it hard to figure out what function ends up at which offset. It's also not made easier by having to understand exactly how and when Windows calls functions in this driver.

In this post, I'll show how to leverage a debugger to try and fill in some of the blanks. In my reversing journey so far, this is has been helpful, but still quite tedious. I only resort to it when I am stuck and want to understand a particular piece of code better.

WinDbg - Setup

There are a few Windows debuggers out there and initially I tried to use x64dbg, however, I realised quickly that it doesn't do so well with debugging Windows drivers. Instead, it seems WinDbg is the recommended solution for that.

Given that I'm reversing on Linux, I first needed to spin up a VM, pass through the USB device and get everything working. I made sure the driver version matched (so I'm analysing the exact same binary), then set up Windows Hello with my fingerprint, did a reboot and login. Throughout this process I captured the USB traffic, just to confirm it looks similar to what I covered in the first post. Once I had confirmed all of this, I began debugging.

The first problem we face is that we can't exactly start the process from the debugger, we need to attach to it. This isn't so hard as we can just attach to the only WUDFHost.exe process we find (make sure to select "Show processes from all users").

Attach to  WUDFHost.exe

This works well enough, but brings up the next problem: When the USB device is connected it performs an initial handshake. Presumably, we need to understand this handshake so we actually want to attach from the start which is a bit difficult. Luckily, driver developers face a similar problem, so Microsoft offers a solution: How to Enable Debugging of a UMDF Driver.

It's a bit tricky to get right, and there are several ways to get to the same destination: Setting a registry key that tells Windows to just wait a bit before loading the driver. In my case, I had to set Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WUDF\Services\{193a1820-d9ac-4997-8c55-be817523f6aa}\HostProcessDbgBreakOnDriverLoad to a number of seconds long enough for me to attach a debugger. It's currently set to 0x60, for no particular reason. It's long enough.

Once you've set this value, reconnect the USB device (not sure if you have to reboot) and you can attach using WinDbg and catch the initialisation process.

WinDbg - Debugging

In my case, I disable USB passthrough to the VM, then re-enable it and attach the debugger. Take the following steps to make sure everything works as expected:

  1. With the USB device connected, open WinDbg
  2. Go to File > Attach to process
  3. Tick "Show processes from all users"
  4. Search for "wudf". Confirm there is a single WUDFHost.exe.
  5. Disconnect the device, e.g. by unplugging or disabling passthrough
  6. Hit the refresh symbol in WinDbg to confirm that the list is now empty.
  7. Reconnect the USB device. This starts the "timer" that waits for the debugger to attach.
  8. Refresh again, there should again be a single WUDFHost.exe.
  9. Select it and "Attach"

You should now find yourself attached, at a breakpoint, with the program paused.

WinDbg paused on attaching to driver

At this point, the driver has not been loaded and we can trace through its initialisation steps. The most challenging part, in my opinion, was aligning what I saw in Ghidra with what's in WinDbg. While Ghidra has a nice decompiled view and a lot of helpful context, WinDbg is much more bare bones, showing just assembly, register values etc, like a standard debugger. In addition, the addresses aren't identical, because Windows picks different base addresses to load the library into. I assume this is ASLR or something, but the important thing is that we need to find a way to navigate.

To achieve this, we use relative offsets, because those remain the same between Ghidra and WinDbg. Let's see what that means on an example: Set a breakpoint on the DllGetClassObject function that initialises the driver.

  • Open up Ghidra and navigate to the function. In the "Listing" window in the centre, select the function entry point.
  • Open up WinDbg, open the "Modules" view (select View > Modules). Find the driver in the list. In our case it's synawudfbiousb132.dll. Right click and "Copy Name". This will copy the entire path.
  • Go to View > Disassembly (if not already open) and at the top under "Address" paste the path. Delete everything except the name, so that it just shows synawudfbiousb132.dll.
  • Back in Ghidra, right click on the function and select "Copy Special" and then "Imagebase Offset". This copies the relative offset from the start of the file into the clipboard.
  • In WinDbg, in the "Address" field enter + 0x and then "paste". The full address should now look something like this: synawudfbiousb132.dll + 0xdb50.
  • Deselect the "Follow current instruction" if it's selected. Otherwise WinDbg will replace what we've put in here which gets annoying when we want to navigate to another offset.
  • Press Enter and it should navigate you to an address. Look through the first few rows of disassembled instructions. They should match between Ghidra & WinDbg. It means you've found the right spot.
  • Right click on the first instruction and set a break point (F9).
  • Select Home > Go. You may have to continue once more. Then you'll find yourself at the breakpoint.

WinDbg - Back to Ghidra

Back in Ghidra let's look at the disassembly for our code again:

/* WARNING: Function: _guard_dispatch_icall replaced with injection: guard_dispatch_icall */

HRESULT __stdcall DllGetClassObject(IID *rclsid,IID *riid,LPVOID *ppv)

{
  longlong lVar1;
  HRESULT HVar2;
  longlong *plVar3;

                    /* 0xdb50  1  DllGetClassObject */
  *ppv = NULL;
  lVar1._0_4_ = rclsid->Data1;
  lVar1._4_2_ = rclsid->Data2;
  lVar1._6_2_ = rclsid->Data3;
  if ((lVar1 == 0x4b29b08096710705) && (*(longlong *)rclsid->Data4 == 0x3a66ae3569b1eca3)) {
    plVar3 = (longlong *)operator_new(0x10);
    if (plVar3 == NULL) {
      plVar3 = NULL;
    }
    else {
      *(undefined4 *)((longlong)plVar3 + 0xc) = 0;
      *plVar3 = (longlong)&PTR_FUN_1800b8828;
      *(undefined4 *)(plVar3 + 1) = 1;
    }
    if (plVar3 == NULL) {
      HVar2 = -0x7ff8fff2;
    }
    else {
      HVar2 = (**(code **)*plVar3)(plVar3,riid,ppv);
      (**(code **)(*plVar3 + 0x10))(plVar3);
    }
  }
  else {
    if (((undefined **)PTR_LOOP_180131028 != &PTR_LOOP_180131028) &&
       (((PTR_LOOP_180131028[0x1c] & 1) != 0 && (1 < (byte)PTR_LOOP_180131028[0x19])))) {
      FUN_18000dcc0(*(undefined8 *)(PTR_LOOP_180131028 + 0x10),10,&DAT_1800b8808,rclsid);
    }
    HVar2 = -0x7ffbfeef;
  }
  return HVar2;
}

There's a line (**(code **)(*plVar3 + 0x10))(plVar3); that's quite hard to decipher. In this example it's actually okay, because we can figure from the rest of the code that this is offset 0x10 from PTR_FUN_1800b8828, but in other places we won't have that. Then' well want to know what the actual address is that this function is located at.

We can leverage WinDbg for that: We set a breakpoint at the instruction that calls this function and we'll see what address it is going to call, which we can then bring back to Ghidra, so let's do that. In Ghidra, we navigate to the instruction:

18000dbdd ff 15 95        CALL       qword ptr [->_guard_dispatch_icall]              int FUN_18000db20(LPVOID param_1)
          7a 0a 00                                                                    undefined _guard_dispatch_icall(

Then, using the previous method we navigate to this address in WinDbg. You can also "spot" the offset in the text above: It's 0xdbdd Set a breakpoint in WinDbg and run until it hits the breakpoint. In WinDbg's Disassembly view we see this now:

00007ffc`713bdbdd ff15957a0a00   call    qword ptr [7FFC71465678h]

This is a function call to a specific address. We can turn this back into a Ghidra address by similarly taking this address' offset. The easiest way I've found so far is to write down the "Base Address" for the driver DLL from the "Modules" window in WinDbg (in my example 0x7FFC713B0000) and then grab a calculator that can handle hex (e.g. KCalc) and calculate the offset: 0x7FFC71465678 - 0x7FFC713B0000 = 0xB5678. In Ghidra, select Navigation > Go To and enter 0x180000000 + 0xB5678 (the base address Ghidra uses plus the offset we want to go to). We find ourselves here:

       1800b5678 50 15 0b        addr       _guard_dispatch_icall
                 80 01 00 
                 00 00

What happened here? Well, the function call actually isn't direct, but Ghidra abstracted that away for us. However, if we go to that function, we find that there's only one instruction here:

       1800b1550 ff e0           JMP        RAX

Effectively, it calls whatever is in RAX. So let's look at what value is currently in RAX in WinDbg. It's this ability to look up dynamic values that we're interested in as Ghidra doesn't have them during static analysis. In WinDbg's "Registers" window, RAX currently has 0x00007FFC713BDB20 so let's do the same thing again: 0x7FFC713BDB20 - 0x7FFC713B0000 = 0xDB20. And let's go there in Ghidra: 0x180000000 + 0xDB20. This puts us exactly at the entry point of a function that Ghidra shows as this:

int FUN_18000db20(LPVOID param_1)

{
  int *piVar1;
  int iVar2;

  LOCK();
  piVar1 = (int *)((longlong)param_1 + 8);
  iVar2 = *piVar1;
  *piVar1 = *piVar1 + -1;
  UNLOCK();
  if (iVar2 + -1 == 0) {
    thunk_FUN_18008f8d0(param_1);
  }
  return iVar2 + -1;
}

If you ever don't land precisely where you expected, double check your values. If it doesn't "feel" right and doesn't seem to make sense, you might have made a mistake. Or there's something new to learn.

In this instance, we can actually confirm we found the right function: The original code had a direct reference to the vtable so if we go there and then navigate to offset 0x10 we should find a pointer to this function. Let's go to PTR_FUN_1800b8828:

                     PTR_FUN_1800b8828                               XREF[4]:     DllGetClassObject:18000db9b(*), 
                                                                                  DllGetClassObject:18000dba2(*), 
                                                                                  DllGetClassObject:18000dbc8(R), 
                                                                                  DllGetClassObject:18000dbd1(*)  
1800b8828 e0 da 00        addr       AutoClass1::FUN_18000dae0
         80 01 00 
         00 00
1800b8830 d0 da 00        addr       FUN_18000dad0
         80 01 00 
         00 00
                     PTR_FUN_1800b8838                               XREF[1]:     DllGetClassObject:18000dbd9(R)  
1800b8838 20 db 00        addr       FUN_18000db20
         80 01 00 
         00 00

At offset 0x10 from PTR_FUN_1800b8828 we find FUN_18000db20 which is exactly what we found above, too.

This means, as we continue our analysis, if we come across a function that calls into the vtable we can now try dynamic analysis with WinDbg to figure out what that vtable looks like.