yankywilson/graphworm-webworm-detection
GitHub: yankywilson/graphworm-webworm-detection
针对 Webworm APT 组织 GraphWorm 后门的威胁检测包,提供 YARA、KQL、Sigma 规则及 IOC 与 ATT&CK 映射以检测利用 OneDrive 进行 C2 通信的恶意活动。
Stars: 0 | Forks: 0
# GraphWorm / Webworm APT — 检测包
**Webworm APT** 组织使用的定制后门 **GraphWorm** 的检测内容,该后门使用 Microsoft Graph API 和 OneDrive 作为其命令和控制(C2)通道。
**作者:** Yaakov Wilson ([@yankywilson](https://github.com/yankywilson)) | Digacore Technology Consulting
**日期:** 2026-06-16
**TLP:** TLP:WHITE — 可自由分享
## 样本
| 字段 | 值 |
|---|---|
| SHA-256 | `6eb6a34252195ddc7f5fb94c4fb382dedde227c4dfee4a80e9e0ee6f80c8bcb1` |
| 原始文件名 | `C2OverOneDrive_v0316.exe` |
| 大小 | 2.15 MB |
| 类型 | PE32 native C++, MSVC 19.29, 静态链接 OpenSSL + libcurl |
| 首次发现 | 2026-05-20 (MalwareBazaar,由 smica83 提交) |
| VT 检出率 | 24/46 |
| 家族 | GraphWorm |
| 威胁行为者 | Webworm (具有中国背景的 APT) |
**MalwareBazaar:** https://bazaar.abuse.ch/sample/6eb6a34252195ddc7f5fb94c4fb382dedde227c4dfee4a80e9e0ee6f80c8bcb1/
**ESET 报告:** https://www.welivesecurity.com/en/eset-research/webworm-new-burrowing-techniques/
## GraphWorm 的工作原理
GraphWorm 使用 **Microsoft OneDrive 作为其 C2 通道** — 所有任务下发和数据外传都通过合法的 Microsoft 365 流量进行。传统的基于网络的检测实际上处于致盲状态。
```
Operator Microsoft Graph API Victim
│ │ │
│──── writes task to ─────────►│◄──── polls /job/ ──────────│
│ /job/task_xxx.txt │ │
│ │ │
│◄─── reads result from ───────│◄──── uploads to ───────────│
│ /result/yyyymmdd.txt │ /result/ │
```
**OneDrive 文件夹结构(由操作者控制):**
```
/
├── job/ ← operator writes encrypted task files
├── result/ ← agent uploads encrypted results
├── files/ ← file transfer staging
├── alive.txt ← heartbeat timestamp (written each interval)
└── info.txt ← victim system fingerprint (AES-256-CBC encrypted)
```
**命令集:** `shell`, `exec`, `upload`, `download`, `poll`, `heartbeat`, `rest`, `kill`, `upgrade`, `keyExchange`, `sessionKey`
## 逆向工程的关键发现
此检测包基于对 debug 版本的完整静态 RE(FLOSS + Ghidra)。这些是 ESET 报告中未提及的新发现:
**1. 凭据轮换能够在 token 撤销后存活。**
`executeUpgradeCommand` 处理程序允许操作者在单次操作中远程推送新的 `client_id`、`client_secret`、`tenant_id` 和 `refresh_token`。**仅撤销 token 并不能消灭此植入物。** 必须暂停应用注册。
**2. 双层加密。**
初始通信使用带有预共享部署密钥的 AES-256-CBC。RSA 密钥交换(`keyExchange`/`sessionKey` 命令)将其升级为基于会话的密钥 — 这是一种前向保密路径。
**3. 硬件 UUID 指纹识别。**
受害者通过 `MD5(MAC_address + CPU_serial_WMI + HDD_serial_WMI)` 进行识别 — 即使主机名/IP 发生变化也不会失效。
**4. OPSEC 失败 — 发布了 debug 版本。**
该样本是一个 `DebugStatic` 版本,保留了完整的 PDB 路径(`F:\1-Codefield\VS2019\C2OverOneDrive_v0316\DebugStatic\C2OverOneDrive_v0316.pdb`)、源文件名以及详细的 debug 日志记录。
## 操作者基础设施
| 遗留物 | 值 |
|---|---|
| Azure `client_id` | `675b5280-b233-4368-ba9e-b4c55cbeebe9` |
| Azure `tenant_id` | `e2aa8d24-85a2-41e6-b993-572b35980557` |
| 租户区域 | 北美 (`NA`) — 商业版 Azure |
| OAuth scope | `offline_access Files.Read Files.ReadWrite` |
## 仓库内容
```
graphworm-webworm-detection/
├── README.md
├── yara/
│ └── graphworm.yar ← Three YARA rules (build-specific, family, memory)
├── kql/
│ └── graphworm_mde_advanced_hunting.kql ← Six MDE Advanced Hunting queries
├── sigma/
│ └── graphworm_sigma.yml ← Three Sigma rules (SIEM-agnostic)
├── iocs/
│ └── iocs.csv ← All IOCs with type, confidence, MITRE mapping
└── attck/
└── attck_mapping.md ← Full ATT&CK mapping with evidence
```
## 检测优先级
| 检测项 | 文件 | 置信度 | 备注 |
|---|---|---|---|
| 操作者应用认证(命中 `client_id`) | KQL Q1, Sigma 规则 2 | **严重** | 零误报 — 该操作者独有 |
| 对 `e2aa8d24` 的跨租户认证 | KQL Q2, Sigma 规则 3 | **高** | 确认存在活跃受害者 |
| OneDrive 信标文件 | KQL Q3, Sigma 规则 1, YARA 规则 2 | **高** | 文件遗留物名称 |
| 云遥测中的操作者 ID | KQL Q4 | **高** | 原始事件数据搜索 |
| OneDrive 上的 libcurl UA | KQL Q5 | **中** | 需要调试 |
| 二进制文件 (YARA 规则 1) | YARA | **严重** | PDB 路径或凭据匹配 — 零误报 |
| 二进制文件家族 (YARA 规则 2) | YARA | **高** | 行为字符串 — 捕获变体 |
| 内存凭据搜寻 (YARA 规则 3) | YARA | **高** | 用于内存转储 / 字符串输出 |
## MSRC 披露
此二进制文件中硬编码的 Azure 凭据是存活的。如果您遇到此样本,请报告给 Microsoft Security Response Center:
**邮箱:** `secure@microsoft.com`
**请求:** 暂停应用注册 `675b5280-b233-4368-ba9e-b4c55cbeebe9` 并撤销颁发给租户 `e2aa8d24-85a2-41e6-b993-572b35980557` 的所有 token。
**重要提示:** 请求暂停应用,而不仅仅是撤销 token。`executeUpgradeCommand` C2 处理程序允许操作者远程轮换凭据 — 仅撤销 token 是不够的。
## 目标国家(根据 ESET / MISP)
比利时、捷克共和国、匈牙利、意大利、尼日利亚、波兰、塞尔维亚、南非、西班牙
## ATT&CK 覆盖范围
`T1078.004` `T1550.001` `T1059.003` `T1047` `T1547.001` `T1547.009` `T1070.004` `T1112` `T1027` `T1027.013` `T1573.001` `T1573.002` `T1071.001` `T1102.002` `T1567.002` `T1074.001` `T1074.002` `T1041` `T1082` `T1033` `T1016` `T1090` `T1005` `T1083` `T1012` `T1568`
包含证据的完整映射请见 [`attck/attck_mapping.md`](attck/attck_mapping.md)。
## 逆向工程
### 环境
| 项目 | 详情 |
|---|---|
| 平台 | FLARE-VM, Windows 11 (Build 26200), VirtualBox, 仅主机网络 |
| 主要 RE 工具 | Ghidra 11.x, FLOSS v3.1.1, DIE v3.10, pefile |
| 网络 | 物理隔离 — 未进行沙箱测试,无活跃 C2 连接 |
| 方法 | 仅静态分析 — FLOSS → Ghidra,所有发现均已独立确认 |
此样本没有可用的沙箱或 PCAP 数据。所有分析均通过对磁盘上二进制文件的静态方式进行。
### 阶段 1 — 初始分类 (DIE + pefile)
**DIE 输出** 立即确定了工具路径:
| 字段 | 值 | 意义 |
|---|---|---|
| 文件类型 | PE32, I386, 控制台 | 原生 x86 — 适用 Ghidra/IDA,而非 dnSpyEx |
| 编译器 | MSVC 19.29 (VS2019 v16.11) | MSVC C++ 调用约定 |
| 库 | OpenSSL, Curl | 在反汇编前即确定了加密 + HTTP 引擎 |
| 调试数据 | PDB 文件链接 (CodeView 7.0) | **嵌入了完整的 PDB 路径** |
| 子系统 | 控制台 | 负责隐藏窗口的父进程 |
**通过 pefile 提取的 PDB 路径:**
```
F:\1-Codefield\VS2019\C2OverOneDrive_v0316\DebugStatic\C2OverOneDrive_v0316.pdb
```
此单一遗留物指明了开发者的项目名称、构建配置(`DebugStatic`)、驱动器号和版本字符串。将 debug 版本发布给受害者是操作者的 OPSEC 失误。
### 阶段 2 — FLOSS 静态字符串提取
针对二进制文件运行 FLOSS v3.1.1,结果如下:
| 类别 | 数量 |
|---|---|
| 静态字符串 (ASCII) | 8,600 |
| 静态字符串 (UTF-16LE) | 1,651 |
| 栈字符串 | 16 |
| Tight strings | 0 |
| 解码字符串 | 13 |
| **总计** | **10,251** |
**决定了调查方向的五个字符串:**
```
offline_access Files.Read Files.ReadWrite ← OAuth scope
https://graph.microsoft.com/v1.0 ← C2 base URL (hardcoded)
675b5280-b233-4368-ba9e-b4c55cbeebe9 ← operator client_id
e2aa8d24-85a2-41e6-b993-572b35980557 ← operator tenant_id
REDACTED — extract from binary via FLOSS ← operator client_secret
```
完整的 OAuth refresh token(>1,300 个字符)也被以明文形式提取出来。
**源模块映射**(来自二进制文件中的调试字符串):
| 源文件 | 确认的关键能力 |
|---|---|
| `Beacon.cpp` | C2 信标循环、轮询线程、心跳线程、任务分发 |
| `OneDriveGraphAPI.cpp` | 所有 Graph API 调用 — 认证、文件夹操作、文件上传/下载 |
| `Crypto.cpp` | RSA 密钥对生成、AES-256-CBC 加密/解密、RAND_bytes IV |
| `AutoStart.cpp` | 注册表 Run Key + LNK 快捷方式持久化(两种方法) |
| `BaseInfo.cpp` | 系统指纹 — OS、主机名、用户名、MAC、WMI 硬件 ID |
| `HttpProxy.cpp` | libcurl 代理支持 — 服务器/用户名/密码可远程配置 |
| `main.cpp` | 入口点 — 代理设置、自启动、信标初始化/启动 |
从任务分发字符串中提取的**完整命令集**:
`shell` `exec` `upload` `download` `poll` `heartbeat` `rest` `sleep` `kill` `upgrade` `keyExchange` `sessionKey`
### 阶段 3 — Ghidra 分析
Ghidra 被用于独立确认所有主要的 FLOSS 发现,并提取单纯依靠字符串无法展现的数据。
#### Beacon::initialize (`FUN_00469260`)
通过字符串 `"Initializing Beacon..."` 的 XREF 确认 → 唯一调用者。
关键发现:
- 在创建 API 对象之前调用 `thunk_FUN_0044be90()`(OAuth scope 结构构造函数)
- 为 `OneDriveGraphAPI` 对象分配 `0x148` (328) 字节
- 将 API 对象指针存储在 `Beacon+0xA0` 处
- 返回的成功/失败状态映射到 `"API session restored successfully"` / `"Failed to restore session from refresh token"` — 与 FLOSS 字符串完全匹配
**确认 Beacon 对象凭据布局:**
| Beacon 偏移量 | 凭据字段 |
|---|---|
| `+0x1C` | `client_id` |
| `+0x38` | `client_secret` |
| `+0x54` | `tenant_id` |
| `+0x70` | `refresh_token` |
| `+0xA0` | `OneDriveGraphAPI*` 指针 |
#### OneDriveGraphAPI 构造函数 (`FUN_004881b0`)
确认是 `__thiscall`。五次 `thunk_FUN_00416720` 调用(std::string 拷贝构造函数)从 Beacon 对象的 `+0x1C`、`+0x38`、`+0x54`、`+0x70` 偏移处拉取凭据 — **与 FLOSS 推导出的偏移量完全匹配**。
```
// Simplified decompiler output — Graph API base URL hardcoded at compile time
builtin_strncpy(pcVar6, "https://graph.microsoft.com/v1.0", 0x21);
puVar4[0x4b] = pcVar6; // stored at OneDriveGraphAPI+0x12C
// libcurl initialized once globally
if (bVar9) {
curl_global_init(3); // CURL_GLOBAL_ALL
DAT_0061537e = '\x01';
}
```
#### Crypto::encrypt (`FUN_004819e0`)
通过符号表 → `EVP_EncryptInit_ex` → `FUN_004819e0:004851d2` 处的唯一 XREF 定位。
**AES 密钥未硬编码** — 它通过密钥对象(`*(param_3 + )`)传入:
```
// Simplified decompiler output
iVar3 = RAND_bytes(puStack_24, 16); // random IV per message
uVar4 = EVP_aes_256_cbc();
iVar3 = EVP_EncryptInit_ex(local_18, // context
uVar4, // AES-256-CBC
0,
*(param_3 + 4), // key from key object (not inline)
puStack_24); // IV
```
这证实了**托管密钥对象架构** — 部署密钥不是静态字节数组,而是运行时结构体,允许操作者通过 `keyExchange`/`sessionKey` 命令对安装基于会话的 RSA 包装密钥。
#### executeUpgradeCommand (`FUN_00461bc0`)
最具实战意义的函数。从调试字符串 `local_3f0 = "executeUpgradeCommand"` 确认。
关键序列:
1. 验证 JSON 参数:`redirect_uri` → `config_data`
2. 提取新的代理配置:`server` / `username` / `password`
3. **销毁所有 5 个现有凭据字符串**(五次 `thunk_FUN_0044eaf0` 调用)
4. 通过 `thunk_FUN_00416720` 复制传入 5 个新凭据
5. 使用新凭据重新初始化 OAuth scope 结构体
6. 测试与新 OneDrive 账户的连接(`thunk_FUN_00495b80`)
7. 成功时:写入 `config.dat`,替换活跃 API 实例
**这就是为什么仅撤销 token 是无效的原因。** 操作者可以在撤销之前或之后,通过现有的 C2 通道分发新的凭据。
### 双重来源确认
在纳入此检测包之前,每一项主要发现都通过了两个独立分析层的验证:
| 发现 | FLOSS | Ghidra |
|---|---|---|
| 凭据集(5 个字段) | ✅ 明文字符串 | ✅ 对象偏移量 +0x1C/0x38/0x54/0x70 |
| C2 文件夹结构 `/job` `/result` | ✅ 字符串提取 | ✅ 列表中的原始字节 `2f 6a 6f 62` |
| AES-256-CBC 加密方案 | ✅ `EVP_aes_256_cbc` 导入 | ✅ `Crypto::encrypt` 中的 `EVP_EncryptInit_ex` 调用 |
| 随机的每条消息 IV | ✅ `RAND_bytes` 导入 | ✅ 反编译器中的 `RAND_bytes(puStack_24, 16)` |
| libcurl 作为唯一的 HTTP 引擎 | ✅ 导入表 | ✅ 构造函数中的 `curl_global_init(3)` |
| 硬编码的 Graph API URL | ✅ 字符串 | ✅ 构造函数中的 `builtin_strncpy` |
| 通过 `upgrade` 轮换凭据 | ✅ 命令字符串 | ✅ `executeUpgradeCommand` 中的完整函数流 |
| OAuth scope `offline_access` | ✅ 字符串 | ✅ 存储在构造函数中 + 在 `Beacon::initialize` 中被引用 |
| 注册表 + LNK 持久化 | ✅ `AutoStart.cpp` 字符串 | ✅ `SOFTWARE\Microsoft\Windows\CurrentVersion\Run` |
| WMI 硬件 UUID | ✅ WMI 查询字符串 | ✅ VT 沙箱中确认的 T1047 |
*另请参阅:[github.com/yankywilson](https://github.com/yankywilson) 了解相关的 CTI 工作。*
标签:APT检测, DAST, KQL, Sigma规则, YARA, 云资产可视化, 威胁情报, 安全, 开发者工具, 恶意软件分析, 目标导入, 网络信息收集, 超时处理