自己從高中那時對黑客比較感興趣,那時候去過很多論壇學所謂的“黑客技術”。同時也經常在一些QQ羣,論壇裏混,但見效甚微。
因爲我很喜歡玩遊戲,上大學後,同學們都發瘋似的玩一款遊戲—三國殺,現在玩的人已經很少了,被某公司抄襲後壟斷了。由於當時學業繁重,然後自己又愛面子,所以在想如何學業不落下的同時遊戲等級也能比別人高,上網找到了一款三國殺的刷分軟件。興高采烈得打開準備刷分,用了一天之後就不能用了,30元一個月的收費標準,窮學生哪能捨得啊?於是想找人破解,但轉念一想,誰會幫你破解呢?還是自己來吧~自己動手,豐衣足食。上網+圖書館瘋狂找資料,花了一個週末,把這個軟件KO了,當時是高興死了。那種成就感和興奮感恐怕也只有親身試過的人才知道。很快就刷到了150級了,比整天玩遊戲的同學等級還要高,在滿足了我虛榮心的同時也帶我走向了逆向之門,或許這就是一種緣分吧。
好了,背景就不多說了。今天和大家一起學習一下C++中的HOOK API技術。相信大家都知道這是逆向PJ中的一個特別重要也是實用的技術。(*^__^*)
一、 Hook介紹
鉤子(Hook),是Windows消息處理機制的一個平臺,應用程序可以在上面設置子程以監視指定窗口的某種消息,而且所監視的窗口可以是其他進程所創建的。當消息到達後,在目標窗口處理函數之前處理它。鉤子機制允許應用程序截獲處理window消息或特定事件。
我們知道Windows系統API函數都是被封裝到DLL中,在某個應用程序要調用一個API函數的時候,如果這個函數所在的DLL沒有被加載到本進程中則加載它,然後保存當前環境(各個寄存器和函數調用完後的返回地址等)。接着程序會跳轉到這個API函數的入口地址去執行此處的指令。由此看來,我們想在調用真正的API之前先調用我們的函數,那麼可以修改這個API函數的入口處的代碼,使他先跳轉到我們的函數地址,然後在我們的函數最後再調用原來的API函數。
簡單來說HOOK API 可以理解成對程序將要執行系統函數的一個攔截, 攔截後執行自己寫的代碼以達到完成某種特定的目的,再恢復程序繼續執行,很多PJ中的Patch 機器碼,盜號木馬等都是用這個方法。
二、 Hook API實戰
OK,既然我們需要實現一個這樣的一個HOOK。當然需要兩樣東西,一是目標程序,一是我們的代碼。
1. 程序:
新建一個dll工程文件目錄如圖:
附件 93152
我們自己新建一個Add.def文件,然後添加到工程中即可,如圖我是添加到Source Files裏。
跟exe有個main或者WinMain入口函數一樣,DLL也有它自己的一個入口函數,就是DllMain。
我們打開dllmain.cpp 代碼如下:
// dllmain.cpp : Defines the entry point for the DLL application.
#include "stdafx.h"
int WINAPI add(int a, int b)
{
return a + b;
}
BOOL APIENTRY DllMain(HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
return TRUE;
}
DEF文件是模塊定義文件,模塊定義 (.def) 文件爲鏈接器提供有關被鏈接程序的導出、屬性及其他方面的信息。生成 DLL 時,.def 文件最有用。由於存在可代替模塊定義語句使用的鏈接器選項,通常不需要 .def 文件。也可以將 __declspec(dllexport) 用作指定導出函數的手段。在鏈接器階段可以使用 /DEF(指定模塊定義文件)鏈接器選項調用 .def 文件。如果生成的 .exe 文件沒有導出,使用 .def 文件將使輸出文件較大並降低加載速度。在VC++中,生成DLL可以不使用.def文件。只需要在VC++的函數定義前要加__declspec(dllexport)修飾就可以了。但是使用__declspec(dllexport)和使用.def文件是有區別的。如果DLL是提供給VC++用戶使用的,你只需要把編譯DLL時產生的.lib提供給用戶,它可以很輕鬆地調用你的DLL。但是如果你的DLL是供其他程序如VB、delphi,以及.NET用戶使用的,那麼會產生一個小麻煩。因爲VC++對於__declspec(dllexport)聲明的函數會進行名稱轉換,如下面的函數:
__declspec(dllexport) int __stdcallIsWinNT()
會轉換爲IsWinNT@0,這樣你在VB中必須這樣聲明:
Declare Function IsWinNT Lib "my.dll" Alias "IsWinNT@0" () As Long
@的後面的數由於參數類型不同而可能不同。這顯然不太方便。所以如果要想避免這種轉換,就要使用.def文件方式。
EXPORTS後面的數可以不給,系統會自動分配一個數。對於VB、PB、Delphi用戶,通常使用按名稱進行調用的方式,這個數關係不大,但是對於使用.lib鏈接的VC程序來說,不是按名稱進行調用,而是按照這個數進行調用的,所以最好給出。
.def 文件中的第一條 LIBRARY 語句不是必須的,但LIBRARY 語句後面的 DLL 的名稱必須正確,即與生成的動態鏈接庫的名稱必須匹配。此語句將 .def 文件標識爲屬於 DLL。鏈接器將此名稱放到 DLL 的導入庫中。
EXPORTS 語句列出名稱,可能的話還會列出 DLL 導出函數的序號值。通過在函數名的後面加上 @ 符和一個數字,給函數分配序號值。當指定序號值時,序號值的範圍必須是從 1 到 N,其中 N 是 DLL 導出函數的個數。
LIBRARY BTREE
EXPORTS
Insert @1
Delete @2
Member @3
Min @4
如果使用 MFC DLL 嚮導創建 MFC DLL,則嚮導將爲您創建主幹 .def 文件並將其自動添加到項目中。添加要導出到此文件的函數名。對於非 MFC DLL,必須親自創建 .def 文件並將其添加到項目中。
如果導出 C++ 文件中的函數,必須將修飾名放到 .def 文件中,或者通過使用外部“C”定義具有標準 C 鏈接的導出函數。如果需要將修飾名放到 .def 文件中,則可以通過使用 DUMPBIN 工具或 /MAP 鏈接器選項來獲取修飾名。請注意,編譯器產生的修飾名是編譯器特定的。如果將 Visual C++ 編譯器產生的修飾名放到 .def 文件中,則鏈接到 DLL 的應用程序必須也是用相同版本的 Visual C++ 生成的,這樣調用應用程序中的修飾名才能與 DLL 的 .def 文件中的導出名相匹配。
因此def文件代碼如下:
LIBRARY Add
DESCRIPTION "ADD LA"
EXPORTS
add @1;
在如圖位置可以檢驗是否導入了:
由此,一個簡單的dll我們就完成了。
下面用MFC寫一個程序來調用我們的dll。
還是新建一個工程,目錄如下:
MFC的.Cpp中主要代碼如下:
void CMFCApplication5Dlg::OnBnClickedButton1()
{
// TODO: Add your control notification handler code here
HINSTANCE hAddDll = NULL;
typedef int (WINAPI*AddProc)(int a, int b);//函數原型定義
AddProc add;
if (hAddDll == NULL)
{
hAddDll = ::LoadLibrary(_T("Win32DLL.dll"));//加載dll
}
add = (AddProc)::GetProcAddress(hAddDll, "add");//獲取函數add地址
int a = 123;
int b = 456;
int c = add(a, b);
CString tem;
tem.Format(_T("%d+%d=%d"), a, b, c);
AfxMessageBox(tem);
}
效果如圖:出現這個說明你成功了:
一個調用dll中加法函數的MFC程序我們就完成了,下面就需要自己寫一個dll,讓程序執行我們的代碼。
新建一個MFC的 dll工程,工程名爲Hook,然後我們在Hook.cpp文件裏面編寫的代碼如下:
// Hook.cpp : Defines the initialization routines for the DLL.
#include "stdafx.h"
#include "Hook.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
//變量定義
#pragma data_seg("SHARED") //不同Instance共享的該變量
static HHOOK hhk = NULL; //鼠標鉤子句柄
static HINSTANCE hinst = NULL; //本dll的實例句柄 (hook.dll)
#pragma data_seg()
#pragma comment(linker, "/section:SHARED,rws")
//以上的變量爲共享
CString temp; //用於顯示錯誤的臨時變量
bool bHook = false; //是否Hook了函數
bool m_bInjected = false; //是否對API進行了Hook
BYTE OldCode[5]; //原程序API入口代碼
BYTE NewCode[5]; //新跳轉的API代碼 (jmp xxxx)
typedef int (WINAPI*AddProc)(int a, int b);//add.dll中的add函數定義
AddProc add; //add.dll中的add函數
HANDLE hProcess = NULL; //所處進程的句柄
FARPROC pfadd; //指向add函數的遠指針
DWORD dwPid; //所處進程ID
//end of 變量定義
// CHookApp
BEGIN_MESSAGE_MAP(CHookApp, CWinApp)
END_MESSAGE_MAP()
//鼠標鉤子過程,什麼事情也不做,目的是注入dll到程序中
LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
{
return CallNextHookEx(hhk, nCode, wParam, lParam);
}
//開啓鉤子的函數
void HookOn()
{
ASSERT(hProcess != NULL);
DWORD dwTemp = 0;
DWORD dwOldProtect;
//將內存保護模式改爲可寫,老模式保存入dwOldProtect
VirtualProtectEx(hProcess, pfadd, 5, PAGE_READWRITE, &dwOldProtect);
//將所屬進程中add()的前5個字節改爲Jmp Myadd
WriteProcessMemory(hProcess, pfadd, NewCode, 5, 0);
//將內存保護模式改回爲dwOldProtect
VirtualProtectEx(hProcess, pfadd, 5, dwOldProtect, &dwTemp);
bHook = true;
}
//關閉鉤子的函數
void HookOff()//將所屬進程中add()的入口代碼恢復
{
ASSERT(hProcess != NULL);
DWORD dwTemp = 0;
DWORD dwOldProtect;
VirtualProtectEx(hProcess, pfadd, 5, PAGE_READWRITE, &dwOldProtect);
WriteProcessMemory(hProcess, pfadd, OldCode, 5, 0);
VirtualProtectEx(hProcess, pfadd, 5, dwOldProtect, &dwTemp);
bHook = false;
}
//然後,寫我們自己的Myadd()函數
int WINAPI Myadd(int a, int b)
{
//截獲了對add()的調用,我們給a,b都加上一定的數
a = a + 987;
b = b + 654;
HookOff();//關掉Myadd()鉤子防止死循環
int ret;
ret = add(a, b);
HookOn();//開啓Myadd()鉤子
return ret;
}
//好,最重要的HOOK函數:
void Inject()
{
if (m_bInjected == false)
{ //保證只調用1次
m_bInjected = true;
//獲取add.dll中的add()函數
HMODULE hmod = ::LoadLibrary(_T("Win32DLL.dll"));
add = (AddProc)::GetProcAddress(hmod, "add");
pfadd = (FARPROC)add;
if (pfadd == NULL)
{
AfxMessageBox(L"cannot locate add()");
}
// 將add()中的入口代碼保存入OldCode[]
_asm
{
lea edi, OldCode
mov esi, pfadd
cld
movsd
movsb
}
NewCode[0] = 0xe9;//實際上0xe9就相當於jmp指令
//獲取Myadd()的相對地址
_asm
{
lea eax, Myadd
mov ebx, pfadd
sub eax, ebx
sub eax, 5
mov dword ptr[NewCode + 1], eax
}
//填充完畢,現在NewCode[]裏的指令相當於Jmp Myadd
HookOn(); //可以開啓鉤子了
}
}
CHookApp::CHookApp()
{
}
CHookApp theapp;
//鼠標鉤子安裝函數:
BOOL InstallHook()
{
hhk = ::SetWindowsHookEx(WH_MOUSE, MouseProc, hinst, 0);
return true;
}
//卸載鼠標鉤子函數
void UninstallHook()
{
::UnhookWindowsHookEx(hhk);
}
//在dll實例化中獲得一些參數
BOOL CHookApp::InitInstance()
{
CWinApp::InitInstance();
//獲得dll 實例,進程句柄
hinst = ::AfxGetInstanceHandle();
DWORD dwPid = ::GetCurrentProcessId();
hProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, dwPid);
//調用注射函數
Inject();
return TRUE;
}
接着需要配置一下DEF文件:
; Hook.def : Declares the module parameters for the DLL.
LIBRARY "HOOK"
EXPORTS
InstallHook
UninstallHook
最後HOOK成功的效果如圖所示:
值得一提的是很多人都是改API開頭的5個字節,但是現在很多殺毒軟件用這樣的方法檢查API是否被HOOK,或其他病毒木馬在你之後又改了前5個字節,這樣就會互相覆蓋,最後一個HOOK API的操作纔是有效的。掛鉤的方法很多,這裏只是最基礎的一種。希望大家多多補充。多多指正。
瞭解好以下的問題能夠更好的提升自己的HOOK水平:
1.CPU指令長度問題,在32位系統裏,一條JMP/CALL指令的長度是5個字節,因此你只有替換API裏超過5個字節長度的機器碼(或者替換幾條指令長度加起來是5字節的指令),否則會影響被更改的小於5個字節的機器碼後面的數條指令,甚至程序流程會被打亂,產生不可預料的後果;
2.參數問題,爲了訪問原API的參數,你要通過EBP或ESP來引用參數,因此你要非常清楚你的HOOK代碼裏此時的EBP/ESP的值是多少;
3.時機的問題,有些HOOK必須在API的開頭,有些必須在API的尾部,比如HOOK CreateFilaA(),如果你在API尾部HOOK API,那麼此時你就不能寫文件,甚至不能訪問文件;HOOK RECV(),如果你在API頭HOOK,此時還沒有收到數據,你就去查看RECV()的接收緩衝區,裏面當然沒有你想要的數據,必須等RECV()正常執行後,在RECV()的尾部HOOK,此時去查看RECV()的緩衝區,裏面纔有想要的數據;
4.上下文的問題,有些HOOK代碼不能執行某些操作,否則會破壞原API的上下文,原API就失效了;
5.同步問題,在HOOK代碼裏儘量不使用全局變量,而使用局部變量,這樣也是模塊化程序的需要;
6.最後要注意的是,被替換的CPU指令的原有功能一定要在HOOK代碼的某個地方模擬實現。
若有疏漏之處,歡迎各位大俠指正!
2014.10.27 6:30 pm