自编译openssl库的抓包与溯源

xiaoeryu Lv5

前面几章我们分析了在Java层和JNI层中使用HTTP/HTTPS发送数据和接收时,怎么去进行抓包与溯源

有些App会不使用Android系统提供的SSL库,而使用开源的openssl自己编译一个本地库,用自己的这个库来完成SSL通信。这种情况下我们再对SSL_read/SSL_write进行hook就无法拿到发送和接收的数据了。

甚至APP在有些场景下会直接写汇编使用系统调用号直接与内核交互来进行数据的接收和发送,这样会绕过了使用sendto/recvfrom/read/write这些函数,这样的操作会更加的隐蔽。

接下来我们分别分析,在这些情况下如何抓包。

PS:本章中有些代码在Android11.0环境下没有生效,所以使用了8.0的环境,frida需要使用12.8.0有些在frida16上生效的代码在12.8上不生效。需要修改一下不再赘述

使用私有SSL库通信

使用C编写标准SSL通信的时候,有一个关键的一步->建立SSL通信之后需要去调用SSL_get_fd来完成当前的socketID和当前的SSL的绑定。所以利用这一点可以通过SSL_get_fd来获取当前SSL的socketID

  • 通过主动调用SSL_get_fd传入SSL,可以得到当前SSL通信过程当中的socketID

用设备中拷贝出libssl.so用IDA查看(环境是Android11.0)

  • 可以看到SSL_get_fd是一个导出函数,32位和64位都有。可以使用frida直接调用

编写脚本

只需要对上一章的脚本稍微进行一些修改,从libssl.so中找到SSL_get_fd然后拿到返回值socketID,对socketID进行解析就能拿到IP和端口

function LogPrint(log) {
    // Get the current date and time
    var theDate = new Date();
    var hour = theDate.getHours();
    var minute = theDate.getMinutes();
    var second = theDate.getSeconds();
    var mSecond = theDate.getMilliseconds();

    // Format the time components to ensure two-digit display
    hour = hour < 10 ? "0" + hour : hour;
    minute = minute < 10 ? "0" + minute : minute;
    second = second < 10 ? "0" + second : second;
    mSecond = mSecond < 10 ? "00" + mSecond : mSecond < 100 ? "0" + mSecond : mSecond;

    // Construct the time string
    var time = hour + ":" + minute + ":" + second + ":" + mSecond;
    var threadid = Process.getCurrentThreadId();

    // Log the message with the timestamp and thread ID
    console.log("[" + time + "]" + "->threadid:" + threadid + "--" + log);
}

function printNativeStack(context, name) {
    // Get the native backtrace from the provided context
    var array = Thread.backtrace(context, Backtracer.ACCURATE);

    // Check if the first symbol is related to 'libopenjdk.so!NET_Send'
    var first = DebugSymbol.fromAddress(array[0]);
    if (first.toString().indexOf('libopenjdk.so!NET_Send') < 0) {
        // Log the backtrace if it doesn't match
        var trace = Thread.backtrace(context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join("\n");
        LogPrint("-----------start:" + name + "--------------");
        LogPrint(trace);
        LogPrint("-----------end:" + name + "--------------");
    }
}

function printJavaStack(name) {
    Java.perform(function () {
        var Exception = Java.use("java.lang.Exception");
        var ins = Exception.$new("Exception");
        var straces = ins.getStackTrace();
        if (straces != undefined && straces != null) {
            var strace = straces.toString();
            var replaceStr = strace.replace(/,/g, " \n ");
            LogPrint("=============================" + name + " Stack start=======================");
            LogPrint(replaceStr);
            LogPrint("=============================" + name + " Stack end======================= \n ");
            Exception.$dispose();
        }
    });
}

function isprintable(value) {
    return (value >= 32 && value <= 126);
}

function getsocketdetail(fd) {
    var result = "";
    var type = Socket.type(fd);
    if (type != null) {
        result = result + "type:" + type;
        var peer = Socket.peerAddress(fd);
        var local = Socket.localAddress(fd);
        console.log("Peer address: " + JSON.stringify(peer) + ", Local address: " + JSON.stringify(local));
        result = result + ", address:" + JSON.stringify(peer) + ", local:" + JSON.stringify(local);
    } else {
        result = "unknown";
    }
    return result;
}

function hooklibssl() {
    // SSL_write(SSL *ssl, const void *buf, int num)
    // int SSL_get_fd(const SSL *ssl) { return SSL_get_rfd(ssl);
    var libsslmodule = Process.getModuleByName("libssl.so");
    var SSL_read_addr = libsslmodule.getExportByName("SSL_read");
    var SSL_write_addr = libsslmodule.getExportByName("SSL_write");
    var SSL_get_fd_addr = libsslmodule.getExportByName("SSL_get_rfd");
    var SSL_get_fd = new NativeFunction(SSL_get_fd_addr, 'int', ['pointer']);


    Interceptor.attach(SSL_read_addr, {
        onEnter: function (args) {
            this.ssl = args[0];
            this.buf = args[1];
            this.num = args[2];

            LogPrint("go into libssl.so->SSL_read");
            printNativeStack(this.context, Process.getCurrentThreadId() + "SSL_read");
        },
        onLeave: function (retval) {
            var size = retval.toInt32();
            if (size > 0) {
                console.log("SSL object: " + this.ssl);
                var sockfd = SSL_get_fd(this.ssl);
                console.log("SSL_get_fd returned: " + sockfd);
                var socketdetail = getsocketdetail(sockfd);

                console.log(Process.getCurrentThreadId() + socketdetail + "---libssl.so->SSL_read:" + hexdump(this.buf, {
                    length: size
                }));
            }

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

    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: function (retval) {
            var size = ptr(this.arg2).toInt32();
            if (size > 0) {
                var sockfd = SSL_get_fd(this.arg0);
                var socketdetail = getsocketdetail(sockfd);

                console.log(Process.getCurrentThreadId() + socketdetail + "---libssl.so->SSL_write:" + hexdump(this.arg1, {
                    length: size
                }));
            }

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

function main() {
    hooklibssl();
}

setImmediate(main);
  • 有点奇怪,脚本在Android11.0的设备上执行,返回值出错了,换了8.0的设备执行ok
  • 不过Android8.0的64位没有SSL_get_fd这个函数换成SSL_get_rfd也可以
  • 暂时先换8.0的设备,回头再找问题在哪
执行结果

用了滴答清单App的登陆功能进行测试

Android8.0
Android11.0
SSL object: 0x7b9ea45898
SSL_get_fd returned: -1
getsocketdetail called with fd: -1
Socket type: null
27813unknown--------libssl.so->SSL_read: 
  • 返回值报错,返回了-1

扩大搜索范围

扩大搜索范围在所有加载的模块中匹配,即使没有libssl.so只要符号没有抹除掉就能找到目标函数

function hookallssl() {
    var libsslmodule = Process.getModuleByName("libssl.so");
    var SSL_get_rfd_ptr = libsslmodule.getExportByName('SSL_get_rfd');
    var SSL_get_rfd = new NativeFunction(SSL_get_rfd_ptr, 'int', ['pointer']);
    Process.enumerateModules().forEach(function (module) {
        module.enumerateExports().forEach(function (symbol) {
            var name = symbol.name;
            if (name == 'SSL_read') {
                LogPrint(JSON.stringify(module) + JSON.stringify(symbol));
            }
            if (name == 'SSL_write') {
                LogPrint(JSON.stringify(module) + JSON.stringify(symbol));
                Interceptor.attach(symbol.address, {
                    onEnter: function (args) {
                        this.arg0 = args[0];
                        this.arg1 = args[1];
                        this.arg2 = args[2];
                        LogPrint("go into " + Process.getCurrentThreadId() + "---" + JSON.stringify(module) + "---" + JSON.stringify(symbol));
                        printNativeStack(this.context, Process.getCurrentThreadId() + "---" + JSON.stringify(module) + "---" + JSON.stringify(symbol));
                        var size = ptr(this.arg2).toInt32();
                        if (size > 0) {
                            var sockfd = SSL_get_rfd(this.arg0);
                            var socketdetail = getsocketdetail(sockfd);
                            console.log(socketdetail + "---" + Process.getCurrentThreadId() + "---" + JSON.stringify(module) + "---" + JSON.stringify(symbol) + hexdump(this.arg1, {
                                length: size
                            }));
                        }
                    }, onLeave(retval) {
                        LogPrint("leave " + Process.getCurrentThreadId() + "---" + JSON.stringify(module) + "---" + JSON.stringify(symbol));
                    }
                });
            }
        })
    })
}

总结:本节主要目的在于如果App在进行HTTP/SSL进行加密通信的时候,如果使用的是自己编译的openssl库,可以选择hook第三方库中的SSL_write/read方法,另外通过调用 SSL_get_rfd 的主要目的是获取 SSL 连接的文件描述符,以便进一步获取网络连接的详细信息。这些信息可以帮助分析和调试网络通信,特别是当涉及加密通信时,通过文件描述符可以关联具体的网络连接,从而更好地理解和分析数据的传输过程。

无符号库

前面分析了App没有使用系统SSL库,使用了自己编译的openssl库通信,应该如何抓包。如果符号还在的话较为简单,还是一样对他使用的SSL库进行遍历就拿到SSL_write等hook点就可以了。

因为openssl/boringssl是开源的,大家都可以修改。虽然一般情况下开发人员不会进行大量的修改,经常是把符号抹除掉。

常用的三种方法

那如果符号被抹去了我们应该怎么抓包呢呢?

在符号被抹除的情况下,一般来说还可以通过以下三种方法来找到拦截目标函数

使用基于地址的拦截:如果您知道目标函数的相对地址或偏移量,可以直接拦截该地址。

假设我们知道了ssl_write_offset = 0x123456,那么只需要获取到当前模块的基址然后相加就可以了,不过这种方式随着编译方式的不同,或者库代码的改变等等都会改变偏移的位置。不确定性太大

var baseAddress = Module.findBaseAddress('libssl.so');  // 获取 libssl.so 的基址
var ssl_write_offset = 0x123456;  // 假设 SSL_write 的偏移量是 0x123456
var ssl_write_address = baseAddress.add(ssl_write_offset);  // 计算 SSL_write 的绝对地址

使用模式匹配:通过字节码模式匹配找到目标函数的位置。

在模块中暴力匹配特定的字节码来定位目标函数,比通过偏移来寻找会更为可靠一些。下面以它为例编写代码

通过函数调用上下文或特征识别:分析目标函数的调用上下文或其他特征来识别并拦截。

调用上下文或特征识别的方法主要通过分析目标函数在运行时的行为或周边环境来定位和拦截它。即使符号被抹除,这些方法仍然可以有效地识别目标函数。这些方法包括但不限于:

  1. 调用堆栈特征识别:分析函数调用的堆栈特征。
  2. 参数特征识别:通过函数调用时的参数特征来识别。
  3. 上下文特征识别:通过函数调用前后的上下文信息来识别。

字节匹配模式脚本:

function LogPrint(log) {
    var theDate = new Date();
    var hour = theDate.getHours();
    var minute = theDate.getMinutes();
    var second = theDate.getSeconds();
    var mSecond = theDate.getMilliseconds();

    hour < 10 ? hour = "0" + hour : hour;
    minute < 10 ? minute = "0" + minute : minute;
    second < 10 ? second = "0" + second : second;
    mSecond < 10 ? mSecond = "00" + mSecond : mSecond < 100 ? mSecond = "0" + mSecond : mSecond;
    var time = hour + ":" + minute + ":" + second + ":" + mSecond;
    var threadid = Process.getCurrentThreadId();
    console.log("[" + time + "]" + "->threadid:" + threadid + "--" + log);
}

function printNativeStack(context, name) {
    var trace = Thread.backtrace(context, Backtracer.FUZZY).map(DebugSymbol.fromAddress).join("\n");
    LogPrint("-----------start:" + name + "--------------");
    LogPrint(trace);
    LogPrint("-----------end:" + name + "--------------");
}

function printJavaStack(name) {
    Java.perform(function () {
        var Exception = Java.use("java.lang.Exception");
        var ins = Exception.$new("Exception");
        var straces = ins.getStackTrace();
        if (straces != undefined && straces != null) {
            var strace = straces.toString();
            var replaceStr = strace.replace(/,/g, " \n ");
            LogPrint("=============================" + name + " Stack strat=======================");
            LogPrint(replaceStr);
            LogPrint("=============================" + name + " Stack end======================= \n ");
            Exception.$dispose();
        }
    });
}

function isprintable(value) {
    return value >= 32 && value <= 126;
}

function getsocketdetail(fd) {
    var result = "";
    var type = Socket.type(fd);
    if (type != null) {
        result = result + "type:" + type;
        var peer = Socket.peerAddress(fd);
        var local = Socket.localAddress(fd);
        result = result + ",address:" + JSON.stringify(peer) + ",local:" + JSON.stringify(local);
    } else {
        result = "unknown";
    }
    return result;
}

function getip(ip_ptr) {
    var result = ptr(ip_ptr).readU8() + "." + ptr(ip_ptr.add(1)).readU8() + "." + ptr(ip_ptr.add(2)).readU8() + "." + ptr(ip_ptr.add(3)).readU8();
    return result;
}

function SSL_get_rfd(ssl) {
    var SSL_get_rfd_ptr = Module.findExportByName("libssl.so", "SSL_get_rfd");
    var SSL_get_rfd_func = new NativeFunction(SSL_get_rfd_ptr, 'int', ['pointer']);
    return SSL_get_rfd_func(ssl);
}

function hookMatchbytecode() {
    var baseAddress = Module.findBaseAddress('libssl.so');  // 获取 libssl.so 的基址
    if (baseAddress === null) {
        console.error('Could not find base address for libssl.so');
        return;
    }

    // 获取 libssl.so 的所有可执行范围
    var ranges = Process.enumerateRangesSync({
        protection: 'r-x',
        coalesce: true
    }).filter(function (range) {
        return range.file && range.file.path.indexOf('libssl.so') !== -1;
    });

    // var sizeOfModule = ranges.reduce((total, range) => total + range.size, 0);
    var sizeOfModule = 0;
    ranges.forEach(function (range) {
        sizeOfModule += range.size;
    });


    var pattern = '70 B5 82 B0 06 46 01 20';  // 替换为实际的字节码模式

    Memory.scan(baseAddress, sizeOfModule, pattern, {
        onMatch: function (address, size) {
            var adjustedAddress = address.add(1);  // 调整地址
            console.log('Found SSL_write at: ' + adjustedAddress);
            Interceptor.attach(adjustedAddress, {
                onEnter: function (args) {
                    this.arg0 = args[0];
                    this.arg1 = args[1];
                    this.arg2 = args[2];
                    LogPrint("SSL_write called");
                    printNativeStack(this.context, "SSL_write");
                    var size = ptr(this.arg2).toInt32();
                    if (size > 0) {
                        var sockfd = SSL_get_rfd(this.arg0);
                        var socketdetail = getsocketdetail(sockfd);
                        console.log(socketdetail + " - SSL_write - " + hexdump(this.arg1, { length: size }));
                    }
                },
                onLeave: function (retval) {
                    LogPrint("SSL_write return");
                }
            });
        },
        onComplete: function () {
            console.log('Scan complete');
        }
    });
}

function main() {
    hookMatchbytecode();
}

setImmediate(main);
  • 函数的字节码可以在IDA中提取

    • 需要注意32位和64位的字节码不同
    • 不同版本的源码,字节码可能也不同
    • 在不同环境下,字节码都可能有所不同,所以也不能通用。可以用作一种初筛的方式
  • 在我们的示例代码中以libssl.so库为例,实际使用中自己编译的openssl库可能会是其它的名字,需要先确定是哪一个库再进行扫描,扫描全部的库当然也行,不过比较消耗资源。

测试结果:以腾讯新闻的App为例

总结:不论是使用偏移还是字节码匹配的模式随着环境的改变都不稳定,最好的方式还是打印出来无符号堆栈和标准的有符号的堆栈进行对比,然后尝试找到正确的hook函数,另外也可以尝试去目标so库中搜索关键字符串尝试定位

直接使用系统调用

在某些应用中,确实可能会使用汇编直接调用系统调用号来与内核交互,这样绕过了标准的库函数(如 sendtorecvfromreadwrite),使得传统的拦截方法失效。然而,Frida 仍然可以通过低层次的拦截来捕获这些操作,例如,通过拦截系统调用接口或使用更底层的内存拦截技术

拦截系统调用

一种方法是拦截系统调用的入口点。在 Linux 系统中,系统调用通过 syscall 指令执行,可以通过拦截这些指令来捕获所有的系统调用。

write为例

const SYS_write = 4; // 在 ARM 上,write 的系统调用号是 4

// 帮助函数:打印被拦截的系统调用信息
function logSyscall(context, syscallNumber, args) {
    console.log("Syscall intercepted:");
    console.log("Syscall number:", syscallNumber);
    console.log("Arguments:", args);
    console.log("Context:", context);
}

// 帮助函数:处理被拦截的系统调用
function handleSyscall(context, syscallNumber, args) {
    if (syscallNumber === SYS_write) {
        logSyscall(context, syscallNumber, args);

        // 获取并打印写入的数据
        var fd = args[0].toInt32();
        var buffer = args[1];
        var count = args[2].toInt32();
        var data = buffer.readUtf8String(count);

        console.log("File descriptor:", fd);
        console.log("Data being written:", data);
    }
}

Interceptor.attach(Module.findExportByName(null, 'syscall'), {
    onEnter: function (args) {
        var syscallNumber = args[0].toInt32();
        handleSyscall(this.context, syscallNumber, args.slice(1));
    }
});
  • 代码有问题,暂未修改
  • 标题: 自编译openssl库的抓包与溯源
  • 作者: xiaoeryu
  • 创建于 : 2024-07-29 08:19:13
  • 更新于 : 2024-07-29 09:45:38
  • 链接: https://github.com/xiaoeryu/2024/07/29/自编译openssl库的抓包与溯源/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论