AXUMORTEM 静态二进制分析引擎实战指南:ELF/PE/Mach-O 六道分析流水线、YARA 扫描与 MITRE ATT&CK 威胁评分
AXUMORTEM 静态二进制分析引擎实战指南:ELF/PE/Mach-O 六道分析流水线、YARA 扫描与 MITRE ATT&CK 威胁评分
AXUMORTEM 是 Cybersecurity-Projects 仓库中 PROJECTS/intermediate/binary-analysis-tool 项目实现的一款静态二进制分析引擎。它以 Rust 编写,解析 ELF、PE、Mach-O 三种主流可执行文件格式,将二进制依次送入六道分析流水线(格式解析、导入表提取、字符串提取、熵分析、反汇编、威胁评分),最终输出 0~100 的威胁分数并映射到 MITRE ATT&CK 技术编号。读完本指南,你将掌握静态恶意代码分类的完整实战方法:如何判断文件格式与结构异常、如何从导入表和字符串中识别注入链与 C2 行为、如何用 Shannon 熵识别加壳与加密、如何阅读反汇编与控制流图,以及如何把零散指标聚合为可解释的威胁评分。本文以 learn/00-OVERVIEW.md 为主线,结合仓库中的源码与配置进行纵深验证。
一、什么是 AXUMORTEM:一次上传,六道分析
AXUMORTEM 的核心定位是静态恶意代码分类器:你上传一个二进制文件,它返回一份完整分解报告——头部信息、导入表、字符串、熵值、反汇编,以及一个 0~100 的威胁分数。它绝不执行上传的样本,所有结论都来自对字节流的静态解读,因此非常适合作为恶意文件进入人工分析流程之前的第一道自动分流工具。
在真实世界中,这正是安全分析师处理可疑文件的起点:它是什么格式?导入了什么 API?是否被加壳?字符串泄露了什么意图?如下表所示,多个著名的供应链攻击事件都靠静态分析找到了突破口:
| 事件 | 时间 | 静态分析发现的关键线索 |
|---|---|---|
| SolarWinds SUNBURST | 2020.12 | 木马化的 SolarWinds.Orion.Core.BusinessLayer.dll 导入表出现意外网络 API,字符串包含硬编码 C2 域名生成逻辑,熵分布与正常 .NET 程序集不符 |
| CCleaner 供应链攻击 | 2017 | 木马化 PE 部分区段熵异常,导入表包含 VirtualAlloc + CreateRemoteThread 这一经典进程注入链 |
| 3CX 供应链入侵 | 2023.03 | DLL 高熵区段中隐藏加密 shellcode,存在指向调试器检测与进程镂空(process hollowing)的可疑导入链 |
这些案例正是 AXUMORTEM 逐项检测的指标类型。
从架构上看,仓库 backend/crates/axumortem-engine/src/lib.rs 中的 AnalysisEngine 将六个 pass 注册到一个 PassManager,每个 pass 是独立的分析单元。上传后返回的典型输出类似:
Analysis complete
Format: ELF | Architecture: x86_64 | Size: 15.2 KB
Threat Score: 12/100 (BENIGN)
你需要什么基础
- 必需:能顺畅阅读 Rust(所有权、trait、枚举、模式匹配);理解可执行文件基本概念(区段、头部、链接);熟悉 Web API(REST、JSON、HTTP 状态码);具备 React/TypeScript 基础。
- 可选:x86 汇编(反汇编模块会带你入门);YARA 规则语法(概念模块有讲解);恶意软件分析经验。
- 工具:Docker 与 Docker Compose;Rust 工具链(本地开发);Node.js 22+ 与 pnpm(前端开发);
just命令运行器(可选但推荐)。
快速启动
git clone https://github.com/CarterPerez-dev/Cybersecurity-Projects.git
cd Cybersecurity-Projects/PROJECTS/intermediate/binary-analysis-tool
docker compose up -d
访问 http://localhost:22784 上传一个二进制即可看到分析结果。开发模式(热重载)使用:
docker compose -f dev.compose.yml up -d
前端开发服务器运行在 http://localhost:15723,后端 API 在 3000 端口。生产环境由 compose.yml 编排 Nginx(:22784,负责静态资源与 /api 反向代理)、Axum 后端(:3000)与 PostgreSQL(:5432)三个容器,backend 默认限制 1G 内存,PostgreSQL 带 pg_isready 健康检查。
项目结构速览
binary-analysis-tool/
├── backend/
│ └── crates/
│ ├── axumortem-engine/ # 核心分析库(纯逻辑,无 HTTP/DB)
│ │ └── src/
│ │ ├── lib.rs # 引擎入口,编排所有 pass
│ │ ├── types.rs # BinaryFormat、Architecture、RiskLevel 等枚举
│ │ ├── context.rs # AnalysisContext,pass 间共享结果
│ │ ├── pass.rs # AnalysisPass trait + PassManager(拓扑排序)
│ │ ├── yara.rs # YARA 扫描器,内置 14 条规则
│ │ ├── formats/ # ELF、PE、Mach-O 解析器
│ │ └── passes/ # 六道分析 pass
│ └── axumortem/ # HTTP 服务(上传、查询、健康检查、数据库)
├── frontend/ # React 19 + TS 前端
├── infra/docker/ # 生产构建 Dockerfile
├── compose.yml # 生产编排
├── dev.compose.yml # 开发编排
└── justfile # 命令运行器
二、二进制格式解析:ELF、PE、Mach-O 的字节级基础
三种格式的本质
每个编译出的程序都以特定格式存储,操作系统据此将文件加载进内存并开始执行。三种主流格式分别是:
- ELF(Executable and Linkable Format)——Linux、BSD 及大多数 Unix 系统
- PE(Portable Executable)——Windows
- Mach-O(Mach Object)——macOS、iOS
它们不只是"机器码容器",而是描述内存布局、动态库依赖、符号表、重定位信息与安全属性的结构化元数据。格式解析是所有后续技术的地基:找不到 .text 区段就无法反汇编;解析不了导入表就无法标记可疑 API 用法;识别不出区段就无法计算逐区段熵。
ELF 采用分层结构:
+------------------+
| ELF Header | Magic bytes (7f 45 4c 46)、架构、入口点
+------------------+
| Program Headers | 段(Segment)——内核加载进内存的内容
+------------------+
| .text | 可执行代码
| .rodata | 只读数据(字符串、常量)
| .data | 已初始化的可写数据
| .bss | 未初始化数据(加载时清零)
| .symtab | 符号表(函数/变量名)
| .strtab | 字符串表(符号引用的名称)
| .dynamic | 动态链接信息
| .got / .plt | 全局偏移表 / 过程链接表
+------------------+
| Section Headers | 每个区段的元数据
+------------------+
PE 格式更复杂,携带 ELF 没有的额外元数据:
+------------------+
| DOS Header | 传统 MS-DOS stub("MZ" 签名)
+------------------+
| PE Signature | "PE\0\0" (50 45 00 00)
+------------------+
| COFF Header | 机器类型、区段数量、时间戳
+------------------+
| Optional Header | 入口点、镜像基址、子系统、数据目录
+------------------+
| Section Table | .text、.rdata、.data、.rsrc、.reloc
+------------------+
| Sections | 实际代码与数据
+------------------+
PE 的 Rich Header(编译器指纹)、TLS 回调表(在 main 之前执行的代码)与指向导入/导出/调试信息的数据目录都值得关注——恶意软件常利用 TLS 回调在调试器入口断点触发之前运行反调试代码。
AXUMORTEM 的统一解析入口
仓库通过 goblin crate 的自动检测从魔数识别格式并分发到对应解析器(见 formats/mod.rs):
pub fn parse_format(data: &[u8]) -> Result<FormatResult, EngineError> {
let object = goblin::Object::parse(data).map_err(|e| EngineError::InvalidBinary {
reason: e.to_string(),
})?;
match &object {
goblin::Object::Elf(elf_obj) => elf::parse_elf(elf_obj, data),
goblin::Object::PE(pe_obj) => pe::parse_pe(pe_obj, data),
goblin::Object::Mach(mach_obj) => macho::parse_macho(mach_obj, data),
_ => Err(EngineError::UnsupportedFormat { format: "unknown".into() }),
}
}
三种格式统一输出到同一份 FormatResult:共同属性(区段、段、入口点)使用相同类型;格式特有细节放入可选结构体(PeInfo、ElfInfo、MachOInfo)。ElfInfo 记录 gnu_relro、bind_now、stack_executable 与依赖库列表,PeInfo 记录镜像基址、子系统、DLL 特性(ASLR/DEP/CFG)、时间戳与 TLS 回调数,MachOInfo 记录是否为通用二进制、代码签名、最低系统版本与 dylib 列表。
结构异常检测:解析之外的信号
解析器不只是提取数据,还标记结构性问题。每个异常都是带证据的枚举变体(源码 FormatAnomaly 共 14 种,核心包括):
pub enum FormatAnomaly {
EntryPointOutsideText { ep: u64, text_range: (u64, u64) },
EntryPointInLastSection { ep: u64, section: String },
EntryPointOutsideSections { ep: u64 },
RwxSection { name: String },
EmptySectionName { index: usize },
StrippedBinary,
SuspiciousSectionName { name: String, reason: String },
VirtualRawSizeMismatch { name: String, virtual_size: u64, raw_size: u64, ratio: f64 },
OverlayData { offset: u64, size: u64 },
TlsCallbacksPresent { count: usize },
NoImportTable,
SuspiciousTimestamp { value: u32, reason: String },
// ...
}
为什么入口点位于最后一个区段值得警惕:UPX、Themida、ASPack 等加壳器在运行时解压代码,并把原始入口点(OEP)存到最后的区段再跳回。正常编译器把入口点放在 .text——它几乎从不位于最后一个区段。异常判定逻辑见 detect_common_anomalies:它会同时检查入口点是否落在 .text 之外、是否在最后区段、是否完全落在所有区段之外,以及区段是否 RWX、区段名是否为空或可疑、虚拟尺寸与原始尺寸比值是否超过 10 倍(VIRTUAL_RAW_RATIO_THRESHOLD = 10.0)。
仓库内置的 SUSPICIOUS_SECTION_NAMES 表覆盖 15 个加壳器指纹,例如 UPX0/UPX1/UPX2 → UPX packer、.themida → Themida、.vmp0/.vmp1 → VMProtect、.aspack/.adata → ASPack、.MPress1/.MPress2 → MPress、.enigma1/.enigma2 → Enigma。
现实案例:2010 年针对伊朗核离心机的 Stuxnet 是一个 PE 文件,格式分析发现多处异常:伪造的 2001 年编译时间戳、从 Realtek Semiconductor 窃取的 Authenticode 证书、熵值超过 7.9 的加密负载区段,以及指向西门子 S7-300 PLC 硬件的 DeviceIoControl 导入——任何正常办公软件都不会需要这个 API。
常见陷阱:区段名只是建议性的,恶意软件常把加壳代码区段命名为 .text 伪装正常。AXUMORTEM 不依赖名称,而是同时检查熵与权限。剥离符号的二进制(stripped)也不一定恶意——发布版构建普遍剥离——但结合高熵、可疑导入等其他指标时,剥离会推高威胁分数。
三、熵分析:Shannon 熵与加壳/加密识别
熵能告诉我们什么
Shannon 熵衡量数据的随机性。二进制分析中,熵值指示区段中数据种类:纯文本约 3-4 比特/字节,编译后的原生代码 5-6,压缩数据 6.5-7,加密或加壳数据逼近理论最大值 8 比特/字节。
公式计算每个字节的平均"意外程度":
H = -Σ p(x) * log₂(p(x)) 对每个可能的字节值(0-255)
若每个字节值出现频率完全相等(理想随机),熵为 8.0;若只有一种字节值(全零),熵为 0.0。
加壳与加密是恶意软件隐藏负载的两大常用手段:加壳二进制压缩真实代码并附带一个小的解压 stub 在运行时解压;加密同理。两种技术都会产生熵高于 7.0 的区段——这在初步分流中是强信号。
AXUMORTEM 把熵分为五档(阈值见 passes/entropy.rs):
Plaintext : < 3.5 (配置文件、字符串表)
Native Code : < 6.0 (编译的 x86/x64/ARM 指令)
Compressed : < 7.0 (zlib、gzip、资源数据)
Packed : < 7.2 (UPX、Themida、ASPack)
Encrypted : >= 7.2 (AES 加密负载、XOR 混淆代码)
熵的 Rust 实现
核心计算只用固定大小数组与一次遍历(O(n)):
fn shannon_entropy(data: &[u8]) -> f64 {
if data.is_empty() { return 0.0; }
let mut freq = [0u64; BYTE_RANGE]; // 256 个计数器
for &byte in data { freq[byte as usize] += 1; }
let len = data.len() as f64;
freq.iter()
.filter(|&&c| c > 0) // 跳过 log2(0) = -∞
.map(|&c| { let p = c as f64 / len; -p * p.log2() })
.sum()
}
filter(|&&c| c > 0) 很关键——log2(0) 是负无穷,从未出现的字节值必须被跳过。源码中的单元测试验证了全零数据熵 < 0.001、均匀分布熵 ≈ 8.0、空数据熵为 0,以及各档阈值的分类正确性。
逐区段分析:整体熵会掩盖真相
二进制可能 .text 区段正常(熵约 5.5)而 .rsrc 区段加壳(熵约 7.8)。整体熵可能被平均到约 6.5,看起来只是压缩数据——逐区段分析才能抓住整体分析漏掉的部分。analyze_entropy 对每个区段独立计算并打 EntropyFlag 标记:
HighEntropy(> 7.0)HighVirtualToRawRatio(虚拟/原始尺寸比 > 10)EmptyRawData(raw=0 但 virtual>0)Rwx(读+写+执行权限)PackerSectionName(命中加壳器区段名表)
加壳检测:熵之外的组合信号
单靠熵不够。AXUMORTEM 组合多重信号:
- 已知区段名:
PACKER_SECTION_NAMES表(源码中 16 项)把UPX0/UPX1/UPX2 → UPX、.vmp0/.vmp1/.vmp2 → VMProtect、PEC2TO/PEC2/pec1 → PECompact、.MPRESS1/.MPRESS2 → MPRESS等区段名映射到加壳器。 - 虚拟对原始尺寸比:加壳器分配大的虚拟内存但磁盘上只存最小数据(压缩负载),比值超过 10:1 可疑。
- 入口点 PUSHAD:UPX 和许多加壳器以
PUSHAD(操作码0x60)开头保存全部寄存器再执行解压例程。AXUMORTEM 检查入口点偏移处的首字节。 - RWX 区段:正常二进制几乎不会有读、写、执行同时开启的区段。加壳器需要 RWX,因为它们把解压代码写回自己正在执行的区段。
判定逻辑是:识别出加壳器名称,或至少 2 个结构指标命中(STRUCTURAL_INDICATORS_FOR_PACKING = 2)即判定加壳,避免单个异常区段造成误报:
let packing_detected = packer_name.is_some()
|| structural_count >= STRUCTURAL_INDICATORS_FOR_PACKING;
现实案例:2017 年 5 月的 WannaCry 勒索病毒感染了 150 多个国家的 20 万余系统,其初始投放器是 PE 文件,.rsrc 资源区段熵达 7.89——明显加密,其中藏着真正的勒索负载。同年的 NotPetya(全球损失约 100 亿美元)通过乌克兰税务软件 M.E.Doc 的更新分发,被篡改的二进制两个高熵区段与原始文件不匹配:一个装着 EternalBlue 漏洞利用,一个装着 Mimikatz 凭据转储器。
四、导入与 API 分析:从导入表识别攻击技法
二进制调用操作系统函数(打开文件、分配内存、创建网络连接)时,会把这些函数列在导入表中。导入表是从库名到函数名的映射,动态链接器加载时解析。某些 API 组合几乎是特定攻击技法的签名:VirtualAllocEx + WriteProcessMemory + CreateRemoteThread 是进程注入,NtUnmapViewOfSection + SetThreadContext 是进程镂空。这些组合几乎出现在每款使用对应技法的 Windows 恶意软件中。
AXUMORTEM 维护可疑 API 数据库,每个 API 都带技术类别标签与 MITRE ATT&CK ID(见 passes/imports.rs 中的 SUSPICIOUS_APIS,共 22 个):
pub const SUSPICIOUS_APIS: &[SuspiciousApiDef] = &[
SuspiciousApiDef { name: "VirtualAllocEx", tag: "injection", mitre_id: "T1055" },
SuspiciousApiDef { name: "WriteProcessMemory", tag: "injection", mitre_id: "T1055" },
SuspiciousApiDef { name: "CreateRemoteThread", tag: "injection", mitre_id: "T1055" },
SuspiciousApiDef { name: "NtUnmapViewOfSection", tag: "hollowing", mitre_id: "T1055.012" },
SuspiciousApiDef { name: "IsDebuggerPresent", tag: "anti-debug", mitre_id: "T1622" },
// ... 共 22 个可疑 API
];
系统还检测 14 种可疑 API 组合——一组导入同时出现才指向特定技法。单独导入 VirtualAllocEx 只是可疑;同时导入 VirtualAllocEx + WriteProcessMemory + CreateRemoteThread 几乎必然是进程注入。组合检测实现为:把所有导入收集进 HashSet,再检查特定组是否全部存在。
Linux 专属分析
导入分析不只针对 Windows。AXUMORTEM 标记 Linux 专属指标:
ptrace+PTRACE_ATTACH——Linux 进程注入(T1055.008)mmap+mprotect且带PROT_EXEC——运行时代码生成或注入dlopen+dlsym——动态库加载,rootkit 常见connect+execve——反弹 shell 模式bind+listen+accept——网络监听(潜在后门)
现实案例:2016 年的 Mirai 僵尸网络是面向 IoT 设备的 ELF 二进制。其导入表对嵌入式设备二进制来说不同寻常:既有 socket/connect/send/recv(网络扫描器预期),也有 execve/fork/kill——网络 API 与进程管理 API 的组合将其标记为僵尸代理。剥离符号的 Mirai 只有 14 个唯一导入,远少于正常应用。
常见陷阱:不要孤立看待导入——VirtualAlloc 是最常用的 Windows API 之一,只有与 WriteProcessMemory、CreateRemoteThread 组合才可疑,组合检测避免了这类误报。同时,恶意软件常通过 GetProcAddress 或 dlsym 在运行时解析 API,导入表看起来很干净,但字符串分析能发现二进制内嵌的 API 名——这正是字符串 pass 与导入分析协同工作的原因。
五、字符串分析:最快速的威胁情报提取
字符串是能从二进制中提取的最快捷情报:文件路径暴露目标,URL 暴露 C2 服务器,注册表键暴露持久化机制,shell 命令暴露后渗透行为,甚至调试残留能暴露开发者的构建环境。
AXUMORTEM 以 ASCII 与 UTF-16LE 两种编码提取字符串,并分类为 14 个类别(见 passes/strings.rs 与 types.rs 中的 StringCategory):
pub enum StringCategory {
Url, // http://、https://、ftp://
IpAddress, // IPv4 与 IPv6 地址
FilePath, // /etc/passwd、C:\Windows\System32
RegistryKey, // HKEY_LOCAL_MACHINE\...
ShellCommand, // cmd.exe /c、powershell、/bin/bash
CryptoWallet, // Bitcoin、Ethereum 地址
Email, // user@domain.com
SuspiciousApi, // 未出现在导入表中的 API 名
PackerSignature, // UPX!、.themida、MPRESS
DebugArtifact, // /rustc/、.pdb、DWARF
AntiAnalysis, // VMware、VirtualBox、Sandboxie、x64dbg
PersistencePath, // CurrentVersion\Run、crontab、systemd
EncodedData, // Base64 编码块(至少 20 字符)
Generic, // 其他
}
其中 7 个类别被标记为可疑并直接喂给威胁评分:SuspiciousApi、PackerSignature、AntiAnalysis、PersistencePath、EncodedData、ShellCommand 与 CryptoWallet。
关键实现细节
- 最小长度 4 字符(
MIN_STRING_LENGTH = 4),ASCII 提取扫描可打印字节(0x20-0x7E 加 tab、换行、回车)。 - UTF-16LE 提取:Windows 二进制常以 UTF-16LE 存储字符串(每字符两字节小端序)。
http://evil.com存为 UTF-16LE 后 hex dump 看起来是h\x00t\x00t\x00p\x00...。只扫 ASCII 会漏掉 PE 二进制的一半情报,因此两种编码都扫。 - 分类器为优先级链:
URL_PREFIXES、UNIX_PATH_PREFIXES、REGISTRY_PREFIXES、SHELL_INDICATORS、PACKER_SIGNATURES、DEBUG_ARTIFACTS、ANTI_ANALYSIS_INDICATORS等常量数组做模式匹配,性能上刻意不用正则。 - Base64 检测:用结构校验而非解码——至少 20 字符、仅含合法 Base64 字符、有正确的尾部填充。特别检查
TVqQ、TVpQ、TVoA、TVpB前缀——这些是 "MZ" PE 头签名的 Base64 编码,意味着 Base64 块中隐藏着内嵌的可执行文件。
每个字符串都携带偏移、编码、类别、可疑标记与所在区段(若落在已知区段边界内),并汇总出 StringStatistics(总数、按编码/类别统计、可疑数),供威胁评分与前端快速展示。
现实案例:APT29 的 SUNBURST 后门高度复杂,但字符串分析依然奏效:恶意负载包含硬编码 DGA 种子、Base64 编码的配置块,以及引用反分析工具(apimonitor、dnspy、ilspy、fiddler、wireshark)的字符串——这些字符串用于检测分析师环境并停用后门,同时也成了检测的签名。
六、YARA 规则:行业标准的恶意软件签名引擎
YARA 是为恶意软件识别设计的模式匹配引擎。一条 YARA 规则定义一组字符串模式与一个布尔条件,条件满足即触发。威胁情报源、杀毒引擎与 EDR 产品都使用 YARA 作为共享签名的事实标准。规则有三部分:元数据(描述、严重性)、字符串(待匹配模式)、条件(组合这些模式的布尔逻辑):
rule suspicious_process_injection {
meta:
description = "Detects potential process injection capabilities"
category = "injection"
severity = "critical"
strings:
$api1 = "VirtualAllocEx"
$api2 = "WriteProcessMemory"
$api3 = "CreateRemoteThread"
$api4 = "NtUnmapViewOfSection"
condition:
($api1 and $api2 and $api3) or ($api4 and $api2)
}
该规则在二进制同时包含进程注入三件套,或包含进程镂空对时触发。
AXUMORTEM 内置的 14 条规则
AXUMORTEM 内置 14 条 YARA 规则,覆盖主要恶意软件类别(完整规则嵌入在 yara.rs 的 BUILTIN_RULES 常量中):
| 规则 | 类别 | 严重性 | 检测内容 |
|---|---|---|---|
suspicious_upx_packed |
packer | medium | UPX 区段名 + 魔数 |
suspicious_anti_debug |
evasion | high | IsDebuggerPresent、INT 2Dh 陷阱 |
suspicious_process_injection |
injection | critical | 经典注入 API 链 |
suspicious_keylogger |
spyware | high | GetAsyncKeyState、SetWindowsHookEx |
suspicious_crypto_mining |
miner | medium | Stratum 矿池 URL、挖矿算法 |
suspicious_persistence |
persistence | high | Run 键、服务创建、schtasks |
suspicious_network_backdoor |
backdoor | high | bind/listen/accept + cmd/shell 模式 |
suspicious_ransomware |
ransomware | critical | 加密 API + 赎金通知字符串 |
suspicious_shellcode |
shellcode | critical | NOP sled、egg hunter、常见 stub |
suspicious_obfuscation |
obfuscation | medium | XOR 循环、自修改代码模式 |
suspicious_linux_anti_debug |
evasion | high | /proc/self/status TracerPid 检查 |
suspicious_linux_persistence |
persistence | high | crontab、systemd、.bashrc 修改 |
suspicious_linux_c2 |
c2 | critical | 反弹 shell 模式、编码命令 |
suspicious_mpress |
packer | medium | MPRESS 加壳区段签名 |
以 UPX 规则为例(与源码一致):
rule suspicious_upx_packed {
meta:
description = "Detects UPX packed binaries"
category = "packer"
severity = "medium"
strings:
$upx0 = "UPX0"
$upx1 = "UPX1"
$upx_magic = { 55 50 58 21 }
condition:
($upx0 and $upx1) or $upx_magic
}
只要同时出现 "UPX0" 与 "UPX1" 字符串,或命中 UPX 魔数 0x55 0x50 0x58 0x21(ASCII "UPX!")即触发。hex 模式 { 55 50 58 21 } 可在任意偏移匹配原始字节。
为什么用 yara-x
AXUMORTEM 使用 yara-x(YARA 的 Rust 重写)而非原版 C 实现。yara-x 与现有 YARA 规则完全兼容,但新增编译期校验、更好的错误信息,并消除了 FFI 开销。扫描器在引擎初始化时一次性编译全部规则,之后每个二进制复用同一份编译后的规则集(YaraScanner::new() 编译内置规则,with_custom_rules() 还能从目录加载额外 .yar/.yara 文件)。扫描结果 YaraMatch 携带规则名、标签、元数据与命中的字符串标识符。
现实案例:Flame(sKyWIper,2012 年 5 月发现)是迄今最复杂的恶意软件之一——20MB 模块化网络间谍工具包。它的识别部分依赖 YARA 规则匹配嵌入 Windows DLL 的 Lua 脚本引擎,以及其用于存储窃取数据的自定义 SQLite 数据库签名。没有单条规则能抓住它,但多个低置信度匹配的组合产生了高置信度结论——这正说明规则覆盖面广度的重要。
七、反汇编与控制流图:递归下降的代码结构分析
反汇编把原始机器码字节还原为人类可读的汇编指令。控制流图(CFG)映射执行如何在基本块(单一入口、单一出口的指令序列)之间流动。
AXUMORTEM 用 iced-x86 crate 反汇编 x86/x86_64 二进制。从入口点与符号表提示出发,采用递归下降:跟随每条指令,遇到分支时把两个目标加入工作队列:
Basic Block: 0x401000 - 0x40100F
┌────────────────────────────────────────┐
│ push rbp │
│ mov rbp, rsp │
│ sub rsp, 0x20 │
│ cmp dword [rbp-4], 0 │
│ jne 0x401020 │
└──────────┬────────────────┬────────────┘
│ (false) │ (true)
v v
┌──────────────────┐ ┌──────────────────┐
│ Block: 0x401010 │ │ Block: 0x401020 │
│ call 0x402000 │ │ xor eax, eax │
│ jmp 0x401030 │ │ jmp 0x401030 │
└──────────┬───────┘ └──────────┬───────┘
│ │
v v
┌───────────────────────────┐
│ Block: 0x401030 │
│ leave │
│ ret │
└───────────────────────────┘
每个节点是一个基本块,每条边代表一条可能执行路径:条件分支产生两条边(真/假),无条件跳转一条,调用产生指向被调方的边。
实现细节
disassemble 函数从种子地址(入口点 + 符号表提示)出发维护函数工作队列,用 visited_functions 集合防死循环(递归调用),从调用指令发现新函数并加入队列。关键阈值见 passes/disasm.rs:
const MAX_FUNCTIONS: usize = 1000;
const MAX_INSTRUCTIONS: usize = 50_000;
AXUMORTEM 将反汇编上限设为 1,000 个函数与 50,000 条指令,防止在大型二进制(完整 Chromium 构建有数百万指令)上分析挂起;CFG 可视化对每个函数限制 500 条指令以保持前端响应。非 x86 架构(ARM、AArch64、MIPS、RISC-V)会拿到空的反汇编结果而非错误——格式解析、导入、字符串、熵、威胁评分照常进行,只是反汇编标签页为空。
为什么 CFG 重要
CFG 揭示原始反汇编列表难以呈现的程序结构:分析师面对 10,000 条平铺指令,很难看出存在一个每次迭代都检查调试器的循环,或一个函数有 15 个错误分支却只有一条成功路径。CFG 让这些模式一目了然,也是更高级分析(死代码检测、循环识别、函数相似度评分、反编译)的地基。
现实案例:方程式组织(Shadow Brokers 于 2016 年曝光)创造了包括 EQUATIONDRUG 与 GRAYFISH 在内最复杂的恶意软件。卡巴斯基的分析高度依赖控制流图比较来把不同样本关联到同一开发团队——尽管字节级模式千变万化,关键函数(尤其自定义加密例程)的逻辑图仍可识别。这就是结构化分析的力量:字节模式变了,逻辑图却更难伪装。
八、威胁评分:8 大类别聚合成 0-100 分
威胁评分把每道 pass 的个别发现聚合成一个量化恶意程度的 0-100 总分。AXUMORTEM 在八个独立类别上打分,每类有最大分值上限(常量定义见 passes/threat.rs):
┌──────────────────────────────────────────────┐
│ Category │ Max Points │ │
├──────────────────────────┼────────────┤ │
│ Import/API Analysis │ 20 │ │
│ Entropy Analysis │ 15 │ │
│ Packing Detection │ 15 │ │
│ String Analysis │ 10 │ │
│ Section Anomalies │ 10 │ │
│ Entry Point Anomalies │ 10 │ │
│ Anti-Analysis Detection │ 10 │ │
│ YARA Matches │ 10 │ │
├──────────────────────────┼────────────┤ │
│ Maximum Total │ 100 │ │
└──────────────────────────────────────────────┘
每个类别独立打分并在其上限封顶。例如源码中导入评分的具体权重:注入链 +15、进程镂空链 +15、凭据访问 +12、APC 注入 +8、反调试 API +8、下载执行 +6、注册表 Run 键持久化 +7、导入极少 +5;Linux 链:ptrace +8、RWX 内存 +5、C2 连接 +10、网络监听 +8、动态加载 +5、进程注入 +15。熵评分中高熵区段每个 +6(封顶 2 个)、整体熵超 6.8 再 +3。这防止单个嘈杂类别压垮最终得分。
风险等级
总分映射到五个风险等级(阈值常量与源码一致):
const BENIGN_MAX: u32 = 15;
const LOW_MAX: u32 = 35;
const MEDIUM_MAX: u32 = 55;
const HIGH_MAX: u32 = 75;
| 分数区间 | 风险等级 | 典型二进制 |
|---|---|---|
| 0-15 | BENIGN | 标准应用、系统工具 |
| 16-35 | LOW | 带一些可疑特性的合法工具 |
| 36-55 | MEDIUM | 双用途工具(Sysinternals、Metasploit 模块) |
| 56-75 | HIGH | 很可能恶意,存在强行为指标 |
| 76-100 | CRITICAL | 活跃恶意软件,多项技法确认 |
基于证据的评分:不是黑盒
威胁分数的每一分都附带规则名与理由。分析师能看到确切触发规则,而非笼统的"它得了 72 分":
Import Analysis: 15/20
- Process injection chain (VirtualAllocEx + WriteProcessMemory + CreateRemoteThread): +15
Entropy Analysis: 9/15
- Section .upx0 entropy 7.45 exceeds threshold: +6
- Overall entropy 6.92 exceeds 6.8: +3
Packing Detection: 8/15
- Known packer section name "UPX0": +5
- PUSHAD opcode at entry point: +3
YARA Matches: 5/10
- suspicious_upx_packed (packer): +3
- suspicious_process_injection (injection): +5 (capped at 10)
Total: 37/100 — MEDIUM
compute_threat_score 让每个 score_* 函数独立评估类别并返回带逐条证据的 ScoringCategory,generate_summary 再按得分排序输出前 5 条关键发现。
MITRE ATT&CK 映射
每条发现映射到 ATT&CK 技术编号,把静态分析结果接入更大的威胁情报生态:
- T1055 — 进程注入(VirtualAllocEx、WriteProcessMemory)
- T1055.012 — 进程镂空(NtUnmapViewOfSection)
- T1055.004 — APC 注入(QueueUserAPC)
- T1622 — 调试器逃避(IsDebuggerPresent、NtQueryInformationProcess)
- T1547.001 — 启动自动启动:注册表 Run 键
- T1543.003 — 创建/修改系统服务
- T1105 — 入站工具传输(URLDownloadToFile)
- T1134 — 访问令牌操纵(OpenProcessToken、AdjustTokenPrivileges)
- T1140 — 去混淆/解码数据(CryptDecrypt)
MITRE 技术编号从导入组合与逐 API 映射去重生成,覆盖 Execution、Persistence、Defense Evasion、Credential Access、Command and Control 五个战术维度。
九、架构核心:依赖有序的 pass 流水线
AnalysisPass trait 与密封模式
流水线是架构的中心点。每个分析能力都是一个"pass"——从共享上下文读取、做自己的工作、写回结果的隔离单元。每个 pass 实现此 trait(见 pass.rs):
pub trait AnalysisPass: private::Sealed + Send + Sync {
fn name(&self) -> &'static str;
fn dependencies(&self) -> &[&'static str];
fn run(&self, ctx: &mut AnalysisContext) -> Result<(), EngineError>;
}
name() 返回唯一标识;dependencies() 返回必须先运行的 pass 名;run() 做实际工作。Sealed 超 trait 用 mod private 隐藏,只有 axumortem-engine crate 内部能实现 AnalysisPass——防止外部定义可能破坏不变量(如声明循环依赖)的 pass。这是密封 trait 模式,对安全工具而言,pass 顺序正确性值得牺牲一点灵活性。
AnalysisContext:类型化黑板
AnalysisContext 是流经整个流水线的可变状态袋(见 context.rs)。每个结果字段都是 Option<T>,pass 在运行时检查依赖是否已产出:
pub struct AnalysisContext {
source: BinarySource,
pub sha256: String,
pub file_name: String,
pub file_size: u64,
pub format_result: Option<FormatResult>,
pub import_result: Option<ImportResult>,
pub string_result: Option<StringResult>,
pub entropy_result: Option<EntropyResult>,
pub disassembly_result: Option<DisassemblyResult>,
pub threat_result: Option<ThreatResult>,
}
BinarySource 支持两种模式:Mapped(Mmap) 内存映射大文件,Buffered(Arc<[u8]>) 缓冲网络上传的字节。两者都实现 AsRef<[u8]>,pass 通过统一的 data() 方法访问,不必关心模式。
拓扑排序:Kahn 算法保证依赖顺序
PassManager 用 Kahn 算法(基于 BFS 的拓扑排序)排序 pass,无论注册顺序如何都保证每个 pass 在依赖之后运行。若最终顺序未包含全部 pass 则存在环——会在引擎构造时 panic,而不是分析时静默失败:
assert_eq!(order.len(), n,
"cycle detected in pass dependencies — this is a programmer error");
Pass 依赖图:
FormatPass (no dependencies)
│
├──────────┬──────────┬──────────┐
│ │ │ │
v v v v
ImportPass StringPass EntropyPass DisasmPass
│ │ │
│ │ │
└──────────┴──────────┘
│
v
ThreatPass (depends on all previous)
ThreatPass 依赖全部五个前序 pass(源码中 dependencies() 返回 &["format", "imports", "strings", "entropy", "disasm"]),并在此时运行 YARA 扫描——因为 YARA 匹配只喂给威胁分数,流水线别处不用。
容错执行:单 pass 失败不中断
run_all 顺序执行所有 pass 并逐个计时。某 pass 失败时错误被记录但执行继续——反汇编失败不应阻止威胁评分利用已有的导入与熵数据。PassReport 收集每个 PassOutcome(名称、成功与否、耗时、错误消息),并支持 all_succeeded() 与 failed_passes() 查询。pass.rs 中的单元测试用 MockPass 验证了拓扑顺序、菱形依赖、失败延续、环检测与耗时记录。
工厂注册与宏序列化
所有 pass 在 AnalysisEngine::new() 的单个 vec![] 注册(lib.rs),注册顺序无关紧要。新增 pass 只需加一行注册 + 一个实现。上传路由用 add_pass! 宏把六个 pass 结果统一序列化为 JSONB(routes/upload.rs),消除六段几乎相同的代码:
add_pass!(format_result, "format");
add_pass!(import_result, "imports");
add_pass!(string_result, "strings");
add_pass!(entropy_result, "entropy");
add_pass!(disassembly_result, "disasm"); // 经 PASS_NAME_MAP 改为 "disassembly"
add_pass!(threat_result, "threat");
十、HTTP 层与数据流:上传、去重、持久化、检索
三条路由
PUT /api/upload—— 上传二进制,返回 slugGET /api/analysis/{slug}—— 检索完整分析结果GET /health—— 存活探针
上传处理器(routes/upload.rs)接收 multipart 文件,计算 SHA-256,先查缓存:
pub async fn handle(
State(state): State<AppState>,
mut multipart: Multipart,
) -> Result<Json<UploadResponse>, ApiError> {
let (file_name, data) = extract_file(&mut multipart).await?;
let sha256 = axumortem_engine::sha256_hex(&data);
if let Some(slug) = queries::find_slug_by_sha256(&state.db, &sha256).await? {
return Ok(Json(UploadResponse { slug, cached: true }));
}
let engine = Arc::clone(&state.engine);
let name_clone = file_name.clone();
let (ctx, report) =
tokio::task::spawn_blocking(move || engine.analyze(&data, &name_clone)).await?;
// ... 事务性插入 analyses 行 + 六行 pass_results ...
Ok(Json(UploadResponse { slug, cached: false }))
}
SHA-256 检查-并-返回模式意味着相同二进制绝不会被分析两次:首次上传跑完整流水线(大文件可能耗时数秒),此后同文件上传立即从缓存返回。spawn_blocking 至关重要——分析是 CPU 密集(解析、熵计算、反汇编),若在异步执行器上运行会阻塞其他请求(健康检查、缓存检索都会挂起)。spawn_blocking 把工作移到 Tokio 的阻塞线程池。
slug 是 SHA-256 的前 12 个十六进制字符(SLUG_LENGTH = 12),确定性且不可枚举,48 位碰撞抵抗(约 281 万亿可能值)对单实例工具绰绰有余。
数据库模型
CREATE TABLE analyses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
sha256 TEXT NOT NULL UNIQUE,
file_name TEXT NOT NULL,
file_size BIGINT NOT NULL,
format TEXT NOT NULL DEFAULT '',
architecture TEXT NOT NULL DEFAULT '',
entry_point BIGINT,
threat_score INTEGER,
risk_level TEXT,
slug TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE pass_results (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
analysis_id UUID NOT NULL REFERENCES analyses(id) ON DELETE CASCADE,
pass_name TEXT NOT NULL,
result JSONB NOT NULL,
duration_ms INTEGER
);
两张表:analyses 存摘要元数据(列表视图),pass_results 以 JSONB 存每道 pass 的详细输出。sha256 的 UNIQUE 约束是去重键。检索时按 pass_name 匹配对应反序列化器,把 JSONB 重建为类型化响应。
为什么选 JSONB
pass 结果模式随演化变化。给 EntropyResult 加字段需要零数据库迁移——JSONB 列接受序列化器产出的任何内容。规范化表每次模式变更都要迁移。PostgreSQL 的 JSONB 索引仍保证需要时对 pass 结果的查询够快。
十一、配置与运维:环境变量、常见问题、性能与扩展
后端配置(config.rs)
后端通过 clap 解析,每个字段同时支持 --long 标志与环境变量:
| 参数 | 默认值 | 说明 |
|---|---|---|
DATABASE_URL |
(必填) | PostgreSQL 连接串 |
HOST |
0.0.0.0 |
监听地址 |
PORT |
3000 |
监听端口 |
MAX_UPLOAD_SIZE |
52428800(50 MiB) |
上传上限,防内存耗尽 |
CORS_ORIGIN |
* |
跨域来源限制 |
常见问题排查
Docker 构建 yara-x 失败:yara-x crate 构建时需要 protobuf-compiler。生产 Dockerfile 已安装,本地构建需要 apt install protobuf-compiler(Debian/Ubuntu)或 brew install protobuf(macOS)。
SQLx 编译期错误:SQLx 在编译期对数据库检查查询。数据库未运行时设置 SQLX_OFFLINE=true 使用缓存的查询元数据,仓库中的 sqlx-data.json 提供该缓存。
前端连不上 API:开发模式下 Vite dev server 把 /api 代理到后端。确认 backend 容器运行在 3000 端口,核对 dev.compose.yml 的端口映射。
分析返回空反汇编:反汇编只运行在 x86/x86_64 二进制上。ARM 与 AArch64 二进制其余 pass 正常完成,但反汇编标签页为空。
分析无结果:检查 PassReport。常见原因:二进制小于 4 字节无法解析,或格式不是 ELF/PE/Mach-O。
威胁分数看似不对:检查 ThreatResult 的 categories 数组,每类显示单项分、上限与带证据的触发规则,对照二进制验证正确性。
YARA 规则不匹配:用 strings 工具或 hex 编辑器确认字符串确实存在。YARA 字符串匹配默认区分大小写——"VirtualAllocEx" 不会匹配 "virtualallocex"。
性能考量与优化
瓶颈:反汇编(递归下降最慢,50k 指令上限约束最坏情况);YARA 扫描(编译昂贵但只在启动时一次);熵计算(每个字节一次频次查找,50MB 二进制即 5000 万次)。
优化:SHA-256 去重是最有影响力的优化;YARA 在 YaraScanner::new() 编译一次并复用;逐区段熵是 O(总字节数)(常量因子 256)而非滑动窗口的 O(n × window);spawn_blocking 保持异步运行时响应。
扩展:垂直扩展调 TOKIO_WORKER_THREADS 与阻塞线程池大小;水平扩展时引擎无状态——唯一共享状态是 PostgreSQL,多实例负载均衡无需改代码,数据库中的 SHA-256 去重跨实例也防止重复工作。
设计决策要点
- 为何用 pass 而非单体:模块化(增删能力互不影响)、可测试(每个 pass 独立单测)、部分结果(反汇编失败仍有导入分析)、清晰依赖管理(拓扑排序)。
- 为何用 Rust:二进制解析本质上是不安全领域——把不可信字节解释为结构数据。Rust 的所有权模型防止了困扰 C/C++ 分析工具的缓冲区溢出、use-after-free 与数据竞争。
goblin安全处理格式解析,iced-x86提供高性能反汇编,yara-x消除 FFI 开销。 - 为何用 SHA-256 slug 而非自增 ID:确定性——同一二进制永远产生同一 URL;无顺序 ID 可枚举。
已知限制
- 仅静态分析:无沙箱执行、无行为监控。运行时解密负载的二进制会显示加密熵但看不到解包代码。
- 仅 x86/x86_64 反汇编:ARM、MIPS、RISC-V 二进制获得其余 pass 但无反汇编/CFG。
- 无交叉引用分析:反汇编器不追踪数据引用(哪个函数读哪些字符串),只追踪控制流。
- 单文件分析:无法把多二进制包(MSI 安装器、带原生库的 APK)作为整体分析。
- 无增量分析:改 YARA 规则需重新分析所有二进制,无法只对缓存数据重跑单个 pass。
十二、学习路径与延伸阅读
本指南对应 learn/00-OVERVIEW.md,后续学习模块依次深入:
| 模块 | 主题 |
|---|---|
| 01 - Concepts | 二进制格式内部、熵理论、YARA 规则、威胁建模 |
| 02 - Architecture | pass 流水线设计、数据流、组件交互 |
| 03 - Implementation | 每道 pass 的代码级讲解与真实代码片段 |
| 04 - Challenges | 扩展练习:新 pass、新格式、仪表盘 |
仓库还提供了测试用的真实样本:test-specimens/ 目录下有用 C 源码(clean_hello.c、crypto_tool.c、many_imports.c、packed_stub.c、suspicious_beacon.c)编译的干净/剥离/加壳/可疑样本,引擎单元测试则使用 backend/crates/axumortem-engine/tests/fixtures/ 中的 hello_elf 与 hello_elf_stripped 真实 ELF fixture。运行测试:
cd backend
cargo test
# 只测某个 pass
cargo test -p axumortem-engine --lib passes::entropy
概念自测
- 一个二进制整体熵 7.4,只有 3 个导入(
GetProcAddress、LoadLibraryA、VirtualAlloc),入口点在最后区段且命名为UPX1。你能得出什么结论?AXUMORTEM 的哪些评分类别会触发? - 你分析两个
.text区段 SHA-256 完全相同的二进制,但导入表不同:一个只导入ReadFile+WriteFile,另一个导入ReadFile+WriteFile+CreateRemoteThread+VirtualAllocEx。威胁分数有何差异?第二个二进制可能被如何篡改? - 一个 PE 二进制无可疑导入、整体熵低(5.2)、通过全部 YARA 规则,但字符串分析发现
cmd.exe /c、powershell -enc与三个以TVqQ开头的 Base64 块。这说明了什么?为什么导入分析没抓住它?
与生态的关联
二进制分析是更广泛安全流水线的起点。本仓库中可与之协同的项目包括:网络流量分析器(捕获可与 C2 通信关联的流量)、SIEM 仪表盘(把分析结果作为安全事件接入集中监控)、加密 P2P 聊天(其使用的加密原语会被二进制熵分析标记)。
同时,AXUMORTEM 的指标对接行业标准:OWASP A08:2021(软件与数据完整性失败——验证供应链软件未被篡改)、MITRE ATT&CK(执行/持久化/防御规避/凭据访问/指挥控制战术)、CWE-506(内嵌恶意代码)等,让静态分析结论可以嵌入组织已有的威胁情报体系。