2021-09-03

大数据安全入门安卓r0capture的源码关键点跟踪

  • 手机环境:安卓8.1.0
  • frida_version:12.8.0

本文重点

本篇文章主要介绍一下r0capture项目抓包部分的一个Hook点是如何发现的。大家可以去r0capture项目地址下载体验:https://github.com/r0ysue/r0capture

r0capture简介

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

我们可以看到r0capture有很多优点,当我们在使用其他抓包软件的时候,容易被证书折磨,还有各种抓包的检测手段,这时候可以使用r0capture进行hook抓包一波,小爽一下,无视各种证书问题。那今天我们就来看看r0capture是如何写出来的。

使用Socket进行Http访问

为什么要讲使用Socket进行Http访问呢?要想做到通杀这两个字是不简单的,因为平台版本都会更新迭代,而且很多的APP都会选择使用第三方的框架来做网络请求,版本各异,尽不相同。所以我们要找一个通用的点就需要从更底层来找。而Http又是建立在Tcp协议之上的一种应用,所以我们要从Tcp出发,无论是多么优秀的框架,都离不开系统提供的网络接口进行收发数据。

下面我们给出一个使用Java进行Socket进行Http访问的例子:

//http://www.dtasecurity.cn:18080/demo01/getNotice
private void request() {
    
    
 try {
    
    
 final String host = "www.dtasecurity.cn";
 final int port = 18080;
 final String path = "/demo01/getNotice";
 
 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();
    }
}

首先创建了一个Socket对象,通过host跟port指定我们要访问的Server,然后根据Http协议约定的格式构造了一个HTTP请求的报文,后面将构造好的数据通过Socket管道进行发送,然后读取服务器返回的数据并打印Log。

程序比较简单,但是可以反映出一个Http请求要做的最基本的工作,就是创建Socket对象,然后构造Http报文发送,那么我们接下来就通过这段代码,来分析我们的数据报文流到了哪里,并且哪里可以做一个更好的Hook点。

Http请求关键点的跟踪

先来明确一下我们的目标,要跟着目标来分析。我们要做的是一个Hook抓包对吧,所以我们就是想截获到APP进行HTTP(S)请求的明文数据,这个就是Hook抓包的一个核心点,因为我们需要对数据在任何明文状态下进行DUMP。那只需要跟着我们构造出来的数据包,看看它流向了哪里

OutputStream outputStream = socket.getOutputStream();
outputStream.write(sb.toString().getBytes());

调用了OutputStream类的write方法,把数据报文传了进去,但是这里的OutputStream是一个抽象类,所以我们要看outputStream对象的具体类是哪个,我们可以在这一行打个断点,能够直接看到当前对象的类型

我们可以看到,具体的实现类为SocketOutputStream类,那么我们就直接去看该类的write方法的一个实现

public void write(byte b[]) throws IOException {
    
    
 socketWrite(b, 0, b.length);
}

又调用了socketWrite方法

private void socketWrite(byte b[], int off, int len) throws IOException {
    
    
 if (len <= 0 || off < 0 || len > b.length - off) {
    
    
 if (len == 0) {
    
    
 return;
        }
 throw new ArrayIndexOutOfBoundsException("len == " + len
                + " off == " + off + " buffer length == " + b.length);
    }
 FileDescriptor fd = impl.acquireFD();
 try {
    
    
 // Android-added: Check BlockGuard policy in socketWrite.
 BlockGuard.getThreadPolicy().onNetwork();
 socketWrite0(fd, b, off, len);
    } catch (SocketException se) {
    
    
 if (se instanceof sun.net.ConnectionResetException) {
    
    
 impl.setConnectionResetPending();
            se = new SocketException("Connection reset");
        }
 if (impl.isClosedOrPending()) {
    
    
 throw new SocketException("Socket closed");
        } else {
    
    
 throw se;
        }
    } finally {
    
    
 impl.releaseFD();
    }
}

先对数据进行了一个越界和长度校验,如果长度为0直接return,如果访问越界就直接抛出异常。关键点为socketWrite0方法,在阅读源码的过程中,一定要掌握一个方法,带着目的去阅读源码,比如这里我们想观察数据的流向,所以我们需要关心的就是我们的数据在哪些方法之中进行了传递。来看socketWrite0的一个实现

private native void socketWrite0(FileDescriptor fd, byte[] b, int off,int len) throws IOException;

这里是一个native原生方法,我们就不继续往下跟了,因为在Java层的一个Socket数据最终会经过这里传向native层,所以我们Hook这个方法就能够实现对Java层Socket数据的DUMP,这里也是r0capture的第一个Hook点,我们来写一个代码测试一下

Frida Hook socketWrite0

首先我们要打印这个byte[]数据,在frida中如何对byte数组进行打印呢?可以借助framework层的一个工具类来帮助我们进行打印

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))
}

有了上面的hexdump方法,我们来继续hook socketWrite0方法

Java.use("java.net.SocketOutputStream").socketWrite0.implementation = function(fd,bytes,off,len){
    
    
 hexdump(bytes,off,len)
 this.socketWrite0(fd,bytes,off,len)
}

可以正常Hook到Http请求的报文并进行DUMP,也实现来我们的目标

Frida Hook socketRead0

有了上面的例子,我们能够抓到Http请求了,现在我们来看一下Http的返回数据。
我们可以按照上面找到socketWrite0方法同样的方式,找到socketRead0方法,它们是一对,我们猜也能够猜的到,所以直接就进行Hook一把试试

 Java.use("java.net.SocketInputStream").socketRead0.implementation = function(fd,bytes,off,len,timeout){
    
    
 var result =  this.socketRead0(fd,bytes,off,len,timeout)
 hexdump(bytes,off,len)
 return result
    }

也是没有问题的,能够Hook到。但是这里我们要处理一个问题,读取的数据如果超出buffer的长度,需要多次读取,也就会造成数据不连续。但是没有关系,因为我们多需要关注的是文本型数据,像一个图片之类的数据一般才会超过buffer的长度。而且此处的len永远是buffer的长度,所以这里我们需要特殊处理一下,result是一个int型数据,它表示的是当前读取的大小。

Java.use("java.net.SocketInputStream").socketRead0.implementation = function(fd,bytes,off,len,timeout){
    
    
 var result = this.socketRead0(fd,bytes,off,len,timeout)
 hexdump(bytes,off,result)
 return result
        }

这样我们的返回数据也就能够DUMP出来了,Http的数据部分就结束了。但是我们的r0capture还有两个重要的信息:

  • 打印调用栈
    调用栈的打印非常简单,我们直接来看实现,在需要的地方直接加上这个方法就可以打印当前方法的调用栈
function showStacks() {
    
    
 console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
}
  • 打印本机的地址和Server的地址
    这里我们可以通过查看SocketOutputStream类和SocketInputStream两个类,都有一个成员变量socket,保存的就是我们当前的socket连接,我们就可以直接通过socket这个变量来获取到Socket连接的两端地址
function printAddress(socket, isSend){
    
    
 var localAddress = socket.value.getLocalAddress().toString()
 var remoteAddress = socket.value.getRemoteSocketAddress().toString()
 if(isSend){
    
    
 console.log(localAddress +"====>"+ remoteAddress)
            }else{
    
    
 console.log(localAddress +"<===="+ remoteAddress)
            }
        }

至此,我们Http部分就分析完毕了,然后我们来看Https部分

使用Socket进行Https访问

private void requestHttps() {
    
    
 try {
    
    
 final String host = "www.taobao.com";
 final int port = 443;
 final String path = "/";

 SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
 SSLSocket socket = (SSLSocket) sslSocketFactory.createSocket(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();
    }
}

我们可以发现,跟Http不同:

  • 端口跟普通的端口不同,Http默认的端口是80端口,Https默认的是443端口
  • 使用SSLSocket,而不是Socket
  • SSLSocket是一个抽象类,继承自Socket类。所以不能直接new,需要使用SSLSocketFactory工厂类进行创建SSLSocket连接

Https请求关键点的跟踪

同样的,我们来下个断点观察一下此时outputStream对象的具体实现类

来看ConscryptFileDescriptorSocket类下的内部类SSLOutputStream类对应write方法的实现,我们发现该内部类没有实现我们所调用的write方法,那就是从父类继承过来的

public void write(byte b[]) throws IOException {
    
    
 write(b, 0, b.length);
}

还是调用了三个参数的write方法,SSLOutputStream类对该方法进行了重写,我们来看实现

http://aospxref.com/android-8.1.0_r81/xref/external/conscrypt/common/src/main/java/org/conscrypt/ConscryptFileDescriptorSocket.java

public void write(byte[] buf, int offset, int byteCount) throws IOException {
    
    
 Platform.blockGuardOnNetwork();
 checkOpen();
 ArrayUtils.checkOffsetAndCount(buf.length, offset, byteCount);
 if (byteCount == 0) {
    
    
 return;
    }

 synchronized (writeLock) {
    
    
 synchronized (stateLock) {
    
    
 if (state == STATE_CLOSED) {
    
    
 throw new SocketException("socket is closed");
            }
 if (DBG_STATE) {
    
    
 assertReadableOrWriteableState();
            }
        }

 ssl.write(Platform.getFileDescriptor(socket), buf, offset, byteCount,
                writeTimeoutMilliseconds);

 synchronized (stateLock) {
    
    
 if (state == STATE_CLOSED) {
    
    
 throw new SocketException("socket is closed");
            }
        }
    }
}

关键方法ssl.write。第一个参数是我们socket的一个文件描述符,后面几个参数不需要过多介绍了,多了一个超时的参数,我们继续看该方法的实现

http://aospxref.com/android-8.1.0_r81/xref/external/conscrypt/common/src/main/java/org/conscrypt/SslWrapper.java

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

又调用了NativeCrypto.SSL_write方法,继续往下跟

Frida Hook SSL_write方法

http://aospxref.com/android-8.1.0_r81/xref/external/conscrypt/common/src/main/java/org/conscrypt/NativeCrypto.java

static native void SSL_write(long sslNativePointer, FileDescriptor fd, SSLHandshakeCallbacks shc, byte[] b, int off, int len, int writeTimeoutMillis) throws IOException;

该方法是一个native方法,我们还是无法往下跟了,最终https的数据走到这里,中间也无任何将数据进行加密的环节,所以我们的Https的数据在这里还是一个明文的。至于native层如何处理的,也就无非是我们所说的https的一个ssl层处理,所以这个点就是一个Https的数据还在明文状态下的,我们在Java层能找到的最后的点,我们来Hook这个方法。但是这里要注意一点,该类的包名不能直接使用,需要加上com.android前缀,至于为什么笔者也不清楚,估计是在源码编译的时候,把这个模块下的所有包名都自动加了一个前缀。至于为什么知道是加了com.android前缀呢,也是靠上面断点那张图,com.android.org.conscrypt.ConscryptFileDescriptorSocket,该类的全限定类名前面就有一个com.android,而在阅读源码的过程中,同样是没有这个前缀的。还有就是我使用Objection从内存中搜索了一下类名,确实是有这个前缀的。那么我们来继续Hook这个方法

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

其他的无需介绍,我们来看一下printHttpsAddress方法

function printHttpsAddress(fd, isSend){
    
    
 var local = Socket.localAddress(fd.getInt$())
 var peer = Socket.peerAddress(fd.getInt$())
 if(isSend){
    
    
 console.log(local.ip+":"+local.port +"====>"+ peer.ip+":"+peer.port)
    }else{
    
    
 console.log(local.ip+":"+local.port +"====>"+ peer.ip+":"+peer.port)
    }
}

这个跟前面的解析方法不一样,NativeCrypto类没有socket这个成员变量,而我们从分析的过程中可以看到,到了这个SSL_write方法的时候,跟socket有关的参数就只有一个sslNativePointer和fd,这里我们就是通过这个fd,调用了frida提供的API,来帮助我们解析到local跟peer,从而实现了打印地址的功能

Frida Hook SSL_read方法

write跟read都是成对出现的

static native int SSL_read(long sslNativePointer, FileDescriptor fd, SSLHandshakeCallbacks shc, byte[] b, int off, int len, int readTimeoutMillis) throws IOException;

直接来Hook这个方法

Java.use("com.android.org.conscrypt.NativeCrypto").SSL_read.implementation = function(sslNativePointer,fd,shc,bytes,off,len,timeout){
    
    
 var result =  this.SSL_read(sslNativePointer,fd,shc,bytes,off,len,timeout)
 printHttpsAddress(fd,false)
 hexdump(bytes,off,len)
 showStacks()
 return result
}

总结

至此我们就分析完了r0capture抓包功能的四个Hook点是怎么来的,都是从一个底层的socket出发,进行Http(s)的请求,然后跟着数据一步一步往下跟,这也是我们静态分析的一个流程。我们从找关键点的过程中也能够发现r0capture的弊端,只要不经过这四个方法的数据,我们都无法进行抓取。我们看一下r0ysue在github中对r0capture的局限总结:

部分开发实力过强的大厂或框架,采用的是自身的SSL框架,比如WebView、小程序或Flutter,这部分目前暂未支持。部分融合App本质上已经不属于安卓App,没有使用安卓系统的框架,无法支持。当然这部分App也是少数。暂不支持HTTP/2、或HTTP/3,该部分API在安卓系统上暂未普及或布署,为App自带,无法进行通用hook。各种模拟器架构、实现、环境较为复杂,建议珍爱生命、使用真机。

这只是一种抓包思想的实现,有它方便的地方,也有它的弱势。所以我们要从我们的目的出发,发现什么工具更适合我们的需求,才能高效率的完成作业。

如果本篇文章对你有帮助,欢迎大家加一下我们的星球,不定时分享各种技术,总有一个对你有帮助,感谢您的阅读

猜你喜欢

转载自blog.csdn.net/u010559109/article/details/120082066