[2023]经典永不过时-返回地址欺骗 huoji 汇编,堆栈,红队 2023-02-04 769 次浏览 0 次点赞 在一些攻防场景中,经常会遇到函数调用者检查. 即检查调用者的地址,诺地址不在自己的模块里面,则可以判定为非法调用. 为了对抗这种检查,一种非常经典的技术诞生了,即返回地址欺骗.这项技术从199X年开始被发现,2000年后由于游戏的爆炸式发展加剧了对抗而被推向高峰 ### CALL 一条调用指令实际上是由多条不同的指令组成的“宏” `call someOne` 只意味着两件事 ```cpp push ReturnAddress — 调用后下一条指令的地址 jmp SomeFunc — 修改EIP/RIP到某些地方. ``` ### RET / RET XX 同样的`RET`指令也是两条宏: 如果指定了大小,比如Ret 18h就意味着: ```cpp add esp, 18h — 增加堆栈指针,减少堆栈大小,通常是函数采用的参数数量(实际上被压入堆栈,被调用者负责清理堆栈)。这是由于堆栈向下“增长” pop eip — 实际上将栈顶弹出到指令指针中,在那里进行"jmp" ``` 如果未指定大小,则被调用者不负责清理堆栈,只执行 `pop eip` ### PUSH “EAX” ```cpp sub esp, 4 — 如果是 32 位,则从堆栈指针中减去 4 个字节,有效地增加了堆栈大小 mov [esp], eax — 将被推送的项目移动到当前堆栈指针所在的位置 ``` ### POP “EAX” ```cpp mov eax, [esp] — 将堆栈顶部的值移动到要弹出的内容中 add esp, 4 —增加esp,减小栈的大小。 ``` ### 为什么要这样做 以teamviewer调用了窗口绘制为例:  正如在示例中看到的那样,该链从 `ntdll` 开始,进入 `kernel32`,进入 `teamviewer`,进入 `kernelbase` 等。它自下而上,包括将在“RET”指令上执行的特定返回地址等信息。但是 x32dbg 是怎么知道的呢 还记得`CALL` 指令的作用吗? ```cpp push ReturnAddress — 调用后下一条指令的地址 jmp SomeFunc — 修改EIP/RIP到某些地方. ``` 要知道执行“Ret”后返回到哪里,调用指令必须将 ReturnAddress 压入堆栈。因此,栈应该包括整个调用栈的所有返回地址。让我们来看看. 通过reclass访问ESP,这是全部的路径:  我们可以看到返回地址**确实在堆栈上**, 正是 x32dbg 所说的位置。这就是许多调试器确定调用堆栈的方式, 这也是某些领域用来确认调用者是否合法的位置-**即返回地址如果不是合法模块.就是非法调用** 既然知道了,目标就很明确了。我们可以修改堆栈上的地址以指向其他“授权”的地方。 但是没那么容易,如果我们修改栈上的那个地址,当执行不可避免的“Ret”时,它不会返回正确位置导致**堆栈不平衡**,导致程序崩溃 因此我们需要一个ROP利用链来解决这个问题,这个英文叫做`Gadget`.我喜欢叫做`跳板`,`jmp pad` ### 跳板? 假设合法模块里有这样一段指令: ```cpp call eax ret ``` 这个地方为我们做了两件事。首先,它会调用 eax 存储的地址处的函数,其次,它会返回给它的调用者 然而,正如我们之前提到的,`CALL`和`RET`只是宏,所以让我们进一步分解。它会做 ```cpp push 当前地址 jmp eax pop eip ``` 第一条指令不是很重要 但第二条和第三条指令与我们可以影响的输入源交互。我们可以修改 `eax` 寄存器,而且我们可以将内容压入堆栈以便函数稍后返回(通过 `pop eip`)。所以想法是将`eax` 设置为我们要调用的函数,以便小工具可以为我们调用它。然后我们将我们希望这个函数返回的地址(我们的真正要调用的函数)压入堆栈,以便目标一旦完成执行就可以返回给我们并继续执行代码 ### 编写代码 让我们来完成它: 假设一个程序保护了一个函数不被恶意调用 我们正常的代码调用如下:  这是对应的汇编代码:  让我们来解释一下: ```cpp push ebp mov ebp, esp mov eax, dword ptr ds:[2AFB3388] //把虚表地址0x2AFB3388放到eax中-下面会说为什么 push 1 //第7个参数压入栈 push 0 //第6个参数压入栈 push 0 //第5个参数压入栈 push 0 //第4个参数压入栈 push dword ptr ss:[ebp + C] // 第三个参数压入栈,第三个参数是指针,而且是临时变量存放在栈上,因此ebp + C指的是函数的`position`,而dword ptr ss:代表立即数寻址.SS代表的是 段寄存器,段寄存器的SS指向用于堆栈的内存段. add eax, 1BE660 //虚表地址 + 这个函数的偏移 push dword ptr ss:[ebp + 8] // call eax // 调用函数.注意CALL你可以理解为只是个宏 pop ebp //保持堆栈平衡,之前push ebp必须得pop ebp ``` 您可能已经注意到的一件事是它没有对第一个参数`this`做任何事情。那是因为我们调用的函数是`__thiscall`, 因此第一个参数实际上是使用 ecx 寄存器传递的  理论上,我们可以直接使用 jmp 指令跳转到我们的小工具,然后小工具会执行调用 如果目标函数检查调用者,它会在堆栈上看到我们小工具的返回地址,这是一个合法调用者,因为它是游戏自己的代码。 前面提到的问题是我们的小工具何时返回。小工具将从堆栈中弹出任意地址并返回到那里 因此我们我们将自己的返回地址压入堆栈,以便小工具返回给我们 ```cpp push 1 //arg 7 push 0 //arg 6 push 0 //arg 5 push 0 //arg 4 push dword ptr ss : [ebp + 0xc] //arg3 push dword ptr ss : [ebp + 0x8] //arg2 mov ecx, LocalPlayer_Obj //参数1 mov eax, pIssueOrder //要调用的目标函数 call $0 //自己调用自己 (用于将返回地址压入栈中) pop ebx //由于上面自己调用自己了,返回地址在栈中,即call $0后面一条指令,即这一条地址.这条地址调用pop ebx,就可以将当前的 EIP 放入 ebx,滥用之前的调用.(是的,我们不能直接访问IP寄存器) add ebx, 11 //ebx + 11正好是底下的mov al, 1的位置. push ebx //把算好的指向mov al, 1的位置的ebx当返回地址 jmp pGadget //跳到gadget mov al, 1 //这是真正的返回地址,返回true ``` 是不是很简单?但是 **不能用.程序会直接崩溃** ### 修复问题 首先,有两个错误在这里面 1. `__thiscall` 是被调用者清理堆栈.因此我们不需要找call eax,ret.而只需要一个`retn` 即可.`retn` 也就是 `pop eip` 2. 我们多压了一个`push ebx`为了修改所谓的返回地址 这会导致传入给被调用者的第二个参数变成了`Gadget`的地址...而不是原来的参数了. 因此我们需要: 1. 只要用一个`retn`当`gadget`就行 2. 不是`jmp`到`gadget`,而是压入自己的返回地址后,填充参数,再压入`gadget`的返回地址(还记得堆栈是向下增长的吗?所以压入顺序是反着来的) 这样调用顺序为: 目标被调用者 -> gadget地址 -> 自己的地址 代码如下: 首先得找个gadget,一个字节,`retn` ```cpp Push tag_reurn //利用vs的tag功能,再也不用自己手算偏移了.这里压入返回地址的偏移 push 1 //arg 7 push 0 //arg 6 push 0 //arg 5 push 0 //arg 4 push dword ptr ss : [ebp + 0xc] //arg3 push dword ptr ss : [ebp + 0x8] //arg2 mov ecx, LocalPlayer_Obj //arg1 push pIssueOrder //要执行的目标函数 jmp pGadget //跳到gadget tag_reurn: mov al, 1 //这是真正的返回地址,返回true ``` 当你在pIssueOrder下断点时候,你会发现是pGadget调用的pIssueOrder而不是真实的原始调用者 ### 结论&致谢 为了节约时间,这篇文章大部分代码、图片都来自老外@Hoang Bui的博客,但是为了方便你阅读大部分文字是本人自己写的不是直接翻译! 感谢你花费时间学习这个古老经典的技术. 本文由 huoji 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。 点赞 0
只有地板了