书接上回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也是一样的
运行抓包脚本