SwiftSecur/BrowserShatter

GitHub: SwiftSecur/BrowserShatter

一个用户态漏洞利用概念验证工具,通过 Windows 消息传递机制和系统 DLL 代码复用 gadget 实现跨进程任意内存读写,并附带 Chrome 凭据提取演示。

Stars: 0 | Forks: 0

# BrowserShatter 此代码改编自 https://github.com/waryas/WaryasSWHE,原始 PoC 的所有功劳归 Waryas 所有,该 PoC 是此工具的基础。 本工具仅供教育目的。它作为原始 PoC 的改编/用例,供他人学习和研究。下面我附上了**我**个人的粗浅理解,我鼓励任何希望深入了解此类技术的人也这样做,并得出你们自己的结论…… 主要的 cpp 文件也是基于这些注释由 Claude 添加了注释;这同样是拆解代码、理解结构以及各个部分功能的好方法。 最后——我还在代码库中放了几张图表,试图解释每个阶段发生了什么。 ## 这是什么? 这是一个概念验证(PoC)级别的用户态漏洞利用程序,它实现了**对另一个进程的任意读写**,而无需调用 `WriteProcessMemory` 或 `ReadProcessMemory`。该技术滥用合法的 Windows 消息传递机制以及受信任系统 DLL 中的各种代码复用 gadget。 此代码库包含一个工具,它使用此代码的改编版本来针对用户态进程,在目标进程的内存中搜索 artifacts。 它以两种模式运行:一种模式开箱即用地针对 Chrome 浏览器,并尝试发现预定义的密钥(密码、密钥、会话等)模式。另一种模式是调查模式(可通过交互式菜单使用),允许通过名称或窗口针对**任何**进程进行内存转储和字符串搜索。 ## 它的工作原理(我个人的理解) 以下是我对原始 PoC 工作原理的理解: ### 阶段 1. 共享内存通道 Windows 维护着一个名为 `windows_shell_global_counters`(由 shell 子系统管理)的命名共享内存区,它几乎被映射到每一个 GUI 进程中。由于它在攻击者进程和目标进程中都由相同的物理页支持,因此在一个进程中写入的任何内容都会立即在另一个进程中可见。这是一种规避极其嘈杂的 `WriteProcessMemory` 调用的绝佳方式。 该漏洞利用程序首先在本地映射此内存区,然后扫描目标进程的虚拟地址空间,以找到同一物理页在目标进程中的位置。 为此,它使用了一种形式的“哨兵”探测:在目标线程上安装一个指向 `ntdll!RtlGetIntegerAtom` 的 `WH_SHELL` hook,并向目标窗口发送 `WM_APPCOMMAND`。当 hook 在目标进程内部触发时,`RtlGetIntegerAtom` 会将 hook 代码 (12) 作为 `USHORT` 写入作为 `wParam` 传递的任何地址。如果在共享页面的本地端能看到该写入操作,我们就确认了正确的映射。 ### 阶段 2. WH_SHELL Hook + WM_APPCOMMAND 分发 所有实际的漏洞利用操作都遵循相同的模式: 1. 将一个**分发结构体**写入共享内存偏移量 `0x800` 处。此结构体包含: - 一个 **`memcpy` 字段** - 实际要调用的函数(例如 `RtlCopyString`) - 一个 **`function_ptr` 字段** - 设置为 shell32 的 trampoline gadget (`t`),`w` 使用它来进行分发 - 其**参数**(`arg1`, `arg2`) - 一对 **UNICODE_STRING**(`dst`/`src`),在函数为 `RtlCopyString` 时使用 - 一个**数据槽**(`val`),用于传递或接收值 2. 在目标线程上安装 `WH_SHELL` hook。hook 回调被设置为 **`w`**,这是 `ntdll.dll` 内部的一个 gadget,它从共享页读取分发结构体并调用 `function_ptr`(shell32 trampoline `t`)。然后 trampoline `t` 调用 `memcpy` 字段,即实际的目标函数(例如 `RtlCopyString`)。 3. 发送 `SendMessageA(hwnd, WM_APPCOMMAND, remote_shared_mem + 0x800, ...)`。该消息在**目标进程自己的线程上下文中**分发,从而触发 hook。没有注入任何代码;仅使用了受信任的系统 DLL 中现有的指令。 4. 卸载 hook。 由于函数指针和参数位于共享内存中,攻击者可以完全控制目标中执行的内容。 ### 阶段 3. 读/写原语 **读取**(`ReadU64` / `ReadData`): 将 `write_fn` = `ntdll!RtlCopyString` 设置为要调用的函数。`UNICODE_STRING.Buffer` 的 src 指针被设置为**要读取的目标地址**,而 dst 被设置为指回共享页(`val` 槽)。当 hook 在目标进程中触发时,`RtlCopyString` 将字节从目标地址复制到共享页中,攻击者随后在本地读取这些字节。 **写入**(`WriteU64` / `WriteData`): 相同的 gadget,但反向操作。攻击者将所需的字节写入共享页的 `val` 槽,将 src 设置为指向该槽,并将 dst 设置为**要写入的目标地址**。当 hook 触发时,`RtlCopyString` 从共享页复制到目标地址。大于 `0x50` 字节的写入会自动进行分块处理。 ## 使用的 Gadget | 符号 | 来源 DLL | 用途 | |---|---|---| | `w` | `ntdll.dll` | hook 回调,读取分发结构体并通过 `t` 调用 | | `t` | `shell32.dll` | trampoline:调用来自分发结构体的函数指针 | | `write_fn` (`RtlCopyString`) | `ntdll.dll` | 在任意地址之间复制字节(读/写原语) | ## 凭据扫描(浏览器版) `--scan`(JWT/token/cookie 扫描)直接使用 `ReadProcessMemory`。`--scan-passwords` 使用基于 hook 的 `ReadData` 原语。这意味着……攻击者进程不会发起任何杂乱/高调的 `ReadProcessMemory` 调用。`--dump` 也使用基于 hook 的原语。Chrome 的异常处理程序永远不会被调用。 ### `--scan` 遍历所有已提交、可读、非镜像的内存区。对于每个匹配项,将打印 `Value`、`Origin` 以及(对于 JWT)解码后的 `JWT` 属性信息行,如下所示: ``` [+] JWT (Authorization header) VA=0x00007f... Value : eyJhbGciOiJSUzI1NiIsInR5... Origin : accounts.google.com/o/oauth2/token JWT : iss=https://accounts.google.com sub=110... email=user@gmail.com ``` **匹配的模式:** | 类别 | 说明 | |---|---| | JWT / Bearer token | `eyJ` 前缀;最小标头长度≥20,负载≥50,签名≥20 个字符 | | OAuth `id_token` | 相同的 JWT 验证 | | Session cookies | 特定名称(PHPSESSID, JSESSIONID, ASP.NET\_SessionId, connect.sid …)16+ 个字符;通用的 `session=` 需要 32+ 个字符 | | API key | 常见的 JSON 字段名 + `=` / `:` 分隔符 | | AWS access key | `AKIA…` 前缀 | | Google OAuth token | `ya29.` 前缀 | 来源归属信息会在周围内存中搜索 ±1 KB 范围内的 `"origin"`、`"host"`、`"iss"`、`"url"` JSON 字段或纯粹的 `https://` URL。JWT 负载会进行 base64url 解码,以提取 `iss`、`aud`、`sub`、`email` 和 `name` 字段。 ### Chromium 密码管理器 (第 2 部分) #### “漏洞”(某种程度上) Chrome 使用 DPAPI 和 AES-256-GCM 对静态保存的凭据进行加密。密文存储在 `Login Data` SQLite 数据库中;AES 主密钥以 DPAPI 加密的形式存储在 `Local State` 中。 **加密架构(已针对 [`components/os_crypt/sync/os_crypt_win.cc`](https://source.chromium.org/chromium/chromium/src/+/main:components/os_crypt/sync/os_crypt_win.cc) 进行验证):** - AES-256 主密钥存储在 `Local State` 中的 `os_crypt.encrypted_key` 下,作为一个以 `DPAPI` 为前缀的 base64 字符串,然后通过 `CryptProtectData` 进行 DPAPI 加密。 - 单个密码数据块以 `v10` 为前缀,后跟一个 12 字节的 nonce,然后是 AES-256-GCM 密文。解密调用 `crypto::Aead::Open()` —— Chrome 的 **BoringSSL** 包装器 —— 而不是 Windows BCrypt 或任何系统 DLL。 - 没有 `v10` 前缀的旧数据块完全跳过 BoringSSL,直接通过 `CryptUnprotectData` 处理。 这种区别很重要:**Windows API 监控(`BCryptDecrypt`, `NCryptDecrypt`)无法拦截 Chrome 的密码解密。** BoringSSL 被静态编译到 `chrome.exe` 中。只有单次的主密钥提取调用(`CryptUnprotectData`)触及了 Windows 系统 DLL。 **解密流程:** 1. Chrome 启动 → 读取 `Local State` → [`DecryptStringWithDPAPI()`](https://source.chromium.org/chromium/chromium/src/+/44a2896769d516a2dea9b59c99c24743a23ef87d:components/os_crypt/sync/os_crypt_win.cc;l=62) 调用一次 `CryptUnprotectData`,将 AES 主密钥解密到浏览器进程的堆缓冲区中。由 [`InitWithExistingKey()`](https://source.chromium.org/chromium/chromium/src/+/44a2896769d516a2dea9b59c99c24743a23ef87d:components/os_crypt/sync/os_crypt_win.cc;l=171) 调用。 2. 当密码管理器 UI 打开时 → [`OSCryptImpl::DecryptString()`](https://source.chromium.org/chromium/chromium/src/+/44a2896769d516a2dea9b59c99c24743a23ef87d:components/os_crypt/sync/os_crypt_win.cc;l=124) 检查 `v10` 前缀,使用缓存的密钥初始化 `crypto::Aead aead(crypto::Aead::AES_256_GCM)`,并对每个凭据调用一次 `aead.Open()` —— 在 `chrome.exe` 内部的 BoringSSL 中运行,没有系统 DLL 调用 → 明文 `PasswordForm` 对象出现在浏览器进程的堆中。 3. 这些明文对象会在整个会话期间持续存在 —— 自动填充或重新显示时不会进行进一步的解密调用。 4. 在密码管理器 UI 中点击“显示密码”会通过 BCrypt 触发 Windows Hello / 设备身份验证检查([`chrome/browser/device_reauth/`](https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/device_reauth/)) —— **这不是密码解密**,它是显示已经在内存中的明文之前的重新认证关卡。 **重要含义:** 从首次打开密码管理器 UI 的那一刻起,凭据就以明文形式存在于浏览器进程的堆内存中,并在**整个浏览器会话**期间保留。`--scan-passwords` 之所以有效,是因为它读取的是原始堆内存;它不依赖于任何可观察的系统 API 调用。 *此操作无需事先向密码管理器进行认证即可完成。* 解密后的对象在堆中被序列化为类似 JSON 的字符串,具有清晰标记的字段名(`"username_value"`、`"password_value"`、`"origin_url"`、`"signon_realm"` 等)。这些字段名是 Chromium 内部 IPC 和序列化层的一部分,并且在主要版本中保持稳定。由于数据是明文且结构上可预测,因此可以通过使用一组正则表达式扫描进程的可读堆区域来恢复它。 **为什么会这样?** Chrome 做出了一个蓄意的决定:**操作系统登录即是身份验证**。当你登录 Windows 时,Chrome 认为这足以证明你的身份,从而交出你的密码。这里没有独立的保险库,没有额外的认证因素,也没有会话超时。所有其他主要的密码管理器 —— 1Password、Bitwarden、Dashlane —— 都要求你独立于操作系统登录,向密码管理器本身进行身份验证,然后才会自动填充或暴露凭据。它们从永远不会离开你头脑的主密码派生出保险库密钥;操作系统登录对它们来说无关紧要。 当你在 Chrome 的 UI 中点击“显示密码”时出现的 Windows Hello / PIN 提示是**后来作为修饰性的 UX 层改造的**。它控制着屏幕上已经解密的字符串的*显示*。明文从密码管理器页面打开的那一刻起就一直存在于浏览器进程堆中 —— 重新认证检查从未触及解密过程。任何以你的 Windows 用户身份运行的代码都已经可以访问该内存,无论 Chrome 的 UI 显示什么。 实际后果:任何以相同操作系统用户权限运行的进程 —— 在 Chrome 的渲染器中执行的恶意浏览器扩展、被恶意软件注入的进程,或者像这样的工具 —— 都继承了 Chrome 授予操作系统登录的相同信任级别。DPAPI 并不在乎坐在键盘前的不是合法用户。Chrome 构建了世界上使用最广泛的密码管理器之一,其安全模型从根本上比其第三方竞争对手弱,以 UX 便利性为由为其辩护,然后以“不可行”为由关闭了关于由此产生的暴露的 VRP 报告,因为他们的威胁模型明确排除了同用户本地访问。 这被报告给了 Chrome VRP(issue #532570835)。Google 以**不可行**为由将其关闭,理由是 Chrome 的威胁模型不涵盖已经以相同操作系统用户身份执行本地代码的攻击者。在这种情况下读取另一个进程的内存……据我所知,被认为是超出范围的。 #### 扫描内容 使用基于 hook 的 `ReadData` 原语遍历所有已提交、可读、非镜像、可读写的堆/数据区;即内存转储路径使用的相同 `WH_SHELL` + `WM_APPCOMMAND` gadget 链。攻击者进程没有发起对 `ReadProcessMemory` 或 `NtReadVirtualMemory` 的调用。 仅以 `PROCESS_QUERY_LIMITED_INFORMATION` 权限打开(`VirtualQueryEx` 需要);实际字节数据通过共享内存通道返回。 | 字段 | 模式 | |---|---| | Origin URL | `"origin"` / `"origin_url"` JSON 字段 | | Signon realm | `"signon_realm"` JSON 字段 | | Username | `"username_value"` / `"username_field"` / `"username"` JSON 字段 | | Password | `"password_value"` / `"password_field"` / `"new_password_value"` / `"passwordValue"` JSON 字段 | | Autofill password | 同一对象中的 `"type":"password"` + `"value"` | | POST credentials | 查询字符串格式的 `username=` / `password=` 对 | | HTTP Basic auth | 内存中的 `Authorization: Basic ` 标头 | | Payment card | `"card_number"` / `"number"` JSON 字段(12-25 位数字符串) | 读取分块时有 2 KB 的重叠,因此不会遗漏跨越分块边界的记录。在输出之前,重复的记录(相同的来源 + 用户名 + 密码三元组)会被去重。 ## 规避 这个工具的一大优点是它使用了一种不同的方法来向/从目标进程读取/写入数据,它避免了以下操作: - 攻击者进程调用 `WriteProcessMemory`、`ReadProcessMemory` 或 `NtProtectVirtualMemory`。 - DLL 注入;hook DLL (`uxtheme.dll`) 已经在目标进程中加载。 除此之外: - 在目标进程中执行的所有代码都属于 `ntdll.dll` 或 `shell32.dll`(两者都已签名、受信任且始终存在)。 - 通信通道 (`windows_shell_global_counters`) 是一个正常的系统映射,没有任何可疑特征。 - Hook 的安装和移除发生在每次操作的紧凑时间窗口内。 ## 构建 源代码位于 `browser-edition/` 中。需要 MinGW-w64。在 Ubuntu/Debian 上: ``` sudo apt install mingw-w64 ``` 如有必要,请修复 Windows 头文件的 Linux 大小写敏感性问题(一次性操作): ``` sudo ln -s /usr/x86_64-w64-mingw32/include/windows.h \ /usr/x86_64-w64-mingw32/include/Windows.h ``` 在 `browser-edition/` 内部编译: ``` x86_64-w64-mingw32-g++ -std=c++17 -O2 \ exploit.cpp utils.cpp main.cpp \ -o browser.exe \ -lpsapi -lshlwapi -lshell32 -lwinmm -lntdll \ -static-libgcc -static-libstdc++ ``` `browser.exe` 是一个独立的 Windows x64 二进制文件,没有外部依赖项。 ## 用法 ``` browser.exe [options] ``` ### Chrome PoC | 标志 | 说明 | |---|---| | `--browser chrome` | 查找并附加到 Chrome 的主进程 | | `--scan` | 扫描凭据材料(JWT、Session cookies、API key、OAuth token) | | `--scan-passwords` | 扫描 Chromium 密码管理器记录 | | `--trigger-pwm` | 在扫描之前,使用键盘注入 (`SendInput`) 导航 Chrome 到 `chrome://password-manager`:恢复 Chrome 窗口,使用 Ctrl+L 聚焦地址栏 (Omnibox),以 Unicode 按键事件输入 URL,然后按 Enter 键。适用于所有 Chrome 版本。注意:在输入 URL 时,Chrome 会短暂出现在前台。 | | `--trigger-pwm-wait ` | 触发 UI 后等待多少秒再开始扫描(默认:3) | | `--dump` | 将已提交的非镜像内存转储到磁盘 | | `--dump-heap` | 仅转储堆/数据区(最快) | | `--dump-all` | 还包括 `MEM_IMAGE`(DLL 代码)区 | ### 调查模式(任何进程) | 标志 | 说明 | |---|---| | `--exe ` | 目标可执行文件名 | | `--window-name ` | 通过精确的窗口标题附加 | | `--window-substr <partial>` | 通过不区分大小写的标题子串附加 | | `--window-class <class>` | 通过窗口类名附加 | | `--string-search <term>` | 在进程内存中搜索文字字符串 | | `--string-search-ascii` | 仅 ASCII 编码 | | `--string-search-wide` | 仅 UTF-16LE 编码 | | `--string-search-all` | 还搜索 `MEM_IMAGE` 区 | ### 转储输出 该工具还具有一项粗略的功能,可以将整个进程的副本保存为 .bin 文件。它使用相同的漏洞利用来分块读取内存。出于显而易见的原因,它很慢,但最终会完成。 使用 `--dump` 或 `--dump-all` 时,会将两个文件写入工作目录: | 文件 | 说明 | |---|---| | `memdump_<pid>.bin` | 原始二进制转储。每个区都以一个 8 字节的基地址 (VA) 和 8 字节的区大小作为前缀,然后是原始字节。 | | `memdump_<pid>.map` | 文本文件,列出每个转储的区:基地址、大小、保护标志、类型(`PRIVATE`/`MAPPED`/`IMAGE`)以及在 `.bin` 文件中的字节偏移量。 | **性能说明:** 每次读取都是一个完整的 `SetWindowsHookEx` → `SendMessageA` → `UnhookWindowsHookEx` 往返过程。预计每 100 MB 的已提交内存大约需要 1-3 分钟。 ### 其他 | 标志 | 说明 | |---|---| | `--list-windows` | 打印所有可见的窗口 | | `-h`, `--help` | 打印使用摘要 | ### 示例 ``` # 扫描 Chrome 以获取凭据材料 browser.exe --browser chrome --scan # 扫描 Chrome 以获取已保存的密码 browser.exe --browser chrome --scan-passwords # 先触发 Password Manager UI,然后扫描(不使用 ShellExecuteW) browser.exe --browser chrome --scan-passwords --trigger-pwm # 同上,但在扫描前给 Chrome 5 秒钟完成解密 browser.exe --browser chrome --scan-passwords --trigger-pwm --trigger-pwm-wait 5 # 同时运行两个扫描 browser.exe --browser chrome --scan --scan-passwords # 将 Chrome 的 heap/stack/mapped regions 转储到磁盘 browser.exe --browser chrome --dump # 在任何进程内存中搜索指定词条 browser.exe --exe notepad.exe --string-search "secret" # 交互式菜单 - 我的最爱 :) browser.exe ``` </div><div><strong>标签:</strong>HTTP工具, SSH蜜罐, 云资产清单, 内存读写, 概念验证, 端点可见性, 进程注入, 逆向工程</div></article></div> <!-- 人机验证 --> <script> (function () { var base = (document.querySelector('base') && document.querySelector('base').getAttribute('href')) || ''; var path = base.replace(/\/?$/, '') + '/cap-wasm/cap_wasm.min.js'; window.CAP_CUSTOM_WASM_URL = new URL(path, window.location.href).href; })(); </script> </body> </html>