CVE-2025-13032: Khai thác lỗ hổng double-fetch trong driver Avast để chiếm quyền SYSTEM trên Windows 11
Nhóm nghiên cứu SAFA công bố phần hai của nghiên cứu về CVE-2025-13032, một lỗ hổng double-fetch trong driver kernel của Avast Antivirus. Bài viết mô tả chi tiết cách họ biến lỗi tràn bộ đệm pool có kiểm soát thành primitive đọc/ghi kernel tùy ý, rồi leo thang đặc quyền lên SYSTEM thông qua đánh cắp token.

CVE-2025-13032 là lỗ hổng double-fetch trong driver kernel của Avast Antivirus, được nhóm nghiên cứu SAFA phát hiện và khai thác thành công trên hệ thống Windows 11 đã cập nhật đầy đủ. Phần hai của nghiên cứu này trình bày chi tiết cách họ chuyển lỗi tràn bộ đệm paged pool có kiểm soát thành primitive đọc/ghi kernel tùy ý, rồi leo thang đặc quyền lên SYSTEM.
Đây là một trong những case study đáng chú ý nhất về khai thác lỗ hổng kernel trên Windows trong thời gian gần đây, đặc biệt khi nó nhắm vào một phần mềm diệt virus phổ biến toàn cầu và cả tại Việt Nam.
Bối cảnh và cách khai thác lỗ hổng
Mô tả CVE-2025-13032 trong Avast Antivirus
Lỗ hổng CVE-2025-13032 là một double-fetch — kỹ thuật tấn công mà kẻ tấn công thay đổi dữ liệu giữa hai lần kernel đọc cùng một vùng nhớ từ userland. Trong đoạn mã dễ bị tổn thương, kernel driver của Avast đọc trường Length của cấu trúc _UNICODE_STRING nhiều lần: lần đầu để cấp phát buffer qua ExAllocatePoolWithTag, lần thứ hai để thực hiện memmove.
Nếu kẻ tấn công thay đổi giá trị Length từ một số nhỏ (an toàn) sang một số lớn (ví dụ 0x1000) giữa hai lần đọc, kernel sẽ copy nhiều byte hơn kích thước buffer đã cấp phát, gây ra tràn bộ đệm pool. Chiến thuật là chạy một luồng liên tục đảo giá trị Length, trong khi luồng chính gọi IOCTL dễ bị tổn thương trong vòng lặp. Cửa sổ race rất hẹp nhưng có thể thắng ổn định sau một số lần thử nhất định.
Điểm mấu chốt của lỗ hổng này là cả kích thước cấp phát, kích thước tràn và nội dung tràn đều nằm trong tầm kiểm soát của kẻ tấn công — những điều kiện lý tưởng để khai thác.
Tại sao chọn đối tượng I/O Ring?
Mục tiêu của nhóm nghiên cứu là đối tượng IORing (_IORING_OBJECT), cụ thể là mảng RegBuffers — nơi lưu danh sách các con trỏ tới buffer đã đăng ký. Có bốn lý do chính:
RegBuffersnằm trong PAGED_POOL, khớp trực tiếp với vùng nhớ xảy ra lỗi tràn.- Kích thước cấp phát hoàn toàn do người dùng kiểm soát: đăng ký N buffer tạo ra mảng N con trỏ, mỗi con trỏ 8 byte — rất lý tưởng cho heap spray.
- Chỉ cần sửa một con trỏ duy nhất trong mảng là đã có primitive đọc/ghi kernel tùy ý, không cần đụng đến cấu trúc phức tạp.
- Kỹ thuật này đã được công bố công khai trước đó, xác nhận tính khả thi.
Khi đăng ký buffer qua IoRingRegisterBuffers, kernel tạo mảng con trỏ RegBuffers trỏ tới các cấu trúc _IOP_MC_BUFFER_ENTRY, mỗi cấu trúc chứa trường Address mà kernel dùng làm đích I/O. Vì việc kiểm tra tính hợp lệ chỉ diễn ra lúc đăng ký, nếu ta chuyển hướng một entry RegBuffers sang một cấu trúc giả do mình kiểm soát trong userland, kernel sẽ dereference thẳng vào bộ nhớ người dùng.
Điều này khả thi vì Windows không triển khai SMAP (Supervisor Mode Access Prevention) — cơ chế lẽ ra ngăn kernel truy cập trực tiếp vào bộ nhớ userland.
Từ đây, hai thao tác IORing trở thành primitive đọc/ghi:
IoRingReadFileđọc từ file và ghi vàoRegBuffers[i].Address→ primitive ghi kernel tùy ý.IoRingWriteFileđọc từRegBuffers[i].Addressvà ghi ra file → primitive đọc kernel tùy ý.
Chiến lược heap spray
Kích thước cấp phát RegBuffers là N × 8 byte, có thể rơi vào backend LFH hoặc VS tùy theo N. Nhóm nghiên cứu chọn N sao cho cấp phát rơi vào LFH, nơi việc chọn slot được ngẫu nhiên hóa nên không thể đặt chính xác vị trí.
Chiến lược là làm ngập pool bằng số lượng lớn cấp phát RegBuffers, sau đó giải phóng một phần để tạo các "lỗ hổng" có kích thước phù hợp, rồi kích hoạt lỗ hổng để cấp phát bị tràn rơi vào một trong các lỗ đó và ghi đè entry kề cận. Quy trình gồm bốn bước:
- Cấp phát số lượng lớn cấu trúc
RegBuffers. - Giải phóng một phần trong đó.
- Cấp phát
_UNICODE_STRINGcủa ta. - Kích hoạt tràn bộ đệm cùng lúc.
Khi mảng RegBuffers bị ghi đè thành công, primitive đọc/ghi kernel tùy ý đã sẵn sàng.
Rò rỉ địa chỉ kernel qua MDL
Sơ đồ khai thác IORing trong kernel Windows
Sau khi có primitive đọc/ghi, ta cần một địa chỉ kernel làm đích — cụ thể là địa chỉ _EPROCESS của chính tiến trình ta, phục vụ việc đánh cắp token SYSTEM. Vì địa chỉ kernel được ngẫu nhiên hóa (KASLR), ta phải rò rỉ chúng.
Khi kernel thao tác an toàn trên buffer userland, nó tạo một MDL (Memory Descriptor List) để ghim các trang vật lý. MDL lưu con trỏ tới _EPROCESS của tiến trình sở hữu vùng nhớ trong trường Process. Vì BufferEntry giả của ta nằm trong userland, ta đọc được trường Mdl trực tiếp từ tiến trình mình, rồi dùng primitive đọc để dereference MDL và lấy ra trường Process — cho ta con trỏ _EPROCESS hợp lệ.
Quy trình rò rỉ gồm bốn bước:
RegBuffers[0]trỏ tới_IOP_MC_BUFFER_ENTRYgiả ở địa chỉ userland đã biết.- Kích hoạt thao tác IORing — kernel tạo MDL cho buffer userland và ghi con trỏ vào trường
Mdlcủa entry giả. - Đọc trường
Mdltrực tiếp từ userland, không cần primitive kernel. - Đặt
Addresscủa entry giả thành địa chỉ MDL đó, kích hoạt thao tác khác và đọc trườngProcess.
Xử lý sự cố trước khi leo thang đặc quyền
Cấu trúc pool chunk và trường ProcessBilled
Trước khi leo thang, cần sửa trạng thái đã bị hỏng, nếu không hệ thống sẽ gặp màn hình xanh (BSOD) khi giải phóng tài nguyên.
Vấn đề ProcessBilled: trong quá trình tràn, trường ProcessBilled trong header pool chunk bị ghi đè. Giá trị này là con trỏ bị làm rối tới EPROCESS, tính theo công thức:
@EPROCESS ^ ChunkAddress ^ ExpPoolQuotaCookie
Để tính lại, nhóm nghiên cứu dùng một đối tượng IORing thứ hai chưa bị hỏng: đọc địa chỉ RegBuffers từ nó và giá trị ProcessBilled từ header chunk liền trước, rồi đảo ngược công thức để tìm Cookie. Sau đó tính giá trị ProcessBilled đúng cho chunk bị hỏng và ghi trả lại bằng primitive ghi.
Tham chiếu Buffer Entry: khi rò rỉ, kernel giữ tham chiếu tới entry buffer nằm trong userland, gây crash lúc teardown. Giải pháp là giải phóng đăng ký RegBuffers để kernel dọn MDL liên quan.
Giải phóng buffer người dùng: vì đã sửa một buffer entry, kernel sẽ cố giải phóng con trỏ userland khi đóng đối tượng. Cách xử lý là tăng reference count của entry giả để kernel không bao giờ giải phóng con trỏ đó.
Đánh cắp token SYSTEM
Để leo thang đặc quyền, nhóm nghiên cứu dùng primitive đọc để duyệt danh sách liên kết _EPROCESS nhằm tìm tiến trình SYSTEM và đọc trường Token của nó. Sau đó dùng primitive ghi để ghi đè trường Token của _EPROCESS thuộc về tiến trình ta bằng token SYSTEM — qua đó có được đặc quyền cao nhất trên hệ thống.
Kết luận và khuyến nghị
Quá trình leo thang đặc quyền lên SYSTEM
Nghiên cứu này cho thấy một lỗ hổng double-fetch tưởng chừng nhỏ trong driver của một phần mềm bảo mật lại có thể dẫn tới toàn bộ chuỗi khai thác: từ tràn paged pool có kiểm soát → primitive đọc/ghi kernel tùy ý → rò rỉ _EPROCESS qua MDL → sửa header pool để tránh crash → đánh cắp token SYSTEM.
CVE-2025-13032 đã được vá. Người dùng Avast — và bất kỳ phần mềm bảo mật nào hoạt động ở tầng kernel — nên đảm bảo sản phẩm của mình được cập nhật phiên bản mới nhất. Với người dùng Việt Nam, các phần mềm diệt virus như Avast được cài đặt khá phổ biến, nên việc kiểm tra bản cập nhật định kỳ là bước phòng vệ quan trọng.
Một điểm đáng chú ý về mặt kỹ thuật: kỹ thuật khai thác mô tả trong bài này sẽ không còn hiệu quả trên các bản Windows mới — khi kernel và driver đã chuyển sang dùng user-mode accessors để xác minh từng lần truy cập vùng nhớ userland. Đây là một lớp phòng vệ quan trọng giúp giảm bề mặt tấn công của các driver kernel trong tương lai.
Bên cạnh đó, các cơ chế bảo vệ hiện đại như Pool Segment Heap kể từ Windows 10 19H1 cũng phần nào làm phức tạp hóa việc khai thác, dù như nghiên cứu này chứng minh, chúng không đủ để ngăn chặn hoàn toàn.


