JNI层SSL通信抓包与溯源

xiaoeryu Lv5

在一些场景下对App与服务端交互的数据安全程度要求较高,可能会对通信进行加密传输

  • 使用HTTPS/TLS:使用HTTPS协议,在传输层使用TLS进行加密,确保数据在传输过程中不会被窃取或篡改。
  • SSL Pinning:通过SSL Pinning确保客户端只信任特定的服务器证书,防止中间人攻击。

HTTPS/SSL通信的过程当中可以使用Java层的API完成与服务器的通信,也有可能在JNI层直接使用openssl/boringSSL(在JNI层当中去使用SSL通信与服务器建立一个安全的加密通道来完成数据的发送)。

之前我们分析了在Java层的SSL,现在这篇文章来分析一下JNI层的SSL

要完成对JNI层SSL通信的抓包和溯源,需要对NativeCryptoClass.SSL_read、还有NativeCryptoClass.SSL_write这两个Java层函数继续往下分析进入到libc当中的系统调用(这里以11.0的源码为例)

源码分析

继续往下寻找这个JNI函数的实现,他完整的名字就是NativeCrypto_SSL_writeNativeCrypto_SSL_read

这两个函数所在的文件位置都是一样的,接下来我们以NativeCrypto_SSL_write为例分析

  • jbyteArray b转成了jbyte buf[1024]sslWrite进行循环发送

  • 接下来我们需要搞明白NativeCrypto_SSL_write是如何调用BoringSSL把App的数据进行加密,然后进行发送的

继续分析这个sslWrite函数

  • 调用了boringSSL库中的SSL_write发送buf

boringssl 源码 中都可以找到SSL_write,这里注意大小写

  • 继续往下分析write_app_data
  • 这里不能直接搜索write_app_data,需要在前面加一个前缀ssl3,仅限于当前版本,在之后版本的源码中可能会变
  • 那么继续分析do_ssl3_write,第三个参数是要发送的数据
  • 到了这里之后,看到了参数中开始有cipher相关的关键字。特别关注一下这个函数,可能再往下就开始对数据进行加密了
  • 继续往下跟踪ssl3_write_pending
  • in 参数是指向待发送数据的指针,但在 ssl3_write_pending 函数中并没有直接发送 in 指向的数据。实际上,ssl3_write_pending 函数主要用于处理之前挂起的写操作,并检查当前的写操作是否与之前的挂起操作兼容。

    数据的实际发送是通过 ssl_write_buffer_flush 函数完成的。ssl_write_buffer_flush 会处理实际的数据发送操作,将挂起的数据通过底层的网络套接字发送出去。

  • 继续往下跟踪ssl_write_buffer_flush

  • 根据ssl版本的不同调用不同的API
  • 这两个API都是调用了BIO_write发送数据
  • 继续跟踪bwrite

    这里继续往下跟踪较为麻烦一些,先跟过去

    • BIO_METHOD 结构体定义了很多函数指针来执行各种BIO操作,这里我们只关注bwrite
    • 再往上翻翻其它定义,找到了 BIO_s_socket 方法的声明
    • 接下来,跟进去找到 BIO_s_socket 的实现
    • 这里在socket.c文件中,定义了一个具体的 BIO_METHOD 结构体实例 methods_sockp,并将 bwrite 指向BIO_METHOD 结构体的第一个成员 sock_write 函数,sock_read对应的自然是第二个成员bread

    用IDA分析libc.so文件,我们也能找到write的系统调用号

这里根据跟踪到的结果我们就可以知道实际的调用流程:

当调用 BIO_write 时,实际调用的是 bio->method->bwrite,即 sock_write 函数

sock_write 函数中不同的平台调用了不同的函数来完成数据发送。WINDOWS平台使用send其它平时使用write

总结:所以此时我们可以知道在boringssl中并没有使用libc中的sendto来进行加密数据的发送,而是使用的write。读取使用的是read,同样是在socket.c文件中这里不再展开赘述。所以我们hook libc中的sendtorecvfrom是无法获取到HTTPS/SSL的数据包的

编写脚本

经过我们对源码的分析发现SSL加密的分界线在do_ssl3_write之后

sslWrite、SSL_write、ssl3_write_app_data、do_ssl3_write

明文密文分水岭

ssl3_write_pending、BIO_write、sock_write、write

获取明文的数据就需要hook do_ssl3_write之前的函数

这里我们以SSL_write为hook目标编写脚本获取SSL通信的明文数据

function LogPrint(log) {
    var theDate = new Date();
    var time = theDate.toISOString().split('T')[1].replace('Z', '');
    var threadid = Process.getCurrentThreadId();
    console.log(`[${time}] -> threadid:${threadid} -- ${log}`);
}

function printNativeStack(context, name) {
    var array = Thread.backtrace(context, Backtracer.ACCURATE);
    var first = DebugSymbol.fromAddress(array[0]);
    if (first.toString().indexOf('libopenjdk.so!NET_Send') < 0) {
        var trace = array.map(DebugSymbol.fromAddress).join("\n");
        LogPrint(`-----------start:${name}--------------`);
        LogPrint(trace);
        LogPrint(`-----------end:${name}--------------`);
    }
}

function hooklibssl() {
    var libsslmodule = Process.getModuleByName("libssl.so");
    var SSL_read_addr = libsslmodule.getExportByName("SSL_read");
    var SSL_write_addr = libsslmodule.getExportByName("SSL_write");
    console.log(SSL_read_addr + "---" + SSL_write_addr);
    // SSL_write(SSL *ssl, const void *buf, int num)
    Interceptor.attach(SSL_read_addr, {
        onEnter: function (args) {
            this.arg0 = args[0];
            this.arg1 = args[1];
            this.arg2 = args[2];

            LogPrint("go into libssl.so->SSL_read");

            printNativeStack(this.context, Process.getCurrentThreadId() + "SSL_read");
        },
        onLeave(retval) {
            var size = retval.toInt32();
            if (size > 0) {
                console.log(Process.getCurrentThreadId() + "---libssl.so->SSL_read:" + hexdump(this.arg1, {
                    length: size
                }));
            }

            LogPrint("leave libc.so->SSL_read");
        },
    });
    // int SSL_read(SSL *ssl, void *buf, int num)
    Interceptor.attach(SSL_write_addr, {
        onEnter: function (args) {
            this.arg0 = args[0];
            this.arg1 = args[1];
            this.arg2 = args[2];

            LogPrint("go into libssl.so->SSL_write");
            printNativeStack(this.context, Process.getCurrentThreadId() + "SSL_write");
        },
        onLeave(retval) {
            var size = ptr(this.arg2).toInt32();
            if (size > 0) {
                console.log(Process.getCurrentThreadId() + "---libssl.so->SSL_write:" + hexdump(this.arg1, {
                    length: size
                }));
            }

            LogPrint("leave libssl.so->SSL_write");
        },
    });
}

function main() {
    hooklibssl();
}

setImmediate(main);

测试脚本

这里我们以腾讯新闻App的登陆功能为例:

执行命令:可能抓取到明文的手机号码

frida -UF -l HOpenssl.js -o TXHO.log

直接使用抓包工具无法抓到SSL的明文数据

  • 标题: JNI层SSL通信抓包与溯源
  • 作者: xiaoeryu
  • 创建于 : 2024-07-04 20:50:51
  • 更新于 : 2024-07-05 11:51:17
  • 链接: https://github.com/xiaoeryu/2024/07/04/JNI层SSL通信抓包与溯源/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
此页目录
JNI层SSL通信抓包与溯源