Hooking Chrome浏览器的SSL函数来读取SSL通信数据
2015年,NetRipper首次在Defcon大会上面世。NetRipper是一款针对Windows操作系统的漏洞利用工具,它可以使用API hooking从一个低权限的用户那里截获网络通信数据以及与加密相关的信息,而且还可以捕获明文通信数据以及经过加密的通信数据。这是NetRipper在github上的详细描述,另外,NetRipper还提供了metasploit和powershell版本的利用模块。
何为NetRipper?
在NetRipper刚出现的时候,就有研究人员注意到NetRipper还可以对火狐浏览器,Chrome浏览器,Lync(Skype的一项业务),puTTY,WinSCP,SQL服务器管理程序以及微软Outlook客户端进行数据注入以及捕获相关的网络数据。
NetRipper主要是通过Hook进程的网络函数关键点(封包加密之前与封包解密之后的网络函数)来劫持客户端程序的明文数据。其中包括了许多主流客户端,例如:Chrome,Firefox,IE,WinSCP,Putty以及一些代码库中提供的网络封包加解密函数接口,根据函数接口的函数性质来分的话,可以分为“未导出的函数接口”和“导出的函数接口”。其中Chrome,Putty,SecureCrt以及WinSCP中的网络加解密接口是属于“未导出的函数接口”,需要通过逆向工程来找到其Signature的位置,然后通过HOOK劫持。例如Mozilla Firefox使用了nss3.dll和nspr4.dll这两个模块中的加解密函数,nss3.dll中导出了PR_Read,PR_Write以及PR_GetDescType,后者导出了PR_Send和PR_Recv。但对于无法导出SSL_Read和SSL_Write函数的Chrome来说,要实现HOOK就很难了。
对于想劫持此类调用的人来说,主要问题是无法轻松地在巨大的chrome.dll文件中找到这些SSL函数。所以要在二进制文件中手动找到它们,就得想点妙招了。
从Chrome的源代码入手
为了实现在二进制文件中找到SSL函数的目标,最好的入手点可能是Chrome的源代码。关于Chrome的源代码,你可以点此详细了解,并轻松地搜索和浏览想要的源代码。
在查看Chrome的源代码时,你要注意到Google Chrome使用了boringssl,这是OpenSSL的一个分支项目,此项目可在Chromium源代码中找到。
现在,我们必须找到我们需要的函数: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 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 necessary, complete the handshake implicitly. if (!ssl_can_write(ssl)) { ret = SSL_do_handshake(ssl); if (ret 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宏:
// OPENSSL_PUT_ERROR is used by OpenSSL code to add an error to the error// queue.#define OPENSSL_PUT_ERROR(library, reason) \ ERR_put_error(ERR_LIB_##library, 0, reason, __FILE__, __LINE__)
有些东西非常有用,比如:
1.ERR_put_error是一个函数调用;
2.reason是第二个参数,在我们的例子中SSL_R_UNINITIALIZED的值为226(0xE2);
3.__FILE__是ssl_lib.cc的实际文件名,完整路径;
4.__LINE__是ssl_lib.cc文件中的当前行号;
所有这些信息都可以帮助我们找到SSL_write函数,其背后的原因你知道吗?
1.我们知道了这是一个函数调用,因此参数(如reason,__FILE__和__LINE__)将被放置在堆栈(x86)上;
2.我们知道了这是reason(0xE2);
3.我们知道了__FILE__(ssl_lib.cc);
4.我们知道了__LINE__(在这个版本中是1060或0x424);
但是如果使用了不同的版本呢?那行号就可以完全不同。那么,在这种情况下,我们必须看看Google Chrome如何使用BoringSSL。
我们可以在这里找到特定版本的Chrome。例如,现在在x86上我用的就是版本65.0.3325.181(官方版本)(32位)。我们可以在这里找到它的源代码。所以接下来,我们必须找到BoringSSL代码,但它看起来不在这里。虽然如此,我们还是可以发现DEPS文件是非常的有用,并可以从中提取一些信息。
vars = {... 'boringssl_git': 'https://boringssl.googlesource.com', 'boringssl_revision': '94cd196a80252c98e329e979870f2a462cc4f402',
从以上代码段中,你可以看到,我们的Chrome版本使用获取BoringSSL,并使用此版本:94cd196a80252c98e329e979870f2a462cc4f402。基于此,我们可以在这里获得BoringSSL的确切代码,并找到ssl_lib.cc文件。
现在,让我们看看我们必须采取哪些步骤来获取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
而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就可以直接找到了。
Chrome 32位
既然我们有了关于如何找到SSL函数的总体思路,就让我们来找到它们吧!
我们在本文使用的是x64dbg,但你也可以使用任何其他调试器。我们必须进入“内存”选项卡找到chrome.dll。整个过程,需要两个步骤:
1.在反汇编程序中打开代码部分,右键单击“.text”并选择“在反汇编程序中执行”;
2.在转储窗口中打开只读数据部分,右键单击“.rdata”并选择“转储”;
现在,我们就必须在转储窗口中找到“ssl_lib.cc”字符串,然后点击右键,选择“Find Pattern”并搜索我们的ASCII字符串。此时,你应该得到一个单一的结果,双击它然后返回,直到找到ssl_lib.cc文件的完整路径。右键单击完整路径的第一个字节,如下面的屏幕截图所示,然后选择“查找引用”以查看我们可以找到它的位置(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 |
我们预期的完全一样,这是一个带有五个参数的函数调用,正如你可能知道的那样,参数从右到左被推入栈中:
1. push 3AD:行号;
2. push chrome.6DE92424:我们的字符串,文件路径;
3. push 44-:原因;
4. push 0:始终为0的参数;
5. push 10:第一个参数;
6.调用chrome.6C44D998 :调用ERR_put_error函数;
7.添加esp,1:清理堆栈
但是,0x3AD表示行号941,它位于“ssl_do_post_handshake”内部,因此它不是我们所需要的。
SSL_write
SSL_write在行号1056(0x420)和1061(x0425)上调用了该函数,因此我们需要在开始时通过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 |
我们可以在上面看到这两个函数调用,但只是提到第一个是优化的。现在,我们只需要返回,直到找到一个看起来以函数开头的内容。虽然该方法并不适用于其他函数,但它还是适用于本文的情况的,我们可以通过经典函数序言( classic function prologue)很容易地找到一个看起来以函数开头的内容。
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.6A06529106DEF278 0D48C9C0 ; First parameter of SSL_write (pointer to SSL)06DEF27C 0B3C61F8 ; Second parameter, the payload06DEF280 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),选择“查找引用”和“选定地址(es)”。
此时,我们应该只能找到一个函数调用,它应该是SSL_peek。找到SSL_peek的第一条指令并重复刚才的步骤。此时,我们应该只能得到一个结果,即从SSL_read调用SSL_peek。
6A065F52 | 55 | push ebp | ; SSL_read function6A065F53 | 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.6A065F5206DEF33C 0AF39EA0 ; First parameter of SSL_read, pointer to SSL06DEF340 0D4D5880 ; Second parameter, the payload06DEF344 00001000 ; Third parameter, payload length
现在,我们应该右键单击第二个参数,然后在按下“Execute til return”按钮之前选择“Follow DWORD in Dump”,以便在函数结束时终止调试器,因此在缓冲区中读取数据之后。我们应该能够在转储窗口中看到纯文本数据,这时,我们就可以选择有效载荷了。
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
总结
如果我们从二进制文件中的源代码入手,就很容易Hooking Chrome浏览器的SSL函数来读取SSL通信数据,这种方法应该适用于大多数开源应用程序。由于x64版本非常相似,唯一的区别是汇编代码,因此在此不再赘述。
但是,请注意,Hooking这些函数可能会导致浏览器不稳定的运行和可能发生的崩溃。