本文根据 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 链,并在没有传统分发器的情况下执行。系统由两部分组成:

  1. 编译器:把 Brainfuck 源码转换成 gadget 引用序列;
  2. 运行时:把栈指针初始化为该序列,并用 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 程序的语义一致。

参考


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

下一页:深入理解 Linux 内核如何加载可执行文件