• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            旅途

            如果想飛得高,就該把地平線忘掉

            向其他進(jìn)程注入代碼的三種方法

            原版地址:http://www.codeproject.com/threads/winspy.asp?df=100&forumid=16291&select=1025152&msg=1025152

            下載WinSpy 

            作者:Robert Kuster

            翻譯:袁曉輝(hyzs@sina.com

            摘要:如何向其他線程的地址空間中注入代碼并在這個(gè)線程的上下文中執(zhí)行之。

            目錄:

            ●導(dǎo)言

            Windows 鉤子(Hooks

            CreateRemoteThread LoadLibrary 技術(shù)

                   ○進(jìn)程間通訊

            CreateRemoteThread WriteProcessmemory 技術(shù)

                   ○如何使用該技術(shù)子類(SubClass)其他進(jìn)程中的控件

                   ○什么情況下適合使用該技術(shù)

            ●寫在最后的話

            ●附錄

            ●參考

            ●文章歷史

            導(dǎo)言:

            我們?cè)?font face="Times New Roman">Code projectwww.codeproject.com)上可以找到許多密碼間諜程序(譯者注:那些可以看到別的程序中密碼框內(nèi)容的軟件),他們都依賴于Windows鉤子技術(shù)。要實(shí)現(xiàn)這個(gè)還有其他的方法嗎?有!但是,首先,讓我們簡(jiǎn)單回顧一下我們要實(shí)現(xiàn)的目標(biāo),以便你能弄清楚我在說(shuō)什么。

            要讀取一個(gè)控件的內(nèi)容,不管它是否屬于你自己的程序,一般來(lái)說(shuō)需要發(fā)送 WM_GETTEXT 消息到那個(gè)控件。這對(duì)edit控件也有效,但是有一種情況例外。如果這個(gè)edit控件屬于其他進(jìn)程并且具有 ES_PASSWORD 風(fēng)格的話,這種方法就不會(huì)成功。只有“擁有(OWNS)”這個(gè)密碼控件的進(jìn)程才可以用 WM_GETTEXT 取得它的內(nèi)容。所以,我們的問(wèn)題就是:如何讓下面這句代碼在其他進(jìn)程的地址空間中運(yùn)行起來(lái):

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

            一般來(lái)說(shuō),這個(gè)問(wèn)題有三種可能的解決方案:

            1. 把你的代碼放到一個(gè)DLL中;然后用 windows 鉤子把它映射到遠(yuǎn)程進(jìn)程。
            2. 把你的代碼放到一個(gè)DLL中;然后用 CreateRemoteThread LoadLibrary 把它映射到遠(yuǎn)程進(jìn)程。
            3. 不用DLL,直接復(fù)制你的代碼到遠(yuǎn)程進(jìn)程(使用WriteProcessMemory)并且用CreateRemoteThread執(zhí)行之。在這里有詳細(xì)的說(shuō)明:

            . Windows 鉤子

            示例程序:HookSpy HookInjEx

            Windows鉤子的主要作用就是監(jiān)視某個(gè)線程的消息流動(dòng)。一般可分為:

            1.  局部鉤子,只監(jiān)視你自己進(jìn)程中某個(gè)線程的消息流動(dòng)。

            2.  遠(yuǎn)程鉤子,又可以分為:

            a.  特定線程的,監(jiān)視別的進(jìn)程中某個(gè)線程的消息;

            b.  系統(tǒng)級(jí)的,監(jiān)視整個(gè)系統(tǒng)中正在運(yùn)行的所有線程的消息。

            如果被掛鉤(監(jiān)視)的線程屬于別的進(jìn)程(情況2a2b),你的鉤子過(guò)程(hook procedure)必須放在一個(gè)動(dòng)態(tài)連接庫(kù)(DLL)中。系統(tǒng)把這包含了鉤子過(guò)程的DLL映射到被掛鉤的線程的地址空間。Windows會(huì)映射整個(gè)DLL而不僅僅是你的鉤子過(guò)程。這就是為什么windows鉤子可以用來(lái)向其他線程的地址空間注入代碼的原因了。

            在這里我不想深入討論鉤子的問(wèn)題(請(qǐng)看MSDN中對(duì)SetWindowsHookEx的說(shuō)明),讓我再告訴你兩個(gè)文檔中找不到的訣竅,可能會(huì)有用:

            1.  當(dāng)SetWindowHookEx調(diào)用成功后,系統(tǒng)會(huì)自動(dòng)映射這個(gè)DLL到被掛鉤的線程,但并不是立即映射。因?yàn)樗械?font face="Times New Roman">Windows鉤子都是基于消息的,直到一個(gè)適當(dāng)?shù)氖录l(fā)生后這個(gè)DLL才被映射。比如:

            如果你安裝了一個(gè)監(jiān)視所有未排隊(duì)的(nonqueued)的消息的鉤子(WH_CALLWNDPROC),只有一個(gè)消息發(fā)送到被掛鉤線程(的某個(gè)窗口)后這個(gè)DLL才被映射。也就是說(shuō),如果在消息發(fā)送到被掛鉤線程之前調(diào)用了UnhookWindowsHookEx那么這個(gè)DLL就永遠(yuǎn)不會(huì)被映射到該線程(雖然SetWindowsHookEx調(diào)用成功了)。為了強(qiáng)制映射,可以在調(diào)用SetWindowsHookEx后立即發(fā)送一個(gè)適當(dāng)?shù)南⒌侥莻€(gè)線程。

            同理,調(diào)用UnhookWindowsHookEx之后,只有特定的事件發(fā)生后DLL才真正地從被掛鉤線程卸載。

            2.  當(dāng)你安裝了鉤子后,系統(tǒng)的性能會(huì)受到影響(特別是系統(tǒng)級(jí)的鉤子)。然而如果你只是使用的特定線程的鉤子來(lái)映射DLL而且不截獲如何消息的話,這個(gè)缺陷也可以輕易地避免??匆幌孪旅娴拇a片段:

            BOOL APIENTRY DllMain( HANDLE hModule,

                                   DWORD  ul_reason_for_call,

                                   LPVOID lpReserved )

            {

                if( ul_reason_for_call == DLL_PROCESS_ATTACH )

                {

                    // LoadLibrary增加引用次數(shù)

                    char lib_name[MAX_PATH];

                    ::GetModuleFileName( hModule, lib_name, MAX_PATH );

                    ::LoadLibrary( lib_name );

                    // 安全卸載鉤子

                    ::UnhookWindowsHookEx( g_hHook );

                }   

                return TRUE;

            }

            我們來(lái)看一下。首先,我們用鉤子映射這個(gè)DLL到遠(yuǎn)程線程,然后,在DLL被真正映射進(jìn)去后,我們立即卸載掛鉤(unhook)。一般來(lái)說(shuō)當(dāng)?shù)谝粋€(gè)消息到達(dá)被掛鉤線程后,這DLL會(huì)被卸載,然而我們通過(guò)LoadLibrary來(lái)增加這個(gè)DLL的引用次數(shù),避免了DLL被卸載。

            剩下的問(wèn)題是:使用完畢后如何卸載這個(gè)DLLUnhookWindowsHookEx不行了,因?yàn)槲覀円呀?jīng)對(duì)那個(gè)線程取消掛鉤(unhook)了。你可以這么做:

                   ○在你想要卸載這個(gè)DLL之前再安裝一個(gè)鉤子;

                   ○發(fā)送一個(gè)“特殊”的消息到遠(yuǎn)程線程;

                   ○在你的新鉤子的鉤子過(guò)程(hook procedure)中截獲該消息,調(diào)用FreeLibrary (譯者注:對(duì)新鉤子調(diào)用)UnhookwindowsHookEx

            現(xiàn)在,鉤子只在映射DLL到遠(yuǎn)程進(jìn)程和從遠(yuǎn)程進(jìn)程卸載DLL時(shí)使用,對(duì)被掛鉤線程的性能沒(méi)有影響。也就是說(shuō),我們找到了一種(相比第二部分討論的LoadLibrary技術(shù))WinNTWin9x下都可以使用的,不影響目的進(jìn)程性能的DLL映射機(jī)制。

            但是,我們應(yīng)該在何種情況下使用該技巧呢?通常是在DLL需要在遠(yuǎn)程進(jìn)程中駐留較長(zhǎng)時(shí)間(比如你要子類[subclass]另一個(gè)進(jìn)程中的控件)并且你不想過(guò)于干涉目的進(jìn)程時(shí)比較適合使用這種技巧。我在HookSpy中并沒(méi)有使用它,因?yàn)槟莻€(gè)DLL只是短暫地注入一段時(shí)間――只要能取得密碼就足夠了。我在另一個(gè)例子HookInjEx中演示了這種方法。HookInjEx把一個(gè)DLL映射進(jìn)“explorer.exe”(當(dāng)然,最后又從其中卸載),子類了其中的開(kāi)始按鈕,更確切地說(shuō)我是把開(kāi)始按鈕的鼠標(biāo)左右鍵點(diǎn)擊事件顛倒了一下。

            你可以在本文章的開(kāi)頭部分找到HookSpyHookInjEx及其源代碼的下載包鏈接。

            . CreateRemoteThread LoadLibrary 技術(shù)

            示例程序:LibSpy

            通常,任何進(jìn)程都可以通過(guò)LoadLibrary動(dòng)態(tài)地加載DLL,但是我們?nèi)绾螐?qiáng)制一個(gè)外部進(jìn)程調(diào)用該函數(shù)呢?答案是CreateRemoteThread。

            讓我們先來(lái)看看LoadLibraryFreeLibrary的函數(shù)聲明:

                 HINSTANCE LoadLibrary(

             

                 HINSTANCE LoadLibrary(

                 HINSTANCE LoadLibrary(

                 HINSTANCE LoadLibrary(

              LPCTSTR lpLibFileName   // address of filename of library module

            );

            BOOL FreeLibrary(

              HMODULE hLibModule      // handle to loaded library module

            );

            再和CreateRemoteThread的線程過(guò)程(thread procedureThreadProc比較一下:

            DWORD WINAPI ThreadProc(

              LPVOID lpParameter   // thread data

            );

            你會(huì)發(fā)現(xiàn)所有的函數(shù)都有同樣的調(diào)用約定(calling convention)、都接受一個(gè)32位的參數(shù)并且返回值類型的大小也一樣。也就是說(shuō),我們可以把LoadLibrary/FreeLibrary的指針作為參數(shù)傳遞給CrateRemoteThread。

            然而,還有兩個(gè)問(wèn)題(參考下面對(duì)CreateRemoteThread的說(shuō)明)

            1.  傳遞給ThreadProclpStartAddress 參數(shù)必須為遠(yuǎn)程進(jìn)程中的線程過(guò)程的起始地址。

            2.  如果把ThreadProclpParameter參數(shù)當(dāng)做一個(gè)普通的32位整數(shù)(FreeLibrary把它當(dāng)做HMODULE)那么沒(méi)有如何問(wèn)題,但是如果把它當(dāng)做一個(gè)指針(LoadLibrary把它當(dāng)做一個(gè)char*),它就必須指向遠(yuǎn)程進(jìn)程中的內(nèi)存數(shù)據(jù)。

            第一個(gè)問(wèn)題其實(shí)已經(jīng)迎刃而解了,因?yàn)?font face="Times New Roman">LoadLibraryFreeLibrary都是存在于kernel32.dll中的函數(shù),而kernel32可以保證任何“正常”進(jìn)程中都存在,且其加載地址都是一樣的。(參看附錄A)于是LoadLibrary/FreeLibrary在任何進(jìn)程中的地址都是一樣的,這就保證了傳遞給遠(yuǎn)程進(jìn)程的指針是個(gè)有效的指針。

            第二個(gè)問(wèn)題也很簡(jiǎn)單:把DLL的文件名(LodLibrary的參數(shù))用WriteProcessMemory復(fù)制到遠(yuǎn)程進(jìn)程。

            所以,使用CreateRemoteThreadLoadLibrary技術(shù)的步驟如下:

            1.  得到遠(yuǎn)程進(jìn)程的HANDLE(使用OpenProcess)。

            2.  在遠(yuǎn)程進(jìn)程中為DLL文件名分配內(nèi)存(VirtualAllocEx)。

            3.  DLL的文件名(全路徑)寫到分配的內(nèi)存中(WriteProcessMemory

            4.  使用CreateRemoteThreadLoadLibrary把你的DLL映射近遠(yuǎn)程進(jìn)程。

            5.  等待遠(yuǎn)程線程結(jié)束(WaitForSingleObject),即等待LoadLibrary返回。也就是說(shuō)當(dāng)我們的DllMain(是以DLL_PROCESS_ATTACH為參數(shù)調(diào)用的)返回時(shí)遠(yuǎn)程線程也就立即結(jié)束了。

            6.  取回遠(yuǎn)程線程的結(jié)束碼(GetExitCodeThtread),即LoadLibrary的返回值――我們DLL加載后的基地址(HMODULE)。

            7.  釋放第2步分配的內(nèi)存(VirtualFreeEx)。

            8.  CreateRemoteThreadFreeLibraryDLL從遠(yuǎn)程進(jìn)程中卸載。調(diào)用時(shí)傳遞第6步取得的HMODULEFreeLibrary(通過(guò)CreateRemoteThreadlpParameter參數(shù))。

            9.  等待線程的結(jié)束(WaitSingleObject)。

            同時(shí),別忘了在最后關(guān)閉所有的句柄:第48步得到的線程句柄,第1步得到的遠(yuǎn)程進(jìn)程句柄。

            現(xiàn)在我們看看LibSpy的部分代碼,分析一下以上的步驟是任何實(shí)現(xiàn)的。為了簡(jiǎn)單起見(jiàn),沒(méi)有包含錯(cuò)誤處理和支持Unicode的代碼。

            HANDLE hThread;

            char    szLibPath[_MAX_PATH];  // "LibSpy.dll"的文件名

                                           // (包含全路徑!);

            void*   pLibRemote;             // szLibPath 將要復(fù)制到地址

            DWORD   hLibModule;   //已加載的DLL的基地址(HMODULE;

            HMODULE hKernel32 = ::GetModuleHandle("Kernel32");

            //初始化 szLibPath

            //...

            // 1. 在遠(yuǎn)程進(jì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)程進(jìn)程

            // (通過(guò) CreateRemoteThread & LoadLibrary)

            hThread = ::CreateRemoteThread( hProcess, NULL, 0,

                        (LPTHREAD_START_ROUTINE) ::GetProcAddress( hKernel32,

                                                   "LoadLibraryA" ),

                         pLibRemote, 0, NULL );

            ::WaitForSingleObject( hThread, INFINITE );

            //取得DLL的基地址

            ::GetExitCodeThread( hThread, &hLibModule );

            //掃尾工作

            ::CloseHandle( hThread );

            ::VirtualFreeEx( hProcess, pLibRemote, sizeof(szLibPath), MEM_RELEASE );

            我們放在DllMain中的真正要注入的代碼(比如為SendMessage)現(xiàn)在已經(jīng)被執(zhí)行了(由于DLL_PROCESS_ATTACH),所以現(xiàn)在可以把DLL從目的進(jìn)程中卸載了。

            // 從目標(biāo)進(jìn)程卸載LibSpu.dll

            // (通過(guò) CreateRemoteThread & FreeLibrary)

            hThread = ::CreateRemoteThread( hProcess, NULL, 0,

                        (LPTHREAD_START_ROUTINE) ::GetProcAddress( hKernel32,

                                                   "FreeLibrary" ),

                        (void*)hLibModule, 0, NULL );

            ::WaitForSingleObject( hThread, INFINITE );

            // 掃尾工作

            ::CloseHandle( hThread );

            進(jìn)程間通訊

            到目前為止,我們僅僅討論了任何向遠(yuǎn)程進(jìn)程注入DLL,然而,在多數(shù)情況下被注入的DLL需要和你的程序以某種方式通訊(記住,那個(gè)DLL是被映射到遠(yuǎn)程進(jìn)程中的,而不是在你的本地程序中?。?。以密碼間諜為例:那個(gè)DLL需要知道包含了密碼的的控件的句柄。很明顯,這個(gè)句柄是不能在編譯期間硬編碼(hardcoded)進(jìn)去的。同樣,當(dāng)DLL得到密碼后,它也需要把密碼發(fā)回我們的程序。

            幸運(yùn)的是,這個(gè)問(wèn)題有很多種解決方案:文件映射(Mapping),WM_COPYDATA,剪貼板等。還有一種非常便利的方法#pragma data_seg。這里我不想深入討論因?yàn)樗鼈冊(cè)贛SDN(看一下Interprocess Communications部分)或其他資料中都有很好的說(shuō)明。我在LibSpy中使用的是#pragma data_seg。

            你可以在本文章的開(kāi)頭找到LibSpy及源代碼的下載鏈接。

            .CreateRemoteThread和WriteProcessMemory技術(shù)

            示例程序:WinSpy

            另一種注入代碼到其他進(jìn)程地址空間的方法是使用WriteProcessMemory API。這次你不用編寫一個(gè)獨(dú)立的DLL而是直接復(fù)制你的代碼到遠(yuǎn)程進(jìn)程(WriteProcessMemory)并用CreateRemoteThread執(zhí)行之。

            讓我們看一下CreateRemoteThread的聲明:

            HANDLE CreateRemoteThread(

              HANDLE hProcess,        // handle to process to create thread in

              LPSECURITY_ATTRIBUTES lpThreadAttributes,  // pointer to security

                                                         // attributes

              DWORD dwStackSize,      // initial thread stack size, in bytes

              LPTHREAD_START_ROUTINE lpStartAddress,     // pointer to thread

                                                         // function

              LPVOID lpParameter,     // argument for new thread

              DWORD dwCreationFlags,  // creation flags

              LPDWORD lpThreadId      // pointer to returned thread identifier

            );

            和CreateThread相比,有一下不同:

            ●增加了hProcess參數(shù)。這是要在其中創(chuàng)建線程的進(jìn)程的句柄。

            ●CreateRemoteThread的lpStartAddress參數(shù)必須指向遠(yuǎn)程進(jìn)程的地址空間中的函數(shù)。這個(gè)函數(shù)必須存在于遠(yuǎn)程進(jìn)程中,所以我們不能簡(jiǎn)單地傳遞一個(gè)本地ThreadFucn的地址,我們必須把代碼復(fù)制到遠(yuǎn)程進(jìn)程。

            ●同樣,lpParameter參數(shù)指向的數(shù)據(jù)也必須存在于遠(yuǎn)程進(jìn)程中,我們也必須復(fù)制它。

            現(xiàn)在,我們總結(jié)一下使用該技術(shù)的步驟:

            1.  得到遠(yuǎn)程進(jìn)程的HANDLE(OpenProcess)。

            2.  在遠(yuǎn)程進(jìn)程中為要注入的數(shù)據(jù)分配內(nèi)存(VirtualAllocEx)、

            3.  把初始化后的INJDATA結(jié)構(gòu)復(fù)制到分配的內(nèi)存中(WriteProcessMemory)。

            4.  在遠(yuǎn)程進(jìn)程中為要注入的數(shù)據(jù)分配內(nèi)存(VirtualAllocEx)。

            5.  把ThreadFunc復(fù)制到分配的內(nèi)存中(WriteProcessMemory)。

            6.  用CreateRemoteThread啟動(dòng)遠(yuǎn)程的ThreadFunc。

            7.  等待遠(yuǎn)程線程的結(jié)束(WaitForSingleObject)。

            8.  從遠(yuǎn)程進(jìn)程取回指執(zhí)行結(jié)果(ReadProcessMemory 或 GetExitCodeThread)。

            9.  釋放第2、4步分配的內(nèi)存(VirtualFreeEx)。

            10.  關(guān)閉第6、1步打開(kāi)打開(kāi)的句柄。

            另外,編寫ThreadFunc時(shí)必須遵守以下規(guī)則:

            1.  ThreadFunc不能調(diào)用除kernel32.dll和user32.dll之外動(dòng)態(tài)庫(kù)中的API函數(shù)。只有kernel32.dll和user32.dll(如果被加載)可以保證在本地和目的進(jìn)程中的加載地址是一樣的。(注意:user32并不一定被所有的Win32進(jìn)程加載?。﹨⒖几戒汚。如果你需要調(diào)用其他庫(kù)中的函數(shù),在注入的代碼中使用LoadLibrary和GetProcessAddress強(qiáng)制加載。如果由于某種原因,你需要的動(dòng)態(tài)庫(kù)已經(jīng)被映射進(jìn)了目的進(jìn)程,你也可以使用GetMoudleHandle代替LoadLibrary。同樣,如果你想在ThreadFunc中調(diào)用你自己的函數(shù),那么就分別復(fù)制這些函數(shù)到遠(yuǎn)程進(jìn)程并通過(guò)INJDATA把地址提供給ThreadFunc。

            2.  不要使用static字符串。把所有的字符串提供INJDATA傳遞。為什么?編譯器會(huì)把所有的靜態(tài)字符串放在可執(zhí)行文件的“.data”段,而僅僅在代碼中保留它們的引用(即指針)。這樣,遠(yuǎn)程進(jìn)程中的ThreadFunc就會(huì)執(zhí)行不存在的內(nèi)存數(shù)據(jù)(至少?zèng)]有在它自己的內(nèi)存空間中)。

            3.  去掉編譯器的/GZ編譯選項(xiàng)。這個(gè)選項(xiàng)是默認(rèn)的(看附錄B)。

            4.  要么把ThreadFunc和AfterThreadFunc聲明為static,要么關(guān)閉編譯器的“增量連接(incremental linking)”(看附錄C)。

            5.  ThreadFunc中的局部變量總大小必須小于4k字節(jié)(看附錄D)。注意,當(dāng)degug編譯時(shí),這4k中大約有10個(gè)字節(jié)會(huì)被事先占用。

            6.  如果有多于3個(gè)switch分支的case語(yǔ)句,必須像下面這樣分割開(kāi),或用if-else if代替. 

            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:

            (參考附錄E)

            如果你不按照這些游戲規(guī)則玩的話,你注定會(huì)使目的進(jìn)程掛掉!記住,不要妄想遠(yuǎn)程進(jìn)程中的任何數(shù)據(jù)會(huì)和你本地進(jìn)程中的數(shù)據(jù)存放在相同內(nèi)存地址?。▍⒖锤戒汧)

            (原話如此:You will almost certainly crash the target process if you don't play by those rules. Just remember: Don't assume anything in the target process is at the same address as it is in your process.)

            GetWindowTextRemote(A/W)

            所有取得遠(yuǎn)程edit中文本的工作都被封裝進(jìn)這個(gè)函數(shù):GetWindowTextRemote(A/W):

            int GetWindowTextRemoteA( HANDLE hProcess, HWND hWnd, LPSTR  lpString );

            int GetWindowTextRemoteW( HANDLE hProcess, HWND hWnd, LPWSTR lpString );

            參數(shù):

            hProcess

                目的edit所在的進(jìn)程句柄

            hWnd

                目的edit的句柄

            lpString

                接收字符串的緩沖

            返回值:

                成功復(fù)制的字符數(shù)。

            讓我們看以下它的部分代碼,特別是注入的數(shù)據(jù)和代碼。為了簡(jiǎn)單起見(jiàn),沒(méi)有包含支持Unicode的代碼。

            INJDATA

            typedef LRESULT     (WINAPI *SENDMESSAGE)(HWND,UINT,WPARAM,LPARAM);

            typedef struct {   

                HWND hwnd;                    // handle to edit control

                SENDMESSAGE  fnSendMessage;   // pointer to user32!SendMessageA

                char psText[128];    // buffer that is to receive the password

            } INJDATA;

             

             

             

             

             

             

            INJDATA是要注入遠(yuǎn)程進(jìn)程的數(shù)據(jù)。在把它的地址傳遞給SendMessageA之前,我們要先對(duì)它進(jìn)行初始化。幸運(yùn)的是unse32.dll在所有的進(jìn)程中(如果被映射)總是被映射到相同的地址,所以SendMessageA的地址也總是相同的,這也保證了傳遞給遠(yuǎn)程進(jìn)程的地址是有效的。

            ThreadFunc

            static DWORD WINAPI ThreadFunc (INJDATA *pData)

            {

                pData->fnSendMessage( pData->hwnd, WM_GETTEXT,    // 得到密碼

                                      sizeof(pData->psText),

                                      (LPARAM)pData->psText ); 

                return 0;

            }

            // This function marks the memory address after ThreadFunc.

            // int cbCodeSize = (PBYTE) AfterThreadFunc - (PBYTE) ThreadFunc.

            static void AfterThreadFunc (void)

            {

            }

            ThreadFunc是遠(yuǎn)程線程實(shí)際執(zhí)行的代碼。

            ●注意AfterThreadFunc是如何計(jì)算ThreadFunc的代碼大小的。一般地,這不是最好的辦法,因?yàn)榫幾g器會(huì)改變你的函數(shù)中代碼的順序(比如它會(huì)把ThreadFunc放在AfterThreadFunc之后)。然而,你至少可以確定在同一個(gè)工程中,比如在我們的WinSpy工程中,你函數(shù)的順序是固定的。如果有必要,你可以使用/ORDER連接選項(xiàng),或者,用反匯編工具確定ThreadFunc的大小,這個(gè)也許會(huì)更好。

            如何用該技術(shù)子類(subclass)一個(gè)遠(yuǎn)程控件

            示例程序:InjectEx

            讓我們來(lái)討論一個(gè)更復(fù)雜的問(wèn)題:如何子類屬于其他進(jìn)程的一個(gè)控件?

            首先,要完成這個(gè)任務(wù),你必須復(fù)制兩個(gè)函數(shù)到遠(yuǎn)程進(jìn)程:

            1.  ThreadFunc,這個(gè)函數(shù)通過(guò)調(diào)用SetWindowLong API來(lái)子類遠(yuǎn)程進(jìn)程中的控件,

            2.  NewProc, 那個(gè)控件的新窗口過(guò)程(Window Procedure)。

            然而,最主要的問(wèn)題是如何傳遞數(shù)據(jù)到遠(yuǎn)程的NewProc。因?yàn)镹ewProc是一個(gè)回調(diào)(callback)函數(shù),它必須符合特定的要求(譯者注:這里指的主要是參數(shù)個(gè)數(shù)和類型),我們不能再簡(jiǎn)單地傳遞一個(gè)INJDATA的指針作為它的參數(shù)。幸運(yùn)的我已經(jīng)找到解決這個(gè)問(wèn)題的方法,而且是兩個(gè),但是都要借助于匯編語(yǔ)言。我一直都努力避免使用匯編,但是這一次,我們逃不掉了,沒(méi)有匯編不行的。

            解決方案1

            看下面的圖片:

             

            不知道你是否注意到了,INJDATA緊挨著NewProc放在NewProc的前面?這樣的話在編譯期間NewProc就可以知道INJDATA的內(nèi)存地址。更精確地說(shuō),它知道INJDATA相對(duì)于它自身地址的相對(duì)偏移,但是這并不是我們真正想要的?,F(xiàn)在,NewProc看起來(lái)是這個(gè)樣子:

            static LRESULT CALLBACK NewProc(

              HWND hwnd,       // handle to window

              UINT uMsg,       // message identifier

              WPARAM wParam,   // first message parameter

              LPARAM lParam )  // second message parameter

            {

                INJDATA* pData = (INJDATA*) NewProc;  // pData 指向

                                                      // NewProc;

                pData--;              // 現(xiàn)在pData指向INJDATA;

                                      // 記住,INJDATA 在遠(yuǎn)程進(jìn)程中剛好位于

                                      // NewProc的緊前面;

                //-----------------------------

                // 子類代碼

                // ........

                //-----------------------------

                //調(diào)用用來(lái)的的窗口過(guò)程;

                // fnOldProc (由SetWindowLong返回) 是被ThreadFunc(遠(yuǎn)程進(jìn)程中的)初始化

                // 并且存儲(chǔ)在遠(yuǎn)程進(jìn)程中的INJDATA里的;

                return pData->fnCallWindowProc( pData->fnOldProc,

                                                hwnd,uMsg,wParam,lParam );

            }

            然而,還有一個(gè)問(wèn)題,看第一行:

            INJDATA* pData = (INJDATA*) NewProc;

            pData被硬編碼為我們進(jìn)程中NewProc的地址,但這是不對(duì)的。因?yàn)镹ewProc會(huì)被復(fù)制到遠(yuǎn)程進(jìn)程,那樣的話,這個(gè)地址就錯(cuò)了。

            用C/C++沒(méi)有辦法解決這個(gè)問(wèn)題,可以用內(nèi)聯(lián)的匯編來(lái)解決??葱薷暮蟮腘ewProc:

            static LRESULT CALLBACK NewProc(

              HWND hwnd,      // handle to window

              UINT uMsg,      // message identifier

              WPARAM wParam,  // first message parameter

              LPARAM lParam ) // second message parameter

            {

                // 計(jì)算INJDATA 的地址;

                // 在遠(yuǎn)程進(jì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)用原來(lái)的窗口過(guò)程

                return pData->fnCallWindowProc( pData->fnOldProc,

                                                hwnd,uMsg,wParam,lParam );

            }

            這是什么意思?每個(gè)進(jìn)程都有一個(gè)特殊的寄存器,這個(gè)寄存器指向下一條要執(zhí)行的指令的內(nèi)存地址,即32位Intel和AMD處理器上所謂的EIP寄存器。因?yàn)镋IP是個(gè)特殊的寄存器,所以你不能像訪問(wèn)通用寄存器(EAX,EBX等)那樣來(lái)訪問(wèn)它。換句話說(shuō),你找不到一個(gè)可以用來(lái)尋址EIP并且對(duì)它進(jìn)行讀寫的操作碼(OpCode)。然而,EIP同樣可以被JMP,CALL,RET等指令隱含地改變(事實(shí)上它一直都在改變)。讓我們舉例說(shuō)明32位的Intel和AMD處理器上CALL/RET是如何工作的吧:

            當(dāng)我們用CALL調(diào)用一個(gè)子程序時(shí),這個(gè)子程序的地址被加載進(jìn)EIP。同時(shí),在EIP被改變之前,它以前的值會(huì)被自動(dòng)壓棧(在后來(lái)被用作返回指令指針[return instruction-pointer])。在子程序的最后RET指令自動(dòng)把這個(gè)值從棧中彈出到EIP。

            現(xiàn)在我們知道了如何通過(guò)CALL和RET來(lái)修改EIP的值了,但是如何得到他的當(dāng)前值?

            還記得CALL把EIP的值壓棧了嗎?所以為了得到EIP的值我們調(diào)用了一個(gè)“假(dummy)函數(shù)”然后彈出棧頂值。看一下編譯過(guò)的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

            a.  一個(gè)假的函數(shù)調(diào)用;僅僅跳到下一條指令并且(譯者注:更重要的是)把EIP壓棧。

            b.  彈出棧頂值到ECX。ECX就保存的EIP的值;這也就是那條“pop ECX”指令的地址。

            c.  注意從NewProc的入口點(diǎn)到“pop ECX”指令的“距離”為9字節(jié);因此把ECX減去9就得到的NewProc的地址了。

            這樣一來(lái),不管被復(fù)制到什么地方,NewProc總能正確計(jì)算自身的地址了!然而,要注意從NewProc的入口點(diǎn)到“pop ECX”的距離可能會(huì)因?yàn)槟愕木幾g器/鏈接選項(xiàng)的不同而不同,而且在Release和Degub版本中也是不一樣的。但是,不管怎樣,你仍然可以在編譯期知道這個(gè)距離的具體值。

            1.  首先,編譯你的函數(shù)。

            2.  在反匯編器(disassembler)中查出正確的距離值。

            3.  最后,使用正確的距離值重新編譯你的程序。

            這也是InjectEx中使用的解決方案。InjectEx和HookInjEx類似,交換開(kāi)始按鈕上的鼠標(biāo)左右鍵點(diǎn)擊事件。

            解決方案2

            在遠(yuǎn)程進(jìn)程中把INJDATA放在NewProc的前面并不是唯一的解決方案。看一下下面的NewProc:

            static LRESULT CALLBACK NewProc(

              HWND hwnd,      // handle to window

              UINT uMsg,      // message identifier

              WPARAM wParam,  // first message parameter

              LPARAM lParam ) // second message parameter

            {

                INJDATA* pData = 0xA0B0C0D0;    // 一個(gè)假設(shè)

                //-----------------------------

                // 子類代碼

                // ........

                //-----------------------------

                // 調(diào)用以前的窗口過(guò)程

                return pData->fnCallWindowProc( pData->fnOldProc,

                                                hwnd,uMsg,wParam,lParam );

            }

            這里,0XA0B0C0D0僅僅是INJDATA在遠(yuǎn)程進(jìn)程中的地址的占位符(placeholder)。你無(wú)法在編譯期得到這個(gè)值,然而你在調(diào)用VirtualAllocEx(為INJDATA分配內(nèi)存時(shí))后確實(shí)知道INJDATA的地址!(譯者注:就是VirtualAllocEx的返回值)

            我們的NewProc編譯后大概是這個(gè)樣子:

            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

            編譯后的機(jī)器碼應(yīng)該為:558BECC745FCD0C0B0A0......8BE55DC21000。

            現(xiàn)在,你這么做:

            1.  把INJDATA,ThreadFunc和NewFunc復(fù)制到目的進(jìn)程。

            2.  改變NewPoc的機(jī)器碼,讓pData指向INJDATA的真實(shí)地址。

            比如,假設(shè)INJDATA的的真實(shí)地址(VirtualAllocEx的返回值)為0x008a0000,你把NewProc的機(jī)器碼改為:

             

            558BECC745FCD0C0B0A0......8BE55DC21000

            <- 修改前的 NewProc 1

            558BECC745FC00008A00......8BE55DC21000

            <- 修改后的 NewProc

                也就是說(shuō),你把假值 A0B0C0D0改為INJDATA的真實(shí)地址2

            3.  開(kāi)始指向遠(yuǎn)程的ThreadFunc,它子類了遠(yuǎn)程進(jìn)程中的控件。

            ¹ 你可能會(huì)問(wèn),為什么A0B0C0D0和008a0000在編譯后的機(jī)器碼中為逆序的。這時(shí)因?yàn)镮ntel和AMD處理器使用littl-endian標(biāo)記法(little-endian notation)來(lái)表示它們的(多字節(jié))數(shù)據(jù)。換句話說(shuō):一個(gè)數(shù)的低字節(jié)(low-order byte)在內(nèi)存中被存放在最低位,高字節(jié)(high-order byte)存放在最高位。

            想像一下,存放在四個(gè)字節(jié)中的單詞“UNIX”,在big-endia系統(tǒng)中被存儲(chǔ)為“UNIX”,在little-endian系統(tǒng)中被存儲(chǔ)為“XINU”。

            ² 一些蹩腳的破解者用類似的方法來(lái)修改可執(zhí)行文件的機(jī)器碼,但是一個(gè)程序一旦載入內(nèi)存,就不能再更改自身的機(jī)器碼(一個(gè)可執(zhí)行文件的.text段是寫保護(hù)的)。我們能修改遠(yuǎn)程進(jìn)程中的NewProc是因?yàn)樗幍哪菈K內(nèi)存在分配時(shí)給予了PAGE_EXECUTE_READWRITE屬性。

            何時(shí)使用CreateRemoteThreadWriteProcessMemory技術(shù)

            通過(guò)CreateRemoteThreadWriteProcessMemory來(lái)注入代碼的技術(shù),和其他兩種方法相比,不需要一個(gè)額外的DLL文件,因此更靈活,但也更復(fù)雜更危險(xiǎn)。一旦你的ThreadFunc中有錯(cuò)誤,遠(yuǎn)程線程會(huì)立即崩潰(看附錄F)。調(diào)試一個(gè)遠(yuǎn)程的ThreadFunc也是場(chǎng)惡夢(mèng),所以你應(yīng)該在僅僅注入若干條指令時(shí)才使用這個(gè)方法。要注入大量的代碼還是使用另外兩種方法吧。

            再說(shuō)一次,你可以在文章的開(kāi)頭部分下載到WinSpy,InjectEx和它們的源代碼。

            寫在最后的話

            最后,我們總結(jié)一些目前還沒(méi)有提到的東西:

            方法

            適用的操作系統(tǒng)

            可操作的進(jìn)程進(jìn)程

            I. Windows鉤子

            Win9x 和WinNT

            僅僅鏈接了USER32.DLL的進(jìn)程1

            II. CreateRemoteThread & LoadLibrary

            僅WinNT2

            所有進(jìn)程3,包括系統(tǒng)服務(wù)4

            III. CreateRemoteThread & WriteProcessMemory

            近WinNT

            所有進(jìn)程,包括系統(tǒng)服務(wù)

            1.  很明顯,你不能給一個(gè)沒(méi)有消息隊(duì)列的線程掛鉤。同樣SetWindowsHookEx也對(duì)系統(tǒng)服務(wù)不起作用(就算它們連接了USER32)。

            2.  Win9x下沒(méi)有CreateRemoteThreadVirtualAllocEx(事實(shí)上可以在9x上模擬它們,但是到目前為止還只是個(gè)神話)

            3.  所有進(jìn)程 所有的Win32進(jìn)程 csrss.exe

            本地程序(native application)比如smss.exe, os2ss.exe, autochk.exe,不使用Win32 APIs,也沒(méi)有連接到kernel32.dll。唯一的例外是csrss.exe,win32子系統(tǒng)自身。它是一個(gè)本地程序,但是它的一些庫(kù)(比如winsrv.dll)需要Win32 DLL包括kernel32.dll.

            4.如果你向注入代碼到系統(tǒng)服務(wù)或csrss.exe,在打開(kāi)遠(yuǎn)程進(jìn)程的句柄(OpenProcess)之前把你的進(jìn)程的優(yōu)先級(jí)調(diào)整為“SeDebugprovilege”(AdjustTokenPrivileges)。

            大概就這些了吧。還有一點(diǎn)你需要牢記在心:你注入的代碼(特別是存在錯(cuò)誤時(shí))很容易就會(huì)把目的進(jìn)程拖垮。記住:責(zé)任隨權(quán)利而來(lái)(Power comes with responsibility)!

            這篇文章中的很多例子都和密碼有關(guān),看過(guò)這篇文章后你可能也會(huì)對(duì)Zhefu Zhang(譯者注:大概是一位中國(guó)人,張哲夫??)寫的Supper Password Spy++感興趣。他講解了如何從IE的密碼框中得到密碼,也說(shuō)了如何保護(hù)你的密碼不被這種攻擊。

            最后一點(diǎn):讀者的反饋是文章作者的唯一報(bào)酬,所以如果你認(rèn)為這篇文章有作用,請(qǐng)留下你的評(píng)論或給它投票。更重要的是,如果你發(fā)現(xiàn)有錯(cuò)誤或bug;或你認(rèn)為什么地方做得還不夠好,有需要改進(jìn)的地方;或有不清楚的地方也都請(qǐng)告訴我。

            感謝

            首先,我要感謝我在CodeGuru(這篇文章最早是在那兒發(fā)表的)的讀者,正是由于你們的鼓勵(lì)和支持這篇文章才得以從最初的1200單詞發(fā)展到今天這樣6000單詞的“龐然大物”。如果說(shuō)有一個(gè)人我要特別感謝的話,他就是Rado Picha。這篇文章的一部分很大程度上得益于他對(duì)我的建議和幫助。最后,但也不能算是最后,感謝Susan Moore,他幫助我跨越了那個(gè)叫做“英語(yǔ)”的雷區(qū),讓這篇文章更加通順達(dá)意。

            ――――――――――――――――――――――――――――――――――――

            附錄

            A)     為什么kernel32.dll和user32.dll中是被映射到相同的內(nèi)存地址?

            我的假定:以為微軟的程序員認(rèn)為這么做可以優(yōu)化速度。讓我們來(lái)解釋一下這是為什么。

            一般來(lái)說(shuō),一個(gè)可執(zhí)行文件包含幾個(gè)段,其中一個(gè)為“.reloc”段。

            當(dāng)鏈接器生成EXE或DLL時(shí),它假定這個(gè)文件會(huì)被加載到一個(gè)特定的地址,也就是所謂的假定/首選加載/基地址(assumed/preferred load/base address)。內(nèi)存映像(image)中的所有絕對(duì)地址都時(shí)基于該“鏈接器假定加載地址”的。如果由于某些原因,映像沒(méi)有加載到這個(gè)地址,那么PE加載器(PE loader)就不得不修正該映像中的所有絕對(duì)地址。這就是“.reloc”段存在的原因:它包含了一個(gè)該映像中所有的“鏈接器假定地址”與真正加載到的地址之間的差異的列表(注意:編譯器產(chǎn)生的大部分指令都使用一種相對(duì)尋址模式,所以,真正需要重定位[relocation]的地方并沒(méi)有你想像的那么多)。如果,從另一方面說(shuō),加載器可以把映像加載到鏈接器首選地址,那么“.reloc”段就會(huì)被徹底忽略。

            但是,因?yàn)槊恳粋€(gè)Win32程序都需要kernel32.dll,大部分需要user32.dll,所以如果總是把它們兩個(gè)映射到其首選地址,那么加載器就不用修正kernel32.dll和user32.dll中的任何(絕對(duì))地址,加載時(shí)間就可以縮短。

            讓我們用下面的例子來(lái)結(jié)束這個(gè)討論:

            把一個(gè)APP.exe的加載地址改為kernel32的(/base:"0x77e80000")或user32的(/base:"0x77e10000")首選地址。如果App.exe沒(méi)有引入U(xiǎn)ESE32,就強(qiáng)制LoadLibrary。然后編譯App.exe,并運(yùn)行它。你會(huì)得到一個(gè)錯(cuò)誤框(“非法的系統(tǒng)DLL重定位”),App.exe無(wú)法被加載。

            為什么?當(dāng)一個(gè)進(jìn)程被創(chuàng)建時(shí),Win2000和WinXP的加載器會(huì)檢查kernel32.dll和user32.dll是否被映射到它們的首選地址(它們的名稱是被硬編碼進(jìn)加載器的),如果沒(méi)有,就會(huì)報(bào)錯(cuò)。在WinNT4 中ole32.dll也會(huì)被檢查。在WinNT3.51或更低版本中,則不會(huì)有任何檢查,kernel32.dll和user32.dll可以被加載到任何地方。唯一一個(gè)總是被加載到首選地址的模塊是ntdll.dll,加載器并不檢查它,但是如果它不在它的首選地址,進(jìn)程根本無(wú)法創(chuàng)建。

            總結(jié)一下:在WinNT4或更高版本的操作系統(tǒng)中:

            ●總被加載到它們的首選地址的DLL有:kernel32.dll,user32.dll和ntdll.dll。

            ●Win32程序(連同csrss.exe)中一定存在的DLL:kernel32.dll和ntdll.dll。

            ●所有進(jìn)程中都存在的dll:ntdll.dll。

            B)     /GZ編譯開(kāi)關(guān)

            在Debug時(shí),/GZ開(kāi)關(guān)默認(rèn)是打開(kāi)的。它可以幫你捕捉一些錯(cuò)誤(詳細(xì)內(nèi)容參考文檔)。但是它對(duì)我們的可執(zhí)行文件有什么影響呢?

            當(dāng)/GZ被使用時(shí),編譯器會(huì)在每個(gè)函數(shù),包含函數(shù)調(diào)用中添加額外的代碼(添加到每個(gè)函數(shù)的最后面)來(lái)檢查ESP棧指針是否被我們的函數(shù)更改過(guò)。但是,等等,ThreadFunc中被添加了一個(gè)函數(shù)調(diào)用?這就是通往災(zāi)難的道路。因?yàn)椋粡?fù)制到遠(yuǎn)程進(jìn)程中的ThreadFunc將調(diào)用一個(gè)在遠(yuǎn)程進(jìn)程中不存在的函數(shù)。

            C)     static函數(shù)和增量連接(Incremental linking)

            增量連接可以縮短連接的時(shí)間,在增量編譯時(shí),每個(gè)函數(shù)調(diào)用都是通過(guò)一個(gè)額外的JMP指令來(lái)實(shí)現(xiàn)的(一個(gè)例外就是被聲明為static的函數(shù)?。┻@些JMP允許連接器移動(dòng)函數(shù)在內(nèi)存中的位置而不用更新調(diào)用該函數(shù)的CALL。但是就是這個(gè)JMP給我們帶來(lái)了麻煩:現(xiàn)在ThreadFunc和AfterThreadFunc將指向JMP指令而不是它們的真實(shí)代碼。所以,當(dāng)計(jì)算ThreadFunc的大小時(shí):

             

            const int cbCodeSize = ((LPBYTE) AfterThreadFunc - (LPBYTE) ThreadFunc);

             

            實(shí)際得到的將是指向ThreadFunc和AfterThreadFunc的JMP指令之間的“距離”?,F(xiàn)在假設(shè)我們的ThreadFunc在004014C0,和其對(duì)應(yīng)的JMP指令在00401020

             

            :00401020   jmp  004014C0

              ...

            :004014C0   push EBP          ; ThreadFunc的真實(shí)地址

            :004014C1   mov  EBP, ESP

             ...

            然后,

            WriteProcessMemory( .., &ThreadFunc, cbCodeSize, ..);

            將把“JMP 004014C0”和其后的cbCodeSize范圍內(nèi)的代碼而不是ThreadFunc復(fù)制到遠(yuǎn)程進(jìn)程。遠(yuǎn)程線程首先會(huì)執(zhí)行“JMP 004010C0”,然后一直執(zhí)行到這個(gè)進(jìn)程代碼的最后一條指令(譯者注:這當(dāng)然不是我們想要的結(jié)果)。

            然而,如果一個(gè)函數(shù)被聲明為static,就算使用增量連接,也不會(huì)被替換為JMP指令。這就是為什么我在規(guī)則#4中說(shuō)把ThreadFunc和AfterThreadFunc聲明為static或禁止增量連接的原因了。(關(guān)于增量連接的其他方面請(qǐng)參看Matt Pietrek寫的“Remove Fatty Deposits from Your Applications Using Our 32-bit Liposuction Tools”)

             

            D)     為什么ThreadFunc只能有4K的局部變量?

             

                  局部變量總是保存在棧上的。假設(shè)一個(gè)函數(shù)有256字節(jié)的局部變量,當(dāng)進(jìn)入該函數(shù)時(shí)(更確切地說(shuō)是在functions prologue中),棧指針會(huì)被減去256。像下面的函數(shù):

            void Dummy(void) {

                BYTE var[256];

                var[0] = 0;

                var[1] = 1;

                var[255] = 255;

            }

            會(huì)被編譯為類似下面的指令:

            :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

            請(qǐng)注意在上面的例子中ESP(棧指針)是如何被改變的。但是如果一個(gè)函數(shù)有多于4K的局部變量該怎么辦?這種情況下,棧指針不會(huì)被直接改變,而是通過(guò)一個(gè)函數(shù)調(diào)用來(lái)正確實(shí)現(xiàn)ESP的改變。但是就是這個(gè)“函數(shù)調(diào)用”導(dǎo)致了ThreadFunc的崩潰,因?yàn)樗谶h(yuǎn)程進(jìn)程中的拷貝將會(huì)調(diào)用一個(gè)不存在的函數(shù)。

            讓我們來(lái)看看文檔關(guān)于棧探針(stack probes)和/Gs編譯選項(xiàng)的說(shuō)明:

            “/Gssize選項(xiàng)是一個(gè)允許你控制棧探針的高級(jí)特性。棧探針是編譯器插入到每個(gè)函數(shù)調(diào)用中的一系列代碼。當(dāng)被激活時(shí),棧探針將溫和地按照存儲(chǔ)函數(shù)局部變量所需要的空間大小來(lái)移動(dòng)。

            如果一個(gè)函數(shù)需要大于size指定的局部變量空間,它的棧探針將被激活。默認(rèn)的size為一個(gè)頁(yè)的大?。ㄔ?0x86上為4k)。這個(gè)值可以使一個(gè)Win32程序和Windows NT的虛擬內(nèi)存管理程序和諧地交互,在運(yùn)行期間向程序棧增加已提交的內(nèi)存總數(shù)。

            我能確定你們對(duì)上面的敘述(“棧探針將溫和地按照存儲(chǔ)函數(shù)局部變量所需要的空間大小來(lái)移動(dòng)”)感到奇怪。這些編譯選項(xiàng)(他們的描述!)有時(shí)候真的讓人很惱火,特別是當(dāng)你想真的了解它們是怎么工作的時(shí)候。打個(gè)比方,如果一個(gè)函數(shù)需要12kb的空間來(lái)存放局部變量,棧上的內(nèi)存是這樣“分配”的

            sub    esp, 0x1000    ; 先“分配”4 Kb

            test  [esp], eax      ; touches memory in order to commit a

                                  ; new page (if not already committed)

            sub    esp, 0x1000    ; “分配”第二個(gè) 4 Kb

            test  [esp], eax      ; ...

            sub    esp, 0x1000

            test  [esp], eax

            注意棧指針是如何以4Kb為單位移動(dòng)的,更重要的是每移動(dòng)一步后使用test對(duì)棧底的處理(more importantly, how the bottom of the stack is "touched" after each step)。這可以確保了在“分配”下一個(gè)頁(yè)之前,包含棧底的頁(yè)已經(jīng)被提交。

            繼續(xù)閱讀文檔的說(shuō)明:

            “每一個(gè)新的線程會(huì)擁有(receives)自己的棧空間,這包括已經(jīng)提交的內(nèi)存和保留的內(nèi)存。默認(rèn)情況下每個(gè)線程使用1MB的保留內(nèi)存和一個(gè)頁(yè)大小的以提交內(nèi)存。如果有必要,系統(tǒng)將從保留內(nèi)存中提交一個(gè)頁(yè)。”(看MSDN中GreateThread > dwStackSize  > “Thread Stack Size”)

            ..現(xiàn)在為什么文檔中說(shuō)“這個(gè)值可以使一個(gè)Win32程序和Windows NT的虛擬內(nèi)存管理程序和諧地交互”也很清楚了。

            E)     為什么我要把多于3個(gè)case分支的swith分割開(kāi)來(lái)呢?

            同樣,用例子來(lái)說(shuō)明會(huì)簡(jiǎn)單些:

            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;

            }

            將會(huì)被編譯為類似下面的代碼:

            Address   OpCode/Params    Decoded instruction

            --------------------------------------------------

                                                         ; 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 to one of the addresses in table <B>***</B>

            ; note that ECX contains the offset

            :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

            看到switch-case是如何實(shí)現(xiàn)的了嗎?

            它沒(méi)有去測(cè)試每個(gè)case分支,而是創(chuàng)建了一個(gè)地址表(address table)。我們簡(jiǎn)單地計(jì)算出在地址表中偏移就可以跳到正確的case分支。想想吧,這真是一個(gè)進(jìn)步,假設(shè)你有一個(gè)50個(gè)分支的switch語(yǔ)句,假如沒(méi)有這個(gè)技巧,你不的不執(zhí)行50次CMP和JMP才能到達(dá)最后一個(gè)case,而使用地址表,你可以通過(guò)一次查表即跳到正確的case。使用算法的時(shí)間復(fù)雜度來(lái)衡量:我們把O(2n)的算法替換成了O(5)的算法,其中:

            1.              O代表最壞情況下的時(shí)間復(fù)雜度。

            2.              我們假設(shè)計(jì)算偏移(即查表)并跳到正確的地址需要5個(gè)指令。

            現(xiàn)在,你可能認(rèn)為上面的情況僅僅是因?yàn)閏ase常量選擇得比較好,(1,2,3,4,5)。幸運(yùn)的是,現(xiàn)實(shí)生活中的大多數(shù)例子都可以應(yīng)用這個(gè)方案,只是偏移的計(jì)算復(fù)雜了一點(diǎn)而已。但是,有兩個(gè)例外:

            ●如果少于3個(gè)case分支,或

            ●如果case常量是完全相互無(wú)關(guān)的。(比如 1, 13, 50, 1000)。

            最終的結(jié)果和你使用普通的if-else if是一樣的。

            有趣的地方:如果你曾經(jīng)為case后面只能跟常量而迷惑的話,現(xiàn)在你應(yīng)該知道為什么了吧。這個(gè)值必須在編譯期間就確定下來(lái),這樣才能創(chuàng)建地址表。

            回到我們的問(wèn)題!

            注意到0040100C處的JMP指令了嗎?我們來(lái)看看Intel的文檔對(duì)十六進(jìn)制操作碼FF的說(shuō)明:

            Opcode    Instruction    Description

            FF /4     JMP r/m32      Jump near, absolute indirect,

                                     address given in r/m32

            JMP使用了絕對(duì)地址!也就是說(shuō),它的其中一個(gè)操作數(shù)(在這里是0040102C)代表一個(gè)絕對(duì)地址。還用多說(shuō)嗎?現(xiàn)在遠(yuǎn)程的ThreadFunc會(huì)盲目第在地址表中004101C然后跳到這個(gè)錯(cuò)誤的地方,馬上使遠(yuǎn)程進(jìn)程掛掉了。

            F)     到底是什么原因使遠(yuǎn)程進(jìn)程崩潰了?

            如果你的遠(yuǎn)程進(jìn)程崩潰了,原因可能為下列之一:

            1.              你引用了ThreadFunc中一個(gè)不存在的字符串。

            2.              ThreadFunc中一個(gè)或多個(gè)指令使用了絕對(duì)尋址(看附錄E中的例子)

            3.              ThreadFunc調(diào)用了一個(gè)不存在的函數(shù)(這個(gè)函數(shù)調(diào)用可能是編譯器或連接器添加的)。這時(shí)候你需要在反匯編器中尋找類似下面的代碼:

            :004014C0    push EBP         ; entry point of ThreadFunc

            :004014C1    mov EBP, ESP

             ...

            :004014C5    call 0041550     ; 在這里崩潰了

                                          ; remote process

             ...

            :00401502    ret

            如果這個(gè)有爭(zhēng)議的CALL是編譯器添加的(因?yàn)橐恍┎辉摯蜷_(kāi)的編譯開(kāi)關(guān)比如/GZ打開(kāi)了),它要么在ThreadFunc的開(kāi)頭要么在ThreadFunc接近結(jié)尾的地方

            不管在什么情況下,你使用CreateRemoteThread & WriteProcessMemory技術(shù)時(shí)必須萬(wàn)分的小心,特別是編譯器/連接器的設(shè)置,它們很可能會(huì)給你的ThreadFunc添加一些帶來(lái)麻煩的東西。

            參考(省略)

            文章歷史(省略)

            <結(jié)束>


            posted on 2007-07-22 00:06 旅途 閱讀(430) 評(píng)論(0)  編輯 收藏 引用 所屬分類: 深入windows

            亚洲AV无码成人网站久久精品大| 99精品伊人久久久大香线蕉| 国产91久久综合| 久久无码人妻一区二区三区| 久久久国产精华液| 狠狠色丁香久久婷婷综合_中 | 久久AV无码精品人妻糸列| 久久国产视屏| 看全色黄大色大片免费久久久| 久久久久18| 97视频久久久| 久久精品人人做人人爽97| 久久久久久a亚洲欧洲aⅴ| 亚洲国产成人久久精品影视| 国产99久久久久久免费看| 香蕉aa三级久久毛片| 三级三级久久三级久久| 97久久婷婷五月综合色d啪蜜芽 | 日韩亚洲国产综合久久久| 亚洲伊人久久成综合人影院| 欧美成a人片免费看久久| 99久久精品国产麻豆| 99久久精品久久久久久清纯| 久久久WWW成人| 久久久久久曰本AV免费免费| 色偷偷偷久久伊人大杳蕉| 99久久精品日本一区二区免费| 国产巨作麻豆欧美亚洲综合久久| 久久午夜免费视频| 欧美777精品久久久久网| 色悠久久久久久久综合网| 一本一道久久综合狠狠老| 亚洲伊人久久大香线蕉苏妲己| 欧美大战日韩91综合一区婷婷久久青草| 久久免费视频1| 日韩欧美亚洲综合久久影院d3| 亚洲人成电影网站久久| 99re这里只有精品热久久| 久久久久久久精品妇女99| 国产L精品国产亚洲区久久| 久久亚洲AV成人无码国产 |