文章总结: 本文档详述跨平台二进制加载器自举寻址技术。针对Windows阐述基于PEB遍历模块解析导出表的方法,针对Linux分析利用AuxiliaryVector与link_map获取符号地址的机制。文中对比标准与无库实现,提供地址修正逻辑与安全增强措施,并总结实战建议与Linux极简加载方案。 综合评分: 90 文章分类: 二进制安全,恶意软件,安全工具,逆向分析,实战经验
跨平台二进制loader加载器自举寻址技术比对
原创
haidragon haidragon
安全狗的自我修养
2026年3月12日 12:33 湖南
官网:http://securitytech.cc
#
#
跨平台自举寻址技术说明文档
1. 核心背景
在开发高级 Loader、无文件攻击(Fileless)或安全研究工具时,程序往往无法预先知道系统库(如 Windows 的 kernel32.dll 或 Linux 的 libc.so)在内存中的确切位置。
技术挑战:
- ASLR(Address Space Layout Randomization):现代操作系统在每次程序运行时随机化库文件的加载地址
- 位置无关代码(PIC):shellcode 和反射式 DLL 需要在任意内存位置执行
- 无文件场景:不依赖文件系统,直接从内存中解析 API 地址
解决方案:
- Windows:依赖 PEB (Process Environment Block) 结构
- Linux:依赖 Auxiliary Vector (auxv) 与 link_map 链表
2. Windows 寻址:基于 PEB 的链表爬行
2.1 PEB 结构概述
Windows 所有的模块信息都挂载在 PEB 下的 Ldr 成员中。开发者通过段寄存器(FS 或 GS)定位 PEB,随后遍历双向链表。
核心数据结构:
1. // PEB 结构(简化)
2. typedefstruct _PEB {
3. BYTE Reserved[0x18];
4. PVOID ImageBaseAddress;
5. PPEB_LDR_DATA Ldr;// 关键:加载器数据
6. // ...
7. } PEB,*PPEB;
9. // LDR 数据结构
10. typedefstruct _PEB_LDR_DATA {
11. LIST_ENTRY InLoadOrderModuleList;
12. LIST_ENTRY InMemoryOrderModuleList;// 常用
13. LIST_ENTRY InInitializationOrderModuleList;
14. } PEB_LDR_DATA,*PPEB_LDR_DATA;
16. // 模块表项
17. typedefstruct _LDR_DATA_TABLE_ENTRY {
18. LIST_ENTRY InMemoryOrderLinks;
19. PVOID DllBase;// 模块基址
20. PVOID EntryPoint;
21. SIZE_T SizeOfImage;
22. UNICODE_STRING FullDllName;// 完整路径
23. UNICODE_STRING BaseDllName;// 文件名
24. } LDR_DATA_TABLE_ENTRY,*PLDR_DATA_TABLE_ENTRY;
2.2 访问流程
步骤 1:定位 PEB
1. ; x64 架构
2. mov rax, gs:[0x60]; PEB 偏移量为0x60
3. ; x86 架构
4. mov eax, fs:[0x30]; PEB 偏移量为0x30
步骤 2:遍历模块链表
1. PPEB peb =(PPEB)__readgsqword(0x60);
2. PLDR_DATA_TABLE_ENTRY entry =
3. (PLDR_DATA_TABLE_ENTRY)peb->Ldr->InMemoryOrderModuleList.Flink;
5. do{
6. printf("Module: %wZ, Base: %p\n",
7. &entry->BaseDllName, entry->DllBase);
8. entry =(PLDR_DATA_TABLE_ENTRY)entry->InMemoryOrderLinks.Flink;
9. }while(entry != first_entry);
步骤 3:解析导出表获取 DllBase 后,按照 PE 格式解析导出目录:
1. PIMAGE_DOS_HEADER dos =(PIMAGE_DOS_HEADER)DllBase;
2. PIMAGE_NT_HEADERS nt =(PIMAGE_NT_HEADERS)((BYTE*)dos + dos->e_lfanew);
3. PIMAGE_EXPORT_DIRECTORY exp =(PIMAGE_EXPORT_DIRECTORY)(
4. (BYTE*)DllBase+ nt->OptionalHeader.DataDirectory[0].VirtualAddress);
6. // 遍历 ExportNameTable 查找函数
7. PDWORD names =(PDWORD)((BYTE*)DllBase+ exp->AddressOfNames);
8. for(DWORD i =0; i < exp->NumberOfNames; i++){
9. char* func_name =(char*)((BYTE*)DllBase+ names[i]);
10. // 匹配目标函数名...
11. }
2.3 技术特点
| 特性 | 说明 | | — | — | | 可靠性 | PEB 是 Windows 内部结构,稳定性高 | | 隐蔽性 | 不触发 API 调用,避免 Hook 检测 | | 兼容性 | 适用于所有 Windows NT 系列系统 | | 检测风险 | EDR 可能监控 PEB 访问模式 |
3. Linux 寻址:基于 auxv 与 link_map
Linux 提供两种主要方式获取模块信息:Auxiliary Vector 和 link_map 链表。
3.1 方法一:Auxiliary Vector(辅助向量)
auxv 结构:
1. typedefstruct{
2. Elf64_Word a_type;// 类型标识
3. union{
4. Elf64_Xword a_val;
5. void*a_ptr;
6. } a_un;
7. }Elf64_auxv_t;
9. // 关键类型
10. #define AT_PHDR 3// Program Header 表地址
11. #define AT_PHNUM 4// Program Header 数量
12. #define AT_BASE 7// 解释器基址(动态链接器)
13. #define AT_ENTRY 9// 程序入口点
访问流程(见 test2.c):
1. // 从栈上获取 auxv
2. size_t argc =*sp;
3. size_t*ptr = sp +1+ argc +1;// 跳过 argv 和环境变量
4. while(*ptr !=0) ptr++;// 跳过环境变量
5. ptr++;// 现在指向 auxv
7. Elf64_auxv_t*auxv =(Elf64_auxv_t*)ptr;
8. for(; auxv->a_type != AT_NULL; auxv++){
9. if(auxv->a_type == AT_PHDR)
10. phdr_v = auxv->a_un.a_val;
11. }
优点:
- 不依赖任何库函数(nostdlib 友好)
- 直接从内核获取信息
- 适合 shellcode 和极简环境
3.2 方法二:link_map 链表(推荐)
核心结构:
1. struct link_map {
2. ElfW(Addr) l_addr;// 模块加载基址
3. char*l_name;// 模块名称
4. ElfW(Dyn)*l_ld;// 动态段指针
5. struct link_map *l_next;// 下一个模块
6. struct link_map *l_prev;// 上一个模块
7. // ...
8. };
10. struct r_debug {
11. int r_version;
12. struct link_map *r_map;// 模块链表头
13. ElfW(Addr) r_brk;
14. // ...
15. };
访问流程(见 test.c):
1. // 1. 从 _DYNAMIC 段找到 DT_DEBUG
2. ElfW(Dyn)*dyn = _DYNAMIC;
3. for(; dyn->d_tag != DT_NULL;++dyn){
4. if(dyn->d_tag == DT_DEBUG){
5. debug_struct =(struct r_debug *)dyn->d_un.d_ptr;
6. break;
7. }
8. }
10. // 2. 遍历 link_map 链表
11. struct link_map *map= debug_struct->r_map;
12. while(map){
13. printf("MODULE: %s, BASE: 0x%lx\n",map->l_name,map->l_addr);
14. dump_module_symbols(map);
15. map=map->l_next;
16. }
3.3 符号表解析(核心修复逻辑)
动态段解析:
1. ElfW(Dyn)*dyn =map->l_ld;
2. for(; dyn && dyn->d_tag != DT_NULL;++dyn){
3. unsignedlong val = dyn->d_un.d_ptr;
5. // 关键:地址修正逻辑
6. // 如果 val 小于基址,说明是相对偏移,需要加上基址
7. // 对于主程序 (l_addr=0),特殊处理小地址
8. unsignedlong addr = val;
9. if(val <map->l_addr ||(map->l_addr ==0&& val <0x1000000)){
10. addr =map->l_addr + val;// 相对地址转绝对地址
11. }
13. if(dyn->d_tag == DT_SYMTAB)
14. symtab =(ElfW(Sym)*)addr;
15. if(dyn->d_tag == DT_STRTAB)
16. strtab =(char*)addr;
17. if(dyn->d_tag == DT_HASH)
18. hashtab =(ElfW(Word)*)addr;
19. }
符号过滤与地址计算:
1. for(size_t i =0; i < num_symbols; i++){
2. ElfW(Sym)*sym =&symtab[i];
4. // 只处理函数类型且有地址的符号
5. if(sym->st_value !=0&& ELF64_ST_TYPE(sym->st_info)== STT_FUNC){
6. constchar*name = safe_get_str(strtab, sym->st_name);
7. if(name){
8. // 计算绝对地址:基址 + 偏移
9. unsignedlong func_addr =map->l_addr + sym->st_value;
10. printf(" -> 0x%016lx | %s\n", func_addr, name);
11. }
12. }
13. }
3.4 安全增强措施
防止非法内存访问:
1. constchar*safe_get_str(constchar*strtab,ElfW(Word) index)
2. {
3. if(!strtab || index ==0)
4. return NULL;
5. // 不要访问 0x10000 以下的地址(保护页)
6. if((size_t)(strtab + index)<0x10000)
7. return NULL;
8. return strtab + index;
9. }
防止无限循环:
1. size_t num_symbols = hashtab[1];
2. if(num_symbols >5000)
3. num_symbols =5000;// 强制上限
4. 实战对比:test.c vs test2.c
4.1 test.c – 标准实现
特点:
- 使用标准 C 库(printf 等)
- 依赖
_DYNAMIC外部符号 - 完善的错误处理和边界检查
- 适合常规安全工具开发
编译命令:
1. gcc ./test.c -o test
输出示例:
1. ======================================================================
2. MODULE:/lib/x86_64-linux-gnu/libc.so.6
3. BASE :0x7877ec400000
4. ======================================================================
5. [Symbolsfor/lib/x86_64-linux-gnu/libc.so.6]
6. ->0x00007877ec48ef70| fgetc
7. ->0x00007877ec49a3f0| pthread_attr_setscope
8. ...
4.2 test2.c – 无库实现(Shellcode 友好)
特点:
-nostdlib编译,零依赖- 自定义 syscall 封装(
my_syscall3) - 自定义字符串函数(
m_strcmp,sys_print) - 通过 auxv 定位主程序,再爬 link_map
- 适合 shellcode 和反射式加载
编译命令:
1. gcc test2.c -o test -nostdlib -fPIC -fno-stack-protector -lc
关键差异:
1. // 自定义 syscall
2. staticinlinelong my_syscall3(long n,long a1,long a2,long a3)
3. {
4. long ret;
5. __asm__ volatile("syscall"
6. :"=a"(ret)
7. :"a"(n),"D"(a1),"S"(a2),"d"(a3)
8. :"rcx","r11","memory");
9. return ret;
10. }
12. // 汇编入口点(绕过 CRT)
13. __asm__(
14. ".global _start\n"
15. "_start:\n"
16. "mov %rsp, %rdi\n"// 传递栈指针给 c_start
17. "and $-16, %rsp\n"// 栈对齐
18. "sub $8, %rsp\n"
19. "call c_start\n");
5. 跨平台抽象对比
| 特性 | Windows (PEB) | Linux (link_map) |
| — | — | — |
| 入口点 | GS:[0x60] (x64) | _DYNAMIC 或 auxv |
| 核心结构 | PEB_LDR_DATA | structr_debug |
| 模块链表 | InMemoryOrderModuleList | link_map->l_next |
| 基址字段 | DllBase | l_addr |
| 名称字段 | BaseDllName (UNICODE) | l_name (char*) |
| 符号表 | PE Export Directory | ELF .dynsym |
| 地址计算 | Base+RVA | l_addr+st_value |
6. 应用场景
6.1 高级 Loader 开发
1. // 伪代码示例
2. void* resolve_function(constchar* module_name,constchar* func_name){
3. #ifdef _WIN32
4. PPEB peb = get_peb();
5. void* module_base = find_module_by_name(peb, module_name);
6. return get_export_addr(module_base, func_name);
7. #else
8. struct link_map*map= get_link_map();
9. void* module_base = find_module_by_name(map, module_name);
10. return get_dynsym_addr(module_base, func_name);
11. #endif
12. }
6.2 无文件攻击模拟
- 阶段 1:通过上述技术解析
GetProcAddress/dlsym - 阶段 2:动态获取所需 API 地址
- 阶段 3:执行 payload,不落地文件
6.3 安全研究工具
- API Hook 检测:对比 PEB/link_map 与实际调用地址
- 内存扫描:遍历所有加载模块,搜索特征码
- 反调试:检测调试器注入的异常模块
7. 注意事项与最佳实践
7.1 地址修正陷阱
问题: 动态段中的地址可能是相对偏移或绝对地址
解决(test.c/test2.c 通用逻辑):
1. unsignedlong addr = val;
2. if(val <map->l_addr ||(map->l_addr ==0&& val <0x1000000)){
3. addr =map->l_addr + val;// 相对地址转绝对
4. }
7.2 主程序特殊情况
- 主可执行文件的
l_addr通常为 0 - 此时 small addresses (< 0x1000000) 应视为相对偏移
7.3 虚拟模块排除
1. // linux-vdso.so 是虚拟模块,没有实际文件映射
2. if(map->l_name && strstr(map->l_name,"linux-vdso.so"))
3. return;// 跳过或特殊处理
7.4 符号表损坏防护
1. // 限制最大符号数
2. if(num_symbols >5000)
3. num_symbols =5000;
5. // 指针合法性检查
6. if((size_t)(strtab + index)<0x10000)
7. return NULL;
8. 总结
Windows PEB 方案:
- ✅ 成熟稳定,文档丰富
- ✅ 适用于所有 NT 系统
- ⚠️ 容易被 EDR 监控
Linux link_map 方案:
- ✅ 内核维护,信息准确
- ✅ 支持 nostdlib 环境
- ✅ 适合 shellcode 开发
- ⚠️ 需注意地址修正逻辑
实战建议:
- 常规工具:使用 test.c 方式(标准 C 库)
- Shellcode:参考 test2.c(nostdlib + auxv)
- 跨平台:抽象统一的 API 解析接口
- 安全性:始终进行边界检查和指针验证
9. Linux 极简方案:syscall 自 loader(推荐)
9.1 为什么 Linux 不需要这么麻烦?
与 Windows 复杂的 PEB 结构不同,Linux 的 syscall 接口极其简洁。实际上,你根本不需要解析 auxv 或 link_map,直接使用 syscall() 加载内存并执行即可。
核心优势:
- ✅ 无需符号解析:直接调用 syscall,不依赖库函数地址
- ✅ 零依赖:不需要 libc,不需要动态链接器
- ✅ 代码极简:几行汇编 + 几个 syscall 即可完成
- ✅ 内核原生支持:syscall 是稳定接口,不会随发行版变化
9.2 syscall 自 loader 实现
最小化示例(纯汇编):
1. # shellcode_loader.s
2. .global _start
4. _start:
5. # 1. 直接从栈上获取参数(argc, argv, envp, auxv)
6. mov %rsp,%rdi
8. # 2. 调用内核 syscall(例如:mmap 分配内存)
9. mov $9,%rax # SYS_mmap
10. mov $0,%rdi # addr (0 = 让内核选择)
11. mov $4096,%rsi # length (4KB)
12. mov $3,%rdx # prot (PROT_READ | PROT_WRITE)
13. mov $0x22,%r10 # flags (MAP_PRIVATE | MAP_ANONYMOUS)
14. mov $-1,%r8 # fd (-1 = 匿名映射)
15. mov $0,%r9 # offset
16. syscall
18. # rax 现在包含分配的内存地址
19. # 可以直接写入 shellcode 并跳转执行
20. mov $payload,%rsi
21. mov %rsi,(%rax)# 复制 payload 到新内存
23. # 3. 修改内存权限为可执行
24. mov $10,%rax # SYS_mprotect
25. mov %rax,%rdi # addr
26. mov $4096,%rsi # length
27. mov $7,%rdx # prot (READ|WRITE|EXEC)
28. syscall
30. # 4. 跳转到 shellcode 执行
31. call *%rax
33. payload:
34. # 这里放置你的 shellcode
35. # 例如:execve("/bin/sh", ...)
编译与运行:
1. gcc -nostdlib -fPIC shellcode_loader.s -o loader
2. ./loader
9.3 C 语言封装版本(更实用)
1. // minimal_loader.c
2. #include<sys/syscall.h>
3. #include<unistd.h>
5. // 自定义 syscall 包装器(不依赖 libc)
6. staticinlinelong sys_mmap(void*addr,size_t len,int prot,int flags,int fd,off_t offset){
7. long ret;
8. __asm__ volatile(
9. "mov $9, %%rax\n\t"
10. "syscall\n\t"
11. :"=a"(ret)
12. :"D"(addr),"S"(len),"d"(prot),"r"(flags),"r"(fd),"r"(offset)
13. :"rcx","r11","memory"
14. );
15. return ret;
16. }
18. staticinlinelong sys_mprotect(void*addr,size_t len,int prot){
19. long ret;
20. __asm__ volatile(
21. "mov $10, %%rax\n\t"
22. "syscall\n\t"
23. :"=a"(ret)
24. :"D"(addr),"S"(len),"d"(prot)
25. :"rcx","r11","memory"
26. );
27. return ret;
28. }
30. staticinlinelong sys_write(int fd,constchar*buf,size_t count){
31. long ret;
32. __asm__ volatile(
33. "mov $1, %%rax\n\t"
34. "syscall\n\t"
35. :"=a"(ret)
36. :"D"(fd),"S"(buf),"d"(count)
37. :"rcx","r11","memory"
38. );
39. return ret;
40. }
42. // Shellcode 示例:execve("/bin/sh")
43. unsignedchar shellcode[]=
44. "\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57"
45. "\x54\x5f\x6a\x3b\x58\x99\x0f\x05";
47. void _start(){
48. // 1. 分配可执行内存
49. void*mem =(void*)sys_mmap(NULL,4096, PROT_READ | PROT_WRITE,
50. MAP_PRIVATE | MAP_ANONYMOUS,-1,0);
52. // 2. 复制 shellcode
53. for(int i =0; i <sizeof(shellcode)-1; i++){
54. ((char*)mem)[i]= shellcode[i];
55. }
57. // 3. 修改为可执行权限
58. sys_mprotect(mem,4096, PROT_READ | PROT_WRITE | PROT_EXEC);
60. // 4. 输出调试信息(可选)
61. sys_write(1,"[+] Executing shellcode...\n",27);
63. // 5. 跳转执行
64. ((void(*)())mem)();
66. // 6. 退出
67. __asm__ volatile(
68. "mov $60, %rax\n\t"
69. "xor %rdi, %rdi\n\t"
70. "syscall\n\t"
71. );
72. }
74. // 汇编入口
75. __asm__(
76. ".global _start\n"
77. "_start:\n"
78. "and $-16, %rsp\n"// 栈对齐
79. "sub $8, %rsp\n"// 保留空间
80. "call _start\n"
81. );
编译命令:
1. gcc -nostdlib -fPIC -fno-stack-protector minimal_loader.c -o loader
2. strip loader # 去除符号表,减小体积
3. ./loader
9.4 对比:传统方式 vs syscall 自 loader
| 特性 | 传统方式(auxv/link_map) | syscall 自 loader | | — | — | — | | 复杂度 | 需要解析 ELF 结构、符号表 | 直接 syscall,无需解析 | | 代码量 | 100+ 行 | 30-50 行 | | 依赖 | 需要理解 ELF/DYNAMIC 段 | 只需知道 syscall 号 | | 灵活性 | 受限于符号表完整性 | 完全自由,可加载任意 shellcode | | 隐蔽性 | 可能被监控 link_map 访问 | 纯 syscall,更难检测 | | 兼容性 | 依赖 glibc 结构稳定性 | syscall 接口永久稳定 | | 适用场景 | 需要调用现有库函数 | 独立 payload、无文件攻击 |
9.5 实战示例:无文件执行 /bin/sh
完整代码(单文件,可直接编译):
1. // execve_shellcode.c
2. // gcc -nostdlib -fPIC execve_shellcode.c -o shell
3. #include<stdint.h>
5. // syscall 宏定义
6. #define SYS_mmap 9
7. #define SYS_mprotect 10
8. #define SYS_execve 59
9. #define SYS_exit 60
11. #define PROT_READ 1
12. #define PROT_WRITE 2
13. #define PROT_EXEC 4
14. #define MAP_PRIVATE 0x02
15. #define MAP_ANONYMOUS 0x20
17. staticinlinelong syscall3(long num,long a1,long a2,long a3){
18. long ret;
19. __asm__ volatile("syscall":"=a"(ret):"a"(num),"D"(a1),"S"(a2),"d"(a3):"rcx","r11","memory");
20. return ret;
21. }
23. void _start(){
24. // 分配内存
25. long mem = syscall3(SYS_mmap,0,4096, PROT_READ | PROT_WRITE);
27. // 构造 execve 参数(在栈上)
28. char*argv[2]={"/bin/sh", NULL};
29. char*envp[1]={NULL};
31. // 直接调用 execve(不需要 shellcode)
32. syscall3(SYS_execve,(long)argv[0],(long)argv,(long)envp);
34. // 失败则退出
35. syscall3(SYS_exit,1,0,0);
36. }
38. __asm__(".global _start\n_start: and $-16,%rsp; sub $8,%rsp; call _start\n");
编译运行:
1. gcc -nostdlib -fPIC execve_shellcode.c -o shell
2. ./shell # 直接获得 shell
9.6 进阶:反射式 ELF 加载器
如果需要加载完整的 ELF 文件到内存执行(类似反射式 DLL),可以这样:
1. // 伪代码框架
2. void load_elf_from_memory(uint8_t*elf_data){
3. Elf64_Ehdr*ehdr =(Elf64_Ehdr*)elf_data;
4. Elf64_Phdr*phdr =(Elf64_Phdr*)(elf_data + ehdr->e_phoff);
6. // 1. 遍历 Program Headers,加载 LOAD 段
7. for(int i =0; i < ehdr->e_phnum; i++){
8. if(phdr[i].p_type == PT_LOAD){
9. // mmap 分配内存
10. void*addr = mmap(phdr[i].p_vaddr, phdr[i].p_memsz,
11. PROT_READ | PROT_WRITE,
12. MAP_PRIVATE | MAP_ANONYMOUS,-1,0);
13. // 复制段数据
14. memcpy(addr, elf_data + phdr[i].p_offset, phdr[i].p_filesz);
15. }
16. }
18. // 2. 设置内存权限(根据 p_flags)
19. // 3. 跳转到入口点:ehdr->e_entry
20. ((void(*)())ehdr->e_entry)();
21. }
这种方式比解析 link_map 简单得多,因为:
- 不需要查找符号表
- 不需要处理重定位
- 不需要关心动态链接器
9.7 总结:Linux 的优势
Windows 开发者羡慕的地方:
- syscall 数量少且稳定:Linux 只有 300+ 个 syscall,而 Windows API 成千上万
- 调用约定简单:参数通过寄存器传递(rdi, rsi, rdx, r10, r8, r9)
- 无需处理 PE 结构:ELF 更简洁,且内核直接支持
- 没有导入表限制:可以直接 syscall,不依赖 IAT
- ASLR 更容易绕过:内核分配的地址虽然随机,但 syscall 本身不受影响
最佳实践建议:
- 如果是 独立 payload:直接用 syscall,不要解析任何结构
- 如果需要 调用现有库函数:再使用 link_map/auxv 方案
- 如果做 跨平台工具:Windows 用 PEB,Linux 用 syscall(而不是强行统一)
10. 最终对比与选型指南
10.1 技术路线对比总表
| 方案 | Windows PEB | Linux auxv/link_map | Linux syscall 自 loader | | — | — | — | — | | 复杂度 | 中等 | 高 | 极低 ⭐ | | 代码量 | ~80 行 | ~150 行 | ~30 行 ⭐ | | 稳定性 | 高(NT 内核稳定) | 中(依赖 glibc 结构) | 极高 (syscall 永久稳定)⭐ | | 隐蔽性 | 中(EDR 重点监控) | 中 | 高 (纯 syscall)⭐ | | 灵活性 | 中(受 PE 结构限制) | 高 | 极高 (任意 shellcode)⭐ | | 学习曲线 | 陡峭(PE 结构复杂) | 陡峭(ELF+ 动态链接) | 平缓 (几个 syscall)⭐ | | 推荐指数 | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ |
10.2 场景化选型建议
🎯 场景 1:无文件攻击/Shellcode 加载
- Windows: PEB + GetProcAddress
- Linux: syscall 自 loader(强烈推荐)
1. // Linux 最佳实践
2. void* mem = mmap(NULL,4096, PROT_READ|PROT_WRITE, MAP_ANONYMOUS,-1,0);
3. memcpy(mem, shellcode, size);
4. mprotect(mem,4096, PROT_EXEC);
5. ((void(*)())mem)();
🎯 场景 2:调用系统库函数(如 printf、open)
- Windows: PEB 解析 kernel32.dll 导出表
- Linux: link_map 解析 libc.so 符号表
1. // Linux 次选方案(需要符号时使用)
2. struct link_map *map= get_link_map();
3. // 找到 libc,解析 printf 地址
🎯 场景 3:跨平台安全工具
1. #ifdef _WIN32
2. // Windows: PEB 方案
3. resolve_via_peb("kernel32.dll","CreateProcessA");
4. #else
5. // Linux: 优先 syscall,必要时 link_map
6. if(can_use_syscall()){
7. syscall_direct(...);
8. }else{
9. resolve_via_link_map("libc.so","execve");
10. }
11. #endif
🎯 场景 4:反射式加载器(Reflective Loader)
- Windows: 手动 PE 加载 + 重定位处理
- Linux: 手动 ELF 加载(比 link_map 简单)
1. // Linux 反射式 ELF 加载(简化版)
2. void load_elf(uint8_t*elf){
3. Elf64_Ehdr*ehdr =(Elf64_Ehdr*)elf;
4. // 遍历 PT_LOAD 段,mmap + memcpy
5. // 设置权限,跳转到 e_entry
6. ((void(*)())ehdr->e_entry)();
7. }
10.3 常见误区
❌ 误区 1: “Linux 必须解析 link_map 才能执行代码” ✅ 真相: syscall 可以直接分配内存并执行,完全不需要符号表
❌ 误区 2: “auxv 是获取模块信息的唯一方式” ✅ 真相: auxv 只在 nostdlib 时有用,常规场景 link_map 更简单
❌ 误区 3: “跨平台必须统一实现方式” ✅ 真相: Windows 用 PEB,Linux 用 syscall,各自最优才是真跨平台
❌ 误区 4: “syscall 不稳定,会随内核变化” ✅ 真相: Linux syscall ABI 向后兼容,比 glibc 稳定得多
10.4 性能对比
1. 测试:获取 execve 函数地址并调用
3. 方案 A: link_map 解析
4. -遍历 _DYNAMIC 段:~100次迭代
5. -解析 DT_HASH/DT_SYMTAB:~50次计算
6. -字符串匹配:O(n)复杂度
7. -总耗时:~5000 CPU 周期
9. 方案 B: syscall 直接调用
10. -一条 syscall 指令
11. -总耗时:~100 CPU 周期
13. 结论:syscall 方案快50倍+
10.5 检测与反检测
EDR/杀软检测手段:
| 检测点 | PEB 方案 | link_map 方案 | syscall 方案 | | — | — | — | — | | 内存扫描 | 可检测异常链表访问 | 可检测 link_map 读取 | 难以检测 | | API Hook | 可 Hook NtQueryInformationProcess | 可 Hook dlopen/dlsym | 无法 Hook(直接 syscall) | | 行为分析 | 可疑的 PEB 访问模式 | 异常的符号解析 | 正常 syscall 流 | | 静态特征 | PEB 偏移量特征码 | link_map 结构特征 | 无明显特征 |
反检测建议:
- Linux 优先 syscall:绕过所有用户态监控
- 混淆 syscall 号:不要硬编码
mov $9,%rax,用变量传递 - 减少特征码:避免使用常见的 shellcode 模板
- 动态计算:syscall 号通过计算得到(如
mov $15,%rbx;sub$6,%rbx得到 9)
11. 实战代码仓库推荐
11.1 学习资源
- test.c: 标准 link_map 实现(适合理解 ELF 动态链接)
- test2.c: nostdlib + auxv 实现(适合 shellcode 环境)
- minimal_loader.c: syscall 自 loader(推荐用于实际项目)
11.2 扩展阅读
- Linux syscall 表: https://github.com/torvalds/linux/blob/master/arch/x86/entry/syscalls/syscall_64.tbl
- ELF 规范: https://refspecs.linuxfoundation.org/elf/elf.pdf
- shellcode 编写指南: https://www.exploit-db.com/docs/linux
- 反调试技术: 使用
ptrace自保护(syscall 303)
12. 总结陈词
💡 核心观点
Linux 开发者的福音:
“在 Windows 上你不得不解析复杂的 PEB 结构,但在 Linux 上,一条 syscall 就够了。”
技术本质:
- Windows 的设计哲学是 “一切皆 API”,必须通过官方接口
- Linux 的设计哲学是 “内核即服务”,syscall 就是契约
实战建议:
- 新手入门: 从 test.c 开始,理解 link_map 机制
- 进阶开发: 使用 syscall 自 loader,代码最简洁
- 跨平台项目: Windows 用 PEB,Linux 用 syscall,不要强行统一
- 安全研究: 理解所有三种方案,根据场景灵活选择
🚀 终极建议
如果你正在开发 Linux 平台的 loader 或安全工具:
1. 优先级排序:
2. 1️⃣ syscall 直接调用(最简单、最快、最隐蔽)
3. 2️⃣万不得已再用 link_map(需要符号解析时)
4. 3️⃣ auxv 作为备选(nostdlib 且 syscall 不可用时)
记住这句话:
“能用 syscall 解决的,就不要解析数据结构;能简单的,就不要复杂。”
测试代码1
// gcc ./test.c -o test// ======================================================================// MODULE: [Main Image]// BASE : 0x5983655d2000// ======================================================================// [!] 模块信息缺失,无法解析符号表// ======================================================================// MODULE: linux-vdso.so.1// BASE : 0x7fff072cc000// ======================================================================// ======================================================================// MODULE: /lib/x86_64-linux-gnu/libc.so.6// BASE : 0x7877ec400000// ======================================================================// [Symbols for /lib/x86_64-linux-gnu/libc.so.6]// -> 0x00007877ec48ef70 | fgetc// -> 0x00007877ec49a3f0 | pthread_attr_setscope// -> 0x00007877ec49a0a0 | pthread_attr_getstacksize// -> 0x00007877ec4b1480 | envz_strip// -> 0x00007877ec49a0a0 | pthread_attr_getstacksize// -> 0x00007877ec53dc40 | iruserok_af// -> 0x00007877ec56a010 | _nss_files_getpwent_r// -> 0x00007877ec4a38b0 | pthread_setcancelstate// -> 0x00007877ec44b3b0 | stdc_first_leading_one_ul// -> 0x00007877ec51d0b0 | cfmakeraw// -> 0x00007877ec544bb0 | ns_name_pack// -> 0x00007877ec544bb0 | ns_name_pack// -> 0x00007877ec496fb0 | _IO_iter_begin// -> 0x00007877ec4f98d0 | globfree// -> 0x00007877ec497390 | _IO_str_init_readonly// -> 0x00007877ec538410 | __vswprintf_chk// -> 0x00007877ec4a1c10 | pthread_mutexattr_getprotocol// -> 0x00007877ec4a1c10 | pthread_mutexattr_getprotocol// -> 0x00007877ec4d8fc0 | wmemmove// -> 0x00007877ec5685c0 | _nss_files_gethostbyname4_r// -> 0x00007877ec4a26d0 | __pthread_rwlock_rdlock// -> 0x00007877ec52c390 | __cmsg_nxthdr// -> 0x00007877ec55dbb0 | gethostbyname_r// -> 0x00007877ec579f00 | xdecrypt// -> 0x00007877ec568140 | _nss_files_gethostent_r// -> 0x00007877ec51bc40 | statvfs64// -> 0x00007877ec44b3f0 | stdc_first_leading_one_us// -> 0x00007877ec569b50 | _nss_files_setprotoent// -> 0x00007877ec561480 | getprotoent// -> 0x00007877ec56e370 | svcraw_create// -> 0x00007877ec4a8810 | mq_unlink// -> 0x00007877ec48b6a0 | _IO_free_wbackup_area// -> 0x00007877ec446210 | sigignore// -> 0x00007877ec4a2fe0 | __pthread_rwlock_trywrlock// -> 0x00007877ec4a8810 | mq_unlink// -> 0x00007877ec52aff0 | open_tree// -> 0x00007877ec574560 | clnt_spcreateerror// -> 0x00007877ec517450 | fstatvfs// -> 0x00007877ec577780 | pmap_getport// -> 0x00007877ec488a80 | vsscanf// -> 0x00007877ec4b1560 | memccpy// -> 0x00007877ec43a340 | nl_langinfo// -> 0x00007877ec527ba0 | syslog// -> 0x00007877ec4a7bb0 | lio_listio64// -> 0x00007877ec538780 | __wcstombs_chk// ...#include <stdio.h>#include <elf.h>#include <link.h>#include <string.h>extern ElfW(Dyn) _DYNAMIC[];// 安全的字符串获取函数,防止非法内存访问const char *safe_get_str(const char *strtab, ElfW(Word) index){ if (!strtab || index == 0) return NULL; // 简单的指针合法性粗略判断:不要访问 0x10000 以下的地址 if ((size_t)(strtab + index) < 0x10000) return NULL; return strtab + index;}void dump_module_symbols(struct link_map *map){ // 1. 绝对防御:排除虚拟模块和空指针 if (!map || !map->l_ld) return; if (map->l_name && strstr(map->l_name, "linux-vdso.so")) return; ElfW(Sym) *symtab = NULL; char *strtab = NULL; ElfW(Word) *hashtab = NULL; // 2. 解析动态段 ElfW(Dyn) *dyn = map->l_ld; for (; dyn && dyn->d_tag != DT_NULL; ++dyn) { unsigned long val = dyn->d_un.d_ptr; if (val == 0) continue; // 核心修复逻辑: // 如果 val 大于基址,它可能是绝对地址。 // 如果 val 很小,它肯定是相对偏移,需要加上基址。 // 但对于主程序 (l_addr=0),我们要特别小心。 unsigned long addr = val; if (val < map->l_addr || (map->l_addr == 0 && val < 0x1000000)) { // 这是一个相对地址 addr = map->l_addr + val; } if (dyn->d_tag == DT_SYMTAB) symtab = (ElfW(Sym) *)addr; if (dyn->d_tag == DT_STRTAB) strtab = (char *)addr; if (dyn->d_tag == DT_HASH) hashtab = (ElfW(Word) *)addr; } // 必须要表全了才敢开工 if (!symtab || !strtab || !hashtab) { printf(" [!] 模块信息缺失,无法解析符号表\n"); return; } // SYSV Hash 表的第二项是符号总数 size_t num_symbols = hashtab[1]; if (num_symbols > 5000) num_symbols = 5000; // 强制上限,防止读取损坏的表导致无限循环 printf(" [Symbols for %s]\n", map->l_name[0] ? map->l_name : "Main Program"); for (size_t i = 0; i < num_symbols; i++) { ElfW(Sym) *sym = &symtab[i]; // 只有函数 (STT_FUNC) 且有地址 (st_value) 的才看 if (sym->st_value != 0 && ELF64_ST_TYPE(sym->st_info) == STT_FUNC) { const char *name = safe_get_str(strtab, sym->st_name); if (name) { // 计算函数在内存中的绝对地址 unsigned long func_addr = map->l_addr + sym->st_value; printf(" -> 0x%016lx | %s\n", func_addr, name); } } } printf("\n");}void scan_modules(){ struct r_debug *debug_struct = NULL; ElfW(Dyn) *dyn = _DYNAMIC; for (; dyn->d_tag != DT_NULL; ++dyn) { if (dyn->d_tag == DT_DEBUG) { debug_struct = (struct r_debug *)dyn->d_un.d_ptr; break; } } if (!debug_struct || !debug_struct->r_map) return; struct link_map *map = debug_struct->r_map; while (map) { printf("======================================================================\n"); printf("MODULE: %s\n", map->l_name[0] ? map->l_name : "[Main Image]"); printf("BASE : 0x%lx\n", (unsigned long)map->l_addr); printf("======================================================================\n"); dump_module_symbols(map); map = map->l_next; }}int main(){ scan_modules(); return 0;}
测试代码2
// gcc test2.c -o test -nostdlib -fPIC -fno-stack-protector -lc; ./test// ./test// [MODULE]: [Self]// BASE : 0x0000560ac4846000// [MODULE]: linux-vdso.so.1// BASE : 0x00007fff139ee000// -> 0x00007fff139eea70 | clock_gettime// -> 0x00007fff139ee7b0 | __vdso_gettimeofday// -> 0x00007fff139eedd0 | clock_getres// -> 0x00007fff139eedd0 | __vdso_clock_getres// -> 0x00007fff139ee7b0 | gettimeofday// -> 0x00007fff139eea40 | __vdso_time// -> 0x00007fff139eee70 | __vdso_sgx_enter_enclave// -> 0x00007fff139eea40 | time// -> 0x00007fff139eea70 | __vdso_clock_gettime// -> 0x00007fff139eee40 | __vdso_getcpu// -> 0x00007fff139eee40 | getcpu// [+] 扫描完成。退出程序。#include <elf.h>#include <stddef.h>#include <link.h>// --- 基础底层函数 ---static inline long my_syscall3(long n, long a1, long a2, long a3){ long ret; __asm__ volatile("syscall" : "=a"(ret) : "a"(n), "D"(a1), "S"(a2), "d"(a3) : "rcx", "r11", "memory"); return ret;}void sys_print(const char *s){ if (!s) return; size_t len = 0; while (s[len]) len++; my_syscall3(1, 1, (long)s, len);}void sys_print_hex(size_t val){ char buf[19] = "0x0000000000000000"; const char *hex = "0123456789abcdef"; for (int i = 17; i >= 2; i--) { buf[i] = hex[val & 0xf]; val >>= 4; } sys_print(buf);}int m_strcmp(const char *s1, const char *s2){ while (*s1 && (*s1 == *s2)) { s1++; s2++; } return *(unsigned char *)s1 - *(unsigned char *)s2;}// --- 符号解析核心 ---// 遍历并打印模块的所有导出函数void dump_all_symbols(struct link_map *map){ if (!map->l_ld) return; Elf64_Sym *symtab = NULL; char *strtab = NULL; uint32_t *hashtab = NULL; // 解析 DYNAMIC 段 Elf64_Dyn *dyn = map->l_ld; for (; dyn->d_tag != DT_NULL; dyn++) { // 在 nostdlib 模式下,地址修正逻辑非常关键 unsigned long val = dyn->d_un.d_ptr; // 如果地址明显是一个偏移量,则加上基址 unsigned long addr = (val < map->l_addr || (map->l_addr == 0 && val < 0x1000000)) ? (map->l_addr + val) : val; if (dyn->d_tag == DT_SYMTAB) symtab = (Elf64_Sym *)addr; if (dyn->d_tag == DT_STRTAB) strtab = (char *)addr; if (dyn->d_tag == DT_HASH) hashtab = (uint32_t *)addr; } if (!symtab || !strtab || !hashtab) return; // 获取符号总数 (DT_HASH 第二项) uint32_t num_symbols = hashtab[1]; // 限制一下,防止扫描过大模块时刷屏太猛 if (num_symbols > 2000) num_symbols = 2000; for (uint32_t i = 0; i < num_symbols; i++) { Elf64_Sym *sym = &symtab[i]; // 过滤:必须是函数 (STT_FUNC),且有名称,且有地址 if (ELF64_ST_TYPE(sym->st_info) == STT_FUNC && sym->st_name != 0 && sym->st_value != 0) { const char *name = strtab + sym->st_name; sys_print(" -> "); sys_print_hex(map->l_addr + sym->st_value); sys_print(" | "); sys_print(name); sys_print("\n"); } }}void c_start(size_t *sp){ // 1. 获取 Auxv size_t argc = *sp; size_t *ptr = sp + 1 + argc + 1; while (*ptr != 0) ptr++; ptr++; Elf64_auxv_t *auxv = (Elf64_auxv_t *)ptr; size_t phdr_v = 0, phnum = 0; for (; auxv->a_type != AT_NULL; auxv++) { if (auxv->a_type == AT_PHDR) phdr_v = auxv->a_un.a_val; if (auxv->a_type == AT_PHNUM) phnum = auxv->a_un.a_val; } // 2. 找到主程序的 DYNAMIC Elf64_Phdr *phdr = (Elf64_Phdr *)phdr_v; size_t load_bias = 0; Elf64_Dyn *dyn_ptr = NULL; for (size_t i = 0; i < phnum; i++) { if (phdr[i].p_type == PT_PHDR) load_bias = (size_t)phdr - phdr[i].p_vaddr; if (phdr[i].p_type == PT_DYNAMIC) dyn_ptr = (Elf64_Dyn *)(phdr[i].p_vaddr + load_bias); } // 3. 找到 DT_DEBUG 爬 link_map struct r_debug *r_dbg = NULL; for (; dyn_ptr && dyn_ptr->d_tag != DT_NULL; dyn_ptr++) if (dyn_ptr->d_tag == DT_DEBUG) r_dbg = (struct r_debug *)dyn_ptr->d_un.d_ptr; if (r_dbg && r_dbg->r_map) { struct link_map *map = r_dbg->r_map; while (map) { sys_print("\n[MODULE]: "); sys_print(map->l_name[0] ? map->l_name : "[Self]"); sys_print("\nBASE : "); sys_print_hex(map->l_addr); sys_print("\n"); // 遍历并打印符号 dump_all_symbols(map); map = map->l_next; } } sys_print("\n[+] 扫描完成。退出程序。\n"); my_syscall3(60, 0, 0, 0); // exit(0)}// 汇编入口__asm__( ".global _start\n" "_start:\n" "mov %rsp, %rdi\n" "and $-16, %rsp\n" "sub $8, %rsp\n" "call c_start\n");
- 公众号:安全狗的自我修养
- vx:2207344074
- http://gitee.com/haidragon
- http://github.com/haidragon
- bilibili:haidragonx
#
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:安全狗的自我修养 haidragon haidragon《跨平台二进制loader加载器自举寻址技术比对》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论