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

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

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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Frida是一款免费的,基于Python和JavaScript来实现的,面向开发人员、逆向工程师和安全研究人员的动态检测工具包。 Frida拥有一套全面的测试套件,不但调试效率极高,而且在广泛的使用中经历了多年严格的测试。 尤其是,移动应用安全测试和服务巨头NowSecure对齐钟爱有加,在NowSecure内部,安全人员通过Frida这个工具套装,已经完成对大量的移动应用程序大规模深度的安全分析测试。目前依然在该公司的安全测试中扮演重要的角色。 基于Python和JavaScript的Frida,天生就是跨平台的动态调试工具,不但可以运行在Windows、Linux、macOS之上,而且还可以调试Windows应用程序、Linux应用程序,macOS、iOS、Andriod和QNX等几乎全平台的应用程序。可以说,一旦掌握Frida这套工具,就可以在全平台,对全平台的应用程序进行动态调试和分析。 Frida使用极其方便,在使用过程中,只需将你编写的JavaScript脚本通过Frida自身的工具注入到目标进程中,就可以HOOK任何功能,其中包括但不限于监视加密API或跟踪应用程序关键代码等。在使用过程中,无需知道被“研究”程序的源代码。 尤其是可以一边编辑JavaScript脚本,一边运行JavaScript脚本的功能对于调试分析来说极为友好。只需“保存”正在编辑的JavaScript脚本,就立即就能看到该脚本执行的结果,全称无需其它人工介入,也无需重新启动被“研究”的应用程序,极大地简化了分析流程,同时也极大地提高了工作效率。因此,得到了众多安全分析人士的青睐。 本课程从最基本的调试环境搭建开始,基于经典的Windows“扫雷”游戏的动态调试分析,编码等,循序渐进演示Firda在分析调试Windows应用程序中基本使用方法和技巧。拥有这些知识储备之后,在加上官方的参考文档,你就可以轻松地将这些知识“迁移”至分析和调试其他平台的应用程序。 课程资料,请看第一课中github链接。
使用python中的pymsql完成如下:表结构与数据创建 1. 建立 `users` 表和 `orders` 表。 `users` 表有用户ID、用户名、年龄字段,(id,name,age) `orders` 表有订单ID、订单日期、订单金额,用户id字段。(id,order_date,amount,user_id) 2 两表的id作为主键,`orders` 表用户id为users的外键 3 插入数据 `users` (1, '张三', 18), (2, '李四', 20), (3, '王五', 22), (4, '赵六', 25), (5, '钱七', 28); `orders` (1, '2021-09-01', 500, 1), (2, '2021-09-02', 1000, 2), (3, '2021-09-03', 600, 3), (4, '2021-09-04', 800, 4), (5, '2021-09-05', 1500, 5), (6, '2021-09-06', 1200, 3), (7, '2021-09-07', 2000, 1), (8, '2021-09-08', 300, 2), (9, '2021-09-09', 700, 5), (10, '2021-09-10', 900, 4); 查询语句 1. 查询订单总金额 2. 查询所有用户的平均年龄,并将结果四舍五入保留两位小数。 3. 查询订单总数最多的用户的姓名和订单总数。 4. 查询所有不重复的年龄。 5. 查询订单日期在2021年9月1日至9月4日之间的订单总金额。 6. 查询年龄不大于25岁的用户的订单数量,并按照降序排序。 7. 查询订单总金额排名前3的用户的姓名和订单总金额。 8. 查询订单总金额最大的用户的姓名和订单总金额。 9. 查询订单总金额最小的用户的姓名和订单总金额。 10. 查询所有名字中含有“李”的用户,按照名字升序排序。 11. 查询所有年龄大于20岁的用户,按照年龄降序排序,并只显示前5条记录。 12. 查询每个用户的订单数量和订单总金额,并按照总金额降序排序。
06-03
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值