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, 数据抓包, 数据擦除, 端点可见性, 网络流量分析, 逆向工程, 防御绕过