Using Function Evaluation in WinDBG

People who develop debuggers would know in theory you cannot have a perfect disassembler (especially for x86) and stepper (especially for Step Over). People who develop commercial debuggers would know Function Evaluation (a.k.a. funceval) is a big challenge while implementing an Expression Evaluator. And people who develop the Visual Studio Debugger would face other difficulties - Interop Debugging, Edit & Continue.

In this article, I'm not going to explain the bloody details of funceval, I just demonstrate how to use funceval in WinDBG and how powerful it is.

Previously we mentioned the .call command in Microsoft Binary Technologies, at that time we were not able to invoke the function since we don't have private symbols - funceval requires private symbol since debugger needs to understand the calling convention, which is stripped out in public symbol.

While I cannot use private symbols writting articles for this blog (private symbol is Microsoft privacy, also debugging without private symbol is much more fun), the way I'd take is to create a proxy DLL:


 #include <Windows.h>

VOID WINAPI SetLastError(DWORD dwErrCode){}

Now compile the code into a DLL, with PDB file generated:

cl.exe funceval.cpp /D UNICODE /Fd /GS- /LD /Od /Zi /link /NOENTRY /NODEFAULTLIB /RELEASE /SUBSYSTEM:CONSOLE

In order to use the proxy DLL, we will use the following approach:

  1. Launch a debug session.
  2. Allocate memory from debugee process.
  3. Inject the proxy DLL into the allocated memory (note that we skipped PE relocation).
  4. Load private symbol of the proxy DLL.
  5. Use WinDBG .call command to kick off funceval from proxy DLL.
  6. Change the IP register to the real address we want to execute.
  7. Start evaluating.

Here is the automation script, enjoy!

 $$ cdb.exe -xe cpr -c "$$>a< .\funceval.txt" notepad.exe

.echo [Launch Script]

$$ change the following value to the size of funceval.dll
r $t1 = 0n1536

bp @$exentry; g

.echo [Allocate Memory]
.foreach ( token { .dvalloc @$t1 } ) {
 aS alias token
  .block {
        .if ($spat("${alias}", "[0-9a-f]+")) {
          r $t2 = 0x${alias}
      }
   }
   ad /q alias
}

.printf "[Load Helper DLL(base address = %p, size = %p)]\n", @$t2, @$t1
.readmem funceval.dll @$t2 (@$t1+@$t2-1)

.block {
 .sympath .
}

$$.symopt+ 0x40

.reload /s /f funceval.dll=$t2

$$.symopt- 0x40

.echo [Function Evaluation]
.call /s funceval!SetLastError kernel32!SetLastError(7777)

g

!gle

.dvfree @$t2 0

And here is the output from my machine:

 [Allocate Memory]
[Load Helper DLL(base address = 00020000, size = 00000600)]
Reading 600 bytes.
[Function Evaluation]
Thread is set up for call, 'g' will execute.
WARNING: This can have serious side-effects,
including deadlocks and corruption of the debuggee.
LastErrorValue: (Win32) 0x1e61 (7777) - <Unable to get error code text>
LastStatusValue: (NTSTATUS) 0 - STATUS_WAIT_0
Freed 0 bytes starting at 00020000