~/posts/binary/excel-embedded-png-dll-analysis.md

Excel 嵌入 PNG 中的双架构 DLL

从 OLE 流、PNG 文件头和两个相邻 PE 区间追踪 Excel 样本的载荷,核对 overlay 与文件摘要的关系,再用 VBA 位数分支区分进程内加载意图和实际执行证据。

date[31:24]
read[23:16]
6 分钟
cat[15:8]
二进制
目录
  1. 0x00从对象流找到载荷容器
  2. 0x01PNG 文件头与内部 PE 是两层证据
  3. 0x02overlay 改变了“同一份 DLL”的范围
  4. 0x03用算术复核边界
  5. 0x04VBA 声明连接了架构与文件名
  6. 0x05样本标识与检测落点
  7. 0x06参考资料

一个文件以 PNG 签名开头,并不意味着后面只有图像数据。这个 2019 年的 Excel 样本在嵌入对象中放入两份不同位数的 DLL,VBA 声明则分别指向 exchange1.dll 和 exchange2.dll。把容器边界与宏接口对起来,比单独寻找可疑字符串更有解释力。

分析范围是工作簿的 OLE 结构、嵌入对象和可见的 VBA 声明。下文偏移均绑定这一样本;静态材料未覆盖完整宏函数体和运行日志,因此不把 DLL 成功加载、导出执行或后续联网写成已观测事件。

从对象流找到载荷容器

oledump.py 共枚举出 22 个流。对象标志 O 出现在 4 号流,VBA 标志出现在 12、13、14、20、22 号流;先分别跟进对象与代码,避免把整个工作簿当成一段不分层的字节串。

关键 OLE 流6 行
编号 标志 字节数 流路径
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;流编号和输出数值对应本例,而不是任意工作簿的固定布局。

对象元数据与 PE 规则命中(节选)
$ 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 是两层证据

嵌入对象的前 16 字节
0000000089504E470D0A1A0A0000000D49484452
  1. PNG signature0x00–0x07
  2. IHDR length0x08–0x0B
  3. IHDR0x0C–0x0F

文件头支持“以 PNG 开头”,不证明整个对象是纯图像,也不直接证明图像被完整解析过。-e 提取嵌入对象内容后,pecheck.py -l P 给出两份 DLL 的起点、结构终点和外层文件终点:

对象内 PE 定位
$ 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)
对象内偏移,终点均包含在区间内2 行
载荷 起点 结构终点 长度
DLL32 0x2ebb 0x16eba 0x14000
DLL64 0x16ebb 0x270ba 0x10200

两段连续排列:第一段终点加一就是第二段起点;第二段终点加一恰好是对象长度 159931。不要把外层流的 160402 字节直接代入这些对象内偏移,两者之间还隔着对象包装层。

overlay 改变了“同一份 DLL”的范围

从第一份 PE 起点一直取到对象末尾,会得到 0x24200 字节,而不是第一份 PE 自身的 0x14000 字节。工具因此把余下的 0x10200 字节视为 overlay(尾随数据),其摘要恰好对应第二份 DLL。

第一份 PE 的 overlay(节选)
$ 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 与文件偏移,两者应分开记录:

第二份提取物PE32+
类型
DLL
入口点
0x10001907
MD5
6eede113112f85b0ae99a2210e07cdd0
SHA256
141d71d86cd25b210b67fe8e49d2abf63324b7ce36736b95b51c9258c4b1ddbb
头部字段
AddressOfEntryPoint
0x1907
EntryPointFileOffset
0x0D07
Overlay
None
节区4
名称熵
.text5.25
.rdata2.89
.data6.38
.pdata2.15

熵(位/字节)< 1 稀疏< 6.8 常规6.8–7.2 偏高≥ 7.2 压缩或加密

entry 是工具按首选映像基址计算的地址,不是运行时观测地址。PE 文件没有通用的结束标记,裁剪还需结合节的原始数据范围,以及证书表等可能位于节外的数据;本例的相邻起点、结构终点与对象 EOF 相互吻合。Microsoft PE 格式规范

用算术复核边界

下面的检查只验证已列出的区间关系,不解析或执行恶意文件,也不重新计算样本摘要。Python 切片使用右开区间,因此含终点的边界要加一。

verify_regions.pypython
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 的正确写法。

Module2 声明段vb
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 If

Win64 判断的是 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 是区间标签;文件名则来自宏声明,实际落地内容仍需与哈希关联。

样本与提取物
SHA-256 3
  • 86a07beee7a5d10a9e36eb8cb95abc0254a7e468930197f94094b2611dcd2b16
    SAMPLE.xls
  • 5f66744cef565f0be87c84011293a89931373a34be3eea7c247d2d61f7c499d2
    DLL32 [0x2ebb, 0x16eba]
  • 141d71d86cd25b210b67fe8e49d2abf63324b7ce36736b95b51c9258c4b1ddbb
    DLL64 [0x16ebb, 0x270ba]
MD5 1
  • b9fd4ec14b72f944160ebafa7f45b818
    2AB07F92.png
文件名 2
  • exchange1.dll
  • exchange2.dll
6 个指标 · 3 类 · 已去活(defang)

检测不应止步于“Office 是否产生可疑子进程”。这类路径需要把文档打开、宏执行、DLL 文件写入与 Excel 模块加载关联起来;单独命中文件名或 PNG 魔数都不足以定性。应用控制也要核实策略是否覆盖 DLL,而不只覆盖 EXE,具体结论取决于实际部署的规则。

复核时最值得保留的是对象提取范围、PE 边界、带范围说明的摘要和宏调用关系。它们让后续动态分析有可追溯的输入,也让“包含两份 DLL”与“已经执行两份 DLL”保持清楚的区别。

参考资料

NORMAL~/posts/binary/excel-embedded-png-dll-analysis.md§--
0%zh-CN