[机翻] [ABD] 02_编译器架构与混淆分类
02 编译器架构与混淆分类
本章以 LLVM 为例介绍编译器的工作流程,并据此分析混淆可以在哪些环节插入;随后给出混淆的分类法(Taxonomy),以及已知混淆转换的数量参考;最后介绍恶意软件中的两阶段载荷模型。
2.1 编译器架构(以 LLVM 为例)
编译器(Compiler)是将源代码翻译为机器可执行代码的程序。经典编译器通常分为两大阶段:前端(Frontend) 与 后端(Backend)。以 LLVM 为例,其整体架构如下:
源代码 (.c)
│
▼
┌─────────────────────────────────┐
│ Frontend │ 前端
│ - Lexical Analyzer (词法分析) │
│ - Syntax Analyzer (语法分析) │
│ - Semantic Analyzer(语义分析) │
└──────────────┬──────────────────┘
│ 抽象语法树 (AST)
▼
┌─────────────────────────────────┐
│ Backend │ 后端
│ - Intermediate Code Generator │ 中间代码生成
│ - Optimization Pass │ 优化通道
│ - Code Generator │ 代码生成
└──────────────┬──────────────────┘
│
▼
机器代码
参考:Aho, Lam, Sethi, Ullman. Compilers: Principles, Techniques, and Tools. 1986.(俗称"龙书")
2.1.1 Frontend(前端)
前端的职责是将源代码(如 C 语言)解析为编译器内部可处理的表示形式,主要包含三个子模块:
(1) Lexical Analyzer(词法分析器)
- 作用:将源代码字符流转换为 token 序列(记号流)。
- 词法分析器逐字符扫描源代码,识别出标识符(identifier)、关键字(keyword)、常量(constant)、运算符(operator)等。
(2) Syntax Analyzer / Parser(语法分析器)
- 作用:根据语法规则构建 语法树(Syntax Tree)。
- Parser 检查 token 序列是否符合语言的语法规则,并生成层次化的语法树结构。
(3) Semantic Analyzer(语义分析器)
- 作用:进行语义检查,如类型检查(Type Checking)、类型转换(如
inttofloat)等。
前端示例
以经典赋值语句为例:
源代码:
position = initial + rate * 60
词法分析后得到的 token 序列:
<id, 1> <= > <id, 2> <+> <id, 3> <*> <60>
其中 <id, 1> 表示标识符 position,<id, 2> 表示 initial,<id, 3> 表示 rate。
语义分析阶段会进行类型转换。例如,由于 60 是整型常量,而 rate 是浮点型变量,需要将整型 60 转换为浮点型:
position = initial + rate * inttofloat(60)
2.1.2 Backend(后端)
后端的职责是将前端产生的中间表示转换为目标平台的机器代码,主要包含三个子模块:
(1) Intermediate Code Generator(中间代码生成器)
- 作用:将语法树转换为中间代码(Intermediate Code / IR),通常采用三地址码(Three-Address Code)形式。
对上面的例子,生成的中间代码为:
t1 = inttofloat(60)
t2 = id3 * t1
t3 = id2 + t2
id1 = t3
其中 t1, t2, t3 是临时变量,id1 = position,id2 = initial,id3 = rate。
(2) Optimization Pass(优化通道)
- 作用:对中间代码进行优化,在不改变程序语义的前提下提高执行效率。
- 常见优化:常量折叠(Constant Folding)、死代码消除(Dead Code Elimination)、公共子表达式消除等。
优化后的中间代码:
t1 = id3 * 60.0 ; 60 被折叠为 60.0,inttofloat 被消除
id1 = id2 + t1 ; 中间变量 t2, t3 被消除
注意:这里的优化正是后续反混淆技术的基础——许多混淆变换产生的"多余"代码可以被编译器优化技术识别并消除。
(3) Code Generator(代码生成器)
- 作用:将优化后的中间代码转换为目标平台的机器代码。
生成的机器代码示例(假设采用浮点寄存器 R1, R2):
LDF R2, id3 ; 将 rate 加载到 R2
MULF R2, R2, #60.0 ; R2 = R2 * 60.0
LDF R1, id2 ; 将 initial 加载到 R1
ADDF R1, R1, R2 ; R1 = R1 + R2
STF id1, R1 ; 将结果存入 position
2.1.3 编译流程完整示例汇总
将上述各阶段汇总:
| 阶段 | 输出 |
|---|---|
| 源代码 | position = initial + rate * 60 |
| 词法分析 | <id,1> <=> <id,2> <+> <id,3> <*> <60> |
| 语义分析 | 加入 inttofloat(60) 类型转换 |
| 中间代码 | t1 = inttofloat(60) / t2 = id3 * t1 / t3 = id2 + t2 / id1 = t3 |
| 优化后中间代码 | t1 = id3 * 60.0 / id1 = id2 + t1 |
| 机器代码 | LDF R2,id3 / MULF R2,R2,#60.0 / LDF R1,id2 / ADDF R1,R1,R2 / STF id1,R1 |
2.2 混淆插入点
理解了编译器的架构后,我们可以分析混淆可以在编译流程的哪些环节插入。不同的插入点对应不同的混淆粒度与工具。
2.2.1 Frontend 混淆插入点
在编译器前端插入混淆,即在源代码层面进行变换:
| 插入点 | 说明 |
|---|---|
| Preprocessor Macro(预处理器宏) | 利用 C 预处理器宏定义在预处理阶段展开混淆代码。优点是实现简单;缺点是容易被预处理后还原。 |
| Source Code Analysis(源代码分析) | 在源代码经过词法/语法分析后、生成中间代码前插入混淆变换。 |
2.2.2 Backend 混淆插入点
在编译器后端插入混淆,即在 IR 或二进制层面进行变换:
| 插入点 | 说明 |
|---|---|
| Inline Assembly(内联汇编) | 在源代码中直接嵌入汇编指令,绕过编译器的分析与优化。 |
| Obfuscation Pass(混淆通道) | 在后端优化阶段注册一个额外的 LLVM Pass,对 IR 进行混淆变换。O-LLVM 即采用此方式。 |
| Packing(打包) | 对编译后的二进制文件进行加壳,运行时解压解密。 |
| Binary Rewriting(二进制重写) | 直接对已有的二进制可执行文件进行修改,插入混淆代码。 |
2.2.3 插入点总览
源代码 ──► Frontend ──► IR ──► Backend ──► 机器代码 ──► 可执行文件
│ │ │ │
│ │ │ │
┌───────┴──────┐ ┌────┴────┐ ┌──────┴─────┐ ┌────┴────┐
│Preprocessor │ │Obfusc. │ │Binary │ │Packing │
│Macro │ │Pass │ │Rewriting │ │ │
│Source Code │ │ │ │ │ │ │
│Analysis │ │ │ │ │ │ │
└──────────────┘ └─────────┘ └────────────┘ └─────────┘
(Frontend) (Backend) (Backend) (Post-link)
2.3 混淆分类法(Taxonomy of Obfuscation)
为了系统地描述各种混淆技术,课程采用了一个四维分类法。每个混淆变换都可以从以下四个维度进行刻画:
维度 1:Abstraction(抽象层次)
混淆作用于程序的哪个抽象层次:
| 层次 | 说明 |
|---|---|
| Source Code(源代码) | 在源代码层面进行混淆,如 Tigress。 |
| IR(中间表示) | 在编译器中间表示层面进行混淆,如 O-LLVM。 |
| Binary Machine Code(二进制机器代码) | 直接对二进制可执行文件进行混淆,如 VMProtect、Themida。 |
维度 2:Unit(操作单元)
混淆作用于程序结构的最小单元:
| 单元 | 说明 |
|---|---|
| Instruction(指令) | 单条指令级别的混淆。 |
| Basic Block(基本块) | 基本块级别的混淆。基本块是直线指令序列——除入口外无分支进入,除出口外无分支输出。 |
| Loop(循环) | 循环结构级别的混淆。 |
| Function(函数) | 函数级别的混淆。 |
| Program(程序) | 整个程序级别的混淆。 |
| System(系统) | 跨进程/系统级别的混淆。 |
基本块(Basic Block)的严格定义:
- 基本块是一段连续的指令序列。
- 入口约束:除序列的第一条指令外,没有其他指令可以跳转进入该序列。
- 出口约束:除序列的最后一条指令外,没有其他指令可以跳出该序列。
- 也就是说,基本块内部没有分支,控制流只能从顶部进入、从底部离开。
维度 3:Dynamics(动态性)
混淆是否在运行时动态变化:
| 类别 | 说明 |
|---|---|
| Static(静态) | 混淆在编译/打包时确定,运行时不变。大多数混淆工具属于此类。 |
| Dynamic(动态) | 混淆在运行时动态变化,如自修改代码(Self-Modifying Code)。 |
维度 4:Target(目标)
混淆针对程序中的哪种元素:
| 目标 | 说明 |
|---|---|
| Constants(常量) | 隐藏常量值,如 Encode Literals。 |
| Variables(变量) | 隐藏变量用途与关系。 |
| Code Logic(代码逻辑) | 隐藏控制流逻辑,如 Control Flow Flattening、Opaque Predicate。 |
| Code Abstraction(代码抽象) | 隐藏代码的整体结构,如 Virtualization Obfuscation。 |
分类法总览
Taxonomy of Obfuscation
/ | | \
/ | | \
Abstraction Unit Dynamics Target
| | | |
Source Inst. Static Constants
IR BB Dynamic Variables
Binary Loop Code Logic
Function Code Abstraction
Program
System
2.4 已知混淆转换数量
根据 Banescu 的综述:
已知有 31 种 混淆转换(obfuscation transformations),且其中大多数可以同时适用(即可以组合使用)。
参考:
- Banescu. A Tutorial on Software Obfuscation. 2017.
这意味着混淆技术并非互斥,一个混淆后的程序往往是多种变换叠加的结果。这也正是反混淆困难的原因之一——需要识别并逐一消除多层混淆。
2.5 恶意软件中的混淆
2.5.1 两阶段载荷模型
现代恶意软件通常采用两阶段载荷模型(Two-Stage Payload Model),以降低被发现的风险:
┌──────────────────────────────────────────────────────────┐
│ 1st Stage Payload │
│ │
│ 入口载荷,负责投放与建立持久化通道 │
│ - Malicious Macro / Script(恶意宏/脚本) │
│ - Dropper(投放器) │
│ - Downloader(下载器) │
│ - Beacon(信标,回连 C2 服务器) │
│ │ │
▼ │
┌──────────────────────────────────────────────────────────┐
│ 2nd Stage Payload │
│ │
│ 真正的恶意功能载荷 │
│ - Implant(植入体) │
│ - Agent(代理) │
│ - Rootkit(内核后门) │
└──────────────────────────────────────────────────────────┘
1st Stage Payload(第一阶段载荷)
第一阶段载荷通常体积小、行为相对简单,主要任务是:
- 投放器(Dropper):将第二阶段载荷写入磁盘或内存。
- 下载器(Downloader):从远程 C2 服务器下载第二阶段载荷。
- 信标(Beacon):与 C2 建立通信通道,等待指令。
- 恶意宏/脚本(Malicious Macro / Script):常见于钓鱼文档,作为初始感染入口。
2nd Stage Payload(第二阶段载荷)
第二阶段载荷是真正执行恶意功能的代码:
- 植入体(Implant):在受害主机上执行具体恶意操作。
- 代理(Agent):作为攻击者的内网跳板或横向移动节点。
- Rootkit:在内核层面隐藏自身与持久化。
2.5.2 混淆的作用
在恶意软件的攻击链中,混淆被归类为 "Obfuscated Files or Information"(MITRE ATT&CK 中的技术 ID: T1027)。
混淆的作用与特点:
- 混淆只是一个元素:在完整的攻击链中,混淆本身不是攻击目的,而是辅助手段。
- 隐藏其他元素:混淆可以隐藏其他恶意元素,例如:
- 隐藏真实 payload(防止被沙箱提取)。
- 隐藏 C2 通信内容。
- 隐藏漏洞利用代码的特征。
- 延迟安全产品的检测与自动化分析。
- 分层应用:混淆可以分别应用于第一阶段和第二阶段载荷,且两个阶段的混淆手段可能不同。
攻击链示例(简化):
钓鱼邮件 ──► 恶意文档(宏混淆) ──► Dropper(加壳) ──► 下载 Implant(VMP混淆) ──► C2通信(加密)
[混淆元素] [混淆元素] [混淆元素]
因此,在分析恶意软件时,往往需要先进行反混淆才能提取到真正的 payload 并理解其行为。
2.6 本章小结
- 编译器分为 Frontend(词法/语法/语义分析)与 Backend(中间代码生成/优化/代码生成),混淆可以在这条链路的多个环节插入。
- Basic Block 是直线指令序列,入口唯一、出口唯一,是控制流分析的基本单元。
- 混淆分类法有四个维度:Abstraction(抽象层次)、Unit(操作单元)、Dynamics(动态性)、Target(目标)。
- 已知有 31 种混淆转换,大多数可组合使用。
- 恶意软件采用两阶段载荷模型,混淆作为辅助手段隐藏各阶段的真实 payload。
- 下一章将详细讲解具体的混淆技术。

浙公网安备 33010602011771号