一个文件以 PNG 签名开头,并不意味着后面只有图像数据。这个 2019 年的 Excel 样本在嵌入对象中放入两份不同位数的 DLL,VBA 声明则分别指向 exchange1.dll 和 exchange2.dll。把容器边界与宏接口对起来,比单独寻找可疑字符串更有解释力。
分析范围是工作簿的 OLE 结构、嵌入对象和可见的 VBA 声明。下文偏移均绑定这一样本;静态材料未覆盖完整宏函数体和运行日志,因此不把 DLL 成功加载、导出执行或后续联网写成已观测事件。
从对象流找到载荷容器
oledump.py 共枚举出 22 个流。对象标志 O 出现在 4 号流,VBA 标志出现在 12、13、14、20、22 号流;先分别跟进对象与代码,避免把整个工作簿当成一段不分层的字节串。
| 编号 | 标志 | 字节数 | 流路径 |
|---|---|---|---|
| 4 | O | 160402 | MBD00DF435B/\x01Ole10Native |
| 12 | M | 4824 | _VBA_PROJECT_CUR/VBA/Module1 |
| 13 | M | 1738 | _VBA_PROJECT_CUR/VBA/Module2 |
| 14 | M | 1365 | _VBA_PROJECT_CUR/VBA/UserForm1 |
| 20 | m | 973 | _VBA_PROJECT_CUR/VBA/bb |
| 22 | M | 1409 | VBA 模块流 |
这里保留工具输出中 M 与 m 的大小写,二者不是同一个标志。下列命令使用统一文件名 SAMPLE.xls;流编号和输出数值对应本例,而不是任意工作簿的固定布局。
$ oledump.py -s 4 -i SAMPLE.xls
String 1: 2AB07F92.png
Size embedded file: 159931
MD5 embedded file: b9fd4ec14b72f944160ebafa7f45b818
MAGIC: 89504e47
$ oledump.py -y contains_pe_file.yara SAMPLE.xls
4: O 160402 'MBD00DF435B/\x01Ole10Native'
YARA rule: Contains_PE_File对象元数据还包含用户临时目录和 Content.MSO 路径;这些是包装层记录,并非执行时写出的 DLL 路径。规则文件 contains_pe_file.yara 是该次检查的外部输入,命中只用于定位,PE 边界仍要单独确认。
PNG 文件头与内部 PE 是两层证据
PNG signature0x00–0x07IHDR length0x08–0x0BIHDR0x0C–0x0F
文件头支持“以 PNG 开头”,不证明整个对象是纯图像,也不直接证明图像被完整解析过。-e 提取嵌入对象内容后,pecheck.py -l P 给出两份 DLL 的起点、结构终点和外层文件终点:
$ oledump.py -s 4 -e SAMPLE.xls | pecheck.py -l P
1: 0x00002ebb DLL 32-bit 0x00016eba 0x000270ba (EOF)
2: 0x00016ebb DLL 64-bit 0x000270ba 0x000270ba (EOF)| 载荷 | 起点 | 结构终点 | 长度 |
|---|---|---|---|
| DLL32 | 0x2ebb | 0x16eba | 0x14000 |
| DLL64 | 0x16ebb | 0x270ba | 0x10200 |
两段连续排列:第一段终点加一就是第二段起点;第二段终点加一恰好是对象长度 159931。不要把外层流的 160402 字节直接代入这些对象内偏移,两者之间还隔着对象包装层。
overlay 改变了“同一份 DLL”的范围
从第一份 PE 起点一直取到对象末尾,会得到 0x24200 字节,而不是第一份 PE 自身的 0x14000 字节。工具因此把余下的 0x10200 字节视为 overlay(尾随数据),其摘要恰好对应第二份 DLL。
$ oledump.py -s 4 -e SAMPLE.xls | pecheck.py -l 1 | headtail.py
Overlay:
Start offset: 0x00014000
Size: 0x00010200 64.5 KB 44.64%
MD5: 6eede113112f85b0ae99a2210e07cdd0
SHA-256: 141d71d86cd25b210b67fe8e49d2abf63324b7ce36736b95b51c9258c4b1ddbb
MAGIC: 4d5a9000
PE file without overlay:
MD5: 3bd4fcbee95711392260549669df7236
SHA-256: 5f66744cef565f0be87c84011293a89931373a34be3eea7c247d2d61f7c499d2这里的 0x14000 相对第一份提取物起点,而前一节的 0x16ebb 相对整个嵌入对象。两者满足 0x2ebb + 0x14000 = 0x16ebb;混用坐标系会误判载荷位置。
第二份提取物没有 overlay。其头部检查还提供了入口 RVA 与文件偏移,两者应分开记录:
- 类型
- DLL
- 入口点
- 0x10001907
- MD5
- 6eede113112f85b0ae99a2210e07cdd0
- SHA256
- 141d71d86cd25b210b67fe8e49d2abf63324b7ce36736b95b51c9258c4b1ddbb
- AddressOfEntryPoint
- 0x1907
- EntryPointFileOffset
- 0x0D07
- Overlay
- None
| 名称 | 熵 |
|---|---|
| .text | 5.25 |
| .rdata | 2.89 |
| .data | 6.38 |
| .pdata | 2.15 |
熵(位/字节)< 1 稀疏< 6.8 常规6.8–7.2 偏高≥ 7.2 压缩或加密
entry 是工具按首选映像基址计算的地址,不是运行时观测地址。PE 文件没有通用的结束标记,裁剪还需结合节的原始数据范围,以及证书表等可能位于节外的数据;本例的相邻起点、结构终点与对象 EOF 相互吻合。Microsoft PE 格式规范
用算术复核边界
下面的检查只验证已列出的区间关系,不解析或执行恶意文件,也不重新计算样本摘要。Python 切片使用右开区间,因此含终点的边界要加一。
object_size = 159931
regions = [(0x2ebb, 0x16eba), (0x16ebb, 0x270ba)]
lengths = [end - start + 1 for start, end in regions]
assert lengths == [0x14000, 0x10200]
assert regions[0][1] + 1 == regions[1][0]
assert regions[1][1] + 1 == object_size
assert regions[0][0] + sum(lengths) == object_size
assert object_size - regions[0][0] == 0x24200
print("PASS: contiguous PE ranges; object EOF; overlay = DLL64")$ python verify_regions.py
PASS: contiguous PE ranges; object EOF; overlay = DLL64摘要需要跟着字节范围走:首段去除 overlay 后的摘要,和从首段起点取到 EOF 的摘要,描述的是不同对象。仅凭哈希不同就宣称出现变种,会丢失这一层语义。
VBA 声明连接了架构与文件名
13 号流的 Module2 声明如下。保留它的返回类型,是为了分析样本,而不是将其推荐为 Win64 API 的正确写法。
Attribute VB_Name = "Module2"
#If Win64 Then
Public Declare PtrSafe Function Amway Lib _
"exchange2.dll" () As Integer
Public Declare PtrSafe Function k32LL Lib "kernel32" Alias "LoadLibraryW" (ByVal lpLibFileName As String) As Long
#Else
Public Declare Function Amway Lib _
"exchange1.dll" () As Integer
Public Declare Function k32LL Lib "kernel32" Alias "LoadLibraryW" (ByVal lpLibFileName As String) As Long
#End IfWin64 判断的是 VBA 宿主环境位数:64 位 Office 分支声明 exchange2.dll,另一分支声明 exchange1.dll。64 位 Windows 上的 32 位 Office 仍应走后者,选择依据不是操作系统名称。Microsoft VBA 编译器常量
LoadLibraryW 将模块装入调用进程并返回模块句柄,但上述 64 位声明仍写着 As Long。PtrSafe 不会自动把指针或句柄类型扩宽;需要保存 64 位句柄的声明应采用相应的指针宽度类型。这也说明:看见接口声明,不等于已经证明这条路径正确执行。Microsoft LoadLibraryW、Microsoft PtrSafe
双架构载荷与条件声明支持“按 Office 位数选择 DLL,在 Excel 进程内加载并调用 Amway”这一解释。要把解释补成完整执行证据,还需定位对象读取、文件写入、加载调用与导出调用的函数体,并核对运行日志。现有材料没有给出 Amway 内部逻辑,家族归属、联网和持久化应保持未定。
样本标识与检测落点
以下摘要分别标识工作簿、嵌入对象和两份有明确边界的 DLL。DLL32、DLL64 是区间标签;文件名则来自宏声明,实际落地内容仍需与哈希关联。
86a07beee7a5d10a9e36eb8cb95abc0254a7e468930197f94094b2611dcd2b16SAMPLE.xls5f66744cef565f0be87c84011293a89931373a34be3eea7c247d2d61f7c499d2DLL32 [0x2ebb, 0x16eba]141d71d86cd25b210b67fe8e49d2abf63324b7ce36736b95b51c9258c4b1ddbbDLL64 [0x16ebb, 0x270ba]
b9fd4ec14b72f944160ebafa7f45b8182AB07F92.png
exchange1.dllexchange2.dll
检测不应止步于“Office 是否产生可疑子进程”。这类路径需要把文档打开、宏执行、DLL 文件写入与 Excel 模块加载关联起来;单独命中文件名或 PNG 魔数都不足以定性。应用控制也要核实策略是否覆盖 DLL,而不只覆盖 EXE,具体结论取决于实际部署的规则。
复核时最值得保留的是对象提取范围、PE 边界、带范围说明的摘要和宏调用关系。它们让后续动态分析有可追溯的输入,也让“包含两份 DLL”与“已经执行两份 DLL”保持清楚的区别。