移动端安全 JNI到Native层socket通信hook点追溯
北海 发表于 广东 历史精选 1181浏览 · 2024-10-22 14:45

书接上回Java层socket通信hook点追溯,本文接着从jni函数socketWrite0、socketRead0出发继续分析:

JNI层

安卓对jni函数有固定的格式要求:类名_函数名,于是我们可以搜SocketOutputStream_socketWrite0,SocketInputStream_socketRead0

进入SocketOutputStream_socketWrite0和SocketInputStream_socketRead0

观察代码我们可以发现SocketOutputStream_socketWrite0是使用NET_Send进行数据发送的


而SocketInputStream_socketRead0是使用给NET_Read进行数据接收的

接着如果我们想对NET_Send和NET_Read进行追踪,仅在源码网站进行追踪会遇到困难:

  • JNI 层函数可能会通过多层嵌套调用其他函数。这些函数可能被封装在不同的模块、类或者文件中。这种多层嵌套的结构使得追踪函数调用变得困难。
  • 部分函数可能是动态加载的,如果没有正确地识别这些动态加载的库和函数指针的赋值过程,就很难追踪到具体被调用的函数。

所以下面使用ida反编译上述jni函数被编译后所在so库来追踪。
不过先要解决的问题是SocketOutputStream_socketWrite0和SocketOutputStream_socketRead0所在的SocketOutputStream.c和SocketInputStream.c被编译的到哪个so库了。
这个并不难
点击native退回到SocketOutputStream.c和SocketInputStream.c所在目录中

可以看到android.bp文件

在编译 Android 源码时,构建系统会参考android.bp(以及其他类似的构建描述文件)来进行编译。
Android 的构建系统(如 Soong)使用这些构建描述文件来确定各个模块的构建规则、依赖关系、编译选项等。构建系统会解析android.bp文件中的内容,根据其中定义的模块类型、源文件、依赖关系等信息来执行相应的编译、链接和打包操作。
进入android.bp,name属性被设置为"Libopenjdk native srcs",说明在Android中会被编译成libopenjdk.so

下面把libopenjdk.so拉下来。

本文设备是nexus5,如果大佬们选择模拟器中的so库进行分析会在内核层分析的部分出现跟本文不同的现象
因为在x86_64架构的模拟器上运行的 Android 系统不完全等同于在 ARM 等常见移动设备架构(通常被认为更接近 “安卓原生环境”)上运行的环境
下图为模拟器架构展示

Native层

发送

使用ida打开libopenjdk.so,搜索

查看调用图

看着调用图,很容易就能找到调用链了j_j_NET_Send->j_NET_Send->NET_Send->j_sendto->sendto->__imp_sendto

我们来到sendto函数


ctrl+点击键入查看__imp_sendto


上图的IMPORT imp_sendto通常是一个导入声明,用于指示在链接阶段需要从外部库或动态链接库中导入名为imp_sendto的符号,所以在这个库中调用链的尽头就到这了

接收

同理

调用图


调用链 j_j_NET_Send->j_NET_Read->NET_Read->recvfrom->__imp_recvfrom

与内核接触

sendto

事实上,recvfrom和sendto都是来自libc.so。接下对libc进一步分析,跟上文一样pull下来,用ida反编译
搜索sendto


调用图

阅读汇编代码

解析:
(1)函数开头部分

  • EXPORT sendto:这行指令表示将名为 “sendto” 的函数导出,使得其他模块可以调用这个函数。这通常在构建共享库或可执行文件时使用,以便外部代码能够访问这个函数。
  • sendto:这是函数的标签,表示函数的开始位置。
  • MOV R12, SP:将栈指针(SP)的值复制到寄存器 R12。在 ARM 架构中,栈指针通常用于跟踪当前栈的位置,保存局部变量和函数调用的上下文。
  • PUSH {R4-R7}:将寄存器 R4 到 R7 的值压入栈中,这通常是为了保存这些寄存器的值,以便在函数执行过程中不被破坏。
    (2)中间部分
  • LDM R12, {R4-R6}:从内存地址由寄存器 R12 所指向的位置加载数据到寄存器 R4、R5 和 R6。具体加载的数据取决于内存中的内容和程序的上下文。
  • MOVW R7, #0x122:将立即数 0x122 加载到寄存器 R7。这个立即数的具体用途取决于程序的逻辑,可能是作为一个参数或者标志。
  • SVC 0:这是一条系统调用指令,触发软件中断,将控制权转移到操作系统内核。系统调用的具体功能取决于程序的上下文和操作系统的实现。
    (3)结尾部分
  • POP {R4-R7}:从栈中弹出之前保存的寄存器 R4 到 R7 的值,恢复这些寄存器的状态。
  • CMN RO, #0x1000:比较寄存器 R0 的值与立即数 0x1000,并设置相应的标志。这可能是用于检查系统调用的返回值或某个特定的条件。
  • BXLS LR:如果满足特定条件(可能是根据前面的比较结果),则使用带链接的返回指令跳转到函数的返回地址(保存在链接寄存器 LR 中),同时根据标志位设置指令集状态。
  • RSB RO, RO, #0:如果不满足前面的条件,将寄存器 R0 的值取反。这可能是为了处理错误情况或特定的返回值处理。
  • B j_set_errno_internal:无条件跳转到名为 “j_set_errno_internal” 的标签处。这可能是用于设置错误码或处理错误情况的函数。

这里的 SVC 0 发起了系统调用,从用户态进入内核态以请求内核提供服务。

recvfrom

同理搜索recvfrom


汇编代码


调用图

解析
(1)函数开头部分

  • EXPORT recvfrom:这表示将名为 “recvfrom” 的函数导出,使得其他模块可以调用这个函数。
  • recvfrom:这是函数的标签,标志着函数的开始位置。
  • MOV R12, SP:将栈指针(SP)的值复制到寄存器 R12。这通常是为了在函数内部方便地访问栈上的变量和参数。
  • PUSH {R4 - R7}:将寄存器 R4 到 R7 的值压入栈中,这是为了保存这些寄存器的值,防止在函数执行过程中被修改。
    (2)中间部分
  • LDM R12, {R4 - R6}:从由寄存器 R12 所指向的内存地址处加载数据到寄存器 R4、R5 和 R6。具体加载的数据取决于内存中的内容和程序的上下文。
  • MOV R7, #0x124:将立即数 0x124 加载到寄存器 R7。这个立即数的具体用途可能与函数的特定操作有关,可能是作为参数或者标志。
  • SVC 0:这是一条系统调用指令,触发软件中断,将控制权转移到操作系统内核,请求内核执行特定的操作。
    (3)结尾部分
  • POP {R4 - R7}:从栈中弹出之前保存的寄存器 R4 到 R7 的值,恢复这些寄存器的状态。
  • CMN RO, #0x1000:比较寄存器 R0 的值与立即数 0x1000,并设置相应的标志位。这可能是用于检查系统调用的返回值或者特定的条件。
  • BXLS LR:如果满足特定条件(可能是根据前面的比较结果),则使用带链接的返回指令跳转到函数的返回地址(保存在链接寄存器 LR 中),同时根据标志位设置指令集状态。
  • RSB RO, RO, #0:如果不满足前面的条件,将寄存器 R0 的值取反。这可能是为了处理错误情况或者特定的返回值处理。
  • B jset_errno_internal:无条件跳转到名为 “jset_errno_internal” 的标签处。这可能是用于处理错误情况或者设置错误码的函数调用。

可见同样发起了系统调用

hook代码和demo

hook脚本

有了对socket通信从jni层到native层再到系统内核调用的分析追溯之后,我们来编写一下对libc.so中的sendto、recvFrom的hook脚本
下面借鉴寒冰大佬,xiaoeryu大佬的脚本,抓取数据包并打印SO层的调用栈

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 isprintable(value) {
    return value >= 32 && value <= 126;
}

// 使用frida提供的工具解析socket获取IP和port
function getsocketdetail(fd) {
    var type = Socket.type(fd);
    if (type !== null) {
        var peer = Socket.peerAddress(fd);
        var local = Socket.localAddress(fd);
        return `type:${type}, address:${JSON.stringify(peer)}, local:${JSON.stringify(local)}`;
    }
    return "unknown";
}

function hooklibc() {
    var libcmodule = Process.getModuleByName("libc.so");
    var recvfrom_addr = libcmodule.getExportByName("recvfrom");
    var sendto_addr = libcmodule.getExportByName("sendto");
    console.log(recvfrom_addr + "---" + sendto_addr);

    // ssize_t recvfrom(int fd, void *buf, size_t n, int flags, struct sockaddr *addr, socklen_t *addr_len)
    Interceptor.attach(recvfrom_addr, {
        onEnter: function (args) {
            this.arg0 = args[0];
            this.arg1 = args[1];
            this.arg2 = args[2];
            this.arg4 = args[4];

            LogPrint("go into libc.so->recvfrom");

        }, onLeave: function (retval) {
            var size = this.arg2.toInt32();
            if (size > 0) {
                var result = getsocketdetail(this.arg0.toInt32());
                console.log(result + "---libc.so->recvfrom:" + hexdump(this.arg1, {
                    length: size
                }));
            }

            LogPrint("leave libc.so->recvfrom");
        }
    });

    // ssize_t sendto(int fd, const void *buf, size_t n, int flags, const struct sockaddr *addr, socklen_t addr_len)
    Interceptor.attach(sendto_addr, {
        onEnter: function (args) {
            this.arg0 = args[0];
            this.arg1 = args[1];
            this.arg2 = args[2];
            this.arg4 = args[4];

            LogPrint("go into libc.so->sendto");
        }, onLeave: function (retval) {
            var size = this.arg2.toInt32();
            if (size > 0) {
                var result = getsocketdetail(this.arg0.toInt32());
                console.log(result + "---libc.so->sendto:" + hexdump(this.arg1, {
                    length: size
                }));
            }

            LogPrint("leave libc.so->sendto");
        }
    });
}

function main() {
    hooklibc();
}

setImmediate(main);

打印堆栈

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

}

解析UDP通信的IP

function getip(ip_ptr) {
    return Array.from({ length: 4 }, (_, i) => ptr(ip_ptr.add(i)).readU8()).join('.');
}

function getUdpAddr(addrptr) {
    var port = addrptr.add(2).readU16();
    var ip_addr = getip(addrptr.add(4));
    return `peer:${ip_addr}--port:${port}`;
}

function handleUdp(socketType, sockaddr_in_ptr, sizeofsockaddr_in) {
    var addr_info = getUdpAddr(sockaddr_in_ptr);
    console.log(`this is a ${socketType} udp! -> ${addr_info} --- size of sockaddr_in: ${sizeofsockaddr_in}`);
}

demo


httpClient.c的代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <netinet/in.h>
#include <unistd.h>

int httpClient() {
    char *host = "x.x.x.x:xxxx";
    int sockfd;
    int len;
    struct sockaddr_in address;
    int result;
    char httpstring[10000];

    char *server_ip = "x.x.x.x";

    strcat(httpstring,"GET / HTTP/1.1\r\n");
    strcat(httpstring,"Host: ");
    strcat(httpstring,host);
    strcat(httpstring,"\r\n");
    strcat(httpstring,
           "Connection: keep-alive\r\n"
           "Cache-Control: max-age=0\r\n"
           "Upgrade-Insecure-Requests: 1\r\n"
           "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36\r\n"
           "DNT: 1\r\n"
           "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\r\n"
           "Accept-Encoding: gzip, deflate, br\r\n"
           "Accept-Language: zh-CN,zh;q=0.9\r\n\r\n");
    char buffer[1024];
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = inet_addr(server_ip);
    address.sin_port = htons(80);
    len = sizeof(address);
    result = connect(sockfd,(struct sockaddr *)&address,len);
    if(result == -1){
        perror("oops: client connect error");
        return 1;
    }
    printf("befor connect!!");
    send(sockfd, httpstring, strlen(httpstring), 0);
    printf("after write!!\n");
    while (recv(sockfd, buffer, sizeof(buffer) - 1, 0) > 0) {
        buffer[sizeof(buffer) - 1] = '\0';
        printf("%s", buffer);
    }
    close(sockfd);
    printf("\n");
    return 0;
}

这是如果我们想在android studio中验证发送的调用链,我们可以键入send

recv也是一样的

运行抓包脚本

0 条评论
某人
表情
可输入 255