JNI层Socket抓包与溯源
之前几章分析了Java层的socket与SSL通信源码,了解了如何通过fridaHook抓取Java层的Socket和SSL通信
接下来两章通过对C层源码分析,了解如何抓取C层的通信
环境
Android:11.0
测试demo:使用系统框架层套接字进行通信
- 根据抓包的结果可以看到使用Charles和wireshark都可以抓到包,但是使用我们的java层的hook脚本抓不到任何数据包。说明其没有使用Java层框架的API进行通信
源码分析
从之前分析到的JNI开始往下分析
接下来我们从之前的JNI函数socketWrite0、socketRead0开始继续往下进入C层分析
在Android源码中的命名非常规范,直接搜索类名_函数名可以直接找到
里面分别使用了
NET_Send
、NET_Read
来进行后续的网络通信它是一个JNI函数那么编译完之后自然就是一个SO库了
静态注册的JNI有自己的命名规则Java_包名_类名_自定义函数名_签名,根据这个函数的命名规范可以判断它不是静态注册的函数
so文件分析
找到源码生成的so文件,用IDA对其调用流程进行分析
在源码中找到
SocketOutputStream.c
生成的so文件名字之前编译源码的时候下载过Android11的源码,我们去源码中去找一下
SocketOutputStream.c
编译后的so文件应该叫什么名字
- 从找到的信息中可以看到
SocketOutputStream.c
文件在Android11中会被编译成libopenjdk.so文件去设备中搜索一下这个文件pull下来用IDA分析
adb shell find / -name "libopenjdk.so" 2>/dev/null adb pull ***
用IDA分析libopenjdk找到hook点
- 因为是动态函数,我们需要去
JNI_OnLoad
中查找,JNI_OnLoad()参数如果识别的不对的话手动修改一下int __fastcall JNI_OnLoad(JavaVM *a1, void *a2)
继续往下查看能看到对Java层输入、输出流函数的注册,先不管它
先直接在字符串中搜索,看能不能直接搜索到SocketInputStream_socketRead0
- 这里可以使用交叉引用直接定位到输入输出流函数
找到了这两个函数的为止之后,看一下他们的调用图(Xrefs graph from)
JNI:socketRead0 -> j_NET_Read -> NET_Read -> recvfrom -> __imp_recvfrom
搜索这个函数可以看到最后调用的是
recvfrom
再往后就是got表了,所以等下可以选取recvfrom
作为hook点
ssize_t recvfrom(int fd, void *buf, size_t n, int flags, struct sockaddr *addr, socklen_t *addr_len)
JNI:socketWrite0 -> j_NET_Send -> NET_Send -> sendto -> __imp_sendto
跟前面的一样,这里同样可以选择
sendto
作为hook点
ssize_t sendto(int fd, const void *buf, size_t n, int flags, const struct sockaddr *addr, socklen_t addr_len)
参数:
**
int fd
**:这是一个文件描述符,它表示要发送数据的套接字。在网络编程中,套接字是一个抽象的网络通信端点,它可以是一个监听套接字(用于接受连接),也可以是一个已连接套接字(用于与远程主机进行通信)。在sendto
中,fd
表示的是要向其发送数据的套接字。**
const void \*buf
**:这是一个指向数据缓冲区的指针,其中包含要发送的数据。buf
是一个void
类型的指针,这意味着它可以指向任何类型的数据。发送的数据通常是一个字节数组,可以是文本、二进制数据等等。**
size_t n
**:这是一个size_t
类型的参数,表示要发送的数据的大小(字节数)。size_t
是无符号整数类型,它的大小通常与系统的地址位数相同,用于表示内存中对象的大小。在sendto
中,n
表示要发送的数据的字节数。**
int flags
**:这是一个整数参数,用于指定发送操作的标志。flags
参数通常用于控制发送操作的行为,比如设置发送的方式(阻塞或非阻塞)、设置发送的优先级等。**
const struct sockaddr \*addr
**:这是一个指向目标地址信息结构体的指针,用于指定要发送数据的目标地址。在网络编程中,sockaddr
结构体用于表示网络地址信息,它包含了目标主机的 IP 地址和端口号等信息。**
socklen_t addr_len
**:这是一个socklen_t
类型的参数,表示目标地址结构体的大小(字节数)。socklen_t
是一个整数类型,用于表示套接字地址结构体的长度。在sendto
中,addr_len
表示目标地址结构体的实际大小。返回值:
返回值如果是负值表示发送错误,如果发送成功,返回值就是发送的字节数。
- 根据调用图可以很清晰的看到其中接收和发送数据都调用了哪些函数
recvfrom
和sendto
的参数类型都一样不进行重复解释了
recvfrom
和sendto
都是来自libc.so的,可以也pull下来继续分析。(不感兴趣不分析也行,已经找到hook点了)
分析libc.so了解怎么进入系统调用
这里简单介绍一下libc.so
libc.so
是 Android 系统中的标准 C 库,提供了 C 语言标准库函数和 POSIX 标准函数的实现。它是 Android 平台上的核心库之一,提供基本的系统调用和底层操作支持,包括内存管理、文件操作、线程管理、网络通信等。
- 既然能被调用,它们自然是导出函数。可以直接被搜索到
recvFrom
.text:000716B0 ; unsigned int __fastcall recvfrom(int, void *, size_t, int) // 函数原型有四个参数
.text:000716B0 EXPORT recvfrom
.text:000716B0 recvfrom ; CODE XREF: j_recvfrom+8↓j
.text:000716B0 ; DATA XREF: LOAD:000064FC↑o
.text:000716B0 ; .got.plt:off_8B280↓o
.text:000716B0 ; __unwind {
.text:000716B0 0D C0 A0 E1 MOV R12, SP // 将当前的堆栈指针保存到寄存器R12中。R12寄存器在ARM EABI中有时被称为IP(临时寄存器,作为函数调用过程中的临时寄存器)
.text:000716B4 F0 00 2D E9 PUSH {R4-R7} // 将寄存器R4~R7的值压入堆栈。在调用结束后方便恢复堆栈
.text:000716B8 70 00 9C E8 LDM R12, {R4-R6} // 从R12(SP)开始取出R4、R5、R6的值存储在这三个寄存器中作为参数
.text:000716BC 49 7F A0 E3 MOV R7, #292 // 将系统调用号 292(即 __NR_recvfrom 的值)存储到 R7 中。R7 在 ARM 系统调用约定中用于存储系统调用号
.text:000716C0 00 00 00 EF SVC 0 // 触发一个软中断(Supervisor Call),使处理器进入内核模式执行系统调用。此时,寄存器 R0 到 R6 的值将作为参数传递给系统调用,R7 则作为系统调用号
.text:000716C4 F0 00 BD E8 POP {R4-R7} // 恢复堆栈现场
.text:000716C8 01 0A 70 E3 CMN R0, #0x1000 // 比较 R0 的值与 0x1000。CMN 指令实际上是执行 R0 + 0x1000 并设置条件标志
.text:000716CC 1E FF 2F 91 BXLS LR // 如果 R0 小于 0x1000(即系统调用成功),则跳转到 LR(返回地址),结束函数执行
.text:000716CC
.text:000716D0 00 00 60 E2 RSB R0, R0, #0 // 如果系统调用失败(R0 >= 0x1000),将 R0 的值取负(相当于 R0 = 0 - R0)。此步骤转换错误码为负值
.text:000716D4 A5 51 00 EA B __ARMV7PILongThunk___set_errno_internal // 跳转到错误处理函数 __ARMV7PILongThunk___set_errno_internal,设置 errno
.text:000716D4 ; } // starts at 716B0
- 这段汇编执行了系统调用(recvfrom)并处理返回值,根据返回结果看是处理错误还是直接返回
sendto
.text:00071688 ; unsigned int __fastcall _sendto(int, const void *, size_t, int)
.text:00071688 __sendto ; CODE XREF: sendto+E↑j
.text:00071688 ; DATA XREF: sendto+6↑o
.text:00071688 ; .data:off_8CB70↓o
.text:00071688 ; __unwind {
.text:00071688 0D C0 A0 E1 MOV R12, SP // 保存堆栈指针
.text:0007168C F0 00 2D E9 PUSH {R4-R7} // 保存寄存器值
.text:00071690 70 00 9C E8 LDM R12, {R4-R6} // 加载函数参数
.text:00071694 22 71 00 E3 MOVW R7, #0x122 // 设置系统调用号(系统调用号 290,对应 sendto)
.text:00071698 00 00 00 EF SVC 0 // 发起系统调用
.text:0007169C F0 00 BD E8 POP {R4-R7} // 恢复寄存器值
.text:000716A0 01 0A 70 E3 CMN R0, #0x1000
.text:000716A4 1E FF 2F 91 BXLS LR // 检查返回值
.text:000716A4
.text:000716A8 00 00 60 E2 RSB R0, R0, #0
.text:000716AC AF 51 00 EA B __ARMV7PILongThunk___set_errno_internal // 错误处理
.text:000716AC ; } // starts at 71688
.text:000716AC
.text:000716AC ; End of function __sendto
- 这段汇编实现了一个
_sendto
函数,通过系统调用sendto
向网络套接字发送数据
通过对libc.so
的分析,知道了在libc中是怎么通过系统调用号配合软中断进入内核的
Frida-Hook脚本编写
以上就是数据接收和发送从JNI到C层所经过的API,以及最后是如何发起系统调用,交给内核进行处理的
之前我们写的java层的hook代码,可以拦截到使用Java层框架API完成通信的抓包,并且可以很方便的打印出调用堆栈。但是对于像我们这次的demo中直接使用系统函数send
、recv
进行通信的方式无法拦截。
接下来我们写一个对SO层的hook,来hooklibc.so
中的sendto
、recvFrom
抓取数据包并打印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);
IP和port解析
ssize_t sendto(int fd, const void *buf, size_t n, int flags, const struct sockaddr *addr, socklen_t addr_len)
socket有ID或者叫句柄。对于
sendto
、recvfrom
来说它的第一个参数就是socket的ID,所以可以通过解析这个ID来得到通信对端的IP和port。在Frida中有相关的API 可以来对ID进行解析
抓包结果:
添加栈回溯信息
使用frida的Thread功能打印当前线程的栈回溯信息,这里用腾讯新闻的App作为例子抓包试一下
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是null,没有解析出来
解析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}`);
}
总结:
本章完成了对SO库函数的socket抓包,包括打印IP以及C层堆栈调用。
如果需要打印Java层的堆栈调用,还是需要使用之前写的Java层的hook脚本
附件
- 标题: JNI层Socket抓包与溯源
- 作者: xiaoeryu
- 创建于 : 2024-07-04 20:44:59
- 更新于 : 2024-07-04 20:49:52
- 链接: https://github.com/xiaoeryu/2024/07/04/JNI层Socket抓包与溯源/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。