Hooking Chrome浏览器的SSL函数来读取SSL通信数据

x
用微信扫描二维码
分享至好友和朋友圈

  

  

  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函数调用)。

  

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

  

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

  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)”。

  

  此时,我们应该只能找到一个函数调用,它应该是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这些函数可能会导致浏览器不稳定的运行和可能发生的崩溃。

  

  

特别声明:本文为网易自媒体平台“网易号”作者上传并发布,仅代表该作者观点。网易仅提供信息发布平台。

跟贴 跟贴 0 参与 0
© 1997-2019 网易公司版权所有 About NetEase | 公司简介 | 联系方法 | 招聘信息 | 客户服务 | 隐私政策 | 广告服务 | 网站地图 | 意见反馈 | 不良信息举报

嘶吼RoarTalk

不一样的互联网安全新视界

头像

嘶吼RoarTalk

不一样的互联网安全新视界

3052

篇文章

6755

人关注

列表加载中...
请登录后再关注
x

用户登录

网易通行证/邮箱用户可以直接登录:
忘记密码