📄 进程注入的三种方法 - benny5609的专栏 - csdnblog.htm
字号:
然而,还有两个问题(参考下面对CreateRemoteThread的说明)<BR> <BR> 1.
传递给ThreadProc的lpStartAddress 参数必须为远程进程中的线程过程的起始地址。<BR> 2.
如果把ThreadProc的lpParameter参数当做一个普通的32位整数(FreeLibrary把它当做HMODULE)那么没有如何问题,但是如果把它当做一个指针(LoadLibrary把它当做一个char*),它就必须指向远程进程中的内存数据。<BR> <BR>
第一个问题其实已经迎刃而解了,因为LoadLibrary和FreeLibrary都是存在于kernel32.dll中的函数,而kernel32可以保证任何“正常”进程中都存在,且其加载地址都是一样的。(参看附录A)于是LoadLibrary/FreeLibrary在任何进程中的地址都是一样的,这就保证了传递给远程进程的指针是个有效的指针。<BR> <BR>
第二个问题也很简单:把DLL的文件名(LodLibrary的参数)用WriteProcessMemory复制到远程进程。<BR> <BR>
所以,使用CreateRemoteThread和LoadLibrary技术的步骤如下:<BR> 1.
得到远程进程的HANDLE(使用OpenProcess)。<BR> 2. 在远程进程中为DLL文件名分配内存(VirtualAllocEx)。<BR>
3. 把DLL的文件名(全路径)写到分配的内存中(WriteProcessMemory)<BR> 4.
使用CreateRemoteThread和LoadLibrary把你的DLL映射近远程进程。<BR> 5.
等待远程线程结束(WaitForSingleObject),即等待LoadLibrary返回。也就是说当我们的DllMain(是以DLL_PROCESS_ATTACH为参数调用的)返回时远程线程也就立即结束了。<BR>
6. 取回远程线程的结束码(GetExitCodeThtread),即LoadLibrary的返回值――我们DLL加载后的基地址(HMODULE)。<BR>
7. 释放第2步分配的内存(VirtualFreeEx)。<BR> 8.
用CreateRemoteThread和FreeLibrary把DLL从远程进程中卸载。调用时传递第6步取得的HMODULE给FreeLibrary(通过CreateRemoteThread的lpParameter参数)。<BR>
9. 等待线程的结束(WaitSingleObject)。<BR> <BR>
同时,别忘了在最后关闭所有的句柄:第4、8步得到的线程句柄,第1步得到的远程进程句柄。<BR> <BR>
现在我们看看LibSpy的部分代码,分析一下以上的步骤是任何实现的。为了简单起见,没有包含错误处理和支持Unicode的代码。<BR> HANDLE
hThread;<BR> char szLibPath[_MAX_PATH]; // "LibSpy.dll"的文件名<BR> //
(包含全路径!);<BR> void* pLibRemote; // szLibPath 将要复制到地址<BR> DWORD hLibModule;
//已加载的DLL的基地址(HMODULE);<BR> HMODULE hKernel32 =
::GetModuleHandle("Kernel32");<BR> <BR> //初始化
szLibPath<BR> //...<BR> <BR> // 1. 在远程进程中为szLibPath 分配内存<BR> // 2.
写szLibPath到分配的内存<BR> pLibRemote = ::VirtualAllocEx( hProcess, NULL,
sizeof(szLibPath),<BR> MEM_COMMIT, PAGE_READWRITE
);<BR> ::WriteProcessMemory( hProcess, pLibRemote, (void*)szLibPath,<BR>
sizeof(szLibPath), NULL );<BR> <BR> <BR> // 加载 "LibSpy.dll" 到远程进程<BR> // (通过
CreateRemoteThread & LoadLibrary)<BR> hThread = ::CreateRemoteThread(
hProcess, NULL, 0,<BR> (LPTHREAD_START_ROUTINE) ::GetProcAddress(
hKernel32,<BR> "LoadLibraryA" ),<BR> pLibRemote, 0, NULL
);<BR> ::WaitForSingleObject( hThread, INFINITE
);<BR> <BR> //取得DLL的基地址<BR> ::GetExitCodeThread( hThread, &hLibModule
);<BR> <BR> //扫尾工作<BR> ::CloseHandle( hThread );<BR> ::VirtualFreeEx(
hProcess, pLibRemote, sizeof(szLibPath), MEM_RELEASE
);<BR> <BR> 我们放在DllMain中的真正要注入的代码(比如为SendMessage)现在已经被执行了(由于DLL_PROCESS_ATTACH),所以现在可以把DLL从目的进程中卸载了。<BR> <BR> //
从目标进程卸载LibSpu.dll<BR> // (通过 CreateRemoteThread & FreeLibrary)<BR> hThread
= ::CreateRemoteThread( hProcess, NULL, 0,<BR> (LPTHREAD_START_ROUTINE)
::GetProcAddress( hKernel32,<BR> "FreeLibrary" ),<BR> (void*)hLibModule, 0,
NULL );<BR> ::WaitForSingleObject( hThread, INFINITE );<BR> <BR> //
扫尾工作<BR> ::CloseHandle( hThread );<BR> <BR> 进程间通讯<BR>
到目前为止,我们仅仅讨论了任何向远程进程注入DLL,然而,在多数情况下被注入的DLL需要和你的程序以某种方式通讯(记住,那个DLL是被映射到远程进程中的,而不是在你的本地程序中!)。以密码间谍为例:那个DLL需要知道包含了密码的的控件的句柄。很明显,这个句柄是不能在编译期间硬编码(hardcoded)进去的。同样,当DLL得到密码后,它也需要把密码发回我们的程序。<BR> <BR>
幸运的是,这个问题有很多种解决方案:文件映射(Mapping),WM_COPYDATA,剪贴板等。还有一种非常便利的方法#pragma
data_seg。这里我不想深入讨论因为它们在MSDN(看一下Interprocess
Communications部分)或其他资料中都有很好的说明。我在LibSpy中使用的是#pragma data_seg。<BR> <BR>
你可以在本文章的开头找到LibSpy及源代码的下载链接。<BR> <BR> Ⅲ.CreateRemoteThread和WriteProcessMemory技术<BR> 示例程序:WinSpy<BR> <BR>
另一种注入代码到其他进程地址空间的方法是使用WriteProcessMemory
API。这次你不用编写一个独立的DLL而是直接复制你的代码到远程进程(WriteProcessMemory)并用CreateRemoteThread执行之。<BR> <BR>
让我们看一下CreateRemoteThread的声明:<BR> HANDLE CreateRemoteThread(<BR> HANDLE
hProcess, // handle to process to create thread in<BR> LPSECURITY_ATTRIBUTES
lpThreadAttributes, // pointer to security<BR> // attributes<BR> DWORD
dwStackSize, // initial thread stack size, in bytes<BR> LPTHREAD_START_ROUTINE
lpStartAddress, // pointer to thread<BR> // function<BR> LPVOID lpParameter,
// argument for new thread<BR> DWORD dwCreationFlags, // creation flags<BR>
LPDWORD lpThreadId // pointer to returned thread
identifier<BR> );<BR> <BR> 和CreateThread相比,有一下不同:<BR> <BR> ●增加了hProcess参数。这是要在其中创建线程的进程的句柄。<BR> ●CreateRemoteThread的lpStartAddress参数必须指向远程进程的地址空间中的函数。这个函数必须存在于远程进程中,所以我们不能简单地传递一个本地ThreadFucn的地址,我们必须把代码复制到远程进程。<BR> ●同样,lpParameter参数指向的数据也必须存在于远程进程中,我们也必须复制它。<BR> <BR>
现在,我们总结一下使用该技术的步骤:<BR> <BR> 1. 得到远程进程的HANDLE(OpenProcess)。<BR> 2.
在远程进程中为要注入的数据分配内存(VirtualAllocEx)、<BR> 3.
把初始化后的INJDATA结构复制到分配的内存中(WriteProcessMemory)。<BR> 4.
在远程进程中为要注入的数据分配内存(VirtualAllocEx)。<BR> 5.
把ThreadFunc复制到分配的内存中(WriteProcessMemory)。<BR> 6.
用CreateRemoteThread启动远程的ThreadFunc。<BR> 7.
等待远程线程的结束(WaitForSingleObject)。<BR> 8. 从远程进程取回指执行结果(ReadProcessMemory 或
GetExitCodeThread)。<BR> 9. 释放第2、4步分配的内存(VirtualFreeEx)。<BR> 10.
关闭第6、1步打开打开的句柄。<BR> <BR> 另外,编写ThreadFunc时必须遵守以下规则:<BR> 1.
ThreadFunc不能调用除kernel32.dll和user32.dll之外动态库中的API函数。只有kernel32.dll和user32.dll(如果被加载)可以保证在本地和目的进程中的加载地址是一样的。(注意:user32并不一定被所有的Win32进程加载!)参考附录A。如果你需要调用其他库中的函数,在注入的代码中使用LoadLibrary和GetProcessAddress强制加载。如果由于某种原因,你需要的动态库已经被映射进了目的进程,你也可以使用GetMoudleHandle代替LoadLibrary。同样,如果你想在ThreadFunc中调用你自己的函数,那么就分别复制这些函数到远程进程并通过INJDATA把地址提供给ThreadFunc。<BR>
2.
不要使用static字符串。把所有的字符串提供INJDATA传递。为什么?编译器会把所有的静态字符串放在可执行文件的“.data”段,而仅仅在代码中保留它们的引用(即指针)。这样,远程进程中的ThreadFunc就会执行不存在的内存数据(至少没有在它自己的内存空间中)。<BR>
3. 去掉编译器的/GZ编译选项。这个选项是默认的(看附录B)。<BR> 4.
要么把ThreadFunc和AfterThreadFunc声明为static,要么关闭编译器的“增量连接(incremental
linking)”(看附录C)。<BR> 5.
ThreadFunc中的局部变量总大小必须小于4k字节(看附录D)。注意,当degug编译时,这4k中大约有10个字节会被事先占用。<BR> 6.
如果有多于3个switch分支的case语句,必须像下面这样分割开,或用if-else if代替:<BR> <BR> switch( expression
) {<BR> case constant1: statement1; goto END;<BR> case constant2:
statement2; goto END;<BR> case constant3: statement2; goto
END;<BR> }<BR> switch( expression ) {<BR> case constant4: statement4; goto
END;<BR> case constant5: statement5; goto END;<BR> case constant6:
statement6; goto END;<BR> }<BR> END:<BR> (参考附录E)<BR> <BR>
如果你不按照这些游戏规则玩的话,你注定会使目的进程挂掉!记住,不要妄想远程进程中的任何数据会和你本地进程中的数据存放在相同内存地址!(参看附录F)<BR> (原话如此: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.)<BR> <BR> <BR> GetWindowTextRemote(A/W)<BR> <BR>
所有取得远程edit中文本的工作都被封装进这个函数:GetWindowTextRemote(A/W):<BR> int
GetWindowTextRemoteA( HANDLE hProcess, HWND hWnd, LPSTR lpString );<BR> int
GetWindowTextRemoteW( HANDLE hProcess, HWND hWnd, LPWSTR lpString
);<BR> <BR> 参数:<BR> hProcess <BR> 目的edit所在的进程句柄<BR> hWnd<BR>
目的edit的句柄<BR> lpString<BR> 接收字符串的缓冲<BR> <BR> 返回值:<BR>
成功复制的字符数。<BR> <BR>
让我们看以下它的部分代码,特别是注入的数据和代码。为了简单起见,没有包含支持Unicode的代码。<BR> <BR> INJDATA<BR> <BR> typedef
LRESULT (WINAPI *SENDMESSAGE)(HWND,UINT,WPARAM,LPARAM);<BR> <BR> typedef
struct { <BR> HWND hwnd; // handle to edit control<BR> SENDMESSAGE
fnSendMessage; // pointer to user32!SendMessageA<BR> <BR> char psText[128];
// buffer that is to receive the password<BR> } INJDATA;<BR> <BR> <BR>
INJDATA是要注入远程进程的数据。在把它的地址传递给SendMessageA之前,我们要先对它进行初始化。幸运的是unse32.dll在所有的进程中(如果被映射)总是被映射到相同的地址,所以SendMessageA的地址也总是相同的,这也保证了传递给远程进程的地址是有效的。<BR> <BR> ThreadFunc<BR> <BR> static
DWORD WINAPI ThreadFunc (INJDATA *pData) <BR> {<BR> pData->fnSendMessage(
pData->hwnd, WM_GETTEXT, // 得到密码<BR> sizeof(pData->psText),<BR>
(LPARAM)pData->psText ); <BR> return 0;<BR> }<BR> <BR> // This function
marks the memory address after ThreadFunc.<BR> // int cbCodeSize = (PBYTE)
AfterThreadFunc - (PBYTE) ThreadFunc.<BR> static void AfterThreadFunc
(void)<BR> {<BR> }<BR> <BR> ThreadFunc是远程线程实际执行的代码。<BR>
●注意AfterThreadFunc是如何计算ThreadFunc的代码大小的。一般地,这不是最好的办法,因为编译器会改变你的函数中代码的顺序(比如它会把ThreadFunc放在AfterThreadFunc之后)。然而,你至少可以确定在同一个工程中,比如在我们的WinSpy工程中,你函数的顺序是固定的。如果有必要,你可以使用/ORDER连接选项,或者,用反汇编工具确定ThreadFunc的大小,这个也许会更好。<BR> <BR> 如何用该技术子类(subclass)一个远程控件<BR> 示例程序:InjectEx<BR> <BR>
让我们来讨论一个更复杂的问题:如何子类属于其他进程的一个控件?<BR> <BR> 首先,要完成这个任务,你必须复制两个函数到远程进程:<BR> 1.
ThreadFunc,这个函数通过调用SetWindowLong API来子类远程进程中的控件,<BR> 2. NewProc,
那个控件的新窗口过程(Window Procedure)。<BR> <BR>
然而,最主要的问题是如何传递数据到远程的NewProc。因为NewProc是一个回调(callback)函数,它必须符合特定的要求(译者注:这里指的主要是参数个数和类型),我们不能再简单地传递一个INJDATA的指针作为它的参数。幸运的我已经找到解决这个问题的方法,而且是两个,但是都要借助于汇编语言。我一直都努力避免使用汇编,但是这一次,我们逃不掉了,没有汇编不行的。<BR> <BR> 解决方案1<BR> 看下面的图片:<BR> <BR> <BR>
不知道你是否注意到了,INJDATA紧挨着NewProc放在NewProc的前面?这样的话在编译期间NewProc就可以知道INJDATA的内存地址。更精确地说,它知道INJDATA相对于它自身地址的相对偏移,但是这并不是我们真正想要的。现在,NewProc看起来是这个样子:<BR> static
LRESULT CALLBACK NewProc(<BR> HWND hwnd, // handle to window<BR> UINT uMsg,
// message identifier<BR> WPARAM wParam, // first message parameter<BR>
LPARAM lParam ) // second message parameter<BR> {<BR> INJDATA* pData =
(INJDATA*) NewProc; // pData 指向<BR> // NewProc;<BR> pData--; //
现在pData指向INJDATA;<BR> // 记住,INJDATA 在远程进程中刚好位于 <BR> //
NewProc的紧前面;<BR> <BR> //-----------------------------<BR> // 子类代码<BR> //
........<BR> //-----------------------------<BR> <BR> //调用用来的的窗口过程;<BR>
// fnOldProc (由SetWindowLong返回) 是被ThreadFunc(远程进程中的)初始化<BR> //
并且存储在远程进程中的INJDATA里的;<BR> return pData->fnCallWindowProc(
pData->fnOldProc, <BR> hwnd,uMsg,wParam,lParam );<BR> }<BR> <BR>
然而,还有一个问题,看第一行:<BR> INJDATA* pData = (INJDATA*) NewProc;<BR> <BR>
pData被硬编码为我们进程中NewProc的地址,但这是不对的。因为NewProc会被复制到远程进程,那样的话,这个地址就错了。<BR> <BR>
用C/C++没有办法解决这个问题,可以用内联的汇编来解决。看修改后的NewProc:<BR> <BR> static LRESULT CALLBACK
NewProc(<BR> HWND hwnd, // handle to window<BR> UINT uMsg, // message
identifier<BR> WPARAM wParam, // first message parameter<BR> LPARAM lParam )
// second message parameter<BR> {<BR> // 计算INJDATA 的地址;<BR> //
在远程进程中,INJDATA刚好在<BR> //NewProc的前面;<BR> INJDATA* pData;<BR> _asm {<BR>
call dummy<BR> dummy:<BR> pop ecx // <- ECX 中存放当前的EIP<BR> sub ecx, 9 //
<- ECX 中存放NewProc的地址<BR> mov pData, ecx<BR> }<BR> pData--;<BR> <BR>
//-----------------------------<BR> // 子类代码<BR> // ........<BR>
//-----------------------------<BR> <BR> // 调用原来的窗口过程<BR> return
pData->fnCallWindowProc( pData->fnOldProc, <BR> hwnd,uMsg,wParam,lParam
);<BR> }<BR> <BR>
这是什么意思?每个进程都有一个特殊的寄存器,这个寄存器指向下一条要执行的指令的内存地址,即32位Intel和AMD处理器上所谓的EIP寄存器。因为EIP是个特殊的寄存器,所以你不能像访问通用寄存器(EAX,EBX等)那样来访问它。换句话说,你找不到一个可以用来寻址EIP并且对它进行读写的操作码(OpCode)。然而,EIP同样可以被JMP,CALL,RET等指令隐含地改变(事实上它一直都在改变)。让我们举例说明32位的Intel和AMD处理器上CALL/RET是如何工作的吧:<BR> <BR>
当我们用CALL调用一个子程序时,这个子程序的地址被加载进EIP。同时,在EIP被改变之前,它以前的值会被自动压栈(在后来被用作返回指令指针[return
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -