roysue 发表于 2021-9-3 14:38

大数据安全入门安卓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访问的例子:
```java
//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;
      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。那只需要跟着我们构造出来的数据包,看看它流向了哪里

1.
```java
OutputStream outputStream = socket.getOutputStream();
outputStream.write(sb.toString().getBytes());
```
调用了OutputStream类的write方法,把数据报文传了进去,但是这里的OutputStream是一个抽象类,所以我们要看outputStream对象的具体类是哪个,我们可以在这一行打个断点,能够直接看到当前对象的类型



我们可以看到,具体的实现类为SocketOutputStream类,那么我们就直接去看该类的write方法的一个实现
```java
public void write(byte b[]) throws IOException {
      socketWrite(b, 0, b.length);
}
```
又调用了socketWrite方法
```java
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的一个实现
```java
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层的一个工具类来帮助我们进行打印
```javascript
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方法
```javascript
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一把试试

```javascript
    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型数据,它表示的是当前读取的大小。
```javascript
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还有两个重要的信息:
- 打印调用栈
调用栈的打印非常简单,我们直接来看实现,在需要的地方直接加上这个方法就可以打印当前方法的调用栈

```javascript
function showStacks() {
    console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
}
```

- 打印本机的地址和Server的地址
这里我们可以通过查看SocketOutputStream类和SocketInputStream两个类,都有一个成员变量socket,保存的就是我们当前的socket连接,我们就可以直接通过socket这个变量来获取到Socket连接的两端地址

```javascript
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访问

```java
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;
      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方法,那就是从父类继承过来的

```java
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

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

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

```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这个方法

```javascript
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方法

```java
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都是成对出现的

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

直接来Hook这个方法

```java
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。各种模拟器架构、实现、环境较为复杂,建议珍爱生命、使用真机。

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

不要加推广

CCSYJQ 发表于 2021-9-3 17:00

6666666666

wqs0987 发表于 2021-9-4 08:47

感谢大佬的无私分享

不一般 发表于 2021-9-4 20:48

能做个窗口可视化工具,那就方便多了。

stilllove88 发表于 2021-9-4 21:10

{:1_896:}好难啊 太难了

goda 发表于 2021-9-4 22:10

谢谢分享

way226510 发表于 2021-9-13 09:35

感谢大佬的无私分享,学到了

xdnice 发表于 2021-9-14 11:45

谢谢分享,受教了。

C2021 发表于 2021-9-14 12:10

谢谢分享

xiaoyudengyu 发表于 2021-9-14 15:14

大佬厉害了
页: [1] 2 3
查看完整版本: 大数据安全入门安卓r0capture的源码关键点跟踪