久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

分享

代碼注入的三種方法

 黃島主. 2011-06-16

 


 

目錄

  本文將討論如何把代碼注入不同的進程地址空間,然后在該進程的上下文中執(zhí)行注入的代碼,。 我們在網(wǎng)上可以查到一些窗口/密碼偵測的應(yīng)用例子,,網(wǎng)上的這些程序大多都依賴 Windows 鉤子技術(shù)來實現(xiàn)。本文將討論除了使用 Windows 鉤子技術(shù)以外的其它技術(shù)來實現(xiàn)這個功能,。如圖一所示:

圖一 WinSpy 密碼偵測程序

為了找到解決問題的方法,。首先讓我們簡單回顧一下問題背景。
  要“讀取”某個控件的內(nèi)容——無論這個控件是否屬于當(dāng)前的應(yīng)用程序——通常都是發(fā)送 WM_GETTEXT 消息來實現(xiàn),。這個技術(shù)也同樣應(yīng)用到編輯控件,,但是如果該編輯控件屬于另外一個進程并設(shè)置了 ES_PASSWORD 式樣,那么上面講的方法就行不通了,。用 WM_GETTEXT 來獲取控件的內(nèi)容只適用于進程“擁有”密碼控件的情況,。所以我們的問題變成了如何在另外一個進程的地址空間執(zhí)行:

::SendMessage( hPwdEdit, WM_GETTEXT, nMaxChars, psBuffer );

通常有三種可能性來解決這個問題。

  1. 將你的代碼放入某個 DLL,,然后通過 Windows 鉤子映射該DLL到遠(yuǎn)程進程,;
  2. 將你的代碼放入某個 DLL,然后通過 CreateRemoteThread 和 LoadLibrary 技術(shù)映射該DLL到遠(yuǎn)程進程,;
  3. 如果不寫單獨的 DLL,,可以直接將你的代碼拷貝到遠(yuǎn)程進程——通過 WriteProcessMemory——并用 CreateRemoteThread 啟動它的執(zhí)行。本文將在第三部分詳細(xì)描述該技術(shù)實現(xiàn)細(xì)節(jié),;

第一部分: Windows 鉤子

范例程序——參見HookSpy 和HookInjEx

  Windows 鉤子主要作用是監(jiān)控某些線程的消息流,。通常我們將鉤子分為本地鉤子和遠(yuǎn)程鉤子以及系統(tǒng)級鉤子,,本地鉤子一般監(jiān)控屬于本進程的線程的消息流,遠(yuǎn)程鉤子是線程專用的,,用于監(jiān)控屬于另外進程的線程消息流,。系統(tǒng)級鉤子監(jiān)控運行在當(dāng)前系統(tǒng)中的所有線程的消息流。
  如果鉤子作用的線程屬于另外的進程,,那么你的鉤子過程必須駐留在某個動態(tài)鏈接庫(DLL)中,。然后系統(tǒng)映射包含鉤子過程的DLL到鉤子作用的線程的地址空間。Windows將映射整個 DLL,,而不僅僅是鉤子過程,。這就是為什么 Windows 鉤子能被用于將代碼注入到別的進程地址空間的原因。
  本文我不打算涉及鉤子的具體細(xì)節(jié)(關(guān)于鉤子的細(xì)節(jié)請參見 MSDN 庫中的 SetWindowHookEx API),,但我在此要給出兩個很有用心得,,在相關(guān)文檔中你是找不到這些內(nèi)容的:

  1. 在成功調(diào)用 SetWindowsHookEx 后,系統(tǒng)自動映射 DLL 到鉤子作用的線程地址空間,,但不必立即發(fā)生映射,,因為 Windows 鉤子都是消息,DLL 在消息事件發(fā)生前并沒有產(chǎn)生實際的映射,。例如:
      如果你安裝一個鉤子監(jiān)控某些線程(WH_CALLWNDPROC)的非隊列消息,,在消息被實際發(fā)送到(某些窗口的)鉤子作用的線程之前,該DLL 是不會被映射到遠(yuǎn)程進程的,。換句話說,,如果 UnhookWindowsHookEx 在某個消息被發(fā)送到鉤子作用的線程之前被調(diào)用,DLL 根本不會被映射到遠(yuǎn)程進程(即使 SetWindowsHookEx 本身調(diào)用成功),。為了強制進行映射,,在調(diào)用 SetWindowsHookEx 之后馬上發(fā)送一個事件到相關(guān)的線程。
      在UnhookWindowsHookEx了之后,,對于沒有映射的DLL處理方法也一樣。只有在足夠的事件發(fā)生后,,DLL才會有真正的映射,。
  2. 當(dāng)你安裝鉤子后,它們可能影響整個系統(tǒng)得性能(尤其是系統(tǒng)級鉤子),,但是你可以很容易解決這個問題,,如果你使用線程專用鉤子的DLL映射機制,并不截獲消息,??紤]使用如下代碼:
    BOOL APIENTRY DllMain( HANDLE hModule,
        DWORD  ul_reason_for_call,
        LPVOID lpReserved )
        {
        if( ul_reason_for_call == DLL_PROCESS_ATTACH )
        {
        // Increase reference count via LoadLibrary
        char lib_name[MAX_PATH];
        ::GetModuleFileName( hModule, lib_name, MAX_PATH );
        ::LoadLibrary( lib_name );
        // Safely remove hook
        ::UnhookWindowsHookEx( g_hHook );
        }
        return TRUE;
        }			
      那么會發(fā)生什么呢?首先我們通過Windows 鉤子將DLL映射到遠(yuǎn)程進程,。然后,,在DLL被實際映射之后,,我們解開鉤子。通常當(dāng)?shù)谝粋€消息到達(dá)鉤子作用線程時,,DLL此時也不會被映射,。這里的處理技巧是調(diào)用LoadLibrary通過增加 DLLs的引用計數(shù)來防止映射不成功。
      現(xiàn)在剩下的問題是如何卸載DLL,,UnhookWindowsHookEx 是不會做這個事情的,,因為鉤子已經(jīng)不作用于線程了。你可以像下面這樣做:

    • 就在你想要解除DLL映射前,,安裝另一個鉤子,;
    • 發(fā)送一個“特殊”消息到遠(yuǎn)程線程;
    • 在鉤子過程中截獲這個消息,,響應(yīng)該消息時調(diào)用 FreeLibrary 和 UnhookWindowsHookEx,;

     

      目前只使用了鉤子來從處理遠(yuǎn)程進程中DLL的映射和解除映射。在此“作用于線程的”鉤子對性能沒有影響,。
    下面我們將討論另外一種方法,,這個方法與 LoadLibrary 技術(shù)的不同之處是DLL的映射機制不會干預(yù)目標(biāo)進程。相對LoadLibrary 技術(shù),,這部分描述的方法適用于 WinNT和Win9x,。
      但是,什么時候使用這個技巧呢,?答案是當(dāng)DLL必須在遠(yuǎn)程進程中駐留較長時間(即如果你子類化某個屬于另外一個進程的控件時)以及你想盡可能少的干涉目標(biāo)進程時,。我在 HookSpy 中沒有使用它,因為注入DLL 的時間并不長——注入時間只要足夠得到密碼即可,。我提供了另外一個例子程序——HookInjEx——來示范,。HookInjEx 將DLL映射到資源管理器“explorer.exe”,并從中/解除影射,,它子類化“開始”按鈕,,并交換鼠標(biāo)左右鍵單擊“開始”按鈕的功能。

HookSpy 和 HookInjEx 的源代碼都可以從本文的下載源代碼中獲得,。

第二部分:CreateRemoteThread 和 LoadLibrary 技術(shù)

范例程序——LibSpy

通常,,任何進程都可以通過 LoadLibrary API 動態(tài)加載DLL。但是,,如何強制一個外部進程調(diào)用這個函數(shù)呢,?答案是:CreateRemoteThread。
首先,,讓我們看一下 LoadLibrary 和FreeLibrary API 的聲明:

HINSTANCE LoadLibrary(
LPCTSTR lpLibFileName // 庫模塊文件名的地址
);
BOOL FreeLibrary(
HMODULE hLibModule // 要加載的庫模塊的句柄
);

現(xiàn)在將它們與傳遞到 CreateRemoteThread 的線程例程——ThreadProc 的聲明進行比較,。

DWORD WINAPI ThreadProc(
LPVOID lpParameter // 線程數(shù)據(jù)
);

你可以看到,所有函數(shù)都使用相同的調(diào)用規(guī)范并都接受 32位參數(shù),,返回值的大小都相同,。也就是說,,我們可以傳遞一個指針到LoadLibrary/FreeLibrary 作為到 CreateRemoteThread 的線程例程。但這里有兩個問題,,請看下面對CreateRemoteThread 的描述:

  1. CreateRemoteThread 的 lpStartAddress 參數(shù)必須表示遠(yuǎn)程進程中線程例程的開始地址,。
  2. 如果傳遞到 ThreadFunc 的參數(shù)lpParameter——被解釋為常規(guī)的 32位值(FreeLibrary將它解釋為一個 HMODULE),一切OK,。但是,,如果 lpParameter 被解釋為一個指針(LoadLibraryA將它解釋為一個串指針)。它必須指向遠(yuǎn)程進程的某些數(shù)據(jù),。

  第一個問題實際上是由它自己解決的,。LoadLibrary 和 FreeLibray 兩個函數(shù)都在 kernel32.dll 中。因為必須保證kernel32存在并且在每個“常規(guī)”進程中的加載地址要相同,,LoadLibrary/FreeLibray 的地址在每個進程中的地址要相同,,這就保證了有效的指針被傳遞到遠(yuǎn)程進程。
  第二個問題也很容易解決,。只要通過 WriteProcessMemory 將 DLL 模塊名(LoadLibrary需要的DLL模塊名)拷貝到遠(yuǎn)程進程即可,。

所以,為了使用CreateRemoteThread 和 LoadLibrary 技術(shù),,需要按照下列步驟來做:

  1. 獲取遠(yuǎn)程進程(OpenProcess)的 HANDLE,;
  2. 為遠(yuǎn)程進程中的 DLL名分配內(nèi)存(VirtualAllocEx);
  3. 將 DLL 名,,包含全路徑名,,寫入分配的內(nèi)存(WriteProcessMemory);
  4. 用 CreateRemoteThread 和 LoadLibrary. 將你的DLL映射到遠(yuǎn)程進程,;
  5. 等待直到線程終止(WaitForSingleObject),,也就是說直到 LoadLibrary 調(diào)用返回。另一種方法是,,一旦 DllMain(用DLL_PROCESS_ATTACH調(diào)用)返回,,線程就會終止;
  6. 獲取遠(yuǎn)程線程的退出代碼(GetExitCodeThread),。注意這是一個 LoadLibrary 返回的值,,因此是所映射 DLL 的基地址(HMODULE)。
    在第二步中釋放分配的地址(VirtualFreeEx),;
  7. 用 CreateRemoteThread 和 FreeLibrary從遠(yuǎn)程進程中卸載 DLL。傳遞在第六步獲取的 HMODULE 句柄到 FreeLibrary(通過 CreateRemoteThread 的lpParameter參數(shù)),;
  8. 注意:如果你注入的 DLL 產(chǎn)生任何新的線程,,一定要在卸載DLL 之前將它們都終止掉;
  9. 等待直到線程終止(WaitForSingleObject),;

  此外,,處理完成后不要忘了關(guān)閉所有句柄,,包括在第四步和第八步創(chuàng)建的兩個線程以及在第一步獲取的遠(yuǎn)程線程句柄。現(xiàn)在讓我們看一下 LibSpy 的部分代碼,,為了簡單起見,,上述步驟的實現(xiàn)細(xì)節(jié)中的錯誤處理以及 UNICODE 支持部分被略掉。

HANDLE hThread;
char    szLibPath[_MAX_PATH];  // “LibSpy.dll”模塊的名稱 (包括全路徑);
void*   pLibRemote;   // 遠(yuǎn)程進程中的地址,,szLibPath 將被拷貝到此處;
DWORD   hLibModule;   // 要加載的模塊的基地址(HMODULE)
HMODULE hKernel32 = ::GetModuleHandle("Kernel32");
// 初始化szLibPath
//...
// 1. 在遠(yuǎn)程進程中為szLibPath 分配內(nèi)存
// 2. 將szLibPath 寫入分配的內(nèi)存
pLibRemote = ::VirtualAllocEx( hProcess, NULL, sizeof(szLibPath),
MEM_COMMIT, PAGE_READWRITE );
::WriteProcessMemory( hProcess, pLibRemote, (void*)szLibPath,
sizeof(szLibPath), NULL );
// 將"LibSpy.dll" 加載到遠(yuǎn)程進程(使用CreateRemoteThread 和 LoadLibrary)
hThread = ::CreateRemoteThread( hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE) ::GetProcAddress( hKernel32,
"LoadLibraryA" ),
pLibRemote, 0, NULL );
::WaitForSingleObject( hThread, INFINITE );
// 獲取所加載的模塊的句柄
::GetExitCodeThread( hThread, &hLibModule );
// 清除
::CloseHandle( hThread );
::VirtualFreeEx( hProcess, pLibRemote, sizeof(szLibPath), MEM_RELEASE );		
  假設(shè)我們實際想要注入的代碼——SendMessage ——被放在DllMain (DLL_PROCESS_ATTACH)中,,現(xiàn)在它已經(jīng)被執(zhí)行。那么現(xiàn)在應(yīng)該從目標(biāo)進程中將DLL 卸載:
// 從目標(biāo)進程中卸載"LibSpy.dll"  (使用 CreateRemoteThread 和 FreeLibrary)
hThread = ::CreateRemoteThread( hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE) ::GetProcAddress( hKernel32,
"FreeLibrary" ),
(void*)hLibModule, 0, NULL );
::WaitForSingleObject( hThread, INFINITE );
// 清除
::CloseHandle( hThread );		
進程間通信

  到目前為止,,我們只討論了關(guān)于如何將DLL 注入到遠(yuǎn)程進程的內(nèi)容,,但是,在大多數(shù)情況下,,注入的 DLL 都需要與原應(yīng)用程序進行某種方式的通信(回想一下,,我們的DLL是被映射到某個遠(yuǎn)程進程的地址空間里了,不是在本地應(yīng)用程序的地址空間中),。比如秘密偵測程序,,DLL必須要知道實際包含密碼的控件句柄,顯然,,編譯時無法將這個值進行硬編碼,。同樣,一旦DLL獲得了秘密,,它必須將它發(fā)送回原應(yīng)用程序,,以便能正確顯示出來。
  幸運的是,,有許多方法處理這個問題,,文件映射,WM_COPYDATA,,剪貼板以及很簡單的 #pragma data_seg 共享數(shù)據(jù)段等,,本文我不打算使用這些技術(shù),因為MSDN(“進程間通信”部分)以及其它渠道可以找到很多文檔參考,。不過我在 LibSpy例子中還是使用了 #pragma data_seg,。細(xì)節(jié)請參考 LibSpy 源代碼。

第三部分:CreateRemoteThread 和 WriteProcessMemory 技術(shù)

范例程序——WinSpy

  另外一個將代碼拷貝到另一個進程地址空間并在該進程上下文中執(zhí)行的方法是使用遠(yuǎn)程線程和 WriteProcessMemory API,。這種方法不用編寫單獨的DLL,,而是用 WriteProcessMemory 直接將代碼拷貝到遠(yuǎn)程進程——然后用 CreateRemoteThread 啟動它執(zhí)行。先來看看 CreateRemoteThread 的聲明:

HANDLE CreateRemoteThread(
HANDLE hProcess,        // 傳入創(chuàng)建新線程的進程句柄
LPSECURITY_ATTRIBUTES lpThreadAttributes,  // 安全屬性指針
DWORD dwStackSize,      // 字節(jié)為單位的初始線程堆棧
LPTHREAD_START_ROUTINE lpStartAddress,     // 指向線程函數(shù)的指針
LPVOID lpParameter,     // 新線程使用的參數(shù)
DWORD dwCreationFlags,  // 創(chuàng)建標(biāo)志
LPDWORD lpThreadId      // 指向返回的線程ID
);				
如果你比較它與 CreateThread(MSDN)的聲明,,你會注意到如下的差別:
  • 在 CreateRemoteThread中,,hProcess是額外的一個參數(shù),一個進程句柄,新線程就是在這個進程中創(chuàng)建的,;
  • 在 CreateRemoteThread中,,lpStartAddress 表示的是在遠(yuǎn)程進程地址空間中的線程起始地址。線程函數(shù)必須要存在于遠(yuǎn)程進程中,,所以我們不能簡單地傳遞一個指針到本地的 ThreadFunc,。必須得先拷貝代碼到遠(yuǎn)程進程;
  • 同樣,,lpParameter 指向的數(shù)據(jù)也必須要存在于遠(yuǎn)程進程,,所以也得將它拷貝到那。

綜上所述,,我們得按照如下的步驟來做:

  1. 獲取一個遠(yuǎn)程進程的HANDLE (OpenProces) ,;
  2. 在遠(yuǎn)程進程地址空間中為注入的數(shù)據(jù)分配內(nèi)存(VirtualAllocEx);
  3. 將初始的 INDATA 數(shù)據(jù)結(jié)構(gòu)的一個拷貝寫入分配的內(nèi)存中(WriteProcessMemory),;
  4. 在遠(yuǎn)程進程地址空間中為注入的代碼分配內(nèi)存,;
  5. 將 ThreadFunc 的一個拷貝寫入分配的內(nèi)存;
  6. 用 CreateRemoteThread啟動遠(yuǎn)程的 ThreadFunc 拷貝,;
  7. 等待遠(yuǎn)程線程終止(WaitForSingleObject),;
  8. 獲取遠(yuǎn)程來自遠(yuǎn)程進程的結(jié)果(ReadProcessMemory 或 GetExitCodeThread);
  9. 釋放在第二步和第四步中分配的內(nèi)存(VirtualFreeEx),;
  10. 關(guān)閉在第六步和第一步獲取的句柄(CloseHandle),;

ThreadFunc 必須要遵循的原則:

  1. 除了kernel32.dll 和user32.dll 中的函數(shù)之外,ThreadFunc 不要調(diào)用任何其它函數(shù),,只有 kernel32.dll 和user32.dll被保證在本地和目標(biāo)進程中的加載地址相同(注意,,user32.dll并不是被映射到每個 Win32 的進程)。如果你需要來自其它庫中的函數(shù),,將LoadLibrary 和 GetProcAddress 的地址傳給注入的代碼,,然后放手讓它自己去做。如果映射到目標(biāo)進程中的DLL有沖突,,你也可以用 GetModuleHandle 來代替 LoadLibrary,。
      同樣,如果你想在 ThreadFunc 中調(diào)用自己的子例程,,要單獨把每個例程的代碼拷貝到遠(yuǎn)程進程并用 INJDATA為 ThreadFunc 提供代碼的地址,。
  2. 不要使用靜態(tài)字符串,而要用 INJDATA 來傳遞所有字符串,。之所以要這樣,,是因為編譯器將靜態(tài)字符串放在可執(zhí)行程序的“數(shù)據(jù)段”中,可是引用(指針)是保留在代碼中的,。那么,,遠(yuǎn)程進程中ThreadFunc 的拷貝指向的內(nèi)容在遠(yuǎn)程進程的地址空間中是不存在的,。
  3. 去掉 /GZ 編譯器開關(guān),它在調(diào)試版本中是默認(rèn)設(shè)置的,。
  4. 將 ThreadFunc 和 AfterThreadFunc 聲明為靜態(tài)類型,或者不啟用增量鏈接,。
  5. ThreadFunc 中的局部變量一定不能超過一頁(也就是 4KB),。
    注意在調(diào)試版本中4KB的空間有大約10個字節(jié)是用于內(nèi)部變量的。
  6. 如果你有一個開關(guān)語句塊大于3個case 語句,,將它們像下面這樣拆分開:
    switch( expression ) {
        case constant1: statement1; goto END;
        case constant2: statement2; goto END;
        case constant3: statement2; goto END;
        }
        switch( expression ) {
        case constant4: statement4; goto END;
        case constant5: statement5; goto END;
        case constant6: statement6; goto END;
        }
        END:				
    或者將它們修改成一個 if-else if 結(jié)構(gòu)語句(參見附錄E),。

  如果你沒有按照這些規(guī)則來做,目標(biāo)進程很可能會崩潰,。所以務(wù)必牢記,。在目標(biāo)進程中不要假設(shè)任何事情都會像在本地進程中那樣 (參見附錄F)。

GetWindowTextRemote(A/W)

要想從“遠(yuǎn)程”編輯框獲得密碼,,你需要做的就是將所有功能都封裝在GetWindowTextRemot(A/W):中,。

int GetWindowTextRemoteA( HANDLE hProcess, HWND hWnd, LPSTR lpString );
int GetWindowTextRemoteW( HANDLE hProcess, HWND hWnd, LPWSTR lpString );
參數(shù)說明:
hProcess:編輯框控件所屬的進程句柄;
hWnd:包含密碼的編輯框控件句柄,;
lpString:接收文本的緩沖指針,;
返回值:返回值是拷貝的字符數(shù);

  下面讓我們看看它的部分代碼——尤其是注入數(shù)據(jù)的代碼——以便明白 GetWindowTextRemote 的工作原理,。此處為簡單起見,,略掉了 UNICODE 支持部分。

INJDATA
typedef LRESULT (WINAPI *SENDMESSAGE)(HWND,UINT,WPARAM,LPARAM);
typedef struct {
HWND hwnd; // 編輯框句柄
SENDMESSAGE fnSendMessage; // 指向user32.dll 中 SendMessageA 的指針
char psText[128]; // 接收密碼的緩沖
} INJDATA;

  INJDATA 是一個被注入到遠(yuǎn)程進程的數(shù)據(jù)結(jié)構(gòu),。但在注入之前,,結(jié)構(gòu)中指向 SendMessageA 的指針是在本地應(yīng)用程序中初始化的。因為對于每個使用user32.dll的進程來說,,user32.dll總是被映射到相同的地址,,因此,SendMessageA 的地址也肯定是相同的,。這就保證了被傳遞到遠(yuǎn)程進程的是一個有效的指針,。

ThreadFunc函數(shù)

static DWORD WINAPI ThreadFunc (INJDATA *pData)
{
pData->fnSendMessage( pData->hwnd, WM_GETTEXT, // Get password
sizeof(pData->psText),
(LPARAM)pData->psText );
return 0;
}
// 該函數(shù)在ThreadFunc之后標(biāo)記內(nèi)存地址
// int cbCodeSize = (PBYTE) AfterThreadFunc - (PBYTE) ThreadFunc.
static void AfterThreadFunc (void)
{
}

ThradFunc 是被遠(yuǎn)程線程執(zhí)行的代碼。

  • 注釋:注意AfterThreadFunc 是如何計算 ThreadFunc 大小的,。通常這樣做并不是一個好辦法,,因為鏈接器可以隨意更改函數(shù)的順序(也就是說ThreadFunc可能被放在 AfterThreadFunc之后)。這一點你可以在小項目中很好地保證函數(shù)的順序是預(yù)先設(shè)想好的,,比如 WinSpy 程序,。在必要的情況下,你還可以使用 /ORDER 鏈接器選項來解決函數(shù)鏈接順序問題,?;蛘哂梅磪R編確定 ThreadFunc 函數(shù)的大小,。
如何使用該技術(shù)子類化遠(yuǎn)程控件

范例程序——InjectEx

下面我們將討論一些更復(fù)雜的內(nèi)容,如何子類化屬于另一個進程的控件,。

首先,,你得拷貝兩個函數(shù)到遠(yuǎn)程進程來完成此任務(wù)

  1. ThreadFunc實際上是通過 SetWindowLong子類化遠(yuǎn)程進程中的控件;
  2. NewProc是子類化控件的新窗口過程,;

  這里主要的問題是如何將數(shù)據(jù)傳到遠(yuǎn)程窗口過程 NewProc,,因為 NewProc 是一個回調(diào)函數(shù),它必須遵循特定的規(guī)范和原則,,我們不能簡單地在參數(shù)中傳遞 INJDATA指針,。幸運的是我找到了有兩個方法來解決這個問題,只不過要借助匯編語言,,所以不要忽略了匯編,,關(guān)鍵時候它是很有用的!

方法一:

如下圖所示:

  在遠(yuǎn)程進程中,,INJDATA 被放在NewProc 之前,,這樣 NewProc 在編譯時便知道 INJDATA 在遠(yuǎn)程進程地址空間中的內(nèi)存位置。更確切地說,,它知道相對于其自身位置的 INJDATA 的地址,,我們需要所有這些信息。下面是 NewProc 的代碼:

static LRESULT CALLBACK NewProc(
HWND hwnd,       // 窗口句柄
UINT uMsg,       // 消息標(biāo)示符
WPARAM wParam,   // 第一個消息參數(shù)
LPARAM lParam )  // 第二個消息參數(shù)
{
INJDATA* pData = (INJDATA*) NewProc;  // pData 指向 NewProc
pData--;              // 現(xiàn)在pData 指向INJDATA;
// 回想一下INJDATA 被置于遠(yuǎn)程進程NewProc之前;
//-----------------------------
// 此處是子類化代碼
// ........
//-----------------------------
// 調(diào)用原窗口過程;
// fnOldProc (由SetWindowLong 返回) 被(遠(yuǎn)程)ThreadFunc初始化
// 并被保存在(遠(yuǎn)程)INJDATA;中
return pData->fnCallWindowProc( pData->fnOldProc,
hwnd,uMsg,wParam,lParam );
}
但這里還有一個問題,,見第一行代碼:
INJDATA* pData = (INJDATA*) NewProc;

  這種方式 pData得到的是硬編碼值(在我們的進程中是原 NewProc 的內(nèi)存地址),。這不是我們十分想要的。在遠(yuǎn)程進程中,,NewProc “當(dāng)前”拷貝的內(nèi)存地址與它被移到的實際位置是無關(guān)的,,換句話說,我們會需要某種類型的“this 指針”,。
雖然用 C/C++ 無法解決這個問題,,但借助內(nèi)聯(lián)匯編可以解決,下面是對 NewProc的修改:

static LRESULT CALLBACK NewProc(
HWND hwnd,       // 窗口句柄
UINT uMsg,       // 消息標(biāo)示符
WPARAM wParam,   // 第一個消息參數(shù)
LPARAM lParam )  // 第二個消息參數(shù)
{
// 計算INJDATA 結(jié)構(gòu)的位置
// 在遠(yuǎn)程進程中記住這個INJDATA
// 被放在NewProc之前
INJDATA* pData;
_asm {
call    dummy
dummy:
pop     ecx         // <- ECX 包含當(dāng)前的EIP
sub     ecx, 9      // <- ECX 包含NewProc的地址
mov     pData, ecx
}
pData--;
//-----------------------------
// 此處是子類化代碼
// ........
//-----------------------------
// 調(diào)用原來的窗口過程
return pData->fnCallWindowProc( pData->fnOldProc,
hwnd,uMsg,wParam,lParam );
}
  那么,,接下來該怎么辦呢,?事實上,每個進程都有一個特殊的寄存器,,它指向下一條要執(zhí)行的指令的內(nèi)存位置,。即所謂的指令指針,在32位 Intel 和 AMD 處理器上被表示為 EIP,。因為 EIP是一個專用寄存器,,你無法象操作一般常規(guī)存儲器(如:EAX,EBX等)那樣通過編程存取它,。也就是說沒有操作代碼來尋址 EIP,,以便直接讀取或修改其內(nèi)容,。但是,EIP 仍然還是可以通過間接方法修改的(并且隨時可以修改),,通過JMP,,CALL和RET這些指令實現(xiàn)。下面我們就通過例子來解釋通過 CALL/RET 子例程調(diào)用機制在32位 Intel 和 AMD 處理器上是如何工作的,。
  當(dāng)你調(diào)用(通過 CALL)某個子例程時,,子例程的地址被加載到 EIP,但即便是在 EIP杯修改之前,,其舊的那個值被自動PUSH到堆棧(被用于后面作為指令指針返回)。在子例程執(zhí)行完時,,RET 指令自動將堆棧頂POP到 EIP,。
  現(xiàn)在你知道了如何通過 CALL 和 RET 實現(xiàn) EIP 的修改,但如何獲取其當(dāng)前的值呢,?下面就來解決這個問題,,前面講過,CALL PUSH EIP 到堆棧,,所以,,為了獲取其當(dāng)前值,調(diào)用“啞函數(shù)”,,然后再POP堆棧頂,。讓我們用編譯后的 NewProc 來解釋這個竅門。
Address   OpCode/Params   Decoded instruction
--------------------------------------------------
:00401000  55              push ebp            ; entry point of
; NewProc
:00401001  8BEC            mov ebp, esp
:00401003  51              push ecx
:00401004  E800000000      call 00401009       ; *a*    call dummy
:00401009  59              pop ecx             ; *b*
:0040100A  83E909          sub ecx, 00000009   ; *c*
:0040100D  894DFC          mov [ebp-04], ecx   ; mov pData, ECX
:00401010  8B45FC          mov eax, [ebp-04]
:00401013  83E814          sub eax, 00000014   ; pData--;
.....
.....
:0040102D  8BE5            mov esp, ebp
:0040102F  5D              pop ebp
:00401030  C21000          ret 0010
  • 啞函數(shù)調(diào)用,;就是JUMP到下一個指令并PUSH EIP到堆棧,;
  • 然后將堆棧頂POP到 ECX,ECX再保存EIP,;這也是 POP EIP指令的真正地址,;
  • 注意 NewProc 的入口點和 “POP ECX”之間的“距離”是9 個字節(jié);因此為了計算 NewProc的地址,,要從 ECX 減9,。

  這樣一來,不管 NewProc 被移到什么地方,,它總能計算出其自己的地址,。但是,NewProc 的入口點和 “POP ECX”之間的距離可能會隨著你對編譯/鏈接選項的改變而變化,,由此造成 RELEASE和DEBUG版本之間也會有差別,。但關(guān)鍵是你仍然確切地知道編譯時的值。

  1. 首先,,編譯函數(shù)
  2. 用反匯編確定正確的距離
  3. 最后,,用正確的距離值重新編譯

此即為 InjecEx 中使用的解決方案,,類似于 HookInjEx,交換鼠標(biāo)點擊“開始”左右鍵時的功能,。

方法二:

對于我們的問題,,在遠(yuǎn)程進程地址空間中將 INJDATA 放在 NewProc 前面不是唯一的解決辦法??聪旅?NewProc的變異版本:

static LRESULT CALLBACK NewProc(
HWND hwnd,      // 窗口句柄
UINT uMsg,      // 消息標(biāo)示符
WPARAM wParam,  // 第一個消息參數(shù)
LPARAM lParam ) // 第二個消息參數(shù)
{
INJDATA* pData = 0xA0B0C0D0;    // 虛構(gòu)值
//-----------------------------
// 子類化代碼
// ........
//-----------------------------
// 調(diào)用原來的窗口過程
return pData->fnCallWindowProc( pData->fnOldProc,
hwnd,uMsg,wParam,lParam );
}
  此處 0xA0B0C0D0 只是遠(yuǎn)程進程地址空間中真實(絕對)INJDATA地址的占位符,。前面講過,你無法在編譯時知道該地址,。但你可以在調(diào)用 VirtualAllocEx (為INJDATA)之后得到 INJDATA 在遠(yuǎn)程進程中的位置,。編譯我們的 NewProc 后,可以得到如下結(jié)果:
 Address   OpCode/Params     Decoded instruction
--------------------------------------------------
:00401000  55                push ebp
:00401001  8BEC              mov ebp, esp
:00401003  C745FCD0C0B0A0    mov [ebp-04], A0B0C0D0
:0040100A  ...
....
:0040102D  8BE5              mov esp, ebp
:0040102F  5D                pop ebp
:00401030  C21000            ret 0010
因此,,其編譯的代碼(十六進制)將是:
558BECC745FCD0C0B0A0......8BE55DC21000.
現(xiàn)在你可以象下面這樣繼續(xù):
  1. 將INJDATA,,ThreadFunc和NewProc 拷貝到目標(biāo)進程;
  2. 修改 NewProc 的代碼,,以便 pData 中保存的是 INJDATA 的真實地址,。
    例如,假設(shè) INJDATA 的地址(VirtualAllocEx返回的值)在目標(biāo)進程中是 0x008a0000,。然后象下面這樣修改NewProc的代碼:
    	558BECC745FCD0C0B0A0......8BE55DC21000 <- 原來的NewProc (注1)
        558BECC745FC00008A00......8BE55DC21000 <- 修改后的NewProc,,使用的是INJDATA的實際地址。
    也就是說,,你用真正的 INJDATA(注2) 地址替代了虛擬值 A0B0C0D0(注2),。
  3. 開始執(zhí)行遠(yuǎn)程的 ThreadFunc,它負(fù)責(zé)子類化遠(yuǎn)程進程中的控件,。
  • 注1,、有人可能會問,為什么地址 A0B0C0D0 和 008a0000 在編譯時順序是相反的,。因為 Intel 和 AMD 處理器使用 little-endian 符號來表示(多字節(jié))數(shù)據(jù),。換句話說,某個數(shù)字的低位字節(jié)被存儲在內(nèi)存的最小地址處,,而高位字節(jié)被存儲在最高位地址,。
    假設(shè)“UNIX”這個詞存儲用4個字節(jié),在 big-endian 系統(tǒng)中,,它被存為“UNIX”,,在 little-endian 系統(tǒng)中,它將被存為“XINU”,。
  • 注2,、某些破解(很糟)以類似的方式修改可執(zhí)行代碼,但是一旦加載到內(nèi)存,,一個程序是無法修改自己的代碼的(代碼駐留在可執(zhí)行程序的“.text” 區(qū)域,,這個區(qū)域是寫保護的),。但仍可以修改遠(yuǎn)程的 NewProc,因為它是先前以 PAGE_EXECUTE_READWRITE 許可方式被拷貝到某個內(nèi)存塊中的,。

何時使用 CreateRemoteThread 和 WriteProcessMemory 技術(shù)

  與其它方法比較,,使用 CreateRemoteThread 和 WriteProcessMemory 技術(shù)進行代碼注入更靈活,這種方法不需要額外的 dll,,不幸的是,,該方法更復(fù)雜并且風(fēng)險更大,只要ThreadFunc出現(xiàn)哪怕一丁點錯誤,,很容易就讓(并且最大可能地會)使遠(yuǎn)程進程崩潰(參見附錄 F),,因為調(diào)試遠(yuǎn)程 ThreadFunc 將是一個可怕的夢魘,只有在注入的指令數(shù)很少時,,你才應(yīng)該考慮使用這種技術(shù)進行注入,,對于大塊的代碼注入,最好用 I.和II 部分討論的方法,。

WinSpy 以及 InjectEx 請從這里下載源代碼

結(jié)束語

 

到目前為止,,有幾個問題是我們未提及的,,現(xiàn)總結(jié)如下:

解決方案 OS 進程
I、Hooks Win9x 和 WinNT 僅僅與 USER32.DLL (注3)鏈接的進程
II,、CreateRemoteThread & LoadLibrary 僅 WinNT(注4) 所有進程(注5), 包括系統(tǒng)服務(wù)(注6)
III,、CreateRemoteThread & WriteProcessMemory
 
僅 WinNT 所有進程, 包括系統(tǒng)服務(wù)
  • 注3:顯然,你無法hook一個沒有消息隊列的線程,,此外,,SetWindowsHookEx不能與系統(tǒng)服務(wù)一起工作,即使它們與 USER32.DLL 進行鏈接,;
  • 注4:Win9x 中沒有 CreateRemoteThread,,也沒有 VirtualAllocEx (實際上,在Win9x 中可以仿真,,但不是本文討論的問題了),;
  • 注5:所有進程 = 所有 Win32 進程 + csrss.exe
    本地應(yīng)用 (smss.exe, os2ss.exe, autochk.exe 等)不使用 Win32 API,所以也不會與 kernel32.dll 鏈接,。唯一一個例外是 csrss.exe,,Win32 子系統(tǒng)本身,它是本地應(yīng)用程序,,但其某些庫(~winsrv.dll)需要 Win32 DLLs,,包括 kernel32.dll;
  • 注6:如果你想要將代碼注入到系統(tǒng)服務(wù)中(lsass.exe, services.exe, winlogon.exe 等)或csrss.exe,,在打開遠(yuǎn)程句柄(OpenProcess)之前,,將你的進程優(yōu)先級置為 “SeDebugPrivilege”(AdjustTokenPrivileges),。

  最后,有幾件事情一定要了然于心:你的注入代碼很容易摧毀目標(biāo)進程,,尤其是注入代碼本身出錯的時候,,所以要記住:權(quán)力帶來責(zé)任,!
  因為本文中的許多例子是關(guān)于密碼的,,你也許還讀過 Zhefu Zhang 寫的另外一篇文章“Super Password Spy++” ,在該文中,,他解釋了如何獲取IE 密碼框中的內(nèi)容,,此外,他還示范了如何保護你的密碼控件免受類似的攻擊,。

附錄A

為什么 kernel32.dll 和user32.dll 總是被映射到相同的地址,。

  我的假定:因為Microsoft 的程序員認(rèn)為這樣做有助于速度優(yōu)化,為什么呢,?我的解釋是——通常一個可執(zhí)行程序是由幾個部分組成,,其中包括“.reloc” 。當(dāng)鏈接器創(chuàng)建 EXE 或者 DLL文件時,,它對文件被映射到哪個內(nèi)存地址做了一個假設(shè),。這就是所謂的首選加載/基地址。在映像文件中所有絕對地址都是基于鏈接器首選的加載地址,,如果由于某種原因,,映像文件沒有被加載到該地址,那么這時“.reloc”就起作用了,,它包含映像文件中的所有地址的清單,,這個清單中的地址反映了鏈接器首選加載地址和實際加載地址的差別(無論如何,,要注意編譯器產(chǎn)生的大多數(shù)指令使用某種相對地址尋址,,因此,,并沒有你想象的那么多地址可供重新分配),另一方面,,如果加載器能夠按照鏈接器首選地址加載映像文件,,那么“.reloc”就被完全忽略掉了。
  但kernel32.dll 和user32.dll 及其加載地址為何要以這種方式加載呢,?因為每一個 Win32 程序都需要kernel32.dll,,并且大多數(shù)Win32 程序也需要 user32.dll,那么總是將它們(kernel32.dll 和user32.dll)映射到首選地址可以改進所有可執(zhí)行程序的加載時間,。這樣一來,,加載器絕不能修改kernel32.dll and user32.dll.中的任何(絕對)地址。我們用下面的例子來說明:
  將某個應(yīng)用程序 App.exe 的映像基地址設(shè)置成 KERNEL32的地址(/base:"0x77e80000")或 USER32的首選基地址(/base:"0x77e10000"),如果 App.exe 不是從 USER32 導(dǎo)入方式來使用 USER32,,而是通過LoadLibrary 加載,,那么編譯并運行App.exe 后,會報出錯誤信息("Illegal System DLL Relocation"——非法系統(tǒng)DLL地址重分配),,App.exe 加載失敗,。
為什么會這樣呢?當(dāng)創(chuàng)建進程時,,Win 2000,、Win XP 和Win 2003系統(tǒng)的加載器要檢查 kernel32.dll 和user32.dll 是否被映射到首選基地址(實際上,它們的名字都被硬編碼進了加載器),,如果沒有被加載到首選基地址,,將發(fā)出錯誤。在 WinNT4中,,也會檢查ole32.dll,,在WinNT 3.51 和較低版本的Windows中,由于不會做這樣的檢查,,所以kernel32.dll 和user32.dll可以被加載任何地方,。只有ntdll.dll總是被加載到其基地址,加載器不進行檢查,,一旦ntdll.dll沒有在其基地址,,進程就無法創(chuàng)建。

總之,,對于 WinNT 4 和較高的版本中

  • 一定要被加載到基地址的DLLs 有:kernel32.dll,、user32.dll 和ntdll.dll,;
  • 每個Win32 程序都要使用的 DLLs+ csrss.exe:kernel32.dll 和ntdll.dll,;
  • 每個進程都要使用的DLL只有一個,即使是本地應(yīng)用:ntdll.dll,;

附錄B

/GZ 編譯器開關(guān)

  在生成 Debug 版本時,,/GZ 編譯器特性是默認(rèn)打開的。你可以用它來捕獲某些錯誤(具體細(xì)節(jié)請參考相關(guān)文檔),。但對我們的可執(zhí)行程序意味著什么呢,?
  當(dāng)打開 /GZ 開關(guān),編譯器會添加一些額外的代碼到可執(zhí)行程序中每個函數(shù)所在的地方,,包括一個函數(shù)調(diào)用(被加到每個函數(shù)的最后)——檢查已經(jīng)被我們的函數(shù)修改的 ESP堆棧指針,。什么!難道有一個函數(shù)調(diào)用被添加到 ThreadFunc 嗎,?那將導(dǎo)致災(zāi)難,。ThreadFunc 的遠(yuǎn)程拷貝將調(diào)用一個在遠(yuǎn)程進程中不存在的函數(shù)(至少是在相同的地址空間中不存在)

附錄C

 

靜態(tài)函數(shù)和增量鏈接

  增量鏈接主要作用是在生成應(yīng)用程序時縮短鏈接時間。常規(guī)鏈接和增量鏈接的可執(zhí)行程序之間的差別是——增量鏈接時,,每個函數(shù)調(diào)用經(jīng)由一個額外的JMP指令,,該指令由鏈接器發(fā)出(該規(guī)則的一個例外是函數(shù)聲明為靜態(tài)),。這些 JMP 指令允許鏈接器在內(nèi)存中移動函數(shù),這種移動無需修改引用函數(shù)的 CALL指令,。但這些JMP指令也確實導(dǎo)致了一些問題:如 ThreadFunc 和 AfterThreadFunc 將指向JMP指令而不是實際的代碼,。所以當(dāng)計算ThreadFunc 的大小時:

const int cbCodeSize = ((LPBYTE) AfterThreadFunc - (LPBYTE) ThreadFunc)
  你實際上計算的是指向 ThreadFunc 的JMPs 和AfterThreadFunc之間的“距離” (通常它們會緊挨著,不用考慮距離問題)?,F(xiàn)在假設(shè) ThreadFunc 的地址位于004014C0 而伴隨的 JMP指令位于 00401020,。
:00401020   jmp  004014C0
...
:004014C0   push EBP          ; ThreadFunc 的實際地址
:004014C1   mov  EBP, ESP
...
那么
WriteProcessMemory( .., &ThreadFunc, cbCodeSize, ..);
  將拷貝“JMP 004014C0”指令(以及隨后cbCodeSize范圍內(nèi)的所有指令)到遠(yuǎn)程進程——不是實際的 ThreadFunc。遠(yuǎn)程進程要執(zhí)行的第一件事情將是“JMP 004014C0” ,。它將會在其最后幾條指令當(dāng)中——遠(yuǎn)程進程和所有進程均如此,。但 JMP指令的這個“規(guī)則”也有例外。如果某個函數(shù)被聲明為靜態(tài)的,,它將會被直接調(diào)用,,即使增量鏈接也是如此。這就是為什么規(guī)則#4要將 ThreadFunc 和 AfterThreadFunc 聲明為靜態(tài)或禁用增量鏈接的緣故,。(有關(guān)增量鏈接的其它信息參見 Matt Pietrek的文章“Remove Fatty Deposits from Your Applications Using Our 32-bit Liposuction Tools” )

附錄D

為什么 ThreadFunc的局部變量只有 4k,?

  局部變量總是存儲在堆棧中,如果某個函數(shù)有256個字節(jié)的局部變量,,當(dāng)進入該函數(shù)時,,堆棧指針就減少256個字節(jié)(更精確地說,在函數(shù)開始處),。例如,,下面這個函數(shù):

void Dummy(void) {
BYTE var[256];
var[0] = 0;
var[1] = 1;
var[255] = 255;
}
編譯后的匯編如下:
:00401000   push ebp
:00401001   mov  ebp, esp
:00401003   sub  esp, 00000100           ; change ESP as storage for
; local variables is needed
:00401006   mov  byte ptr [esp], 00      ; var[0] = 0;
:0040100A   mov  byte ptr [esp+01], 01   ; var[1] = 1;
:0040100F   mov  byte ptr [esp+FF], FF   ; var[255] = 255;
:00401017   mov  esp, ebp                ; restore stack pointer
:00401019   pop  ebp
:0040101A   ret
  注意上述例子中,堆棧指針是如何被修改的,?而如果某個函數(shù)需要4KB以上局部變量內(nèi)存空間又會怎么樣呢,?其實,堆棧指針并不是被直接修改,,而是通過另一個函數(shù)調(diào)用來修改的,。就是這個額外的函數(shù)調(diào)用使得我們的 ThreadFunc “被破壞”了,因為其遠(yuǎn)程拷貝會調(diào)用一個不存在的東西,。
  我們看看文檔中對堆棧探測和 /Gs編譯器選項是怎么說的:
——“/GS是一個控制堆棧探測的高級特性,,堆棧探測是一系列編譯器插入到每個函數(shù)調(diào)用的代碼。當(dāng)函數(shù)被激活時,,堆棧探測需要的內(nèi)存空間來存儲相關(guān)函數(shù)的局部變量,。
  如果函數(shù)需要的空間大于為局部變量分配的堆棧空間,,其堆棧探測被激活,。默認(rèn)的大小是一個頁面(在80x86處理器上4kb)。這個值允許在Win32 應(yīng)用程序和Windows NT虛擬內(nèi)存管理器之間進行謹(jǐn)慎調(diào)整以便增加運行時承諾給程序堆棧的內(nèi)存。”
我確信有人會問:文檔中的“……堆棧探測到一塊需要的內(nèi)存空間來存儲相關(guān)函數(shù)的局部變量……”那些編譯器選項(它們的描述)在你完全弄明白之前有時真的讓人氣憤,。例如,,如果某個函數(shù)需要12KB的局部變量存儲空間,堆棧內(nèi)存將進行如下方式的分配(更精確地說是“承諾” ),。
sub    esp, 0x1000    ; "分配" 第一次 4 Kb
test  [esp], eax      ; 承諾一個新頁內(nèi)存(如果還沒有承諾)
sub    esp, 0x1000    ; "分配" 第二次4 Kb
test  [esp], eax      ; ...
sub    esp, 0x1000
test  [esp], eax
  注意4KB堆棧指針是如何被修改的,,更重要的是,每一步之后堆棧底是如何被“觸及”(要經(jīng)過檢查),。這樣保證在“分配”(承諾)另一頁面之前,,當(dāng)前頁面承諾的范圍也包含堆棧底。

注意事項
  “每一個線程到達(dá)其自己的堆??臻g,,默認(rèn)情況下,此空間由承諾的以及預(yù)留的內(nèi)存組成,,每個線程使用 1 MB預(yù)留的內(nèi)存,,以及一頁承諾的內(nèi)存,系統(tǒng)將根據(jù)需要從預(yù)留的堆棧內(nèi)存中承諾一頁內(nèi)存區(qū)域” (參見 MSDN CreateThread > dwStackSize > Thread Stack Size)
  還應(yīng)該清楚為什么有關(guān) /GS 的文檔說在堆棧探針在 Win32 應(yīng)用程序和Windows NT虛擬內(nèi)存管理器之間進行謹(jǐn)慎調(diào)整,。

現(xiàn)在回到我們的ThreadFunc以及 4KB 限制
  雖然你可以用 /Gs 防止調(diào)用堆棧探測例程,,但在文檔對于這樣的做法給出了警告,此外,,文件描述可以用 #pragma check_stack 指令關(guān)閉或打開堆棧探測,。但是這個指令好像一點作用都沒有(要么這個文檔是垃圾,要么我疏忽了其它一些信息,?),??傊?,CreateRemoteThread 和 WriteProcessMemory 技術(shù)只能用于注入小塊代碼,所以你的局部變量應(yīng)該盡量少耗費一些內(nèi)存字節(jié),,最好不要超過 4KB限制,。

附錄E

 

為什么要將開關(guān)語句拆分成三個以上?

用下面這個例子很容易解釋這個問題,,假設(shè)有如下這么一個函數(shù):

int Dummy( int arg1 )
{
int ret =0;
switch( arg1 ) {
case 1: ret = 1; break;
case 2: ret = 2; break;
case 3: ret = 3; break;
case 4: ret = 0xA0B0; break;
}
return ret;
}
編譯后變成下面這個樣子:
地址      操作碼/參數(shù)       解釋后的指令
--------------------------------------------------
; arg1 -> ECX
:00401000  8B4C2404         mov ecx, dword ptr [esp+04]
:00401004  33C0             xor eax, eax     ; EAX = 0
:00401006  49               dec ecx          ; ECX --
:00401007  83F903           cmp ecx, 00000003
:0040100A  771E             ja 0040102A
; JMP 到表***中的地址之一
; 注意 ECX 包含的偏移
:0040100C  FF248D2C104000   jmp dword ptr [4*ecx+0040102C]
:00401013  B801000000       mov eax, 00000001   ; case 1: eax = 1;
:00401018  C3               ret
:00401019  B802000000       mov eax, 00000002   ; case 2: eax = 2;
:0040101E  C3               ret
:0040101F  B803000000       mov eax, 00000003   ; case 3: eax = 3;
:00401024  C3               ret
:00401025  B8B0A00000       mov eax, 0000A0B0   ; case 4: eax = 0xA0B0;
:0040102A  C3               ret
:0040102B  90               nop
; 地址表***
:0040102C  13104000         DWORD 00401013   ; jump to case 1
:00401030  19104000         DWORD 00401019   ; jump to case 2
:00401034  1F104000         DWORD 0040101F   ; jump to case 3
:00401038  25104000         DWORD 00401025   ; jump to case 4
注意如何實現(xiàn)這個開關(guān)語句,?

  與其單獨檢查每個CASE語句,不如創(chuàng)建一個地址表,,然后通過簡單地計算地址表的偏移量而跳轉(zhuǎn)到正確的CASE語句,。這實際上是一種改進。假設(shè)你有50個CASE語句,。如果不使用上述的技巧,,你得執(zhí)行50次 CMP和JMP指令來達(dá)到最后一個CASE。相反,有了地址表后,,你可以通過表查詢跳轉(zhuǎn)到任何CASE語句,,從計算機算法角度和時間復(fù)雜度看,我們用O(5)代替了O(2n)算法,。其中:
  1. O表示最壞的時間復(fù)雜度,;
  2. 我們假設(shè)需要5條指令來進行表查詢計算偏移量,最終跳到相應(yīng)的地址,;

  現(xiàn)在,,你也許認(rèn)為出現(xiàn)上述情況只是因為CASE常量被有意選擇為連續(xù)的(1,2,,3,,4)。幸運的是,,它的這個方案可以應(yīng)用于大多數(shù)現(xiàn)實例子中,,只有偏移量的計算稍微有些復(fù)雜。但有兩個例外:

  • 如果CASE語句少于等于三個,;
  • 如果CASE 常量完全互不相關(guān)(如:“"case 1” ,,“case 13” ,“case 50” ,, 和“case 1000” ),;

  顯然,單獨判斷每個的CASE常量的話,,結(jié)果代碼繁瑣耗時,,但使用CMP和JMP指令則使得結(jié)果代碼的執(zhí)行就像普通的if-else 語句。
有趣的地方:如果你不明白CASE語句使用常量表達(dá)式的理由,,那么現(xiàn)在應(yīng)該弄明白了吧,。為了創(chuàng)建地址表,顯然在編譯時就應(yīng)該知道相關(guān)地址,。

現(xiàn)在回到問題,!
注意到地址 0040100C 處的JMP指令了嗎?我們來看看Intel關(guān)于十六進制操作碼 FF 的文檔是怎么說的:

操作碼 指令     描述
FF /4  JMP r/m32  Jump near, absolute indirect,
           address given in r/m32

  原來JMP 使用了一種絕對尋址方式,,也就是說,,它的操作數(shù)(CASE語句中的 0040102C)表示一個絕對地址。還用我說什么嗎,?遠(yuǎn)程 ThreadFunc 會盲目地認(rèn)為地址表中開關(guān)地址是 0040102C,,JMP到一個錯誤的地方,造成遠(yuǎn)程進程崩潰,。

附錄F

 

為什么遠(yuǎn)程進程會崩潰呢,?

當(dāng)遠(yuǎn)程進程崩潰時,,它總是會因為下面這些原因:

  1. 在ThreadFunc 中引用了一個不存在的串;
  2. 在在ThreadFunc 中 中一個或多個指令使用絕對尋址(參見附錄E),;
  3. ThreadFunc 調(diào)用某個不存在的函數(shù)(該調(diào)用可能是編譯器或鏈接器添加的),。你在反匯編器中可以看到這樣的情形:
    :004014C0    push EBP         ; ThreadFunc 的入口點
        :004014C1    mov EBP, ESP
        ...
        :004014C5    call 0041550     ;  這里將使遠(yuǎn)程進程崩潰
        ...
        :00401502    ret
        
    如果 CALL 是由編譯器添加的指令(因為某些“禁忌” 開關(guān)如/GZ是打開的),它將被定位在 ThreadFunc 的開始的某個地方或者結(jié)尾處,。

  不管哪種情況,,你都要小心翼翼地使用 CreateRemoteThread 和 WriteProcessMemory 技術(shù)。尤其要注意你的編譯器/鏈接器選項,,一不小心它們就會在 ThreadFunc 添加內(nèi)容,。

 

 

參考資料

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,,不代表本站觀點,。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,,謹(jǐn)防詐騙,。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報,。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多