C程序存储多段shellcode段错误原因及函数传参调用方法咨询
C语言Shellcode相关问题技术解答
问题1:同程序放置多段Shellcode触发段错误的原因、多段独立存储方案
故障底层原理
你遇到的段错误和内存区域执行权限无关——编译时加了-zexecstack参数后,整个栈内存的权限都是可读、可写、可执行的,两段栈上存储的Shellcode内存权限完全满足执行要求,故障本质是两个底层逻辑冲突:
- 栈局部变量的偏移变化:你定义的
unsigned char shellcode[]是栈上局部数组,编译器会按照变量定义顺序在当前栈帧上依次分配空间。新增第二个Shellcode数组时,原数组相对于栈帧基址(RBP/EBP)的偏移会发生变化,如果你使用的Shellcode没有做严格的位置无关处理,硬编码了固定栈偏移来访问数据/栈空间,寻址时就会访问到非法地址,触发段错误。 - 栈对齐规则被打乱:编译器分配栈空间时会自动插入填充字节满足栈对齐要求,新增局部数组后,原Shellcode数组的首地址对齐值可能发生变化。如果你的Shellcode包含对齐要求的指令(比如SSE/AVX指令要求16字节地址对齐),执行时会直接触发对齐异常。
多段独立存储实现方案
该需求完全可实现,不需要同时运行多段的场景下没有任何技术障碍,以下三种方案都能实现多段Shellcode独立存储、互不干扰、按需选择调用:
- 方案1:存放在代码段(.text)。将Shellcode定义为全局const数组,编译时会被直接分配到.text段的独立固定偏移位置,段本身默认带可执行权限,不需要依赖
-zexecstack参数,各段Shellcode地址完全独立不会互相干扰。示例代码片段:
// 两段Shellcode独立存放在.text段,编译期确定地址 const unsigned char sc1[] = "\x01\x02<SHELLCODE1>"; const unsigned char sc2[] = "\x01\x02<SHELLCODE2>"; int main() { int (*ret)() = NULL; // 按需选择调用目标 ret = (int(*)())sc1; // ret(); // 切换调用目标直接修改指针指向即可 // ret = (int(*)())sc2; return 0; }
编译时添加-fno-stack-protector -Wl,--omagic参数即可。
- 方案2:独立可执行内存页存储。调用
mmap为每段Shellcode单独申请带PROT_READ|PROT_WRITE|PROT_EXEC权限的独立内存页,将Shellcode拷贝到对应内存页后再调用。该方案下各段Shellcode所在内存页完全独立,地址不相邻,不会出现偏移、对齐干扰,是兼容性最高的方案。 - 方案3:栈上对齐存储。如果一定要将Shellcode存在栈上,给每个Shellcode数组添加GCC对齐修饰
__attribute__((aligned(16))),同时确保使用的Shellcode是严格位置无关编码,不依赖固定栈偏移寻址,也可以正常运行。
问题2:Shellcode作为函数参数传入的正确实现方式
C语言中数组作为函数参数传递时会自动退化为指向数组首地址的指针,不会发生全数组拷贝,本身不会破坏Shellcode内容,正确实现只需要遵循三个规则:
- 形参使用无符号字符指针类型,同时传入Shellcode实际长度,避免用固定长度数组形参引发长度判断错误
- 调用前必须确认Shellcode所在内存区域具备可执行权限,通用场景下建议申请独立可执行内存拷贝Shellcode后再调用,避免受编译选项、段权限限制
- 调用前将地址强转为匹配的函数指针类型,不要直接将数组地址作为函数调用
对应可运行的实现代码如下:
#include <stdio.h> #include <string.h> #include <sys/mman.h> // 定义函数指针类型,和Shellcode返回值、参数匹配 typedef int (*sc_func_t)(); const unsigned char shellcode[] = "\x01\x02..."; void call_shellcode(const unsigned char* sc, size_t sc_len) { // 打印Shellcode信息 printf("Shellcode addr: %p, len: %zu\nHex content: ", sc, sc_len); for (size_t i = 0; i < sc_len; i++) { printf("\\x%02x", sc[i]); } printf("\n"); // 通用执行逻辑:申请独立可执行内存,兼容所有内存段的Shellcode void* exec_mem = mmap(NULL, sc_len, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); memcpy(exec_mem, sc, sc_len); sc_func_t ret = (sc_func_t)exec_mem; // 执行Shellcode // ret(); // 执行完成后释放内存 munmap(exec_mem, sc_len); } int main() { // 传入Shellcode首地址和实际长度,减1是去掉字符串末尾自动追加的\0 call_shellcode(shellcode, sizeof(shellcode) - 1); return 0; }
提示:如果Shellcode是开了
-zexecstack编译的栈上局部数组,可以跳过mmap拷贝逻辑,直接强转指针调用,但独立内存页的方案兼容性最好,不受编译参数、部署环境影响。
内容的提问来源于stack exchange,提问作者asmquestions
相关产品推荐
相关产品推荐

