stack-spoofing-1
太久没写文章了,基本快要告别安全了
公众号:https://mp.weixin.qq.com/s/b6oV_vWR3asE_KSAOgG_9g
或许我们的公众号会有更多你感兴趣的内容

【免杀】堆栈欺骗-1
在B站上有一个会议的中文翻译视频,讲的很细:https://www.bilibili.com/video/BV1hJnqzoEwc
正常使用MessageboxA的代码
|

通过栈回溯发现MessageBox是被loader.exe的main函数加载的
简单shellcode加载器
再看一段简单的shellcode加载器代码
|

这里对messagebox的调用是从loader.exe中一个没有注册过的函数sc调用的,是我我也怀疑。
手动模拟windbg栈回溯
关于这部分的完整、相关材料,可以阅读[6]
在windows 64下,与 32 位依赖 EBP 链不同,x64 采用纯表驱动的栈展开方式。编译器将每个非叶子函数的栈操作信息(如分配了多少栈空间、哪些非易失性寄存器被保存等)写入 PE 文件的 .pdata 异常目录中,具体的步骤如下(Child-SP 定位):
-
获取当前指令指针(RIP),定位其所属模块及函数偏移。
0:000> r rip, rsp
rip=00007ff6ccc31545 rsp=0000009ed26ff9a0 -
在模块的展开信息表中查找该偏移对应的展开码(Unwind Codes),例如
UWOP_PUSH_NONVOL、UWOP_ALLOC_SMALL等。0:000> .fnent loader!main
Debugger function entry 000001cf`c27f0230 for:
[D:\Blog\wechat\stack_spoofer\loader\src\main.cpp @ 35] (00007ff6`ccc31500) loader!main | (00007ff6`ccc315d0) loader!__empty_global_delete
Exact matches:
loader!main (void)
BeginAddress = 00000000`00001500
EndAddress = 00000000`00001569
UnwindInfoAddress = 00000000`0000beec
Unwind info at 00007ff6`ccc3beec, 8 bytes
version 1, flags 0, prolog 17, codes 2
00: offs 6, unwind op 2, op info 7 UWOP_ALLOC_SMALL.
01: offs 2, unwind op 0, op info 7 UWOP_PUSH_NONVOL reg: rdi.得到当前函数所在的栈回溯信息位于
00007ff6ccc3beec0:000> dt _UNWIND_INFO 00007ff6`ccc3beec
loader!_UNWIND_INFO
+0x000 Version : 0y001
+0x000 Flags : 0y00000 (0)
+0x001 SizeOfProlog : 0x17 ''
+0x002 CountOfCodes : 0x2 ''
+0x003 FrameRegister : 0y0000
+0x003 FrameOffset : 0y0000
+0x004 UnwindCode : [1] _UNWIND_CODE通过展开码查看栈帧的偏移值,
CountOfCodes = 2所以有两个回溯值0:000> dt _UNWIND_CODE 00007ff6`ccc3beec+4
loader!_UNWIND_CODE
+0x000 CodeOffset : 0x6 ''
+0x001 UnwindOp : 0y0010
+0x001 OpInfo : 0y0111
+0x000 FrameOffset : 0x7206
0:000> dt _UNWIND_CODE 00007ff6`ccc3beec+4+2
loader!_UNWIND_CODE
+0x000 CodeOffset : 0x2 ''
+0x001 UnwindOp : 0y0000
+0x001 OpInfo : 0y0111
+0x000 FrameOffset : 0x7002UnwindOp = 2对应为UWOP_ALLOC_SMALL。根据[4]可以得到对应的内存计算公式:分配字节数 = OpInfo * 8 + 8在堆栈上分配小型区域。 分配的大小是操作信息字段 * 8 + 8,允许分配为 8 到 128 个字节。
当一个非叶子函数(Nonleaf function)执行了诸如
sub rsp, N的指令来开辟本地栈帧时,必须记录相应的展开码,以便在发生异常时,操作系统可以逆向执行这些操作以恢复调用栈(Stack Unwinding)。所以最近的一次开拓栈帧是在
7*8+8 = 64 字节 = 0x40但是
UnwindOp = 0是UWOP_PUSH_NONVOL,对应要+0x8,所以偏移是0x48 -
根据这些展开码,精确计算出当前栈帧的基址、返回地址位置以及调用者栈指针(
Child-SP)。0:000> r rsp
rsp=0000009ed26ff9a0
0:000> dqs 0000009ed26ff9a0+48 L 2
0000009e`d26ff9e8 00007ff6`ccc31e29 loader!invoke_main+0x39 [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 79]
0000009e`d26ff9f0 0000c4d3`00000001
0:000> k
# Child-SP RetAddr Call Site
00 0000009e`d26ff9a0 00007ff6`ccc31e29 loader!main+0x45 [D:\Blog\wechat\stack_spoofer\loader\src\main.cpp @ 39]
01 0000009e`d26ff9f0 00007ff6`ccc31cd2 loader!invoke_main+0x39 [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 79]
02 0000009e`d26ffa40 00007ff6`ccc31b8e loader!__scrt_common_main_seh+0x132 [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288]
03 0000009e`d26ffab0 00007ff6`ccc31ebe loader!__scrt_common_main+0xe [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 331]
04 0000009e`d26ffae0 00007ff8`1d51e8d7 loader!mainCRTStartup+0xe [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_main.cpp @ 17]
05 0000009e`d26ffb10 00007ff8`1e54c53c KERNEL32!BaseThreadInitThunk+0x17
06 0000009e`d26ffb40 00000000`00000000 ntdll!RtlUserThreadStart+0x2c得到当前函数的返回地址:
00007ff6ccc31e29,我们修改这块内存的值,发现windbg判断的返回地址已经被我们修改了0:000> eq 0000009e`d26ff9e8 0000009e`d26ff9e8
0:000> dqs 0000009ed26ff9a0+48 L 2
0000009e`d26ff9e8 0000009e`d26ff9e8
0000009e`d26ff9f0 0000c4d3`00000001
0:000> k
# Child-SP RetAddr Call Site
00 0000009e`d26ff9a0 0000009e`d26ff9e8 loader!main+0x45 [D:\Blog\wechat\stack_spoofer\loader\src\main.cpp @ 39]
01 0000009e`d26ff9f0 0000c4d3`00000001 0x0000009e`d26ff9e8
02 0000009e`d26ff9f8 00007fff`6f4924f8 0x0000c4d3`00000001这里如果修改的精细一点就可以伪造出重复栈返回地址



从而显得我们的返回地址来自系统模块,这样可以迷惑EDR使其认为调用来自于系统(至少这项技术刚出现时是这样的,堆栈欺骗的初始版本是在unkown cheats上面的老哥为了欺骗游戏反作弊而产生的,原帖地址[5])
初步“堆栈欺骗”
[2]中使用将返回地址写0来打破堆栈的追踪,他举得是一个 hook sleep 的例子
其实这个不算真正的堆栈欺骗,原作者说到
As it’s been pointed out to me, the technique here is not yet truly holding up to its name for being a stack spoofer. Since we’re merely overwriting return addresses on the thread’s stack, we’re not spoofing the remaining areas of the stack itself. Moreover we’re leaving our call stack unwindable meaking it look anomalous since the system will not be able to properly walk the entire call stack frames chain.
正如有人指出的那样,目前的这项技术还算不上名副其实的“栈欺骗”(stack spoofing)。由于我们仅仅覆盖了线程栈上的返回地址,并未对栈的其他区域进行伪造,因此欺骗并不完整。此外,这种做法导致调用栈无法正常回溯(unwind),从而显得异常,因为系统将无法正确遍历完整的调用栈帧链。
void WINAPI MySleep(DWORD _dwMilliseconds) |
按照同样的思路修改
void testMessageBox() { |

这里相当于是把返回地址清空
尝试伪造
上一步中只是截断了堆栈的回溯,这里我们要得到一个看起来完整的栈,即从 KERNEL32!BaseThreadInitThunk+0x17和 ntdll!RtlUserThreadStart+0x2c出发的伪造栈帧(stack frame)
这里由于代码重复度过高,选择改造 [7] https://github.com/susMdT/LoudSunRun
仓库的原作者是对printf、gets的函数调用和pNtAllocateVirtualMemory 的直接系统调用进行了测试,其主要测试思路就是顺着64位函数调用的传参顺序,Windows 64 位(x64)默认使用 Microsoft x64 调用约定:rcx,rdx,r8,r9,stack栈,对于伪造堆栈的话最有影响的就是多参数对栈的影响。
为了保存栈内容的相关信息,作者使用了如下结构体保存
typedef struct |
想这么做的第一步肯定是获得要伪造的两个栈底在当前的值
ReturnAddress = (PBYTE)(GetProcAddress(LoadLibraryA("kernel32.dll"), "BaseThreadInitThunk")) + 0x14; // Would walk export table but am lazy |
其中FindGadget是寻找kernel.dll这种核心模块里面能够执行jmp [rbx]寄存器的值
PVOID FindGadget(LPBYTE Module, ULONG Size) |
然后关键的来了,CalculateFunctionStackSizeWrapper基本模拟了我们刚才手动实现栈回溯的操作,先找到.pdata段的RunTimeFunction
ULONG CalculateFunctionStackSizeWrapper(PVOID ReturnAddress) |
其中RtlLookupFunctionEntry作用如下
NTSYSAPI PRUNTIME_FUNCTION RtlLookupFunctionEntry( |
ControlPc:函数中指令捆绑的虚拟地址ImageBase:函数所属的模块的基址HistoryTable:模块的全局指针值
然后调用CalculateFunctionStackSize根据其中的unwindOperation计算栈的大小
/* Credit to VulcanRaven project for the original implementation of these two*/ |
其中关于栈帧相关的内容有
typedef struct |
最后调用Spoof完成堆栈欺骗的操作但是Spoof是汇编写的,所以可能难读一些
1,保存返回值等等
Spoof proc |
2,处理按栈传参的参数,移动栈上的相关参数
; --------------------------------------------------------------------- |
3,伪造堆栈
-
先开辟足够的栈空间,并截断最底层的
RtlUserThreadStartsub rsp, 200h
push 0 -
开始伪造
RtlUserThreadStart和BaseThreadInitThunk以及选用的jmp [rbx]; ----------------------------------------------------------------------
; RtlUserThreadStart + 0x14 frame
; ----------------------------------------------------------------------
sub rsp, [rdi + 56]
mov r11, [rdi + 64]
mov [rsp], r11
; ----------------------------------------------------------------------
; BaseThreadInitThunk + 0x21 frame
; ----------------------------------------------------------------------
sub rsp, [rdi + 32]
mov r11, [rdi + 40]
mov [rsp], r11
; ----------------------------------------------------------------------
; Gadget frame
; ----------------------------------------------------------------------
sub rsp, [rdi + 48]
mov r11, [rdi + 80]
mov [rsp], r11 -
准备真实函数调用相关的参数配置
; ----------------------------------------------------------------------
; Adjusting the param struct for the fixup
; ----------------------------------------------------------------------
mov r11, rsi ; Copying function to call into r11
mov [rdi + 8], r12 ; Real return address is now moved into the "OG_retaddr" member
mov [rdi + 16], rbx ; original rbx is stored into "rbx" member
lea rbx, [fixup] ; Fixup address is moved into rbx
mov [rdi], rbx ; Fixup member now holds the address of Fixup
mov rbx, rdi ; Address of param struct (Fixup) is moved into rbx
; ----------------------------------------------------------------------
; Syscall stuff. Shouldn't affect performance even if a syscall isnt made
; ----------------------------------------------------------------------
mov r10, rcx
mov rax, [rdi + 72]
jmp r11
4,调用完成后进入收尾阶段(恢复栈帧)
fixup: |


完整demo地址:
https://github.com/Joe1sn/LearnStackSpoofing
引用
[1] https://www.cobaltstrike.com/blog/behind-the-mask-spoofing-call-stacks-dynamically-with-timers
[2] https://github.com/mgeeky/ThreadStackSpoofer
[3] https://www.bilibili.com/video/BV1hJnqzoEwc
[4] https://github.com/MicrosoftDocs/cpp-docs/blob/main/docs/build/exception-handling-x64.md




