跳转至

放置跳转

无条件跳转的字节码类似于近调用,但第一个字节是 0xE9 而不是 0xE8。(需要复习请参阅第一百五十三页「处理内存中的近调用」。)在图 8-1 中,无条件跳转 JMP trampoline 替换了以下四个操作:

POP EAX
MOV AL, 1
POP EDI
POP ESI

图 8-1 跳转钩子

在这种情况下,你需要替换多个顺序操作,以适应无条件跳转的 5 字节大小。你可能会遇到被替换操作的大小大于 5 字节的情况。发生这种情况时,用 NOP 指令替换剩余字节。

现在,让我们看看如何替换这些操作。清单 8-11 展示了放置跳转钩子的代码。

DWORD hookWithJump(DWORD hookAt, DWORD newFunc, 
int size)
{
    if (size > 12) // 不应该需要替换 12+ 字节
        return 0;
  DWORD newOffset = newFunc - hookAt - 5;
    auto oldProtection =
        protectMemory<DWORD[3]>(hookAt + 1, 
PAGE_EXECUTE_READWRITE);
  writeMemory<BYTE>(hookAt, 0xE9);
  writeMemory<DWORD>(hookAt + 1, newOffset);
    for (unsigned int i = 5; i < size; i++)
        writeMemory<BYTE>(hookAt + i, 0x90);
    protectMemory<DWORD[3]>(hookAt + 1, 
oldProtection);
    return hookAt + 5;
}

清单 8-11:如何放置跳转钩子

这个函数把要钩的地址、回调函数地址和要覆盖的内存大小(以字节为单位)作为参数。首先,它计算钩子位置与蹦床之间的偏移量,把结果存储在 newOffset①中。接下来,对要更改的内存应用 PAGE_EXECUTE_READWRITE 权限。然后把无条件跳转(0xE9)②和回调函数地址③写入内存,for 循环向任何废弃字节写入 NOP 指令(0x90)。重新应用旧保护后,hookWithJump() 返回原始地址。

注意 hookWithJump() 函数在放置跳转前确保 size 不超过 12。这个检查很重要,因为跳转占用5 字节,意味着如果前四个命令都是单字节,它可以替换最多五个命令。如果前四个命令都是单字节,第五个命令需要超过 8 字节才会触发 if (size > 12) 子句。由于 9 字节操作非常非常罕见,12 是安全而灵活的极限。有这个限制可以阻止各种 bug 发生,尤其是如果你的机器人动态检测 size 参数。例如,如果机器人搞砸了,传入 500,000,000 的大小,检查会阻止你 NOP 填充整个宇宙。