本文首发先知社区:https://xz.aliyun.com/t/14542
在上篇文章 https://xz.aliyun.com/t/14487 中,我们讨论了静态堆栈欺骗,那是关于 hook sleep ,在睡眠期间改变堆栈行为的欺骗,这篇文章我们来一起讨论一下主动欺骗,允许任意函数发起时的堆栈欺骗。
相关的基础知识在上篇文章已经介绍,并且给出了推荐阅读的链接,这里就不再多说,接下来让我们一起动起手来进行调试。
手动进行堆栈欺骗
我们先在 x64dbg 中手动进行堆栈欺骗,这对我们理解接下来的项目有很大的帮助。
我随便找了一个程序,我们的想法是在栈底再伪造相同的两帧,都是RtlUserThreadStart +0x28,因为这是我系统上常见的偏移量,你在下面的截图中也可以发现相同的帧。
我们首先找到这个函数
可以看到第一条指令是 sub rsp 78,这意味着它需要的帧栈大小为 0x78,注意这里是 16 进制,我们只需要在当前栈底向下移动 15 次(0x78=0x08*15
, 十六进制满 16 进 1),然后就可以在这个位置创建新栈了。
x64dbg 自动标注的范围也证实了我们的理论
我们把这个位置改成想要的帧栈
然后再次重复即可完成第二帧的伪造
此时在 Process Hacker 中查看(注意以管理员权限开启),可以看到两个帧栈已经成功伪造。
LoudSunRun
第一个要介绍的项目 https://github.com/susMdT/LoudSunRun,这是作者在学习另一个项目https://github.com/klezVirus/SilentMoonwalk 时的产物,由于原项目有点大,作者在这个项目较小的代码库、间接系统调用支持和多参数支持。
这个 Poc 实现了 pPrintf,NtAllocateVirtualMemory 的直接调用,以及pNtAllocateVirtualMemory 的间接系统调用,我们接下来看一下项目:
一个参数的 Printf 的调用
首先先获取 Printf 的地址,这是我们要去调用的函数,另外又获取了 BaseThreadInitThunk+0x14 和RtlUserThreadStart+0x21 的位置,这是我们要去伪造的栈帧,因为这是作者电脑上一个线程中常见的栈底,当然在不同 windows 版本下偏移量是不一样的,我的 windows 版本下偏移量是 RtlUserThreadStart+0x28。还有一个FindGadget 函数,这是为了帮助我们寻找一个 jmp [rbx] 小工具的,我们后面会讲到。
也许还有人注意到了 CalculateFunctionStackSizeWrapper 函数,这个函数是用来计算帧栈大小的,就像我们上面手动伪造 0x78,在当前栈底向下移动 15 次一样,这个函数是根据 UnwindOp 来进行计算的,想要深入理解的话可以阅读一下:https://codemachine.com/articles/x64_deep_dive.html
紧接着就来到了 Spoof 函数,这是最关键的函数,是我们的欺骗函数,这个函数的参数是可变的,但是 Spoof的前七个参数是相对固定的,前四个参数是我们想要去调用的函数的前四个参数,第五个参数是一个重要的结构体,里面存储着进程上下文,如果需要间接系统调用的 SSN 以及要伪造的栈帧,第六个参数是要调用的函数的地址,第七个参数用来指示是否还有别的参数,假如为 2 的话在 Spoof 里面就会想办法获取后面两个参数。
下面是第五个参数的结构体。
在这里我还想再多说一句 x64 下参数的传递,前四个参数是放在Rcx,Rdx,R8,R9四个寄存器中,后面的参数就要放在栈上了,如图(图源 Windows x64 调用约定 - 堆栈框架):
准备和调用阶段
ok,现在让我们进入汇编看看到底发生了什么,
首先是一些准备操作,先将栈上的参数分别给 rdi 和 rsi,rdi 就是我们前面提到的结构体,为了便于恢复所以要先将当前寄存器的值给存储起来,rsi 就是要调用的函数的地址。
在下图的最后一行我们将 rax 给到了 r12,而之前 pop rax 则将原始的返回值给到了 rax,这样 r12 就存储了函数的返回值,这是因为 rax 是易失性寄存器,而 r12 是非易失性寄存器,也就是说即使被别的函数调用,r12 也会被 push 保护起来,最后再 pop 出来。
在x64的调用约定中规定易失性寄存器RAX, RCX, RDX, R8, R9, R10, R11, XMM0-XMM5 为易失性寄存器,RBX, RBP, RDI, RSI, RSP, R12, R13, R14, R15, XMM6-XMM15为非易失性寄存器
这段代码是处理参数的准备工作,r11 和 r13 分别存储了需要额外处理的参数的个数和已经额外处理的参数的个数,通过比较这两个寄存器的值就可以处理完所有的额外的参数了。由于 printf 是不需要额外的参数的,所以我们之后再分析
下面是一个循环,和我们上面说的一样,比较两个寄存器的值来判断是否还需要处理,等下我们再说是如何处理的,先跟着代码调试
然后栈上分配一块空间,200h,然后 push 0,将之前的帧栈截断,剩下的就是我们自己要伪造的操作了。
接下来就是在伪造栈帧了,通过上面手动伪造应该很容易可以理解
现在看一下我们的栈帧,成功伪造
接下来是为了跳转和 fixup 做准备的,syscall 的代码等下再讲。将返回地址,rbx 寄存器值,fixup 的值给到前面那个欺骗的结构体,然后将 fixup 的值给到 rbx,因为它也是个非易失性寄存器,最后 jmp11。
此时看一下堆栈,十分干净
返回阶段
然后就是返回阶段了,当执行完 printf 后会进入到我们前面找到 jmp [rbx] 小工具,而我们的 rbx 存的是 fixup 函数地址,所以就会跳转到 fixup 函数
下面是我们的fixup 函数,主要就是恢复帧栈和前面保存的寄存器工作,最后 jmp 回到我们最初保持的返回点。
恢复之后的栈帧又是正常的
多参数调用
我们看一下多参数是怎么处理的
先将 rsp+30h 存储到 r10 里面,这样 r10+0x08 就可以找到下一个参数
这里 r14 是为了获取额外参数应该在的位置的,是我们需要压入栈中的数据的偏移量,首先加上 200h,这是我们在栈上分配的假栈的空间,然后是加 8,这对应着 push 0 指令,然后再加上要伪造的三个帧栈大小,这样就到了我们要调用的函数的帧栈了,然后以此为基础,第一个参数在 r14+0x28 位置处,然后每个参数依次加 0x08 即可
下面上一张图帮助大家理解
我们先找到参数需要移动到的位置,然后再将 r10+0x08 的值给到相应位置就可以了,相应位置是通过 rsp-r14 的值计算出来的,r14 是我们上面说的偏移量
间接系统调用
这个实现起来就很简单了,我们 jmp 去的时候先将 ssn 号存到 rax,然后直接跳转到 syscall 指令就可以了
注意这里跳转的函数直接就是 syscall 指令,Poc 里面作者是手动找到 syscall 指令的
当然获取 syscall 指令可以自动化获取,这里不再展开