引言:不运行一行代码,你能知道多少?
设想一个真实场景:周一早上 9 点,邮件网关拦截了一个附件 2026年度报销明细.exe,用户坚称"是财务发给我的正常文件",发件人邮箱却被伪造了。EDR 没有告警,VirusTotal 检出率只有 2/71。这时候你需要回答一个问题:这东西到底是什么?
摆在你面前有三条路:
- 直接运行------最直观,但等于把未知代码放进你的基础设施,是最危险的选择;
- 扔进沙箱------安全,但要排队、要等报告、而且样本会感知沙箱环境"装死",你什么都看不到;
- 静态分析 ------不运行一行代码,仅通过解析文件的二进制结构、提取字符串、阅读导入表,在 10~15 分钟内给出"恶意/良性/疑似"的定性结论和第一批 IOC。
老练的分析师几乎总是从第三条路开始。静态分析不是"动态分析的阉割版",而是整个分析流程的侦察兵 :它决定你接下来要不要投入沙箱、要不要逆向、要不要上报。更重要的是,静态分析产出的哈希、字符串、导入特征、时间戳,是可以直接沉淀为 YARA 规则和威胁情报的原始材料------动态分析给你"行为",静态分析给你"指纹"。
本文假定你有一定 Windows 基础但从未系统做过恶意软件分析,目标是用一篇博客的篇幅带你打通三个核心技能:读懂 PE 结构(文件的"骨骼")、提取并解读字符串(文件的"语言")、分析导入表(文件的"能力清单"),并最终形成一套可以在 10 分钟内完成的标准化定性流程(SOP)。全文以实操为导向,每一个知识点都配有可直接复制的命令或脚本。
第一章:开工前的准备------工具箱与安全规范
1.1 工具清单(全部免费)
| 工具 | 用途 | 获取方式 |
|---|---|---|
| PEStudio | 一站式 PE 查看:头、节、导入、字符串、熵、签名 | winitor.com |
| PE-bear | 逐字段解析 PE 结构,带十六进制联动视图 | GitHub |
| Detect It Easy (DIE) | 编译器/壳/打包器指纹识别 | GitHub |
| Sysinternals strings.exe | 提取 ASCII / Unicode 字符串 | 微软官网 |
| FLOSS | 自动解码混淆字符串(栈字符串、XOR 解码等) | Mandiant GitHub |
| capa | 自动识别二进制能力并映射 MITRE ATT&CK | Mandiant GitHub |
| Python + pefile | 脚本化批量解析 | pip install pefile |
| 7-Zip | 加密打包样本 | 7-zip.org |
| CyberChef | Base64/Hex/解码瑞士军刀 | gchq.github.io/CyberChef |
| 如果只想装一个工具开始,装 PEStudio ;如果只想学一个 Python 库,学 pefile。这两个组合能覆盖本文 90% 的内容。 |
1.2 处理样本的安全纪律
静态分析虽然"不运行代码",但依然有操作风险------Windows Shell 扩展、预览窗格、缩略图生成器都可能在你不经意间执行文件内代码。三条纪律必须养成肌肉记忆:
- 样本永远改名 + 加密压缩存放 。把
invoice.exe改成sample_a3f8.zip,用 7-Zip 加密打包(密码用行业惯例infected),这样既防止误双击,也方便上传分析平台时穿透邮件网关; - 在专用虚拟机或至少关闭预览窗格的环境中操作 。资源管理器按
Alt+V关闭预览窗格,关闭缩略图视图,用列表视图查看样本目录; - 查询优先于上传 。拿到样本先算 SHA256,去 VirusTotal 只查哈希不传文件------上传等于向全世界的攻击者广播"有人正在分析这个样本"。只有确认哈希未收录且样本不含贵组织敏感信息时才考虑上传。
powershell
# Windows 下计算哈希
certutil -hashfile sample_a3f8.exe SHA256
certutil -hashfile sample_a3f8.exe MD5
1.3 静态分析的"能力边界"
开始之前先校准期望。静态分析能回答:
- 这是不是 Windows 可执行文件?32 位还是 64 位?控制台还是图形界面?
- 用什么编译器/打包器做的?大概什么时候编译的?
- 它声明了哪些 Windows API 调用权(即"它能做什么")?
- 它的字符串里泄露了哪些线索(域名、URL、互斥体名、PDB 路径)?
- 它有没有被加壳、加的什么壳?
静态分析不能回答: - 它运行后实际 做了什么(意图 ≠ 行为,导入表里有
CreateRemoteThread不代表真的注入了); - 加密字符串的真实内容(需要动态解码或模拟执行);
- 它是否真的会连接某个 C2。
记住这个心法:静态分析做"画像",动态分析做"实证",逆向工程做"解剖"。 三者各司其职,静态分析是画像师------它可能画错细节,但能快速告诉你"这个人大概长什么样、该重点查哪里"。
第二章:PE 结构精读------从十六进制到字段
PE(Portable Executable)是 Windows 所有可执行文件(exe/dll/sys/cpl/scr)的统一格式。网上讲 PE 结构的文章大多照抄 MSDN 结构体定义,读完仍然不知道"分析恶意软件时到底该看哪些字节"。本节换一个讲法:假设你手头只有一个十六进制编辑器,我们手动把一个 PE 从头读一遍,每读到一个字段就讲"正常值长什么样、恶意样本会怎么玩花样"。
2.1 0x00:DOS 头------两个字节定生死
用 PE-bear 打开任意 PE 文件,最前面 64 字节是 IMAGE_DOS_HEADER。分析中真正要看的只有两个:
字段一:e_magic(偏移 0x00,2 字节)= 4D 5A
即 ASCII 的 MZ(DOS 之父 Mark Zbikowski 的缩写)。所有合法 PE 必以 MZ 开头。实战意义:
- 拿到陌生文件先
xxd sample | head -1,看到4d5a才是 PE;看到7f45 4c46是 Linux ELF,cffa edfe是 macOS Mach-O,504b 0304是 ZIP(很多 .NET ClickOnce 样本本质是 ZIP 包); - 文档型恶意载荷的常见手法 :把 PE 藏在 RTF/ZIP/ISO 里,第一层文件不是 MZ,需要先剥壳。
字段二:e_lfanew(偏移 0x3C,4 字节)
指向真正的 PE 头在文件中的偏移。标准链接器生成的值一般是0x80、0xE8、0xF0这类小数字。如果这个值异常大(比如 0x1000 甚至更大),说明 DOS Stub 区域被塞了大量自定义数据------有些样本在 DOS Stub 里藏配置或反分析代码,有些加壳器会改写这里。
2.2 0x4D 5A 之后:DOS Stub 与 Rich Header
紧跟 DOS 头的是那段著名的 This program cannot be run in DOS mode.------正常程序这里千篇一律。两个值得留意的变化:
- 这段话被改写:某些自定义加壳器会覆盖 DOS Stub,是"非标准构建"的信号;
- Rich Header :微软链接器在 DOS Stub 与 PE 头之间插入的未公开结构(以
DanS魔术开始、Rich魔术结束),记录了编译所用工具链(VS 版本、链接器版本、导入的库)的指纹。它极难伪造(含校验和),是归因利器------同一名攻击者的开发环境编译的所有样本,Rich Header 指纹高度一致。PEStudio 右侧面板可直接查看 Rich Header 的工具链列表和哈希。
2.3 PE 签名与 COFF 文件头(File Header)
e_lfanew 指向的位置应有 4 字节签名 50 45 00 00(PE\0\0),随后是 20 字节的 COFF 头。对定性最有价值的三个字段:
Machine(2 字节)
| 值 | 架构 |
|---|---|
0x014C |
x86(32位) |
0x8664 |
x64(64位) |
0x01C4 |
ARM NT |
0xAA64 |
ARM64 |
实战用法 :32 位样本在 64 位机器上运行会走 WoW64 子系统,其注入、注册表访问都有重定向(如 32 位进程写 HKLM\Software 实际落在 Wow6432Node)------这直接影响后续动态分析的观察位置。另外,近期大量针对国内用户的样本是 32 位的(兼容老系统 + 木马生成器默认配置),而正规软件早已全面 64 位化,"32 位 + 高检出竞争对手"本身就是弱信号。 |
|
| TimeDateStamp(4 字节) | |
| Unix 时间戳,标示链接时间。用 PEStudio 直接显示为人类可读时间。三种异常: |
- 未来时间(比如比今天晚好几个月)→ 攻击者伪造,常见于想"看起来像未来正式版"的样本;
- 1970-01-01 或 2000-01-01 等整点 → 构建系统故意清零,或用极简 Builder 生成;
- 周日/凌晨的编译时间 → 纯统计特征,工作量大的恶意工程常有"打工人作息",归因时可作旁证。
注意 :时间戳可被工具一键改写,只能当参考证据,不能当决定性证据。
Characteristics(2 字节)
标志位集合,重点看两位:0x2000(DLL)和0x0102(可执行)。一个文件自称.exe却没有 EXECUTABLE_IMAGE 标志,或自称 DLL 却没有 DLL 标志,都是被手工篡改过的信号 。另外注意 DLL 侧加载攻击(下文案例二):恶意 DLL 会伪装成正常系统 DLL 名(如version.dll、winmm.dll),此时 Characteristics 显示的 DLL 标志正常,要靠导出表与合法版本比对来识破。
2.4 Optional Header------名叫"可选",字字千金
这是 PE 头里信息量最大的部分(32 位版本 224 字节,64 位版本 240 字节)。逐个看定性关键字段:
Magic(2 字节) :0x10B = PE32,0x20B = PE32+(64位)。与 Machine 字段交叉验证,不一致 = 手工拼接。
Subsystem(2 字节) :2 = 图形界面(GUI),3 = 控制台(CUI)。一个声称是"文档查看器"的样本如果是控制台程序,就有问题 ------控制台程序运行时黑色窗口一闪,普通用户会起疑,恶意软件通常伪装成 GUI;反过来,很多感染型样本的真实 payload 阶段是控制台程序。
AddressOfEntryPoint(4 字节,简称 EP) :程序第一条执行指令的 RVA。这是定性阶段含金量最高的单个字段,判定规则:
- 正常未加壳程序:EP 落在
.text节内,且通常靠近节区起始; - 加壳程序:EP 几乎总是落在最后一个节区(壳代码先执行,解压出原始程序再跳过去);
- EP 指向的节区既可执行又可写(RWX) → 强烈可疑,正常代码节是"可执行+可读"而非"可写"。
PEStudio 会直接画出"EP 在哪个节区"的可视化,一眼判断。
ImageBase :推荐加载基址,EXE 常见0x400000,DLL 常见0x10000000。如果开启 ASLR(DllCharacteristics 含0x0040),实际基址会随机化,此字段仅供参考。样本 ImageBase 与同家族历史样本一致时可作为关联特征 。
DataDirectory16(数据目录) :16 个"目录→RVA+大小"的索引表,是通往导入表、导出表、资源、重定位、TLS 等一切的地图。定性阶段最常看的是:
| 索引 | 目录 | 定性意义 |
|:-😐:-😐:-😐
| 0 | 导出表 | DLL 是否导出与合法系统 DLL 同名函数(侧加载伪装) |
| 1 | 导入表 | 本文第五章主角 |
| 2 | 资源 | 是否藏大体积加密载荷 / 伪装图标 |
| 9 | TLS | TLS 回调在 main 之前执行,反调试/反沙箱经典藏身处 |
| 14 | CLR 头 | 非零 = .NET 程序 ,分析路径完全不同 |
看到 DataDirectory14.Size 非零,先切换思路:.NET 样本要用 dnSpy/ILSpy 反编译 C# 源码(大量窃密木马、游戏外挂是 .NET 写的),别用 x64dbg 硬啃机器码。
2.5 Section Headers------节区表的"体检指标"
COFF 头里的 NumberOfSections 指示节区数量,每个节区由 40 字节的 IMAGE_SECTION_HEADER 描述。这是"程序是否被加壳/篡改"的主战场 ,四组指标逐一检查:
指标一:节区名字
标准链接器输出:.text(代码)、.rdata(只读数据)、.data(可写数据)、.rsrc(资源)、.reloc(重定位)、.edata/.idata、.tls、.bss。
知名壳的签名节名:
| 节名 | 对应壳 |
|---|---|
UPX0 UPX1 |
UPX |
.aspack .adata |
ASPack |
.themida .winlice |
Themida |
MPRESS1 MPRESS2 |
MPRESS |
.vmp0 .vmp1 |
VMProtect |
.enigma1 .enigma2 |
Enigma |
.petite .pec2 等 |
Petite / PECompact |
| 随机不可读字符 | 自定义壳(警惕) |
| 但节名可任意改写------名字只是线索,下面的量化指标才是证据。 | |
| 指标二:熵值(Entropy) | |
| 熵衡量数据随机度,范围 0~8 bit/byte。经验刻度: | |
| 熵值 | 解读 |
| :-: | :-: |
| 0~2 | 大量重复填充(全0、全FF),可疑的"占位" |
| 2~5 | 正常文本/普通数据 |
| 5~6.5 | 正常代码节的典型区间 |
| 6.5~7.0 | 边缘,可能有轻度压缩 |
| 7.0~7.6 | 高度疑似压缩/加密(壳) |
| 7.6~8.0 | 几乎必然是加密载荷或强压缩 |
判壳经验 :.text 节熵 > 7,或任何节熵 > 7.5,加壳概率极高;反过来,熵值极低(<1)的节区同样值得看------可能是壳解压后的"跳板",或攻击者故意填充的诱饵。 |
|
| 指标三:VirtualSize vs SizeOfRawData |
VirtualSize:节区在内存中展开的大小;SizeOfRawData:节区在文件中占的大小。
正常两者接近(RawData 按 FileAlignment 向上取整)。VirtualSize 远大于 SizeOfRawData(数倍以上)是加壳的典型特征 ------壳在文件里只存压缩数据,运行时解压膨胀到内存;反过来 SizeOfRawData 远大于 VirtualSize 且差异巨大 ,文件尾部可能有大量附加数据(Overlay)。
指标四:节区权限
Characteristics中的权限位。正常布局:.text= R+X,.data= R+W,全部节区不应同时具备 R+W+X 。RWX 节区 = 自修改代码/壳/注入载荷的三重嫌疑。许多 EDR 的静态规则第一刀就砍在 RWX 节区上。
2.6 RVA 与文件偏移的换算(动手能力分水岭)
要真正"看懂"PE,绕不开一个基础换算:同一个地址,在内存(RVA)和文件(File Offset)里的位置不一样。分析工具都帮你换算好了,但遇到手工 patch 分析、写 YARA 规则指定偏移、从内存 dump 修复文件时,必须自己会算:
换算公式:
File Offset = RVA - 节区VirtualAddress + 节区PointerToRawData
步骤:
1. 给定 RVA,遍历节区表找到满足:
VirtualAddress <= RVA < VirtualAddress + VirtualSize 的节区
2. 套用公式
用 pefile 实现:
python
import pefile
pe = pefile.PE('sample.exe')
# 已知 RVA 求文件偏移
rva = pe.OPTIONAL_HEADER.AddressOfEntryPoint
offset = pe.get_offset_from_rva(rva)
print(f"入口点 RVA=0x{rva:x}, 文件偏移=0x{offset:x}")
# 反向:文件偏移求 RVA
print(f"RVA = 0x{pe.get_rva_from_offset(offset):x}")
这个换算在写 YARA 规则时特别有用------YARA 匹配的是文件偏移,而 PE 工具显示的多是 RVA,两者经常需要互转。
第三章:字符串分析------恶意软件"说的每一句话"
3.1 为什么字符串是情报金矿
字符串是样本中未加密的人类可读文本 ,可能泄露的信息包括:C2 域名和 IP、下载 URL、注册表持久化路径、互斥体名(Mutex,家族指纹)、互斥的杀软进程名(对抗情报)、钱包地址、勒索信文本、以及最容易被忽视的------PDB 调试路径 。
PDB 路径形如 C:\Users\dev\Documents\Project\Release\Agent.pdb,它泄露攻击者的开发机用户名和项目名 。归因时,同一个 PDB 路径模式(同一用户名、同一目录习惯)能串联起整个攻击组织的工具集。PEStudio 的字符串面板会单独高亮 PDB 路径,永远值得多看一眼。
3.2 基础操作:strings 的正确姿势
Windows 平台用 Sysinternals 的 strings.exe,Linux 用 GNU strings:
bash
# ASCII 字符串,最小长度 6(过滤单字母垃圾)
strings -a -n 6 sample.exe > s_ascii.txt
# UTF-16LE 宽字符串------Windows API 主战场,必查!
strings -a -e l -n 6 sample.exe > s_utf16.txt
# 合并、排序、去重、统计
cat s_ascii.txt s_utf16.txt | sort -u | tee s_all.txt | wc -l
新手最常见的错误是只提取 ASCII 。Windows 的文件路径、注册表键、Mutex 名大多以宽字符(UTF-16LE)存在,只看 ASCII 会漏掉一半以上关键线索。
拿到字符串清单后,按优先级 grep:
bash
# 1. 网络指标(IOC 的直接来源)
grep -Ei '([0-9]{1,3}\.){3}[0-9]{1,3}' s_all.txt # IP 地址
grep -Ei 'https?://|\.top|\.xyz|\.tk|\.cc|\.ru' s_all.txt # URL与可疑TLD
grep -Ei '\.(ddns|no-ip|hopto|duckdns)\.' s_all.txt # 动态域名
# 2. 持久化与系统痕迹
grep -Ei 'CurrentVersion\\\\Run|schtasks|SOFTWARE\\\\Microsoft' s_all.txt
grep -Ei 'Global\\\\|Local\\\\' s_all.txt # Mutex 名
# 3. 对抗与攻击特征
grep -Ei 'vmware|virtualbox|sandbox|wireshark|x64dbg|procmon' s_all.txt
grep -Ei 'cmd\.exe|powershell|/c |wscript|mshta|rundll32' s_all.txt
# 4. 凭据窃取特征
grep -Ei 'Login Data|cookies\.sqlite|wallet\.dat|logins\.json' s_all.txt
# 5. 编码痕迹(Base64 编码的 PE)
grep -Ei 'TVqQAA' s_all.txt # "MZ\x90\x00" 的 Base64 前缀
grep -Ei '[A-Za-z0-9+/]{200,}={0,2}' s_all.txt # 长Base64块
3.3 字符串解读的"红黄绿"分级
红旗字符串(出现即高度可疑):
- 动态域名家族(
*.ddns.net等)+ 非常规端口; SOFTWARE\Microsoft\Windows\CurrentVersion\Run等持久化键 + 可执行文件名组合;- 杀软/调试器/沙箱进程枚举列表;
- 勒索信文本、
.encrypted扩展名、Tor 网关地址; mimikatz、secretsdump、psexec等渗透工具痕迹。
黄旗字符串(需上下文判断):- 裸 IP 地址------可能是 C2,也可能是合法的 NTP/DNS 配置;
http://链接------检查域名注册时间(whois)和是否在威胁情报库中;Sleep、GetTickCount------反调试 API 名出现,也可能是正常性能计数。
绿旗字符串(指向良性):- 完整规范的版本信息(ProductName/CompanyName 与实际厂商吻合);
- 指向 Microsoft 符号服务器的 PDB 路径;
- 大量与产品功能匹配的业务字符串(如"报表""打印")。
3.4 字符串看不见了?------FLOSS 上场
当 strings 输出只有内存地址和乱码,说明字符串被加密了。FLOSS(FLARE Obfuscated String Solver)能自动还原三类 strings 看不到的字符串:
- 栈字符串(Stack Strings):程序运行时把字符逐个压栈拼出来,文件里从未完整存在;
- 紧凑字符串(Tight Strings):在极小循环中边构造边使用;
- 解码字符串(Decoded Strings):经 XOR/RC4/自定义算法解码,FLOSS 通过模拟执行解码函数拿到明文。
bash
# 全量提取(包含模拟执行,较慢)
floss sample.exe
# 只要解码结果------最快拿到关键情报
floss --only decoded sample.exe
# 排除静态已见内容,聚焦新线索
floss --only stack tight decoded sample.exe
实战判断顺序:先跑 strings(1 秒),有货就不必上 FLOSS;strings 颗粒无收且样本未加壳,再跑 FLOSS(几十秒到几分钟);FLOSS 也一无所获,说明字符串重度加密,老老实实转动态分析。
3.5 中文样本的额外处理
针对国内用户的样本常含 GBK 编码中文(钓鱼话术、勒索信),strings 默认按 ASCII/UTF-16 解析会变乱码:
bash
# GNU strings 支持 8-bit 单字节编码(覆盖GBK)
strings -e S -n 4 sample.exe | iconv -f GBK -t UTF-8 2>/dev/null | grep -E '[一-龥]{2,}'
国内样本高频中文情报词:发票、工资、对账单、检测报告、行程、订单异常、账号验证、解压密码(钓鱼邮件诱导执行的经典话术)------这些词既能帮你判断样本目标人群,也可以直接进 YARA 规则。
第四章:导入表精读------程序的"能力申请清单"
4.1 原理三句话
- Windows 程序的 API 代码不在自身文件里,运行时从系统 DLL(kernel32、user32、ws2_32...)借;
- 导入表 在文件里静态记录"我要从哪个 DLL 借哪些函数",加载器据此填充导入地址表(IAT),把每个函数的真实地址写进内存;
- 因此,导入表是攻击者最容易忽略、却最诚实的能力声明------除非他刻意隐藏(见 4.4)。
4.2 看导入表的正确方式:按"能力组合"看,不按"单个 API"看
单独一个 VirtualAlloc 任何程序都可能有;但 VirtualAllocEx + WriteProcessMemory + CreateRemoteThread 三连 出现在一个 IM 聊天工具里,就有明确含义------远程线程注入。定性靠组合,不靠单点。下面这张组合表建议打印贴墙:
| 恶意能力 | 导入表 API 组合 | ATT&CK |
|---|---|---|
| 远程线程注入 | VirtualAllocEx + WriteProcessMemory + CreateRemoteThread | T1055 |
| APC 注入 | QueueUserAPC + VirtualAllocEx + SetThreadContext | T1055.004 |
| 进程镂空 | CreateProcessA(suspended) + NtUnmapViewOfSection + WriteProcessMemory + SetThreadContext + ResumeThread | T1055.012 |
| 反射加载 | VirtualAlloc + 手工解析 PE 的内存操作组合(导入表少) | T1055 |
| 键盘记录 | SetWindowsHookExA(WH_KEYBOARD_LL) + GetAsyncKeyState + GetForegroundWindow | T1056.001 |
| 勒索加密 | FindFirstFileW + FindNextFileW + CryptEncrypt/CryptEncryptMessage + DeleteFileW | T1486 |
| HTTP C2 | InternetOpenA + InternetConnectA + HttpOpenRequestA + HttpSendRequestA(或 winhttp 同族) | T1071.001 |
| 原始套接字 C2 | WSAStartup + WSASocketA + connect + send/recv | T1071 |
| DNS 隧道 | DnsQuery_A + 异常长域名拼接 | T1071.004 |
| 凭据解密 | CryptUnprotectData + 对 Chrome/Firefox 配置文件路径的字符串 | T1555 |
| 服务持久化 | OpenSCManagerA + CreateServiceA + StartServiceA | T1543.003 |
| 计划任务 | CoCreateInstance(任务计划COM) 或 shell32 的 Sch* | T1053 |
| 截屏监控 | GetDC(GetDesktopWindow) + CreateCompatibleDC + BitBlt + GetDIBits | T1113 |
| 剪贴板劫持 | OpenClipboard + GetClipboardData + SetClipboardData | T1115 |
| 反调试 | IsDebuggerPresent + CheckRemoteDebuggerPresent + NtQueryInformationProcess | T1622 |
| 反虚拟机 | CreateFileA(\.\VBoxGuest / \.\HGFS) + registry 查询 VMware 键 | T1497 |
| 提权 | OpenProcessToken + LookupPrivilegeValue + AdjustTokenPrivileges | T1134 |
| UAC 绕过 | CoCreateInstance + 字符串含 CMSTPLUA/ICMLuaUtil | T1548.002 |
4.3 三个定量红旗
红旗一:导入表极小 。正常 GUI 程序通常导入 100~300 个函数;一个样本只导入 kernel32!LoadLibraryA / GetProcAddress / VirtualAlloc / ExitProcess 寥寥几个------必然加壳或动态解析 API 。
红旗二:DLL 组合与"人设"不符 。自称"计算器"的程序导入 ws2_32.dll(网络)+ gdi32.dll 截屏 API + wininet.dll------计算器不需要上网和截屏。把导入的 DLL 列表当作程序的"简历",逐行追问"你真的需要我吗?"
红旗三:导入时序矛盾 。导入表里有 ws2_32.dll 的初始化函数(WSAStartup),但整个文件没有任何网络相关字符串------典型的"API 加密 + 字符串加密"双重防护样本,标记为高对抗等级,直接转动态分析。
4.4 攻击者的三种"导入表隐身术"与应对
手法一:API Hashing(哈希解析)
不写函数名,只写预计算的哈希值,运行时自己算 GetProcAddress:
asm
; 伪代码:攻击者代码里只有
mov eax, 0x6A4ABC5B ; CreateProcessA 的 ROR13 哈希
call resolve_api ; 自写解析器:遍历导出表算哈希比对
导入表因此只显示 kernel32 的两三个函数。应对 :capa 内置常见哈希算法(ROR13 等)的识别规则,能在反汇编层面还原"哪个哈希对应哪个 API",capa --sample sample.exe 输出直接给出能力清单;也可用 PEStudio 配套的 ImpHash/提交社区情报库比对。
手法二:动态加载(运行时 LoadLibrary)
所有敏感 API 都通过 LoadLibrary("ws2_32.dll") + GetProcAddress 拿,导入表只留这两个"万能钥匙"。应对 :在字符串里搜 DLL 名(LoadLibrary 的参数往往是明文或简单编码);用 FLOSS 还原;或直接动态调试在 GetProcAddress 下断点。
手法三:延迟加载导入表
PE 的延迟加载导入机制本用于优化启动性能,攻击者可借此把敏感 API 挪出主导入表。PEStudio 有专门的 Delayed Imports 页签,记得额外看一眼。
4.5 ImpHash:导入表的家族指纹
ImpHash = 对导入表整体(DLL 名+函数名规范化后)计算的 MD5 。价值在于:同一家族、同一生成器产出的样本,即使代码被轻微修改,导入表往往不变,ImpHash 保持一致。
python
import pefile
pe = pefile.PE('sample.exe')
print(pe.get_imphash())
# 输出如: b5eaad4e1a1ce5d8f1a3f3f83f7cc0d2
用法:把 ImpHash 作为 VT 搜索条件、作为 YARA 规则的 pe.imphash() 条件、作为家族聚类的快速分桶键。注意:对抗者改一个导入顺序就能改变 ImpHash,所以它是"聚类线索"而非"身份证明"。
第五章:五大辅助信号------容易被忽略的定性细节
这五个信号单独看都不定案,但组合起来能大幅提升判断置信度。
信号一:数字签名状态 。PEStudio 的 Signatures 页签显示签名验证结果。三种情况:有效签名(可信但可被滥用 ------近年多起供应链事件用的都是有效签名);签名无效但存在签名块(样本被篡改过,或签名被盗用后文件被重新打包,强可疑);无签名(正常软件很多也没签,中性信号)。用 PowerShell 复核:
powershell
Get-AuthenticodeSignature .\sample.exe | Format-List Status, SignerCertificate, TimeStamperCertificate
信号二:资源节内容 。用 PEStudio 的 Resources 面板看:图标是否与文件名"人设"匹配(伪装成 PDF/Word 的样本常偷正规图标);有无大体积、高熵、无类型(RCDATA/BINARY)的异常资源------那多半是加密的下一阶段载荷或配置 ;版本信息块(VS_VERSION_INFO)里的 FileDescription/CompanyName 是否与签名/图标/文件名自洽------"人设不自洽"是钓鱼样本最常见的破绽 。
信号三:Overlay(覆写数据) 。PE 节区结束之后的剩余原始字节就是 Overlay,签名工具、安装包、自解压档案常用它附加数据,恶意样本也爱用它携带加密配置或真实 payload 。pefile 一行脚本判定:
python
pe = pefile.PE('sample.exe')
print(f"Overlay 偏移: 0x{pe.get_overlay_data_start_offset():x}")
data = pe.__data__
print(f"Overlay 大小: {len(data) - pe.get_overlay_data_start_offset()}")
大体积 Overlay + 高熵 = 强烈建议切出该区域单独分析(可能是嵌套的另一个 PE)。
信号四:TLS 回调 。TLS 目录非零即存在回调------回调代码在入口点之前执行 ,是反调试(检测调试器附加时机)、反沙箱(在分析工具挂钩前跑检测)的经典藏身处。PEStudio 的 TLS 页签列出回调地址,用 x64dbg 在回调地址下断是动态阶段的第一课,但静态阶段就应记录"此样本有 TLS 回调"这一预警 。
信号五:文件年龄与人设合理性 。用时间戳、VT 首次提交时间、域名 whois 交叉验证"故事合理性":一个自称"2026 财务模板"的文件编译于三年前?一个 .exe 的图标却是 Adobe PDF?人设拼不上的地方,就是攻击者露出马脚的地方。
第六章:十分钟定性 SOP------完整决策流程
把前五章压缩成可执行的标准作业程序。每个样本从拿到手到给出结论,目标 10 分钟内完成。
6.1 流程总览
[0min] 取样 → 改名 → 加密打包 → 计算 SHA256
[1min] VT 哈希查询(只查不传)
├─ 高检出/已知家族 → 归档,产出 IOC,结束
└─ 低检出/未收录 ↓
[2min] DIE 指纹识别:编译器 / 壳 / 打包器
├─ UPX 等已知壳 → upx -d 脱壳后重走流程
└─ 无壳/未知 ↓
[3min] PEStudio 全景扫描(对照下方 10 项检查表)
[6min] 导入表组合分析 → 初步能力画像
[8min] strings + FLOSS 字符串提取 → IOC 提取
[10min] 综合定性 → 输出结论 + IOC 清单 + 后续动作建议
6.2 PEStudio 十项检查表
text
□ 1. Machine / Magic 一致且符合"人设"(32 vs 64 位)
□ 2. TimeDateStamp 无异常(非未来、非 1970)
□ 3. Subsystem 与"人设"相符(GUI/CUI)
□ 4. 节区数 3~7,节名规范或为已知壳签名
□ 5. 所有节区熵值 < 7.0(.text 典型 5~6.5)
□ 6. 入口点位于 .text 且无 RWX 节区;EP 在最后节区 = 加壳
□ 7. 导入表大小合理;无"注入三件套/勒索三件套"组合
□ 8. 数据目录:TLS 回调?CLR(.NET)?资源区有无高熵大块?
□ 9. 签名状态:有效 / 无效但存在 / 无
□ 10. 字符串面板:PDB 路径 / URL / 注册表键 / Mutex / 反分析词
6.3 结论输出模板
markdown
## 样本定性报告
- 文件名: 2026年度报销明细.exe → 重命名 sample_a3f8.exe
- SHA256: a3f8c92b...(脱敏示例)
- 类型: PE32 GUI,32位,MSVC 编译,无壳
- VT: 2/71(Frontier/低置信标签)
- 时间戳: 2026-08-24 03:17 UTC(可疑:周末凌晨编译)
- 能力画像: HTTP C2(T1071.001) + 持久化注册表(T1547.001)
+ 凭据窃取(T1555.003) + 反调试(T1622)
- IOC:
- C2: hxxp://185.xx.xx.27/gate.php
- Mutex: Global\_xq_system_mutex
- 持久化键: HKCU\...\Run\WinDefendUpdate
- 人设破绽: 图标为 Excel,但 Subsystem=CUI 且导入 wininet
- 定性: 恶意(窃密木马,疑似 AgentTesla 变种,置信度 中高)
- 建议: ① 提交沙箱确认 C2 握手 ② 溯源邮件原始发件域
③ IOC 入库 + 生成 YARA
第七章:两个完整案例------把 SOP 跑一遍
案例一:三分钟定案------伪装发票的 .NET 窃密木马
样本 :2026年度报销明细.exe,32 位。
第 0 分钟 :改名 sample_a3f8.exe,SHA256 查询 VT------2/71,两条泛型启发式标签,无家族名。未收录,继续。
第 2 分钟 :DIE 识别------Compiler: MSIL/.NET (C#)。立即切换 .NET 分析思路 :PE 头只是外壳,真正的逻辑在 CLR 元数据里,导入表必然稀少(.NET 通过运行时调用 API),不能用传统导入表规则硬套。
第 3 分钟 :PEStudio 快速过一遍结构:时间戳 2026-08-24 03:17 UTC(凌晨,黄旗);4 个标准节区,熵值正常;EP 在 .text;资源节有一个 380KB、熵 7.6 的 BINARY 资源------这不像 .NET 正常布局,像"加载器 + 内嵌载荷"结构 。
第 5 分钟 :换武器。用 ILSpy 打开样本,看到主程序只有 30 行代码:读取资源节 → AES 解密(密钥硬编码在字符串)→ Assembly.Load 内存加载 → 反射调用入口。这是一个典型的 .NET 单阶段 Dropper 。ILSpy 里把内嵌数组导出为 payload.bin,再次用 ILSpy 打开------得到完整的窃密木马源码级视图:Chrome 密码提取、SMTP 外发、硬编码的 smtp.qiye.xxx.com:465 与发件账号。
第 8 分钟 :strings 补充确认:Login Data、Local State、Base64 的 TVqQAAM(内存加载 PE 的痕迹)、PDB 路径 C:\Users\w\source\repos\NewFud\NewFud\obj\Release\NewFud.pdb。
结论 :恶意------.NET Dropper + 内存加载窃密木马(AgentTesla 系生态特征)。IOC:SMTP 外发凭据(高价值情报,可通知涉及企业)、PDB 项目名 NewFud(归因线索)、Mutex 与 C2。后续动作:payload 单独算哈希入库,写 YARA 覆盖"字符串解密函数特征 + SMTP 域名"。
复盘 :本案例若只用传统"看导入表"的思路,会因为 .NET 导入表稀少而误判"低风险"------先识别编译生态,再选分析武器,是定性流程的隐藏第一步。
案例二:十五分钟破案------DLL 侧加载伪装样本
样本 :EDR 从一台办公机隔离出的 winmm.dll,位于某国产办公软件目录下。
第 0 分钟 :VT 查询 0/71------零检出 。注意:0 检出不等于良性,侧加载样本为了长期驻留恰恰要压检出率。
第 2 分钟 :PEStudio:64 位 DLL,时间戳伪造为 2021-06-15(与目录内其他合法文件时间戳一致------刻意模仿 );签名:存在签名块但验证无效 (红旗:合法 winmm.dll 由微软签名且有效);节区 5 个,第 5 个节名 .OOOO(非标准),熵 7.4------代码被加密 ;导入表仅 9 个函数:LoadLibraryA、GetProcAddress、VirtualAlloc、VirtualFree、GetLastError 等------教科书级加壳/动态解析布局。
第 4 分钟 :导出表对比。这一步是侧加载检测的题眼 :合法 winmm.dll 导出 PlaySound、midiOutOpen 等 40+ 个函数;这个样本的导出表只转发了 6 个关键函数 (宿主程序启动时会解析这些函数,不转发就加载失败),其余缺失。对比权威导出表,缺失 + 名称完全一致 = 伪装确认。
第 6 分钟 :DIE 显示自定义壳(无签名匹配)。不强攻脱壳,转 FLOSS------因为壳未加密字符串区:提取到 cmd.exe /c powershell -w hidden -enc、一段 2KB Base64(CyberChef 解码后为另一段 PowerShell,内含 FromBase64String 双层编码,再解出下载器脚本与域名 cdn-metrics[.]example[.]top)、以及互斥体 Global\{7A3F-2026-CAMP}。
第 9 分钟 :capa 对未加密部分仍识别出 Decode data using XOR、Check for debugger via NtQueryInformationProcess、Persist via registry Run key 三项能力。
结论 :恶意------DLL 侧加载投递的加密 Loader(Cobalt Strike/定制远控生态特征:-w hidden -enc 模式 + Beacon 风格互斥体)。IOC:互斥体、多层解码后的下载域名、Loader 哈希。后续动作:先排查该办公软件官方目录是否本应存在 winmm.dll (官方目录无此 DLL = 100% 投放);对同批终端按互斥体与域名做横向排查;0/71 的检出率提示需要提交 YARA 与 VT 情报。
复盘:零检出样本的定性,靠的是**"与合法基线比对"**的思维------签名比对、导出表比对、时间戳与同目录文件比对。静态分析的进阶,就是从"看文件本身"升级到"看文件与世界的差异"。
第八章:自动化------从"逐个看"到"批量筛"
手动流程跑熟之后,必须自动化。一是量(每天几十个样本),二是一致性(人会疲劳漏项,脚本不会)。
8.1 pefile 批量提取定性特征
python
#!/usr/bin/env python3
"""批量 PE 定性脚本:输出结构特征 CSV,供后续筛选"""
import pefile, hashlib, math, csv, sys, os
def entropy(data: bytes) -> float:
if not data: return 0.0
freq = [0]*256
for b in data: freq[b] += 1
n = len(data)
return -sum((f/n)*math.log2(f/n) for f in freq if f)
def triage(path):
row = {"file": os.path.basename(path)}
row["sha256"] = hashlib.sha256(open(path,'rb').read()).hexdigest()
try:
pe = pefile.PE(path, fast_load=True)
pe.parse_data_directories(directories=[
pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_IMPORT'],
pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_TLS']])
except pefile.PEFormatError:
row["note"] = "NOT_PE"; return row
row["machine"] = hex(pe.FILE_HEADER.Machine)
row["ts"] = pe.FILE_HEADER.TimeDateStamp
row["subsystem"] = pe.OPTIONAL_HEADER.Subsystem
row["ep_rva"] = hex(pe.OPTIONAL_HEADER.AddressOfEntryPoint)
row["is_dotnet"] = int(pe.OPTIONAL_HEADER.DATA_DIRECTORY[14].Size > 0)
row["has_tls"] = int(hasattr(pe, "DIRECTORY_ENTRY_TLS"))
secs, max_ent = [], 0.0
for s in pe.sections:
name = s.Name.decode(errors="ignore").rstrip("\x00")
e = entropy(s.get_data())
max_ent = max(max_ent, e)
rwx = bool(s.Characteristics & 0x20000000 and s.Characteristics & 0x80000000 and s.Characteristics & 0x40000000)
secs.append(f"{name}:ent={e:.2f}{'|RWX!' if rwx else ''}")
row["sections"] = "; ".join(secs)
row["max_entropy"] = round(max_ent, 2)
try:
imps = [d.dll.decode() for d in pe.DIRECTORY_ENTRY_IMPORT]
row["dlls"] = ";".join(imps)
row["n_imports"] = sum(len(d.imports) for d in pe.DIRECTORY_ENTRY_IMPORT)
except AttributeError:
row["dlls"] = ""; row["n_imports"] = 0
ov = pe.get_overlay_data_start_offset()
row["overlay_kb"] = round((len(pe.__data__)-ov)/1024, 1) if ov else 0
return row
if __name__ == "__main__":
rows = [triage(p) for p in sys.argv[1:]]
w = csv.DictWriter(sys.stdout, fieldnames=rows[0].keys())
w.writeheader(); [w.writerow(r) for r in rows]
用法:python triage.py samples/*.exe > triage.csv,然后用 Excel 按条件筛选:max_entropy > 7.0(疑似壳)、n_imports < 15(动态解析)、is_dotnet = 1(走 .NET 流程)、has_tls = 1(有反分析预警)、overlay_kb > 500(藏货)。一张 CSV 就完成了初筛分桶。
8.2 capa:能力识别的自动化
bash
# 基础用法:输出 ATT&CK 能力清单
capa sample.exe
# JSON 输出对接 SOC 平台
capa --json sample.exe > capa.json
# 对加壳样本(部分有效)
capa --quiet sample.exe
capa 的规则库由 Mandiant 维护数千条,覆盖 API Hashing 识别、加密算法常量识别(AES S-box、RC4 状态表)、ATT&CK 映射。它对"导入表隐身"的样本尤其有效------这是导入表分析最大的盲区。
8.3 沉淀为 YARA:让今天的工作保卫明天
每完成一个定性案例,问自己一句:"这个样本有没有一条'即使换文件名、重新编译也依然命中'的特征?"有的话,写进规则库:
yaml
import "pe"
rule Suspect_Dropper_NewFud {
meta:
author = "analyst_me"
date = "2026-09-03"
family = "AgentTesla.ecosystem"
confidence = "medium"
strings:
// 攻击者自定义、难撞车的特征
$pdb = "NewFud\\obj\\Release" ascii // 项目名指纹
$mtx = "Global\\_xq_system_mutex" ascii wide // 互斥体
$smtp = "smtp.qiye" ascii // 外发通道
// .NET 内存加载行为特征
$load = "Assembly.Load" ascii wide
$enc = { AE 8F 3D 91 77 } // 自定义解密函数开头字节
condition:
uint16(0) == 0x5A4D and filesize < 3MB
and (2 of ($pdb, $mtx, $smtp))
or ($load and $enc)
}
写规则的纪律 :特征必须"独特"(互斥体、项目名、硬编码域名 > 通用 API 名);必须"抗重编译"(优先字符串常量与加密常量,其次代码字节);上线前用 yaratest/规则库全量回扫,确认不误报良性样本。一条高质量规则的长期价值,远超十次手动分析。
结语:定性是一门"证据链"的手艺
回头看这篇文章,真正想传递的不是某个 API 名或某个工具快捷键,而是三层思维:
第一层:结构思维 。PE 文件是一份被规范约束的"档案",加壳、伪装、拼接都会在结构上留痕------熵值、节区权限、入口点位置、导入表大小,这些量化指标比任何"感觉"都可靠。当你不确定时,回到结构,用数字说话 。
第二层:组合思维 。单点证据会撒谎:单个可疑 API 可能是误报,单个可疑字符串可能是巧合;但"凌晨编译 + 无签名 + 导入表 9 个函数 + RWX 节区 + 动态域名"五点连成线,概率就完全不同了。定性是拼证据链,不是找银弹 。
第三层:基线思维 。第二个案例告诉我们,最强有力的判断往往不是"这个文件里有什么",而是"它和它假装的那个东西差在哪 "------签名是否有效、导出表是否完整、时间戳是否与邻居一致。先建立正常基线,再审视偏差 ,这是所有检测科学的通用方法论。
最后送给入门者一句话:不要追求第一次就把样本看穿,要追求每一次都比上一次多确认一个假设、多沉淀一条规则。定性能力的成长曲线不陡,但极其稳定------分析一百个样本之后,你会在打开 PEStudio 的三秒内"闻到"味道,而那份直觉,正是由这一百次结构阅读、字符串 grep 和导入表对照一点点喂出来的。