Linux x86_64下Shellcode调用dlopen触发SIGSEGV问题求助
问题解决:Shellcode调用dlopen触发SIGSEGV地址边界错误
你要实现无libc依赖的Shellcode加载自定义库,但一直触发SIGSEGV (Address boundary error),核心问题出在指令格式错误、栈对齐、CET指令干扰和执行内存选择这几个点上,下面是具体修复方案:
核心错误分析
call指令的占位符替换逻辑错误:x86-64的call rel32是相对偏移调用,你直接把6字节占位符替换成8字节绝对地址,会破坏指令对齐,导致CPU执行非法指令。endbr64指令干扰:这个是CET安全机制的指令,在mmap的匿名可执行页中执行时会触发验证失败,直接删掉就行。- 栈未对齐:x86-64要求函数调用前栈必须是16字节对齐,你的代码里
sub rsp,16后栈指针没有满足对齐要求,调用dlopen时会出错。 - 执行栈上的Shellcode:你最后直接调用栈上的
shellcode数组,即使开了-z execstack,栈内存的权限和对齐也不如mmap的匿名页稳定。
分步修复方案
1. 修正call指令为寄存器间接调用
把原来的相对call改成先加载dlopen地址到rax,再call rax,这样可以直接替换绝对地址,不用计算偏移:
- 原汇编中的
call dlopen改成:mov rax, 0xDEADBEEFDEADBEEF ; 8字节占位符 call rax - 对应Shellcode中的字节改成:
"\x48\xb8XXXXXXXX\xff\xd0"(XXXXXXXX是8字节占位符)
2. 删除endbr64指令
直接删掉汇编中的endbr64语句,以及Shellcode中对应的"\xf3\x0f\x1e\xfa"字节。
3. 强制栈16字节对齐
在进入main后添加and rsp, ~0xf指令,确保栈指针是16字节的倍数:
main: push rbp mov rbp, rsp and rsp, ~0xf ; 强制栈对齐到16字节 sub rsp, 16
4. 执行mmap后的可执行内存
把void (*func)() = shellcode;改成void (*func)() = mem;,调用mmap分配的可执行内存,避免栈内存的潜在问题。
5. 修正占位符替换逻辑
原代码中遍历前先自增指针会跳过第一个字节,改成从shellcode起始位置开始遍历,并且用8字节占位符(比如"XXXXXXXX")来匹配,确保替换的是完整的8字节地址:
for (size_t i = 0; i < sizeof(shellcode) - 8; i++) { if (memcmp(&shellcode[i], "XXXXXXXX", 8) == 0) { memcpy(&shellcode[i], &addrOfDlopen, sizeof(uint64_t)); break; } }
修复后的完整代码
生成Shellcode用的汇编(main.S)
BITS 64 global _start section .text _start: call main main: push rbp mov rbp, rsp and rsp, ~0xf ; 强制栈16字节对齐 sub rsp, 16 mov esi, 2 ; RTLD_NOW | RTLD_GLOBAL lea rax, [rel str_lib] mov rdi, rax mov rax, 0xDEADBEEFDEADBEEF ; 8字节占位符 call rax mov eax, 60 xor edi, edi syscall section .data str_lib db '/home/nitanmarcel/shell/example/libhello.so', 0
Shellcode加载器C代码
#include <stdint.h> #include <string.h> #include <stdio.h> #include <dlfcn.h> #include <sys/mman.h> #include <stdlib.h> int main() { char shellcode[] = "\xe8\x00\x00\x00\x00" // call main "\x55" // push rbp "\x48\x89\xe5" // mov rbp, rsp "\x48\x83\xe4\xf0" // and rsp, ~0xf "\x48\x83\xec\x10" // sub rsp, 0x10 "\xbe\x02\x00\x00\x00" // mov esi, 2 "\x48\x8d\x05\x10\x00\x00\x00" // lea rax, [rip + 0x10] "\x48\x89\xc7" // mov rdi, rax "\x48\xb8" // mov rax, 0x0000000000000000 "XXXXXXXX" // 8字节占位符 "\xff\xd0" // call rax "\xb8\x3c\x00\x00\x00" // mov eax, 0x3c "\x31\xff" // xor edi, edi "\x0f\x05" // syscall "\x00\x2f\x68\x6f\x6d\x65\x2f\x6e" // 字符串:/home/nitanmarcel/... "\x69\x74\x61\x6e\x6d\x61\x72\x63" "\x65\x6c\x2f\x73\x68\x65\x6c\x6c" "\x2f\x65\x78\x61\x6d\x70\x6c\x65" "\x2f\x6c\x69\x62\x68\x65\x6c\x6c" "\x6f\x2e\x73\x6f\x00"; uint64_t addrOfDlopen = (uint64_t)dlopen; // 寻找并替换8字节占位符 for (size_t i = 0; i < sizeof(shellcode) - 8; i++) { if (memcmp(&shellcode[i], "XXXXXXXX", 8) == 0) { printf("dlopen @%llx\n", addrOfDlopen); memcpy(&shellcode[i], &addrOfDlopen, sizeof(uint64_t)); break; } } void *mem = mmap(NULL, sizeof(shellcode), PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (mem == MAP_FAILED) { perror("mmap"); exit(1); } memcpy(mem, shellcode, sizeof(shellcode)); if (mprotect(mem, sizeof(shellcode), PROT_READ | PROT_EXEC) == -1) { perror("mprotect"); exit(1); } void (*func)() = mem; // 执行mmap后的可执行内存 func(); munmap(mem, sizeof(shellcode)); return 0; }
编译与运行
编译加载器时需要链接dl库并开启可执行栈:
gcc loader.c -o loader -ldl -z execstack
调试时可以关闭地址随机化:
sudo sysctl -w kernel.randomize_va_space=0
内容的提问来源于stack exchange,提问作者Marcel Alexandru
相关产品推荐
相关产品推荐

