堆栈欺骗(ThreadStackSpoofer分析)

2025-03-11 154 0

堆栈欺骗(被动)

上一篇文章已经讲过在不使用Rbp寄存器的情况下,如何通过.pata段和Rep寄存器来读取变量,这是杀软回溯栈的核心,现在我们来讲,如何进行堆栈欺骗

ThreadStackSpoofer

总体来讲这个项目可以分为以下部分

1、传入shellcode部分

2、保存和恢复部分

3、使用跳板

4、注入shellcode并且运行新线程

从总体上看,流程是先读取并保存 shellcode,再保存被 hook 函数的原始信息,通过 trampoline 技术安装 hook(把 hook 函数 MySleep 传入跳板),然后注入 shellcode(分配内存、修改保护),最后创建一个新线程运行 shellcode。

最重要的点在于要修改MySleep的返回地址,让工具无法栈回溯,并且要在结束之后还原被Hook函数的各种信息,防止异常

我们首先来看shellcode部分

log函数为作者自定义的函数,原型为下图所示,log的主要作用为用于把数据转为字符串,并且输出字符串。

然后他告诉你了,使用ThreadStackSpoofer.exe 需要传入俩参数,一个是shellcode,一个是spoof

创建了一个<uint8_t>类型vector容器用来存储shellcode

然后来到readShellcode函数,Handptr原型如下所示,创建了一个用来管理Handle句柄的智能指针,并且使用std::remove_pointer来去掉HANDLE的指针部分,关于这个为什么要这么做是因为HANDLE的原型在不同的编译器里面是不同的,有的是*void,有的是uintprt_t

这么做的原因是因为要确保智能指针管理的是句柄本身,而不是指向句柄的指针,而且要保证跟删除器相兼容

typedef std::unique_ptr<std::remove_pointer<HANDLE>::type, decltype(&::CloseHandle)> HandlePtr;

这里干的事情大体就是两点,一是扩展vector容器的大小,二是读取文件里面的内容,

然后会检测spoof是否为true,如果是则进入hacksleep函数

下面主要的作用是在hook之前,保存目标函数的的原始字节和在解除hook的时候,恢复被hook函数的数据

从汇编的层面来看

假设Sleep的汇编代码如下

0x1000: mov eax, 0x1
0x1005: call ntdll!NtDelayExecution
0x100A: ret

Hook 过程

  1. 在 Hook 之前

    • originalBytes = {mov eax, 0x1, call ntdll!NtDelayExecution}
    • previousBytes = {mov eax, 0x1, call ntdll!NtDelayExecution}
  2. 执行 Hook,覆盖Sleep入口

    • Sleep入口被修改为jmp MySleep
    • previousBytes里保存的就是原始Sleep的前几条指令
    • originalBytes仍然保持原始数据,等待将来恢复
  3. 解除 Hook

    • 直接把originalBytes的内容拷贝回Sleep的入口,使其恢复原样

    可能到这个时候就会有疑惑了,这两个的作用不是一样的吗?为什么会要两个

    区别就在下面hooksleep函数里面,originalBytes是在没有hook之前你就已经知道了sleep的前几条指令,在你结束hook之后要恢复的数据,但是previousBytes实际上保存的则是在跳板之前sleep的字节,这样可以让你知道你的hook是修改了哪些字节

然后下面的代码主要是包含了要hook的数据和改写到哪里的字段,然后它还存储了sleep的函数指针

然后进入到fastTrampoline函数

然后接着往下走,来到了 trampoline跳转

关于trampoline(跳板)指令,正常来讲在64位系统里面,你是不能够直接jmp立即数的,但是我们可以通过trampoline来跳转到我们设置的地址,这里就用了先把地址传递给 r10寄存器,然后jmp到r10寄存器

0x49,0xBa 为 mov r10

这个时候jumpaddress存储的值是MySleep的地址,但是我们从下图可以看到jumpaddress实际上存的并不是Mysleep的值

具体原因不好说,我也没有实验出来是为啥

然后把要hook的地址也就是MySleep的地址传给了跳板

它首先进行判断是不是有空指针或者数据为0如果是则说明没有正确指向sleep地址或者没有正确获取到数据范围

然后还保存了原始的 addressToHook,然后在将trampoline写到addressToHook里面,这样的话下一次调用就是调用的我们的MySleep

然后这里它又定义了一个函数指针,作用是用来刷新进程的,确保在cpu调度的时候,确保代码段的修改(如 Hook、Shellcode 注入)被正确执行,防止 CPU 继续使用旧的缓存指令。

然后一开始它判断函数指针是不是为空,是则通过getProcAddress来拿到NtFlushInstructionCache的函数地址

然后因为pNtFlushInstructionCache作为一个指向函数的指针变量,它上面已经指向了NtFlushInstructionCache的地址,实际上就是调用的NtFlushInstructionCache函数,这里的作用就是刷新缓存,确保cpu会执行hook函数

然后进入到injectShellcode注入函数里面

首先分配可读写页面,然后复制shellcode到分配的内存里面,然后又使用VirtualProtect把分配的好的内存改为可执行,然后清除原来的shellcode,最后新建线程,需要注意的是哪怕并没有在main或者其他函数里面调用runShellcode函数,注入线程这个函数也没有接受这个参数,但是当 CreateThread 被调用后,操作系统会在新线程中自动调用 runShellcode。传入的参数(这里是 alloc,即存放 shellcode 的内存地址)会被传递给 runShellcode 的 param 参数。

然后我们来看MySleep

_AddressOfReturnAddress()是 Microsoft 特定的扩展,用于获取当前函数的返回地址的地址

PULONG_PTR是一个指向ULONG_PTR类型的指针,它存储的是返回地址的指针,而不是返回地址本身。

也就是说现在overwrite存储的是一个指向返回地址的指针

然后最关键的代码,他把返回地址直接设置成了0,也就是说这样子就没有办法进行栈回溯了,因为不知道是谁调用的MySleep,这样子我们的shellcode就被隐藏了

*overwrite = 0;

然后这里使用了SleepEx来进行休眠,同时第二个参数为false 防止apc进行唤醒

最后把正常的返回地址给改回来,这样最后执行ret的时候就不会因为返回地址异常而报错了


4A评测 - 免责申明

本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。

不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。

本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。

如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!

程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。

侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)

相关文章

幽灵勒索软件肆虐全球70余国企业
2025年CISO最青睐的五大安全框架
如何有效实施SOAR以缩短事件响应时间
微软Entra新安全功能引发大规模账户锁定事件
网络安全周报:LLM软件包幻觉威胁供应链,Nagios日志服务器漏洞修复
某省4A平台密码改造建设方案

发布评论