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:

  1. Sử dụng các thư viện mã nguồn mở chẳng hạn như Detours của Microsoft hay Minhook.
  2. Sử dụng Windows API (bị giới hạn hơn).
  3. 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_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êm 0x5 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ành 0x0).

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úc HOOK_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úc HOOK_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_STATUSMH_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_InitializeMH_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_tuint32_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ào NULL.
  • 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ố wParamlParam 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, DispatchMessageTranslateMessage để 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 TranslateMessageDispatchMessageW:

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).

Resources

Footnotes

  1. Xem thêm: Virtual Address Spaces - Windows drivers | Microsoft Learn