魔兽首页最新地图防守地图对抗地图角色剧情TD塔类图标准战役ORPG地图其它地图
魔兽论坛攻略秘籍补丁工具RPG 录像制图教学制图资源魔兽战队新图试玩通魔作坊

您的位置:魔兽争霸U9网 >> 制图教学

向其他进程注入代码的三种方法

作者:onlyxuyang    文章来源:本站原创    点击数:    更新时间:2006-10-10 21:06:52


CODE:
:00401020   jmp  004014C0
...
:004014C0   push EBP          ; ThreadFunc的真实地址
:004014C1   mov  EBP, ESP
...

然后,
WriteProcessMemory( .., &ThreadFunc, cbCodeSize, ..);
将把“JMP 004014C0”和其后的cbCodeSize范围内的代码而不是ThreadFunc复制到远程进程。远程线程首先会执行“JMP 004010C0”,然后一直执行到这个进程代码的最后一条指令(译者注:这当然不是我们想要的结果)。

然而,如果一个函数被声明为static,就算使用增量连接,也不会被替换为JMP指令。这就是为什么我在规则#4中说把ThreadFunc和AfterThreadFunc声明为static或禁止增量连接的原因了。(关于增量连接的其他方面请参看Matt Pietrek写的“Remove Fatty Deposits from Your Applications Using Our 32-bit Liposuction Tools”)

D) 为什么ThreadFunc只能有4K的局部变量?
局部变量总是保存在栈上的。假设一个函数有256字节的局部变量,当进入该函数时(更确切地说是在functions prologue中),栈指针会被减去256。像下面的函数:


void Dummy(void) {
    BYTE var[256];
    var[0] = 0;
    var[1] = 1;
    var[255] = 255;
}

会被编译为类似下面的指令:


:00401000   push ebp
:00401001   mov  ebp, esp
:00401003   sub  esp, 00000100           ; change ESP as storage for
                                         ; local variables is needed
:00401006   mov  byte ptr [esp], 00      ; var[0] = 0;
:0040100A   mov  byte ptr [esp+01], 01   ; var[1] = 1;
:0040100F   mov  byte ptr [esp+FF], FF   ; var[255] = 255;
:00401017   mov  esp, ebp                ; restore stack pointer
:00401019   pop  ebp
:0040101A   ret


请注意在上面的例子中ESP(栈指针)是如何被改变的。但是如果一个函数有多于4K的局部变量该怎么办?这种情况下,栈指针不会被直接改变,而是通过一个函数调用来正确实现ESP的改变。但是就是这个“函数调用”导致了ThreadFunc的崩溃,因为它在远程进程中的拷贝将会调用一个不存在的函数。

让我们来看看文档关于栈探针(stack probes)和/Gs编译选项的说明:
“/Gssize选项是一个允许你控制栈探针的高级特性。栈探针是编译器插入到每个函数调用中的一系列代码。当被激活时,栈探针将温和地按照存储函数局部变量所需要的空间大小来移动

如果一个函数需要大于size指定的局部变量空间,它的栈探针将被激活。默认的size为一个页的大小(在80x86上为4k)。这个值可以使一个Win32程序和Windows NT的虚拟内存管理程序和谐地交互,在运行期间向程序栈增加已提交的内存总数。

我能确定你们对上面的叙述(“栈探针将温和地按照存储函数局部变量所需要的空间大小来移动”)感到奇怪。这些编译选项(他们的描述!)有时候真的让人很恼火,特别是当你想真的了解它们是怎么工作的时候。打个比方,如果一个函数需要12kb的空间来存放局部变量,栈上的内存是这样“分配”的


sub    esp, 0x1000    ; 先“分配”4 Kb
test  [esp], eax      ; touches memory in order to commit a
                      ; new page (if not already committed)
sub    esp, 0x1000    ; “分配”第二个 4 Kb
test  [esp], eax      ; ...
sub    esp, 0x1000
test  [esp], eax


注意栈指针是如何以4Kb为单位移动的,更重要的是每移动一步后使用test对栈底的处理(more importantly, how the bottom of the stack is "touched" after each step)。这可以确保了在“分配”下一个页之前,包含栈底的页已经被提交。

继续阅读文档的说明:
“每一个新的线程会拥有(receives)自己的栈空间,这包括已经提交的内存和保留的内存。默认情况下每个线程使用1MB的保留内存和一个页大小的以提交内存。如果有必要,系统将从保留内存中提交一个页。”(看MSDN中GreateThread > dwStackSize  > “Thread Stack Size”)

..现在为什么文档中说“这个值可以使一个Win32程序和Windows NT的虚拟内存管理程序和谐地交互”也很清楚了。

E) 为什么我要把多于3个case分支的swith分割开来呢?
同样,用例子来说明会简单些:


int Dummy( int arg1 )
{
    int ret =0;

    switch( arg1 ) {
    case 1: ret = 1; break;
    case 2: ret = 2; break;
    case 3: ret = 3; break;
    case 4: ret = 0xA0B0; break;
    }
    return ret;
}

将会被编译为类似下面的代码:


Address   OpCode/Params    Decoded instruction
--------------------------------------------------
                                             ; arg1 -> ECX
:00401000  8B4C2404         mov ecx, dword ptr [esp+04]
:00401004  33C0             xor eax, eax     ; EAX = 0
:00401006  49               dec ecx          ; ECX --
:00401007  83F903           cmp ecx, 00000003
:0040100A  771E             ja 0040102A

; JMP to one of the addresses in table ***
; note that ECX contains the offset
:0040100C  FF248D2C104000   jmp dword ptr [4*ecx+0040102C]

:00401013  B801000000       mov eax, 00000001   ; case 1: eax = 1;
:00401018  C3                ret
:00401019  B802000000       mov eax, 00000002   ; case 2: eax = 2;
:0040101E  C3                ret
:0040101F  B803000000       mov eax, 00000003   ; case 3: eax = 3;
:00401024  C3                ret
:00401025  B8B0A00000       mov eax, 0000A0B0   ; case 4: eax = 0xA0B0;
:0040102A  C3                ret
:0040102B  90                nop

; 地址表 ***
:0040102C  13104000         DWORD 00401013   ; jump to case 1
:00401030  19104000         DWORD 00401019   ; jump to case 2
:00401034  1F104000         DWORD 0040101F   ; jump to case 3
:00401038  25104000         DWORD 00401025   ; jump to case 4


看到switch-case是如何实现的了吗?
它没有去测试每个case分支,而是创建了一个地址表(address table)。我们简单地计算出在地址表中偏移就可以跳到正确的case分支。想想吧,这真是一个进步,假设你有一个50个分支的switch语句,假如没有这个技巧,你不的不执行50次CMP和JMP才能到达最后一个case,而使用地址表,你可以通过一次查表即跳到正确的case。使用算法的时间复杂度来衡量:我们把O(2n)的算法替换成了O(5)的算法,其中:
1. O代表最坏情况下的时间复杂度。
2. 我们假设计算偏移(即查表)并跳到正确的地址需要5个指令。

现在,你可能认为上面的情况仅仅是因为case常量选择得比较好,(1,2,3,4,5)。幸运的是,现实生活中的大多数例子都可以应用这个方案,只是偏移的计算复杂了一点而已。但是,有两个例外:
●如果少于3个case分支,或
●如果case常量是完全相互无关的。(比如 1, 13, 50, 1000)。
最终的结果和你使用普通的if-else if是一样的。

有趣的地方:如果你曾经为case后面只能跟常量而迷惑的话,现在你应该知道为什么了吧。这个值必须在编译期间就确定下来,这样才能创建地址表。

回到我们的问题!
注意到0040100C处的JMP指令了吗?我们来看看Intel的文档对十六进制操作码FF的说明:
Opcode   Instruction  Description
FF /4    JMP r/m32   Jump near, absolute indirect, address given in r/m32

JMP使用了绝对地址!也就是说,它的其中一个操作数(在这里是0040102C)代表一个绝对地址。还用多说吗?现在远程的ThreadFunc会盲目第在地址表中004101C然后跳到这个错误的地方,马上使远程进程挂掉了。

F) 到底是什么原因使远程进程崩溃了?
如果你的远程进程崩溃了,原因可能为下列之一:
1. 你引用了ThreadFunc中一个不存在的字符串。
2. ThreadFunc中一个或多个指令使用了绝对寻址(看附录E中的例子)
3. ThreadFunc调用了一个不存在的函数(这个函数调用可能是编译器或连接器添加的)。这时候你需要在反汇编器中寻找类似下面的代码:


:004014C0    push EBP         ; entry point of ThreadFunc
:004014C1    mov EBP, ESP
...
:004014C5    call 0041550     ; 在这里崩溃了
                              ; remote process
...
:00401502    ret

如果这个有争议的CALL是编译器添加的(因为一些不该打开的编译开关比如/GZ打开了),它要么在ThreadFunc的开头要么在ThreadFunc接近结尾的地方

不管在什么情况下,你使用CreateRemoteThread & WriteProcessMemory技术时必须万分的小心,特别是编译器/连接器的设置,它们很可能会给你的ThreadFunc添加一些带来麻烦的东西。
 

[1] [2] [3] [4] 上一页


责任编辑:瞳瞳
进入论坛参与针对本文章的讨论
文中部分附件请进入论坛下载
本文章地址:
  • 上一篇:通魔之路(WE主讨论区):[系统]Local Handle GUI v1.03-汉化增强版WE
  • 下一篇:
  • 相关文章

    进入论坛参与针对本文章的讨论用户评论

    地图 攻略
    热门搜索: 仙之侠道 真三国无双 Dota Allstar

    地图专区导航

    热门地图攻略

    热门地图周排行

    最新推荐地图

    地图下载总排行

    关于我们  -  联系我们  -  广告优势  -  广告服务
    建议意见:玩家点击留言  商务合作:客户点击留言
    西安优久数码科技有限公司 版权所有 陕ICP证08000654号