全局区shellcode转函数执行为何需-z execstack编译选项?
-z execstack才能执行? 问题场景
在SMP Debian 4.19.260-1 (2022-09-29) x86_64环境下,使用gcc (Debian 8.3.0-6) 8.3.0编译这段代码:
#include <stdio.h> const char shellcode[]="\xb8\x01\x00\x00\x00\x48\xbe\x48\x65\x6c\x6c\x6f\x00\x00\x00\x56\x48\x89\xe6\xba\x05\x00\x00\x00\x0f\x05\xb8\x3c\x00\x00\x00\x0f\x05"; int main(){ (*(void (*) ()) shellcode)(); return 0; }
直接编译运行会触发SIGSEGV段错误,但添加-z execstack选项编译后能正常输出Hello。调试确认shellcode位于初始化全局区(准确说是const修饰的只读数据段.rodata),并非栈区,因此产生疑惑:为什么非栈区的代码也需要这个选项?
核心原因
内存页的W^X保护机制
现代Linux系统默认启用W^X(Write XOR Execute,写/执行互斥)安全机制,每个内存页只能具备写权限或者执行权限,不能同时拥有,以此防范代码注入类攻击。
你的shellcode被const修饰,编译后会被归类到.rodata只读数据段,这个段对应的内存页默认权限是只读、不可执行。当你强制将这段数据当作函数指针执行时,CPU尝试在不可执行的内存页上运行指令,必然触发段错误。-z execstack的实际作用
你找到的结论完全正确:-z execstack并非仅让栈页可执行,它会直接关闭整个程序的W^X保护,让所有内存页(包括.rodata、.data、栈、堆等)同时拥有可执行权限。这就使得原本不可执行的.rodata段里的shellcode能够被CPU正常执行。
更安全的替代方案
如果不想全局放开内存页的可执行权限,可以通过编译器属性把shellcode放到默认可执行的.text代码段:
#include <stdio.h> const char shellcode[] __attribute__((section(".text"))) = "\xb8\x01\x00\x00\x00\x48\xbe\x48\x65\x6c\x6c\x6f\x00\x00\x00\x56\x48\x89\xe6\xba\x05\x00\x00\x00\x0f\x05\xb8\x3c\x00\x00\x00\x0f\x05"; int main(){ (*(void (*) ()) shellcode)(); return 0; }
这样编译时无需添加-z execstack选项,shellcode也能正常运行,同时保留系统的W^X安全保护,避免全局权限放开带来的风险。
内容的提问来源于stack exchange,提问作者diciotto

