吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 6746|回复: 22
收起左侧

[Android 原创] 大数据安全入门安卓r0capture的源码关键点跟踪

[复制链接]
roysue 发表于 2021-9-3 14:38
  • 手机环境:安卓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。那只需要跟着我们构造出来的数据包,看看它流向了哪里

1.

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

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

01.png

我们可以看到,具体的实现类为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,也实现来我们的目标
02.png

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对象的具体实现类

03.png

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

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

不要加推广

免费评分

参与人数 15威望 +2 吾爱币 +112 热心值 +13 收起 理由
jb007 + 1 热心回复!
3303232005 + 1 + 1 热心回复!
小飞侠666 + 1 我很赞同!
victos + 1 + 1 谢谢@Thanks!
#sky# + 1 + 1 热心回复!
gaosld + 1 + 1 谢谢@Thanks!
公过水蚊 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
ogli324 + 1 + 1 肉丝大佬又来收割精华优秀贴了
fengbolee + 2 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
yixi + 1 + 1 谢谢@Thanks!
qtfreet00 + 2 + 100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
叶隽 + 1 热心回复!
chinawolf2000 + 1 + 1 热心回复!
悦来客栈的老板 + 1 我很赞同!
lingyun011 + 1 我很赞同!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

CCSYJQ 发表于 2021-9-3 17:00
6666666666
wqs0987 发表于 2021-9-4 08:47
不一般 发表于 2021-9-4 20:48
stilllove88 发表于 2021-9-4 21:10
好难啊 太难了
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
大佬厉害了
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-28 10:17

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表