Skip to main content

Racing in kernel pool with CVE-2025-29824

·1687 words·8 mins
wh0isthatguy
Author
wh0isthatguy
big brother is watching you
Images not loading? Try accessing this site using a VPN.
cve

I. Overview
#

Trước đây mình từng đọc các bài blog phân tích tactic của các nhóm APT thì mình thấy có CLFS (Common Log File System) thường được sử dụng như 0day để leo quyền thành system từ đó load rootkit. Do mình thấy tỉ lệ CLFS xảy ra nhiều vuln như vậy nên mình sẽ phân tích nó. Vậy đầu tiên ta cần hiểu CLFS là gì. Dựa vào các docs từ microsoft hoặc các bài blog ta đại khái có thể hình dung ra nó liên quan đến cơ chế lưu giữ log trên windows và cho phép user có thể revert lại khi cần.

image
Từ đây mình thấy có các attack vector liên quan đến các yếu tố đọc ghi log, parse data trong log, handle các log operation như nào. Các yếu tố này sẽ được sử dụng để dễ dàng hơn trong việc diff check cũng như exploit

Thành phần handle các operation trên chính là clfs.sys driver. Do đó ta sẽ focus vào nó.

II. Vuln description
#

CVE mình chọn là CVE-2025-29824 sau khi đọc bài phân tích từ qianxinstarlab. CVE này là một bug use after free (UAF) từ đó lợi dụng để LPE. Bên cạnh đó CVE này chỉ có writeup exploit cũng như poc crash mà không có full script LPE.

image

Ngoài ra, nó có ảnh hưởng đa phần đến windows 11 (<=24H2), windows 10 và win server. Chi tiết về version bị lỗi có thể xem ở đây

image

III. Analysis
#

a. Diff check
#

Ở đây mình sẽ sử dụng winbindex để tìm kiếm các version bị lỗi và không bị lỗi để diffcheck

image
Phiên bản windows lỗi mình sử dụng là Windows 11 23H2 22631.2715 Sử dụng Bindiff để diff thì thấy đa phần các hàm đều giống nhau chỉ có hàm CClfsRequest::Close là thay đổi đáng kể
image
Trong đó, hàm này thêm các check và có flag là mitigation của Microsoft.
image
Mình đi trace cái mitigation trên (g_Feature_3334536505_56880687_FeatureDescriptorDetails) thì còn thấy nó được implement ở hàm CClfsRequest::Cleanup
image
Hàm cleanup trên thì được call ở CClfsRequest::Cleanup
image
Hàm CClfsRequest::Close khi patch được implement như sau:
image
Trong khi đó trước khi patch nó được implement như sau:
image
Ta dễ thấy khi patch, chương trình focus vào check field FsContext2 do đó field này chắc chắc xảy ra vấn đề gì đó. Trong đó, nó đa phần focus vào việc check FsContext2 có null không, nếu không sẽ free và clear field đó thành null. Đây cũng là một check điển hình hay gặp trong các bài heap trong CTF. Do đó, mình sẽ dựa vào binary trước khi patch để xem FsContext2 được sử dụng như thế nào. Ta có thể thấy như sau:

  • FsContext2 được free CClfsRequest::Cleanup
  • Để có thể tạo ra bug UAF như CVE description, FsContext2 phải được sử dụng ở đâu đấy sau khi free

Từ đây ta sẽ focus làm sau để trigger ra được CClfsRequest::Cleanup trước. Trace ngược lại thì nó sẽ được thực thi khi có IO Request gửi đến trong ClfsDispatchIoRequest

image
Trong đó, theo mình tìm hiểu IO request như này sẽ được tạo ra khi ta gọi CloseHandle ở usermode với handle là handle của clfs chúng ta tạo ra. Ngoài ra, nếu ta set breakpoint ở 2 hàm CClfsRequest::CleanupCClfsRequest::Close thì hàm Cleanup sẽ được call trước rồi mới đến Close. Từ đây, ta thấy flow UAF như sau:

  • User gọi CloseHandle
  • Cleanup thực thi và free FsContext2
  • Tìm cách malloc lại chunk FsContext2 và trigger tương ứng
  • Close thực thi

Hay nói cách khác, dù có UAF, ta phải tìm cách race trong khoảng thời gian giữa Cleanup và Close mới có thể LPE thành công.

b. Exploitation
#

Vậy ta cần trigger qua hàm nào, khi reverse một hồi ta sẽ thấy nó FsContext2 được sử dụng khá nhiều ở ClfsMgmtDispatchIo

image
Ngoài ra, dựa vào bài phân tích từ starlab, có thể trigger thông qua các hàm sau:

  • CClfsRequest::ReserveAndAppendLog()
  • CClfsRequest::WriteRestart()
  • CClfsRequest::ReadArchiveMetadata()
  • CClfsManagedLogClientUser::ReadNotification()

Trong đó ở đây mình sẽ sử dụng hàm ReadNotification do thấy nó dễ nhất. Để call được hàm này, đầu tiên nó sẽ deference FsContext2 ở offset nào đấy và check

image
Sau khi nó sẽ tiếp tục defer đến và check tiếp các giá trị, sau đó call đến địa chỉ trong đó
image
Dễ thấy, nếu ta có thể control được chunk FsContext2 này thì có thể dễ dàng call đến địa chỉ bất kỳ ta muốn. Từ đây ta có primiative đầu tiên: arbitrary call

Để LPE, ta cần ghi token của process hiện tại thành system, hoặc ghi đè token->privilege của process hiện tại. Mà để ghi được như vậy ta cần leak địa chỉ kernel base để call địa chỉ ta muốn.

image

Đối với các Windows version < 24H2, để leak địa chỉ đơn giản có thể thực thi qua các API

image
Đối với windows version >=24H2, microsoft họ đã aware chuyện này và fix nên để leak kernel base buộc phải sử dụng Prefetch (bug hardware level). Đối với các địa chỉ khác như pool, token,… buộc phải có primiative để leak hoặc phải tìm cách bypass mitigation trên. Do mình sử dụng trên win 11 23H2 nên các địa chỉ trên có thể leak dễ dàng. Từ đây mình có primiative thứ 2: arbitrary read

Từ đây mình có thể call đến địa chỉ mình muốn, địa chỉ mình chọn ở đây là RtlSetAllBits, hàm này sẽ memset địa chỉ mình muốn với len thành 0xff hết. Mình sẽ memset _TOKEN->privileges thành 0xff hết để enable hết bit privilege trong đó từ đó có quyền system

image
Để tính offset cho stable cũng đơn giản, mình sẽ load ntoskrnl.exe vào memory sau đó trừ đi so với base để ra offset rồi cộng vào địa chỉ leak là được
image

Ta sẽ tóm tắt exploit như sau:

  • Leak địa chỉ cần thiết
  • Gọi CloseHandle
  • FsContext2 được free
  • Race và spray để control được FsContext2 và call RtlSetAllBits Trong đó, bước quan trọng và khó nhất chính là bước race để spray

c. Racing in kernel pool
#

Chunk FsContext2 có size là 0x120 và có structure như sau

image
Do pool ở kernel được sử dụng chung, do đó ta cần tìm cách tạo được chunk size trên ở process nào cũng được. Dựa theo blog này, mình thấy có thể spray bằng 2 hướng: thông qua pipe hoặc socket. Ngoài ra pipe thường được sử dụng vì nó dễ hơn. Trong đó để tạo được chunk 0x120 trong Nonpage pool thông qua ta cần ghi data 0xe0 vào pipe do trước đó còn metadata và header. Ta có thể control 0xe0 data trong đó
image
Nếu thử ghi 0xe0 byte như vậy vào pipe ta được chunk như sau:
image
Tiếp theo, ta cần tìm hướng để spray. Dựa vào writeup ở qianxin, tác giả tạo 2 thread:

  • Thread 1: alloc trước N pipe và ghi vào pipe đó để kernel allocate N chunk 0x120 byte, sau đó read M pipe trên xen kẻ để free n chunk trên, sau đấy create K clfs handle với mục đích cho FsContext2 vô tính sử dụng lại 1 trong các chunk trong M pipe được free trên. Sau đó CloseHandle các clfs handle trên và write lại M pipe nhằm allocate lại chunk FsContext2 trên
    image
  • Thread 2: thread này đơn giản hơn, chỉ việc gọi DeviceIoctl liên tục để race
    image
  • Trong đó các giá trị N M K, được tune lại tuỳ thuộc vào version windows. Ngoài ra để tăng success rate, mỗi thread sẽ được chạy ở 1 cpu

Mình cũng làm tương tự lại như trên nhưng trong quá trình debug, mình chỉ lâu lâu mới trigger được 1 lần, các lần còn lại DeviceIoControl đều thực hiện trước sau đó mới free, dẫn đến UAF không được trigger. Mất một khoảng thời gian khá lâu mình mới để ý nguyên nhân là do field Overlapped trong DeviceIoControl. Lúc đầu mình để field này là null nên dẫn đến thread này sẽ wait nó thực thi xong, dẫn đến rate cực kỳ thấp. Ngược lại nếu field này được setup, thì nó sẽ được return liền. Khi setup xong, rate trigger được hầu như là 100%

image

Ngoài ra, khi ta gọi free, nó sẽ không free ngay lập tức mà được đưa vào lookaside list, khi vượt quá len list này là 4 thì nó mới free. Do đó ta cần free trước 4 handle.

image

Sau khi mình overwrite privileges của process hiện tại, mình đã có quyền ngang system nên mình tạo clean shell bằng cách chạy cmd.exe bằng cách inherit token của winlogon.exe dựa trên code snippet ở đây

image

d. The patch
#

Trước update, refcount của FsContext2 đầu tiên sẽ được tăng lên 2 ở CClfsLogCcb::AddRef sau đó khi thực thi CClfsLogCcb::Cleanup refcount sẽ được giảm về 1 và cuối cùng được free trong CClfsLogCcb::Release

image

Sau update, hàm CClfsLogCcb::Cleanup thêm đoạn check nếu có mitigation thì nó sẽ không được release dẫn đến refcount được giữ nguyên từ đó nó sẽ không được free ngay trong cleanup mà được free sau đó ngay close. Điều này dẫn đến UAF không còn tồn tại.

image

III. Conclusion
#

Khi thực thi ý tưởng trên mình đã có được shell với quyền system

image
image

Các kỹ thuật mình dùng để leak và spray này dễ bị AV flag là malware, tuy vẫn có cách để bypass behavior và heuristic detect trên AV nhưng vì mục đích poc nên ta sẽ dừng ở đây. Trong quá trình mình race, mình đã thử tạo rất nhiều pipe để spray với mục đích race đúng 1 lần duy nhất. Sau quá trình debug mình thấy nó khá unstable hơn so với việc cho nó try nếu loop này không được thì qua loop khác. Số lượng pipe giảm xuống và vẫn đảm bảo được độ stable Exploit LPE được test stable trên Win 10 19045.4651 và Win 11 22631.2715

IV. References:
#