转自:http://blog.sina.com.cn/s/blog_4ad042e50102e6a2.html
原版地址:http://www.codeproject.com/threads/winspy.asp?df=100&forumid=16291&select=1025152&msg=1025152
作者:Robert Kuster
翻译:袁晓辉(hyzs@sina.com)
摘要:如何向其他线程的地址空间中注入代码并在这个线程的上下文中执行之。
目录:
●导言
●Windows
●CreateRemoteThread
●CreateRemoteThread
●写在最后的话
●附录
●参考
●文章历史
导言:
我们在Code project(www.codeproject.com)上可以找到许多密码间谍程序(译者注:那些可以看到别的程序中密码框内容的软件),他们都依赖于Windows钩子技术。要实现这个还有其他的方法吗?有!但是,首先,让我们简单回顾一下我们要实现的目标,以便你能弄清楚我在说什么。
要读取一个控件的内容,不管它是否属于你自己的程序,一般来说需要发送
::SendMessage( hPwdEdit, WM_GETTEXT, nMaxChars, psBuffer );
一般来说,这个问题有三种可能的解决方案:
- 把你的代码放到一个DLL中;然后用
windows 钩子把它映射到远程进程。 - 把你的代码放到一个DLL中;然后用
CreateRemoteThread 和 LoadLibrary 把它映射到远程进程。 - 不用DLL,直接复制你的代码到远程进程(使用WriteProcessMemory)并且用CreateRemoteThread执行之。在这里有详细的说明:
Ⅰ. Windows
示例程序:HookSpy
Windows钩子的主要作用就是监视某个线程的消息流动。一般可分为:
1.
2.
a.
b.
如果被挂钩(监视)的线程属于别的进程(情况2a和2b),你的钩子过程(hook procedure)必须放在一个动态连接库(DLL)中。系统把这包含了钩子过程的DLL映射到被挂钩的线程的地址空间。Windows会映射整个DLL而不仅仅是你的钩子过程。这就是为什么windows钩子可以用来向其他线程的地址空间注入代码的原因了。
在这里我不想深入讨论钩子的问题(请看MSDN中对SetWindowsHookEx的说明),让我再告诉你两个文档中找不到的诀窍,可能会有用:
1.
如果你安装了一个监视所有未排队的(nonqueued)的消息的钩子(WH_CALLWNDPROC),只有一个消息发送到被挂钩线程(的某个窗口)后这个DLL才被映射。也就是说,如果在消息发送到被挂钩线程之前调用了UnhookWindowsHookEx那么这个DLL就永远不会被映射到该线程(虽然SetWindowsHookEx调用成功了)。为了强制映射,可以在调用SetWindowsHookEx后立即发送一个适当的消息到那个线程。
同理,调用UnhookWindowsHookEx之后,只有特定的事件发生后DLL才真正地从被挂钩线程卸载。
2.
BOOL APIENTRY DllMain( HANDLE hModule,
{
}
我们来看一下。首先,我们用钩子映射这个DLL到远程线程,然后,在DLL被真正映射进去后,我们立即卸载挂钩(unhook)。一般来说当第一个消息到达被挂钩线程后,这DLL会被卸载,然而我们通过LoadLibrary来增加这个DLL的引用次数,避免了DLL被卸载。
剩下的问题是:使用完毕后如何卸载这个DLL?UnhookWindowsHookEx不行了,因为我们已经对那个线程取消挂钩(unhook)了。你可以这么做:
现在,钩子只在映射DLL到远程进程和从远程进程卸载DLL时使用,对被挂钩线程的性能没有影响。也就是说,我们找到了一种(相比第二部分讨论的LoadLibrary技术)WinNT和Win9x下都可以使用的,不影响目的进程性能的DLL映射机制。
但是,我们应该在何种情况下使用该技巧呢?通常是在DLL需要在远程进程中驻留较长时间(比如你要子类[subclass]另一个进程中的控件)并且你不想过于干涉目的进程时比较适合使用这种技巧。我在HookSpy中并没有使用它,因为那个DLL只是短暂地注入一段时间――只要能取得密码就足够了。我在另一个例子HookInjEx中演示了这种方法。HookInjEx把一个DLL映射进“explorer.exe”(当然,最后又从其中卸载),子类了其中的开始按钮,更确切地说我是把开始按钮的鼠标左右键点击事件颠倒了一下。
你可以在本文章的开头部分找到HookSpy和HookInjEx及其源代码的下载包链接。
Ⅱ. CreateRemoteThread
示例程序:LibSpy
通常,任何进程都可以通过LoadLibrary动态地加载DLL,但是我们如何强制一个外部进程调用该函数呢?答案是CreateRemoteThread。
让我们先来看看LoadLibrary和FreeLibrary的函数声明:
);
BOOL FreeLibrary(
);
再和CreateRemoteThread的线程过程(thread procedure)ThreadProc比较一下:
DWORD WINAPI ThreadProc(
);
你会发现所有的函数都有同样的调用约定(calling convention)、都接受一个32位的参数并且返回值类型的大小也一样。也就是说,我们可以把LoadLibrary/FreeLibrary的指针作为参数传递给CrateRemoteThread。
然而,还有两个问题(参考下面对CreateRemoteThread的说明)
1.
2.
第一个问题其实已经迎刃而解了,因为LoadLibrary和FreeLibrary都是存在于kernel32.dll中的函数,而kernel32可以保证任何“正常”进程中都存在,且其加载地址都是一样的。(参看附录A)于是LoadLibrary/FreeLibrary在任何进程中的地址都是一样的,这就保证了传递给远程进程的指针是个有效的指针。
第二个问题也很简单:把DLL的文件名(LodLibrary的参数)用WriteProcessMemory复制到远程进程。
所以,使用CreateRemoteThread和LoadLibrary技术的步骤如下:
1.
2.
3.
4.
5.
6.
7.
8.
9.
同时,别忘了在最后关闭所有的句柄:第4、8步得到的线程句柄,第1步得到的远程进程句柄。
现在我们看看LibSpy的部分代码,分析一下以上的步骤是任何实现的。为了简单起见,没有包含错误处理和支持Unicode的代码。
HANDLE hThread;
char
void*
DWORD
HMODULE hKernel32 = ::GetModuleHandle("Kernel32");
//初始化
//...
// 1.
// 2.
pLibRemote = ::VirtualAllocEx( hProcess, NULL, sizeof(szLibPath),
::WriteProcessMemory( hProcess, pLibRemote, (void*)szLibPath,
//
// (通过
hThread = ::CreateRemoteThread( hProcess, NULL, 0,
::WaitForSingleObject( hThread, INFINITE );
//取得DLL的基地址
::GetExitCodeThread( hThread, &hLibModule );
//扫尾工作
::CloseHandle( hThread );
::VirtualFreeEx( hProcess, pLibRemote, sizeof(szLibPath), MEM_RELEASE );
我们放在DllMain中的真正要注入的代码(比如为SendMessage)现在已经被执行了(由于DLL_PROCESS_ATTACH),所以现在可以把DLL从目的进程中卸载了。
//
// (通过
hThread = ::CreateRemoteThread( hProcess, NULL, 0,
::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(
);
和CreateThread相比,有一下不同:
●增加了hProcess参数。这是要在其中创建线程的进程的句柄。
●CreateRemoteThread的lpStartAddress参数必须指向远程进程的地址空间中的函数。这个函数必须存在于远程进程中,所以我们不能简单地传递一个本地ThreadFucn的地址,我们必须把代码复制到远程进程。
●同样,lpParameter参数指向的数据也必须存在于远程进程中,我们也必须复制它。
现在,我们总结一下使用该技术的步骤:
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
另外,编写ThreadFunc时必须遵守以下规则:
1.
2.
3.
4.
5.
6.
switch( expression ) {
}
switch( expression ) {
}
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
int GetWindowTextRemoteW( HANDLE hProcess, HWND hWnd, LPWSTR lpString );
参数:
hProcess
hWnd
lpString
返回值:
让我们看以下它的部分代码,特别是注入的数据和代码。为了简单起见,没有包含支持Unicode的代码。
INJDATA
typedef LRESULT
typedef struct {
} INJDATA;
INJDATA是要注入远程进程的数据。在把它的地址传递给SendMessageA之前,我们要先对它进行初始化。幸运的是unse32.dll在所有的进程中(如果被映射)总是被映射到相同的地址,所以SendMessageA的地址也总是相同的,这也保证了传递给远程进程的地址是有效的。
ThreadFunc
static DWORD WINAPI ThreadFunc (INJDATA *pData)
{
}
// 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.
2.
然而,最主要的问题是如何传递数据到远程的NewProc。因为NewProc是一个回调(callback)函数,它必须符合特定的要求(译者注:这里指的主要是参数个数和类型),我们不能再简单地传递一个INJDATA的指针作为它的参数。幸运的我已经找到解决这个问题的方法,而且是两个,但是都要借助于汇编语言。我一直都努力避免使用汇编,但是这一次,我们逃不掉了,没有汇编不行的。
解决方案1
看下面的图片:
不知道你是否注意到了,INJDATA紧挨着NewProc放在NewProc的前面?这样的话在编译期间NewProc就可以知道INJDATA的内存地址。更精确地说,它知道INJDATA相对于它自身地址的相对偏移,但是这并不是我们真正想要的。现在,NewProc看起来是这个样子:
static LRESULT CALLBACK NewProc(
{
}
然而,还有一个问题,看第一行:
INJDATA* pData = (INJDATA*) NewProc;
pData被硬编码为我们进程中NewProc的地址,但这是不对的。因为NewProc会被复制到远程进程,那样的话,这个地址就错了。
用C/C++没有办法解决这个问题,可以用内联的汇编来解决。看修改后的NewProc:
static LRESULT CALLBACK NewProc(
{
dummy:
}
这是什么意思?每个进程都有一个特殊的寄存器,这个寄存器指向下一条要执行的指令的内存地址,即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
--------------------------------------------------
:00401000
:00401001
:00401003
:00401004
:00401009
:0040100A
:0040100D
:00401010
:00401013
.....
.....
:0040102D
:0040102F
:00401030
a.
b.
c.
这样一来,不管被复制到什么地方,NewProc总能正确计算自身的地址了!然而,要注意从NewProc的入口点到“pop ECX”的距离可能会因为你的编译器/链接选项的不同而不同,而且在Release和Degub版本中也是不一样的。但是,不管怎样,你仍然可以在编译期知道这个距离的具体值。
1.
2.
3.
这也是InjectEx中使用的解决方案。InjectEx和HookInjEx类似,交换开始按钮上的鼠标左右键点击事件。
解决方案2
在远程进程中把INJDATA放在NewProc的前面并不是唯一的解决方案。看一下下面的NewProc:
static LRESULT CALLBACK NewProc(
{
}
这里,0XA0B0C0D0仅仅是INJDATA在远程进程中的地址的占位符(placeholder)。你无法在编译期得到这个值,然而你在调用VirtualAllocEx(为INJDATA分配内存时)后确实知道INJDATA的地址!(译者注:就是VirtualAllocEx的返回值)
我们的NewProc编译后大概是这个样子:
Address
--------------------------------------------------
:00401000
:00401001
:00401003
:0040100A
....
....
:0040102D
:0040102F
:00401030
编译后的机器码应该为:558BECC745FCD0C0B0A0......8BE55DC21000。
现在,你这么做:
1.
2.
比如,假设INJDATA的的真实地址(VirtualAllocEx的返回值)为0x008a0000,你把NewProc的机器码改为:
558BECC745FCD0C0B0A0......8BE55DC21000 | <- |
558BECC745FC00008A00......8BE55DC21000 | <- |
3.
¹
想像一下,存放在四个字节中的单词“UNIX”,在big-endia系统中被存储为“UNIX”,在little-endian系统中被存储为“XINU”。
²
何时使用CreateRemoteThread和WriteProcessMemory技术
通过CreateRemoteThread和WriteProcessMemory来注入代码的技术,和其他两种方法相比,不需要一个额外的DLL文件,因此更灵活,但也更复杂更危险。一旦你的ThreadFunc中有错误,远程线程会立即崩溃(看附录F)。调试一个远程的ThreadFunc也是场恶梦,所以你应该在仅仅注入若干条指令时才使用这个方法。要注入大量的代码还是使用另外两种方法吧。
再说一次,你可以在文章的开头部分下载到WinSpy,InjectEx和它们的源代码。
写在最后的话
最后,我们总结一些目前还没有提到的东西:
方法 | 适用的操作系统 | 可操作的进程进程 |
I. Windows钩子 | Win9x | 仅仅链接了USER32.DLL的进程1 |
II. CreateRemoteThread & LoadLibrary | 仅WinNT2 | 所有进程3,包括系统服务4 |
III. CreateRemoteThread & WriteProcessMemory | 近WinNT | 所有进程,包括系统服务 |
1.
2.
3.
本地程序(native application)比如smss.exe, os2ss.exe, autochk.exe,不使用Win32 APIs,也没有连接到kernel32.dll。唯一的例外是csrss.exe,win32子系统自身。它是一个本地程序,但是它的一些库(比如winsrv.dll)需要Win32 DLL包括kernel32.dll.
4.如果你向注入代码到系统服务或csrss.exe,在打开远程进程的句柄(OpenProcess)之前把你的进程的优先级调整为“SeDebugprovilege”(AdjustTokenPrivileges)。
大概就这些了吧。还有一点你需要牢记在心:你注入的代码(特别是存在错误时)很容易就会把目的进程拖垮。记住:责任随权利而来(Power comes with responsibility)!
这篇文章中的很多例子都和密码有关,看过这篇文章后你可能也会对Zhefu Zhang(译者注:大概是一位中国人,张哲夫??)写的Supper Password Spy++感兴趣。他讲解了如何从IE的密码框中得到密码,也说了如何保护你的密码不被这种攻击。
最后一点:读者的反馈是文章作者的唯一报酬,所以如果你认为这篇文章有作用,请留下你的评论或给它投票。更重要的是,如果你发现有错误或bug;或你认为什么地方做得还不够好,有需要改进的地方;或有不清楚的地方也都请告诉我。
感谢
首先,我要感谢我在CodeGuru(这篇文章最早是在那儿发表的)的读者,正是由于你们的鼓励和支持这篇文章才得以从最初的1200单词发展到今天这样6000单词的“庞然大物”。如果说有一个人我要特别感谢的话,他就是Rado Picha。这篇文章的一部分很大程度上得益于他对我的建议和帮助。最后,但也不能算是最后,感谢Susan Moore,他帮助我跨越了那个叫做“英语”的雷区,让这篇文章更加通顺达意。
――――――――――――――――――――――――――――――――――――
附录
A)
我的假定:以为微软的程序员认为这么做可以优化速度。让我们来解释一下这是为什么。
一般来说,一个可执行文件包含几个段,其中一个为“.reloc”段。
当链接器生成EXE或DLL时,它假定这个文件会被加载到一个特定的地址,也就是所谓的假定/首选加载/基地址(assumed/preferred load/base address)。内存映像(image)中的所有绝对地址都时基于该“链接器假定加载地址”的。如果由于某些原因,映像没有加载到这个地址,那么PE加载器(PE loader)就不得不修正该映像中的所有绝对地址。这就是“.reloc”段存在的原因:它包含了一个该映像中所有的“链接器假定地址”与真正加载到的地址之间的差异的列表(注意:编译器产生的大部分指令都使用一种相对寻址模式,所以,真正需要重定位[relocation]的地方并没有你想像的那么多)。如果,从另一方面说,加载器可以把映像加载到链接器首选地址,那么“.reloc”段就会被彻底忽略。
但是,因为每一个Win32程序都需要kernel32.dll,大部分需要user32.dll,所以如果总是把它们两个映射到其首选地址,那么加载器就不用修正kernel32.dll和user32.dll中的任何(绝对)地址,加载时间就可以缩短。
让我们用下面的例子来结束这个讨论:
把一个APP.exe的加载地址改为kernel32的(/base:"0x77e80000")或user32的(/base:"0x77e10000")首选地址。如果App.exe没有引入UESE32,就强制LoadLibrary。然后编译App.exe,并运行它。你会得到一个错误框(“非法的系统DLL重定位”),App.exe无法被加载。
为什么?当一个进程被创建时,Win2000和WinXP的加载器会检查kernel32.dll和user32.dll是否被映射到它们的首选地址(它们的名称是被硬编码进加载器的),如果没有,就会报错。在WinNT4
总结一下:在WinNT4或更高版本的操作系统中:
●总被加载到它们的首选地址的DLL有:kernel32.dll,user32.dll和ntdll.dll。
●Win32程序(连同csrss.exe)中一定存在的DLL:kernel32.dll和ntdll.dll。
●所有进程中都存在的dll:ntdll.dll。
B)
在Debug时,/GZ开关默认是打开的。它可以帮你捕捉一些错误(详细内容参考文档)。但是它对我们的可执行文件有什么影响呢?
当/GZ被使用时,编译器会在每个函数,包含函数调用中添加额外的代码(添加到每个函数的最后面)来检查ESP栈指针是否被我们的函数更改过。但是,等等,ThreadFunc中被添加了一个函数调用?这就是通往灾难的道路。因为,被复制到远程进程中的ThreadFunc将调用一个在远程进程中不存在的函数。
C)
增量连接可以缩短连接的时间,在增量编译时,每个函数调用都是通过一个额外的JMP指令来实现的(一个例外就是被声明为static的函数!)这些JMP允许连接器移动函数在内存中的位置而不用更新调用该函数的CALL。但是就是这个JMP给我们带来了麻烦:现在ThreadFunc和AfterThreadFunc将指向JMP指令而不是它们的真实代码。所以,当计算ThreadFunc的大小时:
const int cbCodeSize = ((LPBYTE) AfterThreadFunc - (LPBYTE) ThreadFunc);
实际得到的将是指向ThreadFunc和AfterThreadFunc的JMP指令之间的“距离”。现在假设我们的ThreadFunc在004014C0,和其对应的JMP指令在00401020
:00401020
:004014C0
:004014C1
然后,
WriteProcessMemory( .., &ThreadFunc, cbCodeSize, ..);
将把“JMP
然而,如果一个函数被声明为static,就算使用增量连接,也不会被替换为JMP指令。这就是为什么我在规则#4中说把ThreadFunc和AfterThreadFunc声明为static或禁止增量连接的原因了。(关于增量连接的其他方面请参看Matt Pietrek写的“Remove Fatty Deposits from Your Applications Using Our 32-bit Liposuction Tools”)
D)
void Dummy(void) {
}
会被编译为类似下面的指令:
:00401000
:00401001
:00401003
:00401006
:0040100A
:0040100F
:00401017
:00401019
:0040101A
请注意在上面的例子中ESP(栈指针)是如何被改变的。但是如果一个函数有多于4K的局部变量该怎么办?这种情况下,栈指针不会被直接改变,而是通过一个函数调用来正确实现ESP的改变。但是就是这个“函数调用”导致了ThreadFunc的崩溃,因为它在远程进程中的拷贝将会调用一个不存在的函数。
让我们来看看文档关于栈探针(stack probes)和/Gs编译选项的说明:
“/Gssize选项是一个允许你控制栈探针的高级特性。栈探针是编译器插入到每个函数调用中的一系列代码。当被激活时,栈探针将温和地按照存储函数局部变量所需要的空间大小来移动。
如果一个函数需要大于size指定的局部变量空间,它的栈探针将被激活。默认的size为一个页的大小(在80x86上为4k)。这个值可以使一个Win32程序和Windows NT的虚拟内存管理程序和谐地交互,在运行期间向程序栈增加已提交的内存总数。
我能确定你们对上面的叙述(“栈探针将温和地按照存储函数局部变量所需要的空间大小来移动”)感到奇怪。这些编译选项(他们的描述!)有时候真的让人很恼火,特别是当你想真的了解它们是怎么工作的时候。打个比方,如果一个函数需要12kb的空间来存放局部变量,栈上的内存是这样“分配”的
sub
test
sub
test
sub
test
注意栈指针是如何以4Kb为单位移动的,更重要的是每移动一步后使用test对栈底的处理(more importantly, how the bottom of the stack is "touched" after each step)。这可以确保了在“分配”下一个页之前,包含栈底的页已经被提交。
继续阅读文档的说明:
“每一个新的线程会拥有(receives)自己的栈空间,这包括已经提交的内存和保留的内存。默认情况下每个线程使用1MB的保留内存和一个页大小的以提交内存。如果有必要,系统将从保留内存中提交一个页。”(看MSDN中GreateThread > dwStackSize
..现在为什么文档中说“这个值可以使一个Win32程序和Windows NT的虚拟内存管理程序和谐地交互”也很清楚了。
E)
同样,用例子来说明会简单些:
int Dummy( int arg1 )
{
}
将会被编译为类似下面的代码:
Address
--------------------------------------------------
:00401000
:00401004
:00401006
:00401007
:0040100A
; JMP to one of the addresses in table ***
; note that ECX contains the offset
:0040100C
:00401013
:00401018
:00401019
:0040101E
:0040101F
:00401024
:00401025
:0040102A
:0040102B
;
:0040102C
:00401030
:00401034
:00401038
看到switch-case是如何实现的了吗?
它没有去测试每个case分支,而是创建了一个地址表(address table)。我们简单地计算出在地址表中偏移就可以跳到正确的case分支。想想吧,这真是一个进步,假设你有一个50个分支的switch语句,假如没有这个技巧,你不的不执行50次CMP和JMP才能到达最后一个case,而使用地址表,你可以通过一次查表即跳到正确的case。使用算法的时间复杂度来衡量:我们把O(2n)的算法替换成了O(5)的算法,其中:
1.
2.
现在,你可能认为上面的情况仅仅是因为case常量选择得比较好,(1,2,3,4,5)。幸运的是,现实生活中的大多数例子都可以应用这个方案,只是偏移的计算复杂了一点而已。但是,有两个例外:
●如果少于3个case分支,或
●如果case常量是完全相互无关的。(比如
最终的结果和你使用普通的if-else if是一样的。
有趣的地方:如果你曾经为case后面只能跟常量而迷惑的话,现在你应该知道为什么了吧。这个值必须在编译期间就确定下来,这样才能创建地址表。
回到我们的问题!
注意到0040100C处的JMP指令了吗?我们来看看Intel的文档对十六进制操作码FF的说明:
Opcode
FF /4
JMP使用了绝对地址!也就是说,它的其中一个操作数(在这里是0040102C)代表一个绝对地址。还用多说吗?现在远程的ThreadFunc会盲目第在地址表中004101C然后跳到这个错误的地方,马上使远程进程挂掉了。
F)
如果你的远程进程崩溃了,原因可能为下列之一:
1.
2.
3.
:004014C0
:004014C1
:004014C5
:00401502
如果这个有争议的CALL是编译器添加的(因为一些不该打开的编译开关比如/GZ打开了),它要么在ThreadFunc的开头要么在ThreadFunc接近结尾的地方
不管在什么情况下,你使用CreateRemoteThread & WriteProcessMemory技术时必须万分的小心,特别是编译器/连接器的设置,它们很可能会给你的ThreadFunc添加一些带来麻烦的东西。
参考(省略)