导语:NetRipper是一款针对Windows操作系统的漏洞利用工具,它可以使用API hooking从一个低权限的用户那里截获网络通信数据以及与加密相关的信息,而且还可以捕获明文通信数据以及经过加密的通信数据。

u=2312645635,2777691502&fm=27&gp=0.jpg

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 <= 0) {
 return ret;
 }
 // TODO(davidben): In DTLS, should the rest of the record be discarded? DTLS
 // is not a stream. See https://crbug.com/boringssl/65.
 ssl->s3->pending_app_data =
 ssl->s3->pending_app_data.subspan(static_cast<size_t>(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 < 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宏:

// 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 <= 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就可以直接找到了。

Chrome 32位

既然我们有了关于如何找到SSL函数的总体思路,就让我们来找到它们吧!

我们在本文使用的是x64dbg,但你也可以使用任何其他调试器。我们必须进入“内存”选项卡找到chrome.dll。整个过程,需要两个步骤:

1.在反汇编程序中打开代码部分,右键单击“.text”并选择“在反汇编程序中执行”;

2.在转储窗口中打开只读数据部分,右键单击“.rdata”并选择“转储”;

现在,我们就必须在转储窗口中找到“ssl_lib.cc”字符串,然后点击右键,选择“Find Pattern”并搜索我们的ASCII字符串。此时,你应该得到一个单一的结果,双击它然后返回,直到找到ssl_lib.cc文件的完整路径。右键单击完整路径的第一个字节,如下面的屏幕截图所示,然后选择“查找引用”以查看我们可以找到它的位置(OPENSSL_PUT_ERROR函数调用)。

9.jpg

从上图中,你大概能找到多个引用,但我们还必须一个个来试一下,才能地找合适的引用,查找结果如下。

10.jpg

我们来看最后一个例子,看看它是怎样的?

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.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),选择“查找引用”和“选定地址(es)”。

17.jpg

此时,我们应该只能找到一个函数调用,它应该是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

现在,我们应该右键单击第二个参数,然后在按下“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这些函数可能会导致浏览器不稳定的运行和可能发生的崩溃。

源链接

Hacking more

...