vc++ 向其他进程注入代码的三种方法
作者:admin 日期:2009-08-25
导言:
我们在Code project(www.codeproject.com)上可以找到许多密码间谍程序(译者注:那些可以看到别的程序中密码框内容的软件),他们都依赖于Windows钩子技术。要实现这个还有其他的方法吗?有!但是,首先,让我们简单回顾一下我们要实现的目标,以便你能弄清楚我在说什么。
要读取一个控件的内容,不管它是否属于你自己的程序,一般来说需要发送 WM_GETTEXT 消息到那个控件。这对edit控件也有效,但是有一种情况例外。如果这个edit控件属于其他进程并且具有 ES_PASSWORD 风格的话,这种方法就不会成功。只有“拥有(OWNS)”这个密码控件的进程才可以用 WM_GETTEXT 取得它的内容。所以,我们的问题就是:如何让下面这句代码在其他进程的地址空间中运行起来:
::SendMessage( hPwdEdit, WM_GETTEXT, nMaxChars, psBuffer );
一般来说,这个问题有三种可能的解决方案:
1. 把你的代码放到一个DLL中;然后用 windows 钩子把它映射到远程进程。
2. 把你的代码放到一个DLL中;然后用 CreateRemoteThread 和 LoadLibrary 把它映射到远程进程。
3. 不用DLL,直接复制你的代码到远程进程(使用WriteProcessMemory)并且用CreateRemoteThread执行之。在这里有详细的说明:
Ⅰ. Windows 钩子
示例程序:HookSpy 和 HookInjEx
Windows钩子的主要作用就是监视某个线程的消息流动。一般可分为:
1. 局部钩子,只监视你自己进程中某个线程的消息流动。
2. 远程钩子,又可以分为:
a. 特定线程的,监视别的进程中某个线程的消息;
b. 系统级的,监视整个系统中正在运行的所有线程的消息。
如果被挂钩(监视)的线程属于别的进程(情况2a和2b),你的钩子过程(hook procedure)必须放在一个动态连接库(DLL)中。系统把这包含了钩子过程的DLL映射到被挂钩的线程的地址空间。Windows会映射整个DLL而不仅仅是你的钩子过程。这就是为什么windows钩子可以用来向其他线程的地址空间注入代码的原因了。
在这里我不想深入讨论钩子的问题(请看MSDN中对SetWindowsHookEx的说明),让我再告诉你两个文档中找不到的诀窍,可能会有用:
1. 当SetWindowHookEx调用成功后,系统会自动映射这个DLL到被挂钩的线程,但并不是立即映射。因为所有的Windows钩子都是基于消息的,直到一个适当的事件发生后这个DLL才被映射。比如:
如果你安装了一个监视所有未排队的(nonqueued)的消息的钩子(WH_CALLWNDPROC),只有一个消息发送到被挂钩线程(的某个窗口)后这个DLL才被映射。也就是说,如果在消息发送到被挂钩线程之前调用了UnhookWindowsHookEx那么这个DLL就永远不会被映射到该线程(虽然SetWindowsHookEx调用成功了)。为了强制映射,可以在调用SetWindowsHookEx后立即发送一个适当的消息到那个线程。
同理,调用UnhookWindowsHookEx之后,只有特定的事件发生后DLL才真正地从被挂钩线程卸载。
2. 当你安装了钩子后,系统的性能会受到影响(特别是系统级的钩子)。然而如果你只是使用的特定线程的钩子来映射DLL而且不截获如何消息的话,这个缺陷也可以轻易地避免。看一下下面的代码片段:
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved )
{
if( ul_reason_for_call == DLL_PROCESS_ATTACH )
{
//用 LoadLibrary增加引用次数
char lib_name[MAX_PATH];
::GetModuleFileName( hModule, lib_name, MAX_PATH );
::LoadLibrary( lib_name );
// 安全卸载钩子
::UnhookWindowsHookEx( g_hHook );
}
return TRUE;
}
我们来看一下。首先,我们用钩子映射这个DLL到远程线程,然后,在DLL被真正映射进去后,我们立即卸载挂钩(unhook)。一般来说当第一个消息到达被挂钩线程后,这DLL会被卸载,然而我们通过LoadLibrary来增加这个DLL的引用次数,避免了DLL被卸载。
剩下的问题是:使用完毕后如何卸载这个DLL?UnhookWindowsHookEx不行了,因为我们已经对那个线程取消挂钩(unhook)了。你可以这么做:
○在你想要卸载这个DLL之前再安装一个钩子;
○发送一个“特殊”的消息到远程线程;
○在你的新钩子的钩子过程(hook procedure)中截获该消息,调用FreeLibrary 和 (译者注:对新钩子调用)UnhookwindowsHookEx。
现在,钩子只在映射DLL到远程进程和从远程进程卸载DLL时使用,对被挂钩线程的性能没有影响。也就是说,我们找到了一种(相比第二部分讨论的LoadLibrary技术)WinNT和Win9x下都可以使用的,不影响目的进程性能的DLL映射机制。
但是,我们应该在何种情况下使用该技巧呢?通常是在DLL需要在远程进程中驻留较长时间(比如你要子类[subclass]另一个进程中的控件)并且你不想过于干涉目的进程时比较适合使用这种技巧。我在HookSpy中并没有使用它,因为那个DLL只是短暂地注入一段时间――只要能取得密码就足够了。我在另一个例子HookInjEx中演示了这种方法。HookInjEx把一个DLL映射进“explorer.exe”(当然,最后又从其中卸载),子类了其中的开始按钮,更确切地说我是把开始按钮的鼠标左右键点击事件颠倒了一下。
你可以在本文章的开头部分找到HookSpy和HookInjEx及其源代码的下载包链接。
Ⅱ. CreateRemoteThread 和 LoadLibrary 技术
示例程序:LibSpy
通常,任何进程都可以通过LoadLibrary动态地加载DLL,但是我们如何强制一个外部进程调用该函数呢?答案是CreateRemoteThread。
让我们先来看看LoadLibrary和FreeLibrary的函数声明:
HINSTANCE LoadLibrary(
LPCTSTR lpLibFileName // address of filename of library module
);
BOOL FreeLibrary(
HMODULE hLibModule // handle to loaded library module
);
再和CreateRemoteThread的线程过程(thread procedure)ThreadProc比较一下:
DWORD WINAPI ThreadProc(
LPVOID lpParameter // thread data
);
你会发现所有的函数都有同样的调用约定(calling convention)、都接受一个32位的参数并且返回值类型的大小也一样。也就是说,我们可以把LoadLibrary/FreeLibrary的指针作为参数传递给CrateRemoteThread。
然而,还有两个问题(参考下面对CreateRemoteThread的说明)
1. 传递给ThreadProc的lpStartAddress 参数必须为远程进程中的线程过程的起始地址。
2. 如果把ThreadProc的lpParameter参数当做一个普通的32位整数(FreeLibrary把它当做HMODULE)那么没有如何问题,但是如果把它当做一个指针(LoadLibrary把它当做一个char*),它就必须指向远程进程中的内存数据。
第一个问题其实已经迎刃而解了,因为LoadLibrary和FreeLibrary都是存在于kernel32.dll中的函数,而kernel32可以保证任何“正常”进程中都存在,且其加载地址都是一样的。(参看附录A)于是LoadLibrary/FreeLibrary在任何进程中的地址都是一样的,这就保证了传递给远程进程的指针是个有效的指针。
第二个问题也很简单:把DLL的文件名(LodLibrary的参数)用WriteProcessMemory复制到远程进程。
所以,使用CreateRemoteThread和LoadLibrary技术的步骤如下:
1. 得到远程进程的HANDLE(使用OpenProcess)。
2. 在远程进程中为DLL文件名分配内存(VirtualAllocEx)。
3. 把DLL的文件名(全路径)写到分配的内存中(WriteProcessMemory)
4. 使用CreateRemoteThread和LoadLibrary把你的DLL映射近远程进程。
5. 等待远程线程结束(WaitForSingleObject),即等待LoadLibrary返回。也就是说当我们的DllMain(是以DLL_PROCESS_ATTACH为参数调用的)返回时远程线程也就立即结束了。
6. 取回远程线程的结束码(GetExitCodeThtread),即LoadLibrary的返回值――我们DLL加载后的基地址(HMODULE)。
7. 释放第2步分配的内存(VirtualFreeEx)。
8. 用CreateRemoteThread和FreeLibrary把DLL从远程进程中卸载。调用时传递第6步取得的HMODULE给FreeLibrary(通过CreateRemoteThread的lpParameter参数)。
9. 等待线程的结束(WaitSingleObject)。
同时,别忘了在最后关闭所有的句柄:第4、8步得到的线程句柄,第1步得到的远程进程句柄。
现在我们看看LibSpy的部分代码,分析一下以上的步骤是任何实现的。为了简单起见,没有包含错误处理和支持Unicode的代码。
HANDLE hThread;
char szLibPath[_MAX_PATH]; // "LibSpy.dll"的文件名
// (包含全路径!);
void* pLibRemote; // szLibPath 将要复制到地址
DWORD hLibModule; //已加载的DLL的基地址(HMODULE);
HMODULE hKernel32 = ::GetModuleHandle("Kernel32");
//初始化 szLibPath
//...
// 1. 在远程进程中为szLibPath 分配内存
// 2. 写szLibPath到分配的内存
pLibRemote = ::VirtualAllocEx( hProcess, NULL, sizeof(szLibPath),
MEM_COMMIT, PAGE_READWRITE );
::WriteProcessMemory( hProcess, pLibRemote, (void*)szLibPath,
sizeof(szLibPath), NULL );
// 加载 "LibSpy.dll" 到远程进程
// (通过 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)现在已经被执行了(由于DLL_PROCESS_ATTACH),所以现在可以把DLL从目的进程中卸载了。
// 从目标进程卸载LibSpu.dll
// (通过 CreateRemoteThread & FreeLibrary)
hThread = ::CreateRemoteThread( hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE) ::GetProcAddress( hKernel32,
"FreeLibrary" ),
(void*)hLibModule, 0, NULL );
::WaitForSingleObject( hThread, INFINITE );
// 扫尾工作
::CloseHandle( hThread );
进程间通讯
到目前为止,我们仅仅讨论了任何向远程进程注入DLL,然而,在多数情况下被注入的DLL需要和你的程序以某种方式通讯(记住,那个DLL是被映射到远程进程中的,而不是在你的本地程序中!)。以密码间谍为例:那个DLL需要知道包含了密码的的控件的句柄。很明显,这个句柄是不能在编译期间硬编码(hardcoded)进去的。同样,当DLL得到密码后,它也需要把密码发回我们的程序。
幸运的是,这个问题有很多种解决方案:文件映射(Mapping),WM_COPYDATA,剪贴板等。还有一种非常便利的方法#pragma data_seg。这里我不想深入讨论因为它们在MSDN(看一下Interprocess Communications部分)或其他资料中都有很好的说明。我在LibSpy中使用的是#pragma data_seg。
你可以在本文章的开头找到LibSpy及源代码的下载链接。
Ⅲ.CreateRemoteThread和WriteProcessMemory技术
示例程序:WinSpy
另一种注入代码到其他进程地址空间的方法是使用WriteProcessMemory API。这次你不用编写一个独立的DLL而是直接复制你的代码到远程进程(WriteProcessMemory)并用CreateRemoteThread执行之。
让我们看一下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参数。这是要在其中创建线程的进程的句柄。
●CreateRemoteThread的lpStartAddress参数必须指向远程进程的地址空间中的函数。这个函数必须存在于远程进程中,所以我们不能简单地传递一个本地ThreadFucn的地址,我们必须把代码复制到远程进程。
●同样,lpParameter参数指向的数据也必须存在于远程进程中,我们也必须复制它。
现在,我们总结一下使用该技术的步骤:
1. 得到远程进程的HANDLE(OpenProcess)。
2. 在远程进程中为要注入的数据分配内存(VirtualAllocEx)、
3. 把初始化后的INJDATA结构复制到分配的内存中(WriteProcessMemory)。
4. 在远程进程中为要注入的数据分配内存(VirtualAllocEx)。
5. 把ThreadFunc复制到分配的内存中(WriteProcessMemory)。
6. 用CreateRemoteThread启动远程的ThreadFunc。
7. 等待远程线程的结束(WaitForSingleObject)。
8. 从远程进程取回指执行结果(ReadProcessMemory 或 GetExitCodeThread)。
9. 释放第2、4步分配的内存(VirtualFreeEx)。
10. 关闭第6、1步打开打开的句柄。
另外,编写ThreadFunc时必须遵守以下规则:
1. ThreadFunc不能调用除kernel32.dll和user32.dll之外动态库中的API函数。只有kernel32.dll和user32.dll(如果被加载)可以保证在本地和目的进程中的加载地址是一样的。(注意:user32并不一定被所有的Win32进程加载!)参考附录A。如果你需要调用其他库中的函数,在注入的代码中使用LoadLibrary和GetProcessAddress强制加载。如果由于某种原因,你需要的动态库已经被映射进了目的进程,你也可以使用GetMoudleHandle代替LoadLibrary。同样,如果你想在ThreadFunc中调用你自己的函数,那么就分别复制这些函数到远程进程并通过INJDATA把地址提供给ThreadFunc。
2. 不要使用static字符串。把所有的字符串提供INJDATA传递。为什么?编译器会把所有的静态字符串放在可执行文件的“.data”段,而仅仅在代码中保留它们的引用(即指针)。这样,远程进程中的ThreadFunc就会执行不存在的内存数据(至少没有在它自己的内存空间中)。
3. 去掉编译器的/GZ编译选项。这个选项是默认的(看附录B)。
4. 要么把ThreadFunc和AfterThreadFunc声明为static,要么关闭编译器的“增量连接(incremental linking)”(看附录C)。
5. ThreadFunc中的局部变量总大小必须小于4k字节(看附录D)。注意,当degug编译时,这4k中大约有10个字节会被事先占用。
6. 如果有多于3个switch分支的case语句,必须像下面这样分割开,或用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)
如果你不按照这些游戏规则玩的话,你注定会使目的进程挂掉!记住,不要妄想远程进程中的任何数据会和你本地进程中的数据存放在相同内存地址!(参看附录F)
(原话如此: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)
所有取得远程edit中文本的工作都被封装进这个函数:GetWindowTextRemote(A/W):
int GetWindowTextRemoteA( HANDLE hProcess, HWND hWnd, LPSTR lpString );
int GetWindowTextRemoteW( HANDLE hProcess, HWND hWnd, LPWSTR lpString );
参数:
hProcess
目的edit所在的进程句柄
hWnd
目的edit的句柄
lpString
接收字符串的缓冲
返回值:
成功复制的字符数。
让我们看以下它的部分代码,特别是注入的数据和代码。为了简单起见,没有包含支持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是要注入远程进程的数据。在把它的地址传递给SendMessageA之前,我们要先对它进行初始化。幸运的是unse32.dll在所有的进程中(如果被映射)总是被映射到相同的地址,所以SendMessageA的地址也总是相同的,这也保证了传递给远程进程的地址是有效的。
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是远程线程实际执行的代码。
●注意AfterThreadFunc是如何计算ThreadFunc的代码大小的。一般地,这不是最好的办法,因为编译器会改变你的函数中代码的顺序(比如它会把ThreadFunc放在AfterThreadFunc之后)。然而,你至少可以确定在同一个工程中,比如在我们的WinSpy工程中,你函数的顺序是固定的。如果有必要,你可以使用/ORDER连接选项,或者,用反汇编工具确定ThreadFunc的大小,这个也许会更好。
如何用该技术子类(subclass)一个远程控件
示例程序:InjectEx
让我们来讨论一个更复杂的问题:如何子类属于其他进程的一个控件?
首先,要完成这个任务,你必须复制两个函数到远程进程:
1. ThreadFunc,这个函数通过调用SetWindowLong API来子类远程进程中的控件,
2. NewProc, 那个控件的新窗口过程(Window Procedure)。
然而,最主要的问题是如何传递数据到远程的NewProc。因为NewProc是一个回调(callback)函数,它必须符合特定的要求(译者注:这里指的主要是参数个数和类型),我们不能再简单地传递一个INJDATA的指针作为它的参数。幸运的我已经找到解决这个问题的方法,而且是两个,但是都要借助于汇编语言。我一直都努力避免使用汇编,但是这一次,我们逃不掉了,没有汇编不行的。
解决方案1
看下面的图片:
不知道你是否注意到了,INJDATA紧挨着NewProc放在NewProc的前面?这样的话在编译期间NewProc就可以知道INJDATA的内存地址。更精确地说,它知道INJDATA相对于它自身地址的相对偏移,但是这并不是我们真正想要的。现在,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 = (INJDATA*) NewProc; // pData 指向
// NewProc;
pData--; // 现在pData指向INJDATA;
// 记住,INJDATA 在远程进程中刚好位于
// NewProc的紧前面;
//-----------------------------
// 子类代码
// ........
//-----------------------------
//调用用来的的窗口过程;
// fnOldProc (由SetWindowLong返回) 是被ThreadFunc(远程进程中的)初始化
// 并且存储在远程进程中的INJDATA里的;
return pData->fnCallWindowProc( pData->fnOldProc,
hwnd,uMsg,wParam,lParam );
}
然而,还有一个问题,看第一行:
INJDATA* pData = (INJDATA*) NewProc;
pData被硬编码为我们进程中NewProc的地址,但这是不对的。因为NewProc会被复制到远程进程,那样的话,这个地址就错了。
用C/C++没有办法解决这个问题,可以用内联的汇编来解决。看修改后的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 的地址;
// 在远程进程中,INJDATA刚好在
//NewProc的前面;
INJDATA* pData;
_asm {
call dummy
dummy:
pop ecx // <- ECX 中存放当前的EIP
sub ecx, 9 // <- ECX 中存放NewProc的地址
mov pData, ecx
}
pData--;
//-----------------------------
// 子类代码
// ........
//-----------------------------
// 调用原来的窗口过程
return pData->fnCallWindowProc( pData->fnOldProc,
hwnd,uMsg,wParam,lParam );
}
这是什么意思?每个进程都有一个特殊的寄存器,这个寄存器指向下一条要执行的指令的内存地址,即32位Intel和AMD处理器上所谓的EIP寄存器。因为EIP是个特殊的寄存器,所以你不能像访问通用寄存器(EAX,EBX等)那样来访问它。换句话说,你找不到一个可以用来寻址EIP并且对它进行读写的操作码(OpCode)。然而,EIP同样可以被JMP,CALL,RET等指令隐含地改变(事实上它一直都在改变)。让我们举例说明32位的Intel和AMD处理器上CALL/RET是如何工作的吧:
当我们用CALL调用一个子程序时,这个子程序的地址被加载进EIP。同时,在EIP被改变之前,它以前的值会被自动压栈(在后来被用作返回指令指针[return instruction-pointer])。在子程序的最后RET指令自动把这个值从栈中弹出到EIP。
现在我们知道了如何通过CALL和RET来修改EIP的值了,但是如何得到他的当前值?
还记得CALL把EIP的值压栈了吗?所以为了得到EIP的值我们调用了一个“假(dummy)函数”然后弹出栈顶值。看一下编译过的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. 一个假的函数调用;仅仅跳到下一条指令并且(译者注:更重要的是)把EIP压栈。
b. 弹出栈顶值到ECX。ECX就保存的EIP的值;这也就是那条“pop ECX”指令的地址。
c. 注意从NewProc的入口点到“pop ECX”指令的“距离”为9字节;因此把ECX减去9就得到的NewProc的地址了。
这样一来,不管被复制到什么地方,NewProc总能正确计算自身的地址了!然而,要注意从NewProc的入口点到“pop ECX”的距离可能会因为你的编译器/链接选项的不同而不同,而且在Release和Degub版本中也是不一样的。但是,不管怎样,你仍然可以在编译期知道这个距离的具体值。
1. 首先,编译你的函数。
2. 在反汇编器(disassembler)中查出正确的距离值。
3. 最后,使用正确的距离值重新编译你的程序。
这也是InjectEx中使用的解决方案。InjectEx和HookInjEx类似,交换开始按钮上的鼠标左右键点击事件。
解决方案2
在远程进程中把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; // 一个假值
//-----------------------------
// 子类代码
// ........
//-----------------------------
// 调用以前的窗口过程
return pData->fnCallWindowProc( pData->fnOldProc,
hwnd,uMsg,wParam,lParam );
}
这里,0XA0B0C0D0仅仅是INJDATA在远程进程中的地址的占位符(placeholder)。你无法在编译期得到这个值,然而你在调用VirtualAllocEx(为INJDATA分配内存时)后确实知道INJDATA的地址!(译者注:就是VirtualAllocEx的返回值)
我们的NewProc编译后大概是这个样子:
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。
现在,你这么做:
1. 把INJDATA,ThreadFunc和NewFunc复制到目的进程。
2. 改变NewPoc的机器码,让pData指向INJDATA的真实地址。
比如,假设INJDATA的的真实地址(VirtualAllocEx的返回值)为0x008a0000,你把NewProc的机器码改为:
558BECC745FCD0C0B0A0......8BE55DC21000 <- 修改前的 NewProc 1
558BECC745FC00008A00......8BE55DC21000 <- 修改后的 NewProc
也就是说,你把假值 A0B0C0D0改为INJDATA的真实地址2
3. 开始指向远程的ThreadFunc,它子类了远程进程中的控件。
我们在Code project(www.codeproject.com)上可以找到许多密码间谍程序(译者注:那些可以看到别的程序中密码框内容的软件),他们都依赖于Windows钩子技术。要实现这个还有其他的方法吗?有!但是,首先,让我们简单回顾一下我们要实现的目标,以便你能弄清楚我在说什么。
要读取一个控件的内容,不管它是否属于你自己的程序,一般来说需要发送 WM_GETTEXT 消息到那个控件。这对edit控件也有效,但是有一种情况例外。如果这个edit控件属于其他进程并且具有 ES_PASSWORD 风格的话,这种方法就不会成功。只有“拥有(OWNS)”这个密码控件的进程才可以用 WM_GETTEXT 取得它的内容。所以,我们的问题就是:如何让下面这句代码在其他进程的地址空间中运行起来:
::SendMessage( hPwdEdit, WM_GETTEXT, nMaxChars, psBuffer );
一般来说,这个问题有三种可能的解决方案:
1. 把你的代码放到一个DLL中;然后用 windows 钩子把它映射到远程进程。
2. 把你的代码放到一个DLL中;然后用 CreateRemoteThread 和 LoadLibrary 把它映射到远程进程。
3. 不用DLL,直接复制你的代码到远程进程(使用WriteProcessMemory)并且用CreateRemoteThread执行之。在这里有详细的说明:
Ⅰ. Windows 钩子
示例程序:HookSpy 和 HookInjEx
Windows钩子的主要作用就是监视某个线程的消息流动。一般可分为:
1. 局部钩子,只监视你自己进程中某个线程的消息流动。
2. 远程钩子,又可以分为:
a. 特定线程的,监视别的进程中某个线程的消息;
b. 系统级的,监视整个系统中正在运行的所有线程的消息。
如果被挂钩(监视)的线程属于别的进程(情况2a和2b),你的钩子过程(hook procedure)必须放在一个动态连接库(DLL)中。系统把这包含了钩子过程的DLL映射到被挂钩的线程的地址空间。Windows会映射整个DLL而不仅仅是你的钩子过程。这就是为什么windows钩子可以用来向其他线程的地址空间注入代码的原因了。
在这里我不想深入讨论钩子的问题(请看MSDN中对SetWindowsHookEx的说明),让我再告诉你两个文档中找不到的诀窍,可能会有用:
1. 当SetWindowHookEx调用成功后,系统会自动映射这个DLL到被挂钩的线程,但并不是立即映射。因为所有的Windows钩子都是基于消息的,直到一个适当的事件发生后这个DLL才被映射。比如:
如果你安装了一个监视所有未排队的(nonqueued)的消息的钩子(WH_CALLWNDPROC),只有一个消息发送到被挂钩线程(的某个窗口)后这个DLL才被映射。也就是说,如果在消息发送到被挂钩线程之前调用了UnhookWindowsHookEx那么这个DLL就永远不会被映射到该线程(虽然SetWindowsHookEx调用成功了)。为了强制映射,可以在调用SetWindowsHookEx后立即发送一个适当的消息到那个线程。
同理,调用UnhookWindowsHookEx之后,只有特定的事件发生后DLL才真正地从被挂钩线程卸载。
2. 当你安装了钩子后,系统的性能会受到影响(特别是系统级的钩子)。然而如果你只是使用的特定线程的钩子来映射DLL而且不截获如何消息的话,这个缺陷也可以轻易地避免。看一下下面的代码片段:
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved )
{
if( ul_reason_for_call == DLL_PROCESS_ATTACH )
{
//用 LoadLibrary增加引用次数
char lib_name[MAX_PATH];
::GetModuleFileName( hModule, lib_name, MAX_PATH );
::LoadLibrary( lib_name );
// 安全卸载钩子
::UnhookWindowsHookEx( g_hHook );
}
return TRUE;
}
我们来看一下。首先,我们用钩子映射这个DLL到远程线程,然后,在DLL被真正映射进去后,我们立即卸载挂钩(unhook)。一般来说当第一个消息到达被挂钩线程后,这DLL会被卸载,然而我们通过LoadLibrary来增加这个DLL的引用次数,避免了DLL被卸载。
剩下的问题是:使用完毕后如何卸载这个DLL?UnhookWindowsHookEx不行了,因为我们已经对那个线程取消挂钩(unhook)了。你可以这么做:
○在你想要卸载这个DLL之前再安装一个钩子;
○发送一个“特殊”的消息到远程线程;
○在你的新钩子的钩子过程(hook procedure)中截获该消息,调用FreeLibrary 和 (译者注:对新钩子调用)UnhookwindowsHookEx。
现在,钩子只在映射DLL到远程进程和从远程进程卸载DLL时使用,对被挂钩线程的性能没有影响。也就是说,我们找到了一种(相比第二部分讨论的LoadLibrary技术)WinNT和Win9x下都可以使用的,不影响目的进程性能的DLL映射机制。
但是,我们应该在何种情况下使用该技巧呢?通常是在DLL需要在远程进程中驻留较长时间(比如你要子类[subclass]另一个进程中的控件)并且你不想过于干涉目的进程时比较适合使用这种技巧。我在HookSpy中并没有使用它,因为那个DLL只是短暂地注入一段时间――只要能取得密码就足够了。我在另一个例子HookInjEx中演示了这种方法。HookInjEx把一个DLL映射进“explorer.exe”(当然,最后又从其中卸载),子类了其中的开始按钮,更确切地说我是把开始按钮的鼠标左右键点击事件颠倒了一下。
你可以在本文章的开头部分找到HookSpy和HookInjEx及其源代码的下载包链接。
Ⅱ. CreateRemoteThread 和 LoadLibrary 技术
示例程序:LibSpy
通常,任何进程都可以通过LoadLibrary动态地加载DLL,但是我们如何强制一个外部进程调用该函数呢?答案是CreateRemoteThread。
让我们先来看看LoadLibrary和FreeLibrary的函数声明:
HINSTANCE LoadLibrary(
LPCTSTR lpLibFileName // address of filename of library module
);
BOOL FreeLibrary(
HMODULE hLibModule // handle to loaded library module
);
再和CreateRemoteThread的线程过程(thread procedure)ThreadProc比较一下:
DWORD WINAPI ThreadProc(
LPVOID lpParameter // thread data
);
你会发现所有的函数都有同样的调用约定(calling convention)、都接受一个32位的参数并且返回值类型的大小也一样。也就是说,我们可以把LoadLibrary/FreeLibrary的指针作为参数传递给CrateRemoteThread。
然而,还有两个问题(参考下面对CreateRemoteThread的说明)
1. 传递给ThreadProc的lpStartAddress 参数必须为远程进程中的线程过程的起始地址。
2. 如果把ThreadProc的lpParameter参数当做一个普通的32位整数(FreeLibrary把它当做HMODULE)那么没有如何问题,但是如果把它当做一个指针(LoadLibrary把它当做一个char*),它就必须指向远程进程中的内存数据。
第一个问题其实已经迎刃而解了,因为LoadLibrary和FreeLibrary都是存在于kernel32.dll中的函数,而kernel32可以保证任何“正常”进程中都存在,且其加载地址都是一样的。(参看附录A)于是LoadLibrary/FreeLibrary在任何进程中的地址都是一样的,这就保证了传递给远程进程的指针是个有效的指针。
第二个问题也很简单:把DLL的文件名(LodLibrary的参数)用WriteProcessMemory复制到远程进程。
所以,使用CreateRemoteThread和LoadLibrary技术的步骤如下:
1. 得到远程进程的HANDLE(使用OpenProcess)。
2. 在远程进程中为DLL文件名分配内存(VirtualAllocEx)。
3. 把DLL的文件名(全路径)写到分配的内存中(WriteProcessMemory)
4. 使用CreateRemoteThread和LoadLibrary把你的DLL映射近远程进程。
5. 等待远程线程结束(WaitForSingleObject),即等待LoadLibrary返回。也就是说当我们的DllMain(是以DLL_PROCESS_ATTACH为参数调用的)返回时远程线程也就立即结束了。
6. 取回远程线程的结束码(GetExitCodeThtread),即LoadLibrary的返回值――我们DLL加载后的基地址(HMODULE)。
7. 释放第2步分配的内存(VirtualFreeEx)。
8. 用CreateRemoteThread和FreeLibrary把DLL从远程进程中卸载。调用时传递第6步取得的HMODULE给FreeLibrary(通过CreateRemoteThread的lpParameter参数)。
9. 等待线程的结束(WaitSingleObject)。
同时,别忘了在最后关闭所有的句柄:第4、8步得到的线程句柄,第1步得到的远程进程句柄。
现在我们看看LibSpy的部分代码,分析一下以上的步骤是任何实现的。为了简单起见,没有包含错误处理和支持Unicode的代码。
HANDLE hThread;
char szLibPath[_MAX_PATH]; // "LibSpy.dll"的文件名
// (包含全路径!);
void* pLibRemote; // szLibPath 将要复制到地址
DWORD hLibModule; //已加载的DLL的基地址(HMODULE);
HMODULE hKernel32 = ::GetModuleHandle("Kernel32");
//初始化 szLibPath
//...
// 1. 在远程进程中为szLibPath 分配内存
// 2. 写szLibPath到分配的内存
pLibRemote = ::VirtualAllocEx( hProcess, NULL, sizeof(szLibPath),
MEM_COMMIT, PAGE_READWRITE );
::WriteProcessMemory( hProcess, pLibRemote, (void*)szLibPath,
sizeof(szLibPath), NULL );
// 加载 "LibSpy.dll" 到远程进程
// (通过 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)现在已经被执行了(由于DLL_PROCESS_ATTACH),所以现在可以把DLL从目的进程中卸载了。
// 从目标进程卸载LibSpu.dll
// (通过 CreateRemoteThread & FreeLibrary)
hThread = ::CreateRemoteThread( hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE) ::GetProcAddress( hKernel32,
"FreeLibrary" ),
(void*)hLibModule, 0, NULL );
::WaitForSingleObject( hThread, INFINITE );
// 扫尾工作
::CloseHandle( hThread );
进程间通讯
到目前为止,我们仅仅讨论了任何向远程进程注入DLL,然而,在多数情况下被注入的DLL需要和你的程序以某种方式通讯(记住,那个DLL是被映射到远程进程中的,而不是在你的本地程序中!)。以密码间谍为例:那个DLL需要知道包含了密码的的控件的句柄。很明显,这个句柄是不能在编译期间硬编码(hardcoded)进去的。同样,当DLL得到密码后,它也需要把密码发回我们的程序。
幸运的是,这个问题有很多种解决方案:文件映射(Mapping),WM_COPYDATA,剪贴板等。还有一种非常便利的方法#pragma data_seg。这里我不想深入讨论因为它们在MSDN(看一下Interprocess Communications部分)或其他资料中都有很好的说明。我在LibSpy中使用的是#pragma data_seg。
你可以在本文章的开头找到LibSpy及源代码的下载链接。
Ⅲ.CreateRemoteThread和WriteProcessMemory技术
示例程序:WinSpy
另一种注入代码到其他进程地址空间的方法是使用WriteProcessMemory API。这次你不用编写一个独立的DLL而是直接复制你的代码到远程进程(WriteProcessMemory)并用CreateRemoteThread执行之。
让我们看一下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参数。这是要在其中创建线程的进程的句柄。
●CreateRemoteThread的lpStartAddress参数必须指向远程进程的地址空间中的函数。这个函数必须存在于远程进程中,所以我们不能简单地传递一个本地ThreadFucn的地址,我们必须把代码复制到远程进程。
●同样,lpParameter参数指向的数据也必须存在于远程进程中,我们也必须复制它。
现在,我们总结一下使用该技术的步骤:
1. 得到远程进程的HANDLE(OpenProcess)。
2. 在远程进程中为要注入的数据分配内存(VirtualAllocEx)、
3. 把初始化后的INJDATA结构复制到分配的内存中(WriteProcessMemory)。
4. 在远程进程中为要注入的数据分配内存(VirtualAllocEx)。
5. 把ThreadFunc复制到分配的内存中(WriteProcessMemory)。
6. 用CreateRemoteThread启动远程的ThreadFunc。
7. 等待远程线程的结束(WaitForSingleObject)。
8. 从远程进程取回指执行结果(ReadProcessMemory 或 GetExitCodeThread)。
9. 释放第2、4步分配的内存(VirtualFreeEx)。
10. 关闭第6、1步打开打开的句柄。
另外,编写ThreadFunc时必须遵守以下规则:
1. ThreadFunc不能调用除kernel32.dll和user32.dll之外动态库中的API函数。只有kernel32.dll和user32.dll(如果被加载)可以保证在本地和目的进程中的加载地址是一样的。(注意:user32并不一定被所有的Win32进程加载!)参考附录A。如果你需要调用其他库中的函数,在注入的代码中使用LoadLibrary和GetProcessAddress强制加载。如果由于某种原因,你需要的动态库已经被映射进了目的进程,你也可以使用GetMoudleHandle代替LoadLibrary。同样,如果你想在ThreadFunc中调用你自己的函数,那么就分别复制这些函数到远程进程并通过INJDATA把地址提供给ThreadFunc。
2. 不要使用static字符串。把所有的字符串提供INJDATA传递。为什么?编译器会把所有的静态字符串放在可执行文件的“.data”段,而仅仅在代码中保留它们的引用(即指针)。这样,远程进程中的ThreadFunc就会执行不存在的内存数据(至少没有在它自己的内存空间中)。
3. 去掉编译器的/GZ编译选项。这个选项是默认的(看附录B)。
4. 要么把ThreadFunc和AfterThreadFunc声明为static,要么关闭编译器的“增量连接(incremental linking)”(看附录C)。
5. ThreadFunc中的局部变量总大小必须小于4k字节(看附录D)。注意,当degug编译时,这4k中大约有10个字节会被事先占用。
6. 如果有多于3个switch分支的case语句,必须像下面这样分割开,或用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)
如果你不按照这些游戏规则玩的话,你注定会使目的进程挂掉!记住,不要妄想远程进程中的任何数据会和你本地进程中的数据存放在相同内存地址!(参看附录F)
(原话如此: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)
所有取得远程edit中文本的工作都被封装进这个函数:GetWindowTextRemote(A/W):
int GetWindowTextRemoteA( HANDLE hProcess, HWND hWnd, LPSTR lpString );
int GetWindowTextRemoteW( HANDLE hProcess, HWND hWnd, LPWSTR lpString );
参数:
hProcess
目的edit所在的进程句柄
hWnd
目的edit的句柄
lpString
接收字符串的缓冲
返回值:
成功复制的字符数。
让我们看以下它的部分代码,特别是注入的数据和代码。为了简单起见,没有包含支持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是要注入远程进程的数据。在把它的地址传递给SendMessageA之前,我们要先对它进行初始化。幸运的是unse32.dll在所有的进程中(如果被映射)总是被映射到相同的地址,所以SendMessageA的地址也总是相同的,这也保证了传递给远程进程的地址是有效的。
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是远程线程实际执行的代码。
●注意AfterThreadFunc是如何计算ThreadFunc的代码大小的。一般地,这不是最好的办法,因为编译器会改变你的函数中代码的顺序(比如它会把ThreadFunc放在AfterThreadFunc之后)。然而,你至少可以确定在同一个工程中,比如在我们的WinSpy工程中,你函数的顺序是固定的。如果有必要,你可以使用/ORDER连接选项,或者,用反汇编工具确定ThreadFunc的大小,这个也许会更好。
如何用该技术子类(subclass)一个远程控件
示例程序:InjectEx
让我们来讨论一个更复杂的问题:如何子类属于其他进程的一个控件?
首先,要完成这个任务,你必须复制两个函数到远程进程:
1. ThreadFunc,这个函数通过调用SetWindowLong API来子类远程进程中的控件,
2. NewProc, 那个控件的新窗口过程(Window Procedure)。
然而,最主要的问题是如何传递数据到远程的NewProc。因为NewProc是一个回调(callback)函数,它必须符合特定的要求(译者注:这里指的主要是参数个数和类型),我们不能再简单地传递一个INJDATA的指针作为它的参数。幸运的我已经找到解决这个问题的方法,而且是两个,但是都要借助于汇编语言。我一直都努力避免使用汇编,但是这一次,我们逃不掉了,没有汇编不行的。
解决方案1
看下面的图片:
不知道你是否注意到了,INJDATA紧挨着NewProc放在NewProc的前面?这样的话在编译期间NewProc就可以知道INJDATA的内存地址。更精确地说,它知道INJDATA相对于它自身地址的相对偏移,但是这并不是我们真正想要的。现在,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 = (INJDATA*) NewProc; // pData 指向
// NewProc;
pData--; // 现在pData指向INJDATA;
// 记住,INJDATA 在远程进程中刚好位于
// NewProc的紧前面;
//-----------------------------
// 子类代码
// ........
//-----------------------------
//调用用来的的窗口过程;
// fnOldProc (由SetWindowLong返回) 是被ThreadFunc(远程进程中的)初始化
// 并且存储在远程进程中的INJDATA里的;
return pData->fnCallWindowProc( pData->fnOldProc,
hwnd,uMsg,wParam,lParam );
}
然而,还有一个问题,看第一行:
INJDATA* pData = (INJDATA*) NewProc;
pData被硬编码为我们进程中NewProc的地址,但这是不对的。因为NewProc会被复制到远程进程,那样的话,这个地址就错了。
用C/C++没有办法解决这个问题,可以用内联的汇编来解决。看修改后的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 的地址;
// 在远程进程中,INJDATA刚好在
//NewProc的前面;
INJDATA* pData;
_asm {
call dummy
dummy:
pop ecx // <- ECX 中存放当前的EIP
sub ecx, 9 // <- ECX 中存放NewProc的地址
mov pData, ecx
}
pData--;
//-----------------------------
// 子类代码
// ........
//-----------------------------
// 调用原来的窗口过程
return pData->fnCallWindowProc( pData->fnOldProc,
hwnd,uMsg,wParam,lParam );
}
这是什么意思?每个进程都有一个特殊的寄存器,这个寄存器指向下一条要执行的指令的内存地址,即32位Intel和AMD处理器上所谓的EIP寄存器。因为EIP是个特殊的寄存器,所以你不能像访问通用寄存器(EAX,EBX等)那样来访问它。换句话说,你找不到一个可以用来寻址EIP并且对它进行读写的操作码(OpCode)。然而,EIP同样可以被JMP,CALL,RET等指令隐含地改变(事实上它一直都在改变)。让我们举例说明32位的Intel和AMD处理器上CALL/RET是如何工作的吧:
当我们用CALL调用一个子程序时,这个子程序的地址被加载进EIP。同时,在EIP被改变之前,它以前的值会被自动压栈(在后来被用作返回指令指针[return instruction-pointer])。在子程序的最后RET指令自动把这个值从栈中弹出到EIP。
现在我们知道了如何通过CALL和RET来修改EIP的值了,但是如何得到他的当前值?
还记得CALL把EIP的值压栈了吗?所以为了得到EIP的值我们调用了一个“假(dummy)函数”然后弹出栈顶值。看一下编译过的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. 一个假的函数调用;仅仅跳到下一条指令并且(译者注:更重要的是)把EIP压栈。
b. 弹出栈顶值到ECX。ECX就保存的EIP的值;这也就是那条“pop ECX”指令的地址。
c. 注意从NewProc的入口点到“pop ECX”指令的“距离”为9字节;因此把ECX减去9就得到的NewProc的地址了。
这样一来,不管被复制到什么地方,NewProc总能正确计算自身的地址了!然而,要注意从NewProc的入口点到“pop ECX”的距离可能会因为你的编译器/链接选项的不同而不同,而且在Release和Degub版本中也是不一样的。但是,不管怎样,你仍然可以在编译期知道这个距离的具体值。
1. 首先,编译你的函数。
2. 在反汇编器(disassembler)中查出正确的距离值。
3. 最后,使用正确的距离值重新编译你的程序。
这也是InjectEx中使用的解决方案。InjectEx和HookInjEx类似,交换开始按钮上的鼠标左右键点击事件。
解决方案2
在远程进程中把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; // 一个假值
//-----------------------------
// 子类代码
// ........
//-----------------------------
// 调用以前的窗口过程
return pData->fnCallWindowProc( pData->fnOldProc,
hwnd,uMsg,wParam,lParam );
}
这里,0XA0B0C0D0仅仅是INJDATA在远程进程中的地址的占位符(placeholder)。你无法在编译期得到这个值,然而你在调用VirtualAllocEx(为INJDATA分配内存时)后确实知道INJDATA的地址!(译者注:就是VirtualAllocEx的返回值)
我们的NewProc编译后大概是这个样子:
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。
现在,你这么做:
1. 把INJDATA,ThreadFunc和NewFunc复制到目的进程。
2. 改变NewPoc的机器码,让pData指向INJDATA的真实地址。
比如,假设INJDATA的的真实地址(VirtualAllocEx的返回值)为0x008a0000,你把NewProc的机器码改为:
558BECC745FCD0C0B0A0......8BE55DC21000 <- 修改前的 NewProc 1
558BECC745FC00008A00......8BE55DC21000 <- 修改后的 NewProc
也就是说,你把假值 A0B0C0D0改为INJDATA的真实地址2
3. 开始指向远程的ThreadFunc,它子类了远程进程中的控件。
评论: 1 | 查看次数: 8722