[RE 101] Địa chỉ Kernel32.dll và các hàm API
Trong bài blog https://sec.vnpt.vn/2023/05/pe-injection/ mình có đề cập đến việc shellcode tìm thư viện Kernel32.dll và các hàm của thư viện này bằng cách sử dụng cấu trúc của PEB. Nói ngắn gọn thì shellcode đã phân giải hàm một cách thủ công. Trong bài viết này chúng ta sẽ cùng tìm hiểu tại sao mã độc lại làm như vậy và làm như thế nào.
Bên cạnh việc tránh bị các chương trình anti malware phát hiện phát hiện (mà chúng ta sẽ tìm hiểu ở bài sau) thì lý do tôi muốn nhắc đến là shellcode không thể sử dụng Windows API để tương tác với Windows OS một cách bình thường. Shellcode không có cấu trúc của một file thực thi hợp lệ, nên Windows loader không thể nhận ra và thực thi nó. Bên cạnh đó, Windows loader phụ thuộc vào Import Address Table(IAT) , mà rất có thể IAT này không cung cấp các địa chỉ hàm mà shellcode cần nên nó không thể phụ thuộc vào IAT của process mà nó inject vào. Vậy Shellcode phải tự tìm các thư viện và các hàm cần thiết để có thể thực thi. May mắn thay (hoặc xui xẻo), trong thư viện Kernel32.dll có chứa 2 API có thể giải quyết được vấn để này:
– LoadLibrary: load thư viện và trả về handle đến thư viện đó.
– GetProcAddress: trả về địa chỉ của hàm được export của thư viện.
Nếu shellcode có thể sử dụng 2 API này thì nó có thể sử dụng tất cả các thư viện cũng như các hàm trong các thư viện, hay nói cách khác là sử dụng được các API.
Nhưng mà không phải vừa nãy chúng ta đã nói rằng shellcode không thể sử dụng API một cách bình thường, vậy tại sao nó có thể sử dụng 2 API này ???
Thực tế thì một process bất kì trong windows mà shellcode inject vào, sẽ luôn có Kernel32.dll trong danh sách các modules đã được load. Thử kiểm tra lại bằng Windbg, ví dụ calc.exe
Hoặc thậm chí một chương trình đơn giản ntn cũng cần load kernel32.dll:
Vậy thì đã đảm bảo là có Kernel32.dll trong danh sách các modules đã được load.
Tóm lại shellcode cần phải thực hiện hai việc sau:
- Tìm thư viện Kernel32.dll
- Tìm các hàm LoadLibrary và GetProcAddress
Đầu tiên chúng ta sẽ tìm thư viện Kernel32.dll bằng cách sử dụng các cấu trúc dữ liệu của Windows.
Cấu trúc cần xem xét đầu tiên là Thread Environment Block.
Nguồn: https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-teb)
Mỗi thread sẽ có một cấu trúc TEB riêng để lưu thông tin về thread. Địa chỉ của cấu trúc TEB này được lưu trong thanh ghi FS. Trong cấu trúc này chúng ta chỉ cần quan tâm đến trường ProcessEnvironmentBlock. Đây là con trỏ trỏ đến cấu trúc Process Environment Block (PEB). Mỗi process cũng sẽ có một cấu trúc PEB riêng để lưu thông tin về process. Con trỏ trỏ đến PEB có offset 0x30 trong cấu trúc TEB. Dưới đây là cấu trúc PEB:
(Nguồn: https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb)
Trong PEB, chúng ta lại quan tâm đến trường Ldr, đây là con trỏ trỏ đến cấu trúc PEB_LDR_DATA:
Trong cấu trúc này chứa trường quan trọng là InMemoryOrderModuleList trỏ đến cấu trúc LDR_DATA_TABLE_ENTRY, trường này chứa danh sách các dll theo thứ tự xuất hiện trong vùng nhớ.
Cấu trúc LIST_ENTRY là một danh sách liên kết đôi trỏ đến phần tử trước và sau.
Cấu trúc LDR_TABLE_ENTRY chứa thông tin về dll đã được load. Trong đó, có một số quan trọng cần chú ý là InMemoryOrderLinks(trỏ tới LIST_ENTRY), DllBase(Địa chỉ của Dll), FullDllName(Tên của Dll )
Sơ đồ tóm tắt quá trình tìm thư viện Kernel32.dll
(Nguồn: Practical Malware Analysis book)
Đoạn mã thực hiện tìm địa chỉ Kernel32.dll:
//Trỏ tới PEB
PEB* peb;
__asm
{
mov eax, fs: [0x30]
mov peb, eax
}
// Trỏ tới ldr
PEB_LDR_DATA* ldr = *(PEB_LDR_DATA**)((DWORD)peb + 0x0C);
LIST_ENTRY* head = *(LIST_ENTRY**)((DWORD)ldr + 0x14);
LIST_ENTRY* entry = head->Flink;// Tạo vòng lặp tìm kernel32.dll trong danh sách
while (entry != head)
{LDR_DATA_TABLE_ENTRY* module= *(LDR_DATA_TABLE_ENTRY**)((DWORD)entry + 0x08);
USHORT length = *(USHORT*)((DWORD)module+ 0x24);
WCHAR* buffer = *(WCHAR**)((DWORD)module+ 0x28);// Kiểm tra nếu dll có độ dài 24 và tên Dll là kernel32.dll
if (length == 24 && wcsncmp(buffer, L”kernel32.dll”, 12) == 0)
{
return *(HMODULE*)((DWORD)module+ 0x18); // Trả về địa chỉ của kernel32.dll
}
entry = entry->Flink;
}
Tiếp theo chúng ta sẽ tìm địa chỉ hàm GetProcAddress và LoadLibrary bằng cách sử dụng bảng Export của thư viện Kernel32.dll:
HMODULE kernel32 = GetKernel32Base(); // Đã thực hiện ở trên
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)kernel32;
PIMAGE_NT_HEADERS pNtHeader= (PIMAGE_NT_HEADERS)((DWORD)kernel32 + *(DWORD*)(pDosHeader + 0x3c));
PIMAGE_EXPORT_DIRECTORY pExpTable= (PIMAGE_EXPORT_DIRECTORY)((DWORD)kernel32 + pNtHeader+ 0x78);
PDWORD name_table = (PDWORD)((DWORD)kernel32 + pExpTable+ 0x20);
PWORD ordinal_table = (PWORD)((DWORD)kernel32 + pExpTable+ 0x24);
PDWORD address_table = (PDWORD)((DWORD)kernel32 + pExpTable+ 0x1c);
// Lặp đến khi tìm được hàm “GetProcAddress”
for (int i = 0; i < exp->NumberOfNames; i++)
{
char* name = (char*)((DWORD)kernel32 + name_table[i]);
if (strcmp(name, “GetProcAddress”) == 0)
{
WORD ordinal = ordinal_table[i];
DWORD address = address_table[ordinal];
return (FARPROC)((DWORD)kernel32 + address);
}
}
Sau khi có được địa chỉ hàm GetProcAddress chúng ta có thể dễ dàng sử dùng hàm này để tìm hàm LoadLibrary. Đương nhiên sau khi có được địa chỉ 2 hàm này, chúng ta có thể load thư viện bất kì và sử dụng các hàm được export của chúng.
Tuy nhiên cần lưu ý đoạn code trên chỉ mang tính trực quan hóa, giúp người đọc dễ dàng hình dung cách shellcode load thư viện và tìm hàm. Nếu trong thực tế làm như thế này rất dễ bị phát hiện bởi các trình anti malware, ở bài viết sau chúng ta sẽ cùng tìm hiểu cách để hạn chế tối đa việc này. Cảm ơn các bạn đã đọc!
By,
Blue_team
References
https://idafchev.github.io/exploit/2017/09/26/writing_windows_shellcode.html
https://aroundthemalware.wordpress.com/2021/12/05/peb-malwares-favourite/
https://rvsec0n.wordpress.com/2019/09/13/routines-utilizing-tebs-and-pebs/