网安行业知识普及-3:二进制分析

7 阅读14分钟

一、先建立直观理解

一个类比:考古学家的工作

想象你发现了一块古代的石碑(这就是二进制文件):

考古工作对应二进制分析
石碑本身二进制文件(exe、dll、elf)
石碑上的文字机器指令(CPU能执行的代码)
识别这是什么文字格式解析(PE/ELF格式识别)
翻译成现代语言反汇编(机器码→汇编)
理解句子意思反编译(汇编→伪代码)
分析文章结构控制流图(函数、跳转关系)
找出谁引用谁函数调用图
提取关键人名地名信息提取(字符串、API调用)

作为源码开发者,平时是看着源码(文章的原始手稿)在工作。而二进制分析是只有石碑(编译后的成品),要反推文章内容。


二、核心概念详解

2.1 二进制格式解析

二进制文件就是计算机可以直接执行的程序。比如你双击运行的 .exe 文件,Linux 下的可执行文件,都是二进制文件。

打个比方

  • 源代码(C/Python)就像菜谱(人类能看懂)
  • 二进制文件就像做好的菜(计算机直接"吃")
  • 二进制分析就像尝菜分析用了什么调料(反推做法)

是什么:识别二进制文件的组织结构,就像解读一本书的目录和章节划分。

为什么需要:不同的操作系统用不同的格式组织二进制文件,必须先理解这个组织方式,才能正确解析内容。

常见格式

  • PE:Windows可执行文件(.exe、.dll)
  • ELF:Linux可执行文件
  • Mach-O:macOS可执行文件

PE文件结构示例

┌─────────────────────┐
│     DOS头           │  <- "MZ"开头,兼容性用的
├─────────────────────┤
│     PE头            │  <- 关键信息:入口点、编译时间
├─────────────────────┤
│     节表            │  <- 有几个节?各节叫什么?
├─────────────────────┤
│  .text (代码节)     │  <- 真正的机器指令在这里
├─────────────────────┤
│  .data (数据节)     │  <- 全局变量、初始化的数据
├─────────────────────┤
│  .rdata (只读数据)  │  <- 字符串常量、只读数据
├─────────────────────┤
│  .idata (导入表)    │  <- 调用了哪些Windows API?
└─────────────────────┘

解析出的元数据

{
  "entry_point": "0x401000",      // 程序从哪开始执行
  "timestamp": "2023-01-01",      // 编译时间
  "sections": [                   
    { "name": ".text", "size": 1024, "permissions": "RX" },
    { "name": ".data", "size": 256, "permissions": "RW" }
  ],
  "imported_functions": [          // 调用了哪些系统函数
    "MessageBoxA",
    "CreateFile",
    "WriteFile"
  ],
  "is_packed": false               // 是否被加壳保护
}

查看方法:

# 用 file 命令查看
file constraint1.x86
# 输出: ELF 32-bit LSB executable, Intel 80386...

2.2 信息提取

是什么:从二进制中提取出人类可读的有意义信息,这些信息是理解程序行为的线索。

提取的内容

(1) 字符串提取

机器码无法直接看出文字,但程序运行时需要显示文字、连接网络,这些字符串会以明文形式存在:

# 从恶意软件中提取的字符串示例
http://evil.com/payload.exe
C:\Windows\System32\cmd.exe
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
Mutex_MyMalware_2023

这些字符串直接暴露了程序的意图:

  • URL → 要连接哪里
  • 文件路径 → 要操作什么文件
  • 注册表路径 → 要写什么注册表项
  • Mutex → 互斥体名(常用来防止多开)

(2) API调用提取

程序要操作文件、网络、注册表,必须调用操作系统API。从导入表可以提取:

// 程序调用了这些函数,暗示了它的行为
CreateFile        // 可能要读写文件
RegSetValue       // 可能要修改注册表(持久化)
socket            // 可能要网络通信
CreateRemoteThread // 可能要做进程注入(恶意行为)

Java视角理解:就像看一个Java类的import语句,虽然不知道具体实现,但能看出它用到了哪些库。

(3) 资源提取

二进制里可以嵌入其他文件:

  • 图标、图片
  • 配置文件
  • 其他可执行文件
  • 加密的payload

2.3 反汇编

是什么:把机器码(CPU能执行的字节)转换成汇编语言(人类勉强能读的低级语言)。

为什么需要:机器码是纯粹的字节,如 0x55 0x89 0xE5,没人能直接理解。

转换示例

机器码 (十六进制)      →      汇编指令
0x55                  →      push ebp
0x89 0xE5             →      mov ebp, esp
0x83 0xEC 0x40        →      sub esp, 0x40
0x8B 0x45 0x08        →      mov eax, [ebp+0x8]  ; 获取参数

示例对比

二进制: 01010101 10001001 11100101
汇编:   push ebp      # 把ebp压入栈
        mov ebp, esp  # 把esp的值赋给ebp
C语言:  function() {  # 函数开始

Java开发者视角:这相当于你看到的class文件是字节码,javap命令反汇编成可读格式。只是Java字节码比x86汇编更高级一些。


2.4 中间表示(IR)

是什么:把不同CPU架构(x86、ARM、MIPS)的汇编指令,统一转换成一种架构无关的中间语言

为什么需要

  • x86和ARM指令完全不同
  • 如果直接分析汇编,每种架构都要一套工具
  • 转换成统一IR后,分析逻辑可以复用

著名的IR

  • REIL:BinNavi使用的中间语言
  • VEX:Valgrind和angr使用的IR
  • LLVM IR:LLVM编译框架的IR
  • Ghidra的P-Code:NSA Ghidra使用的IR

例子:同一逻辑在不同架构的表现

x86汇编:
mov eax, [ebp+0x8]
add eax, 0x1

ARM汇编:
ldr r0, [sp, #0x8]
add r0, r0, #1

统一IR (REIL):
load  eax, [ebp+0x8]    // 读取
add   eax, eax, 0x1      // 加1
store [结果地址], eax    // 存回

好处:你只需要写一套分析规则,就能分析所有架构的二进制。


2.5 反编译

基本原理:通过分析汇编指令的模式(如函数调用、变量访问等),还原出高级语言结构。

是什么:把汇编语言(或IR)进一步转换成类似高级语言的伪代码,让分析者更容易理解程序逻辑。

为什么需要:汇编语言还是太底层,看几百行汇编很难理解整体逻辑。反编译尝试恢复出接近C语言的表达。

例子

汇编代码

push ebp
mov ebp, esp
sub esp, 0x10
mov dword [ebp-0x4], 0x0
cmp dword [ebp+0x8], 0x0
je 0x401020
mov eax, [ebp+0x8]
add eax, 0x5
mov [ebp-0x8], eax
jmp 0x401028
mov dword [ebp-0x8], 0x0
mov eax, [ebp-0x8]
leave
ret

反编译后的伪代码(IDA Pro/Ghidra输出):

int func(int input) {
    int result;
    if (input != 0) {
        result = input + 5;
    } else {
        result = 0;
    }
    return result;
}

Java开发者视角:这就像从字节码反编译回Java代码。


2.6 控制流图(CFG)

控制流图展示了程序的"路线图"——代码执行的顺序和可能的分支。

是什么:表示程序执行路径的图,展示代码中所有可能的执行顺序。

基本概念

  • 基本块:一段顺序执行的指令,只有一个入口、一个出口
  • :表示控制流转移(跳转、调用、返回)

一个简单函数的CFG

          ┌─────────────┐
          │  基本块A     │
          │  mov eax, 1 │
          │  cmp ebx, 0 │
          └──────┬──────┘
                  │
          ┌───────┴───────┐
          ▼               ▼
   ┌─────────────┐  ┌─────────────┐
   │  基本块B    │  │  基本块C    │
   │  je 条件成立│  │  jne条件不立│
   │  add eax, 1 │  │  sub eax, 1 │
   └──────┬──────┘  └──────┬──────┘
          └────────┬───────┘
                   ▼
          ┌─────────────┐
          │  基本块D    │
          │  ret       │
          └─────────────┘

为什么重要

  • 识别循环结构
  • 发现不可达代码(可能是混淆)
  • 理解程序逻辑
  • 找漏洞(比如路径未覆盖)

2.7 函数调用图

是什么:展示函数之间调用关系的图。

          ┌──────────┐
          │  main    │
          └────┬─────┘
               │
       ┌───────┼───────┐
       ▼       ▼       ▼
  ┌────────┐ ┌────────┐ ┌────────┐
  │ funcA  │ │ funcB  │ │ funcC  │
  └───┬────┘ └───┬────┘ └───┬────┘
      │          │          │
      └─────┬────┘          │
            ▼               │
       ┌────────┐           │
       │ funcD  │◄──────────┘
       └────────┘

提取的信息

  • 谁是入口点?
  • 哪些函数是关键函数?(被很多函数调用)
  • 有没有孤立函数?
  • 调用深度和路径

三、把这些概念串起来:一个完整流程

让我们用一个实际的恶意软件分析场景,展示所有这些概念如何协同工作:

场景:收到一个可疑的exe文件

步骤1:格式解析

  • 识别是Windows PE文件
  • 查看节表:有奇怪的节名,可能加壳
  • 导入表:调用了 VirtualAllocWriteProcessMemory(可疑!)

步骤2:信息提取

  • 字符串:发现 http://evil.com/payload.exe
  • 资源节:嵌入了一个加密的DLL
  • 时间戳:2023年,但编译器的版本看起来像旧的(可能伪造)

步骤3:反汇编/反编译

  • 将机器码转成汇编
  • 进一步反编译成伪代码
  • 发现关键逻辑:解密资源中的DLL,然后注入到explorer.exe

步骤4:构建中间表示

  • 统一成P-Code或REIL
  • 准备进行深入分析

步骤5:提取控制流图

  • 识别出解密循环
  • 发现反调试技巧(检测 IsDebuggerPresent
  • 发现条件跳转:如果检测到调试器,跳转到无害代码

步骤6:提取函数调用图

  • maincheck_debuggerdecrypt_payloadinject_to_process
  • 清晰看到程序的核心逻辑链

步骤7:综合分析

  • 这是一个恶意软件
  • 它用反调试躲避分析
  • 核心行为:下载payload,注入进程
  • 归因:代码特征和某个已知恶意家族相似

二进制分析如何工作

  1. 加载(Loading)

    proj = angr.Project("program.exe")
    # 解析文件格式,加载到虚拟内存
    
  2. 反汇编(Disassembly)

    block = proj.factory.block(0x80483ed)
    # 把二进制转成汇编指令
    
  3. 构建CFG

    cfg = proj.analyses.CFGFast()
    # 分析跳转指令,构建控制流图
    
  4. 提升到IR

    # 把x86指令转成VEX IR,统一格式
    
  5. 分析(Analysis)

    • 数据流分析:变量怎么传递
    • 符号执行:探索所有可能路径
    • 约束求解:找出满足条件的输入
  6. 反编译(Decompilation)

    dec = proj.analyses.Decompiler(func)
    # 把IR转成高级语言结构
    

四、作为Java开发者,如何理解这些概念?

从你熟悉的东西出发

Java世界二进制世界对应关系
.class 文件PE/ELF 文件都是编译后的格式
javap -c 反汇编IDA Pro反汇编都展示字节码/指令
JD-GUI反编译Ghidra反编译都恢复成伪代码
方法调用关系函数调用图都是调用关系
if-else/循环控制流图都是执行路径
Maven依赖导入表都是外部依赖

核心思维转变

维度Java开发二进制分析
信息有源代码,信息丰富丢失了高级信息,只有机器码
理解方式自上而下(读源码)自下而上(从指令反推)
工具IDE、调试器反汇编器、反编译器
难点业务逻辑复杂信息缺失、混淆保护

五、核心工具与其对应的概念

工具主要功能涉及的概念
Detect It Easy格式识别、查壳格式解析
Binwalk固件解包、扫描格式解析、信息提取
strings提取字符串信息提取
Ghidra反汇编、反编译、分析所有概念都涉及
IDA Pro商业级反汇编/反编译所有概念都涉及
x64dbg动态调试验证CFG、理解逻辑
Radare2开源逆向框架所有概念都涉及

六、总结:一张图理解全部

原始二进制文件 (.exe/.elf)
        ↓
   [格式解析] ─→ 元数据:入口点、节表、导入表
        ↓
   [反汇编] ───→ 汇编代码
        ↓
   [中间表示] ──→ 架构无关的IR (REIL/VEX/P-Code)
        ↓
   [反编译] ───→ 伪代码(类C语言)
        ↓
   [分析引擎]
        ├── [控制流图] ─→ 基本块、跳转关系、循环
        ├── [调用图] ───→ 函数调用关系
        └── [数据流] ───→ 变量如何传递
        ↓
   [信息提取] ──→ 字符串、API调用、资源
        ↓
   [分析结果] ──→ 这是什么?它做什么?是恶意吗?

为什么需要这些分析

  1. 恶意软件分析

    # 找出病毒做了什么
    - 修改了哪些文件?
    - 连接了哪些网络?
    - 有没有隐藏功能?
    
  2. 漏洞挖掘

    # 找出程序漏洞
    - 数组越界?
    - 缓冲区溢出?
    - 整数溢出?
    
  3. 逆向工程

    # 理解别人的程序
    - 算法是什么?
    - 协议怎么实现的?
    - 有没有后门?
    
  4. 代码优化

    # 分析性能瓶颈
    - 哪些函数执行最多?
    - 循环嵌套多深?
    - 内存访问模式?
    

真实检测示例

============================================================
分析二进制文件: ./test/constraint1.x86
============================================================

[1] 加载二进制文件...
    ✓ 架构: X86
    ✓ 入口点: 0x80482f0
    ✓ 字节序: Iend_LE

[2] 二进制元数据:
    - 文件类型: ELF
    - 加载地址: 0x8048000 - 0x804a01f
    - 段数量: 30: 0x8048000-0x804857c (文件偏移: 0x0) [RX]
        段 1: 0x8049f08-0x804a000 (文件偏移: 0xf08) [R]
        段 2: 0x804a000-0x804a020 (文件偏移: 0x1000) [RW]

[3] 符号信息:
    - deregister_tm_clones: 0x8048330
    - register_tm_clones: 0x8048360
    - frame_dummy: 0x80483c0
    - main: 0x80483ed

[4] 构建控制流图 (CFG)...
    发现 26 个函数
    发现 53 个基本块
    发现 66 条控制流边

[5] 函数列表 (前20个):
    0x8048294: _init                (大小:  35 字节, 基本块: 4)
    0x80482d0: __gmon_start__       (大小:  28 字节, 基本块: 2)
    0x80482e0: __libc_start_main    (大小:   6 字节, 基本块: 1)
    0x80482f0: _start               (大小:  33 字节, 基本块: 1)
    0x8048311: sub_8048311          (大小:   1 字节, 基本块: 1)
    0x8048312: sub_8048312          (大小:  14 字节, 基本块: 1)
    0x8048320: __x86.get_pc_thunk.bx (大小:   4 字节, 基本块: 1)
    0x8048324: sub_8048324          (大小:  12 字节, 基本块: 1)
    0x8048330: deregister_tm_clones (大小:  42 字节, 基本块: 5)
    0x804835a: sub_804835a          (大小:   6 字节, 基本块: 1)
    0x8048360: register_tm_clones   (大小:  55 字节, 基本块: 5)
    0x8048397: sub_8048397          (大小:   9 字节, 基本块: 1)
    0x80483a0: __do_global_dtors_aux (大小:  32 字节, 基本块: 4)
    0x80483be: sub_80483be          (大小:   2 字节, 基本块: 1)
    0x80483c0: frame_dummy          (大小:  44 字节, 基本块: 5)
    0x80483e7: sub_80483e7          (大小:   1 字节, 基本块: 1)
    0x80483ed: main                 (大小:  25 字节, 基本块: 1)
    0x8048406: sub_8048406          (大小:  10 字节, 基本块: 1)
    0x8048410: __libc_csu_init      (大小:  97 字节, 基本块: 7)
    0x8048471: sub_8048471          (大小:   2 字节, 基本块: 1)

[6] 函数调用图分析:
    发现 0 个函数有调用关系

    [main函数调用关系] @ 0x80483ed:
        没有调用其他函数

[7] 控制流图分析:
    main函数有 1 个基本块:
        块 0: 0x80483ed (大小: 25 字节)
            push ebp
            mov ebp, esp
            sub esp, 0x10
            ... 还有 8 条指令

[8] 基本块详细分析:
    分析地址 0x80483ed 的基本块:
        - 大小: 25 字节
        - 指令数: 11
        - VEX语句数: 49
        - 跳转类型: 条件跳转/间接跳转

    指令序列分析:
        [0] 0x80483ed: push ebp
        [1] 0x80483ee: mov ebp, esp
        [2] 0x80483f0: sub esp, 0x10
        [3] 0x80483f3: mov eax, dword ptr [ebp - 8]
        [4] 0x80483f6: mov edx, dword ptr [ebp - 0xc]

    数据流(寄存器使用):
        - 读取寄存器次数: 1
        - 写入寄存器次数: 1

[9] 路径分析:
    探索前向路径...
        - 活跃路径: 1
        - 终止路径: 0
        - 当前路径地址: 0x8048405

[10] 函数调用图摘要:
    被调用最多的函数:
        0x80483ed (main): 被 0 个函数调用

============================================================
分析完成!
统计: 26 个函数, 1 个调用关系
============================================================