前言

参考前面R0capture的代码,具体通过案例来分析抓包的原理和关键源码的追踪。主要包含下面内容:

  • Http Request
  • Http Response
  • Https Request
  • Https Response

Hook抓包原理&关键点源码追踪(HTTP request)

demo-实现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

分析

根据代码可以看到这块是调用了 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

手写R0capture的Hook代码

这块根据上面的分析逻辑,对 Http Request调用的 SocketOutputStream中的socketWrite0函数进行hook:

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

Http response

分析

同理,对照源码,可以看到这块是调用inputStream.read方法来获取http的response的。

img

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

image.png

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

image.png

hook代码

//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

https的Request和Response

编写demo

//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

request

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

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

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

image.png

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

image.png

img

img

这块传入的参数是一个数组。但是没有在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)

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”,它里面有定义一些方法,可以获取地址:

img

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

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

img

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

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

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

img

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

img

直接hook,打印:

image.png

image.png

整合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)

image.png