WinDBG - the Fun Way: Part 1

09 Apr, 21

A while ago, WinDbg added support for a new debugger data model, a change that completely changed the way we can use WinDbg. No more horrible MASM commands and obscure syntax. No more copying addresses or parameters to a Notepad file so that you can use them in the next commands without scrolling up. No more running the same command over and over with different addresses to iterate over a list or an array.

This is part 1 of this guide, because I didn’t actually think anyone would read through 8000 words of me explaining WinDbg commands. So you get 2 posts of 4000 words! That’s better, right?

In this first post we will learn the basics of how to use this new data model — using custom registers and new built-in registers, iterating over objects, searching them and filtering them and customizing them with anonymous types. And finally we will learn how to parse arrays and lists in a much nicer and easier way than you’re used to.

And in the net post we’ll learn the more complicated and fancier methods and features that this data model gives us. Now that we all know what to expect and grabbed another cup of coffee, let’s start!

This data model, accessed in WinDbg through the dx command, is an extremely powerful tool, able to define custom variables, structures, functions and use a wide range of new capabilities. It also lets us search and filter information with LINQ — a natural query language built on top of database languages such as SQL.

This data model is documented and even has usage examples on GitHub. Additionally, all of its modules have documentation that can be viewed in the debugger with dx -v <method> (though you will get the same documentation if you run dx <method> without the -v flag):

dx -v Debugger.Utility.Collections.FromListEntry
Debugger.Utility.Collections.FromListEntry [FromListEntry(ListEntry, [<ModuleName | ModuleObject>], TypeName, FieldExpression) — Method which converts a LIST_ENTRY specified by the ‘ListEntry’ parameter of types whose name is specified by the string ‘TypeName’ and whose embedded links within that type are accessed via an expression specified by the string ‘FieldExpression’ into a collection object. If an optional module name or object is specified, the type name is looked up in the context of such module]

There has also been some external documentation, but I felt like there were things that needed further explanation and that this feature is worth more attention than it receives.

Custom Registers

First, NatVis adds the option for custom registers. Kind of like MASM had @$t1@$t2@$t3 , etc. Only now you can call them whatever name you want, and they can have a type of your choice:

dx @$myString = “My String”
dx @$myInt = 123

We can see all our variables with dx @$vars and remove them with dx @$vars.Remove("var name"), or clear all with @$vars.Clear(). We can also use dx to show handle more complicated structures, such as an EPROCESS. As you might know, symbols in public PDBs don’t have type information. With the old debugger, this wasn’t always a problem, since in MASM, there’s no types anyway, and we could use the poi command to dereference a pointer.

0: kd> dt nt!_EPROCESS poi(nt!PsInitialSystemProcess)
 +0x000 Pcb : _KPROCESS
 +0x2e0 ProcessLock : _EX_PUSH_LOCK
 +0x2e8 UniqueProcessId : (null)

But things got messier when the variable isn’t a pointer, like with PsIdleProcess:

0: kd> dt nt!_KPROCESS @@masm(nt!PsIdleProcess)
   +0x000 Header           : _DISPATCHER_HEADER
   +0x018 ProfileListHead  : _LIST_ENTRY [ 0x00000048`0411017e - 0x00000000`00000004 ]
   +0x028 DirectoryTableBase : 0xffffb10b`79f08010
   +0x030 ThreadListHead   : _LIST_ENTRY [ 0x00001388`00000000 - 0xfffff801`1b401000 ]
   +0x040 ProcessLock      : 0
   +0x044 ProcessTimerDelay : 0
   +0x048 DeepFreezeStartTime : 0xffffe880`00000000

We first have to use explicit MASM operators to get the address of PsIdleProcess and then print it as an EPROCESS. With dx we can be smarter and cast symbols directly, using c-style casts. But when we try to cast nt!PsInitialSystemProcess to a pointer to an EPROCESS:

dx @$systemProc = (nt!_EPROCESS*)nt!PsInitialSystemProcess
Error: No type (or void) for object at Address 0xfffff8074ef843a0

We get an error.

Like I mentioned, symbols have no type. And we can’t cast something with no type. So we need to take the address of the symbol, and cast it to a pointer to the type we want (In this case, PsInitialSystemProcess is already a pointer to an EPROCESS so we need to cast its address to a pointer to a pointer to an EPROCESS).

dx @$systemProc = *(nt!_EPROCESS**)&nt!PsInitialSystemProcess

Now that we have a typed variable, we can access its fields like we would do in C:

0: kd> dx @$systemProc->ImageFileName
@$systemProc->ImageFileName               [Type: unsigned char [15]]
[0]              : 0x53 [Type: unsigned char]
[1]              : 0x79 [Type: unsigned char]
[2]              : 0x73 [Type: unsigned char]
[3]              : 0x74 [Type: unsigned char]
[4]              : 0x65 [Type: unsigned char]
[5]              : 0x6d [Type: unsigned char]
[6]              : 0x0 [Type: unsigned char]

And we can cast that to get a nicer output:

dx (char*)@$systemProc->ImageFileName
(char*)@$systemProc->ImageFileName                 : 0xffffc10c8e87e710 : "System" [Type: char *]

We can also use ToDisplayString to cast it from a char* to a string. We have two options — ToDisplayString("s"), which will cast it to a string and keep the quotes as part of the string, or ToDisplayString("sb"), which will remove them:

dx ((char*)@$systemProc->ImageFileName).ToDisplayString("s")
((char*)@$systemProc->ImageFileName).ToDisplayString("s") : "System"
    Length           : 0x8
dx ((char*)@$systemProc->ImageFileName).ToDisplayString("sb")
((char*)@$systemProc->ImageFileName).ToDisplayString("sb") : System
    Length           : 0x6

Built-in Registers

This is fun, but for processes (and a few other things) there is an even easier way. Together with NatVis’ implementation in WinDbg we got some “free” registers already containing some useful information — curframecurprocesscursessioncurstack and curthread. It’s not hard to guess their contents by their names, but let’s take a look:


Gives us information about the current frame. I never actually used it myself, but it might be useful:

dx -r1 @$curframe.Attributes
    InstructionOffset : 0xfffff8074ebda1e1
    ReturnOffset     : 0xfffff80752ad2b61
    FrameOffset      : 0xfffff80751968830
    StackOffset      : 0xfffff80751968838
    FuncTableEntry   : 0x0
    Virtual          : 1
    FrameNumber      : 0x0


A container with information about the current process. This is not an EPROCESS (though it does contain it). It contains easily accessible information about the current process, like its threads, loaded modules, handles, etc.

dx @$curprocess
@$curprocess                 : System [Switch To]
    KernelObject     [Type: _EPROCESS]
    Name             : System
    Id               : 0x4
    Handle           : 0xf0f0f0f0

In KernelObject we have the EPROCESS, but we can also use the other fields. For example, we can access all the handles held by the process through @$curprocess.Io.Handles, which will lead us to an array of handles, indexed by their handle number:

dx @$curprocess.Io.Handles

System has a lot of handles, these are just the first few! Let’s just take a look at the first one (which we can also access through @$curprocess.Io.Handles[0x4]):

dx @$curprocess.Io.Handles.First()
    Handle           : 0x4
    Type             : Process
    GrantedAccess    : Delete | ReadControl | WriteDac | WriteOwner | Synch | Terminate | CreateThread | VMOp | VMRead | VMWrite | DupHandle | CreateProcess | SetQuota | SetInfo | QueryInfo | SetPort
    Object           [Type: _OBJECT_HEADER]

We can see the handle, the type of object the handle is for, its granted access, and we even have a pointer to the object itself (or its object header, to be precise)!

There are plenty more things to find under this register, and I encourage you to investigate them, but I will not show all of them.

By the way, have we mentioned already that dx allows tab completion?


As its name suggests, this register gives us information about the current debugger session:

dx @$cursession
@$cursession                 : Remote KD: KdSrv:Server=@{<Local>},Trans=@{NET:Port=55556,Key=,Target=}
    Id               : 0

So, we can get information about our debugger session, which is always fun. But there are more useful things to be found here, such as the Processes field, which is an array of all processes, indexed by their PID. Let’s pick one of them:

dx @$cursession.Processes[0x1d8]
@$cursession.Processes[0x1d8]                 : smss.exe [Switch To]
    KernelObject     [Type: _EPROCESS]
    Name             : smss.exe
    Id               : 0x1d8
    Handle           : 0xf0f0f0f0

Now we can get all that useful information about every single process! We can also search through processes by filtering them based on a search (such as by their name, specific modules loaded into them, strings in their command line, etc. But I will explain all of that later.


This register contains a single field — frames — which shows us the current stack in an easily-handled way:

dx @$curstack.Frames
    [0x0]        : nt!DbgBreakPointWithStatus + 0x1 [Switch To]
    [0x1]        : kdnic!TXTransmitQueuedSends + 0x125 [Switch To]
    [0x2]        : kdnic!TXSendCompleteDpc + 0x14d [Switch To]
    [0x3]        : nt!KiProcessExpiredTimerList + 0x169 [Switch To]
    [0x4]        : nt!KiRetireDpcList + 0x4e9 [Switch To]
    [0x5]        : nt!KiIdleLoop + 0x7e [Switch To]


Gives us information about the current thread, just like @$curprocess:

dx @$curthread
@$curthread                 : nt!DbgBreakPointWithStatus+0x1 (fffff807`4ebda1e1)  [Switch To]
    KernelObject     [Type: _ETHREAD]
    Id               : 0x0

It contains the ETHREAD in KernelObject, but also contains the TEB in Environment, and can show us the thread ID, stack and registers.

dx @$curthread.Registers

We have them conveniently separated to userkernelSIMD and FloatingPoint registers, and we can look at each separately:

dx -r1 @$curthread.Registers.Kernel
    cr0              : 0x80050033
    cr2              : 0x207b8f7abbe
    cr3              : 0x6d4002
    cr4              : 0x370678
    cr8              : 0xf
    gdtr             : 0xffff9d815ffdbfb0
    gdtl             : 0x57
    idtr             : 0xffff9d815ffd9000
    idtl             : 0xfff
    tr               : 0x40
    ldtr             : 0x0
    kmxcsr           : 0x1f80
    kdr0             : 0x0
    kdr1             : 0x0
    kdr2             : 0x0
    kdr3             : 0x0
    kdr6             : 0xfffe0ff0
    kdr7             : 0x400
    xcr0             : 0x1f

Searching and Filtering

A very useful thing that NatVis allows us to do, which we briefly mentioned before, is searching, filtering and ordering information in an SQL-like way through SelectWhereOrderBy and more.

For example, let’s try to find all the processes that don’t enable high entropy ASLR. This information is stored in the EPROCESS->MitigationFlags field, and the value for HighEntropyASLREnabled is 0x20 (all values can be found here and in the public symbols).

First, we’ll declare a new register with that value, just to make things more readable:

dx @$highEntropyAslr = 0x20
@$highEntropyAslr = 0x20 : 32

And then create our query to iterate over all processes and only pick ones where the HighEntropyASLREnabled bit is not set:

dx -r1 @$cursession.Processes.Where(p => (p.KernelObject.MitigationFlags & @$highEntropyAslr) == 0)
@$cursession.Processes.Where(p => (p.KernelObject.MitigationFlags & @$highEntropyAslr) == 0)                
    [0x910]          : spoolsv.exe [Switch To]
    [0xb40]          : IpOverUsbSvc.exe [Switch To]
    [0x1610]         : explorer.exe [Switch To]
    [0x1d8c]         : OneDrive.exe [Switch To]

Or we can check the flag directly through MitigationFlagsValues and get the same results:

dx -r1 @$cursession.Processes.Where(p => (p.KernelObject.MitigationFlagsValues.HighEntropyASLREnabled == 0))
@$cursession.Processes.Where(p => (p.KernelObject.MitigationFlagsValues.HighEntropyASLREnabled == 0))                
    [0x910]          : spoolsv.exe [Switch To]
    [0xb40]          : IpOverUsbSvc.exe [Switch To]
    [0x1610]         : explorer.exe [Switch To]
    [0x1d8c]         : OneDrive.exe [Switch To]

We can also use Select() to only show certain attributes of things we iterate over. Here we choose to see only the number of threads each process has:

dx @$cursession.Processes.Select(p => p.Threads.Count())
@$cursession.Processes.Select(p => p.Threads.Count())                
    [0x0]            : 0x6
    [0x4]            : 0xeb
    [0x78]           : 0x4
    [0x1d8]          : 0x5
    [0x244]          : 0xe
    [0x294]          : 0x8
    [0x2a0]          : 0x10
    [0x2f8]          : 0x9
    [0x328]          : 0xa
    [0x33c]          : 0xd
    [0x3a8]          : 0x2c
    [0x3c0]          : 0x8
    [0x3c8]          : 0x8
    [0x204]          : 0x15
    [0x300]          : 0x1d
    [0x444]          : 0x3f

We can also see everything in decimal by adding , d to the end of the command, to specify the output format (we can also use b for binary, o for octal or s for string):

dx @$cursession.Processes.Select(p => p.Threads.Count()), d
@$cursession.Processes.Select(p => p.Threads.Count()), d                
    [0]              : 6
    [4]              : 235
    [120]            : 4
    [472]            : 5
    [580]            : 14
    [660]            : 8
    [672]            : 16
    [760]            : 9
    [808]            : 10
    [828]            : 13
    [936]            : 44
    [960]            : 8
    [968]            : 8
    [516]            : 21
    [768]            : 29
    [1092]           : 63

Or, in a slightly more complicated example, see the ideal processor for each thread running in a certain process (I chose a process at random, just to see something that is not the System process):

dx -r1 @$cursession.Processes[0x1b2c].Threads.Select(t => t.Environment.EnvironmentBlock.CurrentIdealProcessor.Number)
@$cursession.Processes[0x1b2c].Threads.Select(t => t.Environment.EnvironmentBlock.CurrentIdealProcessor.Number)                
    [0x1b30]         : 0x1 [Type: unsigned char]
    [0x1b40]         : 0x2 [Type: unsigned char]
    [0x1b4c]         : 0x3 [Type: unsigned char]
    [0x1b50]         : 0x4 [Type: unsigned char]
    [0x1b48]         : 0x5 [Type: unsigned char]
    [0x1b5c]         : 0x0 [Type: unsigned char]
    [0x1b64]         : 0x1 [Type: unsigned char]

We can also use OrderBy to get nicer results, for example to get a list of all processes sorted by alphabetical order:

dx -r1 @$cursession.Processes.OrderBy(p => p.Name)
@$cursession.Processes.OrderBy(p => p.Name)                
    [0x1848]         : ApplicationFrameHost.exe [Switch To]
    [0x0]            : Idle [Switch To]
    [0xb40]          : IpOverUsbSvc.exe [Switch To]
    [0x106c]         : LogonUI.exe [Switch To]
    [0x754]          : MemCompression [Switch To]
    [0x187c]         : MicrosoftEdge.exe [Switch To]
    [0x1b94]         : MicrosoftEdgeCP.exe [Switch To]
    [0x1b7c]         : MicrosoftEdgeSH.exe [Switch To]
    [0xb98]          : MsMpEng.exe [Switch To]
    [0x1158]         : NisSrv.exe [Switch To]
    [0x1d8c]         : OneDrive.exe [Switch To]
    [0x78]           : Registry [Switch To]
    [0x1ed0]         : RuntimeBroker.exe [Switch To]

If we want them in a descending order, we can use OrderByDescending.

But what if we want to pick more than one attribute to see? There is a solution for that too.

Anonymous Types

We can declare a type of our own, that will be unnamed and only used in the scope of our query, using this syntax: Select(x => new { var1 = x.A, var2 = x.B, ...}).

We’ll try it out on one of our previous examples. Let’s say for each process we want to show a process name and its thread count:

dx @$cursession.Processes.Select(p => new {Name = p.Name, ThreadCount = p.Threads.Count()})
@$cursession.Processes.Select(p => new {Name = p.Name, ThreadCount = p.Threads.Count()})                

But now we only see the process container, not the actual information. To see the information itself we need to go one layer deeper, by using -r2. The number specifies the output recursion level. The default is -r1-r0 will show no output, -r2 will show two levels, etc.

dx -r2 @$cursession.Processes.Select(p => new {Name = p.Name, ThreadCount = p.Threads.Count()})
@$cursession.Processes.Select(p => new {Name = p.Name, ThreadCount = p.Threads.Count()})                
        Name             : Idle
        ThreadCount      : 0x6
        Name             : System
        ThreadCount      : 0xeb
        Name             : Registry
        ThreadCount      : 0x4
        Name             : smss.exe
        ThreadCount      : 0x5
        Name             : csrss.exe
        ThreadCount      : 0xe
        Name             : wininit.exe
        ThreadCount      : 0x8
        Name             : csrss.exe
        ThreadCount      : 0x10

This already looks much better, but we can make it look even nicer with the new grid view, accessed through the -g flag:

dx -g @$cursession.Processes.Select(p => new {Name = p.Name, ThreadCount = p.Threads.Count()})

OK, this just looks awesome. And yes, these headings are clickable and will sort the table!

And if we want to see the PIDs and thread numbers in decimal we can just add , d to the end of the command:

dx -g @$cursession.Processes.Select(p => new {Name = p.Name, ThreadCount = p.Threads.Count()}),d

Arrays and Lists

DX also gives us a new, much easier way, to handle arrays and lists with new syntax.
Let’s look at arrays first, where the syntax is dx *(TYPE(*)[Size])<pointer to array start>.

For this example, we will dump the contents on PsInvertedFunctionTable, which contains an array of up to 256 cached modules in its TableEntry field.

First, we will get the pointer of this symbol and cast it to _INVERTED_FUNCTION_TABLE:

dx @$inverted = (nt!_INVERTED_FUNCTION_TABLE*)&nt!PsInvertedFunctionTable
@$inverted = (nt!_INVERTED_FUNCTION_TABLE*)&nt!PsInvertedFunctionTable                 : 0xfffff8074ef9b010 [Type: _INVERTED_FUNCTION_TABLE *]
    [+0x000] CurrentSize      : 0xbe [Type: unsigned long]
    [+0x004] MaximumSize      : 0x100 [Type: unsigned long]
    [+0x008] Epoch            : 0x19e [Type: unsigned long]
    [+0x00c] Overflow         : 0x0 [Type: unsigned char]
    [+0x010] TableEntry       [Type: _INVERTED_FUNCTION_TABLE_ENTRY [256]]

Now we can create our array. Unfortunately, the size of the array has to be static and can’t use a register, so we need to input it manually, based on CurrentSize (or just set it to 256, which is the size of the whole array). And we can use the grid view to print it nicely:

dx -g @$tableEntry = *(nt!_INVERTED_FUNCTION_TABLE_ENTRY(*)[0xbe])@$inverted->TableEntry

Alternatively, we can use the Take() method, which receives a number and prints that amount of elements from a collection, and get the same result:

dx -g @$inverted->TableEntry->Take(@$inverted->CurrentSize)

We can also do the same thing to see the UserInvertedFunctionTable (right after we switch to user that’s not System), starting from nt!KeUserInvertedFunctionTable:

dx @$inverted = *(nt!_INVERTED_FUNCTION_TABLE**)&nt!KeUserInvertedFunctionTable
@$inverted = *(nt!_INVERTED_FUNCTION_TABLE**)&nt!KeUserInvertedFunctionTable                 : 0x7ffa19e3a4d0 [Type: _INVERTED_FUNCTION_TABLE *]
    [+0x000] CurrentSize      : 0x2 [Type: unsigned long]
    [+0x004] MaximumSize      : 0x200 [Type: unsigned long]
    [+0x008] Epoch            : 0x6 [Type: unsigned long]
    [+0x00c] Overflow         : 0x0 [Type: unsigned char]
    [+0x010] TableEntry       [Type: _INVERTED_FUNCTION_TABLE_ENTRY [256]]
dx -g @$inverted->TableEntry->Take(@$inverted->CurrentSize)

And of course we can use Select() , Where() or other functions to filter, sort or select only specific fields for our output and get tailored results that fit exactly what we need.

The next thing to handle is lists — Windows is full of linked lists, you can find them everywhere. Linking processes, threads, modules, DPCs, IRPs, and more.

Fortunately the new data model has a very useful Debugger method - Debugger.Utiilty.Collections.FromListEntry, which takes in a linked list head, type and name of the field in this type containing the LIST_ENTRY structure, and will return a container of all the list contents.

So, for our example let’s dump all the handle tables in the system. Our starting point will be the symbol nt!HandleTableListHead, the type of the objects in the list is nt!_HANDLE_TABLE and the field linking the list is HandleTableList:

dx -r2 Debugger.Utility.Collections.FromListEntry(*(nt!_LIST_ENTRY*)&nt!HandleTableListHead, "nt!_HANDLE_TABLE", "HandleTableList")
Debugger.Utility.Collections.FromListEntry(*(nt!_LIST_ENTRY*)&nt!HandleTableListHead, "nt!_HANDLE_TABLE", "HandleTableList")                
    [0x0]            [Type: _HANDLE_TABLE]
        [+0x000] NextHandleNeedingPool : 0x3400 [Type: unsigned long]
        [+0x004] ExtraInfoPages   : 0 [Type: long]
        [+0x008] TableCode        : 0xffff8d8dcfd18001 [Type: unsigned __int64]
        [+0x010] QuotaProcess     : 0x0 [Type: _EPROCESS *]
        [+0x018] HandleTableList  [Type: _LIST_ENTRY]
        [+0x028] UniqueProcessId  : 0x4 [Type: unsigned long]
        [+0x02c] Flags            : 0x0 [Type: unsigned long]
        [+0x02c ( 0: 0)] StrictFIFO       : 0x0 [Type: unsigned char]
        [+0x02c ( 1: 1)] EnableHandleExceptions : 0x0 [Type: unsigned char]
        [+0x02c ( 2: 2)] Rundown          : 0x0 [Type: unsigned char]
        [+0x02c ( 3: 3)] Duplicated       : 0x0 [Type: unsigned char]
        [+0x02c ( 4: 4)] RaiseUMExceptionOnInvalidHandleClose : 0x0 [Type: unsigned char]
        [+0x030] HandleContentionEvent [Type: _EX_PUSH_LOCK]
        [+0x038] HandleTableLock  [Type: _EX_PUSH_LOCK]
        [+0x040] FreeLists        [Type: _HANDLE_TABLE_FREE_LIST [1]]
        [+0x040] ActualEntry      [Type: unsigned char [32]]
        [+0x060] DebugInfo        : 0x0 [Type: _HANDLE_TRACE_DEBUG_INFO *]
    [0x1]            [Type: _HANDLE_TABLE]
        [+0x000] NextHandleNeedingPool : 0x400 [Type: unsigned long]
        [+0x004] ExtraInfoPages   : 0 [Type: long]
        [+0x008] TableCode        : 0xffff8d8dcb651000 [Type: unsigned __int64]
        [+0x010] QuotaProcess     : 0xffffb90a530e4080 [Type: _EPROCESS *]
        [+0x018] HandleTableList  [Type: _LIST_ENTRY]
        [+0x028] UniqueProcessId  : 0x78 [Type: unsigned long]
        [+0x02c] Flags            : 0x10 [Type: unsigned long]
        [+0x02c ( 0: 0)] StrictFIFO       : 0x0 [Type: unsigned char]
        [+0x02c ( 1: 1)] EnableHandleExceptions : 0x0 [Type: unsigned char]
        [+0x02c ( 2: 2)] Rundown          : 0x0 [Type: unsigned char]
        [+0x02c ( 3: 3)] Duplicated       : 0x0 [Type: unsigned char]
        [+0x02c ( 4: 4)] RaiseUMExceptionOnInvalidHandleClose : 0x1 [Type: unsigned char]
        [+0x030] HandleContentionEvent [Type: _EX_PUSH_LOCK]
        [+0x038] HandleTableLock  [Type: _EX_PUSH_LOCK]
        [+0x040] FreeLists        [Type: _HANDLE_TABLE_FREE_LIST [1]]
        [+0x040] ActualEntry      [Type: unsigned char [32]]
        [+0x060] DebugInfo        : 0x0 [Type: _HANDLE_TRACE_DEBUG_INFO *]

See the QuotaProcess field? That field points to the process that this handle table belongs to. Since every process has a handle table, this allows us to enumerate all the processes on the system in a way that’s not widely known. This method has been used by rootkits in the past to enumerate processes without being detected by EDR products. So to implement this we just need to Select() the QuotaProcess from each entry in our handle table list. To create a nicer looking output we can also create an anonymous container with the process name, PID and EPROCESS pointer:

dx -r2 (Debugger.Utility.Collections.FromListEntry(*(nt!_LIST_ENTRY*)&nt!HandleTableListHead, "nt!_HANDLE_TABLE", "HandleTableList")).Select(h => new { Object = h.QuotaProcess, Name = ((char*)h.QuotaProcess->ImageFileName).ToDisplayString("s"), PID = (__int64)h.QuotaProcess->UniqueProcessId})
(Debugger.Utility.Collections.FromListEntry(*(nt!_LIST_ENTRY*)&nt!HandleTableListHead, "nt!_HANDLE_TABLE", "HandleTableList")).Select(h => new { Object = h.QuotaProcess, Name = ((char*)h.QuotaProcess->ImageFileName).ToDisplayString("s"), PID = (__int64)h.QuotaProcess->UniqueProcessId})
[0x0]            : Unspecified error (0x80004005)
    Object           : 0xffffb10b70906080 [Type: _EPROCESS *]
    Name             : "Registry"
    PID              : 120 [Type: __int64]
    Object           : 0xffffb10b72eba0c0 [Type: _EPROCESS *]
    Name             : "smss.exe"
    PID              : 584 [Type: __int64]
    Object           : 0xffffb10b76586140 [Type: _EPROCESS *]
    Name             : "csrss.exe"
    PID              : 696 [Type: __int64]
    Object           : 0xffffb10b77132140 [Type: _EPROCESS *]
    Name             : "wininit.exe"
    PID              : 772 [Type: __int64]
    Object           : 0xffffb10b770a2080 [Type: _EPROCESS *]
    Name             : "csrss.exe"
    PID              : 780 [Type: __int64]
    Object           : 0xffffb10b7716d080 [Type: _EPROCESS *]
    Name             : "winlogon.exe"
    PID              : 852 [Type: __int64]

The first result is the table belonging to the System process and it does not have a QuotaProcess, which is the reason this query returns an error for it. But it should work perfectly for every other entry in the array. If we want to make our output prettier, we can filter out entries where QuotaProcess == 0 before we do the Select():

dx -r2 (Debugger.Utility.Collections.FromListEntry(*(nt!_LIST_ENTRY*)&nt!HandleTableListHead, “nt!_HANDLE_TABLE”, “HandleTableList”)).Where(h => h.QuotaProcess != 0).Select(h => new { Object = h.QuotaProcess, Name = ((char*)h.QuotaProcess->ImageFileName).ToDisplayString("s"), PID = h.QuotaProcess->UniqueProcessId})

As we already showed before, we can also print this list in a graphic view or use any LINQ queries to make the output match our needs.

This is the end of our first part, but don’t worry, the second part is available too, and it contains all the fancy new dx methods such as a new disassembler, defining our own methods, conditional breakpoints that actually work, and more.