Overview
In this post, we will review some of the basics of expression evaluation in WinDBG. We will look at the MASM and C++ expression evaluators, their capabilities, their limitations, and their use cases. This is required pre-course reading for the Windows Security Developer Bootcamp.
To try these commands on our own, you can simply launch notepad.exe from WinDBG on a Windows 10 or Windows 11 64-bit system. Prior to launching WinDBG, please ensure that your symbol path is properly set up by running the following command in an elevated (administrative) instance of CMD.exe.
setx _NT_SYMBOL_PATH SRV*C:\symsrv*https://msdl.microsoft.com/download/symbols
Alternatively, you can generate a memory dump of any process from Task Manager and then load that memory dump in WinDBG.
WinDBG Expression Evaluators
WinDBG has two different commands for evaluating expressions - ? and ??. The ? command invokes WinDBG 's current expression evaluator, which by default is the MASM evaluator. The MASM evaluator can evaluate the most common expressions including those that reference global symbols. The ?? command, on the other hand, always invokes the C++ evaluator.
The current expression evaluator can be changed during a debug session using the .expr command. Executing the .expr command by itself displays the current expression elevator.
0:000> .expr Current expression evaluator: MASM - Microsoft Assembler expressions
To change the current expression evaluator to the C++ evaluator use:
0:000> .expr /s c++ Current expression evaluator: C++ - C++ source expressions
Switching the current expression evaluator results in the ? command invoking the C++ evaluator. The ?? command, however, continues to invoke the C++ evaluator. To switch back the current evaluator to the MASM evaluator use:
0:000> .expr /s masm Current expression evaluator: MASM - Microsoft Assembler expressions
MASM Evaluator
In MASM expressions, all references to symbols are translated to their corresponding addresses. The MASM evaluator translates the name of any DLL loaded in a process or any driver in the system to the base address of the module in memory. This provides a convenient way of accessing the base address of any module in MASM expressions as shown below:
0:000> ? ntdll Evaluate expression: 140707260792832 = 00007ff8`f64c0000
Similarly, the MASM evaluator can be used to look up the addresses of symbols by symbol name, as shown below:
0:000> ? ntdll!RtlpDosDevicesUncPrefix Evaluate expression: 140707261939840 = 00007ff8`f65d8080
The grave (`) character can be omitted when specifying 64-bit addresses in expressions.
By default, the MASM evaluator treats all numbers based on the current radix setting which is set to 16 (hexadecimal). We can query or modify the current radix using the n command. The following command queries the current radix.
0:000> n base is 16
To change the radix to a different number, use n Radix where Radix can be 8, 10, 16.
0:000> n 10 base is 10
MASM expressions can access all the CPU registers and can contain any of the following operators from high-level programming languages that we are all familiar with. These include operators such as arithmetic (+ - * / %), shift (<< >>), bitwise ( & | ^), comparison (== < > <= >= !=), etc. Some special operators such as arithmetic right shift (>>>) and mod(%) which returns the remainder of a division, are supported. Automatic pseudo-registers (discussed later), user-defined pseudo-registers ( @$t0 - @$t19) and aliases can be used in MASM expressions. Accesses to any registers in an expression must always include a @ before the register name to avoid ambiguity about whether the name that follows is a register or a local or global variable name.
As an example, to check if the zero flag (bit 6) is set in the CPU EFLAGS register, we can use the following expression:
0:000> ? (@efl & ( 1 << 6)) != 0 Evaluate expression: 1 = 00000000`00000001
The MASM evaluator also supports operators that are useful when performing string comparison in breakpoint commands during live debugging such as $scmp(), $sicmp(), and $spat().
Public Symbols
Public symbols i.e. symbols of Windows OS binaries do not contain type information for global variables. When using global variables in an expression, the MASM evaluator is necessary. Consider the global variable PebLdr in NTDLL.dll which is of type PEB_LDR_DATA. This variable is not accessible using the C++ evaluator parser hence the following statement gives an error.
0:000> ?? ntdll!PebLdr Type information missing error for PebLdr
However, if the MASM evaluator is able to evaluate the above expression to an address.
0:000> ? ntdll!PebLdr Evaluate expression: 140707262256064 = 00007ff8`f66253c0
C++ Evaluator
While the MASM evaluator is well suited for reasonably complex expressions, it falls short when accessing fields of C or C++ data structures. This is where the C++ evaluator comes to the rescue. The C++ evaluator always interprets any numbers as decimal numbers. Hex numbers can be specified using the 0x prefix.
To display the Length field of the _PEB_LDR_DATA structure at the address 0x00007ff8f66253c0 observed above, we can use the following command :
0:000> ?? ((ntdll!_PEB_LDR_DATA*)0x00007ff8f66253c0)
struct _PEB_LDR_DATA * 0x00007ff8`f66253c0
+0x000 Length : 0x58
+0x004 Initialized : 0x1 ''
+0x008 SsHandle : (null)
+0x010 InLoadOrderModuleList : _LIST_ENTRY [ 0x000002d1`e2ea2bf0 - 0x000002d1`e2ef2690 ]
+0x020 InMemoryOrderModuleList : _LIST_ENTRY [ 0x000002d1`e2ea2c00 - 0x000002d1`e2ef26a0 ]
+0x030 InInitializationOrderModuleList : _LIST_ENTRY [ 0x000002d1`e2ea2a40 - 0x000002d1`e2ef10c0 ]
+0x040 EntryInProgress : (null)
+0x048 ShutdownInProgress : 0 ''
+0x050 ShutdownThreadId : (null)
The C++ evaluator is able to evaluate expressions that contain any valid C++ operator. These include structure and array access (-> . []), arithmetic (* - % + - << >> ++ -- ), bitwise (& ^ |) ), sizeof(), typecasts (dynamic_cast static_cast reinterpret_cast const_cast). For example, to get the length of the UNICODE_STRING structure we can use the following C++ expression.
0:000> ?? sizeof(ntdll!_UNICODE_STRING) unsigned int64 0x10
Assuming that there is a wide character (wchar) string stored at the address 0x00007ff8f65e2b30, we can use the following C++ evaluator to display the string:
0:000> ?? (wchar *)0x00007ff8f65e2b30 wchar_t * 0x00007ff8`f65e2b30 "KERNEL32.DLL"
In addition, the following operators are only valid in C++ expressions - #CONTAINING_RECORD(), #FIELD_OFFSET(), #RTL_CONTAINS_FIELD(), #RTL_FIELD_SIZE(), #RTL_NUMBER_OF(), #RTL_SIZEOF_THROUGH_FIELD(). These operators do not work with the MASM evaluator. For example, to find the offset of the field InLoadOrderModuleList in the structure PEB_LDR_DATA, we can use the following expression:
0:000> ?? #FIELD_OFFSET(ntdll!_PEB_LDR_DATA,InLoadOrderModuleList) long 0n16
Mixed evaluator usage
Oftentimes, in complex expressions, there is a need to use both the MASM and the C++ evaluators. To meet this need, WinDBG provides the @@(), @@masm() and the @@c++() operators. These operators can be used to switch the expression evaluator multiple times within the same expression. For instance, if we have to evaluate the value of the Length field of the structure PebLdr, we must typecast the global variable ntdll!PebLdr to its datatype ntdll!_PEB_LDR_DATA using the C++ evaluator. The C++ evaluator, however, fails to achieve this due to the fact that it is unable to evaluate the expression ntdll!PebLdrData.
0:000> ?? ((ntdll!_PEB_LDR_DATA*)ntdll!PebLdr)->Length Type information missing error for PebLdr Couldn't resolve error at 'ntdll!PebLdr)->Length'
To address this issue we use the MASM evaluator for evaluating the ntdll!PebLdr component of the aforementioned typecast expression and the C++ evaluator for the rest of the expression.
0:000> ?? ((ntdll!_PEB_LDR_DATA*)@@masm(ntdll!PebLdr))->Length unsigned long 0x58
Since the C++ evaluator (??) was used to evaluate the expression, we can avoid using the full expression @@masm() and instead just toggle the expression evaluator from C++ to MASM using just @@().
0:000> ?? ((ntdll!_PEB_LDR_DATA*)@@(ntdll!PebLdr))->Length unsigned long 0x58
The @@masm() operator is also useful when using expressions that perform global variable references and passed as parameters to other WinDBG commands. For example, when displaying the UNICODE_STRING structure stored at the global variable ntdll!SlashSystem32SlashString, the dt command is unable to evaluate the expression ntdll!SlashSystem32SlashString correctly.
0:000> dt ntdll!SlashSystem32SlashString ntdll!_UNICODE_STRING Symbol ntdll!SlashSystem32SlashString not found.
This can be fixed using the @@masm() operator as follows :
0:000> dt @@masm(ntdll!SlashSystem32SlashString) ntdll!_UNICODE_STRING
"\SYSTEM32\"
+0x000 Length : 0x14
+0x002 MaximumLength : 0x16
+0x008 Buffer : 0x00007ff8`f65e31f8 "\SYSTEM32\"
This can also be resolved using the special escape sequence @!"...", as shown below:
0:000> dt @!"ntdll!SlashSystem32SlashString" ntdll!_UNICODE_STRING
"\SYSTEM32\"
+0x000 Length : 0x14
+0x002 MaximumLength : 0x16
+0x008 Buffer : 0x00007ff8`f65e31f8 "\SYSTEM32\"
This escape sequence turns out to be very useful when evaluating C++ class method names, as shown below:
0:000> ? combase!RegistrationStore::TopLevelNode::`scalar deleting destructor' Couldn't resolve error at 'combase!RegistrationStore::TopLevelNode::`scalar deleting destructor'' 0:000> ? @!"combase!RegistrationStore::TopLevelNode::`scalar deleting destructor'" Evaluate expression: 140707258230144 = 00007ff8`f624e580
On the other hand, @@c++() is useful when evaluating structure fields and providing the results to WinDBG commands. For instance, in order to parse the PE headers to find the name of the function that serves as the entry point of the module notepad.exe, we can use the following command:
0:000> ln notepad + @@c++( ( (ntdll!_IMAGE_NT_HEADERS64*)( @@(notepad) + (( nt!_IMAGE_DOS_HEADER*)@@(notepad))->e_lfanew) )->OptionalHeader.AddressOfEntryPoint ) (00007ff6`37020100) notepad!WinMainCRTStartup | (00007ff6`37020118) notepad!__mainCRTStartup
The MASM operator $iment() (image entry) returns the same result, which can be used to set a breakpoint on the entry points of a module.
0:000> ? $iment(notepad) Evaluate expression: 140695461560576 = 00007ff6`37020100
Automatic Pseudo Registers
WinDBG commands can be batched together, placed in a file, and executed as a script. To help with scripting, WinDBG provides a set of registers similar to CPU registers but allows access to commonly used Windows data structures. Such registers are called automatic pseudo-registers. All pseudo-registers start with the $ symbol but are otherwise used in the same way as CPU registers.
For example, when evaluating an expression using the ? or ??, WinDBG automatically caches the result of the expression evaluation in the $exp pseudo-register. This allows us to use the results of one expression evaluation in another expression, potentially allowing us to build an expression evaluation pipeline.
Pseudo Registers that point to kernel mode data structure are :
- $proc = nt!_EPROCESS and nt!_KPROCESS structures for the current process.
- $thread = nt!_EHTREAD and nt!_KTHREAD structures for the current thread.
- $pcr = nt!_KPCR for the current CPU.
- $pcrb = nt!_KPRCB for the current CPU.
Pseudo Registers that point to user mode data structures are :
- $peb = ntdll!_PEB for the current process.
- $teb = ntdll!_TEB for the current thread.
Other pseudo-registers that don't point to data structures but nevertheless contain information useful in a script are as follows:
- The $ptrsize pseudo-register contains the pointer size of the process in user mode or pointer size of the system in kernel mode. In user mode, the values are 8 for 64-bit processes, 4 for WOW64 processes, and 4 for 32-bit processes on an x86 system. In kernel mode, the values are 8 on 64-bit systems and 4 on 32-bit systems. $prtsize helps scripts perform pointer arithmetic in a platform agnostic manner.
- The $pagesize pseudo-register contains the page size on the system. For all practical purposes, this is set to 4K (4096) on all CPU architectures supported by WinDBG today i.e. x86, x64, ARM, and ARM64. Large page sizes i.e. 2MB (on x64) are not accounted for in this variable.
References to automatic pseudo-registers in C++ expressions include the type and value of the pseudo-registers. Whereas, such references in MASM expressions only yield the value of these pseudo-registers.
0:000> ?? @$teb->ClientId.UniqueThread void * 0x00000000`000046fc
The above command works since the @$teb pseudo register includes the structure type for @$teb i.e. ntdll!_TEB as well as its value for the current thread. The example below shows that the $ptrsize pseudo-register contains type information.
0:000> ?? @$ptrsize unsigned int 8 0:000> ? @$ptrsize Evaluate expression: 8 = 00000000`00000008
In addition, there are other automatic pseudo-registers that are useful during live debugging, especially in breakpoint commands. Examples of such register are - the return address on stack ($ra), platform-independent instruction pointer value ($ip), platform-independent stack pointer value ($rsp), 64-bit return value ($retreg64), current thread ID ($tid), current process ID ($tpid) and many more.
Accessing Memory
The MASM evaluator supports multiple operators that allow reading data from memory, in various sizes, given the address of the memory location. These operators are poi() for reading pointer sized data, dqo(), dwo, wo(), and by() for reading 64-bit, 32-bit, 16-bit, and bit data respectively. The poi() operator can be nested to perform multiple levels of pointer dereferences as shown below. Consider the following expression performed using the C++ evaluator whose goal is to retrieve the pointer sized value stored at the Flink field of the InLoadOrderModuleList structure stored in the structure PEB_LDR_DATA.
0:000> ?? (void*)@$peb->Ldr->InLoadOrderModuleList.Blink void * 0x000002d1`e2ef2690
The raw structure fields involved here are as follows
0:000> dt ntdll!_PEB Ldr
+0x018 Ldr : Ptr64 _PEB_LDR_DATA
0:000> dt ntdll!_PEB_LDR_DATA InLoadOrderModuleList
+0x010 InLoadOrderModuleList : _LIST_ENTRY
0:000> dt ntdll!_LIST_ENTRY Blink
+0x008 Blink : Ptr64 _LIST_ENTRY
To get the same results using the MASM evaluator we must use nested poi() operators with the proper structure field offsets to get to the final result.
0:000> ? poi(poi(@$peb+18)+10+8) Evaluate expression: 3100478744208 = 000002d1`e2ef2690