⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 进程注入的三种方法 - benny5609的专栏 - csdnblog.htm

📁 很好的收集,看了以后都不知道说什么了. 都是关于内存调试方面的.十分有用.
💻 HTM
📖 第 1 页 / 共 5 页
字号:
instruction-pointer])。在子程序的最后RET指令自动把这个值从栈中弹出到EIP。<BR>  <BR>   
现在我们知道了如何通过CALL和RET来修改EIP的值了,但是如何得到他的当前值?<BR>  还记得CALL把EIP的值压栈了吗?所以为了得到EIP的值我们调用了一个“假(dummy)函数”然后弹出栈顶值。看一下编译过的NewProc:<BR>  <BR>  Address 
OpCode/Params Decoded 
instruction<BR>  --------------------------------------------------<BR>  :00401000 
55 push ebp ; entry point of<BR>   ; NewProc<BR>  :00401001 8BEC mov ebp, 
esp<BR>  :00401003 51 push ecx<BR>  :00401004 E800000000 call 00401009 ; *a* 
call dummy<BR>  :00401009 59 pop ecx ; *b*<BR>  :0040100A 83E909 sub ecx, 
00000009 ; *c*<BR>  :0040100D 894DFC mov [ebp-04], ecx ; mov pData, 
ECX<BR>  :00401010 8B45FC mov eax, [ebp-04]<BR>  :00401013 83E814 sub eax, 
00000014 ; pData--;<BR>  .....<BR>  .....<BR>  :0040102D 8BE5 mov esp, 
ebp<BR>  :0040102F 5D pop ebp<BR>  :00401030 C21000 ret 0010<BR>  <BR>   a. 
一个假的函数调用;仅仅跳到下一条指令并且(译者注:更重要的是)把EIP压栈。<BR>   b. 
弹出栈顶值到ECX。ECX就保存的EIP的值;这也就是那条“pop ECX”指令的地址。<BR>   c. 注意从NewProc的入口点到“pop 
ECX”指令的“距离”为9字节;因此把ECX减去9就得到的NewProc的地址了。<BR>  <BR>   
这样一来,不管被复制到什么地方,NewProc总能正确计算自身的地址了!然而,要注意从NewProc的入口点到“pop 
ECX”的距离可能会因为你的编译器/链接选项的不同而不同,而且在Release和Degub版本中也是不一样的。但是,不管怎样,你仍然可以在编译期知道这个距离的具体值。<BR>   
1. 首先,编译你的函数。<BR>   2. 在反汇编器(disassembler)中查出正确的距离值。<BR>   3. 
最后,使用正确的距离值重新编译你的程序。<BR>  <BR>   
这也是InjectEx中使用的解决方案。InjectEx和HookInjEx类似,交换开始按钮上的鼠标左右键点击事件。<BR>  <BR>  <BR>  解决方案2<BR>  <BR>   
在远程进程中把INJDATA放在NewProc的前面并不是唯一的解决方案。看一下下面的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 = 0xA0B0C0D0; // 
一个假值<BR>  <BR>   //-----------------------------<BR>   // 子类代码<BR>   // 
........<BR>   //-----------------------------<BR>  <BR>   // 调用以前的窗口过程<BR>   
return pData-&gt;fnCallWindowProc( pData-&gt;fnOldProc, <BR>   
hwnd,uMsg,wParam,lParam );<BR>  }<BR>  <BR>   
这里,0XA0B0C0D0仅仅是INJDATA在远程进程中的地址的占位符(placeholder)。你无法在编译期得到这个值,然而你在调用VirtualAllocEx(为INJDATA分配内存时)后确实知道INJDATA的地址!(译者注:就是VirtualAllocEx的返回值)<BR>  <BR>   
我们的NewProc编译后大概是这个样子:<BR>  Address OpCode/Params Decoded 
instruction<BR>  --------------------------------------------------<BR>  :00401000 
55 push ebp<BR>  :00401001 8BEC mov ebp, esp<BR>  :00401003 C745FCD0C0B0A0 mov 
[ebp-04], A0B0C0D0<BR>  :0040100A ...<BR>  ....<BR>  ....<BR>  :0040102D 8BE5 
mov esp, ebp<BR>  :0040102F 5D pop ebp<BR>  :00401030 C21000 ret 
0010<BR>  <BR>   编译后的机器码应该为:558BECC745FCD0C0B0A0......8BE55DC21000。<BR>  <BR>   
现在,你这么做:<BR>   1. 把INJDATA,ThreadFunc和NewFunc复制到目的进程。<BR>   2. 
改变NewPoc的机器码,让pData指向INJDATA的真实地址。<BR>   
比如,假设INJDATA的的真实地址(VirtualAllocEx的返回值)为0x008a0000,你把NewProc的机器码改为:<BR>   
<BR>  558BECC745FCD0C0B0A0......8BE55DC21000 &lt;- 修改前的 NewProc 1 
<BR>  558BECC745FC00008A00......8BE55DC21000 &lt;- 修改后的 NewProc <BR>  <BR>   
也就是说,你把假值 A0B0C0D0改为INJDATA的真实地址2<BR>   3. 
开始指向远程的ThreadFunc,它子类了远程进程中的控件。<BR>  <BR>   &amp;sup1; 
你可能会问,为什么A0B0C0D0和008a0000在编译后的机器码中为逆序的。这时因为Intel和AMD处理器使用littl-endian标记法(little-endian 
notation)来表示它们的(多字节)数据。换句话说:一个数的低字节(low-order byte)在内存中被存放在最低位,高字节(high-order 
byte)存放在最高位。<BR>  想像一下,存放在四个字节中的单词“UNIX”,在big-endia系统中被存储为“UNIX”,在little-endian系统中被存储为“XINU”。<BR>  <BR>   
&amp;sup2; 
一些蹩脚的破解者用类似的方法来修改可执行文件的机器码,但是一个程序一旦载入内存,就不能再更改自身的机器码(一个可执行文件的.text段是写保护的)。我们能修改远程进程中的NewProc是因为它所处的那块内存在分配时给予了PAGE_EXECUTE_READWRITE属性。<BR>  <BR>   
何时使用CreateRemoteThread和WriteProcessMemory技术<BR>  <BR>   
通过CreateRemoteThread和WriteProcessMemory来注入代码的技术,和其他两种方法相比,不需要一个额外的DLL文件,因此更灵活,但也更复杂更危险。一旦你的ThreadFunc中有错误,远程线程会立即崩溃(看附录F)。调试一个远程的ThreadFunc也是场恶梦,所以你应该在仅仅注入若干条指令时才使用这个方法。要注入大量的代码还是使用另外两种方法吧。<BR>  <BR>   
再说一次,你可以在文章的开头部分下载到WinSpy,InjectEx和它们的源代码。<BR>  <BR>  <BR>   写在最后的话<BR>  <BR>   
最后,我们总结一些目前还没有提到的东西:<BR>   <BR>   方法 适用的操作系统 可操作的进程进程 <BR>   I. Windows钩子 Win9x 
和WinNT 仅限链接了USER32.DLL的进程1 <BR>   II. CreateRemoteThread &amp; LoadLibrary 
仅WinNT2 所有进程3,包括系统服务4 <BR>   III. CreateRemoteThread &amp; WriteProcessMemory 
近WinNT 所有进程,包括系统服务 <BR>  <BR>   1. 
很明显,你不能给一个没有消息队列的线程挂钩。同样SetWindowsHookEx也对系统服务不起作用(就算它们连接了USER32)。<BR>   2. 
在Win9x下没有CreateRemoteThread和VirtualAllocEx(事实上可以在9x上模拟它们,但是到目前为止还只是个神话)<BR>   3. 
所有进程 = 所有的Win32进程 + csrss.exe<BR>   本地程序(native application)比如smss.exe, 
os2ss.exe, autochk.exe,不使用Win32 
APIs,也没有连接到kernel32.dll。唯一的例外是csrss.exe,win32子系统自身。它是一个本地程序,但是它的一些库(比如winsrv.dll)需要Win32 
DLL包括kernel32.dll.<BR>   
4.如果你向注入代码到系统服务或csrss.exe,在打开远程进程的句柄(OpenProcess)之前把你的进程的优先级调整为“SeDebugprovilege”(AdjustTokenPrivileges)。<BR>  <BR>  <BR>   
大概就这些了吧。还有一点你需要牢记在心:你注入的代码(特别是存在错误时)很容易就会把目的进程拖垮。记住:责任随权利而来(Power comes with 
responsibility)!<BR>  <BR>   这篇文章中的很多例子都和密码有关,看过这篇文章后你可能也会对Zhefu 
Zhang(译者注:大概是一位中国人,张哲夫??)写的Supper Password 
Spy++感兴趣。他讲解了如何从IE的密码框中得到密码,也说了如何保护你的密码不被这种攻击。<BR>  <BR>   
最后一点:读者的反馈是文章作者的唯一报酬,所以如果你认为这篇文章有作用,请留下你的评论或给它投票。更重要的是,如果你发现有错误或bug;或你认为什么地方做得还不够好,有需要改进的地方;或有不清楚的地方也都请告诉我。<BR>  <BR>  感谢<BR>   
首先,我要感谢我在CodeGuru(这篇文章最早是在那儿发表的)的读者,正是由于你们的鼓励和支持这篇文章才得以从最初的1200单词发展到今天这样6000单词的“庞然大物”。如果说有一个人我要特别感谢的话,他就是Rado 
Picha。这篇文章的一部分很大程度上得益于他对我的建议和帮助。最后,但也不能算是最后,感谢Susan 
Moore,他帮助我跨越了那个叫做“英语”的雷区,让这篇文章更加通顺达意。<BR>  ――――――――――――――――――――――――――――――――――――<BR>  附录<BR>  A) 
为什么kernel32.dll和user32.dll中是被映射到相同的内存地址?<BR>  我的假定:以为微软的程序员认为这么做可以优化速度。让我们来解释一下这是为什么。<BR>  一般来说,一个可执行文件包含几个段,其中一个为“.reloc”段。<BR>  <BR>  当链接器生成EXE或DLL时,它假定这个文件会被加载到一个特定的地址,也就是所谓的假定/首选加载/基地址(assumed/preferred 
load/base 
address)。内存映像(image)中的所有绝对地址都时基于该“链接器假定加载地址”的。如果由于某些原因,映像没有加载到这个地址,那么PE加载器(PE 
loader)就不得不修正该映像中的所有绝对地址。这就是“.reloc”段存在的原因:它包含了一个该映像中所有的“链接器假定地址”与真正加载到的地址之间的差异的列表(注意:编译器产生的大部分指令都使用一种相对寻址模式,所以,真正需要重定位[relocation]的地方并没有你想像的那么多)。如果,从另一方面说,加载器可以把映像加载到链接器首选地址,那么“.reloc”段就会被彻底忽略。<BR>  <BR>  但是,因为每一个Win32程序都需要kernel32.dll,大部分需要user32.dll,所以如果总是把它们两个映射到其首选地址,那么加载器就不用修正kernel32.dll和user32.dll中的任何(绝对)地址,加载时间就可以缩短。<BR>  <BR>  让我们用下面的例子来结束这个讨论:<BR>  把一个APP.exe的加载地址改为kernel32的(/base:"0x77e80000")或user32的(/base:"0x77e10000")首选地址。如果App.exe没有引入UESE32,就强制LoadLibrary。然后编译App.exe,并运行它。你会得到一个错误框(“非法的系统DLL重定位”),App.exe无法被加载。<BR>  <BR>  为什么?当一个进程被创建时,Win2000和WinXP的加载器会检查kernel32.dll和user32.dll是否被映射到它们的首选地址(它们的名称是被硬编码进加载器的),如果没有,就会报错。在WinNT4 
中ole32.dll也会被检查。在WinNT3.51或更低版本中,则不会有任何检查,kernel32.dll和user32.dll可以被加载到任何地方。唯一一个总是被加载到首选地址的模块是ntdll.dll,加载器并不检查它,但是如果它不在它的首选地址,进程根本无法创建。<BR>  <BR>  总结一下:在WinNT4或更高版本的操作系统中:<BR>  ●总被加载到它们的首选地址的DLL有:kernel32.dll,user32.dll和ntdll.dll。<BR>  ●Win32程序(连同csrss.exe)中一定存在的DLL:kernel32.dll和ntdll.dll。<BR>  ●所有进程中都存在的dll:ntdll.dll。<BR>  <BR>  B) 
/GZ编译开关<BR>  在Debug时,/GZ开关默认是打开的。它可以帮你捕捉一些错误(详细内容参考文档)。但是它对我们的可执行文件有什么影响呢?<BR>  <BR>  当/GZ被使用时,编译器会在每个函数,包含函数调用中添加额外的代码(添加到每个函数的最后面)来检查ESP栈指针是否被我们的函数更改过。但是,等等,ThreadFunc中被添加了一个函数调用?这就是通往灾难的道路。因为,被复制到远程进程中的ThreadFunc将调用一个在远程进程中不存在的函数。<BR>  <BR>  C) 
static函数和增量连接(Incremental 
linking)<BR>  增量连接可以缩短连接的时间,在增量编译时,每个函数调用都是通过一个额外的JMP指令来实现的(一个例外就是被声明为static的函数!)这些JMP允许连接器移动函数在内存中的位置而不用更新调用该函数的CALL。但是就是这个JMP给我们带来了麻烦:现在ThreadFunc和AfterThreadFunc将指向JMP指令而不是它们的真实代码。所以,当计算ThreadFunc的大小时:<BR>  const 
int cbCodeSize = ((LPBYTE) AfterThreadFunc - (LPBYTE) 
ThreadFunc);<BR>  你实际得到的将是指向ThreadFunc和AfterThreadFunc的JMP指令之间的“距离”。现在假设我们的ThreadFunc在004014C0,和其对应的JMP指令在00401020<BR>  :00401020 
jmp 004014C0<BR>   ...<BR>  :004014C0 push EBP ; ThreadFunc的真实地址<BR>  :004014C1 
mov EBP, ESP<BR>   ...<BR>  然后,<BR>  WriteProcessMemory( .., &amp;ThreadFunc, 
cbCodeSize, ..);<BR>  将把“JMP 
004014C0”和其后的cbCodeSize范围内的代码而不是ThreadFunc复制到远程进程。远程线程首先会执行“JMP 
004010C0”,然后一直执行到这个进程代码的最后一条指令(译者注:这当然不是我们想要的结果)。<BR>  <BR>  然而,如果一个函数被声明为static,就算使用增量连接,也不会被替换为JMP指令。这就是为什么我在规则#4中说把ThreadFunc和AfterThreadFunc声明为static或禁止增量连接的原因了。(关于增量连接的其他方面请参看Matt 
Pietrek写的“Remove Fatty Deposits from Your Applications Using Our 32-bit 
Liposuction Tools”)<BR>  <BR>  D) 
为什么ThreadFunc只能有4K的局部变量?<BR>  局部变量总是保存在栈上的。假设一个函数有256字节的局部变量,当进入该函数时(更确切地说是在functions 
prologue中),栈指针会被减去256。像下面的函数:<BR>  void Dummy(void) {<BR>   BYTE var[256];<BR>   
var[0] = 0;<BR>   var[1] = 1;<BR>   var[255] = 
255;<BR>  }<BR>  会被编译为类似下面的指令:<BR>  :00401000 push ebp<BR>  :00401001 mov ebp, 
esp<BR>  :00401003 sub esp, 00000100 ; change ESP as storage for<BR>   ; local 
variables is needed<BR>  :00401006 mov byte ptr [esp], 00 ; var[0] = 
0;<BR>  :0040100A mov byte ptr [esp+01], 01 ; var[1] = 1;<BR>  :0040100F mov 
byte ptr [esp+FF], FF ; var[255] = 255;<BR>  :00401017 mov esp, ebp ; restore 
stack pointer<BR>  :00401019 pop ebp<BR>  :0040101A 
ret<BR>  <BR>  请注意在上面的例子中ESP(栈指针)是如何被改变的。但是如果一个函数有多于4K的局部变量该怎么办?这种情况下,栈指针不会被直接改变,而是通过一个函数调用来正确实现ESP的改变。但是就是这个“函数调用”导致了ThreadFunc的崩溃,因为它在远程进程中的拷贝将会调用一个不存在的函数。<BR>  <BR>  让我们来看看文档关于栈探针(stack 
probes)和/Gs编译选项的说明:<BR>  “/Gssize选项是一个允许你控制栈探针的高级特性。栈探针是编译器插入到每个函数调用中的一系列代码。当被激活时,栈探针将温和地按照存储函数局部变量所需要的空间大小来移动<BR>  <BR>  如果一个函数需要大于size指定的局部变量空间,它的栈探针将被激活。默认的size为一个页的大小(在80x86上为4k)。这个值可以使一个Win32程序和Windows 
NT的虚拟内存管理程序和谐地交互,在运行期间向程序栈增加已提交的内存总数。<BR>  <BR>  我能确定你们对上面的叙述(“栈探针将温和地按照存储函数局部变量所需要的空间大小来移动”)感到奇怪。这些编译选项(他们的描述!)有时候真的让人很恼火,特别是当你想真的了解它们是怎么工作的时候。打个比方,如果一个函数需要12kb的空间来存放局部变量,栈上的内存是这样“分配”的<BR>  sub 
esp, 0x1000 ; 先“分配”4 Kb<BR>  test [esp], eax ; touches memory in order to commit 
a<BR>   ; new page (if not already committed)<BR>  sub esp, 0x1000 ; “分配”第二个 4 
Kb<BR>  test [esp], eax ; ...<BR>  sub esp, 0x1000<BR>  test [esp], 
eax<BR>  <BR>  注意栈指针是如何以4Kb为单位移动的,更重要的是每移动一步后使用test对栈底的处理(more importantly, how 
the bottom of the stack is "touched" after each 
step)。这可以确保了在“分配”下一个页之前,包含栈底的页已经被提交。<BR>  <BR>  继续阅读文档的说明:<BR>  “每一个新的线程会拥有(receives)自己的栈空间,这包括已经提交的内存和保留的内存。默认情况下每个线程使用1MB的保留内存和一个页大小的以提交内存。如果有必要,系统将从保留内存中提交一个页。”(看MSDN中GreateThread 
&gt; dwStackSize &gt; “Thread Stack 
Size”)<BR>  <BR>  ..现在为什么文档中说“这个值可以使一个Win32程序和Windows 
NT的虚拟内存管理程序和谐地交互”也很清楚了。<BR>  <BR>  E) 
为什么我要把多于3个case分支的swith分割开来呢?<BR>  同样,用例子来说明会简单些:<BR>  int Dummy( int arg1 ) 
<BR>  {<BR>   int ret =0;<BR>  <BR>   switch( arg1 ) {<BR>   case 1: ret = 1; 
break;<BR>   case 2: ret = 2; break;<BR>   case 3: ret = 3; break;<BR>   case 4: 
ret = 0xA0B0; break;<BR>   }<BR>   return 
ret;<BR>  }<BR>  将会被编译为类似下面的代码:<BR>  Address OpCode/Params Decoded 
instruction<BR>  --------------------------------------------------<BR>   ; arg1 
-&gt; ECX<BR>  :00401000 8B4C2404 mov ecx, dword ptr [esp+04]<BR>  :00401004 
33C0 xor eax, eax ; EAX = 0<BR>  :00401006 49 dec ecx ; ECX --<BR>  :00401007 
83F903 cmp ecx, 00000003<BR>  :0040100A 771E ja 0040102A<BR>  <BR>  ; JMP to one 
of the addresses in table ***<BR>  ; note that ECX contains the 
offset<BR>  :0040100C FF248D2C104000 jmp dword ptr 
[4*ecx+0040102C]<BR>  <BR>  :00401013 B801000000 mov eax, 00000001 ; case 1: eax 
= 1;<BR>  :00401018 C3 ret<BR>  :00401019 B802000000 mov eax, 00000002 ; case 2: 
eax = 2;<BR>  :0040101E C3 ret<BR>  :0040101F B803000000 mov eax, 00000003 ; 
case 3: eax = 3;<BR>  :00401024 C3 ret<BR>  :00401025 B8B0A00000 mov eax, 
0000A0B0 ; case 4: eax = 0xA0B0;<BR>  :0040102A C3 ret<BR>  :0040102B 90 
nop<BR>  <BR>  ; 地址表 ***<BR>  :0040102C 13104000 DWORD 00401013 ; jump to case 
1<BR>  :00401030 19104000 DWORD 00401019 ; jump to case 2<BR>  :00401034 
1F104000 DWORD 0040101F ; jump to case 3<BR>  :00401038 25104000 DWORD 00401025 
; jump to case 
4<BR>  <BR>  看到switch-case是如何实现的了吗?<BR>  它没有去测试每个case分支,而是创建了一个地址表(address 
table)。我们简单地计算出在地址表中偏移就可以跳到正确的case分支。想想吧,这真是一个进步,假设你有一个50个分支的switch语句,假如没有这个技巧,你不的不执行50次CMP和JMP才能到达最后一个case,而使用地址表,你可以通过一次查表即跳到正确的case。使用算法的时间复杂度来衡量:我们把O(2n)的算法替换成了O(5)的算法,其中:<BR>  1. 
O代表最坏情况下的时间复杂度。<BR>  2. 

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -