### 1. Giới Thiệu Tổng Quan :::success * Chuyện là mình có đang đọc về chương rootkit của sách Malware Analysis and Detection Engineering. Trong quá trình đọc thì mình thấy tác giả có giải thích về cách hoạt động của DKOM rootkit và sau đó demo cách hoạt động của nó bằng một sample có sẵn. * Nhưng tác giả chỉ giải thích ý tưởng hoạt động của DKOM rootkit theo một cách chung chung và chưa thuyết phục mình lắm :nerd_face:. Chính vì vậy, mình quyết định reverse lại sample của tác giả để hiểu sâu hơn về cách hoạt động của DKOM rootkit. * Link sách và sample mình sẽ để ở dưới phần mô tả :grin:. ::: ### 2. Tải Rookit Vào Kernel :::info * Mở đầu hàm **main()** của malware, ta thấy nó gọi lần lượt 3 API **FindResourceA()**, **LoadResource()**, và **LockResource()**. Bộ 3 API này dùng để trích xuất một resource nào đó từ .rsrc section của chính malware. Tiếp theo, nó gọi API **SizeOfResource()** để lấy độ lớn của resource. ::: ![](https://hackmd.io/_uploads/Bkn7y9hon.png) :::info * Tiếp theo, nó tạo một directory có đường dẫn là **C:\hidden** bằng API **CreateDirectoryA()**. Tiếp tục nó tạo một file có đường dẫn **C:\hidden\dkom.sys** bằng API **CreateFileA()**. Cuối cùng, nó ghi resource vừa tải từ .rsrc vào file này. Hmm, file vừa được tạo là một kernel driver :grin:. Nên ta có thể dễ đoán rằng nó là một rootkit ở mức kernel. * Sau khi đã có rootkit ở ổ cứng, malware tiếp tục gọi một hàm **RegisRootkit()**. Ta cùng tìm hiểu chức năng của hàm này nhoa :kissing:. ::: ![](https://hackmd.io/_uploads/ByL7l5hsn.png) :::info * Ban đầu đập vào mắt ta là hàm này gọi API **OpenSCManagerA()**, hàm này được sử dụng rất phổ biến trong quá trình ta muốn đăng kí một service nào đó ở Windows. * Tiếp theo, malware chèn rootkit vào kernel bằng cách đăng ký nó dưới dạng một driver của hệ thống. Việc này được thực hiện bằng cách gọi API **CreateServiceA()**. * Sau khi chèn rootkit vào kernel rồi thì ta phải bắt đầu chạy rootkit. Việc này được thực hiện bằng cách gọi API **StartServiceA()**. * Trong trường hợp rootkit đã có sẵn ở kernel thì ta không cần phải đăng kí nó nữa mà chỉ cần khởi động nó bằng API **OpenServiceA()**. Ta để ý giá trị tham số thứ 2 được truyền vào API này là **0xF01FF**. Giá trị này tương đương với việc yêu cầu tất cả các quyền truy cập có sẵn cho một service bao gồm cả quyền bắt đầu thực thi. ::: ![](https://hackmd.io/_uploads/r1rDwc2jh.png) :::info * Thông tin về giá trị này được mình tham khảo trên trang chủ chính thức của Microsoft. ::: ![](https://hackmd.io/_uploads/SJhjrk6o2.png) :::info * Ở thời điểm hiện tại thì rootkit đã được chạy ở kernel :grin:. Nhưng vẫn chưa xong, con malware lại tiếp tục tiếp tục thực hiện việc gì đó :nerd_face:. * Huh, nó gọi API **CreateFileA()** với mục đích để tương tác với một device có đường dẫn là \\\\.\\DKOM. Chắc chắn rằng đây là device do rootkit tạo ra rồi, không phải bàn cãi gì về việc này nữa :grin:. * Malware tiếp tục gọi API **GetCurrentProcessId()** để lấy PID của nó và sau đó nhảy đến **loc_4010F6**. ::: ![](https://hackmd.io/_uploads/Hkb2u5nsn.png) :::info * Tại **loc_4010F6**, nó thực hiện API **WriteFile()** để ghi 4 byte giá trị PID vào device do rootkit tạo ra. * Tiếp theo, ta cùng phân tích cơ chế hoạt động của rootkit để xem nó sẽ xử lý giá trị PID được gửi bởi malware này như nào nhé :kissing:. ::: ![](https://hackmd.io/_uploads/Bk3kFc2s2.png) ### 2. Chức Năng Của DKOM Rootkit :diamond_shape_with_a_dot_inside: **Chức năng hàm DriverEntry()** :::info * Mở đầu hàm **DriverEntry()** của rootkit, nó tạo một device với đường dẫn là **\Device\DKOM**. Và sau đó tạo một symbolic link đến device này bằng API **IoCreateSymbolicLink()**. * Đúng như mình dự đoán ở trên, device DKOM được tạo bởi rootkit :grin:. ::: ![](https://hackmd.io/_uploads/HJjCKc2jn.png) ![](https://hackmd.io/_uploads/SynkKyTih.png) :::info * Bởi vì mỗi loại IRP thì cần có một cách xử lý khác nhau nên khi một driver được tạo ra, ta phải định nghĩa các hàm xử lý cho các loại IRP khác nhau. Ví dụ driver sẽ xử lý read IRP và write IRP theo một cách khác nhau. * IRP là cấu trúc được tạo từ I/O manager, nó đại diện cho một I/O request được khởi tạo từ các process ở user space :smiley:. * Trong trường hợp của rootkit, nó gán tất cả các hàm xử lý IRP bằng hàm **OnStubDispatch()**. Tức là mọi loại IRP mà rootkit nhận được đều xử lý bằng hàm **OnStubDispatch()**. ::: ![](https://hackmd.io/_uploads/BygU5chon.png) :::info * Tiếp theo, rootkit thực hiện thay đổi Flags cho đối tượng **DeviceObject** tương ứng với device DKOM vừa tạo ra ở trên. * Cụ thể, nó loại bỏ cờ có giá trị 0x80 ra khỏi Flags và thêm cờ có giá trị 0x10 vào Flags. Vậy thì 0x80 và 0x10 ở đây tương ứng với cờ nào ? Và cờ đó có ý nghĩa gì :thinking_face:? ::: ![](https://hackmd.io/_uploads/BJybWgaj2.png) :::info * Các macro này được mình lấy trong file header **wdm.h**. Dựa vào hình ta có thể thấy 0x80 tương ứng với cờ **DO_DEVICE_INITIALIZING** và 0x10 tương ứng với cờ **DO_DIRECT_IO**. * Về lí do tại sao rootkit gỡ bỏ cờ DO_DEVICE_INITIALIZING thì thật sự mình không hiểu lắm, bạn đọc có thể comment để giải thích cho này cho mình nha :kissing:. * Trước khi giải thích về cờ DO_DIRECT_IO, thì mình sẽ giải thích một cách cơ bản về cơ chế mà driver tương tác dữ liệu với user buffer. Theo ta biết thì các process ở user space có thể trao đổi dữ liệu với các device ở mức hệ thống. Việc trao đổi dữ liệu giữa process và device được thông qua các driver nằm giữa chúng. Vì lí do đó nên driver có thể tương tác với user buffer trước khi gửi nó đến driver ở tầng dưới hoặc thực hiện API IoCompleteRequest() để thông báo với I/O manager rằng I/O request đã được xử lý xong. * Có 2 cơ chế chính để driver thực hiện việc này là Buffered I/O và Direct I/O. ::: ![](https://hackmd.io/_uploads/SkcRZgTih.png) :::info * Đối với Buffered I/O thì driver cấp phát một buffer trên RAM từ non-paged pool có độ lớn giống với user buffer được gửi từ process, nếu IRP là write IRP thì dữ liệu ban đầu của buffer trên RAM được sao chép từ user buffer. Non-paged pool tức là một vùng nhớ gồm nhiều page luôn hiện diện trong RAM, các page này không bị chuyển ra vùng swap space. Bạn nào học hệ điều hành rồi thì sẽ hiểu mình đang nói gì :grin:. * Tiếp theo, driver sẽ ánh xạ buffer vừa tạo vào trong kernel space dưới dạng là system buffer. Vậy thì lúc này thứ mà driver tương tác là system buffer, sau khi driver xử lý xong IRP thì những thay đổi từ system buffer được sao chép đến user buffer. ::: ![](https://hackmd.io/_uploads/SJxKtxToh.png) :::info * Sau khi quá trình tương tác được hoàn thành thì driver phải giải phóng system buffer để tránh lãng phí tài nguyên. * Tư duy một xíu thì ta có thể thấy rằng cơ chế Buffered I/O tỏ ra hiệu quả khi user buffer có độ lớn nhỏ. Bởi vì khi user buffer nhỏ thì quá trình sao chép sẽ diễn ra rất nhanh đúng chứ :sunglasses:. * Để dùng cơ chế Buffered I/O thì cần thêm cờ DO_BUFFRED_IO vào trường Flags của DeviceObject là được :smiling_imp:. ::: ![](https://hackmd.io/_uploads/HJ-U5gao3.png) :::info * Tiếp theo là Direct I/O, với cơ chế này thì không có bất kỳ một buffer nào được tạo ra để dành riêng cho system buffer ánh xạ đến cả :smiling_face_with_smiling_eyes_and_hand_covering_mouth:. Việc này có nghĩa là cả user buffer và system buffer đều ánh xạ chung đến một buffer trên RAM. Tức là khi dữ liệu của system buffer bị thay đổi thì user buffer cũng bị thay đổi theo và ngược lại. * Trong quá trình driver tương tác với system buffer, thì các page của buffer không được phép được chuyển ra khỏi vùng swap space. Theo một cách để dễ hình dung thì các page trong buffer sẽ bị khóa lại ở trong RAM :grin:. * Để thực hiện cơ chế Direct I/O thì driver cần sự hỗ trợ của cấu trúc MDL (Memory Discriptor List). Cấu trúc MDL đại diện cho một vùng nhớ trên RAM thuộc về một tiến trình cụ thể nào đó. Từ cấu trúc IRP được gửi từ I/O manager đến driver, ta có thể trích xuất địa chỉ của cấu trúc MDL tương ứng với user buffer. Sau khi có địa chỉ của cấu trúc MDL, ta có thể dùng API **MnGetSystemAddressForMdlSafe()** để lấy địa chỉ của system buffer một cách an toàn. * Lúc này, mọi thay đổi do driver thực hiện vào system buffer cũng ảnh hưởng trực tiếp đến dữ liệu của user buffer. Việc này có nghĩa là khi sử dụng Direct I/O thì việc sao chép giữa các buffer không diễn ra, cho nên cơ chế này thể hiện ưu điểm rất lớn khi user buffer có độ lớn rất lớn. Nếu ta sử dụng cơ chế Buffered I/O cho user buffer có độ lớn rất lớn thì quá trình sao chép dữ liệu giữa các buffer sẽ diễn ra lâu và ảnh hưởng đến hiệu suất của hệ thống. * Để sử dụng cơ chế Direct I/O thì ta cần thêm cờ DO_DIRECT_IO vào trường Flags của DeviceObject là được :smiling_imp:. ::: ![](https://hackmd.io/_uploads/rJ1qhlpj3.png) :::info * Dưới đây là mô tả chi tiết của cấu trúc MDL mà mình vừa đề cập ở trên. ::: ![](https://hackmd.io/_uploads/B1X0UkAsn.png) :diamond_shape_with_a_dot_inside: **Chức năng hàm OnStubDispatch()** :::info * Bắt đầu hàm **OnStubDispatch()** là thấy nhức đầu rồi :grin:, nó khai báo rất nhiều biến và đồng thời thực hiện lấy I/O stack location hiện tại của driver. * I/O stack location chi tiết là gì thì ta cũng không cần quan tâm lắm đâu :grin:. Ta chỉ cần biết là để truy cập địa chỉ của system buffer thì cần phải thông qua cấu trúc này là được. ::: ![](https://hackmd.io/_uploads/BJnYEC3ih.png) :::info * Ở hình dưới mô tả cấu trúc của IRP, ta có thể thấy rằng IRP không bao giờ đi một mình, nó luôn đi kèm theo bởi một hoặc nhiều cấu trúc **IO_STACK_LOCATION** :grin:. ::: ![](https://hackmd.io/_uploads/r1fasyTo2.png) :::info * Hình dưới mô tả chi tiết cấu trúc **IO_STACK_LOCATION**. Ở đây ta chỉ quan tâm đến trường MdlAddress thôi bởi vì rootkit ta dùng cơ chế Direct I/O để truy cập dữ liệu. Trường này chứa địa chỉ của cấu trúc MDL, cấu trúc MDL sẽ đại diện cho một vùng nhớ thuộc về một process nào đó trên RAM. Trong trường hợp này cấu trúc MDL sẽ đại diện cho user buffer của chúng ta. ::: ![](https://hackmd.io/_uploads/Sym23k6o2.png) :::info * Trong hàm **UnStubDispatch()** của rootkit, nó trích xuất địa chỉ của system buffer. Có hai trường hợp xảy ra: * Buffer trên RAM đã được ánh xạ vào kernel dưới dạng là system buffer nên ta chỉ cần tương tác trực tiếp với nó là được. * Buffer trên RAM vẫn chưa ánh xạ vào kernel, nên ta phải dùng API **MnMapLockedPagesSpecifyCache()** để ánh xạ buffer trên RAM vào kernel thành system buffer. ::: ![](https://hackmd.io/_uploads/HkAsIA2i2.png) :::info * Để biết buffer đã ánh xạ vào kernel hay chưa thì ta chỉ cần kiểm tra 2 cờ **MDL_MAPPED_TO_SYSTEM_VA** và **MDL_SOURCE_IS_NONPAGED_POOL** có xuất hiện trong trường MdlFlags của cấu trúc MDL hay không. ::: ![](https://hackmd.io/_uploads/H1jF0Anon.png) :::info * Khi đã có được địa chỉ system buffer, rootkit sẽ trích xuất dữ liệu từ nó. Bản chất dữ liệu của system buffer chính là 4 byte chứa PID của malware process mà mình đã đề cập ở trên. * Sau khi có PID của malware, rootkit dùng API **PsLookupProcessByProcessId()** để lấy địa chỉ của cấu trúc **EPROCESS** tương ứng với PID của malware. Địa chỉ EPROCESS của malware sau đó được lưu vào biến pEPROCESS. ::: ![](https://hackmd.io/_uploads/rk-mDC3o3.png) :::info * Tiếp theo, rookit thực hiện một vòng lặp để thực hiện việc gì đó :thinking_face:. Mình phải mất khá nhiều thời gian tìm hiểu kiến thức để hiểu là nó đang muốn làm gì. * Ở trong vòng lặp, ta có thể thấy rằng nó đang so sánh giá trị PID với 4 byte tại vị trí **pEPROCESS + i * 4** và giá trị i thuộc khoảng từ 0 đến 0x200. Vậy thì việc so sánh như này có ý nghĩa như thế nào :thinking_face: ? * Trong cấu trúc EPROCESS, ta có một trường chứa giá trị PID của process. Nhưng vấn đề là offset của trường này bị thay đổi thông qua nhiều phiên bản hệ điều hành. Vì vậy, ta không thể nào biết chính xác offset của trường PID trong EPROCESS là bao nhiêu. Để xác định được offset của trường này thì rootkit phải kiểm tra tất cả các trường hợp có thể bằng cách so sánh liên tục 4 byte tại offset i * 4 với giá trị của PID. Vậy thì việc mà ta đi xác định offset của trường PID trong cấu trúc EPROCESS có ý nghĩa gì :thinking_face: ? ::: ![](https://hackmd.io/_uploads/SkNsDA2on.png) :::info * Sau một lúc tìm hiểu thì mình biết rằng hóa ra trường **ActiveProcessLinks** luôn đi theo sau trường PID của EPROCESS :grin:. Cho nên việc tìm kiếm offset của PID là để tìm kiếm offset của ActiveProcessLinks. ActiveProcessLinks được biểu diễn bởi cấu trúc **LIST_ENTRY**, cấu trúc này giúp kernel có thể liên kết các EPROCESS lại với nhau dưới dạng một danh sách liên kết vòng. Ở đoạn code trên, ta có thể thấy offset của ActiveProcessLinks được lưu trữ trong biến flinkOffset. Vậy thì tại sao rootkit muốn tìm kiếm offset của trường ActiveProcessLinks, mục đích phía sau là gì :thinking_face: ? ::: ![](https://hackmd.io/_uploads/HkT9903s3.png) :::info * Hình dưới mô tả mối liên hệ giữa các cấu trúc EPROCESS trong kernel space. Bản chất việc rootkit muốn lấy offset của trường ActiveProcessLinks là để thực hiện việc che dấu malware process ở user space :grin:. * Để thực hiện điều này thì rootkit cần phải gỡ bỏ khối EPROCESS tương ứng với malware process ra khỏi danh sách liên kết vòng. ::: ![](https://hackmd.io/_uploads/rJI3t0hs3.png) :::info * Việc này được thực hiện khá đơn giản. Ta chỉ cần lấy Flink của backward process trỏ đến địa chỉ của Flink của forward process và lấy Blink của forward process trỏ đến địa chỉ của Flink của backward process. * Tiếp theo, rootkit gọi API **ObDereferenceObject()** để giải phóng cấu trúc EPROCESS tương ứng với malware process ra khỏi bộ nhớ. ::: ![](https://hackmd.io/_uploads/Bkx0PAhi2.png) :::info * Cuối cùng, rootkit gọi API **IofCompleRequest()** để thông báo với hệ thống là nó đã xử lý xong IRP :grin:. Việc này rất quan trọng vì hệ thống cần biết rootkit đã xử lý xong I/O request hay chưa để còn giải phóng tài nguyên và thực hiện các bước tiếp theo. ::: ![](https://hackmd.io/_uploads/HJoy_A2s2.png) :::info * Tới lúc này thì malware process đã được ẩn khỏi hệ thống :grin:. Ta cùng chuyển sang phần demo để xem thử con malware này hoạt động như nào. ::: ### 3. Triển Khai Cụ Thể :::info * Đầu tiên, mình chạy malware :grin:. Nó cho mình biết PID của nó là 2476 và thách thức mình kill nó. ::: ![](https://hackmd.io/_uploads/S1nksZ6oh.png) :::info * Mình kiểm tra các kernel module được tải vào kernel thì thấy có dkom.sys, đây là rootkit đấy :smiling_face_with_smiling_eyes_and_hand_covering_mouth:. ::: ![](https://hackmd.io/_uploads/rkZMoZai2.png) :::info * Tại đường dẫn C:\hidden\dkom.sys chính là nơi lưu trữ con rootkit này. ::: ![](https://hackmd.io/_uploads/rJpbhZpih.png) :::info * Mình dùng 2 công cụ kiểm tra tiến trình là Process Hacker và Task Manager thì chẳng thấy có bất cứ một tiến trình nào có PID bằng 2476 cả. Vậy là con malware này đã thành công việc ẩn chính bản thân nó bằng rootkit :nerd_face:. ::: ![](https://hackmd.io/_uploads/SkuYo-6ih.png) ### 4. Tham Khảo * https://www.amazon.com/Malware-Analysis-Detection-Engineering-Comprehensive/dp/1484261925 * https://github.com/Apress/malware-analysis-detection-engineering/tree/master/samples_all_malware_analysis_and_detection_engineering/chapter_11