一 R0Capture介绍

1. 下载地址

r0ysue/r0capture: 安卓应用层抓包通杀脚本 (github.com)

2. 介绍

  • 仅限安卓平台,测试安卓7、8、9、10、11、12、13、14 可用(禁止模拟器) ;
  • 无视所有证书校验或绑定,不用考虑任何证书的事情;
  • 通杀TCP/IP四层模型中的应用层中的全部协议;
  • 通杀协议包括:Http,WebSocket,Ftp,Xmpp,Imap,Smtp,Protobuf等等、以及它们的SSL版本;
  • 通杀所有应用层框架,包括HttpUrlConnection、Okhttp1/3/4、Retrofit/Volley等等;
  • 无视加固,不管是整体壳还是二代壳或VMP,不用考虑加固的事情;

3. 用法Usage

  • Spawn 模式:
$ python3 r0capture.py -U -f com.coolapk.market -v
  • Attach 模式,抓包内容保存成pcap文件供后续分析:
$ python3 r0capture.py -U 酷安 -v -p iqiyi.pcap

建议使用Attach模式,从感兴趣的地方开始抓包,并且保存成pcap文件,供后续使用Wireshark进行分析。

老版本Frida使用包名,新版本Frida使用APP名。APP名必须是点开app后,frida-ps -U显示的那个app名字。

img

  • 收发包函数定位:Spawnattach模式均默认开启;

可以使用python r0capture.py -U -f cn.soulapp.android -v >> soul3.txt这样的命令将输出重定向至txt文件中稍后过滤内容

image-20240628100039131

  • 客户端证书导出功能:默认开启;必须以Spawm模式运行;

运行脚本之前必须手动给App加上存储卡读写权限;

并不是所有App都部署了服务器验证客户端的机制,只有配置了的才会在Apk中包含客户端证书

导出后的证书位于/sdcard/Download/包名xxx.p12路径,导出多次,每一份均可用,密码默认为:r0ysue,推荐使用keystore-explorer打开查看证书。

image-20240628100019436

4. 知识点汇总

image-20240628100124057

微信图片_20240628100321

二 Hook原理&关键源码追踪

1. HTTP Request

实现http请求

创建一个httpsock函数,本质就是简单的java代码,通过socket实现客户端与服务端通信

//http://www.dtasecurity.cn:18080/demo01/getNotice
private void httpsock() {
try {
final String host = "www.dtasecurity.cn";
final int port = 18080;
final String path = "/demo01/getNotice";
Socket socket = new Socket(host,port); //创建socket对象

StringBuilder sb = new StringBuilder();
sb.append("GET "+path+" HTTP/1.1\r\n");
sb.append("user-Agent: test\r\n");
sb.append("Host: "+host+"\r\n");
sb.append("\r\n");
Log.d("DTA===>", sb.toString());
OutputStream outputStream = socket.getOutputStream(); //字符串拼接,拼接好之后调用socket发送到服务端
outputStream.write(sb.toString().getBytes());

//这块同上,接收服务端的返回数据
InputStream inputStream = socket.getInputStream();
byte[] buffer = new byte[1024];
int len;
while( ( len = inputStream.read(buffer,0,buffer.length) ) != -1 ){
Log.d("DTA===>", new String(Arrays.copyOf(buffer,len)));
}
}catch (Exception e){
e.printStackTrace();
}
}

image.png

可以看到这块Java代码是调用了 outputStream 的 write 方法,实现将数据传送到管道中的。

image.png

跟进write方法。可以看到是调用byte数组,进行数据写入。。

image.png

这块 outputStream 是一个抽象类,抽象类是不能直接hook的,所以这块需要找到 outputStream 的实现类。。

那么如何找到它的实现类呢???

这块可以在 outputStream.write 处下断点,debug调试找到它的具体实现类为:SocketOutputStream

image.png

所以,这块可以查找下 SocketOutputStream 中的write方法,参数类型为byte

image.png

调用了 socketWrite 方法,继续往下跟。可以看到真正调用的是try中包裹的socketWrite0方法

image.png

跟进socketWrite0 方法,可以看到这块修饰符多了个 native,这是因为函数不是在java层实现的,它的实现在so层。

image.png

Frida Hook Http-request

function main(){
Java.perform(function(){
console.log("Start!")
//hook SocketOutputStream中的socketWrite0函数
//这块的目的就是拿到函数中的参数 bytes
Java.use("java.net.SocketOutputStream").socketWrite0.implementation = function(fd,bytes,off,len){
//打印了客户端和服务端地址
var localAddress = this.socket.value.getLocalAddress().toString()
var remoteAddress = this.socket.value.getRemoteSocketAddress().toString()
console.log(localAddress +"====>"+ remoteAddress)

//这块通过自定义hexdump函数,获取输入的byte内容
hexdump(bytes,off,len)

showStacks()
this.socketWrite0(fd,bytes,off,len)
}

//调用该函数,可以实现将内存中的 byte 转换为 String
function hexdump(bytearry,offset,length){
// bytearray => [B
// offset => I
// length => I
var HexDump = Java.use("com.android.internal.util.HexDump")
console.log(HexDump.dumpHexString(bytearry,offset,length))
}
//打印它的调用栈
function showStacks() {
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
}
})
}
setImmediate(main)

image.png

2. HTTP Response

这块是调用inputStream.read方法来获取http的response的。

image.png

inputStream.read和 outputStream.write方法是对应关系,这块可以大胆猜测一下,inputStream的实现类是SocketIntputStream

image.png

在它的里面,也是有socketRead0方法的,我们这块hook,也是hook的这个方法

image.png

Frida Hook Http-response

//http response
function main() {
Java.perform(function() {
Java.use("java.net.SocketInputStream").socketRead0.implementation = function(fd,bytes,off,len,timeout) {
console.log("Intercepted socket read");
//打印了客户端和服务端地址
var localAddress = this.socket.value.getLocalAddress().toString()
var remoteAddress = this.socket.value.getRemoteSocketAddress().toString()
console.log(remoteAddress +"<===="+ localAddress)

//打印了读取的数据
hexdump(bytes,off,len);

showStacks() ;

return this.socketRead0(fd,bytes,off,len,timeout);
};
});
}


//调用该函数,可以实现将内存中的 byte 转换为 String
function hexdump(bytearry,offset,length){
// bytearray => [B
// offset => I
// length => I
var HexDump = Java.use("com.android.internal.util.HexDump")
console.log(HexDump.dumpHexString(bytearry,offset,length))
}

//打印它的调用栈
function showStacks() {
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
}

setImmediate(main);

image.png

3. HTTPS Request

实现https请求

//https://www.taobao.com
private void httpsock() {
try {
final String host = "www.taobao.com";
final int port = 18080;
final String path = "/";

//这块使用SSL来传输数据
SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
SSLSocket socket = (SSLSocket) sslSocketFactory.createSocket(host,port);
//Socket socket = new Socket(host,port);

StringBuilder sb = new StringBuilder();
sb.append("GET "+path+" HTTP/1.1\r\n");
sb.append("user-Agent: test\r\n");
sb.append("Host: "+host+"\r\n");
sb.append("\r\n");
Log.d("DTA===>", sb.toString());
OutputStream outputStream = socket.getOutputStream();
outputStream.write(sb.toString().getBytes());

InputStream inputStream = socket.getInputStream();
byte[] buffer = new byte[1024];
int len;
while( ( len = inputStream.read(buffer,0,buffer.length) ) != -1 ){
Log.d("DTA===>", new String(Arrays.copyOf(buffer,len)));
}
}catch (Exception e){
e.printStackTrace();
}
}

image.png

运行程序,可以看到成功实现了https的socket请求和返回。

image.png

源码跟踪

根据demo可以看到,处理加上了一层SSLSocket,剩下的发送和接收的代码相较于http都是没有改动的。

所以,这块request的代码还是: outputStream.write

该位置打断点,查看 outputStream 在此处的实现类为 :ConscryptFileDescriptorSocket$SSLOutputStream,这块的SSLOutputStream为内部类。。。

image.png

检索 ConscryptFileDescriptorSocket类,没有找到。猜测可能是系统中的某个类。

image.png

可以通过网页查找: http://aospxref.com/android-8.1.0_r81/

image.png

image.png

这块传入的参数是一个数组。但是没有在SSLOutputStream这个内部类中找到,说明没有对这个方法进行重写,所以,直接查看 OutputStream中的write方法:

image.png

这块调用的是三个参数的write方法,也就是SSLOutputStream中的write方法:

image.png

调用了 ssl.write(Platform.getFileDescriptor(socket), buf, offset, byteCount,writeTimeoutMilliseconds); 传入了5个参数。

跳转,可以看到实际是调用了SslWraper这个类中的write方法。

image.png

void write(FileDescriptor fd, byte[] buf, int offset, int len, int timeoutMillis)  throws IOException {
NativeCrypto.SSL_write(ssl, fd, handshakeCallbacks, buf, offset, len, timeoutMillis);
}

image.png

这块最终是到了 NativeCrypto.SSL_write 方法,这个方法是Native类中的了,到此就可以了。

image.png

总结一下,整体的一个outputStream.write的调用链为:

outputStream.write-->ConscryptFileDescriptorSocket$SSLOutputStream(实现类)-->SSLOutputStream.write(内部类)-->SslWraper.write-->NativeCrypto.SSL_write(Native层)

Frida Hook Https-request

image.png

运行后,发现报错????提示找不到这个 “org.conscrypt.NativeCrypto” 这个类。

image.png

猜测,NativeCrypto这个类的包名可能有问题,一般类加载到内存中后,包名可能发生变化。

java文件中NativeCrypto类的包名为 “org.conscrypt”

image.png

这块可以使用objection,查看内存中加载的类名:

android   hooking search classes NativeCrypto

image.png

修改全类名为 “com.android.org.conscrypt.NativeCrypto”,成功打印出请求的数据和调用堆栈。

image.png

function hexdump(bytearry,offset,length){
// bytearray => [B
// offset => I
// length => I
var HexDump = Java.use("com.android.internal.util.HexDump")
console.log(HexDump.dumpHexString(bytearry,offset,length))
}

//打印它的调用栈
function showStacks() {
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
}


function main(){
Java.perform(function(){
Java.use("com.android.org.conscrypt.NativeCrypto").SSL_write.implementation = function(sslNativePointer,fd,shc, bytes,off,len,timeout){
hexdump(bytes,off,len);
showStacks();
return this.SSL_write(sslNativePointer,fd,shc, bytes,off,len,timeout);
}
})
}

setImmediate(main)

4. HTTPS Response

Frida Hook Https-response

同理,根据上面的调用链,可以得到response的最终调用方法为:NativeCrypto.SSL_read

image.png

function hexdump(bytearry,offset,length){
// bytearray => [B
// offset => I
// length => I
var HexDump = Java.use("com.android.internal.util.HexDump")
console.log(HexDump.dumpHexString(bytearry,offset,length))
}

//打印它的调用栈
function showStacks() {
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
}


function main(){
Java.perform(function(){
Java.use("com.android.org.conscrypt.NativeCrypto").SSL_read.implementation = function(sslNativePointer,fd,shc, bytes,off,len,timeout){
hexdump(bytes,off,len);
showStacks();
return this.SSL_write.call(sslNativePointer,fd,shc, bytes,off,len,timeout);
}
})
}

setImmediate(main)

image.png

打印https的local和remote地址

根据之前http打印地址的方法,可以看到是调用了this.socket的方法来打印的

var localAddress = this.socket.value.getLocalAddress().toString()
var remoteAddress = this.socket.value.getRemoteSocketAddress().toString()

但在 NativeCrypto 类中没有找到socket方法。

继续向前看,可以看到在 “SSLOutputStream.write” 方法里面,使用ssl.write的时候,是有socket的。

image.png

只不过,这块将 Platform.getFileDescriptor(socket) 作为了ssl.write的一个参数,这块Platform.getFileDescriptor函数,是一个文件的描述符。可以理解为C语言中的open函数,打开了一个文件,返回了一个文件操作符。。。这块想要拿到它的参数 “socket” ,就是我们的目的。

这块Frida提供了API接口:”Socket”,它里面有定义一些方法,可以获取地址:

image.png

Socket.localAddress(handle)
Socket.peerAddress(handle)

那么问题来了?这块的handle参数到底是什么?

image.png

确定参数类型,这块是要输入一个int数字number。。。

function(sslNativePointer,fd,shc, bytes,off,len,timeout)

function中传入参数有7个,但其中关于参数 “fd” 的描述是: FileDescriptor fd(文件描述符)

image.png

查找 FileDescriptor 类,发现里面是有一个**getInt$()**的系统方法,是可以获取Descriptor的数值的。

image.png

直接hook,打印:

image.png

image.png

5. 整合HTTP+HTTPS代码如下

function main() {
Java.perform(function() {
console.log("Start hooking http/https request/response");
//Hook http request
Java.use("java.net.SocketOutputStream").socketWrite0.implementation = function(fd,bytes,off,len){
console.log("<===Hook http request===>");
printHttpAddr(this.socket,true);
hexdump(bytes,off,len);
showStacks();
return this.socketWrite0(fd,bytes,off,len);
}
//Hook http response
Java.use("java.net.SocketInputStream").socketRead0.implementation = function(fd,bytes,off,len,timeout){
console.log("<===Hook http response===>");
printHttpAddr(this.socket,false);
hexdump(bytes,off,len);
showStacks();
return this.socketRead0(fd,bytes,off,len,timeout);
}
//Hook https request
Java.use("com.android.org.conscrypt.NativeCrypto").SSL_write.implementation = function(sslNativePointer,fd,shc,bytes,off,len,timeout){
console.log("<===Hook https request===>");
printHttpsAddr(fd,true);
hexdump(bytes,off,len);
showStacks();
return this.SSL_write(sslNativePointer,fd,shc,bytes,off,len,timeout);
}
//Hook https response
Java.use("com.android.org.conscrypt.NativeCrypto").SSL_read.implementation = function(sslNativePointer,fd,shc, bytes,off,len,timeout){
console.log("<===Hook https response===>");
printHttpsAddr(fd,false);
hexdump(bytes,off,len);
showStacks();
return this.SSL_read(sslNativePointer,fd,shc, bytes,off,len,timeout);
}

//将内存中的 byte 转换为 String
function hexdump(bytearry,offset,length){
var HexDump = Java.use("com.android.internal.util.HexDump")
console.log(HexDump.dumpHexString(bytearry,offset,length))
}

//打印它的调用栈
function showStacks() {
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
}

//打印http请求的地址
function printHttpAddr(socket,isSend) {
var localAddress = socket.value.getLocalAddress().toString();
var remoteAddress = socket.value.getRemoteSocketAddress().toString();
if(isSend) {
console.log("Send http request to " + remoteAddress + " from " + localAddress);
} else {
console.log("Recv http response from " + remoteAddress + " to " + localAddress);
}
}

//打印https请求的地址
function printHttpsAddr(fd,isSend) {
var localAddress = Socket.localAddress(fd.getInt$());
var remoteAddress = Socket.peerAddress(fd.getInt$());
if(isSend) {
console.log("Send https request to " + remoteAddress.ip + ":" + remoteAddress.port + " from " + localAddress.ip + ":" + localAddress.port);
} else {
console.log("Recv https response from " + remoteAddress.ip + ":" + remoteAddress.port + " to " + localAddress.ip + ":" + localAddress.port);
}
}
}
)
}


setImmediate(main)