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, 云资产可视化, 威胁情报, 安全, 开发者工具, 恶意软件分析, 目标导入, 网络信息收集, 超时处理