本文根据 tmp.0ut 第五期中 febnug 的文章编译整理,非逐字翻译;文中观点均属于原作者。
原文:Brainfuck as a ROP Compiler
文章内容来自 tmpout.sh,本译文依原站 CC BY-NC-SA 4.0 许可作为改编内容发布。如有侵权请通知,我会下架文章。 全文字符数:4524
摘要
本文提出一个最小而有表达力的执行模型:把 Brainfuck 直接编译成 Return-Oriented Programming(ROP)链。程序不再走传统的取指-译码-执行循环,而是被翻译成放在栈上的一串 gadget 地址,执行完全由 ret 指令驱动——栈指针实际上变成了程序计数器。
结果是一台紧凑的“怪异机器”(weird machine):控制流、内存操作和循环结构都只从栈算术中涌现。文章描述该设计、实现与含义,并讨论它对漏洞利用、混淆和非常规计算模型的意义。
1. 引言
ROP 通常与漏洞利用技术联系在一起:复用已有代码片段(gadget)完成任意计算。但抛开攻击用途,ROP 也可以看作一种通用执行模型。
本文采取不同视角:不在严苛约束下手工拼 ROP 链,而是把 ROP 当作编译目标——把一门高级语言直接翻译成 gadget 地址序列,形成一条合法执行链,不需要分发器或解释器循环。Brainfuck 因指令集极小、语义清晰而被用作前端。
核心想法很直接:每条 Brainfuck 指令映射到一个小 gadget,把结果序列放到栈上;CPU 通过反复执行 ret 来运行程序,一次消费链上的一个地址。ROP 在这里不是约束,而是编译目标;在这个模型里,高级程序与漏洞利用载荷没有区别。
2. 执行模型
系统完全去掉传统分发器循环,也没有传统意义上的指令指针,而是:
rsp → [gadget_0][gadget_1][gadget_2]...
执行过程:
ret → gadget_0
ret → gadget_1
ret → gadget_2
栈指针(rsp)充当程序计数器,每个 gadget 以 ret 结尾,形成连续执行链。专用寄存器 rbx 用作 Brainfuck 数据指针,引用一条内存“纸带”。
3. 指令映射
Brainfuck 指令映射成小 gadget:
| 指令 | 行为 |
|---|---|
+ |
递增 [rbx] 处的字节 |
- |
递减 [rbx] 处的字节 |
> |
递增 rbx |
< |
递减 rbx |
. |
向 stdout 写一个字节 |
, |
从 stdin 读一个字节 |
每个 gadget 都尽量小并以 ret 结尾,保证无缝串联。
4. 用栈算术实现控制流
循环不用传统分支实现,而是通过修改栈指针来完成。
[ 编译成条件前跳(跳过循环体):
cmp byte ptr [rbx], 0
jne continue
add rsp, N * 8
continue:
ret
] 编译成条件回跳:
cmp byte ptr [rbx], 0
je continue
sub rsp, N * 8
continue:
ret
这里的 N 是循环体内 gadget 的个数。控制流被变成指针算术——这正是怪异机器的标志之一。
4.1 没有分支的控制流
该系统一个显著特性是:没有显式的控制流指令(跳转、调用)。所有分支行为都从施加于栈指针的算术中涌现。这实际上消除了“控制流”与“数据操作”的界限:程序计数器不再是专用寄存器,而是栈状态的隐式属性。这与“怪异机器”的概念一致——计算来自非预期或非常规的执行路径。
5. 编译器设计
编译器对 Brainfuck 源码做单遍扫描,用栈匹配括号并计算跳转偏移。每条指令翻译成一个 gadget 引用,产生线性地址数组;循环结构在边界确定后回填。输出是汇编指令形式的地址序列:
.quad g_add
.quad g_inc_ptr
...
这种表示可以直接汇编并链接进执行环境。
6. 实现要点
- 链必须位于可执行内存。放在不可执行段(如
.data)会在控制流被错误重定向时产生段错误。 - 对齐很重要。保证 8 字节对齐可以简化寻址并避免隐蔽崩溃。
- 栈调整的差一错误是常见 bug 来源。因为
ret会隐式推进 rsp,偏移必须把这个副作用算进去。
7. 观察
这个系统说明:
- 单靠栈就能同时充当代码和控制流;
- 一门极简语言可以编译成 ROP 载荷;
- 控制流可以纯粹用指针算术表达。
值得注意的是,产物与手工构造的 ROP 链无法区分——“程序”与“漏洞利用”的界限变得模糊。
8. 潜在应用
- 用高级抽象生成漏洞利用;
- 用非常规执行模型做混淆;
- 涉及怪异机器的 CTF 题目;
- 非传统计算的研究。
9. 结论
本文给出了把 Brainfuck 编译为栈驱动 ROP 执行模型中程序的方法。通过去掉分发器、只依赖 ret,得到一台最小而有表达力的系统:栈指针成为程序计数器。这项工作展示了 ROP 超越漏洞利用的通用性,也说明哪怕是一门简单语言,也能作为底层执行范式的前端。
后续工作可以探索优化、自修改链,或与真实 gadget 集成的方案。
10. 概念验证
为验证上述执行模型,作者实现了一个最小编译器与运行时:把 Brainfuck 程序翻译成 ROP 链,并在没有传统分发器的情况下执行。系统由两部分组成:
- 编译器:把 Brainfuck 源码转换成 gadget 引用序列;
- 运行时:把栈指针初始化为该序列,并用
ret开始执行。
产物没有解释器循环,控制流从栈自身的结构中涌现。
10.1 测试程序
用来测试的 Brainfuck 程序会计算并打印字符 A 和换行 \n:
+++++++[>++++++++<-]>+.
>++++++++++.
这个程序初始化循环计数器,把它的值乘进下一个单元,然后输出结果。
10.2 编译
编译器把每条指令翻译成 gadget 引用,循环结构解析成条件栈调整。生成产物是一个汇编片段,包含:线性 gadget 地址链,以及用于条件前跳/回跳的辅助 gadget。示例片段:
.quad g_add
.quad g_add
...
.quad g_jz_skip_*
...
.quad g_jnz_back_*
10.3 执行
运行时做如下初始化:
lea rbx, tape
lea rsp, chain
ret
执行完全经由 ret 进行,一次消费链上的一个地址;除 gadget 自身外没有任何显式控制流指令。
10.4 观察到的行为
执行时程序向 stdout 写两个字节:
write(1, "A\n", 2)
换行保证输出正确终止,shell 提示符出现在下一行。这与普通程序输出一致——尽管实现完全依赖裸系统调用。
10.5 稳定性说明
几个实现细节至关重要:
- 链必须放在可执行段(
.text); - 栈必须保持 8 字节对齐;
- 循环偏移必须把
ret的隐式栈移动算进去。
偏移错误会因非法控制流立即崩溃,这凸显了基于栈的执行对环境的高度敏感。
11. 附录 A:编译器源码
下面的 Python 脚本实现了 Brainfuck 到 ROP 的编译器:
#!/usr/bin/env python3
import sys
GADGETS = {
'+': 'g_add',
'-': 'g_sub',
'>': 'g_inc_ptr',
'<': 'g_dec_ptr',
'.': 'g_write',
',': 'g_read',
}
def compile_bf(code):
chain = []
loop_stack = []
for i, c in enumerate(code):
if c in GADGETS:
chain.append(GADGETS[c])
elif c == '[':
chain.append(("JZ", None))
loop_stack.append(len(chain) - 1)
elif c == ']':
if not loop_stack:
raise Exception("Unmatched ]")
start = loop_stack.pop()
end = len(chain)
skip_len = end - start
chain[start] = ("JZ", skip_len)
back_len = end - start + 1
chain.append(("JNZ", back_len))
if loop_stack:
raise Exception("Unmatched [")
return chain
def emit(chain):
print(".section .text")
print(".global chain")
print(".p2align 3")
print("chain:")
for item in chain:
if isinstance(item, tuple):
op, val = item
if op == "JZ":
print(f" .quad g_jz_skip_{val}")
elif op == "JNZ":
print(f" .quad g_jnz_back_{val}")
else:
print(f" .quad {item}")
print(" .quad g_exit")
def emit_helpers(chain):
skips = set()
backs = set()
for item in chain:
if isinstance(item, tuple):
op, val = item
if op == "JZ":
skips.add(val)
elif op == "JNZ":
backs.add(val)
print("\n# ==== GENERATED GADGET HELPERS ====\n")
for n in skips:
print(f"g_jz_skip_{n}:")
print(" cmp byte ptr [rbx], 0")
print(f" jne 1f")
print(f" add rsp, {n}*8")
print("1: ret\n")
for n in backs:
print(f"g_jnz_back_{n}:")
print(" cmp byte ptr [rbx], 0")
print(f" je 1f")
print(f" sub rsp, {n}*8")
print("1: ret\n")
if __name__ == "__main__":
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} program.bf")
sys.exit(1)
with open(sys.argv[1]) as f:
code = f.read().strip()
chain = compile_bf(code)
emit(chain)
emit_helpers(chain)
12. 附录 B:运行时(ROP 虚拟机)
运行时提供执行环境与 gadget 集:
.intel_syntax noprefix
.global _start
.section .bss
tape: .skip 30000
.section .text
_start:
lea rbx, tape
lea rsp, chain
ret
g_add: inc byte ptr [rbx]; ret
g_sub: dec byte ptr [rbx]; ret
g_inc_ptr: inc rbx; ret
g_dec_ptr: dec rbx; ret
g_write:
mov rax, 1
mov rdi, 1
mov rsi, rbx
mov rdx, 1
syscall
ret
g_read:
mov rax, 0
mov rdi, 0
mov rsi, rbx
mov rdx, 1
syscall
ret
g_exit:
mov rax, 60
xor rdi, rdi
syscall
13. 附录 C:构建与运行
示例工作流:
python3 bf2rop.py test.bf > chain.s
cat rop_vm.s chain.s > full.s
as full.s -o rop_vm.o
ld rop_vm.o -o rop_vm
./rop_vm
输出应当与原 Brainfuck 程序的语义一致。
参考
- Shacham, H., “The Geometry of Innocent Flesh on the Bone”,Proceedings of CCS, 2007
- Bratus, S. 等,“Exploit Programming: From Buffer Overflows to Weird Machines”
- Brainfuck(维基百科)
- ROPgadget
- exrop
- Brainfvck Programming(echo-zine)