[机翻] [ABD] 02_编译器架构与混淆分类

02 编译器架构与混淆分类

https://github.com/malrev/ABD

本章以 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 = positionid2 = initialid3 = 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。
  • 下一章将详细讲解具体的混淆技术。
posted @ 2026-08-05 15:54  DirWangK  阅读(5)  评论(0)    收藏  举报