gaidardzhiev/elfmutator
GitHub: gaidardzhiev/elfmutator
一个极简的ARM32 ELF二进制注入器,通过追加shellcode段并劫持入口点演示供应链攻击与「信任的信任」问题。
Stars: 0 | Forks: 0
# elfmutator
## 摘要
`elfmutator` 是一个极简的 ARM32 ELF 二进制注入器。它接收一个已编译的二进制文件,将 shellcode payload 作为新的可加载段追加进去,将 ELF 入口点重定向到该 payload,并用一条跳转回原程序的指令修补 payload 的桩代码,这样宿主二进制文件在 payload 运行后仍能正常执行。
结果就是一个二进制文件做了它本不该做的事,而且表面上毫无迹象。
```
--------------------------------
| out.elf |
| |
kernel | [entry] ------------------> | payload runs
execve -------->| | (write syscall)
| payload: b main ----------> | main() runs
| | (original code)
| [exit] |
--------------------------------
```
宿主二进制文件从未同意这样做。当你运行别人编译的二进制文件时,你也未必同意。
## 信任的信任问题 (The Trusting Trust Problem)
1984 年,Ken Thompson 证明了 C 编译器可以被植入木马,在其编译的程序中插入恶意代码,包括将木马的新副本插入它编译的任何未来编译器中,而在任何源代码中都找不到攻击的痕迹。攻击存在于编译器二进制文件本身。你无法通过阅读源代码发现它。你也无法通过重新编译来发现它,因为受感染的编译器会再次感染其自身的输出。
Thompson 的结论并不是你应该审计你的编译器。而是你无法审计。信任是传递性的,这条链回溯得比任何人能追踪的都要远。
`elfmutator` 是这条链中微小一环的具体、可检视的例证,即二进制层面。
## 工作原理
### 1. ELF 结构
一个 ELF 可执行文件包含描述可加载段的程序头。内核在 `execve` 时读取这些头,并将每个 `PT_LOAD` 段映射到进程的虚拟地址空间。入口点 (`e_entry`) 是映射完成后内核跳转到的虚拟地址。
elfmutator 添加了一个额外的 `PT_LOAD` 段,页对齐,可读且可执行,包含 payload,然后将 `e_entry` 设置为该段的起始位置。
```
Original ELF: Mutated ELF:
LOAD [0x10000 R E] LOAD [0x10000 R E] original code
LOAD [0x7ed48 RW ] LOAD [0x7ed48 RW ] original data
e_entry = 0x1029c LOAD [0x85000 R E] injected payload
e_entry = 0x85000 redirected
```
### 2. 桩代码模式 (The stub pattern)
Payload 汇编代码包含一个有意设计的无限循环 `b .`,它在 ARM 小端序模式下被汇编为 4 字节模式 `fe ff ff ea`。`elfmutator` 在 payload 二进制文件中扫描此模式,然后用计算好的 `b ` 指令覆盖它,该指令会跳转到宿主二进制文件中的 `main()`。
```
//find the stub
for (size_t i = 0; i + 4 <= payload_size; i++) {
if (payload[i] == 0xfe && payload[i+1] == 0xff &&
payload[i+2] == 0xff && payload[i+3] == 0xea) {
stub_offset = i;
break;
}
}
//patch it with a real branch
int32_t offset_words = (target - (stub_vaddr + 8)) / 4;
uint32_t branch = 0xea000000 | (offset_words & 0x00ffffff);
memcpy(payload + stub_offset, &branch, 4);
```
`+8` 是 ARM 流水线预取偏移量,PC 总是比正在执行的指令领先 8 字节。
### 3. 为什么需要 `-nostdlib`
宿主二进制文件必须在不含 glibc (`-nostdlib -nostartfiles`) 的情况下编译。这不是注入器的限制,而是 glibc 初始化自身方式的结果。
glibc 的 `_start` 调用 `__libc_start_main`,后者设置 TLS、栈保护罐 (stack canary)、stdio 虚表、`atexit` 处理程序以及其他全局状态,然后调用 `main()`。这种初始化只发生一次,不是幂等的。如果 payload 返回到 `_start`,所有这些内容会在已经初始化的内存上再运行一次并破坏它。如果 payload 直接跳转到 `main()`,libc 的内部状态将不完整,`printf` 会解引用空虚表指针。
对于 libc 二进制文件,干净的解决方案是一个尚未解决的独立问题。对于 `-nostdlib` 二进制文件,干净的解决方案很简单:`main()` 只是一个函数。跳转到它。它调用 `sys_exit()` 并且永不返回。
## 用法
```
#build the injector
make
#assemble the payload to a flat binary
make payload
#compile a target, inject, and run
make test
#debug: disassembly, segment inspection, strace
make debug
```
### 手动注入
```
./elfmutator
```
输入必须是一个至少包含一个 `PT_LOAD` 段且具有可访问符号表(未剥离)的 ARM32 ELF 可执行文件。Payload 必须是一个在其代码段中某处包含 `fe ff ff ea` 桩代码的扁平二进制文件。
## Payload 格式
Payload 是一个扁平的 ARM32 二进制文件(无 ELF 头)。它必须包含且仅包含一个 `b .` 桩代码 `fe ff ff ea`,elfmutator 将用跳转到目标的分支修补它。数据必须位于所有代码之后,以便桩代码偏移扫描能找到正确的字节。
```
_start:
mov r7, #4
mov r0, #1
adr r1, msg
mov r2, #34
svc 0
b .
msg:
.asciz "Malicious ARM32 payload executed!\n"
```
汇编并剥离为二进制文件:
```
as -o payload_arm.o payload_arm.S
objcopy -O binary payload_arm.o payload.bin
```
## 限制
- 仅支持 ARM32 (`EM_ARM`, `ELFCLASS32`)
- 目标二进制文件不得被剥离 (stripped)(需要符号表来定位 `main`)
- 分支范围限制在 payload 和目标之间 ±32MB
- 不支持 PIE 二进制文件(需要 `-no-pie`)
- 不支持 Thumb 入口点
- 需要一个可写的程序头表槽位(elfmutator 会追加一个)
## 文件
| 文件 | 用途 |
|-----------------|-----------------------------------------------|
| `elfmutator.c` | 注入器 |
| `payload_arm.S` | 示例 ARM32 payload (write + branch stub) |
| `test.c` | 极简目标二进制文件 (raw syscalls, no libc) |
| `Makefile` | 构建、测试和调试规则 |
## 关于信任
Thompson 的攻击需要访问编译器。`elfmutator` 需要在二进制文件到达你之前访问它,这是一个供应链位置、一个受损的构建服务器、一个被篡改的下载。
你运行以到达这里的二进制文件是由一个你没有编译的编译器编译的,在一台你没有构建的机器上,运行着一个你没有编写的 OS。其中每一个都是插入点。`elfmutator` 只是这样一个点,变得可见并被理解。
理解机制是唯一的部分防御。你无法消除信任。你只能知道它存在于何处。
## 许可证
GPL-3.0-only
标签:ARM32, DNS 反向解析, ELF文件格式, ELF注入, Shellcode, SSH蜜罐, TLS配置检查, Trustign Trust, 中高交互蜜罐, 二进制修改, 二进制注入, 云资产清单, 入口点劫持, 后门开发, 客户端加密, 嵌入式安全, 恶意软件开发, 技术调研, 编译器安全, 进程注入, 逆向工程