难译 | windbg 乐趣之道(上)

前言

Yarden Shafir 分享了两篇非常通俗易懂的,关于 windbg 新引入的调试数据模型的文章。链接如下:

part1https://medium.com/@yardenshafir2/windbg-the-fun-way-part-1-2e4978791f9b

part2https://medium.com/@yardenshafir2/windbg-the-fun-way-part-2-7a904cba5435

本文是第一部分的译文。在有道词典、必应词典、谷歌翻译的大力帮助下完成,感谢以上翻译工具,我只是一个搬运工。强烈建议英文好的朋友阅读原文,因为在翻译的过程中不可避免的按我的理解做了调整。

说明:

  1. 翻译已征得原作者同意。

translation-permission

  1. 鸽了太久了,对不住原作者

  2. 标题实在想不到更好的翻译了

  3. 作者的 github

以下是译文!


不久前,WinDbg 添加了对新调试数据模型(debugger data model)的支持,这一变化彻底改变了我们使用 WinDbg 的方式。不再有可怕的 MASM 命令和晦涩的语法。无需再将地址或参数复制到记事本以便在后续命令中使用它们。不用再一遍又一遍地运行相同的命令(传递不同的地址)来遍历列表或数组。

这是该指南的第一部分,因为我认为实际上不会有人从头到尾读完长达 8000 字的 WinDbg 命令解释。所以,我准备了 24000 字的文章!这样会好些,对吧?

在第一篇文章中,我们将学习如何使用这个新数据模型的基础知识 —— 使用自定义寄存器和新的内置寄存器,遍历对象,使用匿名类型来搜索、过滤、自定义对象。最后,我们将学习如何以一种比以前更好、更简单的方式解析数组和列表。

在这两篇文章中,我们将学习这个数据模型为我们提供的更复杂、更高级的方法和特性。现在我们都知道将会发生什么,准备好一杯咖啡,让我们开始吧!

这个数据模型,可以在 WinDbg 中通过 dx 命令使用,是一个极其强大的工具,能够定义自定义变量、结构体、函数并使用很多其它新功能。它还允许我们使用 LINQ(一种建立在 SQL 数据库语言之上的自然查询语言)进行查询和过滤信息。

这个数据模型已经被文档化了,甚至在 GitHub 上还有使用范例。此外,所有模块都有对应的文档,可以在调试器中通过 dx -v <method> 查看。(您也可以通过运行不带 -vdx <method> 命令获得相同的文档 ) :

1
2
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]

除此之外,还有一些外部文档,但我觉得有些事情需要进一步解释,并且这个特性值得更多的关注。

译注:

debugger data model 文档链接:https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/data-model-cpp-overview)

使用范例 github 链接:https://github.com/microsoft/WinDbg-Samples)

自定义寄存器(Custom Registers)

首先,NatVis 允许我们添加自定义寄存器。有点像 MASM 中的 @$t1@$t2@$t3 等。 只是,现在你可以起任何想要的名字,并且可以指定类型:

1
2
dx @$myString = "My String"
dx @$myInt = 123

我们可以使用 dx @$vars 来查看所有变量,使用 dx @$vars.Remove("var name") 来移除某个变量,或者使用 @$vars.Clear() 清除所有变量。我们还可以使用 dx 来处理更复杂的结构,比如 EPROCESS。您可能知道,公共调试符号(public PDBs)中的符号不包含类型信息。使用旧调试器,这并不总是一个问题,因为在 MASM 中,反正也没有类型,我们可以使用 poi 命令对指针进行解引用。

1
2
3
4
5
0: kd> dt nt!_EPROCESS poi(nt!PsInitialSystemProcess)
+0x000 Pcb : _KPROCESS
+0x2e0 ProcessLock : _EX_PUSH_LOCK
+0x2e8 UniqueProcessId : (null)
...

但当变量不是指针时,事情就变得更混乱了,比如 PsIdleProcess

1
2
3
4
5
6
7
8
9
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
...

我们必须先使用显式的 MASM 操作符来获取 PsIdleProcess 的地址,然后将它当作 EPROCESS 显示出来。通过使用 dx,我们可以更聪明地直接使用 C 风格的类型转换对符号进行强制转换。但是当我们尝试把 nt!Psinitialsystemprocess 转换为一个指向 EPROCESS 的指针:

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

我们得到了一个错误。

就像我提到的那样,符号没有类型。我们不能转换没有类型的东西。所以我们需要获取符号的地址,并转换成一个指向我们想要的类型的指针(在当前示例中,PsInitialSystemProcess 已经是一个指向 EPROCESS 的指针,所以我们需要将其地址转换为一个指向 EPROCESS 指针的指针)。

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

现在,我们有了一个有类型的变量,我们可以像在 C 中那样访问它的字段:

1
2
3
4
5
6
7
8
9
10
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]

我们还可以对它进行类型转换以得到更好的输出:

1
2
3
4
dx (char*)@$systemProc->ImageFileName

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

我们还可以使用 ToDisplayString 将它从 char* 转换为 string。我们有两个选项 —— ToDisplayString("s"),将输出结果转换为字符串并将引号作为字符串的一部分,或者ToDisplayString("sb"),将删除引号:

1
2
3
4
5
6
7
8
9
10
11
12
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)

这很有趣,但对于进程(和其他一些东西),还有一种更简单的方法。WinDbg 中的 NatVis 为我们提供了一些有用的“免费”寄存器 —— curframe, curprocess, cursession, curstackcurthread。不难通过它们的名字猜出对应的内容,但还是看一看吧:

@$curframe

提供有关当前帧的信息。我自己从来没用过,但它也许有用:

1
2
3
4
5
6
7
8
9
10
dx -r1 @$curframe.Attributes

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

@$curprocess

一个包含当前进程信息的容器。但不是 EPROCESS (尽管包含它)。它包含了关于当前进程的易于访问的信息,比如线程、加载的模块、句柄等。

1
2
3
4
5
6
7
8
9
10
11
12
dx @$curprocess

@$curprocess : System [Switch To]
KernelObject [Type: _EPROCESS]
Name : System
Id : 0x4
Handle : 0xf0f0f0f0
Threads
Modules
Environment
Devices
Io

我们不仅可以访问 EPROCESS(通过 KernelObject ),还可以使用其他字段。例如,我们可以通过 @$curprocess.Io.Handles 访问进程持有的所有句柄。@$curprocess.Io.Handles 会返回一个由句柄值索引的句柄数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dx @$curprocess.Io.Handles

@$curprocess.Io.Handles
[0x4]
[0x8]
[0xc]
[0x10]
[0x14]
[0x18]
[0x1c]
[0x20]
[0x24]
[0x28]
...

System 进程有很多句柄,这只是开始的几个!让我们来看看第一个(我们也可以通过 @$curprocess.Io.Handles[0x4] 来访问它):

1
2
3
4
5
6
7
dx @$curprocess.Io.Handles.First()

@$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]

我们可以看到句柄,句柄对应的对象类型,它被授予的访问权限,甚至有一个指向对象本身的指针(或者更准确地说,它指向的是对象头)!

这个寄存器还有很多东西可以探索,我鼓励您去调查它们,但我不会全部展示。

顺便说一下,我是否提过 dx 允许 tab 补全?

@$cursession

顾名思义,这个寄存器为我们提供关于当前调试会话的信息:

1
2
3
4
5
6
7
dx @$cursession

@$cursession : Remote KD: KdSrv:Server=@{<Local>},Trans=@{NET:Port=55556,Key=1.2.3.4,Target=192.168.251.21}
Processes
Id : 0
Devices
Attributes

因此,我们可以获得关于调试会话的信息,这总是很有趣。但是还可以找到更多有用的东西,比如 Processes 字段,它是所有进程的数组,由 PID 索引。让我们选一个吧:

1
2
3
4
5
6
7
8
9
10
11
12
dx @$cursession.Processes[0x1d8]

@$cursession.Processes[0x1d8] : smss.exe [Switch To]
KernelObject [Type: _EPROCESS]
Name : smss.exe
Id : 0x1d8
Handle : 0xf0f0f0f0
Threads
Modules
Environment
Devices
Io

现在我们可以获得每个进程的全部有用信息!我们还可以通过过滤来搜索进程(例如,通过进程名、加载到进程中的特定模块、命令行中的字符串等等)。我稍后再解释这些。

@$curstack

这个寄存器只包含一个字段 —— 帧,它以一种易于处理的方式向我们显示当前调用栈:

1
2
3
4
5
6
7
8
9
dx @$curstack.Frames

@$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]

@$curthread

为我们提供当前线程的相关信息,就像 @$curprocess

1
2
3
4
5
6
7
8
dx @$curthread

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

KernelObject 字段包含了 ETHREADEnvironment 字段包含了 TEB,除此之外,还包括线程 ID、调用栈和寄存器。

1
2
3
4
5
6
7
dx @$curthread.Registers

@$curthread.Registers
User
Kernel
SIMD
FloatingPoint

寄存器被方便地分为用户寄存器、内核寄存器、SIMD 寄存器和浮点寄存器,我们可以分别查看它们:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
dx -r1 @$curthread.Registers.Kernel

@$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

搜索和过滤(Searching and Filtering)

我们在前面简要提到过,NatVis 允许我们做一些非常有用的操作 —— 通过类似 SQL 中的 SelectWhereOrderderby 及其它方法对信息进行查找、过滤和排序。

例如,让我们尝试找到所有没启用 high entropy ASLR 的进程。该信息存储在 EPROCESS->MitigationFlags 字段中,HighEntropyASLREnabled 的值是 0x20 (所有值都可以在这里和公共符号中找到)。

首先,我们声明一个值为 0x20 的新寄存器,这只是为了使代码更具可读性:

1
2
3
0: kd> dx @$highEntropyAslr = 0x20

@$highEntropyAslr = 0x20 : 32

然后创建查询来遍历所有进程,只选择那些没有设置 HighEntropyASLREnabled 标志位的进程:

1
2
3
4
5
6
7
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]

或者我们可以直接检查 MitigationFlagsValues 并获得相同的结果:

1
2
3
4
5
6
7
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]

我们还可以使用 Select() 来显示正在遍历的对象的某些特定属性。这里我们只查看每个进程的线程数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
...

我们还可以通过在命令末尾加上 ,d 来以十进制格式显示所有内容。(b 表示以二进制显示,o 表示以八进制显示,s 表示以字符串显示):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
...

再看一个稍微复杂一点的示例 —— 查看某个特定进程中每个线程的理想处理器(我随机选择了一个进程,只是为了查看非 System 进程的一些信息):

1
2
3
4
5
6
7
8
9
10
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]

我们还可以使用 OrderBy 来获得更好的输出,例如获取按字母顺序排序的进程列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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]
...

如果我们希望按降序排列,可以使用 OrderByDescending

但是,如果我们想要查看多个属性该怎么办呢?有办法。

匿名类型(Anonymous Types)

我们可以声明一个自定义的类型,它将是未命名的并且只在查询作用域中有效,语法如下:Select(x => new {var1 = x.A, var2 = x.B,…})

我们把它用在前面的示例中。假设我们想要显示每个进程的进程名和线程数:

1
2
3
4
5
6
7
8
9
10
11
12
13
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()})
[0x0]
[0x4]
[0x78]
[0x1d8]
[0x244]
[0x294]
[0x2a0]
[0x2f8]
[0x328]
...

但是现在我们只看到进程容器,而不是实际的信息。为了查看信息本身,我们需要使用 -r2 来多显示一层。r 后面的数字表示递归层数。默认值是 -r1-r0 将不显示输出,-r2 将显示两层,以此类推。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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()})
[0x0]
Name : Idle
ThreadCount : 0x6
[0x4]
Name : System
ThreadCount : 0xeb
[0x78]
Name : Registry
ThreadCount : 0x4
[0x1d8]
Name : smss.exe
ThreadCount : 0x5
[0x244]
Name : csrss.exe
ThreadCount : 0xe
[0x294]
Name : wininit.exe
ThreadCount : 0x8
[0x2a0]
Name : csrss.exe
ThreadCount : 0x10
...

这看起来已经好多了,但是我们可以使用新的表格视图让它看起来更好(通过 -g 选项):

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

output-of-dx-g-cursession.Processes.Select

这看起来太棒了。是的,这些标题是可点击的,点击标题会将表格排序!

如果我们想看到以十进制显示的 PID 和线程数,我们可以在命令的末尾加上 ,d

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

数组和链表(Arrays and Lists)

dx 为我们提供了一种新的、更简单的方法来处理数组和列表。

让我们先来看看数组,语法是 dx *(TYPE(*)[Size])<pointer to array start>

作为示例,我们将转储 PsInvertedFunctionTable 中的内容,它的 TableEntry 字段包含了最多容纳 256 个缓存模块的数组。

首先,我们将获得该符号的指针,并将其转换为 _INVERTED_FUNCTION_TABLE

1
2
3
4
5
6
7
8
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]]

现在我们可以创建数组了。不幸的是,数组的大小必须是静态的,不能使用变量,所以我们需要根据 CurrentSize 手动输入(或者将它设置为 256,这是整个数组的大小)。我们可以用表格视图很好地显示它:

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

output-of-dx-g-tableEntry-_INVERTED_FUNCTION_TABLE_ENTRY

或者,我们可以使用 Take() 方法来获得相同的结果,该方法接收一个数字并显示指定数量的集合元素:

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

我们也可以用同样的方法来查看 UserInvertedFunctionTable (在我们切换到非 System 进程的用户态空间后),从 nt!KeUserInvertedFunctionTable 开始:

1
2
3
4
5
6
7
8
9
10
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)

dx-inverted-_INVERTED_FUNCTION_TABLE

当然,我们可以使用 Select()Where() 或者其他函数来过滤、排序或者选择特定的字段进行输出,以得到完全满足我们需求的结果。

接下来要处理的是列表 —— Windows 中到处都是链表,你可以在任何地方找到它们。进程、线程、模块、DPCsIRPs还有更多

幸运的是,新数据模型提供了一个非常有用的方法—— Debugger.Utiilty.Collections.FromListEntry,它接收一个链表头,链表中的对象类型和类型中包含 LIST_ENTRY 的字段名称,并返回一个包含所有列表内容的容器。

让我们以转储系统中所有的句柄表为例。我们的起点将是符号 nt!HandleTableListHead,链表中的对象类型是 nt!HANDLE_TABLE ,连接列表的字段是 HandleTableList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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 *]
...

看到 QuotaProcess 字段了吗?该字段指向当前句柄表所属的进程。由于每个进程都有一个句柄表,这允许我们以一种不为人所知的方式枚举系统中的所有进程。这种方法在过去被 rootkit 用于枚举进程,而不被 EDR 产品检测到。因此,要实现这一点,我们只需要从句柄表列表中的每个条目中选择 QuotaProcess (通过 Select() 方法)。为了让输出结果更易读,我们还可以创建一个包含进程名、PIDEPROCESS 指针的匿名容器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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)
[0x1]
Object : 0xffffb10b70906080 [Type: _EPROCESS *]
Name : "Registry"
PID : 120 [Type: __int64]
[0x2]
Object : 0xffffb10b72eba0c0 [Type: _EPROCESS *]
Name : "smss.exe"
PID : 584 [Type: __int64]
[0x3]
Object : 0xffffb10b76586140 [Type: _EPROCESS *]
Name : "csrss.exe"
PID : 696 [Type: __int64]
[0x4]
Object : 0xffffb10b77132140 [Type: _EPROCESS *]
Name : "wininit.exe"
PID : 772 [Type: __int64]
[0x5]
Object : 0xffffb10b770a2080 [Type: _EPROCESS *]
Name : "csrss.exe"
PID : 780 [Type: __int64]
[0x6]
Object : 0xffffb10b7716d080 [Type: _EPROCESS *]
Name : "winlogon.exe"
PID : 852 [Type: __int64]
...

第一条结果属于 System 进程,它没有 QuotaProcess,这就是查询返回错误的原因。但对于数组中的其他元素,应该是完美的。如果我们想让输出结果更漂亮,可以在执行 Select() 之前过滤掉 QuotaProcess == 0 的条目:

1
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})

如前所示,我们也可以在表格视图中显示这个列表,或者使用任何 LINQ 查询来使输出满足我们的需要。

第一部分到此结束,但不要担心,第二部分就在这里。它包含了全部新奇的 dx 方法,比如一个新的反汇编器,自定义方法,可以真正工作的条件断点,以及更多。

BianChengNan wechat
扫描左侧二维码关注公众号,扫描右侧二维码加我个人微信:)
0%