3-回答先知上的问题:影子空间有必要吗?
1、问题

佬,我调试的时候发现这里的sub rsp,20h是不是没有必要啊,jmp rax进api后,api会自己sub和add,也没有保存这四个寄存器的值

然后后面也没有再add rsp,20h回来,我跟完一个GetProcAddressByHash后,调用前和调用后的堆栈是不相等的我汇编不是很熟,可能理解的不对😣
以上是先知社区上 Windows Shellcode开发(x64 stager)-先知社区某位师傅的疑问,可能我我文章写的不够明白,所以有必要在这里解释一下。
2、解释
为什么要预留20h(32)字节的影子空间?
这里说一下有歧义的点:一般来说影子空间是预留存放API的4个64 位寄存器参数寄存器RCX、RDX、R8、R9 ,调用方分配了32字节的影子空间,被调用方一定要用吗?其实“一般”这个修饰词就暗示了影子空间的作用,也就是被调用方可用可不用
我查阅了网上很多资料,最权威的解释肯定是微软官方文档x64 调用约定 | Microsoft Learn


ok,我们提取上面两个图对于影子空间的解释:调用者必须在CALL前给足32B影子空间;被调者把它当成自己的临时草稿纸,可随意乱写,用完不必还原,调用者最后统一收拾。
我就以MessageBoxA举例
#include <windows.h>
int main(void)
{
MessageBoxA(
NULL, /* 父窗口句柄,无父窗口填 NULL */
"Hello, Win32 MessageBoxA!",/* 提示文本 */
"Demo", /* 标题栏文本 */
MB_OK | MB_ICONINFORMATION /* 按钮与图标风格 */
);
return 0;
}下断点,如下图。

我们继续跟进 MessageBoxA 的内部,来到跟你给的图差不多相同的汇编代码,这里的 MessageBoxA 的实现内部,MessageBoxA 其实也不是最终实现,它最后还会调用 MessageBoxTimeoutA。可以看到 MessageBoxA 并未使用调用方提供的影子空间,而是自己又分配了38h字节的栈空间,这个栈空间的作用看下文。

我们主要看与栈相关的寄存器,也就是RSP,可以看到汇编代码中是有
①sub rsp,38h
……
②or dword ptr [rsp+28h],0FFFFFFFFh
……
③word ptr [rsp+20h],r11w
……
④add rsp,38h① sub rsp,38h :我们来看看MessageBoxTimeoutA的函数声明,就能明白为什么会是分配38h字节的栈空间了。
int WINAPI MessageBoxTimeoutA(
IN HWND hWnd,
IN LPCSTR lpText,
IN LPCSTR lpCaption,
IN UINT uType,
IN WORD wLanguageId,
IN DWORD dwMilliseconds);其中前4个参数就是MessageBoxA的4个参数,而后面是第5个参数是wLanguageId,第6个参数是dwMilliseconds。按照前面对影子空间的解释,我们需要给前4个参数预留20h字节的影子空间;按照windows x64调用约定,后面的参数要存放在栈上,故分配38h=20h(影子空间)+10h(存放后续参数)+8(对齐用的),就如下图所示。有两种不同的RSP视角,一种是以原本RSP,比如说rsp-8,另一种是分配栈空间的RSP,比如说rsp+8。我假设 MessageBoxTimeoutA 的内部需要将rcx、rdx、r8、r9的值转储到影子空间,得到下图栈分配图。

⚠注意:为什么要分配8字节用于对齐,这主要是Windows x64调用约定的,即在调用(call)之前,rsp必须保证位16字节对齐,即以0结尾,这是为了使返回地址存放在地址以8结尾的栈区域,这个我在非PEB获取ntdll和kernel32模块基址的精妙之道-先知社区说过。
② or dword ptr [rsp+28h],0FFFFFFFFh:就是将第6个参数存放到栈上

③ word ptr [rsp+20h],r11w :将第5个参数存放到栈上

④ add rsp,38h:按照约定Windows x64调用约定,谁分配了栈空间谁来清理,保证调用前后 rsp 一致,之前rsp-38h,函数返回前rsp+38h,保持rsp平衡。
我们继续跟进到 MessageBoxTimeoutA 的内部,请看下图,这是进入到内部后的初始栈空间布局。

观察涉及栈的操作,MessageBoxTimeoutA 并未将影子空间存储4个参数寄存器,而是用来存储rbx、rbp、rsi这三个非易失性寄存器。

可以看到 mov rax,rsp,这意味着rax就是rsp指针
00007FFF0424ADC0 48 8B C4 mov rax,rsp
00007FFF0424ADC3 48 89 58 08 mov qword ptr [rax+8],rbx
00007FFF0424ADC7 48 89 68 18 mov qword ptr [rax+18h],rbp
00007FFF0424ADCB 48 89 70 20 mov qword ptr [rax+20h],rsi
00007FFF0424ADCF 57 push rdi
00007FFF0424ADD0 41 54 push r12
00007FFF0424ADD2 41 56 push r14
00007FFF0424ADD4 48 83 EC 40 sub rsp,40h我们截取关键的,继续看下面的汇编代码,这个代码主要是存放非易失性寄存器,这没问题。
00007FFF0424ADC3 48 89 58 08 mov qword ptr [rax+8],rbx
00007FFF0424ADC7 48 89 68 18 mov qword ptr [rax+18h],rbp
00007FFF0424ADCB 48 89 70 20 mov qword ptr [rax+20h],rsi 这里就很好的回答了我上文提到的:影子空间不是“必须”放参数,而是“预留”,被调函数可存可不用,随意怎么用,但始终存在
这么做的前提是:RCX、RDX、R8、R9存放的值不能丢失,无论你是存放在自己开辟的栈空间还是存放在其他寄存器上,参数值是一定不能丢失的。
验证一下rcx为 0000000000000000 这没问题,rdx存放"Hello, Win32 MessageBoxA!"字符串的地址

r8存放"Demo"的地址,r9的值为0000000000000040

我们再以其他API函数来举例:WSAStartup

WSASocketA 调用前

调用后,进入内部。

又举例个例子,我不分配影子空间会怎么样,使用 Linux Shellcode开发(Stager & Reverse Shell)-先知社区提供的windows 反弹shell的代码。修改 GetProcAddressByHash 的一处代码。不分配20h字节的影子空间。

在 3.调用WSAStartup函数 下断点,如下图

上一步成功断下来后,在 GetProcAddressByHash 的下面断一个断点

停在这个断点后,步进,进入到 WSAStartup 内部,如下

由于我们只分配了10h字节的影子空间,如果要使用rsp+8,rsp+10h的栈空间,这没问题,可是rsp+18h,rsp+20h我并为分配给被调用函数,所以调用者的栈上的值被覆盖,比如说覆盖返回地址,覆盖某个变量等等,最终导致程序出错。
我举的这个例子并没有报错,而是不能成功反弹shell。如果各位师傅还想尝试,可以不分配影子空间,这样程序会运行后出错,我就不举例说明了。
这一切的一切都源于 x64 调用约定 | Microsoft Learn,请不懂的师傅查阅相关文档。这是一篇意识流的解答性文章,如果还不懂也可以跟我交流一下。