此处 0xA0B0C0D0 只是远程进程地址空间中真实(绝对)INJDATA地址的占位符。前面讲过,你无法在编译时知道该地址。但你可以在调用 VirtualAllocEx (为INJDATA)之后得到 INJDATA 在远程进程中的位置。编译我们的 NewProc 后,可以得到如下结果:

01.Address OpCode/Params Decoded instruction

02.--------------------------------------------------

03.:00401000 55 push ebp

04.:00401001 8BEC mov ebp, esp

05.:00401003 C745FCD0C0B0A0 mov [ebp-04], A0B0C0D0

06.:0040100A ...

07.....

08.:0040102D 8BE5 mov esp, ebp

09.:0040102F 5D pop ebp

10.:00401030 C21000 ret 0010

因此,其编译的代码(十六进制)将是:

1.558BECC745FCD0C0B0A0......8BE55DC21000.

现在你可以象下面这样继续:

1.INJDATAThreadFuncNewProc 拷贝到目标进程;

2.修改 NewProc 的代码,以便 pData 中保存的是 INJDATA 的真实地址。

例如,假设 INJDATA 的地址(VirtualAllocEx返回的值)在目标进程中是 0x008a0000。然后象下面这样修改NewProc的代码:

1.558BECC745FCD0C0B0A0......8BE55DC21000 <- 原来的NewProc (注1

2.558BECC745FC00008A00......8BE55DC21000 <- 修改后的NewProc,使用的是INJDATA的实际地址。

也就是说,你用真正的 INJDATA(注2) 地址替代了虚拟值 A0B0C0D0(注2)。

1.开始执行远程的 ThreadFunc,它负责子类化远程进程中的控件。

1、有人可能会问,为什么地址 A0B0C0D0 008a0000 在编译时顺序是相反的。因为 Intel AMD 处理器使用 little-endian 符号来表示(多字节)数据。换句话说,某个数字的低位字节被存储在内存的最小地址处,而高位字节被存储在最高位地址。

假设“UNIX”这个词存储用4个字节,在 big-endian 系统中,它被存为“UNIX”,在 little-endian 系统中,它将被存为“XINU”。

2、某些破解(很糟)以类似的方式修改可执行代码,但是一旦加载到内存,一个程序是无法修改自己的代码的(代码驻留在可执行程序的“.text” 区域,这个区域是写保护的)。但仍可以修改远程的 NewProc,因为它是先前以 PAGE_EXECUTE_READWRITE 许可方式被拷贝到某个内存块中的。

何时使用 CreateRemoteThread WriteProcessMemory 技术

与其它方法比较,使用 CreateRemoteThread WriteProcessMemory 技术进行代码注入更灵活,这种方法不需要额外的 dll,不幸的是,该方法更复杂并且风险更大,只要ThreadFunc出现哪怕一丁点错误,很容易就让(并且最大可能地会)使远程进程崩溃(参见附录 F),因为调试远程 ThreadFunc 将是一个可怕的梦魇,只有在注入的指令数很少时,你才应该考虑使用这种技术进行注入,对于大块的代码注入,最好用 I.II 部分讨论的方法。

WinSpy 以及 InjectEx 请从这里。

结束语

到目前为止,有几个问题是我们未提及的,现总结如下:

解决方案

OS

进程

IHooks

Win9x WinNT

仅仅与 USER32.DLL (注3)链接的进程

IICreateRemoteThread & LoadLibrary

WinNT(注4

所有进程(注5, 包括系统服务(注6

IIICreateRemoteThread & WriteProcessMemory 
 

WinNT

所有进程, 包括系统服务

3:显然,你无法hook一个没有消息队列的线程,此外,SetWindowsHookEx不能与系统服务一起工作,即使它们与 USER32.DLL 进行链接;

4Win9x 中没有 CreateRemoteThread,也没有 VirtualAllocEx (实际上,在Win9x 中可以仿真,但不是本文讨论的问题了);

5:所有进程 = 所有 Win32 进程 + csrss.exe

本地应用 (smss.exe, os2ss.exe, autochk.exe 等)不使用 Win32 API,所以也不会与 kernel32.dll 链接。唯一一个例外是 csrss.exeWin32 子系统本身,它是本地应用程序,但其某些库(~winsrv.dll)需要 Win32 DLLs,包括 kernel32.dll

6:如果你想要将代码注入到系统服务中(lsass.exe, services.exe, winlogon.exe 等)或csrss.exe,在打开远程句柄(OpenProcess)之前,将你的进程优先级置为 “SeDebugPrivilege”(AdjustTokenPrivileges)。

最后,有几件事情一定要了然于心:你的注入代码很容易摧毁目标进程,尤其是注入代码本身出错的时候,所以要记住:权力带来责任!

因为本文中的许多例子是关于密码的,你也许还读过 Zhefu Zhang 写的另外一篇文章“Super Password Spy++” ,在该文中,他解释了如何获取IE 密码框中的内容,此外,他还示范了如何保护你的密码控件免受类似的***。

附录A

为什么 kernel32.dll user32.dll 总是被映射到相同的地址。

我的假定:因为Microsoft 的程序员认为这样做有助于速度优化,为什么呢?我的解释是――通常一个可执行程序是由几个部分组成,其中包括“.reloc” 。当链接器创建 EXE 或者 DLL文件时,它对文件被映射到哪个内存地址做了一个假设。这就是所谓的首选加载/基地址。在映像文件中所有绝对地址都是基于链接器首选的加载地址,如果由于某种原因,映像文件没有被加载到该地址,那么这时“.reloc”就起作用了,它包含映像文件中的所有地址的清单,这个清单中的地址反映了链接器首选加载地址和实际加载地址的差别(无论如何,要注意编译器产生的大多数指令使用某种相对地址寻址,因此,并没有你想象的那么多地址可供重新分配),另一方面,如果加载器能够按照链接器首选地址加载映像文件,那么“.reloc”就被完全忽略掉了。

kernel32.dll user32.dll 及其加载地址为何要以这种方式加载呢?因为每一个 Win32 程序都需要kernel32.dll,并且大多数Win32 程序也需要 user32.dll,那么总是将它们(kernel32.dll user32.dll)映射到首选地址可以改进所有可执行程序的加载时间。这样一来,加载器绝不能修改kernel32.dll and user32.dll.中的任何(绝对)地址。我们用下面的例子来说明:

将某个应用程序 App.exe 的映像基地址设置成 KERNEL32的地址(/base:"0x77e80000")或 USER32的首选基地址(/base:"0x77e10000"),如果 App.exe 不是从 USER32 导入方式来使用 USER32,而是通过LoadLibrary 加载,那么编译并运行App.exe 后,会报出错误信息("Illegal System DLL Relocation"――非法系统DLL地址重分配),App.exe 加载失败。

为什么会这样呢?当创建进程时,Win 2000Win XP Win 2003系统的加载器要检查 kernel32.dll user32.dll 是否被映射到首选基地址(实际上,它们的名字都被硬编码进了加载器),如果没有被加载到首选基地址,将发出错误。在 WinNT4中,也会检查ole32.dll,在WinNT 3.51 和较低版本的Windows中,由于不会做这样的检查,所以kernel32.dll user32.dll可以被加载任何地方。只有ntdll.dll总是被加载到其基地址,加载器不进行检查,一旦ntdll.dll没有在其基地址,进程就无法创建。

总之,对于 WinNT 4 和较高的版本中

一定要被加载到基地址的DLLs 有:kernel32.dlluser32.dll ntdll.dll

每个Win32 程序都要使用的 DLLs+ csrss.exekernel32.dll ntdll.dll

每个进程都要使用的DLL只有一个,即使是本地应用:ntdll.dll

附录B

/GZ 编译器开关

在生成 Debug 版本时,/GZ 编译器特性是默认打开的。你可以用它来捕获某些错误(具体细节请参考相关文档)。但对我们的可执行程序意味着什么呢?

当打开 /GZ 开关,编译器会添加一些额外的代码到可执行程序中每个函数所在的地方,包括一个函数调用(被加到每个函数的最后)――检查已经被我们的函数修改的 ESP堆栈指针。什么!难道有一个函数调用被添加到 ThreadFunc 吗?那将导致灾难。ThreadFunc 的远程拷贝将调用一个在远程进程中不存在的函数(至少是在相同的地址空间中不存在)

附录C

静态函数和增量链接

增量链接主要作用是在生成应用程序时缩短链接时间。常规链接和增量链接的可执行程序之间的差别是――增量链接时,每个函数调用经由一个额外的JMP指令,该指令由链接器发出(该规则的一个例外是函数声明为静态)。这些 JMP 指令允许链接器在内存中移动函数,这种移动无需修改引用函数的 CALL指令。但这些JMP指令也确实导致了一些问题:如 ThreadFunc AfterThreadFunc 将指向JMP指令而不是实际的代码。所以当计算ThreadFunc 的大小时:

1.const int cbCodeSize = ((LPBYTE) AfterThreadFunc - (LPBYTE) ThreadFunc)

你实际上计算的是指向 ThreadFunc JMPs AfterThreadFunc之间的“距离” (通常它们会紧挨着,不用考虑距离问题)。现在假设 ThreadFunc 的地址位于004014C0 而伴随的 JMP指令位于 00401020

1.:00401020 jmp 004014C0

2. ...

3.:004014C0 push EBP ; ThreadFunc 的实际地址

4.:004014C1 mov EBP, ESP

5. ...

那么

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

将拷贝“JMP 004014C0”指令(以及随后cbCodeSize范围内的所有指令)到远程进程――不是实际的 ThreadFunc。远程进程要执行的第一件事情将是“JMP 004014C0” 。它将会在其最后几条指令当中――远程进程和所有进程均如此。但 JMP指令的这个“规则”也有例外。如果某个函数被声明为静态的,它将会被直接调用,即使增量链接也是如此。这就是为什么规则#4要将 ThreadFunc AfterThreadFunc 声明为静态或禁用增量链接的缘故。(有关增量链接的其它信息参见 Matt Pietrek的文章“Remove Fatty Deposits from Your Applications Using Our 32-bit Liposuction Tools” )

附录D

为什么 ThreadFunc的局部变量只有 4k

局部变量总是存储在堆栈中,如果某个函数有256个字节的局部变量,当进入该函数时,堆栈指针就减少256个字节(更精确地说,在函数开始处)。例如,下面这个函数:

1.void Dummy(void) {

2. BYTE var[256];

3. var[0] = 0;

4. var[1] = 1;

5. var[255] = 255;

6.}

编译后的汇编如下:

01.:00401000 push ebp

02.:00401001 mov ebp, esp

03.:00401003 sub esp, 00000100 ; change ESP as storage for

04. ; local variables is needed

05.:00401006 mov byte ptr [esp], 00 ; var[0] = 0;

06.:0040100A mov byte ptr [esp+01], 01 ; var[1] = 1;

07.:0040100F mov byte ptr [esp+FF], FF ; var[255] = 255;

08.:00401017 mov esp, ebp ; restore stack pointer

09.:00401019 pop ebp

10.:0040101A ret

注意上述例子中,堆栈指针是如何被修改的?而如果某个函数需要4KB以上局部变量内存空间又会怎么样呢?其实,堆栈指针并不是被直接修改,而是通过另一个函数调用来修改的。就是这个额外的函数调用使得我们的 ThreadFunc “被破坏”了,因为其远程拷贝会调用一个不存在的东西。

我们看看文档中对堆栈探测和 /Gs编译器选项是怎么说的:

――“/GS是一个控制堆栈探测的高级特性,堆栈探测是一系列编译器插入到每个函数调用的代码。当函数被激活时,堆栈探测需要的内存空间来存储相关函数的局部变量。

如果函数需要的空间大于为局部变量分配的堆栈空间,其堆栈探测被激活。默认的大小是一个页面(在80x86处理器上4kb)。这个值允许在Win32 应用程序和Windows NT虚拟内存管理器之间进行谨慎调整以便增加运行时承诺给程序堆栈的内存。”

我确信有人会问:文档中的“……堆栈探测到一块需要的内存空间来存储相关函数的局部变量……”那些编译器选项(它们的描述)在你完全弄明白之前有时真的让人气愤。例如,如果某个函数需要12KB的局部变量存储空间,堆栈内存将进行如下方式的分配(更精确地说是“承诺” )。

1.sub esp, 0x1000 ; "分配" 第一次 4 Kb

2.test [esp], eax ; 承诺一个新页内存(如果还没有承诺)

3.sub esp, 0x1000 ; "分配" 第二次4 Kb

4.test [esp], eax ; ...

5.sub esp, 0x1000

6.test [esp], eax

注意4KB堆栈指针是如何被修改的,更重要的是,每一步之后堆栈底是如何被“触及”(要经过检查)。这样保证在“分配”(承诺)另一页面之前,当前页面承诺的范围也包含堆栈底。

注意事项

“每一个线程到达其自己的堆栈空间,默认情况下,此空间由承诺的以及预留的内存组成,每个线程使用 1 MB预留的内存,以及一页承诺的内存,系统将根据需要从预留的堆栈内存中承诺一页内存区域” (参见 MSDN CreateThread > dwStackSize > Thread Stack Size

还应该清楚为什么有关 /GS 的文档说在堆栈探针在 Win32 应用程序和Windows NT虚拟内存管理器之间进行谨慎调整。

现在回到我们的ThreadFunc以及 4KB 限制

虽然你可以用 /Gs 防止调用堆栈探测例程,但在文档对于这样的做法给出了警告,此外,文件描述可以用 #pragma check_stack 指令关闭或打开堆栈探测。但是这个指令好像一点作用都没有(要么这个文档是垃圾,要么我疏忽了其它一些信息?)。总之,CreateRemoteThread WriteProcessMemory 技术只能用于注入小块代码,所以你的局部变量应该尽量少耗费一些内存字节,最好不要超过 4KB限制。