Windows驱动程序逆向工程方法

Windows驱动程序逆向工程方法

通过这篇博文,我想总结一下我长达一年的 Windows 驱动程序研究;分享并详细介绍我自己的逆向工程 ( WDM ) Windows 驱动程序方法,找到一些可能存在漏洞的代码路径并了解它们的可利用性。我试图让它尽可能“对新手友好”,记录我在研究期间通常执行的所有步骤,并为读者提供奖励练习。

设置实验室

过去,为内核调试设置实验室是管道、波特、缓慢和奇怪的 VMware 配置的痛苦,而现在它很容易,只需拥有两台机器:

  1. Debugger:安装了最新版本的WinDbg Preview的物理 Windows 操作系统机器(旧版 WinDbg 也可以)。
  2. Debuggee:安装在您首选的虚拟机风格(VMwareHyper-V、VirtualBox)上的 Windows 操作系统的副本;NAT 或桥接网络配置很好。

调试符号

  1. 在调试器机器上,创建一个名为_NT_SYMBOL_PATH.
  2. 将此新变量的值设置为srv*c:\symbols*http://msdl.microsoft.com/download/symbols
    确保没有前导/尾随空格。
Windows驱动程序逆向工程方法

3.重新启动机器。

4.打开 WinDbg,加载“calc.exe”并在 WinDbg 命令栏中键入以下内容:

x kernel32!IoCallDriver
x ntdll!*alloc*
!peb

等待命令输出;这取决于您的互联网连接速度,因为上述命令将触发符号kernel32ntdllDLL 的下载。

检查!peb命令是否报告了一些有意义的输出(没有错误消息)。

输出应该是这样的:

Windows驱动程序逆向工程方法

远程内核调试

检索调试器机器的 IP 并记下它 ( ipconfig /all)。

Debuggee – 设置远程内核调试

  1. 管理员命令提示符下运行以下命令:
bcdedit /dbgsettings NET HOSTIP:<DEBUGGER_IP> PORT:50000

示例 – 设置调试器机器 IP 地址:

bcdedit /dbgsettings NET HOSTIP:192.168.1.1 PORT:50000
  1. 运行bcdedit /dbgsettings;确认设置并复制“key”值。
  2. 运行bcdedit /debug on以启用调试。您应该会返回“操作成功完成”消息。
  3. 关闭 Debuggee 机器。
Windows驱动程序逆向工程方法

调试器——尝试连接

  1. 打开WinDbg。
  2. 配置 WinDbg 以侦听远程内核调试连接:“ File -> Attach to Kernel -> Net tab ”;配置如下:
Port: 50000
Key: <insert the key taken from the debuggee machine>
Target: <leave blank>
Click OK
Windows驱动程序逆向工程方法

3.结果应该是一条沿着这条线的调试消息:

Using NET for debugging
Opened WinSock 2.0
Waiting to reconnect...
  1. 启动 Debuggee VM。
  2. 等待 WinDbg 显示类似以下消息:
Connected to target 192.168.1.9 on port 50000 on local IP 192.168.1.1.
You can get the target MAC address by running .kdtargetmac command.
Connected to Windows 10 18362 x64 target at (Wed Dec 15 10:53:59.166 2021 (UTC + 1:00)), ptr64 TRUE
Kernel Debugger connection established.

************* Path validation summary **************
Response                         Time (ms)     Location
Deferred                                       srv*
Deferred                                       srv*c:\symbols*http://msdl.microsoft.com/download/symbols
Symbol search path is: srv*;srv*c:\symbols*http://msdl.microsoft.com/download/symbols
Executable search path is: 
Windows 10 Kernel Version 18362 MP (2 procs) Free x64
Product: WinNt, suite: TerminalServer SingleUserTS
Edition build lab: 18362.1.amd64fre.19h1_release.190318-1202
Machine Name:
Kernel base = 0xfffff804`06000000 PsLoadedModuleList = 0xfffff804`06443290
Debug session time: Wed Dec 15 10:53:58.578 2021 (UTC + 1:00)
System Uptime: 0 days 0:00:35.310
KDTARGET: Refreshing KD connection

WinDbg 可能(也可能不会)在启动时破坏调试对象。如果是这样,请点击左上角的绿色“开始”按钮(有时需要点击 2-3 次)。

调试器——测试连接

  1. 在 WinDbg 中单击“中断”。通常的 “*BUSY* Debuggee is running...” 消息应该替换为命令提示符 “0: kd>”
  2. 运行.reload以加载 MS 符号。(这需要一些时间)。
  3. 前面的命令完成后,运行该lm命令,您应该会返回 Debuggee 上加载的模块(驱动程序)列表。
0: kd> .reload
Connected to Windows 10 18362 x64 target at (Wed Dec 15 11:13:08.268 2021 (UTC + 1:00)), ptr64 TRUE
Loading Kernel Symbols
.......

Press ctrl-c (cdb, kd, ntsd) or ctrl-break (windbg) to abort symbol loads that take too long.
Run !sym noisy before .reload to track down problems loading symbols.

........................................................
................................................................
.................................................
Loading User Symbols

Loading unloaded module list
...Unable to enumerate user-mode unloaded modules, Win32 error 0n30
0: kd> lm
start             end                 module name
ffffc79c`2bc00000 ffffc79c`2bfa7000   win32kfull   (deferred)             
ffffc79c`2bfb0000 ffffc79c`2c264000   win32kbase   (deferred)             
ffffc79c`2c270000 ffffc79c`2c2b8000   cdd        (deferred)             
ffffc79c`2c2d0000 ffffc79c`2c35c000   win32k     (deferred)             
fffff804`05f5d000 fffff804`06000000   hal        (deferred)             
fffff804`06000000 fffff804`06ab2000   nt         (pdb symbols)          C:\ProgramData\Dbg\sym\ntkrnlmp.pdb\35A038B1F6E2E8CAF642111E6EC66F571\ntkrnlmp.pdb
[--SNIP--]

Windows 驱动程序 101

驱动程序与内核交互和/或控制硬件资源的软件(模块) 。可以将驱动程序视为 DLL,加载到内核地址空间,并以与内核相同的权限执行。

驱动入口

驱动程序有一个定义明确的入口点,称为DriverEntry. 它们没有主执行线程,它们只包含在某些情况下可由内核调用的代码。出于这个原因,驱动程序通常必须在 I/O 管理器中注册“调度例程”,以服务来自用户区或其他驱动程序的请求。

在分析驱动程序时,第一个也是最重要的任务是识别这些调度例程并了解它们如何与内核交互。

设备和符号链接

为了从用户模式访问,驱动程序必须创建一个DeviceName和一个符号链接。设备是让进程与驱动程序交互的接口,Symlink 是您在调用 Win32 函数时可以使用的别名

  • IoCreateDevice创建设备名称:\Device\VulnerableDevice
  • IoCreateSymbolicLink创建符号链接:\\.\VulnerableDevice

在对驱动程序进行逆向工程时,当您看到这两个 API 被连续调用时,您可以确定您正在查看驱动程序中实例化设备和符号链接的部分。大多数情况下它只发生一次,因为大多数驱动程序只公开一个设备。

调度例程

驱动程序根据在其公开的设备上调用的 Windows API 执行不同的例程。此行为由驱动程序开发人员通过结构的MajorFunctions函数指针数组)成员控制DriverObject

API 类似WriteFile,ReadFile并且DeviceIoControl在里面有一个对应的索引,MajorFunctions以便在 API函数调用之后调用相关的函数指针

假设驱动程序开发人员定义了一个名为“ MyDriverRead”的函数,并且他希望在进程调用ReadFile驱动程序设备上的 API 时调用它。在内部DriverEntry(或在它调用的函数中)他必须编写以下代码:

DriverObject->MajorFunctions[IRP_MJ_READ] = MyDriverRead;

使用此语句,驱动程序开发人员确保每次ReadFile在驱动程序设备上调用 API 时,MyDriverRead驱动程序代码都会调用“ ”函数。像这样的函数采用 Dispatch Routines 的名称。

就像MajorFunctions 一个大小有限的数组一样,我们可以分配给我们的驱动程序的调度例程也只有这么多。当开发人员想要更多自由时,用户模式功能DeviceIoControl就派上用场了。

DeviceIoControl 和 IOCTL 代码

里面有一个特定的索引MajorFunctions定义为IRP_MJ_DEVICE_CONTROL。在此索引处,存储了调度例程的函数指针(DeviceIoControl在驱动程序设备上的 API 调用之后调用)。这个函数非常重要,因为它的参数之一是一个 32 位整数,称为 I/O 控制代码 ( IOCTL )。

此 I/O 代码被传递给驱动程序,并使其根据通过DeviceIoControlAPI 传递给它的不同 IOCTL 执行不同的例程。本质上,index 处的调度例程IRP_MJ_DEVICE_CONTROL将在其代码中的某个点像下面的 switch case 一样工作:

switch(IOCTL)
{
    case 0xDEADBEEF:
        DoThis();
        break;
    case 0xC0FFEE;
        DoThat();
        break;
    case 0x600DBABE;
    DoElse();
    break;
}

这样,开发人员可以根据传递给驱动程序的不同IOCTL代码,使他的驱动程序调用不同的函数。

在对驱动程序进行逆向工程时,这种“代码指纹”很容易找到。了解哪个 IOCTL 导致哪个代码路径可以更轻松地分析和/或模糊驱动程序,同时查找漏洞。

IOCTL 代码由几个值组成,可以通过移位和屏蔽其位来解码:

IOCTL Value: 0x226003
Target Device:      UNKNOWN (0x22)
Target Function:     0x800
Access Mode:       FILE_READ_ACCESS
Communication Method:   METHOD_NEITHER
  • 目标设备:此值必须与驱动程序结构DeviceType成员中设置的值匹配。DEVICE_OBJECT
  • 目标功能:标识要由驱动程序执行的功能。
  • 访问模式:指示调用者在打开代表设备的文件对象时必须请求的访问类型。它可以具有以下系统定义的常量之一:
    • FILE_ANY_ACCESS:I/O 管理器为任何调用者发送 IRP,该调用者拥有代表目标设备对象的文件对象的句柄,而不管授予设备的访问权限如何。
    • FILE_READ_DATA:I/O 管理器仅为具有读取访问权限的调用者发送 IRP。
    • FILE_WRITE_DATA:I/O 管理器仅为具有写访问权限的调用者发送 IRP。
    • FILE_READ_DATAFILE_WRITE_DATAORred 一起。驱动程序还可以IoValidateDeviceIoControlAccess用来执行更严格的访问 (ACL) 检查。
  • 通信方法:指示系统将如何在调用者DeviceIoControl和处理 IRP 的驱动程序之间传递数据。它可以具有以下系统定义的常量之一:
    • METHOD_BUFFERED: 通常用于每个请求传输少量数据。
    • METHOD_IN_DIRECTor METHOD_OUT_DIRECT: 通常用于读取或写入必须快速传输的大量数据。
    • METHOD_NEITHER:I/O 管理器不提供任何系统缓冲区,也不对所提供的缓冲区执行任何类型的验证。IRP 提供指定的输入和输出缓冲区的用户模式虚拟地址,而DeviceIoControl无需验证或映射它们。这是最不安全的通信方式。

Windows 驱动程序逆向工程方法

让我们开始检查MSI Afterburner v.4.6.4.16117 Beta 4附带的驱动程序。与往常一样,您可以在我的GitHub 存储库中找到驱动程序以及 IDA 的项目 DB 和反编译的调度函数。

动因分析

执行驱动程序分析时,收集以下信息很重要:

  • 识别DriverEntry并确定 IRP 调度处理程序。
  • 确定驱动程序是否附加到另一个设备以过滤/拦截其 I/O 请求。如果是这样,目标设备是什么?
  • 确定DeviceName.
  • 识别所有 IOCTL 代码及其相应的功能。确定他们使用什么缓冲方法。
  • 尝试了解所有部分是如何组合在一起的。

在 IDA 中加载RTCore64.sys文件,我们应该看到以下代码块:

Windows驱动程序逆向工程方法

驱动入口

IDA 完全能够自动识别DriverEntry函数,并且鉴于这个驱动程序非常简单,我们也可以轻松地DeviceName从这个代码块中恢复。

设备名称

正如您从代码流(图表概述)中看到的那样,从DriverEntry块中只有一条路径可以遵循。该路径直接通向另一个IoCreateSymbolicLink调用 Windows API 的块。根据 Microsoft定义,需要IoCreateSymbolicLink两个参数:SymbolicLinkNameDeviceName(由 IDA 明确标记的参数)。

从静态分析的角度来看,为了提取DeviceName更复杂的驱动程序,通常足以“外部参照” IoCreateDevice“导入”选项卡下的 API 函数调用,找到它的第三个参数并将其追溯到定义的位置。

在调试驱动程序时,一切都会变得更容易,因为我们将能够设置断点、跟踪和查看寄存器内容以及内存内容。在处理严重混淆的代码时,它也是首选过程(因为DeviceName通常从字符串常量中“剥离”或“加密”)。

无论如何,继续我们的分析,我们可以看到,在 Windows API 调用之后,还有两个函数存在:

  1. sub_1143c:深入到这个sub,我们可以看到“Graph overview”窗口爆炸了,它比DriverEntry我们正在查看的功能块更复杂和“神秘”。

对于有经验的逆向工程师来说,检查几个开始的块,注意图形的形状(嵌套的 if-case/switch-case)并知道函数的地址用于填充DriverObject结构中的一些“字段”(查看并追溯rbx以下指令引用的寄存器:)mov [rbx+70h], rax足以将其标记为可能的调度例程。

Windows驱动程序逆向工程方法
  1. sub_11008:另一方面,这个 sub 几乎是不言自明的。查看被调用的 Windows API ( IoDeleteSymbolicLink, IoDeleteDevice),我们可以清楚地了解到,调用该函数是为了“销毁”驱动程序的设备;仅在卸载驱动程序时发生的操作。

让我们用一些有意义的名称( sub_1143c : DispatchDeviceControlsub_11008: )重命名新发现的潜艇,DriverUnload然后继续。

调度程序

让我们切入正题,所谓的 Dispatch Routine(DispatchDeviceControl函数)确实是负责根据通过DeviceIoControlAPI 传递给它的不同 IOCTL 执行不同例程的 Dispatch Routine。

IOCTL

检查其中一个块,沿着函数中第一个块之后的红线DispatchDeviceControl,我们可以看到以下几行:

mov eax, dword ptr [rax+(IO_STACK_LOCATION.Parameters+10h)]
mov r8d, 8000202Ch
cmp eax, r8d
ja sub_xxxx

上面条件检查的32位整数无非就是我们的第一个IOCTL代码。

我们已经知道我们可以解码回有意义的值:

  • IOCTL 代码0x8000202C
  • 地址0x1147A
  • 设备0x8000 <UNKNOWN>
  • 功能0x80B
  • 方法METHOD_BUFFERED
  • 访问FILE_ANY_ACCESS

现在,我们可以手动解码我们将遇到的所有 IOCTL 值,或者我们可以“懒惰”并使用我最近重构并移植到最新版本的 IDA 和 Python 的可爱工具。

驱动好友重装

Driver Buddy Reloaded是一个 IDA Pro Python 插件,可帮助自动化和加速一些繁琐的 Windows 内核驱动程序逆向工程任务(您可以在GitHub 上阅读有关它及其功能的更多信息)。

将 Driver Buddy Reloaded 安装到 IDA 的插件文件夹中并在继续之前重新启动 IDA。

在 Driver Buddy Reloaded 的出色功能中,我们将利用其自动驱动程序分析。使用CTRL+ALT+A快捷方式,我们将启动驱动程序分析。它会自动报告大量信息。

我们对以下位感兴趣:

  • DispatchDeviceControl常规。
  • 驱动程序的设备名称和符号链接。
  • 有趣的操作码和 Windows API 使用。

将光标定位在发现的DispatchDeviceControl例程的第一个块中,右键单击并选择“Driver Buddy Reloaded -> Decode all IOCTls in function”(或者使用CTRL+ALT+D快捷方式),下表将出现在“Output”视图中。

Driver Buddy Reloaded - IOCTLs
-----------------------------------------------
Address | IOCTL Code | Device           | Function | Method            | Access
0x1147A | 0x8000202C | <UNKNOWN> 0x8000 | 0x80B    | METHOD_BUFFERED 0 | FILE_ANY_ACCESS (0)
0x11492 | 0x80002000 | <UNKNOWN> 0x8000 | 0x800    | METHOD_BUFFERED 0 | FILE_ANY_ACCESS (0)
0x1149D | 0x80002004 | <UNKNOWN> 0x8000 | 0x801    | METHOD_BUFFERED 0 | FILE_ANY_ACCESS (0)
0x114A8 | 0x80002008 | <UNKNOWN> 0x8000 | 0x802    | METHOD_BUFFERED 0 | FILE_ANY_ACCESS (0)
0x114B3 | 0x8000200C | <UNKNOWN> 0x8000 | 0x803    | METHOD_BUFFERED 0 | FILE_ANY_ACCESS (0)
0x114BE | 0x80002010 | <UNKNOWN> 0x8000 | 0x804    | METHOD_BUFFERED 0 | FILE_ANY_ACCESS (0)
0x114C9 | 0x80002014 | <UNKNOWN> 0x8000 | 0x805    | METHOD_BUFFERED 0 | FILE_ANY_ACCESS (0)
0x114D4 | 0x80002018 | <UNKNOWN> 0x8000 | 0x806    | METHOD_BUFFERED 0 | FILE_ANY_ACCESS (0)
0x114DF | 0x8000201C | <UNKNOWN> 0x8000 | 0x807    | METHOD_BUFFERED 0 | FILE_ANY_ACCESS (0)
0x114E6 | 0x80002028 | <UNKNOWN> 0x8000 | 0x80A    | METHOD_BUFFERED 0 | FILE_ANY_ACCESS (0)
0x116F7 | 0x80000000 | <UNKNOWN> 0x8000 | 0x0      | METHOD_BUFFERED 0 | FILE_ANY_ACCESS (0)
0x11727 | 0x80002030 | <UNKNOWN> 0x8000 | 0x80C    | METHOD_BUFFERED 0 | FILE_ANY_ACCESS (0)
0x11732 | 0x80002034 | <UNKNOWN> 0x8000 | 0x80D    | METHOD_BUFFERED 0 | FILE_ANY_ACCESS (0)
0x1173D | 0x80002040 | <UNKNOWN> 0x8000 | 0x810    | METHOD_BUFFERED 0 | FILE_ANY_ACCESS (0)
0x11748 | 0x80002044 | <UNKNOWN> 0x8000 | 0x811    | METHOD_BUFFERED 0 | FILE_ANY_ACCESS (0)
0x11753 | 0x80002048 | <UNKNOWN> 0x8000 | 0x812    | METHOD_BUFFERED 0 | FILE_ANY_ACCESS (0)
0x1175E | 0x8000204C | <UNKNOWN> 0x8000 | 0x813    | METHOD_BUFFERED 0 | FILE_ANY_ACCESS (0)
0x11769 | 0x80002050 | <UNKNOWN> 0x8000 | 0x814    | METHOD_BUFFERED 0 | FILE_ANY_ACCESS (0)
0x11774 | 0x80002054 | <UNKNOWN> 0x8000 | 0x815    | METHOD_BUFFERED 0 | FILE_ANY_ACCESS (0)

所有 IOCTL 都可以方便地解码并以表格格式显示。

搜索易受攻击的代码路径

查看 Driver Buddy Reloaded 输出,我们还对列出的潜在危险操作码/Windows API/C++ 函数感兴趣。它们是漏洞利用的主要目标,也是我们漏洞研究的一些良好起点。

注意MmMapIoSpaceWindows APIrdmsrwrmsrs操作码,并尝试将它们追溯到其特定的 IOCTL 代码(到达其代码路径的代码)。

最终,您会发现以下匹配项:

  • rdmsr 0x11727 | 0x80002030 | <UNKNOWN> 0x8000 | 0x80C | METHOD_BUFFERED 0 | FILE_ANY_ACCESS (0)
  • wrmsrs 0x11732 | 0x80002034 | <UNKNOWN> 0x8000 | 0x80D | METHOD_BUFFERED 0 | FILE_ANY_ACCESS (0)

现在找到wrmsrs代码块;单击操作码旁边重新加载的 Driver Buddy 显示的地址应该很容易wrmsrs

[>] Searching for interesting opcodes...
- Found wrmsr in sub_1143C at 0x00011a7c

IDA 非常好,可以自动将您带到正确的街区。现在,设置可见节点颜色并双击指向块的箭头;焦点将改变,并显示另一个节点。给它上色,然后继续追溯并为通向wrmsrs操作码的所有节点上色。

最后,您应该有类似下图的内容(注意:由于 IOCTL 节点距离下一个节点太远,因此出于编辑目的对图像进行了编辑):

Windows驱动程序逆向工程方法

特定于模型的寄存器 (MSR)

特定于模型的寄存器(MSR) 是用于切换或查询 CPU 信息的寄存器。操作码会将寄存器的wrmsrs内容写入寄存器EDX:EAX指定的特定型号寄存器 (MSR) ECX

MSR 最有趣的地方在于,在现代系统中,MSR_LSTAR寄存器用于从用户模式到内核模式的系统调用转换。

从用户模式到内核模式的转换可以概括如下:

  1. 系统调用。
  2. 读取 MSR_LSTAR寄存器。
  3. 调用 MSR_LSTAR指针 (Ring-0)。
  4. 内核函数处理系统调用逻辑。

暴露的 WRMSR( __writemsr) 指令/wrmsrs操作码为我们提供了一个指针覆盖原语;该_LSTAR寄存器实际上保存了一个函数指针,在发出任何系统调用时都会调用它(并且它是从ring-0调用的)。

即使漏洞利用阶段超出了本文的范围,感兴趣的读者也可以使用msrexec 之类的工具包,将此类漏洞快速武器化为成熟的漏洞利用。

很好,现在我们有了通向潜在易受攻击的操作码的代码路径,让我们看看我们是否可以利用它。如果我们提供的任何输入UserBufferIn用作操作码的“参数” wrmsrs,填充 ECX、EDX、EAX 寄存器,我们将能够写入任意模型特定寄存器 (MSR),并可能实现本地特权在 Windows 内核 (ring-0 / NT AUTHORITY\SYSTEM)上下文中执行代码的升级 (LPE )

加载驱动程序

为了调试我们的RTCore64.sys驱动程序,我们应该使用 OSR 在线工具的OSRLOADER创建一个可以加载我们的驱动程序的“服务”。

在被调试机器上,启动 OSR Driver Loader 并配置如下:

Windows驱动程序逆向工程方法

现在按下“注册服务”和“启动服务”按钮,我们应该会收到一条对话框消息,让我们知道服务已成功启动。使用Process Hacker / Process MonitorWinObj仔细检查驱动程序是否正在运行;您应该能够在目录RTCore64下找到一个条目。GLOBAL??

Windows驱动程序逆向工程方法

与驱动互动

现在已经加载了驱动程序并且调试器机器已连接到我们的 WinDbg 调试器,您可能想知道我们如何与驱动程序交互。在这里,我们有两个选择:编写我们自己的“C/C++”包装器以与驱动程序交互,直接管理 IOCTL 代码、缓冲区和 Windows API 调用,或者使用我最近重构和升级的一个可爱的小工具。

IOCTLpus

IOCTLpus是一个开源 C# 应用程序,可用于发出DeviceIoControl带有任意输入的请求,其功能有点类似于 Burp Repeater。

以管理员权限启动 IOCTLpus 并配置如下:

  • 路径/GUID: \\.\RTCore64
  • IOCTL 代码: 70000
  • 人类 ACL: ANY_ACCESS
Windows驱动程序逆向工程方法

现在按下“发送”按钮,我们将返回“参数不正确”错误消息,因为我们使用的 IOCTL 代码对 RTCore64 驱动程序无效。

让我们用应该到达潜在易受攻击的操作码的代码(以前发现的)来改变它wrmsrs80002034.

现在,如果我们再次按下“发送”按钮,就会打印出同样的错误信息,为什么?让我们来了解一下!

在 WinDbg 中发出lmDvmRTCore64命令并找到 RTCore64 条目(请注意,由于 Windows 的 KASLR,您的机器上显示的地址会有所不同):

start end module name
fffff804`36cb0000 fffff804`36cba000 RTCore64 (deferred)

第一个地址是我们驱动程序在内存中的起始地址,复制它并进入“IDA -> Edit -> Segments -> Rebase Program”并粘贴地址(删除反引号`字符,你应该有类似的东西0xfffff80436cb0000:) . 现在您已经将 IDA 与驱动程序在实时系统中占用的内存空间“同步”了,您将能够使用 IDA 的地址在调试器中放置断点。

调度设备控制

在 IDA 中定位DispatchDeviceControl例程并检索第一个比较指令的地址 ( FFFFF80436CB146E – cmp byte ptr [rax], 14) 使用该地址在 WinDbg: 中放置一个断点bp 0xFFFFF80436CB146E

恢复执行并使用 IOCTLpus 重新发出请求,WinDbg 应该在断点处中断,我们应该能够调查导致我们的请求失败的原因。

如果你看一下 IDA 的图表,你会看到,从cmp byte ptr [rax], 14指令开始,有两个分支;如果您同时关注两个分支,您会看到其中一个分支出现在R8寄存器与 IOCTL 代码进行比较的块中,而另一个分支出现在函数的最后一个块中,然后是一个IofCompleteRequest调用。

Windows驱动程序逆向工程方法

从这里我们可以做出有根据的猜测并假设,如果我们无法通过cmp byte ptr [rax], 14检查,驱动程序将终止其例程并发送回错误消息。

IOCTL 80002034

接下来,我们将在负责处理wrmsrs操作码逻辑的块的开头设置一个断点,并检查我们是否触发它。bp FFFFF80436CB1732

Windows驱动程序逆向工程方法

我们还将UserBufferInput使用易于识别的模式更新 IOCTLpus,然后按“发送”:

Windows驱动程序逆向工程方法

这样,如果我们缓冲区的任何字段将被加载到内存中,或者寄存器将更容易被发现。

一旦我们到达断点,我们将能够看到一些有趣的值被加载到寄存器中:

  • RAX:包含我们传递给DeviceIoControl请求的 IOCTL 代码。
  • RDX:包含我们UserBufferInput(20h)的大小。
  • RBX:包含指向我们的指针UserBufferInput;取消引用 RBX 将向我们显示缓冲区的内容:
dd @rbx
ffff8086`e24f00c0 41414141 42424242 43434343 44444444
ffff8086`e24f00d0 45454545 46464646 47474747 48484848
  • R9:包含我们UserBufferOutput(20h)的大小。

好的,让我们进一步检查我们是否“登陆”到处理wrmsrs操作码逻辑的块中。

UserBufferIn – 要求和约束

后面的一些指令我们可以看到EDX寄存器的内容与 ; 的立即数进行比较Ch。asRDX包含大小,UserBufferInput我们应该更新它的大小,以通过此检查。

Windows驱动程序逆向工程方法

此时,我们应该看到缓冲区的第一个字段已加载到ECX:mov ecx, [rbx]中。
的值的副本ECX存储到EAX之前174h被减去:lea eax, [rcx-174h]
然后,将结果与值 2 进行比较;如果结果 <= 2,我们将退出例程:

cmp eax, 2
jbe short loc_FFFFF80436CB1A99

由于我们的第一个字段的内容是41414141h - 174h = 41413fcdh >= 2执行没有任何麻烦。此时将执行另一组操作:EAX加载 的内容ECX和值的相加结果3FFFFF80h。如果结果 <= 4,我们将退出例程。同样,我们对这个要求没有任何问题,因为41414141h+3FFFFF80h=814140c1h.

正如 Twitter 上的@AlexUnwinder所指出的(帽子提示),前面的两个算术运算将作为 MSR 地址范围的黑名单:0x174-0x1760xC0000080-0xC0000084; 该0x3FFFFF80值无非是-0xC0000080将编译器优化的代码从lea eax, [rcx+ 3FFFFF80h ]进入等价  eax, 0xC0000080手术。

wrmsrs 操作码

现在我们进入该例程的最后一段代码:

Windows驱动程序逆向工程方法

此处加载寄存器以充当wrmsrs操作码的“参数”。如果我们在wrmsrs指令之前中断,我们将能够看到我们感兴趣的所有寄存器的内容:

edx=42424242
eax=43434343
ecx=41414141

操作码会将寄存器的wrmsrs内容写入寄存器EDX:EAX指定的 64 位特定型号寄存器 (MSR) ECX。寄存器的内容EDX被复制到所选 MSR 的高 32 位,EAX寄存器的内容被复制到 MSR 的低 32 位。RAX, RCX, RDX在支持 Intel 64 架构的处理器上,忽略的高 32 位。

可利用性和结论

我们可以清楚地控制操作码的所有“参数”,wrmsrs但不幸的是,我们无法克服现有的限制;阻止我们将0xc0000082MSR Long System Target-Address Register – LSTAR)值加载到ECX寄存器中。无论如何,如果我们能够做到这一点,我们就会在 Windows 内核(ring-0 / NT AUTHORITY\SYSTEM)的上下文中获得任意代码执行。

您仍然可以对其进行测试,使 VM 崩溃,ECX检查通过后手动强制寄存器值。但是,在虚拟机上,您可能会遇到各种麻烦:

这就是为什么我总是建议在您发现可能的漏洞后确认针对物理机器设置的可利用性。

Windows驱动程序逆向工程方法

如果您还记得,我们还必须以管理员身份执行 IOCTLpus,然后才能与驱动程序交互(获取句柄),这个限制进一步阻止了我们的本地特权升级 (LPE) 漏洞。

奖金练习

如果您想对该驱动程序进行更多试验并提高您的逆向工程技能,请尝试了解包含MmMapIoSpace函数调用的块的逻辑;它比前面的例子复杂一点,但它是一个很好的练习。

  1. 到达MmMapIoSpace代码块需要什么 IOCTL 代码?
  2. 有哪些要求、约束和限制UserBufferInput
  3. MmMapIoSpaceAPI的定义是什么?它的参数是什么?
  4. 什么被用作MmMapIoSpaceAPI 的参数?它的调用约定是什么?我们控制哪些寄存器?我们可以用我们喜欢的任何值加载它们吗?
  5. 可以将其武器化为任意内存读取原语吗?
    • 是的?然后为它写一个漏洞利用:)。
    • 不?阻止利用的限制是什么?

提示

如果您不知道从哪里开始,请提供一些提示(前方剧透)

资源和参考

from

转载请注明出处及链接

Leave a Reply

您的电子邮箱地址不会被公开。