注入远程线程使用大型本地数组时目标进程崩溃的原因探究
咱们先从你的代码现象说起:当你在注入的shellcode里定义DWORD myStackArray[1000](也就是4000字节的栈空间)时,目标进程直接崩溃;但改成DWORD myStackArray[500](2000字节)就完全正常。这背后的核心原因和**栈探测(Stack Probe)**以及远程线程的特殊执行环境密切相关。
先看两种场景的函数序言差异
当你定义大型数组时,编译器生成的函数序言会主动调用__chkstk函数:
DWORD WINAPI shellcode_start(LPVOID lpParameter) { 00000001400010F0 mov qword ptr [rsp+8],rcx 00000001400010F5 push rbp 00000001400010F6 mov eax,11A0h 00000001400010FB call __chkstk (0140001210h) 0000000140001100 sub rsp,rax 0000000140001103 mov rbp,rsp ...
而定义小型数组时,编译器直接用sub rsp指令分配栈空间,完全没有__chkstk的调用:
DWORD WINAPI shellcode_start(LPVOID lpParameter) { 00000001400010F0 mov qword ptr [rsp+8],rcx 00000001400010F5 push rbp 00000001400010F6 sub rsp,8D0h 00000001400010FD mov rbp,rsp ...
为什么__chkstk会导致远程线程崩溃?
__chkstk是微软编译器用来实现栈探测的内置函数:当本地变量需要的栈空间超过默认阈值(大概4KB)时,编译器会自动插入这个调用。栈探测的作用是逐步扩展栈空间——因为栈是从高地址向低地址增长的,未提交的栈页访问会触发页错误,系统会自动提交新的栈页,但一次性跨越多个内存页的话,可能直接触发访问违规,栈探测就是为了避免这种情况。
但问题出在远程线程的执行环境:你注入的shellcode只包含了shellcode_start函数的代码,并没有携带__chkstk的实现!当远程线程执行到call __chkstk时,这个函数的地址在目标进程里根本不是有效的可执行代码(要么是未初始化的内存,要么是无关的数据),执行到这里自然会触发内存访问违规,导致进程崩溃。
而小型数组不需要栈探测,直接用sub rsp分配空间,不需要调用任何外部函数,所以能在远程进程里正常执行。
为什么添加/Gs8192链接器选项就能解决问题?
/Gs选项的作用是设置栈探测的触发阈值:/Gs8192表示当本地变量占用的栈空间超过8192字节(8KB)时,才会触发栈探测逻辑。你的DWORD myStackArray[1000]只占用4000字节,远低于这个阈值,所以编译器不再生成__chkstk的调用,而是直接用sub rsp指令分配栈空间:
0000000000400000 48 89 4C 24 08 mov qword ptr [rsp+8],rcx 0000000000400005 55 push rbp 0000000000400006 48 81 EC C0 14 00 00 sub rsp,14C0h 000000000040000D 48 8D 6C 24 20 lea rbp,[rsp+20h] 0000000000400012 48 C7 45 00 00 00 00 00 mov qword ptr [rbp],0
这样就彻底避免了调用目标进程中不存在的__chkstk函数,远程线程自然就能正常运行了。
总结一下
远程线程崩溃的根本原因可以归纳为三点:
- 大型本地数组触发了编译器的栈探测逻辑,生成了
__chkstk的调用指令 - 注入的shellcode仅包含目标函数代码,未携带
__chkstk的实现,目标进程中该地址无有效可执行代码 - 调整
/Gs阈值让编译器跳过栈探测,直接分配栈空间,从而规避了无效函数调用的问题
备注:内容来源于stack exchange,提问作者vengy

