调用栈欺骗技术

admin 2024年2月17日19:23:52评论73 views字数 4721阅读15分44秒阅读模式

前言

2023年10月6日,Bobby CookeDylan Tran联合发表了一篇名为Reflective call stack detections and evasions1文章,深入探讨了调用堆栈检测和规避以及BokuLoader如何通过反射加载的方式将堆栈欺骗功能加载到Beacon中,并提到了BokuLoader中堆栈欺骗是利用了栈帧的合成,是对DylanLoudSunRun项目的整合。

提到LoudSunRun项目实际上是Dylan在理解堆栈欺骗这一技术过程中的产物,同时他还编写了一篇介绍性的文章,以便读者更好的理解这一技术,文章在此获得2

正文

概览

LoudSunRun针对64位实现堆栈欺骗的过程主要分为两部分

  1. 1. 获取伪造栈帧的关键参数,主要通过Testing.c来实现

  2. 2. 伪造栈帧,主要通过test.asm实现

前置知识

在开始之前,我们可能需要了解一些关于x64栈帧展开的前置知识:

  • • x64默认调用约定Fastcall使用RCX RDX R8 R9对函数的前四个参数进行传参,多余参数入栈

  • • 在x64上RBP不再像x86上作为栈指针,不使用RBP来访问函数的局部变量和参数,而是作为一个通用寄存器,调试器不能够再使用RBP遍历栈帧

  • • x64可执行文件包含一个名为pdata区段,这个“异常目录”为可执行文件中的每个非叶子函数包含一个RUNTIME FUNCTION结构,调试器利用这些结构来进行栈帧的遍历。

  • • 无论如何你可以通过RUNTIME FUNCTION找到UNWIND INFO,UNWIND CODE,并获得函数的起始地址结束地址UNWIND CODE则用来描述如下某种序言操作:

- SAVE_NONVOL - Save a non-volatile register on the stack.
- PUSH_NONVOL - Push a non-volatile register on the stack.
- ALLOC_SMALL - Allocate space (up to 128 bytes) on the stack.
- ALLOC_LARGE - Allocate space (up to 4GB) on the stack.

关于序言:On X64, once a function has finished its prologue (and hence stack modifications), it does not modify the stack pointer until its epilogue reverses them, hence Rsp is static throughout the function body.

关于x64更多内容详见:https://codemachine.com/articles/x64_deep_dive.html

调用栈欺骗技术

代码部分

如何编译

我们通过Visual Stdio打开项目,之前提到项目包含一个汇编文件,为了能够顺利编译,需要先进行如下操作

  1. 1. 右键test.asm文件,点击属性

  2. 2. 在项目类型中选择“自定义生成工具”

  3. 3. 点击应用,然后点击左边“自定义生成工具”中的"常规"

  4. 4. 在命令行中输入

ml64 /c %(fileName).asm

  1. 1. 在输出中输入

%(fileName).obj;%(Outputs)

调用栈欺骗技术

如何使用

项目中对函数调用的栈欺骗都通过Spoof实现,Spoof是一个可变参数函数,由test.asm文件实现

PVOID NTAPI Spoof(PVOID a, ...);

无论如何Spoof的前七个参数是相对固定的,比如我们想对VirtualAllocEx进行栈帧调用的欺骗,那么通过Spoof可以这样实现:

Spoof((PVOID)(-1), 0, 1024, MEM_COMMIT | MEM_RESERVE, &p, VirtualAllocEx, (PVOID)1, (PVOID)PAGE_EXECUTE_READWRITE);
  • • 参数一到参数四分别对应64位Fastcall调用约定下的Rcx,Rdx,R8,R9四个寄存器

  • • 参数五代表一个特定结构体指针,其中包含栈帧伪造的关键参数,比如跳板指令地址返回地址...等。

  • • 参数六表示调用函数的地址,比如VirtualAllocEx

  • • 参数七代表入栈参数数量,比如VirtualAllocEx入栈数量为1

代码理解

在解析Testing.c中代码含义之前,先通过下图对Spoof进行一个整体的理解,以便我们知道涉及具体代码时,为何这样做?为何需要这个?

调用栈欺骗技术

上图就是最终进行函数调用时候的栈帧布局JMP R11会跳转到函数起始位置,比如VirtualAllocEx...:),所以当我们使用ProcessHacker之类的工具查看函数调用栈时候就会依次遍历到跳板指令gadget位于的函数,BaseThreadInitThunkRtlUserThreadStart,最后在此结束。

标注的返回地址则指向跳板指令,跳板指令必须位于某个合法库中,因为在栈帧展开时候此地址也会包含其中

下面我们再来查看主函数中详细代码实现:

首先在合法库中查找特定的跳板指令

p.trampoline = FindGadget((LPBYTE)GetModuleHandle(L"kernel32.dll"), 0x200000);

获取第一个伪造的返回地址,位于BaseThreadInitThunk

ReturnAddress = (PBYTE)(GetProcAddress(LoadLibraryA("kernel32.dll"), "BaseThreadInitThunk")) + 0x14;

获取第二个伪造的返回地址,位于RtlUserThreadStart

ReturnAddress = (PBYTE)(GetProcAddress(LoadLibraryA("ntdll.dll"), "RtlUserThreadStart")) + 0x21;

分别获取上面三个函数的栈帧大小(还记得前面的UNWIND_CODE:?)

p.BTIT_ss = (PVOID)CalculateFunctionStackSizeWrapper(ReturnAddress);
p.RUTS_ss = (PVOID)CalculateFunctionStackSizeWrapper(ReturnAddress);
p.Gadget_ss = (PVOID)CalculateFunctionStackSizeWrapper(p.trampoline);

获取到这些关键参数后调用Spoof

关于Spoof的实现,Dylan已经在代码中做了详细的注释,同时为了节省篇幅,不再在此堆砌大量的汇编代码,只展示重要部分:

  • • 调用函数参数处理部分

将参数赋值到正确的位置,以便最后的调用

调用栈欺骗技术

    looping:

        xor r15, r15            ; r15 will hold the offset + rsp base
        cmp r11, r13            ; comparing # of stack args added vs # of stack args we need to add
        je finish
    
        ; ---------------------------------------------------------------------
        ; Getting location to move the stack arg to
        ; ---------------------------------------------------------------------
        
        sub r14, 8          ; 1 arg means r11 is 0, r14 already 0x28 offset.
        mov r15, rsp        ; get current stack base
        sub r15, r14        ; subtract offset
        
        ; ---------------------------------------------------------------------
        ; Procuring the stack arg
        ; ---------------------------------------------------------------------
        
        add r10, 8
        push [r10]
        pop [r15]     ; move the stack arg into the right location

        ; ---------------------------------------------------------------------
        ; Increment the counter and loop back in case we need more args
        ; ---------------------------------------------------------------------
        add r11, 1
        jmp looping
    
    finish:
  • • 伪造栈帧部分

// Spoof栈帧
sub    rsp, 200h
// 栈帧截断
push 0 
// 伪造RtlUserThreadStart栈帧
sub    rsp, [rdi + 56]
mov    r11, [rdi + 64]
mov    [rsp], r11
// 伪造BaseThreadInitThunk栈帧
sub    rsp, [rdi + 32]
mov    r11, [rdi + 40]
mov    [rsp], r11
// 伪造跳板指令函数栈帧
sub    rsp, [rdi + 48]
mov    r11, [rdi + 80]
mov    [rsp], r11
  • 调用栈欺骗技术

  • • 跳板指令寄存器赋值

为了执行完最终的调用函数并利用跳板指令 jmp [rbx] 顺利返回到Spoof中,需要先对rbx赋值,将jmp r11指令的下一条指令地址放到Rbx指向的内存中

mov    [rdi], rbx // rbx此时保存的是返回地址 
mov    rbx, rdi   // [rbx]此时保存的是返回地址
  • • 执行函数调用

jmp    r11

总结

通过栈帧拼凑的方法进行主动调用栈欺骗,并利用跳板指令返回到主模块起始调用地址,同时又不会让主模块出现在调用栈中,除了作者使用的两个函数作为伪造栈帧,我们还可以通过修改代码使用其他函数进行栈帧的伪造。

引用

[1]:https://securityintelligence.com/x-force/reflective-call-stack-detections-evasions/

[2]:https://dtsec.us/2023-09-15-StackSpoofin/

原文始发于微信公众号(无名之):调用栈欺骗技术

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年2月17日19:23:52
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   调用栈欺骗技术http://cn-sec.com/archives/2149720.html

发表评论

匿名网友 填写信息