菜鳥學習HOOK

前言
自己從高中那時對黑客比較感興趣,那時候去過很多論壇學所謂的“黑客技術”。同時也經常在一些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 


發佈了31 篇原創文章 · 獲贊 2 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章