勾點編程

勾點編程(hooking),也稱作「掛鈎」,是電腦程式設計術語,指通過攔截軟件模組間的函數呼叫、訊息傳遞、事件傳遞來修改或擴充作業系統、應用程式或其他軟件組件的行為的各種技術。處理被攔截的函數呼叫、事件、訊息的代碼,被稱為勾點(hook)。

勾點編程有多種用途,如除錯、擴充功能。例如在鍵盤或滑鼠事件到達應用程式之前攔截這些事件;攔截應用程式或其他模組的作業系統呼叫以監督行為、修改功能。也廣泛用於benchmarking程式,如度量3D遊戲的幀率

勾點編程也被用於惡意代碼,如rootkit是各種通過假扮系統API呼叫輸出來隱藏自身行程的可見性的工具與技術;遊戲外掛是另一類例子。

方法

物理修改

在應用程式執行之前,物理修改可執行程式,這典型通過找到函數呼叫入口點,修改入口點使之在函數被執行前先執行其他的代碼。另一種掛鈎的方法是修改可執行程式的輸入表(import table)。還有一種掛鈎方法是採用包裝庫,使得應用程式不需修改即可呼叫該包裝庫完成其功能,而在包裝庫中插入勾點然後再呼叫原有的庫。

執行時修改

作業系統與軟件可提供方法,在執行時插入事件勾點。Microsoft Windows允許插入勾點以處理或修改對話方塊、捲軸、選單等的系統事件、應用程式事件;插入、刪除、處理或修改鍵盤滑鼠事件。Linux允許類似的勾點通過NetFilter以處理網絡事件。

如果沒有上述機制或權限,也可攔截行程的庫函數呼叫,在函數呼叫開始處注入代碼。這可通過修改主記憶體中的行程的中斷向量表英語Interrupt vector table輸入表(import table)實現。

範例代碼

虛擬函式表掛鈎

C++使用虛擬函式,因此可在執行時直接修改虛擬函式表的內容來掛鈎。 Window上很多軟件庫以COM方式提供的(如DirectX), 所以有需求攔截COM呼叫的COM Hook。COM里的介面是C++虛表的形式提供的,所以COM的Hook其實就是虛表(vtable)的Hook。ATL就是用介面替代的方式來除錯和記錄COM介面參照計數的次數

class VirtualTable {  // example class  
 public:  
 virtual void VirtualFunction01( ticket );  
};

void VirtualTable::VirtualFunction01( ticket )  {  
  printf("VirtualFunction01 called");  
}

typedef void ( __thiscall* VirtualFunction01_t )( ticket* thisptr );  
VirtualFunction01_t g_org_VirtualFunction01;

 //our detour function  
void __fastcall hk_VirtualFunction01( ticket* thisptr, int edx )  {  
  printf("Custom function called");  
  //call the original function  
  g_org_VirtualFunction01(thisptr);  
}  
int _tmain(int argc, _TCHAR* argv[])  {

 DWORD oldProtection;

 VirtualTable* myTable = new VirtualTable();  
 void** base = *(void***)myTable;

 VirtualProtect( &base[0], 4, PAGE_EXECUTE_READWRITE, &oldProtection );  
 //save the original function  
 g_org_VirtualFunction01 = (VirtualFunction01_t)base[0];  
 //overwrite  
 base[0] = &hk_VirtualFunction01;  
 VirtualProtect( &base[0], 4, oldProtection, 0 );

 //call the virtual function (now hooked) from our class instance  
 myTable->VirtualFunction01();

 return 0;  
}

C#鍵盤事件勾點

using System.Runtime.InteropServices;

namespace Hooks
{
    public class KeyHook
    {
        /* Member variables */
        protected static int Hook;
        protected static LowLevelKeyboardDelegate Delegate;
        protected static readonly object Lock = new object();
        protected static bool IsRegistered = false;

        /* DLL imports */
        [DllImport("user32")]
        private static extern int SetWindowsHookEx(int idHook, LowLevelKeyboardDelegate lpfn,
            int hmod, int dwThreadId);

        [DllImport("user32")]
        private static extern int CallNextHookEx(int hHook, int nCode, int wParam, KBDLLHOOKSTRUCT lParam);

        [DllImport("user32")]
        private static extern int UnhookWindowsHookEx(int hHook);

        /* Types & constants */
        protected delegate int LowLevelKeyboardDelegate(int nCode, int wParam, ref KBDLLHOOKSTRUCT lParam);
        private const int HC_ACTION = 0;
        private const int WM_KEYDOWN = 0x0100;
        private const int WM_KEYUP = 0x0101;
        private const int WH_KEYBOARD_LL = 13;

        [StructLayout(LayoutKind.Sequential)]
        public struct KBDLLHOOKSTRUCT
        {
            public int vkCode;
            public int scanCode;
            public int flags;
            public int time;
            public int dwExtraInfo;
        }

        /* Methods */
        private static int LowLevelKeyboardHandler(int nCode, int wParam, ref KBDLLHOOKSTRUCT lParam)
        {
            if (nCode == HC_ACTION)
            {
                if (wParam == WM_KEYDOWN)
                    System.Console.Out.WriteLine("Key Down: " + lParam.vkCode);
                else if (wParam == WM_KEYUP)
                    System.Console.Out.WriteLine("Key Up: " + lParam.vkCode);
            }
            return CallNextHookEx(Hook, nCode, wParam, lParam);
        }
        
        public static bool RegisterHook()
        {
            lock (Lock)
            {
                if (IsRegistered)
                    return true;
                Delegate = LowLevelKeyboardHandler;
                Hook = SetWindowsHookEx(
                    WH_KEYBOARD_LL, Delegate,
                    Marshal.GetHINSTANCE(
                        System.Reflection.Assembly.GetExecutingAssembly().GetModules()[0]
                    ).ToInt32(), 0
                );

                if (Hook != 0)
                    return IsRegistered = true;
                Delegate = null;
                return false;
            }
        }

        public static bool UnregisterHook()
        {
            lock (Lock)
            {
                return IsRegistered = (UnhookWindowsHookEx(Hook) != 0);
            }
        }
    }
}

使用跳轉指令的API/函數勾點攔截

下述例子使用JMP英語JMP (x86 instruction)指令,修改windows API中的MessageBoxW函數的前6個位元組執行其他代碼。這段代碼編譯為DLL檔案,採用DLL注入技術讓應用程式使用。[1]微軟提供了封裝好的Detours庫用於此目的。

/*
 This idea is based on chrom-lib approach, Distributed under GNU LGPL License.
 Source chrom-lib: https://github.com/linuxexp/chrom-lib
 Copyright (C) 2011  Raja Jamwal
*/
#include <windows.h>  
#define SIZE 6

 typedef int (WINAPI *pMessageBoxW)(HWND, LPCWSTR, LPCWSTR, UINT);  // Messagebox prototype
 int WINAPI MyMessageBoxW(HWND, LPCWSTR, LPCWSTR, UINT);            // Our detour

 void BeginRedirect(LPVOID);                                        
 pMessageBoxW pOrigMBAddress = NULL;                                // address of original
 BYTE oldBytes[SIZE] = {0};                                         // backup
 BYTE JMP[SIZE] = {0};                                              // 6 byte JMP instruction
 DWORD oldProtect, myProtect = PAGE_EXECUTE_READWRITE;

 INT APIENTRY DllMain(HMODULE hDLL, DWORD Reason, LPVOID Reserved)  
 {  
   switch(Reason)  
   {  
   case DLL_PROCESS_ATTACH:                                        // if attached
     pOrigMBAddress = (pMessageBoxW)                      
       GetProcAddress(GetModuleHandle("user32.dll"),               // get address of original 
               "MessageBoxW");  
     if(pOrigMBAddress != NULL)  
       BeginRedirect(MyMessageBoxW);                               // start detouring
     break;

   case DLL_PROCESS_DETACH:  
     memcpy(pOrigMBAddress, oldBytes, SIZE);                       // restore backup

   case DLL_THREAD_ATTACH:  
   case DLL_THREAD_DETACH:  
     break;  
   }  
   return TRUE;  
 }

 void BeginRedirect(LPVOID newFunction)  
 {  
   BYTE tempJMP[SIZE] = {0xE9, 0x90, 0x90, 0x90, 0x90, 0xC3};         // 0xE9 = JMP 0x90 = NOP 0xC3 = RET
   memcpy(JMP, tempJMP, SIZE);                                        // store jmp instruction to JMP
   DWORD JMPSize = ((DWORD)newFunction - (DWORD)pOrigMBAddress - 5);  // calculate jump distance
   VirtualProtect((LPVOID)pOrigMBAddress, SIZE,                       // assign read write protection
           PAGE_EXECUTE_READWRITE, &oldProtect);  
   memcpy(oldBytes, pOrigMBAddress, SIZE);                            // make backup
   memcpy(&JMP[1], &JMPSize, 4);                              // fill the nop's with the jump distance (JMP,distance(4bytes),RET)
   memcpy(pOrigMBAddress, JMP, SIZE);                                 // set jump instruction at the beginning of the original function
   VirtualProtect((LPVOID)pOrigMBAddress, SIZE, oldProtect, NULL);    // reset protection
 }

 int WINAPI MyMessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uiType)  
 {  
   VirtualProtect((LPVOID)pOrigMBAddress, SIZE, myProtect, NULL);     // assign read write protection
   memcpy(pOrigMBAddress, oldBytes, SIZE);                            // restore backup
   int retValue = MessageBoxW(hWnd, lpText, lpCaption, uiType);       // get return value of original function
   memcpy(pOrigMBAddress, JMP, SIZE);                                 // set the jump instruction again
   VirtualProtect((LPVOID)pOrigMBAddress, SIZE, oldProtect, NULL);    // reset protection
   return retValue;                                                   // return original return value
 }

Netfilter勾點

使用Netfilter勾點修改Linux內核的網絡交通。

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/skbuff.h>

#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/in.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>

/* Port we want to drop packets on */
static const uint16_t port = 25;

/* This is the hook function itself */
static unsigned int hook_func(unsigned int hooknum,
                       struct sk_buff **pskb,
                       const struct net_device *in,
                       const struct net_device *out,
                       int (*okfn)(struct sk_buff *))
{
        struct iphdr *iph = ip_hdr(*pskb);
        struct tcphdr *tcph, tcpbuf;

        if (iph->protocol != IPPROTO_TCP)
                return NF_ACCEPT;

        tcph = skb_header_pointer(*pskb, ip_hdrlen(*pskb), sizeof(*tcph), &tcpbuf);
        if (tcph == NULL)
                return NF_ACCEPT;

        return (tcph->dest == port) ? NF_DROP : NF_ACCEPT;
}

/* Used to register our hook function */
static struct nf_hook_ops nfho = {
        .hook     = hook_func,
        .hooknum  = NF_IP_PRE_ROUTING,
        .pf       = NFPROTO_IPV4,
        .priority = NF_IP_PRI_FIRST,
};

static __init int my_init(void)
{
        return nf_register_hook(&nfho);
}

static __exit void my_exit(void)
{
    nf_unregister_hook(&nfho);
}

module_init(my_init);
module_exit(my_exit);

內部IAT勾點

下例展示通過修改可執行程式的輸入地址表(IAT),把勾點函數替代原函數。

#include <windows.h>

typedef int(__stdcall *pMessageBoxA) (HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType); //This is the 'type' of the MessageBoxA call.
pMessageBoxA RealMessageBoxA; //This will store a pointer to the original function.

void DetourIATptr(const char* function, void* newfunction, HMODULE module);

int __stdcall NewMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) { //Our fake function
    printf("The String Sent to MessageBoxA Was : %s\n", lpText);
    return RealMessageBoxA(hWnd, lpText, lpCaption, uType); //Call the real function
}

int main(int argc, CHAR *argv[]) {
   DetourIATptr("MessageBoxA",(void*)NewMessageBoxA,0); //Hook the function
   MessageBoxA(NULL, "Just A MessageBox", "Just A MessageBox", 0); //Call the function -- this will invoke our fake hook.
   return 0;
}

void **IATfind(const char *function, HMODULE module) { //Find the IAT (Import Address Table) entry specific to the given function.
	int ip = 0;
	if (module == 0)
		module = GetModuleHandle(0);
	PIMAGE_DOS_HEADER pImgDosHeaders = (PIMAGE_DOS_HEADER)module;
	PIMAGE_NT_HEADERS pImgNTHeaders = (PIMAGE_NT_HEADERS)((LPBYTE)pImgDosHeaders + pImgDosHeaders->e_lfanew);
	PIMAGE_IMPORT_DESCRIPTOR pImgImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)((LPBYTE)pImgDosHeaders + pImgNTHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);

	if (pImgDosHeaders->e_magic != IMAGE_DOS_SIGNATURE)
		printf("libPE Error : e_magic is no valid DOS signature\n");

	for (IMAGE_IMPORT_DESCRIPTOR *iid = pImgImportDesc; iid->Name != NULL; iid++) {
		for (int funcIdx = 0; *(funcIdx + (LPVOID*)(iid->FirstThunk + (SIZE_T)module)) != NULL; funcIdx++) {
			char *modFuncName = (char*)(*(funcIdx + (SIZE_T*)(iid->OriginalFirstThunk + (SIZE_T)module)) + (SIZE_T)module + 2);
			const uintptr_t nModFuncName = (uintptr_t)modFuncName;
			bool isString = !(nModFuncName & (sizeof(nModFuncName) == 4 ? 0x80000000 : 0x8000000000000000));
			if (isString) {
				if (!_stricmp(function, modFuncName))
					return funcIdx + (LPVOID*)(iid->FirstThunk + (SIZE_T)module);
			}
		}
	}
	return 0;
}

void DetourIATptr(const char *function, void *newfunction, HMODULE module) {
	void **funcptr = IATfind(function, module);
	if (*funcptr == newfunction)
		 return;

	DWORD oldrights, newrights = PAGE_READWRITE;
	//Update the protection to READWRITE
	VirtualProtect(funcptr, sizeof(LPVOID), newrights, &oldrights);

	RealMessageBoxA = (pMessageBoxA)*funcptr; //Some compilers require the cast like "MinGW" not sure about MSVC
	*funcptr = newfunction;

	//Restore the old memory protection flags.
	VirtualProtect(funcptr, sizeof(LPVOID), oldrights, &newrights);
}

Windows API提供的掛鈎函數

  • SetWinEventHook:基本沒有權限問題,也就是說這個API可以Hook到高權限程式的事件,同時支援行程內(WINEVENT_INCONTEXT)和行程外(WINEVENT_OUTOFCONTEXT)2種Hook方式,可以行程外的方式Hook到64位元程式的事件。
  • SetWindowsHookEx:64位元編程情形,32位元DLL沒法直接注入到64位元的應用程式裏面, 因為地址空間完全不一樣。UAC打開的情況下低權限程式沒法Hook高權限程式。對於64位元問題,解決方法是提供2個DLL,分別可以Hook32和64位元程式。對於權限問題,通過註冊系統服務,由服務行程建立工作行程。因為Windows Vista開始有了Session隔離機制,服務行程執行在Session 0,用戶程式執行在Session 1, Session 2等,如果直接在服務程式里幹活,我們就只能在Session 0里工作。通過建立行程,可以在DuplicateTokenEx後將Token的SessionID設置成目標Session,並且在CreateProcessAsUser時指定目標WinStation和Desktop, 這樣就獲得了System權限,並且也可以和當前桌面行程互動。

參見

參考文獻

外部連結

Windows

Linux

  • [1] A student research project that utilizes hooking.
  • [2] Functionality that allows a piece of software to observe and control the execution of another process.
  • [3]頁面存檔備份,存於互聯網檔案館) Use of LD_PRELOAD to hook shared library calls.

Emacs

  • Emacs Hooks頁面存檔備份,存於互聯網檔案館) Hooks are an important mechanism for customization of Emacs. A hook is a Lisp variable which holds a list of functions, to be called on some well-defined occasion. (This is called running the hook.)

OS X 與 iOS

Depth API Hooking