为何Shellcode在独立可执行文件中正常运行,在C程序中执行却触发段错误?
我来帮你梳理下这个问题——即使开了gcc -z execstack还是触发段错误,大概率是Shellcode本身或者C程序的执行方式有几个容易忽略的坑,咱们一步步来排查:
1. 先检查Shellcode是不是**位置无关(PIC)**的
你单独编译链接的Shellcode程序,链接器会给它分配固定的内存地址,所以如果你的Shellcode里用了硬编码的绝对地址(比如mov rax, 0x401000这种),放到C程序里执行时,Shellcode是被加载到栈/堆的随机地址,原来的绝对地址就完全无效了,自然会触发段错误。
解决办法:
重写Shellcode为位置无关代码,64位下推荐用rip相对寻址:
- 用
lea rax, [rip+offset]来计算字符串(比如"/bin/sh")的地址,而不是硬编码; - 如果要获取当前Shellcode的地址,可以用
call next; next: pop rbx这种技巧,call指令会把下一条指令的地址压栈,pop出来就是当前位置的地址,再基于这个地址做偏移。
2. 检查Shellcode有没有NULL字节
如果你的Shellcode里包含0x00字节,用C的char数组存放时,会被当作字符串结束符截断,导致Shellcode不完整,执行到一半就出错。比如mov rax, 0会生成0x48 0xc7 0xc0 0x00 0x00 0x00 0x00,这里面有一堆NULL字节,直接废了。
解决办法:
- 用
xxd your_shellcode_program查看Shellcode的十六进制内容,排查有没有00字节; - 替换产生NULL的指令,比如把
mov rax, 0换成xor rax, rax(生成0x48 0x31 0xc0,没有NULL字节)。
3. 检查栈对齐或编译器优化问题
有些64位指令要求内存地址对齐到16字节,当Shellcode被复制到栈上时,可能刚好没对齐,导致执行指令时出错。另外,编译器的栈保护机制(比如Stack Smashing Protection)可能会干扰栈上的Shellcode。
解决办法:
- 编译C程序时加上
-fno-stack-protector关闭栈保护:gcc -z execstack -fno-stack-protector your_code.c -o test; - 给Shellcode数组加上对齐属性:
unsigned char __attribute__((aligned(16))) shellcode[] = "...";; - 或者在Shellcode开头加几个NOP指令(
0x90),比如加4个或8个,手动调整对齐。
4. 试试手动申请可执行内存代替栈执行
有时候即使开了execstack,栈的权限还是会有隐性限制(比如系统安全机制的额外管控),这时候可以手动用mmap申请一块可读可写可执行的内存,把Shellcode复制过去再执行,绕开栈的问题。
示例代码:
#include <sys/mman.h> #include <string.h> #include <stdio.h> int main() { // 替换成你的Shellcode unsigned char shellcode[] = "\x48\x31\xc0\x48\x31\xff\x48\x31\xf6\x48\x31\xd2\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05"; // 申请可执行内存 void *exec_mem = mmap(NULL, sizeof(shellcode), PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANON | MAP_PRIVATE, -1, 0); if (exec_mem == MAP_FAILED) { perror("mmap failed"); return 1; } // 复制Shellcode到可执行内存 memcpy(exec_mem, shellcode, sizeof(shellcode)); // 执行Shellcode ((void(*)())exec_mem)(); return 0; }
5. 用GDB定位具体错误
如果上面的方法都没用,直接用GDB调试C程序,看段错误到底发生在哪个指令:
gdb ./test_program run # 触发段错误后,查看当前指令 x/20i $rip # 查看调用栈 bt
这样能精准定位是Shellcode里的哪条指令出了问题,方便针对性修改。
另外,还要注意系统的安全机制,比如SELinux如果处于enforcing模式,可能会阻止栈上代码执行,可以临时用sudo setenforce 0关闭测试,确认是不是这个原因。
内容的提问来源于stack exchange,提问作者Ricardo Pereira

