TopSoftdeveloper/Hook-Chrome-Chromium-SSL

GitHub: TopSoftdeveloper/Hook-Chrome-Chromium-SSL

通过在 Windows 环境下逆向定位并 Hook Chrome 的 BoringSSL 内部函数,实现对浏览器 SSL_read 和 SSL_write 的拦截以获取明文数据。

Stars: 68 | Forks: 4

# Hook-Chrome-Chromium-SSL — Chrome/Chromium SSL 函数 Hook C++ 项目,通过定位并 Hook 内部的 SSL 例程,拦截 Windows/Chromium 环境下 Chrome 的 SSL_read/SSL_write 函数。 # Hook Chrome 的 SSL 函数 目的是捕获加密或解密数据并通过网络发送的函数。对于 Firefox 这样的应用程序,这很容易实现,只需找到两个 DLL 导出函数:`PR_Read` 和 `PR_Write` 即可。但对于 Google Chrome 来说,这要困难得多,因为 `SSL_Read` 和 `SSL_Write` 函数并没有被导出。 对于想要拦截此类调用的人来说,主要问题是我们很难在庞大的 `chrome.dll` 文件中找到这些函数。因此,我们必须在二进制文件中手动查找。但我们该怎么做呢? ## Chrome 的源代码 为了实现我们的目标,Chrome 的源代码可能是最好的起点。我们可以在[这里](https://cs.chromium.org/)找到它。它让我们能够轻松地搜索和浏览源代码。 我们可能一开始就应该注意到,Google Chrome 使用的是 **boringssl**,这是 OpenSSL 的一个分支。该项目的源代码可以在 Chromium 的源代码中找到,位于[这里](https://boringssl.googlesource.com/)。 现在,我们需要找到我们需要的函数:`SSL_read` 和 `SSL_write`,我们可以很容易地在 `ssl_lib.cc` 文件中找到它们。 ### SSL_read ``` int SSL_read(SSL *ssl, void *buf, int num) { int ret = SSL_peek(ssl, buf, num); if (ret <= 0) { return ret; } ssl->s3->pending_app_data = ssl->s3->pending_app_data.subspan(static_cast(ret)); if (ssl->s3->pending_app_data.empty()) { ssl->s3->read_buffer.DiscardConsumed(); } return ret; } ``` ### SSL_write ``` int SSL_write(SSL *ssl, const void *buf, int num) { ssl_reset_error_state(ssl); if (ssl->do_handshake == NULL) { OPENSSL_PUT_ERROR(SSL, SSL_R_UNINITIALIZED); return -1; } if (ssl->s3->write_shutdown != ssl_shutdown_none) { OPENSSL_PUT_ERROR(SSL, SSL_R_PROTOCOL_IS_SHUTDOWN); return -1; } int ret = 0; bool needs_handshake = false; do { if (!ssl_can_write(ssl)) { ret = SSL_do_handshake(ssl); if (ret < 0) { return ret; } if (ret == 0) { OPENSSL_PUT_ERROR(SSL, SSL_R_SSL_HANDSHAKE_FAILURE); return -1; } } ret = ssl->method->write_app_data(ssl, &needs_handshake, (const uint8_t *)buf, num); } while (needs_handshake); return ret; } ``` 为什么我们要看这段代码?原因很简单:在二进制文件中,我们可能会找到在源代码中同样存在的东西,比如字符串或特定的值。 实际上,我不久前(大概就是在这里)发现了我在此要介绍的基本思路,但我会涵盖所有方面,以确保任何人都能找到这些函数,这不仅适用于 Chrome,也适用于 Putty 或 WinSCP 等其他工具。 ### SSL_write 函数 即使 SSL_read 函数没有提供有用的信息,我们可以从 SSL_write 开始,我们会看到一些看起来有用的东西: ``` OPENSSL_PUT_ERROR(SSL, SSL_R_UNINITIALIZED); ``` 这里是 OPENSSL_PUT_ERROR 宏: ``` #define OPENSSL_PUT_ERROR(library, reason) \ ERR_put_error(ERR_LIB_##library, 0, reason, __FILE__, __LINE__) ``` 有些信息非常有用: - `ERR_put_error` 是一个函数调用 - `reason` 是第二个参数,在我们的例子中,`SSL_R_UNINITIALIZED` 的值为 226 (0xE2) - `__FILE__` 是实际的文件名,即 `ssl_lib.cc` 的完整路径 - `__LINE__` 是 `ssl_lib.cc` 文件中当前的行号 所有这些信息都能帮助我们找到 `SSL_write` 函数。为什么?我们知道这是一个函数调用,所以参数(比如 `reason`、`__FILE__` 和 `__LINE__`)将会被压入栈中(x86 架构下)。我们知道 reason (0xE2)、`__FILE__` (`ssl_lib.cc`) 以及 `__LINE__`(在此版本中是 1060 或 0x424)。 但是,如果使用了不同的版本怎么办?行号可能会完全不同。好吧,在这种情况下,我们必须看看 Google Chrome 是如何使用 BoringSSL 的。 我们可以在[这里](https://chromium.googlesource.com/chromium/src/)找到 Chrome 的特定版本。例如,现在我的 x86 环境上是这个版本:**Version 65.0.3325.181 (Official Build) (32-bit)**。我们可以在[这里](https://chromium.googlesource.com/chromium/src/)找到它的源代码。 现在,我们需要找到 BoringSSL 的代码,但看起来它并不在那里。不过,我们发现 `DEPS` 文件非常有用,可以提取一些信息: ``` vars = { ... 'boringssl_git': 'https://boringssl.googlesource.com', 'boringssl_revision': '94cd196a80252c98e329e979870f2a462cc4f402', } ``` 我们可以看到我们的 Chrome 版本使用 `https://boringssl.googlesource.com` 来获取 BoringSSL,并且使用的是这个版本:`94cd196a80252c98e329e979870f2a462cc4f402`。基于此,我们可以在[这里](https://boringssl.googlesource.com/)获取 BoringSSL 的准确代码。 ### 查找 SSL_write 函数地址的步骤 1. 在 `chrome.dll` 的只读数据段 (.rdata) 中搜索 `ssl_lib.cc` 文件名 2. 获取完整路径并搜索引用 3. 检查对该字符串的所有引用,并根据 `reason` 和行号参数找到正确的那一个 ### SSL_read 函数 找到 `SSL_write` 函数并不难,因为那里有一个 `OPENSSL_PUT_ERROR`,但是在 `SSL_read` 中并没有。让我们看看 `SSL_read` 是如何工作的,并顺藤摸瓜。 我们可以很容易地看到它调用了 `SSL_peek`: ``` int ret = SSL_peek(ssl, buf, num); ``` 我们可以看到 `SSL_peek` 将调用 `ssl_read_impl` 函数: ``` int SSL_peek(SSL *ssl, void *buf, int num) { int ret = ssl_read_impl(ssl); if (ret <= 0) { return ret; } ... } ``` 而 `ssl_read_impl` 函数似乎在帮我们的忙: ``` static int ssl_read_impl(SSL *ssl) { ssl_reset_error_state(ssl); if (ssl->do_handshake == NULL) { OPENSSL_PUT_ERROR(SSL, SSL_R_UNINITIALIZED); return -1; } } ... ``` 我们可以在代码中搜索并发现 `ssl_read_impl` 函数仅仅被调用了两次,分别是被 `SSL_peek` 和 `SSL_shutdown` 函数调用,因此找到 `SSL_peek` 应该相当容易。在我们找到 `SSL_peek` 之后,找到 `SSL_read` 就水到渠成了。 ### 32 位 Chrome 既然我们已经掌握了如何找到这些函数的总体思路,那就让我们来实际寻找它们吧。我将使用 `x64dbg`,但你大概也可以使用任何其他调试器。我们必须转到 “Memory” 选项卡并找到 `chrome.dll`。我们首先需要做两件事: 1. 在反汇编窗口中打开代码段,因此在 “.text” 上单击右键并选择 “Follow in Disassembler” 2. 在内存转储窗口中打开只读数据段,因此在 “.rdata” 上单击右键并选择 “Follow in Dump” 我们现在需要在内存转储窗口中找到 `ssl_lib.cc` 字符串,所以在窗口内单击右键,选择 “Find Pattern” 并搜索我们的 ASCII 字符串。你应该只会得到一个结果,双击它并往上翻页,直到找到 `ssl_lib.cc` 文件的完整路径。右键点击完整路径的第一个字节,选择 “Find References”,看看我们在哪里能找到它的使用(即 OPENSSL_PUT_ERROR 函数调用)。 看起来我们有多个引用,但我们可以逐一检查并找到正确的那一个。这是结果。 例如,让我们转到最后一个引用,看看它长什么样。 ``` 6D44325C | 68 AD 03 00 00 | push 3AD | 6D443261 | 68 24 24 E9 6D | push chrome.6DE92424 | 6DE92424:"../../third_party/boringssl/src/ssl/ssl_lib.cc" 6D443266 | 6A 44 | push 44 | 6D443268 | 6A 00 | push 0 | 6D44326A | 6A 10 | push 10 | 6D44326C | E8 27 A7 00 FF | call chrome.6C44D998 | 6D443271 | 83 C4 14 | add esp,14 | ``` 它看起来正如我们所期望的那样,是一个带有五个参数的函数调用。你可能知道,参数是从右到左依次压入栈中的,我们有以下内容: - `push 3AD` – 行号 - `push chrome.6DE92424` – 我们的字符串,即文件路径 - `push 44` – reason - `push 0` – 始终为 0 的参数 - `push 10` – 第一个参数 - `call chrome.6C44D998` – 调用 `ERR_put_error` 函数 - `add esp,14` – 清理栈 然而,`0x3AD` 代表第 941 行,它位于 `ssl_do_post_handshake` 内部,所以这不是我们需要的。 ### SSL_write `SSL_write` 在第 1056 (0x420) 和 1061 (0x425) 行调用了这个函数,所以我们需要找到以 `push 420` 或 `push 425` 开头的函数调用。浏览这些结果只需几秒钟就能找到它: ``` 6BBA52D0 | 68 25 04 00 00 | push 425 | 6BBA52D5 | 68 24 24 E9 6D | push chrome.6DE92424 | 6DE92424:"../../third_party/boringssl/src/ssl/ssl_lib.cc" 6BBA52DA | 68 C2 00 00 00 | push C2 | 6BBA52DF | EB 0F | jmp chrome.6BBA52F0 | 6BBA52E1 | 68 20 04 00 00 | push 420 | 6BBA52E6 | 68 24 24 E9 6D | push chrome.6DE92424 | 6DE92424:"../../third_party/boringssl/src/ssl/ssl_lib.cc" 6BBA52EB | 68 E2 00 00 00 | push E2 | 6BBA52F0 | 6A 00 | push 0 | 6BBA52F2 | 6A 10 | push 10 | 6BBA52F4 | E8 9F 86 8A 00 | call chrome.6C44D998 | ``` 我们可以在这里看到这两处函数调用,不过需要注意第一处被优化了。现在,我们只需往上回溯,直到找到看起来像函数起点的代码。虽然这对于其他函数可能并不总是适用,但在我们的情况下它应该可行,而且我们可以通过经典的函数序言轻松找到它: ``` 6BBA5291 | 55 | push ebp | 6BBA5292 | 89 E5 | mov ebp,esp | 6BBA5294 | 53 | push ebx | 6BBA5295 | 57 | push edi | 6BBA5296 | 56 | push esi | ``` 让我们在 `6BBA5291` 处设置一个断点,看看当我们使用 Chrome 浏览一些 HTTPS 网站时会发生什么(为了避免问题,请浏览不支持 SPDY 或 HTTP/2.0 的网站)。 以下是断点触发时,我们在栈顶可能获取到的内容示例: ``` 06DEF274 6A0651E8 return to chrome.6A0651E8 from chrome.6A065291 06DEF278 0D48C9C0 ; First parameter of SSL_write (pointer to SSL) 06DEF27C 0B3C61F8 ; Second parameter, the payload 06DEF280 0000051C ; Third parameter, payload size ``` 如果你右键点击第二个参数并选择 “Follow DWORD in Dump”,你应该能看到明文数据,例如: ``` 0B3C61F8 50 4F 53 54 20 2F 61 68 2F 61 6A 61 78 2F 72 65 POST /ah/ajax/re 0B3C6208 63 6F 72 64 2D 69 6D 70 72 65 73 73 69 6F 6E 73 cord-impressions 0B3C6218 3F 63 34 69 3D 65 50 6D 5F 66 48 70 72 78 64 48 ?c4i=ePm_fHprxdH ``` ### SSL_read 现在让我们找到 `SSL_read` 函数。我们应该在 `ssl_read_impl` 函数中找到对 `OPENSSL_PUT_ERROR` 的调用。这个调用位于第 962 (0x3C2) 行。让我们再次检查这些结果并找到它。在这里: ``` 6B902FAC | 68 C2 03 00 00 | push 3C2 | 6B902FB1 | 68 24 24 35 6C | push chrome.6C352424 | 6C352424:"../../third_party/boringssl/src/ssl/ssl_lib.cc" 6B902FB6 | 68 E2 00 00 00 | push E2 | 6B902FBB | 6A 00 | push 0 | 6B902FBD | 6A 10 | push 10 | 6B902FBF | E8 D4 A9 00 FF | call chrome.6A90D998 | ``` 现在,我们应该找到函数的开头,这应该很简单。右键点击第一条指令 (`push EBP`),转到 “Find references to” 并选择 “Selected Address(es)”。我们应该只能找到一处对该函数的调用,这应该就是 `SSL_peek`。找到 `SSL_peek` 的第一条指令并重复相同的步骤。我们应该只有一个结果,也就是从 `SSL_read` 对 `SSL_peek` 的调用。这样我们就找到它了。 ``` 6A065F52 | 55 | push ebp | ; SSL_read function 6A065F53 | 89 E5 | mov ebp,esp | ... 6A065F60 | 57 | push edi | 6A065F61 | E8 35 00 00 00 | call chrome.6A065F9B | ; Call SSL_peek ``` 让我们设置一个断点,在正常的调用中我们可以看到以下内容: ``` 06DEF338 6A065D8F return to chrome.6A065D8F from chrome.6A065F52 06DEF33C 0AF39EA0 ; First parameter of SSL_read, pointer to SSL 06DEF340 0D4D5880 ; Second parameter, the payload 06DEF344 00001000 ; Third parameter, payload length ``` 现在,我们应该右键点击第二个参数并选择 “Follow DWORD in Dump”,然后再按下 “Execute til return” 按钮,以便让调试器在函数结束时停下(即数据被读取到缓冲区之后)。这样,我们应该能在内存转储窗口中看到明文数据,也就是我们选中的 payload。 ``` 0D4D5880 48 54 54 50 2F 31 2E 31 20 32 30 30 20 4F 4B 0D HTTP/1.1 200 OK. 0D4D5890 0A 43 6F 6E 74 65 6E 74 2D 54 79 70 65 3A 20 69 .Content-Type: i 0D4D58A0 6D 61 67 65 2F 67 69 66 0D 0A 54 72 61 6E 73 66 mage/gif..Transf ``` 我们成功地找到了它。 ## 结论 一开始可能看起来很困难,但正如你所见,如果我们对照源代码在二进制文件中查找,这其实相当容易。这种方法应该适用于大多数开源应用程序。 由于 x64 版本会非常相似,唯一的区别在于汇编代码,因此这里就不做详细说明了。 但是,请注意,Hook 这些函数可能会导致程序行为不稳定甚至崩溃。 ## 📬 联系方式 有任何问题或想要做出贡献吗? - **Telegram**: [@somerwork](https://t.me/somerwork) - **捐赠(BTC)**: bc1q43u0n865fuxc4j2vgm4wp98xuuaawgkgq8yrf4
标签:Aarch64, C++, 云资产清单, 函数Hook, 数据抓包, 数据擦除, 端点可见性, 网络流量分析, 逆向工程, 防御绕过