---
title: 一封退货邮件的陷阱：“开箱视频”背后的 WPS、隐藏 DLL 与恶意加载链
tags:
  - 网络安全
  - 钓鱼邮件
  - 恶意软件分析
  - 逆向工程
  - Linux
categories:
  - 技术分享
date: 2026-11-05 10:36:46
updated: 2026-11-05 10:36:46
desc: 一封以商品损坏为由要求退货的邮件，让收件人通过 Google 短链接查看开箱视频，最终下载到的却是装着 WPS 程序和隐藏 DLL 的压缩包。本文从邮件和浏览器实景出发，拆解文件名伪装、DLL 侧加载、加密命令、Python 与 .NET 载荷之间的关系，再用断网虚拟机中的运行记录核对持久化写入和域名解析尝试。结合 isMalicious 的域名信誉截图，区分已经观察到的危险与尚未确认的窃取、外传行为，并给出普通收件人识别邮件、核对文件和误运行后的处置方法。
abbrlink: outlook-return-malware
cover: /images/outlook-return-malware/cover.webp
---
> **AI 辅助声明｜AI agent coding 辅助创作**
>
> Codex 辅助编写取证脚本、分析静态与隔离运行记录，并参与本文整理和写作。

> **安全警告：不要点击文中的可疑地址、下载样本或运行其中的程序。**
>
> 我有相关技术经验，日常使用 Linux 桌面，才另外准备隔离环境分析这封邮件并写下本文。
>
> 样本执行只发生在断网虚拟机里的 Wine 中，宿主个人目录和账号会话没有共享进去。
>
> Linux 和 Wine 都不提供“随便运行 Windows 恶意程序也安全”的保证，虚拟机也有剩余风险，请勿照着尝试。

![退货邮件中的“开箱视频”诱导示意：邮件与视频外观背后连接着 EXE 和 DLL 文件。](/images/outlook-return-malware/cover.webp)

*AI 生成的封面插画：以查看售后视频为由，引向 EXE 与 DLL 加载链；不是取证截图。*

一封声称“商品损坏，想申请退货”的邮件，最后交来的是一个 Windows 程序包。

最显眼的文件名里写着 `mp4`，内部身份却是 WPS PDF；它启动时还会加载旁边的 DLL，继续解包、运行 Python，并尝试装入 .NET 代码。

这就是我在 2026 年 11 月 4 日收到的那封邮件。

对方没有索要密码，也没有催我付款，而是请我先看开箱视频和照片，再处理售后。

对于每天处理客户邮件的人，这个请求很像正常工作的一部分。

沿着它给出的链接往下看，危险的转折发生在“查看材料”被换成“启动程序”的时候。

<!--more-->

## 售后故事给外链下载找好了理由

邮件标题是「有關產品無法使用的退貨查詢」。

发件人显示为「張慧敏」，声称刚收到的商品内部严重损坏、无法使用，已经保留开箱视频和照片，希望安排退货、换货或退款。

材料没有附在邮件里，因为“文件太大”，需要打开外链查看。

![Outlook 中的原始退货邮件，声称文件太大，要求通过链接查看开箱视频和照片。](/images/outlook-return-malware/01-outlook-return-email.webp)

*原始收件画面：退货诉求与“文件太大，请查看链接”连在一起。*

| 邮件字段 | 实际内容 |
| --- | --- |
| 收件时间 | 2026-11-04 16:16:19，东八区 |
| 主题 | 有關產品無法使用的退貨查詢 |
| 显示名称／发件地址 | 張慧敏／`noreply@appsheet.com` |
| 业务信息 | 未提供订单号和商品名称 |
| 附件 | 无 |
| 链接入口 | `share[.]google/psgMaz996t9HspoPs` |
| 页脚 | Sent from AppSheet |

这封信借用了售后处理中很自然的一步：先看证据，再决定怎么解决问题。

它缺少订单号和商品名称，却给了收件人一件可以立刻做的事，也就是打开链接。

发件地址里的 AppSheet 属于正规服务，但地址本身无法证明这是一位真实客户。

[AppSheet 官方文档](https://support.google.com/appsheet/answer/11447614?hl=en)说明，自动邮件的主题、正文和发件人显示名称可以配置。

使用正规平台发送邮件，与邮件中的售后故事真实，是两件需要分别核对的事。

我没有取得这封信的完整原始认证头，因此不判断用于核验邮件来源的 SPF、DKIM、DMARC 认证是否通过。

## Google 短链接把浏览器带到了第三方下载站

邮件里的 `share.google` 确实是 Google 使用的短链接域名。

[Google 官方中文说明](https://support.google.com/websearch/answer/7013866?hl=zh-Hans)提到，Google 应用分享网页时会生成 `share.google` 或 `search.app` 链接。

这能解释入口为什么看起来熟悉，最终目的地仍要沿跳转记录检查。

这次短链经过 Google 的几次重定向后，进入了 `filehub.freshpartners[.]co.nz` 的下载路径。

下文可疑地址里的 `[.]` 表示点号，`hxxps` 表示已去活化的协议名，避免文章形成可直接点击的样本入口。

```text title="邮件链接的主要跳转节点，省略临时参数"
hxxps://share[.]google/psgMaz996t9HspoPs
  → www.google.com/share.google?q=…                 HTTP 302
  → www.google.com.hk/url?…                         HTTP 302
  → www.google.com.hk/share.google?q=…              HTTP 301
  → filehub.freshpartners[.]co.nz/dl/
      Complete_Item_Assessment_mp4/ca50…b467?…      HTTP 302
```

301 和 302 是服务器要求浏览器转到其他地址的响应，这些中间节点没有各自呈现一张网页。

### 同一台 Linux，两次会话得到了不同内容

我先用 Linux 浏览器的默认客户端标识访问短链，最后看到的是微软 OneDrive 产品介绍页。

它没有展示开箱视频，也没有要求我更换操作系统。

![Linux 默认浏览器会话落到微软 OneDrive 产品介绍页，顶部可见 Cookie 提示。](/images/outlook-return-malware/02-linux-onedrive-landing.webp)

*第一份浏览器结果：真实的 OneDrive 产品页，邮件里的售后材料并未出现。*

另一次会话把请求中的客户端标识改成 Windows Chrome，下载站就交付了 ZIP 附件，没有出现视频播放器。

两次浏览器实际都运行在 Linux 上，这里的 Windows 只是请求头中的标识。

![Windows 伪装请求触发下载后，Chrome 下载记录出现文件。](/images/outlook-return-malware/03-windows-download-record.webp)

*另一份浏览器结果：Chrome 已记录下载；自动化保存时使用内部标识，服务器给出的文件名见下方响应头。*

ZIP 大小是 **138,900,867 字节，约 132.47 MiB**，文件名仍在强调 `mp4`。

```http title="实际下载响应节选"
HTTP/2 200
Content-Type: application/octet-stream
Content-Length: 138900867
Content-Disposition: attachment; filename="Complete_Item_Assessment_mp4_838a86d1.zip"
Cache-Control: no-store, no-cache, must-revalidate, max-age=0
```

**这两份结果能证明下载站返回了不同内容，尚不能证明它只按操作系统筛选。**

<details>
<summary>浏览器条件：两次请求并非只改变了操作系统</summary>

访问发生在 2026 年 11 月 4 日 18:08 左右，使用系统安装的 Chrome 157 无头模式、独立会话和 1440 × 1000 视口。

User-Agent 是浏览器随请求发送的客户端标识，通常包含操作系统与浏览器版本。

| 请求条件 | 当时结果 |
| --- | --- |
| 默认 Linux：`X11; Linux x86_64`、`HeadlessChrome/157…` | 下载站重定向到 OneDrive 产品页 |
| Windows 伪装：`Windows NT 10.0; Win64; x64`、`Chrome/142…`，`sec-ch-ua-platform` 为 `"Windows"`，但 `sec-ch-ua` 品牌版本仍为 157 | 跳到带 `f=` 参数的下载地址，返回 ZIP；自动化报告 `Download is starting` |
| 后续用独立 HTTP 客户端补查，版本统一为 Chrome 157 | 两种平台标识都得到前往 OneDrive 产品页的重定向 |

Cookie、版本组合、其他请求特征、访问时序或服务端规则都可能影响结果，现有记录没有隔离这些变量。

OneDrive 那张正常网页也不能因此被当成恶意下载页；需要分析的是前面的下载站，以及它实际交付的文件。

</details>

```mermaid title="邮件到下载文件的实际观察"
flowchart TD
    A[售后邮件：查看开箱视频和照片] --> B[Google 短链接与多次重定向]
    B --> C[filehub 第三方下载站]
    C --> D[初次 Linux 默认会话]
    C --> E[初次 Windows 伪装会话]
    D --> F[微软 OneDrive 产品介绍页]
    E --> G[138900867 字节 ZIP]
```

*分支是两次会话的记录，不是对服务器筛选算法的还原。*

## 文件名带着 mp4，扩展名却是 exe

我先读取压缩包目录、提取字节分析，没有在日常桌面双击程序。

包内共有 791 个条目，其中 742 个文件、49 个目录；与下载名对应的目录顶层，只有一个 EXE、一个 DLL 和一个目录。

EXE 的完整名称是 `Complete_Item_Assessment_mp4.exe`，表中简写为 `…_mp4.exe`。

| 顶层内容 | 字节数 | 内容与 ZIP 属性 |
| --- | ---: | --- |
| `…_mp4.exe` | 1,321,728 | Windows 程序；普通归档，`0x20` |
| `krpt.dll` | 126,144,000 | 启动时加载的 DLL；隐藏＋系统，`0x06` |
| `x/` | 目录 | 后续载体；目录＋隐藏＋系统，`0x16` |

隐藏与系统属性来自 ZIP 中央目录的实际记录。

如果解压工具保留属性，Windows 又按相关设置隐藏这些文件，收件人最容易看见的就会是那个名称带着 `mp4` 的 EXE。

读这个文件名时，应看最后的 **`.exe`**，而不是中间的 `_mp4`。

扩展名也可以被伪装，真实格式还要结合文件头和内容核对；这里已经确认交付的是 Windows 程序。

[微软的扩展名说明](https://support.microsoft.com/en-us/windows/experience/storage-filemanagement/common-file-name-extensions-in-windows)也把 `.exe` 与 `.mp4` 分别列为可执行程序与视频类型。

到这一步，普通收件人已经有充分理由停下：售后照片和视频，没有解释为什么需要运行 EXE。

![真实下载响应、ZIP 顶层目录与 EXE 导入关系的证据摘录。](/images/outlook-return-malware/04-download-and-import-evidence.webp)

*真实 HTTP 与文件分析输出的排版摘录，方便对照文件名、大小和 DLL 依赖；不是下载站页面。*

### 这个 EXE 内部仍然叫 WPS PDF

读取程序内部字段后，可以看到 `WPS PDF`、`WPS Office` 和原始文件名 `wpspdf.exe`。

它保留了办公软件的身份，外部文件名却被改成了“商品评估 mp4”。

更关键的是，EXE 的导入表明确依赖同目录里的 `krpt.dll`。

导入表记录程序启动时需要装入的外部模块，这个依赖把 EXE 与旁边的 DLL 接到了一起。

```text title="EXE 的导入关系节选"
DLL: krpt.dll
Imported symbol: ?_force_link_krpt@@YGXXZ
Import Address Table RVA: 0x00097260
```

DLL 是程序调用的功能模块，装入进程时可以执行初始化代码。

将一个现成 EXE 与恶意 DLL 放在它会加载的位置，借 EXE 启动 DLL，通常称为 **DLL side-loading，DLL 侧加载**。

[微软的 DLL 安全说明](https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-security)指出，攻击者控制 DLL 搜索位置时，程序可能加载非预期的库，并以宿主进程的权限执行其代码。

具体[搜索顺序](https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order)受应用和加载方式影响；这个样本除了有明确导入依赖，后面的运行日志也记录到了该 DLL 的实际装载。

<details>
<summary>EXE 内部资源与签名信息的验证范围</summary>

| EXE 内部字段 | 实际值 |
| --- | --- |
| `FileDescription` | `WPS PDF` |
| `OriginalFilename` | `wpspdf.exe` |
| `ProductName` | `WPS Office` |
| `FileVersion` / `ProductVersion` | `11,2,0,9363` |
| 公司字段 | `Zhuhai Kingsoft Office Software Co.,Ltd` |
| 证书表中的签名者证书主体 | `Zhuhai Kingsoft Office Software Co., Ltd.` |

分析工具读取到了 Authenticode 证书结构，但没有验证当前文件的摘要、证书链和时间戳。

因此，这里能确认文件携带金山办公的证书信息，不能只凭这一点宣布当前签名有效。

即使 EXE 的签名有效，也不会替同目录的 DLL 或整包内容作安全担保。

</details>

## 隐藏 DLL 把“看文档”接成了多层执行

沿着 DLL 的初始化路径分析，程序打开了 `x` 目录中的一个伪装 PDF，解码其中的数据，再把恢复出的命令交给进程创建函数。

本文使用 REA（Reverse Engineer Anything）接口读取文件结构，并通过 Hopper 查看原生代码的反汇编与伪代码。

反汇编把机器指令转换成可读指令，反编译尝试还原接近高级语言的表达式，二者都不等于取得了作者的原始源码。

这条路径解释了为什么只看 WPS 的名称会漏掉问题：用户启动的是 EXE，后续动作已经转入 DLL 的初始化代码。

```mermaid title="样本各组件的职责和加载关系"
classDiagram
    class RenamedEXE {
        WPS_PDF身份
        导入krpt.dll
    }
    class KrptDLL {
        读取伪装PDF
        解码命令
        创建子进程
    }
    class CommandStage {
        cmd.exe
        调用解压器
    }
    class PythonRuntime {
        pythonw.exe身份
        解释混淆脚本
        写入登录启动项
    }
    class ManagedPayload {
        NET_Framework_v4
        完整行为未恢复
    }
    RenamedEXE --> KrptDLL : 普通导入
    KrptDLL --> CommandStage : 启动命令
    CommandStage --> PythonRuntime : 解包并启动
    PythonRuntime ..> ManagedPayload : 交给CLR从内存加载
```

*类框按实际文件划分职责，用于讲解组件关系，并非声称这些二进制定义了图中的同名类。*

### 小 PDF 装命令，PNG 负责解压，更新程序其实是 Python

DLL 读取的文件路径是 `x\수박_유튜브시청기록_2015-12-10.pdf`，总共只有 1379 字节，没有 PDF 文件头。

它使用 RC4 流加密保存命令：文件前段是密文，后段是密钥，最后四字节给出密钥长度。

| 文件范围 | 字节数 | 用途 |
| --- | ---: | --- |
| 开头 | 1164 | 待解码的命令字节 |
| 随后 | 211 | RC4 密钥 |
| 最后 | 4 | 小端整数，值为 211 |

只对字节执行同样的解码运算，就能恢复连贯的 UTF-16 命令文本，不需要运行恢复出的命令。

命令先进入 `x` 目录尝试打开 `document.pdf`，随后调用后缀为 PNG 的程序，解开 DAT 归档，再启动解包出来的 `MicrosoftEdgeUpdate.exe`。

逐一核对格式后，PNG 外观的文件被识别为 RAR 解压程序，DAT 是加密归档，`document.pdf` 则是 Word 容器。

名字像浏览器更新组件的 `MicrosoftEdgeUpdate.exe`，内部身份是 Python 解释器；它随后要解释的另一个 PDF 外观文件，也是一份混淆脚本。

<details>
<summary>伪装文件的完整名称、格式与版本资源</summary>

PE 是 Windows 可执行文件常见的格式，真实类型来自文件内容，而非它外面的 PDF 或 PNG 后缀。

| 外观或名称 | 实际内容 | 在链中的作用 |
| --- | --- | --- |
| 1379 字节、以 `2015-12-10.pdf` 结尾的文件 | RC4 命令载体 | 提供第一段命令 |
| `ZB_2022년_산책중넘어짐기록_해봤자.png` | PE32+ x64 控制台程序，版本资源为 WinRAR／Command line RAR／7.20.2 | 解压器 |
| `빨래건조현황표_20230226_953.dat` | 加密 RAR5 归档 | 携带下一阶段运行环境 |
| `document.pdf` | 含 `word/document.xml` 的 OOXML Word 容器 | 被尝试作为文档打开 |
| `MicrosoftEdgeUpdate.exe` | PE32 x86 GUI 程序，版本资源为 `pythonw.exe`／Python Software Foundation／3.14.3 | Python 解释器 |
| 解包后的 `WD_도토리_빨래건조현황표_2019.pdf` | 1,828,010 字节的 XOR 混淆 Python 源码 | 持久化和后续加载逻辑 |
| Python 内嵌数据 | 433,152 字节的 PE32 .NET Framework v4.0 载荷 | 从内存装入 CLR |

</details>

文件内部的版本与厂商字段是识别线索，不代表已经逐个验证原厂完整性或有效签名。

`MicrosoftEdgeUpdate.exe` 旁边还有 `python314.dll`、`python314.zip` 和 `python314._pth`，与[Python 官方中文文档](https://docs.python.org/zh-cn/3.14/using/windows.html#the-embeddable-package)描述的嵌入式发行结构相符。

在这份投递包里，解释器被改成浏览器更新组件的名字，又被用来解释另一个 PDF 外观文件里的代码。

![RC4 命令载体分段、伪装解压器、归档与 Python 文件身份的静态证据。](/images/outlook-return-malware/06-disguised-file-chain.webp)

*按真实文件字节、可信解压器与版本资源整理的证据摘录，不含口令和完整执行参数。*

CLR 是执行 .NET 代码的公共语言运行时，Python 在这里又承担了调用 CLR、装入下一阶段程序的宿主角色。

“载荷”指这份等待执行的下一阶段代码，不表示它的完整功能已经被还原。

这份 .NET 文件的字节已完整恢复，但元数据存在重复行和无效索引，REA 只完成部分读取；图里的 `partial` 指分析覆盖范围，不是只恢复了一部分文件。

```mermaid title="从伪装文件恢复出的完整加载方向"
flowchart TD
    A[改名的 WPS EXE] --> B[krpt.dll 初始化]
    B --> C[读取 1379 字节伪装 PDF]
    C --> D[RC4 解出 cmd.exe 命令]
    D --> E[尝试打开 document.pdf]
    D --> F[运行 PNG 外观的 RAR 解压器]
    F --> G[解开 DAT 归档]
    G --> H[Public 目录中的改名 Python]
    H --> I[XOR 解码另一个伪装 PDF]
    I --> J[持久化与自重启逻辑]
    J --> K[调用 CLR 加载内存中的 NET 载荷]
```

*这张图汇总静态恢复出的文件关系与脚本控制流，运行到哪一步，由后面的日志核对。*

### 脚本还准备在下次登录继续运行

恢复出的 Python 源码包含大量算术、随机名称和间接表达式。

我只对白名单内的常量和字节运算做折叠，把源码当作数据整理，再用隔离运行记录核对结果。

整理后的逻辑尝试创建计划任务，并从两处用户登录入口调用它。

| 目标 | 代码中的作用 |
| --- | --- |
| 计划任务 `MicrosoftEdgeUpdateTaskMachineCore__` | 通过任务计划程序 COM 接口创建，指向改名 Python 和脚本参数 |
| `HKCU\Environment\UserInitMprLogonScript` | 写入调用该任务的字符串 |
| `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` | 再增加一处登录启动入口 |
| 解包目录及文件 | 增加 Hidden、System 属性 |
| 标记值与命名互斥量 | 控制已运行状态和单实例 |

[微软对 Run 键的说明](https://learn.microsoft.com/en-us/windows/win32/setupapi/run-and-runonce-registry-keys)确认了它在用户登录时启动程序的用途。

这种为后续登录留下启动入口的做法称为持久化。

它让风险超过了一次误打开：关闭眼前窗口，并不一定移除了重新启动代码的入口。

脚本另有针对扫描接口的内存修改逻辑，现有记录没有确认其在实验中生效，本文不展示修改字节或绕过步骤。

<details>
<summary>原生逆向细节：DLL 入口、API 哈希与进程创建</summary>

### 导出函数是空的，不代表 DLL 没有动作

用 REA 接入 Hopper 对 `krpt.dll` 做反汇编和关键函数反编译后，一个容易误判的地方出现了：大量导出落到 `0x100179e0`，那里只是一个返回用的空桩。

如果只看这个导出，很容易以为 DLL 什么也没做。

但 DLL 还有加载入口。

这个样本的入口位于 `0x1001c728`，经运行库初始化包装后进入 `0x10001000`；后者在进程附加条件下进入文件读取和后续处理路径。

“进程附加”指 DLL 被装入进程时收到的初始化通知，不需要用户再单独点击一次 DLL。

| 地址或位置 | 静态分析看到的行为 |
| --- | --- |
| `0x1001c728` | PE 入口，转入初始化包装 |
| `0x1001c5ef` | 运行库包装中存在到 `0x10001000` 的调用 |
| `0x100167c7` | 比较初始化原因是否为 `1`，即进程附加 |
| `0x1001687a` | 调用 `0x10019280`，准备动态解析的 API 地址 |
| `0x10016959`、`0x10016ca7` | 经解析后的指针调用 `CreateFileW` |
| `0x100172ca` | 调用 `ReadFile`，读取数据到缓冲区 |
| `0x100176aa` | 调用 `0x10018160`，含 RC4 式状态表初始化、交换和异或处理 |
| `0x1001778b` | 将同一缓冲区传入 `0x100179f0` |
| `0x10018100` | 在 `0x100179f0` 内，间接调用进程创建 API |

这些是按 PE 首选基址整理的静态地址和调用关系。

Wine 观察中，`krpt.dll` 实际被映射到 `0x7B590000`，不能把表里的静态地址原样当成运行时断点地址。

### 为什么导入表里找不到完整行为

`krpt.dll` 的普通导入表主要是 `KERNEL32.dll` 的运行库相关函数，没有直接列出上面所有关键 API。

它另外实现了按哈希值查找函数地址的逻辑。

简单说，程序不在这个位置写出容易搜索的 `CreateProcessW` 字符串，而是保存一个数字，运行时再根据函数名称计算结果、寻找对应地址。

我把反汇编中的哈希运算单独转写成只处理字符串的 JavaScript，核对候选 API 名称；这不需要执行样本。

| 哈希常量 | 对应的候选函数名 | DLL 保存地址 |
| --- | --- | --- |
| `0xc563eb80` | `CreateFileW` | `0x1004d2a0` |
| `0x35d5ea3a` | `CloseHandle` | `0x1004d29c` |
| `0xacb08688` | `ReadFile` | `0x1004d298` |
| `0xa1059b95` | `GetFileSizeEx` | `0x1004d294` |
| `0x628857b4` | `CreateProcessW` | `0x1004d290` |

哈希可能碰撞，所以还要核对调用参数，不能凭一个数字直接给任意间接调用命名。

最后一项同时吻合了进程创建函数的十个参数、启动信息结构和进程信息结构，支持把这个调用识别为 `CreateProcessW`。

下面是把关键调用整理成易读形式后的节选，**变量名是为讲解重命名的，省略了大量常量运算和包装，不是恢复出的原始 C 源码，也不是可直接编译的完整程序**。

```c title="按 REA 伪代码与反汇编整理的进程创建调用"
// 0x10019280 中的解析结果写入 0x1004d290。
// 0x100179f0 的调用处使用这个函数指针。
STARTUPINFOW startup = {0};
startup.cb = 0x44;                  // 32 位 STARTUPINFOW 的大小
PROCESS_INFORMATION process = {0};

resolved_CreateProcessW(
    NULL,                          // 由命令行中的首个项目决定启动目标
    decoded_buffer,                // 上游读取、处理后传来的缓冲区
    NULL, NULL, FALSE,
    0x08000000,                    // CREATE_NO_WINDOW
    NULL, NULL,
    &startup, &process
);
```

[`CreateProcessW` 的官方定义](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw)是创建进程和主线程。

其中 `0x08000000` 对应 [`CREATE_NO_WINDOW`](https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags)，对于控制台程序，可以不创建控制台窗口；它不等于对任何类型程序都能“完全隐身”。

这个链条已经明显超出了“显示商品照片”的用途：旁加载的 DLL 在读取数据、进行解密式处理，并准备以处理后的内容启动另一个进程。

继续恢复初始化代码构造的路径后，缓冲区的来源也明确了：它来自 `x` 目录里一个伪装成 PDF 的小文件，解码后是一段 `cmd.exe` 命令。

![REA 反编译中解析 API 和创建进程的关键输出。](/images/outlook-return-malware/05-rea-process-creation.webp)

*REA 静态反编译输出节选：哈希解析写入函数指针，后续以 `0x08000000` 标志调用；变量保留反编译器命名。*

```mermaid title="从静态反汇编还原的关键调用顺序"
sequenceDiagram
    participant L as Windows 加载器
    participant D as krpt.dll 初始化路径
    participant R as API 哈希解析器
    participant F as 文件与缓冲区
    participant P as 进程创建 API
    L->>D: 装载 DLL 并发送进程附加通知
    D->>R: 0x1001687a 调用解析函数
    R-->>D: CreateFileW / ReadFile / CreateProcessW 等地址
    D->>F: 打开并读取 1379 字节的伪装 PDF
    D->>D: 0x100176aa 进行 RC4 解码
    D->>P: 0x1001778b 传入处理后的缓冲区
    Note over D,P: 创建标志为 CREATE_NO_WINDOW
    Note over F,P: 解出 cmd.exe 命令，进入后续解包链
```

*图表示静态代码安排的顺序；后文用隔离 Wine 的进程和文件记录核对它的运行结果。*

</details>

<details>
<summary>大 DLL 的附加数据：高熵不等于已经找到隐藏功能</summary>

`krpt.dll` 大小为 126,144,000 字节，但 PE 最后一个节的原始数据结束于文件偏移 `0x58600`，也就是 361,984 字节。

剩余 **125,782,016 字节**位于节表所描述的数据范围之后，通常称为 overlay，附加数据区。

按这段数据计算，零字节约占 0.3902%，香农熵约为 7.999998 bit/byte。

香农熵在这里衡量字节分布的不确定性，每字节理论上限为 8 bit；这个结果接近上限，不能描述为一大堆零填充。

高熵只能说明字节分布接近均匀，压缩、加密或随机数据都可能产生这种现象，单靠它不能断言某种恶意家族，也不能证明每一个附加字节都有用途。

命令载体位于 `x` 目录，不能把这个目录笼统当成无害诱饵。

巨大 overlay 的具体用途仍未确认；已经恢复的执行链，依赖的是几个小载体与加密归档。

</details>

## 断网运行核对：后台解包、启动与注册表写入确实发生了

静态分析指出了执行方向，运行记录则用来回答其中哪些动作真的发生。

我把原始投递包放进断网 KVM 虚拟机里的 Wine 观察，启动前核对 EXE、DLL 的哈希，没有修改载荷或修补它的调用逻辑。

Wine 提供 Windows 兼容环境，但新建一个 Wine prefix 并不能把未知程序安全地关住。

prefix 是虚拟 Windows 目录、注册表和配置的一组文件；Wine 仍可能把文件操作映射到 Linux 路径，删除 `Z:` 盘符也不能单独阻断这种访问能力。

因此，隔离边界放在虚拟机及外层限制上。

```mermaid title="样本运行环境与宿主之间的边界"
flowchart TD
    A[宿主：Bubblewrap 独立命名空间与资源上限]
    A --> B[非特权 QEMU 与 KVM：无虚拟网卡]
    B --> C[临时 Debian 客体：只读 ISO 输入样本]
    C --> D[普通用户 Wine：写入落在客体盘]
    D --> E[客体 root 记录：系统调用、注册表、串口]
```

*宿主个人目录、账号会话、图形与剪贴板均未共享给样本。*

<details>
<summary>实验环境、观察窗口与隔离核验条件</summary>

| 层次 | 实际实验条件 |
| --- | --- |
| 虚拟机 | QEMU 10.2.1，Ubuntu 包版本 `1:10.2.1+ds-1ubuntu3.3`，KVM、`q35`、2 vCPU、2560 MiB 客体内存 |
| 客体 | Debian 13，内核 `6.12.115+deb13.1-cloud-amd64` |
| Wine | Debian `wine-10.0 (10.0~repack-6)`；fresh win64 prefix，兼容 32 位和 64 位程序 |
| 网络 | 运行样本时没有虚拟网卡，外层也有独立网络命名空间；客体只有 loopback、没有外部路由 |
| 文件交换 | 通过只读样本 ISO 输入，写入落在本次客体临时盘；没有宿主 home、共享目录、剪贴板、显示或 DBus 套接字 |
| 外层权限 | QEMU 为非特权进程，capabilities 为零，`NoNewPrivs` 与 seccomp 开启 |
| 资源边界 | 外层内存上限 5 GiB、96 tasks、CPU 配额 180%，独立运行上限 5 分钟 |
| 观察窗口 | 第一轮原始 EXE 最长观察 60 秒，补齐 CLR 后为 90 秒；启动前校验 EXE、DLL 哈希 |
| 观察记录 | 客体 root 保存 strace，样本以普通 analyst 用户运行；脱离控制终端，输入为 `/dev/null` |

准备合法系统和 Wine 时允许安装软件，样本没有挂载到准备阶段；实际样本进入环境后，网络设备已移除。

无害探针先核对了客体不能读取或写入未映射的宿主哨兵，不能连接准备阶段的网关或宿主控制端口，并分别成功运行 Wine 的 32 位、64 位内置命令程序。

外层检查也确认了运行中的 QEMU 看不到宿主个人目录，独立超时能终止整个 scope。

[QEMU 的安全文档](https://www.qemu.org/docs/master/system/security.html)把硬件虚拟化、最小权限、命名空间和资源约束列为相关隔离机制，也明确指出虚拟设备本身仍然是攻击面。

这些设置减少了暴露面，不构成“绝对不可能逃逸”的保证。

</details>

### 第一轮已经走到了 Python 和持久化写入

原始 EXE 启动后，Wine 加载了 `krpt.dll`，随后出现 `cmd.exe`、伪装解压器和改名的 Python 程序。

`document.pdf` 没能打开，日志中 `winebrowser` 报告找不到合适应用，后续解包与 Python 启动仍然继续。

| 核对位置 | 运行记录 |
| --- | --- |
| DLL 与载体 | `C:\sample\krpt.dll` 作为 native 模块装入；系统调用记录成功打开命令载体 |
| 解包与 Python | PNG 外观的程序作为 native 模块启动；Public 下出现解包目录；改名程序加载 `python314.dll`、`_ctypes.pyd`，随后以标记参数再次启动自身 |
| 登录启动入口 | 用户注册表新增 `UserInitMprLogonScript`，以及 `Run` 下的任务触发值 |
| 计划任务 | 调用了 `TaskFolder_RegisterTaskDefinition`，但 Wine 标为 `stub`，无法确认任务已注册成功 |
| 下一阶段 | CLR 宿主报告 `Wine Mono is not installed`，本轮没有确认 .NET 载荷执行 |

`native` 在这些 Wine 日志里表示按外部 Windows 模块加载，与 Wine 的 `builtin` 内建实现相对，不表示这是原生 Windows 上的测试。

`stub` 表示相关接口在该环境中是桩实现，所以“调用过任务接口”和“下次登录一定能启动”之间还缺一段证据。

![隔离 Wine 中的实际进程链、注册表新增项和缺少 CLR 的日志摘录。](/images/outlook-return-malware/07-wine-runtime-observation.webp)

*来自客体运行记录的排版摘录；持久化值写入、任务接口的 stub 和 CLR 错误分别保留。*

缺少 Wine Mono 是实验环境的兼容性终点，不能据此推断它在 Windows 上也会停住。

Python 脚本虽然包含虚拟机检查函数，但对应的主入口提前退出条件经常量折叠为 `if 0`，现有记录也不支持把这次停止归因于反虚拟机检测。

### 补齐运行时后，Mono 与微软 CLR 得到不同结果

我又准备了 Wine Mono 和微软 .NET Framework 4.8 两种环境，先用无害的 x86 探针核对运行时，再观察原始投递链。

[Wine Mono](https://github.com/wine-mono/wine-mono) 是 .NET Framework 的替代实现，与微软原版 CLR 需要分开看待。

| 环境 | 确认运行时的依据 | 样本的观察终点 |
| --- | --- | --- |
| Wine Mono 9.4.0 | 安装包哈希与 Wine 10.0 源码一致；无害探针运行成功 | 内存程序集 `Hwteqj` 装入；方法追踪看到了初始化执行，随后出现无效 IL 和类型初始化异常，未观察到已知主入口进入 |
| 微软 .NET Framework 4.8 | 官方离线包；无害探针成功；实际加载 native `mscoree.dll`、`clr.dll`，注册表 Release 为 `528049` | 原链 Python 进程反复请求解析一个域名；伴随周期性异常，达到 90 秒上限后由实验机制终止 |

Wine Mono 的版本与安装包哈希可对照 [Wine 10.0 安装源码](https://github.com/wine-mirror/wine/blob/wine-10.0/dlls/appwiz.cpl/addons.c)，微软运行时来自 [.NET Framework 4.8 官方下载页](https://dotnet.microsoft.com/en-us/download/dotnet-framework/net48)。

Mono 中确认执行的是初始化路径；微软 CLR 中确认的是模块加载和该进程的域名解析请求，两者都不足以宣布最终功能完整运行。

![补齐 CLR 后的真实日志摘录：Mono 初始化异常、微软 CLR 模块与本地 DNS 回复。](/images/outlook-return-malware/08-dotnet-runtime-comparison.webp)

*两种运行时的结果并列展示；DNS 报文来自断网客体记录。*

<details>
<summary>CLR 技术细节：探针、参数回退、初始化异常与终止方式</summary>

Python 通过 CLR `v4.0.30319` 和 COM 接口加载程序集；可以对照 [`CLRCreateInstance` 的官方说明](https://learn.microsoft.com/en-us/dotnet/framework/unmanaged-api/hosting/clrcreateinstance-function)理解宿主接口的获取方式。

两种实现都能让无害探针打印下面的结果，但相同的版本字符串，不代表它们能以相同方式处理这份经过保护和混淆的程序集。

```text title="无害 x86 CLR 探针的实际输出"
BENIGN_CLR_OK Version=4.0.30319.42000 PointerBytes=4 System=v4.0.30319
BENIGN_REFLECTION_OK mscorlib
```

### Mono 中，初始化已经开始，主入口仍未证实

原始加载器先带着一个 `string[]` 参数调用入口，但 REA 读出的入口签名是无参数的 `static void`。

方法追踪确实先记录到 `TargetParameterCountException`，不过加载器并没有停在这里：它原本就有一次无参数调用的回退。

回退以后，日志出现了 `<Module>:.cctor`，以及名称以 `<Module>{…}` 开头的类型初始化器，说明已经执行到托管初始化代码。

随后，Mono 在一个混淆方法的 `IL_0229: stloc 3` 位置报告 `InvalidProgramException`，后面又出现了 `TypeInitializationException` 和 `TargetInvocationException`。

```text title="Mono 方法追踪的关键顺序，省略长类型名和调用参数"
EXCEPTION handling: System.Reflection.TargetParameterCountException
ENTER:c <Module>:.cctor
ENTER:c <Module>{…}:.cctor
EXCEPTION handling: System.InvalidProgramException: Invalid IL code
  ... RMFVJx67UD (bool&): IL_0229: stloc 3
EXCEPTION handling: System.TypeInitializationException
EXCEPTION handling: System.Reflection.TargetInvocationException
```

*`<Module>{…}` 是原日志中某个类型名的缩写，不能由这个名字推断它是另一个程序集；节选只保留调用顺序和异常类型。*

完整追踪中没有出现已知入口 `jyKqla8aC` 的方法进入记录。

所以，这次能确认的是“初始化代码开始执行后失败”，还不是“最终恶意功能全部跑完”。

### 微软 CLR 的周期异常与实验终止

在另一轮微软 .NET Framework 4.8 对照中，Wine 日志里约每五秒出现一组 `0x80131604` 与 `0x80131500` 异常信息，分别对应[反射目标调用异常](https://learn.microsoft.com/en-us/dotnet/api/system.reflection.targetinvocationexception?view=netframework-4.8.1)和[一般异常](https://github.com/microsoft/referencesource/blob/main/mscorlib/system/__hresults.cs)。

它们是异常派发记录里的信息字段，不是完整的 COM 返回值清单；缺少内部异常和托管调用栈，无法仅凭这个周期定位失败方法，也不能认定是 C2 重连。

原版 CLR 实验达到 90 秒观察上限后，由实验的超时机制终止，最终 launcher 返回 `137`，不是程序自然退出。

</details>

实验结束后，Wine 进程停止，客体关机，QEMU 退出，宿主上用于检查文件变化的哨兵文件哈希保持不变。

## 域名出现在日志里，但记录停在本地 DNS

微软 CLR 那轮观察中，原链重新启动的 Python 进程向客体本地解析器 `127.0.0.53:53` 请求了 `tea[.]vexexo[.]com` 的 A 和 AAAA 记录，分别用于寻找 IPv4 和 IPv6 地址。

这些请求都没有得到地址答案。

回复码是 `5`，按 [RFC 1035](https://www.rfc-editor.org/rfc/rfc1035#section-4.1.1)表示 `REFUSED`。

**拒绝来自客体本地 DNS 解析器，样本没有因此成功联系上域名对应的外部服务器。**

```mermaid title="系统调用记录支持的名称解析路径"
flowchart TD
    A[原链 Python 进程：已加载 native CLR]
    A --> B[查询 tea［.］vexexo［.］com 的 A 与 AAAA]
    B --> C[客体本地解析器 127.0.0.53:53]
    C --> D[REFUSED：没有地址答案]
    D --> E[客体无外部网卡与路由：未观察到成功外部连接]
```

DNS 记录定位了待解析的名字，没有给出上层协议、请求路径或传输内容。

它是这份样本的一条候选通信线索，尚不能被叫作已经确认的上传地址。

### 另行查询的公开记录与第三方信誉

样本观察结束后，我另行查询了公共 DNS、域名登记与路由信息。

以下是 2026 年 11 月 4 日晚取得的公开快照，与样本在客体里产生的流量是两组记录，之后也可能变化。

| 查询对象 | 当晚结果 | 解释范围 |
| --- | --- | --- |
| `tea[.]vexexo[.]com` 的 A 记录 | `185.139.214.102` | 公共解析器另行取得的地址，客体实验中并未解析成功 |
| `vexexo[.]com` 的权威名称服务器 | `tani.ns.cloudflare.com`、`leland.ns.cloudflare.com` | 使用这些名称服务器，不足以证明内容由 Cloudflare 托管或代理 |
| IP 网段与路由 | `185.139.214.0/24`，登记网络名称 `AlexHost`，路由 `AS200019` | 网络资源记录，不等于攻击者身份或机器实际所在地 |

记录可以分别对照 [Google 公共 DNS](https://dns.google/resolve?name=tea.vexexo.com&type=A)、[Verisign 域名登记](https://rdap.verisign.com/com/v1/domain/VEXEXO.COM)、[RIPE 地址登记](https://rdap.db.ripe.net/ip/185.139.214.102)及[路由信息](https://stat.ripe.net/data/network-info/data.json?resource=185.139.214.102)；这些是公开信息服务的查询入口。

同一天晚上的 [isMalicious 域名报告](https://ismalicious.com/domain/tea.vexexo.com)还把这个名字标为 **Medium Risk（中等风险）**。

![isMalicious 对 tea.vexexo.com 的实际域名报告，显示中等风险、来源数量和名单。](/images/outlook-return-malware/09-ismalicious-domain-report.webp)

*2026 年 11 月 4 日查询时的原始页面截图，评级与收录状态可能变化。*

| 页面字段 | 当时内容 |
| --- | --- |
| 风险等级 | Medium Risk |
| 名单和来源 | 8 份名单、7 个来源；同一发布者或镜像按一个来源计数 |
| 分类 | malware、phishing、c2、malicious、threat-intel |
| 最近出现在名单中 | 2026-11-04 |
| 数据集首次记录 | 2026-08-28 |
| 页面记录更新日期 | 2026-10-20 |

这份聚合记录表明该域名已经出现在第三方威胁情报中，多个名单也可能共享上游数据，不能把 7 个来源直接当成 7 次独立样本分析。

其中 C2 指 Command and Control，即恶意程序与控制端之间的指令和通信机制。

名单标签提供了调查方向，不能代替这份样本的通信协议还原，更不证明实验中发生了数据上传。

## 已确认的危险，已经足以拒绝这个包

这份包将售后图片、视频的查看请求，变成了改名 EXE、DLL 侧加载、解包、Python 和内存载荷加载的执行链。

运行记录还确认了两处登录启动值写入，危害判断不必等到恶意软件家族有了名字才成立。

| 证据层次 | 可以得出的结论 |
| --- | --- |
| 邮件与下载 | 外链实际交付可执行程序包，与所谓开箱视频不符 |
| 文件与静态代码 | EXE 依赖旁边 DLL，DLL 解码命令，脚本包含持久化和 .NET 加载逻辑 |
| 隔离运行 | 实际发生 DLL 装载、解包、Python 启动和自重启，以及用户注册表写入 |
| 兼容性与网络边界 | 计划任务注册成功未证实；Mono 初始化失败；微软 CLR 中观察到本地 DNS 查询，没有成功的外部连接 |
| 仍未确认 | 最终载荷的完整行为、恶意软件家族、控制协议、密码窃取、实际数据外传，以及原生 Windows 下的完整运行结果 |

DLL 侧加载通常先以宿主进程的权限执行，不会因此自动取得管理员权限。

普通用户权限下的代码仍可能访问该用户可读的文档和敏感数据，具体范围受系统权限与应用保护约束。

这是未知程序执行带来的风险范围，当前记录没有证明这份样本已经完成了上述窃取。

实验中的注册表与 Public 目录属于客体 Wine 环境，也不表示日常 Linux 宿主已被感染。

### 已有调查使用了相同的 WPS 宿主文件

我取得的 WPS EXE，还能与 OSArmor 在 2026 年 10 月 23 日发表的[原始调查](https://blog.osarmor.com/459/phishing-email-abuses-appsheet-and-a-signed-wps-office-executable/)对应上：两份 EXE 的 SHA-256 完全相同，DLL 的哈希和大小则不同。

该报告中的邮件写成日文，同样以商品损坏为由，通过 AppSheet 和 Google 短链投递改名 WPS EXE 与 `krpt.dll`。

文件哈希相同支持“复用了相同宿主文件”的判断，相似包装不能直接确定同一个攻击者，也不能把另一份 DLL 的行为套在本样本上。

<details>
<summary>供专业人员比对的文件大小与 SHA-256</summary>

哈希用于核对文件身份，本文不提供样本下载。

```text title="原始文件与恢复出的 .NET 载荷的 SHA-256"
ZIP（138900867 字节）
28e53894076225e468fd237bcfb1cdd8d683448c9757770d8cfdd7a948ae13b5

Complete_Item_Assessment_mp4.exe（1321728 字节）
c73e3b98b5f631669fb3d4b114e98ad55d2b015e2c55003c265e0b75e0c31d9e

krpt.dll（126144000 字节）
8f7eb86566d45ecfa85a7dd46c72b8db55a24a7a0cc7882fe5929d8506664bd4

从原包恢复出的 .NET 载荷（433152 字节）
5e9488d34be24163c971679f5d5d5d3a32c7ae94978c5d01dcbd8793fd6b0fdd
```

</details>

## 收到类似邮件，先核对业务，再核对文件用途

普通收件人不需要会反编译，也能在下载前或启动前识别这条链上的问题。

先回到自己已有的订单系统、工单和客户联系方式，确认是否真有这笔业务，再决定如何接收材料。

| 遇到的情况 | 可采取的做法 |
| --- | --- |
| 陌生人要求退货，却没有订单号、商品信息 | 在已有业务渠道核对，不依赖这封邮件自己提供的身份信息 |
| 以“文件太大”为由要求访问外链 | 要求通过已知售后平台提交，检查跳转后的实际域名 |
| 入口经过 Google、微软等正规服务 | 继续核对最终目的地和文件，入口品牌不能为内容背书 |
| 照片、视频变成 EXE、脚本或“专用查看器” | 停止打开，查看完整扩展名，不凭图标或名称中的 `mp4` 判断 |
| 程序有签名，旁边还有 DLL | 不把单个程序的签名当成整包安全证明 |
| 页面要求关闭防护、放行或绕过系统提示 | 停止操作，通过独立渠道确认，组织设备向安全人员反馈 |
| 已确认是诱导下载的邮件 | 在 Outlook 中报告钓鱼，保留必要证据，不把可运行样本转给别人试 |

在 Outlook 中，可以选中可疑邮件，再通过“报告 → 报告钓鱼”提交，具体入口见[微软的 Outlook 中文帮助](https://support.microsoft.com/zh-CN/Outlook/mail/phishing-and-suspicious-behavior-in-outlook)。

报告钓鱼并不等于阻止发件人继续发信，是否加入阻止列表需要另行处理。

### 已运行程序时，处置重点转向隔离和排查

是否执行过文件、是否提交过凭据，会改变下一步处理方式。

| 已经发生的操作 | 下一步 |
| --- | --- |
| 只阅读邮件 | 不再访问可疑链接，报告邮件 |
| 只打开网页，没有提交信息或运行下载文件 | 关闭可疑页面、检查下载记录，保持浏览器和系统更新。<br>仅访问不等于已经感染，也不能据此排除浏览器漏洞等其他风险。 |
| 下载 ZIP，但未执行内容 | 不解压试运行，不通过 Wine 打开；按留证需要交给安全人员处理，或删除不需保留的样本 |
| 输入密码、验证码或授权登录 | 从干净设备访问真实服务，修改凭据，撤销可疑会话或授权，检查登录记录 |
| 已运行 EXE 或脚本 | 立即断开该设备网络，停止在上面登录敏感账户，联系 IT 或安全人员排查；组织设备遵循事件响应流程 |

如果 Windows 设备已经运行过未知程序，可以参考[微软 Defender 脱机扫描的中文说明](https://learn.microsoft.com/zh-cn/defender-endpoint/microsoft-defender-offline)，了解在常规 Windows 环境之外进行扫描的方法。

一次未检出不能代替完整排查，也不能证明程序留下的登录启动项已经清除。

如果还向可疑页面提交过 Outlook 或 Microsoft 账户凭据，账户与设备要分别处理。

在受影响电脑上更改密码之前，应先排查其中的恶意软件；也可以使用另一台干净设备处理账户。

[微软的账户恢复中文帮助](https://support.microsoft.com/zh-CN/accounts-billing/manage/how-to-recover-a-hacked-or-compromised-microsoft-account)列出了扫描设备、更改或重置密码，以及检查连接账户、邮件转发和自动答复设置的步骤。

这封退货邮件没有把危险写在第一句话里。

它让“帮客户看一下损坏情况”成为打开外链的理由，又把本该直接查看的材料换成了程序包。

我能从这份样本确认到后台加载、持久化写入和一次次被本地拒绝的域名解析，最终载荷的完整行为仍然未知。

对真正处理售后的人来说，可以更早停在那个文件名面前：**看开箱视频，为什么需要运行 `.exe`？**


---

**作者：**xingwangzhe

**本文链接：**[https://xingwangzhe.fun/posts/outlook-return-malware/](https://xingwangzhe.fun/posts/outlook-return-malware/)

本文采用[知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议](https://creativecommons.org/licenses/by-nc-sa/4.0/)进行许可。