Introduction
API Hooking là kỹ thuật dùng để can thiệp và chỉnh sửa hành vi của một hàm nào đó trong vùng nhớ của tiến trình. Kỹ thuật này thường được dùng cho việc debug, dịch ngược hoặc hack game.
Info
API hooking còn được sử dụng bởi các giải pháp bảo mật nhằm theo dõi các Windows API thường bị lạm dụng bởi malware.
Về bản chất, API hooking sẽ thay thế một phần mã máy của hàm gốc bằng một phiên bản custom để thực hiện một số hành động tùy ý nào đó trước khi hoặc sau khi gọi hàm gốc. Điều này cho phép ta có thể thay đổi hành vi của chương trình mà không cần thay đổi mã nguồn.
Trampolines
Cách truyền thống để hiện thực API hooking là sử dụng các trampoline. Về bản chất, trampoline là một đoạn shellcode được dùng để thay đổi luồng thực thi bằng cách nhảy đến một địa khác ở trong không gian địa chỉ của tiến trình.
Để hook một hàm, ta sẽ chèn trampoline vào đầu thân hàm của hàm đó. Khi chương trình gọi hàm bị hook, trampoline sẽ được thực thi thay vì hàm gốc.
Inline Hooking
Là một cách khác để hiện thực API hooking. Điểm khác nhau giữa inline hooking với trampoline là inline hooking sẽ chuyển luồng thực thi về lại cho hàm gốc sau khi trampoline được thực thi xong.
Why API Hooking?
Mặc dù thường được dùng để phân tích malware và debug, API hooking vẫn có thể được dùng bởi chính malware để:
- Thu thập dữ liệu nhạy cảm từ hệ thống hoặc tiến trình chẳng hạn các tài khoản xác thực.
- Thay đổi hoặc can thiệp các lời gọi hàm vì các mục đích xấu.
- Bypass các giải pháp bảo mật bằng cách thay đổi hành vi của hệ điều hành hoặc của chương trình.
Implementing Hooking
Có 2 cách để hiện thực API hooking:
- Sử dụng các thư viện mã nguồn mở chẳng hạn như Detours của Microsoft hay Minhook.
- Sử dụng Windows API (bị giới hạn hơn).
- Tự thực hiện việc hooking.
Detours Library
Là một thư viện được cung cấp bởi Microsoft giúp can thiệp và chuyển hướng các lời gọi hàm ở trong Windows. Cụ thể hơn, thư viện sẽ chuyển hướng các lời gọi hàm của những hàm cụ thể đến một hàm thay thế mà người dùng tự định nghĩa để thực hiện các tác vụ hoặc chỉnh sửa hành vi của hàm gốc.
Detours thường được dùng với C/C++ cho ứng dụng 32-bit và 64-bit.
Seealso
Wiki của Detours giải thích rất chi tiết và dễ hiểu về cách mà Detours hoạt động: Home · microsoft/Detours Wiki
Interception of Binary Functions
Để hook vào một hàm mục tiêu (target function), Detours thay thế một vài instruction ở đầu thân hàm với một unconditional jump (chính là instruction jmp
) đến hàm detour (DetourFunction
) được cung cấp bởi người dùng.
Các instruction bị thay thế của target function được lưu ở trong một hàm khác có tên là trampoline function. Ở cuối trampoline function cũng là một unconditional jump đến phần còn lại của target function (TargetFunction+5
). Hàm trampoline được sử dụng trong trường hợp detour function muốn chuyển luồng thực thi lại cho target function.
Minh họa:
Với source function là hàm gọi target function, chẳng hạn như hàm main
gọi hàm printf
thì main
là source function còn printf
là target function.
Sau khi target function thực thi xong và trả về, luồng thực thi sẽ được chuyển lại cho trampoline function rồi đến detour function (bước 4). Khi đó, chúng ta có thể xem hoặc thay đổi giá trị trả về của target function.
How Detours Writes Functions
Khi hook một hàm nào đó, trước tiên Detours sẽ cấp phát vùng nhớ cho trampoline function.
Tiếp theo, nó sẽ thay thế chế độ bảo vệ của cả target function và trampoline function nhằm cho phép việc ghi dữ liệu.
Sau đó, Detours sẽ sao chép từng byte từ target function vào trampoline function cho đến khi có ít nhất 5-byte đã được sao chép (vừa đủ chỗ cho một instruction jmp
). Trong trường hợp hàm cần hook có ít hơn 5-byte, Detours sẽ dừng lại và trả về mã lỗi.
Cuối cùng, Detours sẽ ghi vào target function một unconditional jump đến detour function và ghi vào cuối trampoline function một unconditional jump đến phần còn lại của target function.
The Infinite Loop Problem
Xét trường hợp không có trampoline function và ta có nhu cầu gọi lại target function ở trong detour function như đoạn code bên dưới:
INT WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
// Printing original parameters value
printf("Original lpText Parameter : %s\n", lpText);
printf("Original lpCaption Parameter : %s\n", lpCaption);
// DON'T DO THIS
// Changing the parameters value
return MessageBoxA(hWnd, "different lpText", "different lpCaption", uType); // Calling MessageBoxA (this is hooked)
}
Do target function đã bị hook, việc gọi lại nó sẽ dẫn đến việc gọi lại detour function. Điều này khiến chương trình rơi vào vòng lặp vô hạn. Đây là một vấn đề chung của kỹ thuật API hooking chứ không phải của thư viện Detours.
Có 2 cách giải quyết:
Solution 1 - Global Original Function Pointer
Sử dụng trampoline function.
Đối với thư viện Detours, chúng ta sẽ cần phải tạo ra một con trỏ hàm toàn cục để lưu trữ địa chỉ của target function trước khi hook nó. Khi function bị hook, Detours sẽ thay đổi giá trị của con trỏ hàm để nó trỏ đến trampoline function. Khi đó, chúng ta có thể gọi target function thông qua trampoline function bằng cách sử dụng con trỏ hàm này.
// Used as a unhooked MessageBoxA in `MyMessageBoxA`
fnMessageBoxA g_pMessageBoxA = MessageBoxA;
INT WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
// Printing original parameters value
printf("Original lpText Parameter : %s\n", lpText);
printf("Original lpCaption Parameter : %s\n", lpCaption);
// Changing the parameters value
// Calling an unhooked MessageBoxA
return g_pMessageBoxA(hWnd, "different lpText", "different lpCaption", uType);
}
Sau khi hook bị gỡ bỏ, Detours sẽ thay đổi giá trị của con trỏ để nó trỏ đến target function như ban đầu.
Solution 2 - Using a Different API
Sử dụng một hàm khác có chức năng tương tự nhưng không bị hook. Ví dụ: MessageBoxW
có chức năng tương tự MessageBoxA
:
INT WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
// Printing original parameters value
printf("Original lpText Parameter : %s\n", lpText);
printf("Original lpCaption Parameter : %s\n", lpCaption);
// Changing the parameters value
return MessageBoxW(hWnd, L"different lpText", L"different lpCaption", uType);
}
Detours Hooking Routine
Thư viện Detours sử dụng cơ chế transaction để cài đặt và gỡ bỏ các hook một cách hiệu quả. Việc này cho phép nhóm nhiều hook lại với nhau, giúp quá trình cài đặt hoặc gỡ bỏ diễn ra trong một thao tác duy nhất. Để thiết lập hook bằng Detours, ta khởi tạo một transaction, thêm các hook cần thiết, rồi commit. Khi transaction được commit, các hook sẽ được áp dụng hoặc loại bỏ khỏi các hàm mục tiêu một cách đồng bộ.
Using The Detours Library
Để sử dụng Detours, chúng ta cần tải về thư viện rồi biên dịch nó để có được các file thư viện tĩnh (.lib
) mà có thể được dùng để biên dịch cùng với malware (liên kết tĩnh). Ngoài ra, ta cũng cần phải include file detours.h
vào project.
32-bit Vs 64-bit Detours Library
Chúng ta sẽ sử dụng một đoạn code tiền xử lý sử dụng các macro _M_X64
và _M_IX86
để xác định đúng phiên bản thư viện tĩnh mà ta cần liên kết đối với kiến trúc x64 và kiến trúc x86.
// If compiling as 64-bit
#ifdef _M_X64
#pragma comment (lib, "detoursx64.lib")
#endif // _M_X64
// If compiling as 32-bit
#ifdef _M_IX86
#pragma comment (lib, "detoursx86.lib")
#endif // _M_IX86
Với file detoursx64.lib
sẽ được tạo ra khi biên dịch Detours với kiến trúc x64 còn file detoursx86.lib
sẽ được tạo ra khi biên dịch Detours với kiến trúc x86.
Detour Function
Detour function có thể có cùng kiểu dữ liệu cho giá trị trả về hoặc cho các tham số giống với target function. Điều này cho phép chúng ta xem hoặc chỉnh sửa các giá trị đó. Ví dụ:
// The function that will run instead MessageBoxA when hooked
INT WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
printf("[+] Original Parameters : \n");
printf("\t - lpText : %s\n", lpText);
printf("\t - lpCaption : %s\n", lpCaption);
return g_pMessageBoxA(hWnd, "different lpText", "different lpCaption", uType);
}
Với g_pMessageBoxA
là con trỏ hàm đến trampoline/target function như đã đề cập ở The Infinite Loop Problem.
Warning
Detour function có thể có ít tham số hơn so với hàm gốc nhưng không thể có nhiều hơn bởi vì khi đó chúng ta có thể truy cập đến một tham số có địa chỉ không hợp lệ và dẫn đến crash chương trình.
Detours API Functions
Trước khi hook một hàm nào đó, ta cần phải tìm địa chỉ của nó. Ở đây, ta sẽ minh họa bằng cách hook vào hàm MessageBoxA
.
Các hàm của Detours mà ta sẽ sử dụng để hook:
-
DetourTransactionBegin
- khởi tạo transaction. Ví dụ:// Creating the transaction & updating it if ((dwDetoursErr = DetourTransactionBegin()) != NO_ERROR) { printf("[!] DetourTransactionBegin Failed With Error : %d \n", dwDetoursErr); return FALSE; }
-
DetourUpdateThread
- giúp đảm bảo việc hooking không làm ảnh hưởng đến việc thực thi của các luồng khác. Hàm này nhận vào pseudo handle của thread hiện tại. Ví dụ:if ((dwDetoursErr = DetourUpdateThread(GetCurrentThread())) != NO_ERROR) { printf("[!] DetourUpdateThread Failed With Error : %d \n", dwDetoursErr); return FALSE; }
-
DetourAttach
- cài đặt hook vào target function ở trong transaction hiện tại. Hàm này nhận vào địa chỉ của con trỏ hàm toàn cục và detour function. Ví dụ:// Running MyMessageBoxA instead of g_pMessageBoxA that is MessageBoxA if ((dwDetoursErr = DetourAttach((PVOID)&g_pMessageBoxA, MyMessageBoxA)) != NO_ERROR) { printf("[!] DetourAttach Failed With Error : %d \n", dwDetoursErr); return FALSE; }
-
DetourDetach
- xóa hook khỏi target function ở trong transaction hiện tại. Hàm này cũng nhận vào địa chỉ của con trỏ hàm toàn cục và detour function. Ví dụ:// Removing the hook from MessageBoxA if ((dwDetoursErr = DetourDetach((PVOID)&g_pMessageBoxA, MyMessageBoxA)) != NO_ERROR) { printf("[!] DetourDetach Failed With Error : %d \n", dwDetoursErr); return FALSE; }
-
DetourTransactionCommit
- commit transaction để áp dụng hoặc loại bỏ các hook.// Actual hook installing happen after `DetourTransactionCommit` - commiting the transaction if ((dwDetoursErr = DetourTransactionCommit()) != NO_ERROR) { printf("[!] DetourTransactionCommit Failed With Error : %d \n", dwDetoursErr); return FALSE; }
Các hàm trên đều trả về giá trị NO_ERROR
(0
) nếu thực thi thành công và giá trị khác 0
nếu có lỗi.
Minhook Library
Cũng là một thư viện dùng để hooking giống như Detours Library nhưng nhẹ hơn. Nó tương thích với các ứng dụng 32-it và 64-bit chạy trên Windows cũng như là sử dụng hợp ngữ x86/x64 để thực hiện Inline Hooking.
How MinHook Works
Seealso
Hoạt động tương tự Detours: nó sẽ thay thế các byte đầu của target function bằng chỉ thị jmp
đến detour function.
Trong kiến trúc 32-bit, tổng kích thước của không gian địa chỉ ảo là 4GB và cần 4 bytes để thể hiện địa chỉ vùng nhớ. Tuy nhiên, kích thước của user space (không gian địa chỉ ảo của các chương trình chạy ở user-mode) cho mỗi tiến trình là 2GB (31 bit) còn kích thước của system space (không gian địa chỉ của các chương trình chạy ở kernel mode) là 2GB dùng chung1.
Overwriting the Target Function
Đối với kiến trúc 32-bit, MinHook chỉ cần dùng một chỉ thị jmp
với 5 bytes (1 byte cho opcode và 4 bytes cho toán hạng) là đủ để đi hết toàn bộ không gian địa chỉ của tiến trình (có kích thước 2GB) do toán hạng 4 bytes sẽ thể hiện được giá trị từ -2GB đến +2GB.
; 32bit relative JMPs of 5 bytes cover whole address space
0x40000000: E9 FAFFFFBF JMP 0xFFFFFFFF (EIP+0xBFFFFFFA)
0x40000000: E9 FBFFFFBF JMP 0x0 (EIP+0xBFFFFFFB)
Giải thích:
- Chỉ thị đầu tiên sẽ đi đến địa chỉ
0x40000000 + 0xBFFFFFFA + 0x5 = 0xFFFFFFFF
(lý do mà cộng thêm0x5
là do chúng ta cần phải bỏ qua bản thân chỉ thịjmp
- có kích thước là 5 bytes) - Chỉ thị thứ hai sẽ đi đến địa chỉ
0x40000000 + 0xBFFFFFFB + 0x5 = 0x0
(do địa chỉ tuyệt đối bị tràn ra khỏi không gian địa chỉ, nó sẽ bị overflow và trở thành0x0
).
Tuy nhiên, trong kiến trúc x64, chỉ thị jmp
5 bytes chỉ có thể đi được một khoảng nhỏ so với toàn bộ không gian địa chỉ ảo của tiến trình (các tiến trình 64-bit sử dụng địa chỉ 48-bit nên có khoảng địa chỉ rất lớn).
; 32bit relative JMPs of 5 bytes cover about -2GB ~ +2GB
0x140000000: E9 00000080 JMP 0xC0000005 (RIP-0x80000000)
0x140000000: E9 FFFFFF7F JMP 0x1C0000004 (RIP+0x7FFFFFFF)
Vì thế, Minhook tạo ra thêm một hàm khác có tên là relay function. Bên trong hàm này là một unconditional jump đến một địa chỉ 64-bit (8 bytes) của detour function.
; Target function (Jump to the Relay Function)
0x140000000: E9 FBFF0700 JMP 0x140080000 (RIP+0x7FFFB)
; Relay function (Jump to the Detour Function)
0x140080000: FF25 FAFF0000 JMP [0x140090000 (RIP+0xFFFA)]
0x140090000: xxxxxxxxxxxxxxxx ; 64bit address of the Detour Function
Note
Chỉ thị
JMP [0x140090000 (RIP+0xFFFA)]
sẽ nhảy đến địa chỉ được lưu trong vùng nhớRIP+0xFFFA
chứ không phải nhảy đến vùng nhớRIP+0xFFFA
. Đây được gọi là indirect jump.
Building the Trampoline Function
Thư viện Minhook cũng sử dụng trampoline function để gọi đến target function ở trong detour function.
Nội dung bên trong trampoline function của Minhook cũng tương tự với Detours: nó chứa một vài byte đầu của target function và một unconditional jump đến phần còn lại của target function.
; Original "USER32.dll!MessageBoxW" in x64 mode
0x770E11E4: 4883EC 38 SUB RSP, 0x38
0x770E11E8: 4533DB XOR R11D, R11D
; Trampoline
0x77064BD0: 4883EC 38 SUB RSP, 0x38
0x77064BD4: 4533DB XOR R11D, R11D
0x77064BD7: FF25 5BE8FEFF JMP QWORD NEAR [0x77053438 (RIP-0x117A5)]
; Address Table
0x77053438: EB110E7700000000 ; Address of the Target Function +7 (for resuming)
; Original "USER32.dll!MessageBoxW" in x86 mode
0x7687FECF: 8BFF MOV EDI, EDI
0x7687FED1: 55 PUSH EBP
0x7687FED2: 8BEC MOV EBP, ESP
; Trampoline
0x0014BE10: 8BFF MOV EDI, EDI
0x0014BE12: 55 PUSH EBP
0x0014BE13: 8BEC MOV EBP, ESP
0x0014BE15: E9 BA407376 JMP 0x7687FED4
Note
Có thể thấy, trong kiến trúc 64-bit, Minhook sử dụng một indirect jump để nhảy đến phần còn lại của target function, tương tự với cách mà relay function hoạt động.
Minhook sử dụng HDE (Hacker Disassembler Engine) để disassemble target function nhằm xác định xác định các instruction cần sao chép cũng như là cách sửa đổi các instruction sao cho phù hợp nhằm đảm bảo chương trình hoạt động đúng.
Trong trường hợp target function có các branch instruction (chỉ thị rẽ nhánh chẳng hạn như je
- jump if equal), Minhook sẽ phải thay đổi chúng ở trong trampoline để chúng trỏ đến cùng một địa chỉ như target function.
; Original "kernel32.dll!IsProcessorFeaturePresent" in x64 mode
0x771BD130: 83F9 03 CMP ECX, 0x3
0x771BD133: 7414 JE 0x771BD149
; Trampoline
; (Became a little complex, because 64 bit version of JE doesn't exist)
0x77069860: 83F9 03 CMP ECX, 0x3
0x77069863: 74 02 JE 0x77069867
0x77069865: EB 06 JMP 0x7706986D
0x77069867: FF25 1BE1FEFF JMP QWORD NEAR [0x77057988 (RIP-0x11EE5)]
0x7706986D: FF25 1DE1FEFF JMP QWORD NEAR [0x77057990 (RIP-0x11EE3)]
; Address Table
0x77057988: 49D11B7700000000 ; Where the original JE points.
0x77057990: 35D11B7700000000 ; Address of the Target Function +5 (for resuming)
Trong ví dụ trên, chỉ thị rẽ nhánh je
ở trong trampoline function sẽ nhảy đến một indirect jump instructiuon tại địa chỉ 0x77069867
. Indirect jump này sau đó sẽ nhảy đến địa chỉ 49D11B7700000000 = 0x771BD149
, cũng chính là nơi mà chỉ thị je
của target function đang trỏ đến.
Các địa chỉ tương đối của RIP ở trong kiến trúc 64-bit cũng được sửa lại để trỏ đến cùng một địa chỉ như target function.
; Original "kernel32.dll!GetConsoleInputWaitHandle" in x64 mode
0x771B27F0: 488B05 11790C00 MOV RAX, [0x7727A108 (RIP+0xC7911)]
; Trampoline
0x77067EB8: 488B05 49222100 MOV RAX, [0x7727A108 (RIP+0x212249)]
0x77067EBF: FF25 4BE3FEFF JMP QWORD NEAR [0x77056210 (RIP-0x11CB5)]
; Address Table
0x77056210: F7271B7700000000 ; Address of the Target Function +7 (for resuming)
; Original "user32.dll!TileWindows" in x64 mode
0x770E023C: 4883EC 38 SUB RSP, 0x38
0x770E0240: 488D05 71FCFFFF LEA RAX, [0x770DFEB8 (RIP-0x38F)]
; Trampoline
0x77064A80: 4883EC 38 SUB RSP, 0x38
0x77064A84: 488D05 2DB40700 LEA RAX, [0x770DFEB8 (RIP+0x7B42D)]
0x77064A8B: FF25 CFE8FEFF JMP QWORD NEAR [0x77053360 (RIP-0x11731)]
; Address Table
0x77053360: 47020E7700000000 ; Address of the Target Function +11 (for resuming)
Đối với ví dụ của hàm GetConsoleInputWaitHandle
, instruction mov
của trampoline được chỉnh sửa giá trị của toán hạng để nó lưu trữ giá trị của vùng nhớ 0x7727A108
tương tự như target function.
Còn đối với ví dụ của hàm TileWindows
, instruction lea
của trampoline cũng được chỉnh sửa giá trị của toán hạng để nó lưu trữ giá trị của vùng nhớ 0x770DFEB8
tương tự như target function.
Using The Minhook Library
Tương tự với Detours, thư viện Minhook cũng cần một file thư viện tĩnh .lib
và include file MinHook.h
vào project.
The Detour Function
Detour function của Minhook cũng tương tự như của Detours. Ví dụ:
fnMessageBoxA g_pMessageBoxA = NULL;
INT WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
printf("[+] Original Parameters : \n");
printf("\t - lpText : %s\n", lpText);
printf("\t - lpCaption : %s\n", lpCaption);
return g_pMessageBoxA(hWnd, "Different lpText", "Different lpCaption", uType);
}
Con trỏ hàm toàn cục g_pMessageBoxA
(dùng để ngăn The Infinite Loop Problem) có giá trị khởi tạo là NULL
vì nó sẽ được khởi tạo bởi hàm MH_CreateHook
. Điều này khác với Detours vì khi sử dụng thư viện Detours ta cần phải khởi tạo con trỏ hàm toàn cục với giá trị là địa chỉ của target function.
Minhook API Functions
Chúng ta sẽ sử dụng các hàm sau của thư viện Minhook để thực hiện hooking:
-
MH_Initialize
- khởi tạo cấu trúcHOOK_ENTRY
dùng cho việc cài đặt và gỡ bỏ các hook.DWORD dwMinHookErr = NULL; if ((dwMinHookErr = MH_Initialize()) != MH_OK) { printf("[!] MH_Initialize Failed With Error : %d \n", dwMinHookErr); return FALSE; }
-
MH_CreateHook
- tạo các hook.// Installing the hook on MessageBoxA, to run MyMessageBoxA instead // g_pMessageBoxA will be a pointer to the original MessageBoxA function if ((dwMinHookErr = MH_CreateHook(&MessageBoxA, &MyMessageBoxA, &g_pMessageBoxA)) != MH_OK) { printf("[!] MH_CreateHook Failed With Error : %d \n", dwMinHookErr); return FALSE; }
-
MH_EnableHook
- enable các hook đã tạo.// Enabling the hook on MessageBoxA if ((dwMinHookErr = MH_EnableHook(&MessageBoxA)) != MH_OK) { printf("[!] MH_EnableHook Failed With Error : %d \n", dwMinHookErr); return -1; }
-
MH_DisableHook
- disable các hook đã tạo.if ((dwMinHookErr = MH_DisableHook(&MessageBoxA)) != MH_OK) { printf("[!] MH_DisableHook Failed With Error : %d \n", dwMinHookErr); return -1; }
-
MH_Uninitialize
- dọn dẹp cấu trúcHOOK_ENTRY
.if ((dwMinHookErr = MH_Uninitialize()) != MH_OK) { printf("[!] MH_Uninitialize Failed With Error : %d \n", dwMinHookErr); return -1; }
Các hàm trên trả về giá trị MH_STATUS
, là một user-defined enum. Nếu giá trị của MH_STATUS
là MH_OK
(bản chất là 0
) thì nghĩa là hàm đã thực thi thành công. Các giá trị khác của MH_STATUS
sẽ cho biết rằng có lỗi đã xảy ra.
Note
MH_Initialize
vàMH_Uninitialize
chỉ cần được gọi một lần trong suốt chương trình.
Custom Code
Việc sử dụng các thư viện mã nguồn mở chẳng hạn như Detours hay Minhook có thể được dùng làm IoC cho malware.
Trong trường hợp ta chỉ cần hook một hàm duy nhất, việc tự hook mà không dùng thư viện sẽ thay thế được những đoạn code của thư viện mà có thể được dùng làm IoC cũng như là làm giảm kích thước file nhị phân của malware.
Creating The Trampoline Shellcode
Như đã biết, ta cần ghi vào target function một đoạn code có chứa jmp
instruction để nó nhảy đến detour function. Ta sẽ gọi đoạn code đó là trampoline shellcode.
Để thực thi jmp
, địa chỉ cần nhảy đến phải nằm trong một thanh ghi mà cụ thể là thanh ghi eax
đối với kiến trúc 32-bit và thanh ghi r10
đối với kiến trúc 64-bit. Việc lưu địa chỉ vào thanh ghi sẽ cần phải dùng đến chỉ thị mov
.
64-bit Jump Shellcode
Trampoline shellcode trong kiến trúc 64-bit:
mov r10, pAddress
jmp r10
Với pAddress
là địa chỉ của detour function (chẳng hạn như 0x0000FFFEC32A300
).
Để thể hiện các instruction trên ở trong code, ta cần chuyển chúng sang dạng mã nhị phân:
0x49, 0xBA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r10, pAddress
0x41, 0xFF, 0xE2 // jmp r10
Các byte 0x0
sẽ được thay thế bằng địa chỉ của detour function trong quá trình chạy.
32-bit Jump Shellcode
Trampoline shellcode trong kiến trúc 32-bit:
mov eax, pAddress
jmp eax
Chuyển sang mã máy:
0xB8, 0x00, 0x00, 0x00, 0x00, // mov eax, pAddress
0xFF, 0xE0 // jmp eax
Tương tự, các byte 0x0
sẽ được thay thế bằng địa chỉ của detour function trong quá trình chạy.
Retrieving pAddress
Để truy xuất địa chỉ của detour function, ta có thể sử dụng hàm GetProcAddress
. Sau khi có địa chỉ, ta cần sao chép vào trampoline shellcode ở trên sử dụng hàm memcpy
.
64-bit Patching
uint8_t uTrampoline[] = {
0x49, 0xBA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r10, pFunctionToRun
0x41, 0xFF, 0xE2 // jmp r10
};
uint64_t uPatch = (uint64_t)pAddress;
memcpy(&uTrampoline[2], &uPatch, sizeof(uPatch)); // copying the address to the offset '2' in uTrampoline
32-bit Patching
uint8_t uTrampoline[] = {
0xB8, 0x00, 0x00, 0x00, 0x00, // mov eax, pFunctionToRun
0xFF, 0xE0 // jmp eax
};
uint32_t uPatch = (uint32_t)pAddress;
memcpy(&uTrampoline[1], &uPatch, sizeof(uPatch)); // copying the address to the offset '1' in uTrampoline
Note
Kiểu dữ liệu
uint64_t
vàuint32_t
là để đảm bảo địa chỉ có đúng số byte cho từng kiến trúc hệ thống.
Writing The Trampoline
Sau khi đã có trampoline shellcode thì ta sẽ ghi nó vào các byte đầu của target function. Để có thể ghi shellcode vào target function, ta cần chuyển chế độ bảo vệ vùng nhớ thành PAGE_EXECUTE_READWRITE
bằng hàm VirtualProtect
.
// Changing the memory permissons at 'pFunctionToHook' to be PAGE_EXECUTE_READWRITE
if (!VirtualProtect(pFunctionToHook, sizeof(uTrampoline), PAGE_EXECUTE_READWRITE, &dwOldProtection)) {
return FALSE;
}
Sau đó, sao chép trampoline shellcode vào target function bằng memcpy
:
// Copying the trampoline shellcode to 'pFunctionToHook'
memcpy(pFunctionToHook, uTrampoline, sizeof(uTrampoline));
Unhooking
Để có thể unhook, ta cần lưu lại các byte gốc của target function trước khi thực hiện hook vào một buffer nào đó. Khi unhook, ta chỉ cần chép các byte này ngược lại vào target function:
memcpy(pFunctionToHook, pOriginalBytes, sizeof(pOriginalBytes));
Khi unhook, chúng ta nên thay đổi lại chế độ bảo vệ vùng nhớ như cũ:
if (!VirtualProtect(pFunctionToHook, sizeof(uTrampoline), dwOldProtection, &dwOldProtection)) {
return FALSE;
}
Với dwOldProtection
là biến lưu trữ chế độ bảo vệ vùng nhớ cũ được trả về bởi lời gọi hàm VirtualProtect
đầu tiên.
HookSt
Structure
Ta sẽ tạo ra cấu trúc HookSt
chứa các thông tin cần thiết để thực hiện việc hook và unhook các hàm.
typedef struct _HookSt{
PVOID pFunctionToHook; // address of the function to hook
PVOID pFunctionToRun; // address of the function to run instead
BYTE pOriginalBytes[TRAMPOLINE_SIZE]; // buffer to keep some original bytes (needed for cleanup)
DWORD dwOldProtection; // holds the old memory protection of the "function to hook" address (needed for cleanup)
}HookSt, *PHookSt;
Giá trị của TRAMPOLINE_SIZE
sẽ là 13
nếu mã nguồn được biên dịch thành chương trình 64-bit và sẽ là 7
nếu mã nguồn được biên dịch thành chương trình 32-bit. Việc gán giá trị cho TRAMPOLINE_SIZE
sẽ được thực hiện bởi đoạn code sau:
// if compiling as 64-bit
#ifdef _M_X64
#define TRAMPOLINE_SIZE 13
#endif // _M_X64
// if compiling as 32-bit
#ifdef _M_IX86
#define TRAMPOLINE_SIZE 7
#endif // _M_IX86
Ta sẽ tạo ra hàm sau để khởi tạo các giá trị của cấu trúc HookSt
:
BOOL InitializeHookStruct(IN PVOID pFunctionToHook, IN PVOID pFunctionToRun, OUT PHookSt Hook) {
// Filling up the struct
Hook->pFunctionToHook = pFunctionToHook;
Hook->pFunctionToRun = pFunctionToRun;
// Save original bytes of the same size that we will overwrite (that is TRAMPOLINE_SIZE)
// This is done to be able to do cleanups when done
memcpy(Hook->pOriginalBytes, pFunctionToHook, TRAMPOLINE_SIZE);
// Changing the protection to RWX so that we can modify the bytes
// We are saving the old protection to the struct (to re-place it at cleanup)
if (!VirtualProtect(pFunctionToHook, TRAMPOLINE_SIZE, PAGE_EXECUTE_READWRITE, &Hook->dwOldProtection)) {
printf("[!] VirtualProtect Failed With Error : %d \n", GetLastError());
return FALSE;
}
return TRUE;
}
Installing Hooks
Hàm dùng để cài đặt hook:
BOOL InstallHook (IN PHookSt Hook) {
#ifdef _M_X64
// 64-bit trampoline
uint8_t uTrampoline [] = {
0x49, 0xBA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r10, pFunctionToRun
0x41, 0xFF, 0xE2 // jmp r10
};
// Patching the shellcode with the address to jump to (pFunctionToRun)
uint64_t uPatch = (uint64_t)(Hook->pFunctionToRun);
// Copying the address of the function to jump to, to the offset '2' in uTrampoline
memcpy(&uTrampoline[2], &uPatch, sizeof(uPatch));
#endif // _M_X64
#ifdef _M_IX86
// 32-bit trampoline
uint8_t uTrampoline[] = {
0xB8, 0x00, 0x00, 0x00, 0x00, // mov eax, pFunctionToRun
0xFF, 0xE0 // jmp eax
};
// Patching the shellcode with the address to jump to (pFunctionToRun)
uint32_t uPatch = (uint32_t)(Hook->pFunctionToRun);
// Copying the address of the function to jump to, to the offset '1' in uTrampoline
memcpy(&uTrampoline[1], &uPatch, sizeof(uPatch));
#endif // _M_IX86
// Placing the trampoline function - installing the hook
memcpy(Hook->pFunctionToHook, uTrampoline, sizeof(uTrampoline));
return TRUE;
}
Removing Hooks
Hàm dùng để gỡ bỏ hook:
BOOL RemoveHook (IN PHookSt Hook) {
DWORD dwOldProtection = NULL;
// Copying the original bytes over
memcpy(Hook->pFunctionToHook, Hook->pOriginalBytes, TRAMPOLINE_SIZE);
// Cleaning up our buffer
memset(Hook->pOriginalBytes, '\0', TRAMPOLINE_SIZE);
// Setting the old memory protection back to what it was before hooking
if (!VirtualProtect(Hook->pFunctionToHook, TRAMPOLINE_SIZE, Hook->dwOldProtection, &dwOldProtection)) {
printf("[!] VirtualProtect Failed With Error : %d \n", GetLastError());
return FALSE;
}
// Setting all to null
Hook->pFunctionToHook = NULL;
Hook->pFunctionToRun = NULL;
Hook->dwOldProtection = NULL;
return TRUE;
}
Demo
Do tính chất của trampoline shellcode, ta sẽ không thể sử dụng một con trỏ hàm toàn cục để giải quyết The Infinite Loop Problem. Thay vào đó, ta sẽ áp dụng giải pháp sử dụng hàm thay thế của API mà ta cần hook chẳng hạn như MessageBoxW
thay cho MessageBoxA
.
Hàm MessageBoxA
trước khi bị hook:
Hàm MessageBoxA
sau khi bị hook:
Kết quả thực thi:
Using Windows APIs
Hàm SetWindowsHookExW
của Windows là một cách khác để thực hiện API hooking. Hàm này thường được sử dụng để theo dõi một số system message (là các cấu trúc dữ liệu được truyền qua lại giữa các tiến trình/thread). Thay vì chỉnh sửa hành vi của target function, SetWindowsHookExW/A
chỉ thực thi một callback function nào đó khi một message nhất định được trigger.
SetWindowsHookEx
Usage
Nguyên mẫu của hàm SetWindowsHookExW
:
HHOOK SetWindowsHookExW(
[in] int idHook, // The type of hook procedure to be installed
[in] HOOKPROC lpfn, // A pointer to the hook procedure (function to execute)
[in] HINSTANCE hmod, // Handle to the DLL containing the hook procedure (this is kept as NULL)
[in] DWORD dwThreadId // A thread Id with which the hook procedure is to be associated with (this is kept as NULL)
);
Với:
idHook
là loại message mà ta muốn theo dõi. Ví dụ, cờWH_KEYBOARD_LL
cho biết ta muốn theo dõi các phím bấm từ bàn phím và thường được dùng bởi các keylogger (ngày xưa).lpfn
là con trỏ đến callback function mà ta muốn thực thi bất cứ khi nào message được chỉ định ởidHook
xảy ra.hmod
là handle đến DLL chứa callback function trỏ đến bởi tham sốlpfn
. Nếu callback function nằm trong tiến trình hiện tại thì có thể truyền vàoNULL
.dwThreadId
là TID của thread mà ta muốn hook. Nếu là0
đối với các ứng dụng desktop, hook sẽ áp dụng cho tất cả các thread ở trong desktop hiện tại. Nếu là một TID cụ thể, callback function chỉ được gọi cho thread đó.
Seealso
Hook Procedure
Callback function (hay hook procedure) cần có kiểu HOOKPROC
với nguyên mẫu như sau:
typedef LRESULT (CALLBACK* HOOKPROC)(int nCode, WPARAM wParam, LPARAM lParam);
Do đó, ta cần định nghĩa hook procedure tương tự như sau:
LRESULT HookCallbackFunc(int nCode, WPARAM wParam, LPARAM lParam){
// function's code
}
Với nCode
là hook code mà hook procedure dùng để xác định hành động cần thực hiện. Giá trị của hook code phụ thuộc vào loại hook. Giá trị của hai tham số wParam
và lParam
phụ thuộc vào hook code nhưng chúng thường chứa thông tin về message đã được gửi đến hoặc gửi đi.
Seealso
Ở bên trong hook procedure, ta nên gọi hàm CallNextHookEx
để chuyển message tới hook procedure tiếp theo trong chuỗi hook.
LRESULT HookCallbackFunc(int nCode, WPARAM wParam, LPARAM lParam){
// Function's code
return CallNextHookEx(NULL, nCode, wParam, lParam)
}
Info
Dựa trên tài liệu của Microsoft, việc gọi đến
CallNextHookEx
là tùy chọn nhưng được khuyến nghị để đảm bảo các ứng dụng khác cũng hook vào message có thể nhận được message và hành xử đúng.
Hoàn thiện hook procedure dùng để theo dõi các message liên quan đến chuột:
LRESULT HookCallbackFunc(int nCode, WPARAM wParam, LPARAM lParam){
if (wParam == WM_LBUTTONDOWN){
printf("[ # ] Left Mouse Click \n");
}
if (wParam == WM_RBUTTONDOWN) {
printf("[ # ] Right Mouse Click \n");
}
if (wParam == WM_MBUTTONDOWN) {
printf("[ # ] Middle Mouse Click \n");
}
return CallNextHookEx(NULL, nCode, wParam, lParam)
}
Processing Messages
Cài đặt hook procedure vào các message liên quan đến chuột sử dụng cờ WH_MOUSE_LL
:
BOOL MouseClicksLogger(){
// Installing hook
HHOOK hMouseHook = SetWindowsHookExW(
WH_MOUSE_LL,
(HOOKPROC)HookCallback,
NULL,
NULL
);
if (!hMouseHook) {
printf("[!] SetWindowsHookExW Failed With Error : %d \n", GetLastError());
return FALSE;
}
// Keeping the thread running
while(1){
}
return TRUE;
}
Cờ WH_MOUSE_LL
sẽ tự động bị xóa và hook procedure sẽ không còn được thực thi nếu thread được cài đặt hook kết thúc. Do đó, ta cần đảm bảo thread không được kết thúc bằng cách dùng vòng lặp vĩnh cửu while(1)
.
Ngoài ra, để đảm bảo hook procedure được thực thi trong một khoảng thời gian nhất định, ta sẽ khởi tạo một thread riêng biệt và sử dụng hàm WaitForSingleObject
để duy trì hoạt động của thread trong khoảng thời gian mong muốn thay vì thực thi ở trên main thread.
int main() {
HANDLE hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)MouseClicksLogger, NULL, NULL, NULL);
if (hThread)
WaitForSingleObject(hThread, 10000); // Monitor mouse clicks for 10 seconds
return 0;
}
Hàm WaitForSingleObject
sẽ khiến cho main thread chờ 10 giây để cho mouse-logging thread (thread được tạo bởi hàm CreateThread
) hoàn thành.
Tuy nhiên, do mouse-logging thread chứa một vòng lặp vô hạn (để cho hook procedure có thể tái thực thi), nó sẽ tiếp tục chạy sau khi kết thúc khoảng thời gian này.
Nói cách khác, nếu có thêm các câu lệnh khác ở bên dưới lời gọi hàm WaitForSingleObject
chẳng hạn như getchar()
, chương trình vẫn xử lý các message liên quan đến chuột kể cả khi đã qua 10 giây.
Improving The Implementation
Mỗi cửa sổ trong hệ điều hành Windows đếu tương ứng với một window procedure - là hàm chịu trách nhiệm xử lý tất các message được gửi đến hoặc gửi đi của một cửa sổ. Mọi khía cạnh liên quan đến cách hiển thị của cửa sổ và hành vi của nó đều phụ thuộc vào cách mà window procedure xử lý các message.
Seealso
Ngoài ra, các cửa sổ trong hệ điều hành Windows còn hoạt động dựa trên một message loop (hay message pump) để nhận, xử lý và gửi đi các message (bao gồm cả các message liên quan đến chuột). Khi một hook được cài đặt vào một message, Windows trông chờ thread cài đặt hook đó sẽ chạy một message loop bằng cách gọi các hàm chẳng hạn như GetMessage
, PeekMessage
, DispatchMessage
và TranslateMessage
để xử lý message.
Fail
Có thể thấy, hàm
MouseClicksLogger
ở trên không thực hiện xử lý các message liên quan đến chuột và điều này khiến cho cử động chuột bị lag khi chạy chương trình.
Để giải quyết vấn đề này, chúng ta có thể gọi hàm DefWindowProcW
để chuyển giao việc xử lý các message đến hệ điều hành. Về bản chất, DefWindowProcW
sẽ gọi window procedure mặc định để cung cấp cách xử lý mặc định cho tất cả các message mà một ứng dụng không xử lý.
Để sử dụng DefWindowProcW
, ta cần lấy ra thông tin chi tiết về message từ hàng đợi message bằng cách sử dụng hàm GetMessageW
.
BOOL MouseClicksLogger(){
MSG Msg = { 0 };
// ...
// Process unhandled events
while (GetMessageW(&Msg, NULL, NULL, NULL)) {
DefWindowProcW(Msg.hwnd, Msg.message, Msg.wParam, Msg.lParam);
}
return TRUE;
}
Hàm GetMessageW
sẽ về thông tin của message thông qua một cấu trúc MSG
. Cấu trúc này có đầy đủ thông tin cần thiết cho lời gọi hàm DefWindowProcW
:
Ngoài DefWindowProcW
, ta còn có thể gọi TranslateMessage
và DispatchMessageW
:
while (GetMessageW(&Msg, NULL, NULL, NULL)) {
TranslateMessage(&Msg);
DispatchMessageW(&Msg);
}
Removing Hooks
Để gỡ bỏ hook, ta sẽ sử dụng hàm UnhookWindowsHookEx
và truyền vào handle của hook cần gỡ bỏ (là giá trị trả về của SetWindowsHookExW
).