使用 Ghidra 静态分析 Windows 内核驱动

作者:lu4nx@知道创宇404积极防御实验室

日期:2021年1月25日

加载 PDB 符号文件

没有加载符号文件,很多函数是显示不出函数名的。如果本地有符号文件(比如用

WinDbg 时已经下载),那直接在"File"菜单选择"Load PDB File",浏览目录找到

.pdb 或 .xml 文件即可。

如果本地没有,Ghidra 也支持直接从微软服务器下载:

  1. 点"File"菜单,选择"Download PDB File...";

  2. 弹框确认下载格式是 PDB 还是 XML,选择 PDB;

  3. 选择本地保存路径;

  4. 在配置"Symbol Server URL"时,点"Choose from known

    URLs"按钮,选择微软官方服务器;

  5. 点"Download from URL"即可。

配置 Data Type

Ghidra 没有内置 WDK 的数据类型,在转换成 C

代码时无法更改变量类型,对分析驱动文件来说非常不方便。这个问题很早前就有网友在官方仓库提过

Issue,但是官方至今未支持(参考:https://github.com/NationalSecurityAgency/ghidra/issues/184)。

好在有其他网友提供了 WDK

的数据类型文件,下载地址:https://github.com/0x6d696368/ghidra-data/tree/master/typeinfo

.gdt 是 Data Type 的数据文件,对于分析内核驱动只用根据处理器位数下载

ntddk_32.gdt 或 ntddk_64.gdt 即可。

以 ntddk_64.gdt 为例,下载以后,在"Data

Type"窗口点右上角的"▼"按钮,选择"Open File Archive..",找到并确认

ntddk_64.gdt 文件,如下图:

然后在列表中选中"ntddk_64",点右键,选择"Apply Function Data

Types"即可。

反汇编驱动文件

在逆向驱动时,如果驱动文件本身没有符号文件,就在"Symbol

Tree"窗口的"Functions"中找到,entry 函数,这就是驱动入口点。Windows

驱动的入口函数定义如下:

NTSTATUS DriverEntry(

IN PDRIVER_OBJECT DriverObject,

IN PUNICODE_STRING RegistryPath

){

}

入口函数有两个参数,不过有时候因为编译器的优化,Ghidra

反编译出的入口代码可能长这个样子:

void entry(longlong param_1,longlong param_2)

{

FUN_14000502c();

FUN_140001000(param_1,param_2);

return;

}

上面真正的入口函数应该是 FUN_140001000,进入

FUN_140001000,示例代码如下:

undefined8 FUN_140001000(longlong param_1,longlong param_2)

{

DbgPrint("hello world\n");

if (param_2 != 0) {

DbgPrint("hello world, RegistryPaht:%wZ\n",param_2);

}

if (param_1 != 0) {

*(code **)(param_1 + 0x68) = FUN_140001060;

}

return 0;

}

现在就可以根据函数原型定义来修改参数类型了,比如在变量 param_1

上点右键,选择"Retype Variable",然后更改为 PDRIVER_OBJECT

类型,按这个方法,依次修改两个参数的参数类型和变量名,再将函数名改为"DriverEntry"、返回值改为

NTSTATUS,最终修改后如下:

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject,PUNICODE_STRING RegistryPath)

{

DbgPrint("hello world\n");

if (RegistryPath != (PUNICODE_STRING)0x0) {

DbgPrint("hello world, RegistryPaht:%wZ\n",RegistryPath);

}

if (DriverObject != (PDRIVER_OBJECT)0x0) {

DriverObject->DriverUnload = FUN_140001060;

}

return 0;

}

这里可以注意一个细节,修改第一个参数的类型以前,这句代码长这样:

*(code **)(param_1 + 0x68) = FUN_140001060;

修改以后:

DriverObject->DriverUnload = FUN_140001060;

Ghidra 根据类型将后面的成员变量给自动修正了。

如果结构体成员指向的是另一个结构体时,Ghidra 不会递归修正,比如:

puVar11 = CdpFindEaBufferItem(*(uint **)((longlong)&irp->AssociatedIrp + 4),"attach")

第一个参数的值在 IRP->AssociatedIrp 偏移 4 的位置,这时我们可以借助

WinDbg 来搞清楚,比如这个例子中 irp 变量对应的是 IRP 结构,用 WinDbg

查看 IRP 结构:

1: kd> dt nt!_irp /r

+0x000 Type : Int2B

+0x002 Size : Uint2B

...省略...

+0x018 AssociatedIrp : <anonymous-tag>

+0x000 MasterIrp : Ptr64 _IRP

+0x000 Type : Int2B

+0x002 Size : Uint2B

+0x004 AllocationProcessorNumber : Uint2B

...省略...

注意 dt 命令要加 /r 参数,才能递归列出每个成员。

在这里可以看到 IRP->AssociatedIrp 其实是另外一个 PIRP 类型,+ 4

对应的是 IRP 结构体的 MasterIrp 成员变量。

参考

《Methodology for Static Reverse Engineering of Windows Kernel

Drivers》

https://posts.specterops.io/methodology-for-static-reverse-engineering-of-windows-kernel-drivers-3115b2efed83

以上是 使用 Ghidra 静态分析 Windows 内核驱动 的全部内容, 来源链接: utcz.com/p/199842.html

回到顶部