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

原文:Control Flow That Isn’t There: State Without State

文章内容来自 tmpout.sh,本译文依原站 CC BY-NC-SA 4.0 许可作为改编内容发布。如有侵权请通知,我会下架文章。 全文字符数:3337

摘要

大多数 shellcode 总要把状态存在某处:寄存器、栈、内存——总能指出某个地方。本文这个构造不存状态:状态由标志位承载,控制流在运行时才被选择;从时序中导出的一个比特决定执行如何进入一段共享字节区域。二进制看起来是固定的——确实固定。同样的字节,不同的执行;静态分析恢复出的路径,可能永远不会真正运行。

1. 引言

逆向工程通常从一个简单假设出发:二进制描述的是一个程序。指令是静态的、控制流是固定的、状态是可见的。但这个假设并不总是成立。如果控制流依赖从不被存储的标志位,而且执行可以从同一字节序列的不同偏移开始,那么你恢复出的结构只是一种猜测——你看到的未必是真正运行的。

2. 概览

整个构造由三个小部件组成:

  • 存在标志位里的一个比特;
  • 从时序导出的一个比特;
  • 在运行时选择的一次控制转移。

伪代码流程:

t = rdtsc()
bit = (t.low ^ t.high) & 1

if bit:
    CF = 1
else:
    CF = 0

if CF:
    jump overlap_shift
else:
    jump overlap

3. 用标志位存状态

进位标志(CF)被用作单比特状态容器,不需要内存写入或显式状态跟踪:

if entropy_bit:
    stc
else:
    clc

jc entry_B

状态是隐式的:它不被保存也不被恢复,只作为执行过程的副作用存在,并直接影响控制流。没有任何变量持有它,它只活在标志位里。

4. 用时序当熵源

rdtsc 提取一个熵比特:

rdtsc
xor eax, edx
test al, 1

这个值随执行而变化,并且对时序条件敏感——包括调试和插桩造成的时序偏移。熵只有一比特,但足以影响控制流。

5. 运行时选择控制

这个构造不重写指令。控制流由“执行如何进入共享字节区域”决定:时序比特被映射进 CF,用来在两个入口之间选择:

jc entry_B

entry_A:
    jmp overlap

entry_B:
    jmp overlap_shift

两个入口都指向同一块连续字节区域,只是偏移不同。由于 x86 解码对对齐敏感,从不同偏移开始执行会产生不同的指令流——底层字节完全相同,指令并没有变,变的是对它们的解释。

6. 重叠执行

重叠区域由两条相邻的跳转编码构成:

overlap:        db 0xeb, <rel real>
overlap_shift:  db 0xeb, <rel alt>

这些字节在编译期固定,运行时不修改。但从不同入口进入时,同一区域会被解码成不同指令序列,从而得到不同的控制流路径。这不是“把多条代码路径分别存起来”,而是一段字节序列被多种方式解释。

7. 实现

执行先从 rdtsc 导出单比特并映射进 CF:

rdtsc
xor eax, edx
test al, 1

比特为 1 时用 stc 置位 CF,否则清除;随后用 jc 选择两个入口之一:

jc entry_B

entry_A:
    jmp overlap

entry_B:
    jmp overlap_shift

重叠区域就是两条相邻跳转编码:

overlap:        db 0xeb, <rel real>
overlap_shift:  db 0xeb, <rel alt>

这些字节运行时不修改;只是执行可能从不同偏移开始,从而被解码成不同路径。分歧不是由单独指令编码的,而是从“如何进入共享字节序列”中涌现的。载荷数据内联存放,用短 call/pop 序列访问:

jmp short data
ret:
    pop rsi

这避免了绝对寻址,让代码位置无关。整个执行不需要可写内存,也不重写任何指令;代码全程保持静态,变化的只是进入重叠区域的入口点。

8. 作为 shellcode

这个构造可以表达成位置无关的 shellcode。实现避免绝对寻址,只用相对控制转移;内联数据用短 call/pop 访问。为支持注入场景,可以用字节宽立即数和短格式控制转移写成不含 NUL 字节的版本。它保持同样的性质:没有显式状态存储、控制流依赖运行时、存在重叠执行区域。这说明该技术不依赖 ELF 二进制,可以直接用于注入代码。

9. 意义

静态分析假设“字节到指令”的映射固定。当执行取决于入口偏移和标志位这类隐式状态时,这个假设就失效了。反汇编器通常会恢复出一个合法的控制流图,但该图可能不对应任何一次真实执行。这种差异不是混淆层或加密造成的,而是执行模型本身的根本性质。

10. 行为

多次运行会得到不同输出:

hello
holla
hello
hello

具体序列取决于时序与标志状态。调试时,时序偏移会让执行偏向某条路径,使行为依赖运行环境。

11. 结论

这里给出了一个最小构造:状态由标志位承载,控制流在运行时选择,执行依赖进入重叠字节区域的入口。不需要显式状态存储,不需要代码变异。代码没有变,变的只是穿过它的路径。

12. 概念验证

下面是该想法的最小实现。它位置无关,既可以作为小型 ELF,也可以作为裸 shellcode。执行从 rdtsc 开始,提取一个比特并用 stc/clc 放进 CF——哪里都没有存储任何东西,标志就是唯一的状态。之后执行被推进两个入口之一,都落在同一字节区域内、只是偏移不同。重叠区就是两条相邻跳转:

overlap:        jmp real
overlap_shift:  jmp alt

运行时不改任何字节,差异完全来自执行从哪里开始。同样的字节,不同的解码。payload 内联存放,用短 call/pop 恢复,没有绝对引用和重定位。

程序运行时打印 hello\nholla\n 之一。从静态布局看不出选择依据——静态分析只给你一条路径,而 CPU 可能走另一条。

global _start
section .text

_start:
    rdtsc
    xor eax, edx

    test al, 1
    jz noflip
    stc
    jmp decide

noflip:
    clc

decide:
    jc entry_B

entry_A:
    jmp overlap

entry_B:
    jmp overlap_shift

; --- OVERLAP ---

overlap:
    db 0xeb, real - overlap - 2

overlap_shift:
    db 0xeb, alt - overlap_shift - 2

; --- PAYLOAD ---

alt:
    jmp short alt_data
alt_pop:
    pop rsi
    jmp write
alt_data:
    call alt_pop
    db 'h','o','l','l','a',0x0a

real:
    jmp short real_data
real_pop:
    pop rsi
    jmp write
real_data:
    call real_pop
    db 'h','e','l','l','o',0x0a

write:
    push 1
    pop rdi            ; stdout

    xor eax, eax
    mov al, 1

    xor edx, edx
    mov dl, 6

    syscall

    xor eax, eax
    mov al, 60
    xor edi, edi
    syscall

这个版本无 NUL 字节且完全位置无关,适合作为 shellcode:

unsigned char shellcode[] =
"\x0f\x31\x31\xd0\xa8\x01\x74\x03\xf9\xeb\x01\xf8\x72\x02"
"\xeb\x02\xeb\x02\xeb\x12\xeb\x00\xeb\x03\x5e\xeb\x1b\xe8"
"\xf8\xff\xff\xff\x68\x6f\x6c\x6c\x61\x0a\xeb\x03\x5e\xeb"
"\x0b\xe8\xf8\xff\xff\xff\x68\x65\x6c\x6c\x6f\x0a\x6a\x01"
"\x5f\x31\xc0\xb0\x01\x31\xd2\xb2\x06\x0f\x05\x31\xc0\xb0"
"\x3c\x31\xff\x0f\x05";

13. 参考

  • Covert Computation: Hiding Code in Code,Computers & Security, 2014(论文
  • Christopher Eagle,The IDA Pro Book: The Unofficial Guide to the World’s Most Popular Disassembler, 2nd Edition, 2011
  • Skape & Skywing,Understanding Windows Shellcode,Uninformed Journal, 2007
  • A Taxonomy of Self-Modifying Code for Obfuscation,Computers & Security, 2011(论文

上一页:有 XLAT 就够了:数据驱动的解码实验

下一页:把 Brainfuck 当作 ROP 编译器