Windows x86-64 Shellcode中栈上字符串调用printf的异常原因探究
在Windows 10 x64环境中,尝试调用printf("String: %s\n", "Hello World")并将字符串"Hello World"存在栈上时,发现只有当字符串存放在距rsp至少32字节的位置时才能正常输出,否则会出现乱码或无输出。已了解x64 ABI的16字节对齐要求,但无法解释该现象。
可行代码示例:
bits 64 default rel segment .data msg db "String: %s", 0xd, 0xa, 0 segment .text global main extern ExitProcess extern printf main: push rbp mov rbp, rsp sub rsp, 64 ; 调整栈大小(Number 1) mov rdx, rsp add rdx, 32 ; 字符串在栈上的偏移(Number 2) mov dword [rdx], "Hell" mov dword [rdx + 4], "o Wo" mov dword [rdx + 8], "rld" lea rcx, [msg] call printf xor rcx, rcx call ExitProcess
测试结果表格:
| Number 1 | Number 2 | 是否可行? | 输出结果 |
|---|---|---|---|
| 64 | 0 | 否 | |
| 64 | 8 | 否 | x¨/(☻ |
| 64 | 16 | 否 | Pf{tè☻ |
| 64 | 24 | 否 | Ϩ;í |
| 64 | 32 | 是 | Hello World |
| 64 | 40 | 是 | Hello World |
| 64 | 48 | 是 | Hello World |
| 64 | 56 | 是 | Hello World |
| 64 | 64 | 是 | Hello World |
| 32 | 0 | 否 | |
| 32 | 8 | 否 | °²øRg |
| 32 | 16 | 否 | ákG×│☺ |
| 32 | 24 | 否 | ↑°wd¢ |
| 32 | 32 | 是 | Hello World |
你遇到的问题核心是Windows x64 ABI强制要求的32字节影子空间,和16字节对齐是两个独立规则:
影子空间的定义
Windows x64规定,调用任何函数时,必须在栈上预留32字节的"影子空间"(也叫寄存器参数区)。这块区域是给被调用函数用的,用来保存前4个寄存器参数(rcx、rdx、r8、r9)的值——很多C标准库函数(比如printf)会无条件将这四个寄存器的值写入到这块栈空间中。你的代码中影子空间的位置
在main函数执行sub rsp, 64后,rsp指向栈帧的起始位置。此时,从rsp到rsp+31的32字节区域,就是你为调用printf预留的影子空间。当你把字符串存在rsp+0到rsp+24的位置时,这些数据正好落在影子空间范围内。乱码的本质
当printf被调用后,它会立即将rcx(指向msg的指针)、rdx(指向栈上字符串的指针)等寄存器的值写入影子空间,直接覆盖了你存在这里的"Hello World"字符串。等printf去读取rdx指向的地址时,原来的字符串已经被破坏,自然输出乱码或无内容。偏移32及以上正常的原因
当字符串存放在rsp+32及以上的位置时,这块区域不在影子空间范围内(影子空间是rsp到rsp+31),不会被printf写入寄存器值的操作覆盖,所以字符串能保持完整,printf可以正确读取输出。
内容的提问来源于stack exchange,提问作者Lumen

