深入淺出HOOK API及完美應用

Hook Win32 API 是一項有趣而實用的WINDOWS系統編程技術,應用領域十分廣泛。雖然已經有不少的文章介紹過 Hook Win32 API 的方法了,我還是來作些簡單的介紹,以便大家瞭解其工作原理。

    Hook Win32 API 是什麼意思?就是鉤住Win32 API;那又何謂“鉤”呢?就是繞彎的意思,讓Win32 API函數的調用先繞一個彎路,在它執行實際功能之前,我們可以先做一些“預處理”,這樣我們可以監視或定製某個Win32 API的調用,以實現一些特殊的功能。至於具體可以實現些什麼樣的功能,那就取決於程序設計者的想象力了。

    爲什麼要Hook Win32 API呢?因爲在很多情況下,我們想監視或改變某個應用程序的一些特定的行爲,但是那個應用程序卻沒有提供相應的接口,而我們又幾乎不可能得到其源代碼,怎麼辦呢?因爲大多數WINDOWS引用程序的行爲很大程度上依賴於Win32 API,所以我們可以採用Hook Win32 API的方式來試圖監視和改變應用程序的行爲。

    如何Hook Win32 API呢?實際上Win32 API是由一組動態鏈接庫實現的,使用動態鏈接庫是爲了儘可能的共享內存。由於動態鏈接庫是動態裝入的,所以Win32 API函數的入口點也是動態確定的。當WINDOWS應用程序在調用Win32 API的時候,並不是直接調用某個函數地址,而是調用某處所存儲的一個動態確定的函數地址來實現間接調用地,該處被命名爲Import Address Table(簡稱IAT)。知道了這一點,接下來要做的就是想辦法找到這個存儲單元的位置,然後將其內容替換爲接管函數的入口地址,不過得事先保存原函數的入口地址,以便執行了接管函數的代碼後,可以在適當的地方以適當的方式再調用原函數。最後退出的時候或是不想再鉤着它的時候,再將其恢復爲原函數的入口地址。這就是Hook Win32 API的基本步驟,具體實現過程這裏就不贅述了,可以參閱《WINDOWS 核心編程》(Jeffrer Richter著)

    向大家強烈推薦由微軟開發的一個用於Hook Win32 API的庫(有源代碼):Detours 1.5 - 微軟對自家的操作系統當然是最瞭解的了,相信Detours能在WINDOWS平臺上運行得很好。點擊這裏下載:

Detours 1.5 (538KB)    http://ahzhuo.diy.myrice.com/download/detours.zip

 

 

Hook Win32 API 的應用研究之一:網絡監控》
 絕大多數具有網絡功能的軟件都是基於socket(網絡套接字)實現的,或者是使用了更高層的接口(例如:WinInet API)而最底層仍然是基於socket實現的。在大多數操作系統中都實現了socket接口,在WINDOWS操作系統中的實現稱爲WinSock。WinSock是以DLL的形式實現的,現在WinSock有兩個版本的實現:WinSock 1.1(winsock.dll)和WinSock 2(ws2_32.dll),ws2_32.dll既支持WinSock 1.1的函數又支持WinSock 2規範中增加的許多額外的函數,我們可以像Win32 API一樣的使用它,只是需要額外鏈接一個庫而已。這裏不討論具體的WinSock編程,只是讓大家瞭解,WinSock是WINDOWS應用程序與網絡打交道的接口,是我們實現網絡監控這個目的的突破口。

    好了,那我們就開始吧!“網絡監控”這個範圍有點太泛了,我們先把範圍縮小到監控網絡連接請求這個具體的操作上面吧,這也就是我的作品:IPGate 網址過濾器 的核心技術。我們先來看看一個TCP/IP連接是如何建立的:

    客戶機端               服務器端
    ========               ========
                   監聽套接字     連接套接字
                   ==========     ==========
    socket()       socket()
    bind()         bind()
                   listen()
    connect()----->accept()------>創建連接套接字
    send()----------------------->recv()
    recv()<-----------------------send()
         .
         .
         .
    closesocket()  closesocket()  closesocket()

    我們可以看出,是客戶機端的connect()執行實際的連接請求動作,我們再來看看connect函數的參數:

int connect(
  SOCKET s, // 指定對哪個套接字進行操作
  const struct sockaddr FAR *name, // 這是一個描述服務器IP地址的結構
  int namelen // 指明上面這個結構的大小
);

對於name參數,由於sockaddr結構內容依賴於具體的協議,所以對於TCP/IP協議,我們傳遞sockaddr_in這個結構,再來看看這個結構:

struct sockaddr_in{
  short           sin_family; // 必須爲AF_INET
  unsigned short  sin_port; // IP端口號
  struct in_addr  sin_addr; // 標識IP地址的一個結構體
  char            sin_zero[8]; // 爲了兼容sockaddr而設置的佔位空間
};

    到這兒,我們可以看出,對於一次連接請求的目的地信息,已經全部在傳入的參數中描述清楚了,接下來要做的就設置一個全局API鉤子,鉤住所有程序的connect()調用,在進行實際的connect()操作之前,我們先分析傳入的參數,如果發現連接目的地是我們不允許訪問的,就不進行連接操作,僅返回一個錯誤碼就可以了。就這麼簡單,就能實現一夫當關,萬夫莫開的效果。

    同樣的道理,也可以Hook其它函數而實現監控整個網絡通訊各方面的內容,比如說截取發送和接收的數據包進行分析等等,這就取決於設計者的意圖了,大家不妨動手試試看,感受一下Hook API的魅力。

 

 

 


Hook Win32 API 的應用研究之二:進程防殺
在WINDOWS操作系統下,當我們無法結束或者不知道怎樣結束一個程序的時候,或者是懶得去找“退出”按鈕的時候,通常會按“CTRL+ALT+DEL”呼出任務管理器,找到想結束的程序,點一下“結束任務”就了事了,呵呵,雖然有點粗魯,但大多數情況下都很有效,不是嗎?

    設想一下,如果有這麼一種軟件,它所要做的工作就是對某個使用者在某臺電腦上的活動作一定的限制,而又不能被使用者通過“結束任務”這種方式輕易地解除限制,那該怎麼做?無非有這麼三種方法:1.屏蔽“CTRL+ALT+DEL”這個熱鍵的組合;2.讓程序不出現在任務管理器的列表之中;3.讓任務管理器無法殺掉這個任務。對於第一種方法,這樣未免也太殘酷了,用慣了“結束任務”這種方法的人會很不習慣的;對於第二址椒ǎ赬INDOWS 9X下可以很輕易地使用註冊服務進程的方法實現,但是對於WINDOWS NT架構的操作系統沒有這個方法了,進程很難藏身,雖然仍然可以實現隱藏,但實現機制較爲複雜;對於第三種方法,實現起來比較簡單,我的作品:IPGate 網址過濾器 就是採用的這種方式防殺的,接下來我就來介紹這種方法。

    任務管理器的“結束任務”實際上就是強制終止進程,它所使用的殺手鐗是一個叫做TerminateProcess()的Win32 API函數,我們來看看它的定義:

BOOL TerminateProcess(
  HANDLE      hProcess; // 將被結束進程的句柄
  UINT        uExitCode; // 指定進程的退出碼
);

    看到這裏,是不是覺得不必往下看都知道接下來要做什麼:Hook TerminateProcess()函數,每次TerminateProcess()被調用的時候先判斷企圖結束的進程是否是我的進程,如果是的話就簡單地返回一個錯誤碼就可以了。真的是這麼簡單嗎?先提出一個問題,如何根據hProcess判斷它是否是我的進程的句柄?答案是:在我的進程當中先獲得我的進程的句柄,然後通過進程間通訊機制傳遞給鉤子函數,與hProcess進行比較不就行了?錯!因爲句柄是一個進程相關的值,不同進程中得到的我的進程的句柄的值在進程間進行比較是無意義的。

    怎麼辦?我們來考察一下我的hProcess它是如何得到的。一個進程只有它的進程ID是獨一無二的,操作系統通過進程ID來標識一個進程,當某個程序要對這個進程進行訪問的話,它首先得用OpenProcess這個函數並傳入要訪問的進程ID來獲得進程的句柄,來看看它的參數:

HANDLE OpenProcess(
DWORD      dwDesiredAccess, // 希望獲得的訪問權限
BOOL       bInheritHandle, // 指明是否希望所獲得的句柄可以繼承
DWORD      dwProcessId // 要訪問的進程ID
);

    脈絡漸漸顯現:在調用TerminateProcess()之前,必先調用OpenProcess(),而OpenProcess()的參數表中的dwProcessId是在系統範圍內唯一確定的。得出結論:要Hook的函數不是TerminateProcess()而是OpenProcess(),在每次調用OpenProcess()的時候,我們先檢查dwProcessId是否爲我的進程的ID(利用進程間通訊機制),如果是的話就簡單地返回一個錯誤碼就可以了,任務管理器拿不到我的進程的句柄,它如何結束我的進程呢?

    至此,疑團全部揭開了。由Hook TerminateProcess()到Hook OpenProcess()的這個過程,體現了一個逆向思維的思想。其實我當初鑽進了TerminateProcess()的死衚衕裏半天出也不來,但最終還是蹦出了靈感的火花,注意力轉移到了OpenProcess()上面,實現了進程防殺。喜悅之餘,將這心得體會拿出來與大家分享。

 

 

Hook Win32 API 的應用研究之三:變速控制 <<< 
    這是Hook Win32 API的一個比較另類和有趣的應用方面。

    這裏所指的變速控制,並不是說可以改變任何程序的運行速度,只能改變符合這些條件的程序的運行速度:程序的運行速度依賴於定時控制,也就是說,程序的執行單元執行的頻率是人爲的依靠定時機制控制的,不是依賴於CPU的快慢。比如說,某個程序每隔1秒鐘發出“滴答”聲,它在快的電腦上和慢的電腦上所表現出來的行爲是一致的。這樣的依賴於定時控制的程序纔是我們的研究“變速”對象。

    一個WINDOWS應用程序的定時機制有很多。像上面提到的例子程序可以採用WM_TIMER消息來實現,通過函數SetTimer()可以設定產生WM_TIMER消息的時間間隔。其它的方法還有通過GetTickCount()和timeGetTime()等函數得到系統時間,然後通過比較時間間隔來定時,還有timerSetEvent()設置時鐘事件等等方式。先來看看這些函數的定義:

UINT_PTR SetTimer(
  HWND      hWnd, // 接收WM_TIMER消息的窗口句柄
  UINT_PTR  nIDEvent, // 定時器的ID號
  UINT      uElapse, // 發生WM_TIMER消息的時間間隔
  TIMERPROC lpTimerProc // 處理定時發生時的回調函數入口地址
);

MMRESULT timeSetEvent(
  UINT               uDelay, // 時鐘事件發生的時間間隔
  UINT               uResolution, // 設置時鐘事件的分辨率
  LPTIMERCALLBACK    lpTimerProc, // 處理時鐘事件發生時的回調函數入口地址
  DWORD              dwUser, // 沒峁┑幕氐魘?br />  UINT               fuEvent // 設置事件的類型
);

DWORD GetTickCount(VOID) // 返回系統啓動以來經過了多少毫秒了

DWORD timeGetTime(VOID) // 類似於GetTickCount(),但分辨率更高

    那麼我們來看,如果能控制SetTimer()的uElapse參數、timeSetEvent()的uDelay參數、GetTickCount()和timeGetTime()的返回值,就能實現變速控制,除非應用程序使用的是其它的定時機制,不過大多數應用程序採用的定時機制不外乎都是這些。

    該輪到Hook大法出場了。因爲我們一般只想改變某個程序的速度,比如是說某個遊戲程序,所以我們不設置全局鉤子。又因爲我們不清楚那個應用程序到底使用的是那種定時機制,所以上述幾個函數我們全部都要接管,然後把關於定時參數或返回值按比例縮放就可以了。

    順藤摸瓜,是不是很簡單?
 

 

 

 

 

Hook Win32 API 的應用研究之四:屏幕取詞 <<< 
    用過金山詞霸吧?用過的人一定對它的屏幕取詞功能印象很深刻,因爲這種功能使翻譯過程更加簡便快捷,屏幕取詞是金山詞霸的核心技術之一。

    大家有沒有想過這樣神奇的功能是如何實現的呢?經歷過DOS年代系統編程的人可能知道,屏幕上顯示的字符是存放在顯存裏的,每個座標的字符對應顯存的一個特定的現存單元存儲的字符,直接操作顯存,就可以進行字符的顯示和讀取,若WINDOWS是這樣就好了,可惜事實上相去甚遠。那WINDOWS的字符是怎樣顯示的呢?WINDOWS是圖形界面,顯示的最小單位是像素(Pixel),上面的所有東西都是“畫”上去的,當然也包括了字符,也就沒有什麼字符顯存的概念了。沒有了直接操作顯存而獲得屏幕上字符內容的辦法了,那還有什麼方法呢?

    讓我們來設身處地地想想看,假如我們要在自己的程序中顯示一個字符串,我們會怎樣做呢?不要回答是MessageBox(),我們不是指的這種“顯示”方法,我指的是最低階的方法,也就是直接操作DC的方法,我想一般就是調用上面提到過的Win32 API函數TextOut()了,當然,還有類似的一些其它函數,例如:ExtTextOut()、DrawText()、DrawTextEx()等等。好了,找到點眉目了,我們來看看這些函數的參數能提供哪些信息,這裏只列出TextOut()函數的定義,其它的函數基本都包含這些參數,另外提供了更多的附加選項而已,請查閱MSDN相關文檔:

BOOL TextOut(
  HDC       hdc, // 設備上下文句柄
  int       nXStart, // 開始繪製字符串的位置的x座標
  int       nYStart, // 開始繪製字符串的位置的y座標
  LPCTSTR   lpString, // 指向字符串的指針
  int       cbString // 指明要繪製多少個字符
);

    我們看到,座標和內容都有了,這不正是我們想要的信息嗎?只要Hook住這個函數,這些信息不都唾手可得了嗎?於是祭出Hook大法來做個實驗:先隨便用VC的嚮導開闢一個單文檔應用程序,在OnDraw()函數裏調用TextOut()在某個位置隨便輸出一個字符串(不論是調用pDC->TextOut(...)或者是::TextOut(...)都一樣,CDC類只不過把TextOut()封裝了一下而已),然後在OnInitialUpdate()裏設置Hook(用現成的庫),鉤住TextOut(),截獲TextOut之後,讓TextOut()輸出另外一個字符串而不輸出原來的字符串。還要記住在OnDestroy()裏解除Hook。最後編譯連接,測試程序。你會發現不僅是你調用TextOut()輸出的地方的字符串被替換了,而且連才旦、對話框等等有字的地方也變了,在實驗成功之餘,是不是個意外的收穫?其實WINDOWS內部的大多數文字輸出也是調用了TextOut()函數來實現的。現在水落石出了,我們只要Hook住文字輸出函數,包括我上面提到的和沒有提到的函數,就能截獲屏幕上文字輸出的座標和內容等等信息,只要我們一一作記錄,並加以分析轉換,跟鼠標的位置進行比較,我們就能得到屏幕上某個位置的文字內容是什麼了,要翻譯怎麼的,就看你的了,這就是屏幕取詞,雖然實際上實現的過程並不像說得那麼簡單。

    出了詞霸的屏幕取詞,還有一些動態漢化、外掛中文平臺之類的軟件,也是基於這種技術的,現在看來,它們是不是已經不再神祕了? 

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章