使用WinDbg调试Windows内核(二)

来源:互联网 发布:网络兼职软文写手招聘 编辑:程序博客网 时间:2024/06/05 04:08

使用WinDbg调试Windows内核(二)

上篇文章介绍了windbg调试内核的基本环境设置以及一些基础调试技巧,这篇文章介绍一些windbg的高级调试技巧。

0×01使用断点跟踪数据

断点通常用在暂停某个我们感兴趣的执行代码,例如当某个函数被调用时,我们还可以使用WinDbg断点命令字符串跟踪一些信息。在这里,我们将着眼于跟踪特定用户模式进程的有趣信息,即特定数据NOTEPAD.EXE写入到磁盘的一个例子。在开始前,我们需要在我们的虚拟机中打开一个记事本实例。我们现在可以创建一个断点来拦截文件系统写入记事本的数据,并同时显示要写入磁盘的数据。

第一步,我们需要找到Notepad.exe进程的一些信息,以确保我们只在这个进程里断下,而不是在每个进程都断下。而我们需要寻找的信息是一个指向EPROCESS结构的指针。该EPROCESS结构是用来表示一个进程的主内核数据结构。你可以看到包含“DT _EPROCESS”(在EPROCESS结构dump类型)的信息。为了找到一个给定的过程的EPROCESS结构,我们可以调用!Process扩展命令。该扩展命令打印目标系统中当前活动进程的信息。我们过滤筛选出notepad的进程,并且只显示最低限度的信息:

图片1.png

该EPROCESS的指针为蓝色突出“PROCESS”字段,我们接下来就会用到这个值。

我们要设置在内核中设置NtWriteFile断点。这是系统的调用,所有用户模式写入磁盘会调用该函数。通过在此处设置断点,我们可以看到系统所有写入磁盘数据的过程。这样就显得很烦了,所以我们将使用上述EPROCESS值,要求从我们选择的进程上下文中断NtWriteFile函数。我们可以使用命令如下:

 bp /p fffffa800295d060 nt!NtWriteFile "da poi(@rsp+30); g"

这就只在我们的进程中设置(通过/ P使用我们的EPROCESS值)NT!NtWriteFile(NT为内核模块的名称)断点。当断点命中时,引号后的命令将运行,这里由分号来分隔每个命令。这里已经使用了notepad写显示数据的命令,然后使用“g”重新启动虚拟机执行。但是,为什么“da poi (*@ rsp+ 30)”的显示结果为写入缓冲区数据?

要理解这一部分,我们需要看看NtWriteFile的函数原型:

NTSTATUS NtWriteFile(

  _In_     HANDLE           FileHandle,

  _In_opt_ HANDLE           Event,

  _In_opt_ PIO_APC_ROUTINE  ApcRoutine,

  _In_opt_ PVOID            ApcContext,

  _Out_    PIO_STATUS_BLOCK IoStatusBlock,

  _In_     PVOID            Buffer,

  _In_     ULONG            Length,

  _In_opt_ PLARGE_INTEGER   ByteOffset,

  _In_opt_ PULONG           Key

);

来源于这里

在这个函数的原型中,我们感兴趣的是Buffer参数。这些缓冲区的数据调通过函数调用最后写入磁盘。在微软64位调用约定中,前四个参数由寄存器( RCX ,RDX,R8和R9 )传递的,剩下的参数是通过堆栈来传递的。虽然前4个参数在寄存器中传递调用约定要求在栈上分配空间(这就是所谓的Home Space) 。由于Buffer是第6个参数,在Home Space和第五个参数之后。这意味着,在堆栈中断点断下后,堆栈中是这样的:

图片2.png

于是命令da poi(@ rsp+ 30)取出寄存器RSP中的值,加上30h刚好指向第6个参数,然后使用POI()引用该值(POI()类似于C语言中的*,返回一个指针大小的值)。最后,我们将这个地址传入da(显示ASCII)。我们可以在监视缓冲区数据,因为我们知道记事本存储的是纯文本而不是二进制文件。运行这个断点,并在记事本中保存一些文字,WinDbg的输出如下:

图片3.png

 使用这种技术,可以通过内核跟踪各种有趣的信息。

0×02更先进的命令用法

通过调试来操纵一些数据,经常可以获得更多的有意义的结果。一个很好的例子来说明其中的一些技术是系统服务描述表(SSDT)。该SSDT形成了所有的系统调用而产生的系统调用表。内核导出的SSDT是具有以下格式符号KeServiceDescriptorTable的结构:

typedef struct _KSERVICE_DESCRIPTOR_TABLE {

    PULONG ServiceTableBase;         // Pointer to function/offset table (the table itself is exported as KiServiceTable)

    PULONG ServiceCounterTableBase; 

    ULONG  NumberOfServices;         // The number of entries in ServiceTableBase

    PUCHAR ParamTableBase; 

} KSERVICE_DESCRIPTOR_TABLE,*PKSERVICE_DESCRIPTOR_TABLE;

 在Windows的32位版本,ServiceTableBase是一个指向函数指针数组的指针。在64位中稍微有点复杂,ServiceTableBase指向数组偏移值为32位处,全部都是相对于KiServiceTable在存储器中的表的位置,这使得可视化使用常用的内存显示命令(如dds)是不可能的。相反,我们将不得不使用一些WinDbgs更高级的命令在列表中迭代,数据操纵到一个更合适的形式。

让我们先来看看在内存中是如何偏移,我们可以用dd(显示DWORD)命令列出数组偏移值。使用/c 1选项指示调试器每行显示一个DWORD:

kd> dd /c 1 KiServiceTable

fffff800`02692300  040d9a00

fffff800`02692304  02f55c00

fffff800`02692308  fff6ea00

fffff800`0269230c  02e87805

fffff800`02692310  031a4a06

fffff800`02692314  03116a05

...

 

这些值通过左移4位并和其他数据编码,最终至少显示四位。为了形成我们需要的每个值的绝对存储器地址,需要右移4位移,并加上KiServiceTable的地址。我们希望在表的每个入口点都这样做,并输出与绝对地址相关联的符号。要做到这一点,我们可以使用.foreach命令迭代,使用.printf显示符号。下面是一个命令的实现,会对每个部分进行解释和说明: 

.foreach /ps 1 /pS 1 ( offset {dd /c 1 nt!KiServiceTable L poi(nt!KeServiceDescriptorTable+10)}){ .printf "%y\n", ( offset >>> 4) + nt!KiServiceTable }

 .foreach——该步骤指定每个令牌(在我们的例子中,我们使用dd命令来提供令牌)。这些参数/ PS 1和/ PS 1使得foreach每秒跳过一个令牌。我们这样做是因为dd命令输出<地址> <值>,我们只对当前值感兴趣。这些选项每次跳过令牌地址。

偏移——声明一个名为offset变量,该变量保存当前foreach迭代的令牌(当前的偏移值)

dd ——运行dd命令显示DWORD的偏移列表,这些将被.foreach进行迭代。/C 1确保每行只输出一个DWORD。Nt!KiServiceTable是我们将要显示的地址(这是偏移数组)。”L poi(nt!KeServiceDescriptorTable+10)”描述了要显示多少个值。在这种情况下,我们从指向我们的结构体NumberOfServices的KeServiceDescriptorTable开始处取出16个字节(10H),POI(),然后间接引用实际地址存储的值,例如表中有效入口的值。

.printf ——printf命令让我们执行格式化的打印。这里我们使用格式化字符串%y向给定的内存地址打印符号。当我们传递一个参数“(offset>>> 4)+!NT KiServiceTable”,这是当前偏移值右移4位,并添加到KiServiceTable的地址。我们使用>>>operator而不是>>operator,来保持的符号位,因为一些值是代表负偏移。

如果标志设置正确,上述命令的输出结果应该是这样:

kd> .foreach /ps 1 /pS 1 ( offset {dd /c 1 nt!KiServiceTable L poi(nt!KeServiceDescriptorTable+10)}){ .printf "%y\n", ( offset >>> 4) + nt!KiServiceTable }

nt!NtMapUserPhysicalPagesScatter (fffff800`02a9fca0)

nt!NtWaitForSingleObject (fffff800`029878c0)

nt!NtCallbackReturn (fffff800`026891a0)

nt!NtReadFile (fffff800`0297aa80)

nt!NtDeviceIoControlFile (fffff800`029ac7a0)

nt!NtWriteFile (fffff800`029a39a0)

结果应该显示可供用户态代码的主要内核系统调用一个合适的SSDT功能列表。 

0×03总结 

希望现在你开始有信心去使用调试器,并去探索操作系统。通过结合在这个系列文章你就可以开始梳理出Windows的内部工作原理,它可以用来帮助发现问题和漏洞,以及列出了一些列安全相关的技术和命令。配合这些知识以及调试器来了解Windows如何构建以及用户模式和内核模式交互的API层是很重要的。

0 0
原创粉丝点击