跳转至

构建汇编代码洞穴

与线程注入一样,线程劫持的第一步是知道你希望代码洞穴做什么。但这一次,你不知道劫持时线程正在执行什么,所以你需要确保在代码洞穴开始时保存线程状态,并在劫持完成时恢复状态。这意味着你的 shellcode 需要包裹在汇编代码中,如清单 7-5 所示。

PUSHAD // 把通用寄存器压入栈
PUSHFD // 把 EFLAGS 压入栈
// shellcode 应该在这里
POPFD // 从栈弹出 EFLAGS
POPAD // 把通用寄存器弹出栈
// 在这里不使用寄存器恢复线程

清单 7-5:线程劫持代码洞穴的框架

如果像线程注入那样调用同一个 someFunction,你可以使用与清单 7-2 类似的 shellcode。唯一的区别是你不能通过栈向机器人传递第二个参数,因为你不会使用 CreateRemoteThread()。但这没问题;你可以像压入第一个参数一样把它压入。执行你要调用函数的代码洞穴部分需要看起来像清单 7-6。

PUSH string
PUSH times
MOV EAX, someFunction
CALL EAX
ADD ESP, 0x8

清单 7-6:调用 someFunction 的汇编骨架

这里与清单 7-1 唯一不同的是这个示例显式压入 string,而且没有 RETN。这种情况下你不调用 RETN,因为你希望游戏线程回到你劫持它之前正在做的事情。

要正常恢复线程执行,洞穴需要在不使用寄存器的情况下跳回线程的原始 EIP。幸运的是,你可以用 GetThreadContext() 函数获取 EIP,在 C++ 中填充 shellcode 骨架。然后你可以在代码洞穴内把它压入栈并执行返回。清单 7-7 展示了你的代码洞穴应该如何结束。

PUSH originalEIP
RETN

清单 7-7:间接跳转到 EIP

返回会跳转到栈顶的值,所以压入 EIP 后立即这样做就能实现。你应该用这个方法而不是跳转,因为跳转需要偏移量计算,使 shellcode 生成更复杂。如果你把清单 7-5 到 7-7 串在一起,就得到以下代码洞穴:

// 保存状态
PUSHAD // 把通用寄存器压入栈
PUSHFD // 把 EFLAGS 压入栈
// 用 shellcode 做工作
PUSH string
PUSH times
MOV EAX, someFunction
CALL EAX
ADD ESP, 0x8
// 恢复状态
POPFD // 从栈弹出 EFLAGS
POPAD // 把通用寄存器弹出栈
// 解除劫持:不使用寄存器恢复线程
PUSH originalEIP
RETN

接下来,按照第一百三十五页「把汇编翻译成 Shellcode」中的说明,把这些字节插入代表代码洞穴的数组。