首页 > 其他分享 >初探堆栈欺骗之静态欺骗

初探堆栈欺骗之静态欺骗

时间:2024-06-06 12:00:05浏览次数:22  
标签:调用 纤程 线程 初探 堆栈 欺骗 函数

本文首发先知社区:https://xz.aliyun.com/t/14487
首先介绍一下堆栈欺骗的场景,当我们用一个基本的 shellcode loader 加载 cs 的 shellcode,在没有对堆栈做任何事情时,我们的堆栈是不干净的,我们去看一下堆栈时会发现有很多没有被解析的地址在其中,这显然是不正常的,因此 av/edr 会重点扫描这部分内存区域,就可能会导致我们的 loader gg。
image.png
或者说当我们直接系统调用时,和正常程序也是有区别的,如下:

  • 正常程序:主程序模块->kernel32.dll->ntdll.dll->syscall,这样当0环执行结束返回3环的时候,这个返回地址应该是在ntdll所在的地址范围之内
  • 直接进行系统调用:此时当ring0返回的时候,rip将会是你的主程序模块内,而并不是在ntdll所在的范围内。

因此我们需要堆栈欺骗来帮我们隐藏堆栈。
我们先需要 32 位/64 位下堆栈的知识,推荐阅读:https://cloud.tencent.com/developer/article/2149944
https://pyroxenites.github.io/post/diao-yong-zhan-qi-pian/
https://mp.weixin.qq.com/s/_Cr6Ds0vaeGF7DShlq_XJg
https://codemachine.com/articles/x64_deep_dive.html
我们也来简单的说一下,在 32 位下,是通过rbp 来指向堆栈的开始位置,并且每次移动 rbp 时会 push rbp,然后再 mov rbp,rsp,因此我们只需要不断回溯 rbp 就可以回溯完整个堆栈。
在 64 位下,ebp 不再有这样的功能,它现在是一个通用寄存器,下面上两张图简单解释一下吧,这篇文章涉及到的技术为被动欺骗,不需要很深的理解也能看懂大部分。
x64 PE 文件中存在一个名为 .pdata的区段,区别于x32其属于x64独有区段,值的注意的是.pdata的RVA和异常目录表的RVA是相同。pdata中的数据由 多个 _IMAGE_RUNTIME_FUNCTION_ENTRY 结构体组成,具体的声明如下:

typedef struct _IMAGE_RUNTIME_FUNCTION_ENTRY {
  DWORD BeginAddress;
  DWORD EndAddress;
  union {
    DWORD UnwindInfoAddress;
    DWORD UnwindData;
  } DUMMYUNIONNAME;
} RUNTIME_FUNCTION, *PRUNTIME_FUNCTION, _IMAGE_RUNTIME_FUNCTION_ENTRY, *_PIMAGE_RUNTIME_FUNCTION_ENTRY;

从每个字段类型位DWORD可以看出,其表示的都是RVA,所以在使用时都需要加上模块基地址,BeginAddress代表函数的起始地址RVA,EndAddress代表函数的结束地址RVA,UnwindInfoAddress指向 _UNWIND_INFO结构体,其声明如下:

typedef struct _UNWIND_INFO {
    UBYTE Version       : 3;
    UBYTE Flags         : 5;
    UBYTE SizeOfProlog;
    UBYTE CountOfCodes;
    UBYTE FrameRegister : 4;
    UBYTE FrameOffset   : 4;
    UNWIND_CODE UnwindCode[1];
/*  UNWIND_CODE MoreUnwindCode[((CountOfCodes + 1) & ~1) - 1];
*   union {
*       OPTIONAL ULONG ExceptionHandler;
*       OPTIONAL ULONG FunctionEntry;
*   };
*   OPTIONAL ULONG ExceptionData[]; */
} UNWIND_INFO, *PUNWIND_INFO;

Version默认为1,Flags总共包含四个值,UNW_FLAG_NHANDLER,UNW_FLAG_EHANDLER ,UNW_FLAG_UHANDLER,UNW_FLAG_CHAININFO,SizeOfProlog表示序言大小(字节),CountOfCodes代表序言操作中所有指令总共占用的”槽“数量,FrameRegister用到的帧寄存器,FrameOffset帧寄存器距离栈顶的偏移。
UnwindCode表示的是 _UNWIND_CODE联合体,大小为两个字节,其声明如下:

typedef union _UNWIND_CODE {
    struct {
        UBYTE CodeOffset;
        UBYTE UnwindOp : 4;
        UBYTE OpInfo   : 4;
    };
    USHORT FrameOffset;
} UNWIND_CODE, *PUNWIND_CODE;

CodeOffset紧跟序言的代码起始偏移,UnwindOp操作码,Opinfo对应操作码的附加操作信息。
然后就根据UnwindOp 对应不同操作码对栈的影响,即可计算某个函数的栈帧大小了。
下面上两张图帮大家理解一下:
image.png
image.png

我们在这篇文章中先介绍被动欺骗,或者说是静态欺骗,它是关于 sleep 的欺骗,或者说是睡眠时间混淆,并不能说是真正意义的堆栈欺骗,但是对于 beacon 来说也是有一定意义的,而主动欺骗,支持任何函数的堆栈欺骗,将在下一篇文章进行介绍。下面我们一起来看几个项目。

threadStackSpoofer

第一个方式的项目地址在https://github.com/mgeeky/ThreadStackSpoofer
首先是处理参数和读取 shellcode 的部分,我们不关心。
image.png
然后又调用了 hookSleep 函数,我们跟进去
image.png
在 hookSleep 函数里面,他先准备了一个结构体,结构体里面包含了要 hook 的字段以及将 hook 的函数改写到哪里的字段,然后将 sleep,自实现的 MySleep,buffers 一并传给 fastTrampoline 函数。
image.png
image.png
在接下来构造了一个 trampoline 用于跳转
image.png
调试一个,可以看到 addr 的地址其实就是我们自实现的 MySleep 里面
image.png
然后保存一下原始的 addressToHook 字节,再将我们 trampoline 重写到 addressToHook 的位置,这样调用 Sleep 的时候其实会跳转到我们自实现的 MySleep 里面。
image.png
然后这部分代码相当于对当前进程刷新一下缓存,使得我们修改生效
image.png
然后就是注入 shellcode 的过程,然后当我们的 beacon sleep 时,就会调用到我们的 MySleep 函数,我们接下来再看看 MySleep 是如何处理我们的堆栈的。
image.png
_AddressOfReturnAddress 是编译器提供的一个函数,作用是返回当前函数返回地址的内存地址,给到 overwrite
image.png
然后关键就来了,我们将overwrite 直接改写为 0,这样停止继续回溯栈,然后我们就可以隐藏剩余的栈帧,即我们的 shellcode 栈帧就会被隐藏,当 sleep 结束之后再将栈帧改写回去。
image.png
这是调用堆栈未被欺骗时的样子:
image.png
当启用线程堆栈欺骗时:
image.png
此时帧栈展开到我们的 MySleep 函数,往后 shellcode 的帧栈就被隐藏了,当然我们还可以做更多有趣的事情,比如在 sleep 期间更改 shellcode 内存属性,对 shellcode 内存区域进行加密,或者解除我们对 etw/amsi 的 hook,在 sleep 之后再重新 hook,或者等等等等可以由大家自由发挥。
但是这里还是会有一些问题的,我们将调用堆栈设为不可展开,这意味着它看起来异常,因为系统将无法正确遍历整个调用堆栈帧链。当一个专业的恶意软件分析师在分析时自然会发觉异常,但是那些内存扫描工具就不一定了,它总不能遍历每个线程的堆栈来验证其是否不可展开。

CallStackMasker

这个项目的地址在https://github.com/Cobalt-Strike/CallStackMasker ,cs 官方也写了博客来介绍这个技术https://www.cobaltstrike.com/blog/behind-the-mask-spoofing-call-stacks-dynamically-with-timers
这个项目是计时器欺骗调用堆栈的 PoC ,在 beacon 休眠之前,我们可以对计时器进行排队,用假的调用堆栈覆盖其调用堆栈,然后在恢复执行之前恢复原始调用堆栈。因此,就像我们可以在睡眠期间欺骗属于我们的植入物的内存一样,我们也可以欺骗主线程的调用堆栈。这种方式是比较简单的复制堆栈,避免了主动堆栈欺骗的复杂性。
如果我们考虑一个正在执行任何类型等待的通用线程(waitforsingleobject),它在等待满足之前无法修改自己的堆栈。此外,它的堆栈始终是可读写的。因此,我们可以使用定时器来:

  1. 创建当前线程堆栈的备份
  2. 用假线程堆栈覆盖它
  3. 在恢复执行之前恢复原始线程堆栈

这就是这个技术的核心,PoC 以两种模式运行:静态和动态。静态模式模仿 spoolsv.exe 硬编码调用堆栈。该线程如下所示,通过 KERNELBASE!WaitForSingleObjectEx 可以看到处于‘Wait:UserRequest’ 状态:
image.png
我们的线程的起始地址和调用堆栈与上面 spoolsv.exe 中标识的线程相同:
image.png
静态模式的明显缺点是我们仍然依赖硬编码的调用堆栈。为了解决这个问题,PoC 还实现了动态调用堆栈欺骗。在此模式下,它将枚举主机上所有可访问的线程,并找到一个处于所需目标状态的线程(即通过 WaitForSingleObjectEx 的 UserRequest)。一旦找到合适的线程堆栈,它将复制它并使用它来休眠线程的克隆。同样,PoC 将再次复制克隆线程的起始地址,以确保我们的线程看起来合法。
好的,接下来让我们看看代码:
关于堆栈计算大小等等代码我们先略过,这并不会影响我们理解这项技术,并且解释起来显得太啰嗦。
我们看关键地方,这里创建一个新的线程,并且将 rip 指针指向 go 函数,也就是说要执行我们的 go 函数。
image.png
image.png
我们来看MaskCallStack,先是初始化上下文和句柄,方便后续操作,然后获取 NtContinue 函数地址:通过 GetProcAddress 函数获取 Ntdll 模块中的 NtContinue 函数的地址。这个函数通常用于继续执行线程
image.png
设置定时器,创建定时器,并设置回调函数,以执行一系列操作:备份堆栈、覆盖堆栈、恢复堆栈和设置事件,
当等待事件对象被定时器触发,此时调用堆栈将被遮蔽,然后定时器结束之后又触发事件,堆栈又恢复。
image.png
image.png
image.png

纤程

纤程是一种用户级线程,它允许在一个线程内部进行上下文切换。纤程的切换完全由程序控制,不需要内核的参与,因此效率非常高。纤程的上下文包括寄存器状态和堆栈,当切换纤程时,当前纤程的上下文会被保存,然后加载新纤程的上下文。这意味着,通过纤程切换,可以改变当前线程的堆栈。
一个线程可以创建多个纤程,并通过调用 SwitchToFiber 函数根据需要在它们之间切换。在此之前,当前线程本身必须通过调用 ConvertThreadToFiber 成为纤程,因为只有一个纤程可以创建其他纤程。
所以当我们进行 sleep 时可以切换到新的纤程里面进行 sleep,从而隐藏我们 shellcode 堆栈,当调用返回时,它将再次切换到 shellcode 的纤程,以便可以继续执行。
重要的 api 使用如下:

// 创建纤程
LPVOID lpFiber = CreateFiber(0, FiberFunc, NULL);
// 将当前线程转换为纤程
ConvertThreadToFiber(NULL);
// 切换到新创建的纤程
SwitchToFiber(lpFiber);

项目参考:https://github.com/Kudaes/Fiber
代码实现的话第一个项目改改就可以实现,先 hook sleep 函数,然后调用 sleep 函数的时候就可以将上下文转换到一个新的纤程中,然后 sleep 结束之后,再转回 shellcode 执行的纤程中即可,这里不再分析代码。
但是当我们在执行 shellcode 相关功能时如果被检测到了会直接 gg。

0x00000009b0f6f258 {140730941243392}
00000009B0F6F3F0
image.png
image.png

rbp
rsp
push rbp 保存栈,方便回溯
return to x 返回地址 rip

  • RBP 将指向此功能的堆栈帧的开始地方。
  • RBP 将包含前一个堆栈帧的起始地址。
  • (RBP + 0x8)将指向堆栈跟踪中前一个函数的返回地址。
  • (RBP + 0x10)将指向第 7 个参数(如果有)。
  • (RBP + 0x18)将指向第 8 个参数(如果有)。
  • (RBP + 0x20)将指向第 9 个参数(如果有)。
  • (RBP + 0x28)将指向第十个参数(如果有)。
  • RBP-X,其中 X 是 0x8 的倍数,将引用该函数的局部变量。
  • 程序使用 RBP 的偏移量来访问局部变量或函数参数。之所以能这样是因为 RBP 在函数序言中的函数开始处被设置为 RSP 寄存器的值。

RBP 不再用作帧指针。它现在是一个通用寄存器,就像任何其他寄存器(如 RBX、RCX 等)一样。调试器不能再使用 RBP 寄存器来遍历调用堆栈。
ebp:push ebp 所以可以用 ebp 遍历栈帧
编译器优化可能会减少对RBP的依赖,甚至在某些情况下完全不用它,尤其是在优化较小函数时。

标签:调用,纤程,线程,初探,堆栈,欺骗,函数
From: https://www.cnblogs.com/fdxsec/p/18234871

相关文章

  • c函数堆栈
    使用反汇编分析代码1.无参数无返回值voidfun1(){}intmain(intargc,char*argv[]){ fun1(); return0;}反汇编分析2.有参无返回值代码voidfun2(intx,inty){ x+y;}intmain(intargc,char*argv[]){ fun2(1,2); return0;}返汇编分析3.无参......
  • Linux使用sz/rz命令在服务器上传下载文件 及 GIF89a图片欺骗攻击导致tmp目录很多php开
    一、Linux下使用sz/rz命令从服务器下载文件或上传文件至服务器    使用secureFX可以直接和LINUX进行文件互传,但有些服务器会要求通过中间服务器连接,无法难过SecureFX直接操作文件了,不过Linux有两个很好用的命令:sz和rzSZ:sz命令就是将服务器上的文件下载到本地电脑。s......
  • 初探富文本之基于虚拟滚动的大型文档性能优化方案
    初探富文本之基于虚拟滚动的大型文档性能优化方案虚拟滚动是一种优化长列表性能的技术,其通过按需渲染列表项来提高浏览器运行效率。具体来说,虚拟滚动只渲染用户浏览器视口部分的文档数据,而不是整个文档结构,其核心实现根据可见区域高度和容器的滚动位置计算出需要渲染的列表项,同时......
  • 使用 Scapy 库编写 IP 地址欺骗攻击脚本
    一、介绍1.1概述IP地址欺骗(IPSpoofing)是一种网络攻击技术,攻击者伪造其数据包的源IP地址,使其看起来像是从其他合法地址发送的。这种技术常用于各种攻击中,例如DDoS攻击、Man-in-the-Middle(MITM)攻击和拒绝服务(DoS)攻击等。1.2攻击原理IP地址欺骗的核心在于攻击者能够创建带......
  • YOLOv10(1):初探,训练自己的数据
    目录1.写在前面2.值得关注的点3.训练自己的数据集4.阅读代码的小建议1.写在前面        很多人YOLOv9还没有完全研究透,YOLOv10出来了。        惊不惊喜,意不意外!        据论文里提到,YOLOv10就是为了加速推理,在保证精度的同时,降低参数......
  • 【网络通信】初探Google的reCAPTCHA安全认证技术
        在数字化日益普及的今天,如何有效保护网站免受恶意攻击和滥用成为了每个网站管理员和开发者必须面对的重要问题。而reCAPTCHA正是Google提供的一种强大且智能的解决方案,旨在通过人机验证来增强网站的安全性。一、reCAPTCHA简介        reCAPTCHA是Google推......
  • 线性规划-内点法初探
    参考:最优化理论与算法(第2版)(陈宝林)书中首先介绍了将一般线性规划问题转化为Karmarkar标准问题求解,为简化计算,Karmarkar等人又给出了内点法以求解线性规划问题,此部分在书中为*号引申内容,介绍较为简略,此处也是对书中内容做简要的补充。考虑如下线性规划问题......
  • 初探后缀自动机
    本篇旨在讲解部分常见的SAM技巧,以及经典的SAM题目。几点暴论:如果题目中求的是什么子串的出现次数,那直接无脑上SAM。因为SAM的parent树是反串的后缀树,求出现次数时,二者并无区别。如果题目中涉及了「前缀」「后缀」等字样,请仔细品味在使用SAM时是否应该对反串建pare......
  • AI助力科研:自动化科学构思生成系统初探
    科学研究作为推动创新和知识进步的关键活动,在解决复杂问题和提升人类生活水平方面发挥着至关重要的作用。然而,科学研究的固有复杂性、缓慢的进展速度以及对专业专家的需求,限制了其生产力的提升。为了增强科研效率,本文提出了一个名为ResearchAgent的系统,这是一个由大型语言模型(L......
  • Redis之初探
    Redis是什么Redis是一个开源,内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理。Key-Value格式的缓存和存储服务器;数据结构服务器,它支持字符串、哈希表、列表、集合、有序集合,位图,hyperloglogs等数据类型;所有数据集必须在内存中使用,磁盘是用来持久数据的。是一......