frida沙箱自吐实现

篇幅有限

完整内容及源码关注公众号:ReverseCode,发送

沙箱

沙箱:对于系统来说,单个APP是没有隐私的,不管是脱壳、还是收发包,都是由系统的API来执行的。HOOK系统的API,就能得到很多APP的关键信息。

APP想要对抗沙箱:

  1. 尽可能减少系统API的调用;
  2. 尽可能自己实现一定量的算法;
  3. 对自己实现的算法进行强混淆;
  4. 增加自身算法的复杂度吧:VMP

各大安全公司、杀毒软件公司基本上都会有自己的沙箱,只要病毒/木马在自己的沙箱跑一遍,直接得到执行流、病毒相似性分析,如绑绑安全的安全密钥白盒,对于APP也是一样的。

基于hook的沙箱

Youpk Fart 都是沙箱,由于基于系统本身基本无法对抗。

appmon

appmon wiki

1
2
3
4
5
6
7
8
9
10
11
12
./fs128arm64 
vim /etc/proxychains4.conf
socks5 192.168.0.107 1080 # 电脑主机 ssr选项设置-开启来自局域网的连接
PYTHON_CONFIGURE_OPTS="--disable-ipv6" proxychains4 pyenv install 3.8.2
PYTHON_CONFIGURE_OPTS="--disable-ipv6" proxychains4 pip install frida==12.8.0
PYTHON_CONFIGURE_OPTS="--disable-ipv6" proxychains4 pip install frida-tools==5.3.0
PYTHON_CONFIGURE_OPTS="--disable-ipv6" proxychains4 pip install objection==1.8.4
proxychains wget https://github.com/dpnishant/appmon/archive/refs/heads/master.zip
7z x master.zip
cd appmon-master
pip install argparse flask termcolor dataset --upgrade --ignore-installed six
python appmon.py -a "com.xiaojianbang.app" -p android -s scripts/Android

点击HookTestDemo.apk的算法加密按钮,触发生成./app_dumps/com.xiaojianbang.app.db

访问http://127.0.0.1:5000/ 选择com.xiaojianbang.app

image-20210506003248713

由于显示内容都是[Object Object],修改源码打印hook内容。

1
2
3
4
5
data.value = byteArraytoHexString(digest);  删除
var ByteString = Java.use("com.android.okhttp.okio.ByteString"); 替换
data.value = ByteString.of(digest).hex()

frida -UF -l Hash.js

image-20210506003840372

基于源码的沙箱

aosp810r1 解压
驱动Vendor image 驱动GPS, Audio, Camera, Gestures, Graphics, DRM, Video, Sensors 解压驱动
解压到aosp810r1中后./extrace-google_devices-sailfish.sh./extrace-qcom-sailfish.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# apt update
# git config --global user.email "you@example.com"
# git config --global user.name "Your Name"
# apt install bison tree
# dpkg --add-architecture i386
# apt update
# apt install libc6:i386 libncurses5:i386 libstdc++6:i386
# apt install libxml2-utils

dd if=/dev/zero of=swapfile bs=1024 count=10240000 使用dd创建swapfile作为swap分区空间
mkswap swapfile mkswap创建交换文件

Kali下手动安装openjdk-8-jdk:
# wget http://http.kali.org/pool/main/o/openjdk-8/openjdk-8-jdk-headless_8u212-b01-1_amd64.deb
# dpkg -i openjdk-8-jdk-headless_8u212-b01-1_amd64.deb
# wget http://http.kali.org/pool/main/o/openjdk-8/openjdk-8-jdk_8u212-b01-1_amd64.deb
# dpkg -i openjdk-8-jdk_8u212-b01-1_amd64.deb
安装完成后再用:
# update-alternatives --config java
# update-alternatives --config javac
选择2来切换jdk的版本:见图
最后用version选项来确认版本:
# java -version
# javac -version
source build/envsetup.sh
lunch 选择24 aosp_sailfish-userdebug
make 编译完成的系统镜像位于当前目录的out/target/product/sailfish/下包括各个img

官方镜像 下载下来后解压将上面编译好的所有img替换到官方镜像解压后的image-sailfish-opm1.171019.011文件夹,并还原创建zip包

1
2
adb reboot bootloader
./flash-all.sh

Android Studio 导入 AOSP 源码 development/tools/idegen/idegen.sh

会在根目录下生成
android.iml 和 android.ipr 这两个文件,这两个文件是 Android Studio 的工程配置文件,这时候其实已经可以直接导入 Android Studio,但会导入所有的源码模块,会很慢,可以进行过滤,除了 frameworks 模块和 packages 模块,其他都给过滤掉,不导入 Android Studio,打开 android.iml 文件,搜下excludeFolder,在后面加入如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<excludeFolder url="file://$MODULE_DIR$/art" />
<excludeFolder url="file://$MODULE_DIR$/bionic" />
<excludeFolder url="file://$MODULE_DIR$/bootable" />
<excludeFolder url="file://$MODULE_DIR$/build" />
<excludeFolder url="file://$MODULE_DIR$/cts" />
<excludeFolder url="file://$MODULE_DIR$/dalvik" />
<excludeFolder url="file://$MODULE_DIR$/developers" />
<excludeFolder url="file://$MODULE_DIR$/development" />
<excludeFolder url="file://$MODULE_DIR$/device" />
<excludeFolder url="file://$MODULE_DIR$/docs" />
<excludeFolder url="file://$MODULE_DIR$/external" />
<excludeFolder url="file://$MODULE_DIR$/hardware" />
<excludeFolder url="file://$MODULE_DIR$/kernel" />
<excludeFolder url="file://$MODULE_DIR$/libcore" />
<excludeFolder url="file://$MODULE_DIR$/libnativehelper" />
<excludeFolder url="file://$MODULE_DIR$/out" />
<excludeFolder url="file://$MODULE_DIR$/pdk" />
<excludeFolder url="file://$MODULE_DIR$/platform_testing" />
<excludeFolder url="file://$MODULE_DIR$/prebuilts" />
<excludeFolder url="file://$MODULE_DIR$/sdk" />
<excludeFolder url="file://$MODULE_DIR$/system" />
<excludeFolder url="file://$MODULE_DIR$/test" />
<excludeFolder url="file://$MODULE_DIR$/toolchain" />
<excludeFolder url="file://$MODULE_DIR$/tools" />
<excludeFolder url="file://$MODULE_DIR$/.repo" />

发现 Android Studio 不停 scanning files to index,我的强迫症又犯了,解决:

  1. invalidate and restart 不起作用;
  2. 右击项目 –> Open module setting –> Modules –> 找到 gen 文件夹 –> 右键选择 Resources,终于告别烦人的 scanning files to index。

修改MessageDigest.java

crypto_filter_aosp

基于android6.0.1 Nexus 6P ROM,系统底包

先刷官方原版底包,老版本使用fastboot6.0放到kali的/root/Android/Sdk/plateform-tools,flash-all.sh

  1. 手机先刷入fastbboot flash recovery twrp
  2. 下载rom解压,adb push ROM/ /sdcard/TWRP/BACKUPS
  3. 进入twrp,从备份中恢复Restore,重启手机,然后修改权限 chmod 777 /data/local/tmp/monitor_package
  4. 安装你需要监控的apk(系统自动把最后一次安装的apk添加进去监控的列表 /data/local/tmp/monitor_package),只能同时监控一个adb install HookTestDemo.apk
  5. /data/data/package_name/下面生成APK调用的算法,只有三种(数据均为JSON编码,字段为BASE64编码)/data/data/com.xiaojianbang.app

参考crypto_filter_aosp文件夹源码添加到aosp810r1的源码中,将MyUtil.java,ContextHolder.java,AndroidBase64.java,Cipher.java放到aosp810r1/libcore/ojluni/src/main/java/javax/crypto

将参考20200212/MessageDigest.java代码实现到aosp810r1的MessageDigest.java。同理,修改Mac.java

openjdk_java_files.mk添加新增的需要编译的类

1
2
3
ojluni/src/main/java/javax/crypto/Mac.java \
ojluni/src/main/java/javax/crypto/ContextHolder.java \
ojluni/src/main/java/javax/crypto/MyUtil.java \
1
2
3
source build/envsetup.sh
lunch aosp_sailfish-user
make 如报错make update-api

编译完成后将编译好的img压缩成image-sailfish-opm1.17019.011.zip放到官方系统底包,./flash-all.bat刷机

AOSP网络库自吐

适用于沙箱的原则:我们要可以在安卓源码中找到其实现、彻底的修改其实现。

App开发实力越强,App自己实现的内容越多,对系统的依赖程度越低,沙箱的作用就越小。→ 沙箱只能帮助定位到关键的点,如何把内容解开还是分析自己实现的部分。

为了能抓到包,无数安全研究人员使出浑身解数,我们可以按照OSI七层模型或TCP/IP四层模型。

  1. 我们在谈论MAC地址/ARP的时候,我们聊的就是链路层;
  2. 我们在谈论IP地址/路由器的时候,我们聊的就是网络层;
  3. 我们在谈论连接某个端口的时候,我们聊的就是传输层;
  4. 我们在谈论发送数据的内容的时候,我们聊的就是应用层;

img

应用层/Application:基于中间人的HTTP(S)抓包

  • 该方法继承于网页端的抓包,只不过对抗性全面强化;在设计网站时无法控制客户端,但是App确是可以被厂商全面控制的;
  • 在客户端校验服务器证书的情况下,需要将抓包软件(推荐Charles)的证书置于手机根证书目录下,推荐Magisk插件Move Certificates
  • 在服务器验证客户端证书的情况下,还需要在App中dump出证书导入到Charles中,这就涉及到证书密码和证书的解密;
  • App使用特定API,绕过WIFI代理进行通信→ 使用VPN将所有流量导入到Charles → App还会检测VPN,发现即断网 → 需要hook过VPN检测;

img

哪些是可以改的:(沙箱在辅助中间人抓包的过程中发挥的作用)

  • App使用SSL pinning,只信任自己的证书 → 从数十种框架中找到hook点并绕过 → App进行了代码混淆 → 反混淆并hook绕过,而反混淆总是让人倒吸一口凉气。。。

img

  • 由于厂商可以全面控制客户端,因此可以使用小众协议,比如WebSocket、Protobuf,甚至自己写协议,比如腾讯的JceStruct,此时除了自己分析协议字段别无他法

传输层/Transport:App使用纯Socket通信

  • 比如某应用的数据采用点对点纯Socket的tcp通信,此时只有dump其通信流量,分析其raw data,结合源码分析字段构成;
  • 某厂商开创性地提出了自建代理长连通道的网络加速方案,App中绝大部分的请求通过CIP通道中的TCP子通道与长连服务器通信,长连服务器将收到的请求代理转发到业务服务器,对于业务来讲大大提高了效率,但是对于逆向来说却加大了抓包的难度。

img

  • 也幸亏其SDK中包含了降级方案,可以hook某些关键函数实现降级到HTTP,给了安全研究员一口饭吃。更有大厂已经在通讯标准演进的路线上大步快跑,在目前HTTP/2都没有普及的情况下,受益于相比于网页端而言、App客户端全面可控的优势,提前迈入HTTP/3时代,在性能优化的KPI上一骑绝尘而去,从内核、算法、传输层网络库和服务端全部自研。

img

面对连抓包工具都没有提供支持的kQUIC,逆向分析者只能说欲哭无泪。同样还是幸亏SDK中包含了plan B降级方案,可以通过hook来进行降级,安全研究员续命一秒钟。

网络层/Network:一般而言鲜有App可以更改设备的IP地址

  • 科学上网软件、VPN可以改手机的路由表,因此可以用来抓包;
  • 可以自建路由器进行抓包,对手机完全无侵入、无感知,彻底搞定抓不到包!

img

  • 缺点是加密内容也无法还原,可以dump流量,却无法解密内容;在手机端连标准的SSL也解不开。也可以在手机上安装使用Kali Nethunter,在手机上直接跑Wireshark,接在4G流量卡上进行抓包,这种方式甚至可以抓到手机的流量卡的网卡包,应该是目前已知的唯一抓流量卡的方法。

img

应用层抓包通杀脚本

  1. App在开发过程中,以App自己的权限,可以用代码实现到的最底层为传输层,也就用Socket接口,进行纯二进制的收发包,此处包括Java层和Native层。
  2. 除了少数开发实力雄厚甚至过剩的大厂,掌握着纯二进制收发包的传输层创新、或者自定义协议的技术之外,占绝对数量绝大多数的App厂商采用的还是传统的HTTP/SSL方案。

而且占绝对数量中绝大多数的App,其实现HTTP/SSL的方案也是非常的直白,那就是调用系统的API,或者调用更加易用的网络框架,比如访问网站的Okhttp框架,播放视频的Exoplayer,异步平滑图片滚动加载框架Glide,对于非网络库或协议等底层开发者来说,这些才应当是普罗大众安卓应用开发者的日常。

所以我们在对JavaSocket接口进行trace之后打调用栈,即可清晰地得出从肉眼可见的视频、到被封装成HTTP包、再到进入SSL进行加解密,再通过Socket与服务器进行通信的完整过程。

img

只要开发者使用了应用层框架,即无法避免的使用了系统的Socket进行了收发,如果是HTTP则直接走了Socket,没有加解密、直接是明文,将内容dump下来即可;如果走了HTTPS,那么HTTP包还要“裹上”一层SSL,通过SSL的接口进行收发,SSL则将加密后和解密前的数据走Socket与服务器进行通信,明文数据只有SSL库自己知道。

img

因此想要得到SSL加密前和解密后的HTTP数据的话,就要对SSL库有深入的研究,而像这种大型的、历史悠久的基础库,研究它的人是非常多的;比如谷歌就有研究员对OpenSSL的收发包接口进行了深入的研究,并对其收发包等接口使用frida进行hook,提取明文HTTP数据,最终的成品为ssl_logger项目;因为这种库一般作为互联网世界架构的基础设施,所以其应用非常广泛,这也是为何当其暴漏出“心脏滴血”漏洞时,几乎影响到所有互联网设备的原因,不管是LinuxMacos/iOS、还是安卓,使用的都是OpenSSL,刚刚我们trace到的SSLInputStream.read函数,充其量只是OpenSSL库在Java层的一个包装器罢了。

img

img

而又有来自阿里的巨佬,在使用的过程中,进一步优化了该项目的JS脚本,修复了在新版frida上的语法错误,并在原项目只支持LinuxmacOS的基础上,增加了对iOSAndroid的支持,最终的成品就是frida_ssl_logger项目

该项目的完成度已经非常高,其核心原理就是对SSL_readSSL_write进行hook,得到其收发包的明文数据。

1
2
[Process.platform == "darwin" ? "*libboringssl*" : "*libssl*", ["SSL_read", "SSL_write", "SSL_get_fd", "SSL_get_session", "SSL_SESSION_get_id"]], // for ios and Android
[Process.platform == "darwin" ? "*libsystem*" : "*libc*", ["getpeername", "getsockname", "ntohs", "ntohl"]]

并将明文数据使用RPC传输到电脑上,使用hexdumppython的控制台进行输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
if verbose:
src_addr = socket.inet_ntop(socket.AF_INET,
struct.pack(">I", p["src_addr"]))
dst_addr = socket.inet_ntop(socket.AF_INET,
struct.pack(">I", p["dst_addr"]))
print("SSL Session: " + p["ssl_session_id"])
print("[%s] %s:%d --> %s:%d" % (
p["function"],
src_addr,
p["src_port"],
dst_addr,
p["dst_port"]))
hexdump.hexdump(data)

或者保存至pcap文件,以供后续进一步分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
def log_pcap(pcap_file, ssl_session_id, function, src_addr, src_port,
dst_addr, dst_port, data):
"""Writes the captured data to a pcap file.
Args:
pcap_file: The opened pcap file.
ssl_session_id: The SSL session ID for the communication.
function: The function that was intercepted ("SSL_read" or "SSL_write").
src_addr: The source address of the logged packet.
src_port: The source port of the logged packet.
dst_addr: The destination address of the logged packet.
dst_port: The destination port of the logged packet.
data: The decrypted packet data.
"""
t = time.time()

if ssl_session_id not in ssl_sessions:
ssl_sessions[ssl_session_id] = (random.randint(0, 0xFFFFFFFF),
random.randint(0, 0xFFFFFFFF))
client_sent, server_sent = ssl_sessions[ssl_session_id]

if function == "SSL_read":
seq, ack = (server_sent, client_sent)
else:
seq, ack = (client_sent, server_sent)

for writes in (
# PCAP record (packet) header
("=I", int(t)), # Timestamp seconds
("=I", int((t * 1000000) % 1000000)), # Timestamp microseconds
("=I", 40 + len(data)), # Number of octets saved
("=i", 40 + len(data)), # Actual length of packet
# IPv4 header
(">B", 0x45), # Version and Header Length
(">B", 0), # Type of Service
(">H", 40 + len(data)), # Total Length
(">H", 0), # Identification
(">H", 0x4000), # Flags and Fragment Offset
(">B", 0xFF), # Time to Live
(">B", 6), # Protocol
(">H", 0), # Header Checksum
(">I", src_addr), # Source Address
(">I", dst_addr), # Destination Address
# TCP header
(">H", src_port), # Source Port
(">H", dst_port), # Destination Port
(">I", seq), # Sequence Number
(">I", ack), # Acknowledgment Number
(">H", 0x5018), # Header Length and Flags
(">H", 0xFFFF), # Window Size
(">H", 0), # Checksum
(">H", 0)): # Urgent Pointer
pcap_file.write(struct.pack(writes[0], writes[1]))
pcap_file.write(data)

if function == "SSL_read":
server_sent += len(data)
else:
client_sent += len(data)
ssl_sessions[ssl_session_id] = (client_sent, server_sent)

由于完成度已经相当高了,在构建安卓应用层抓包通杀脚本时,应当尽可能复用其已经实现好的“基础设施”,只要为其再补上明文数据即可,而这明文数据从哪里来?根据多轮trace可以得知,明文数据的收发包接口,正是由java.net.SocketOutputStream.socketWrite0java.net.SocketInputStream.socketRead0这两个API负责的,当然其实二者还有很多上层调用的接口,在选择分析的接口时,应尽量选择离native层更近的、并且在更多安卓版本上适用的,比如这两个API在安卓7、8、9、10上是通用和不变的,以降低工作量。

最后的任务就是与SSL_readSSL_write一样,根据收发的函数、找到收发的IP地址和端口,而正好两个API均有socket的实例域,提供了收发包的IP地址和端口信息。

img

最终就是取出这些信息,构造与SSL一样发给电脑即可,需要注意的是Java[B需要手动转化成JavaScriptByteArray还是略微复杂的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
if (Java.available) {
Java.perform(function () {
Java.use("java.net.SocketOutputStream").socketWrite0.overload('java.io.FileDescriptor', '[B', 'int', 'int').implementation = function (fd, bytearry, offset, byteCount) {
var result = this.socketWrite0(fd, bytearry, offset, byteCount);
var message = {};
message["function"] = "HTTP_send";
message["ssl_session_id"] = "";
message["src_addr"] = ntohl(ipToNumber((this.socket.value.getLocalAddress().toString().split(":")[0]).split("/").pop()));
message["src_port"] = parseInt(this.socket.value.getLocalPort().toString());
message["dst_addr"] = ntohl(ipToNumber((this.socket.value.getRemoteSocketAddress().toString().split(":")[0]).split("/").pop()));
message["dst_port"] = parseInt(this.socket.value.getRemoteSocketAddress().toString().split(":").pop());
var ptr = Memory.alloc(byteCount);
for (var i = 0; i < byteCount; ++i)
Memory.writeS8(ptr.add(i), bytearry[offset + i]);
send(message, Memory.readByteArray(ptr, byteCount))
return result;
}
Java.use("java.net.SocketInputStream").socketRead0.overload('java.io.FileDescriptor', '[B', 'int', 'int', 'int').implementation = function (fd, bytearry, offset, byteCount, timeout) {
var result = this.socketRead0(fd, bytearry, offset, byteCount, timeout);
var message = {};
message["function"] = "HTTP_recv";
message["ssl_session_id"] = "";
message["src_addr"] = ntohl(ipToNumber((this.socket.value.getRemoteSocketAddress().toString().split(":")[0]).split("/").pop()));
message["src_port"] = parseInt(this.socket.value.getRemoteSocketAddress().toString().split(":").pop());
message["dst_addr"] = ntohl(ipToNumber((this.socket.value.getLocalAddress().toString().split(":")[0]).split("/").pop()));
message["dst_port"] = parseInt(this.socket.value.getLocalPort());
if (result > 0) {
var ptr = Memory.alloc(result);
for (var i = 0; i < result; ++i)
Memory.writeS8(ptr.add(i), bytearry[offset + i]);
send(message, Memory.readByteArray(ptr, result))
}
return result;
}
})
}

One more thing,虽然直接调用native层Socket的应用框架几乎没有;但是Javs层的Socket API是可以进一步下沉到C层的Socket,以支援so文件的socket抓包。以java.net.SocketOutputStream.socketWrite0举例,其native层的实现为JNIEXPORT void JNICALL 55SocketOutputStream_socketWrite0(JNIEnv *env, jobject this,jobject fdObj,jbyteArray data,jint off, jint len)地址),其核心为一句话int n = NET_Send(fd, bufP + loff, llen, 0);,进一步追踪NET_Send可以在linux_close.cpp文件中找到其实现(地址),本质上也是libcsend、sendto、recv、recvfrom这些,因此可以直接hook这些接口,捕获该进程的所有通信流量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
int NET_Read(int s, void* buf, size_t len) {
BLOCKING_IO_RETURN_INT( s, recv(s, buf, len, 0) );
}

int NET_ReadV(int s, const struct iovec * vector, int count) {
BLOCKING_IO_RETURN_INT( s, readv(s, vector, count) );
}

int NET_RecvFrom(int s, void *buf, int len, unsigned int flags,
struct sockaddr *from, int *fromlen) {
socklen_t socklen = *fromlen;
BLOCKING_IO_RETURN_INT( s, recvfrom(s, buf, len, flags, from, &socklen) );
*fromlen = socklen;
}

int NET_Send(int s, void *msg, int len, unsigned int flags) {
BLOCKING_IO_RETURN_INT( s, send(s, msg, len, flags) );
}

int NET_WriteV(int s, const struct iovec * vector, int count) {
BLOCKING_IO_RETURN_INT( s, writev(s, vector, count) );
}

int NET_SendTo(int s, const void *msg, int len, unsigned int
flags, const struct sockaddr *to, int tolen) {
BLOCKING_IO_RETURN_INT( s, sendto(s, msg, len, flags, to, tolen) );
}

int NET_Accept(int s, struct sockaddr *addr, int *addrlen) {
socklen_t socklen = *addrlen;
BLOCKING_IO_RETURN_INT( s, accept(s, addr, &socklen) );
*addrlen = socklen;
}

int NET_Connect(int s, struct sockaddr *addr, int addrlen) {
BLOCKING_IO_RETURN_INT( s, connect(s, addr, addrlen) );
}

#ifndef USE_SELECT
int NET_Poll(struct pollfd *ufds, unsigned int nfds, int timeout) {
BLOCKING_IO_RETURN_INT( ufds[0].fd, poll(ufds, nfds, timeout) );
}
#else
int NET_Select(int s, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout) {
BLOCKING_IO_RETURN_INT( s-1,
select(s, readfds, writefds, exceptfds, timeout) );
}

只是如果hook native层的这些接口的话,会混进openssl/boringssl的经过加密的流量,届时会比较难以区分,所以其实duck不必下降到native层,Java层的通信足以覆盖99%以上的场景(这个百分比是我估计的)。

最终也就是现在的效果:r0capture:安卓应用层抓包通杀脚本,地址:https://github.com/r0ysue/r0capture

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

用法

  • Spawn 模式:
1
$ python3 r0capture.py -U -f com.qiyi.video
  • Attach 模式,抓包内容保存成pcap文件供后续分析:
1
$ python3 r0capture.py -U com.qiyi.video -p iqiyi.pcap

建议使用Attach模式,从感兴趣的地方开始抓包,并且保存成pcap文件,供后续使用Wireshark进行分析。

PS:用来抓注册包,效果尤佳。

img

To-do:

  1. 此处还是有部分开发实力过强的大厂或框架,采用的是自身的SSL框架,比如WebView、小程序Flutter,这部分目前暂未支持。当然这部分App也是少数。
  2. 暂不支持HTTP/2、或HTTP/3,该部分API在安卓系统上暂未普及或布署,为App自带,无法进行通用hook。
  3. 各种模拟器架构、实现、环境较为复杂,建议珍爱生命、使用真机。
  4. 暂未添加多进程支持,比如:service或:push等子进程,可以使用Frida的Child-gating来支持一下。
  5. 支持多进程之后要考虑pcap文件的写入锁问题,可以用frida-toolReactor线程锁来支持一下。

TCP/IP中可以实现的部分:

  • 网络层:可以拿到所有的收发包。效果同Wireshark。如果是明文,其实效果跟传输层是一样的。非明文、跟传输层也是一样的。

  • 传输层:可以拿到所有(应用层)的收发包,明文→明文;java.net.SocketInputStream.socketRead0java.net.SocketOutputStream.socketWrite0都是native函数,意味着

  • 应用层非明文→非明文:SSLInputStream.read

  • 应用层2:com.android.okhttp.internal.http.HttpURLConnectionImpl

SSL

安装HttpSocket

1
2
3
objection -g com.onejane.httpsocket explore
android hooking search ssl 将所有打印出的类放到sslandroid8.txt中,前面批量加上android hooking watch class
objection -g com.onejane.httpsocket explore -c sslandroid8.txt 批量hook,报错ClassLoader就删除包括ClassLoader类

android hooking watch class com.android.org.conscrypt.OpenSSLBIOInputStream
android hooking watch class com.android.org.conscrypt.OpenSSLCipher
android hooking watch class com.android.org.conscrypt.OpenSSLCipher$EVP_CIPHER
android hooking watch class com.android.org.conscrypt.OpenSSLCipher​$EVP_CIPHER​$AES
android hooking watch class com.android.org.conscrypt.OpenSSLCipher​$EVP_CIPHER​$AES​$CBC
android hooking watch class com.android.org.conscrypt.OpenSSLCipher$EVP_CIPHER$AES$CBC$PKCS5Padding
android hooking watch class com.android.org.conscrypt.OpenSSLCipher$EVP_CIPHER$AES_BASE
android hooking watch class com.android.org.conscrypt.OpenSSLContextImpl

在安卓8上结果

(agent) [lrxbzy1b2ea] Called javax.net.ssl.HttpsURLConnection.getDefaultHostnameVerifier()
(agent) [lrxbzy1b2ea] Called javax.net.ssl.HttpsURLConnection.getDefaultSSLSocketFactory()
(agent) [lrxbzy1b2ea] Called javax.net.ssl.HttpsURLConnection.getDefaultSSLSocketFactory()
(agent) [nrgnn66lv3e] Called com.android.org.conscrypt.OpenSSLSocketImpl.isClosed()
(agent) [nrgnn66lv3e] Called com.android.org.conscrypt.OpenSSLSocketImpl.isInputShutdown()
(agent) [nrgnn66lv3e] Called com.android.org.conscrypt.OpenSSLSocketImpl.isOutputShutdown()
(agent) [42lol483nwl] Called com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream.write([B, int, int)
(agent) [nrgnn66lv3e] Called com.android.org.conscrypt.OpenSSLSocketImpl.isClosed()
(agent) [43hq04cbdn1] Called com.android.org.conscrypt.SslWrapper.write(java.io.FileDescriptor, [B, int, int, int)
(agent) [yz4ikx9fcpb] Called com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLInputStream.read([B, int, int)
(agent) [nrgnn66lv3e] Called com.android.org.conscrypt.OpenSSLSocketImpl.isClosed()
(agent) [43hq04cbdn1] Called com.android.org.conscrypt.SslWrapper.read(java.io.FileDescriptor, [B, int, int, int)

android hooking watch class_method com.android.org.conscrypt.ConscryptFileDescriptorSocket.$init

在安卓10上结果

(agent) [4816499695697] Called javax.net.ssl.HttpsURLConnection.getDefaultHostnameVerifier()
com.roysue.httpsocket on (google: 10) [usb] #
(agent) [4816499695697] Called javax.net.ssl.HttpsURLConnection.getDefaultSSLSocketFactory()
(agent) [4816499695697] Called javax.net.ssl.HttpsURLConnection.getDefaultSSLSocketFactory()
com.roysue.httpsocket on (google: 10) [usb] # (agent) [2575726777846] Called com.android.org.conscrypt.OpenSSLSocketImpl.isClosed()
(agent) [2575726777846] Called com.android.org.conscrypt.OpenSSLSocketImpl.isInputShutdown()
(agent) [2575726777846] Called com.android.org.conscrypt.OpenSSLSocketImpl.isOutputShutdown()
(agent) [4979599214099] Called com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream.write([B, int, int)
(agent) [2575726777846] Called com.android.org.conscrypt.OpenSSLSocketImpl.isClosed()
(agent) [1105531481296] Called com.android.org.conscrypt.NativeSsl.write(java.io.FileDescriptor, [B, int, int, int)
(agent) [1105531481296] Called com.android.org.conscrypt.NativeSsl.isClosed()
(agent) [7367730933988] Called com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLInputStream.read([B, int, int)
(agent) [2575726777846] Called com.android.org.conscrypt.OpenSSLSocketImpl.isClosed()
(agent) [1105531481296] Called com.android.org.conscrypt.NativeSsl.read(java.io.FileDescriptor, [B, int, int, int)
(agent) [1105531481296] Called com.android.org.conscrypt.NativeSsl.isClosed()

frida -UF -l hookSocket.js 打印http抓包的结果

1
2
plugin wallbreaker objectsearch com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream
plugin wallbreaker objectdump --fullname 0x3486

image-20210516124228857

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function hook_SSLsocketandroid8(){
Java.perform(function(){
console.log("hook_SSLsocket")

Java.use("com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream").write.overload('[B', 'int', 'int').implementation = function(bytearry,int1,int2){
var result = this.write(bytearry,int1,int2);
// console.log("HTTPS write result,bytearry,int1,int2=>",result,bytearry,int1,int2)
// var ByteString = Java.use("com.android.okhttp.okio.ByteString");
// console.log("bytearray contents=>", ByteString.of(bytearry).hex())
//console.log(jhexdump(bytearry,int1,int2));
// console.log(jhexdump(bytearry));

// com.android.org.conscrypt.ConscryptFileDescriptorSocket this$0
console.log(this.this$0.value.sslSession.value.peerHost.value)
console.log(this.this$0.value.sslSession.value.peerPort.value)
console.log(this.this$0.value.sslSession.value.getProtocol())
console.log(this.this$0.value.sslSession.value.getRequestedServerName())
console.log(JSON.stringify( this.this$0.value.sslSession.value.getStatusResponses()))
console.log(this.this$0.value.sslSession.value.getValueNames().toString())
return result;
}

// Java.use("com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLInputStream").read.overload('[B', 'int', 'int').implementation = function(bytearry,int1,int2){
// var result = this.read(bytearry,int1,int2);
// console.log("HTTPS read result,bytearry,int1,int2=>",result,bytearry,int1,int2)
// var ByteString = Java.use("com.android.okhttp.okio.ByteString");
// console.log("bytearray contents=>", ByteString.of(bytearry).hex())
// //console.log(jhexdump(bytearry,int1,int2));
// // console.log(jhexdump(bytearry));
// return result;
// }


})
}

frida 14

android hooking watch class_method com.android.org.conscrypt.ConscryptFileDescriptorSocket.$init

objection -g com.onejane.httpsocket explore -s “android hooking watch class_method com.android.org.conscrypt.ConscryptFileDescriptorSocket.$init”

File

appmon中用到的API。12.8.0报错就切到14.*,frida -UF -l HTTP.jsfrida -UF -l Storage.jsfrida -UF -l SharedPreferences.js

Mobile-Security-Framework-MobSF

1
2
3
objection -g comoolapk.market explore -s "android hooking watch class android.content.ContextWrapper"  下载app查看agent用到的api
android hooking watch class_method android.content.ContextWrapper.getDataDir --dump-args --dump-backtrace --dump-return
android hooking watch class_method android.content.ContextWrapper.getCacheDir --dump-args --dump-backtrace --dump-return
1
2
3
4
frida-ps -U|grep -i gravity
objection -g com.ceco.oreo.gravitybox explore
android hooking watch class android.app.SharedPreferencesImpl
android hooking watch class_method android.app.SharedPreferencesImpl.getString --dump-args --dump-backtrace --dump-return

通杀

1
2
3
4
5
6
7
8
9
objection -g comoolapk.market explore
android hooking search classes File
android hooking watch class java.io.File
android hooking watch class_method java.io.File.getPath --dump-args --dump-backtrace --dump-return
android hooking watch class_method java.io.File.delete --dump-args --dump-backtrace --dump-return
android hooking watch class_method java.io.File.exists --dump-args --dump-backtrace --dump-return
android hooking watch class_method java.io.File.list --dump-args --dump-backtrace --dump-return
android hooking watch class_method java.io.File.getName --dump-args --dump-backtrace --dump-return
cat objection.log | grep Return

java.io.File
java.lang.String

1
2
3
4
5
6
7
8
9
10
11
android hooking watch class java.lang.String 
android hooking watch class_method java.lang.String.toString --dump-args --dump-return 安卓8
android hooking watch class_method java.lang.String.equals --dump-args --dump-return 安卓10
android hooking watch class_method java.lang.StringBuilder.$init --dump-args --dump-return
android hooking watch class android.telephony.TelephonyManager 获取硬件信息
plugin wallbreaker objectsearch android.telephony.TelephonyManager
plugin wallbreaker objectdump 0x4563
plugin wallbreaker classdump android.os.Build
android hooking watch class_method android.telephony.TelephonyManager.getDeviceId --dump-args --dump-backtrace --dump-return
frida -U -f com.coolapk.market -l File.js --no-pause -o file.txt
frida -UF -l File.js --no-pause -o file.txt

利用Frida修改Android设备的唯一标识符

修改Build.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private static String getString(String property) {
String result = SystemProperties.get(property, UNKNOWN) ;
if(property.equals("ro.product.brand")){
result = new String("r0ysueBRAND");
}else if(property.equals(("ro.product.manufacturer"))){
result = new String("r0ysueMANUFACTUERER");
}else if(property.equals("ro.product.board")){
result = new String("r0ysueBOARD");
}else if(property.equals("no.such.thing")){
result = new String("r0ysueAAAABBBBCCCCDDDD");
}
Exception e = new Exception("r0ysueFINGERPRINT");
e.printStackTrace();
return result;
}
@RequiresPermission(Manifest.permission.READ_PHONE_STATE)
public static String getSerial() {
IDeviceIdentifiersPolicyService service = IDeviceIdentifiersPolicyService.Stub
.asInterface(ServiceManager.getService(Context.DEVICE_IDENTIFIERS_SERVICE));
try {
String result =service.getSerial();

return "r0ysueserial1234";
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
return UNKNOWN;
}

修改TelephonyManager.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@RequiresPermission(android.Manifest.permission.READ_PHONE_STATE)
public String getSimSerialNumber(int subId) {
try {
IPhoneSubInfo info = getSubscriberInfo();
String resutlt = info.getIccSerialNumberForSubscriber(subId, mContext.getOpPackageName());
if (info == null)
return null;
return "r0ysueSERIALAAAABBBB";
} catch (RemoteException ex) {
return null;
} catch (NullPointerException ex) {
// This could happen before phone restarts due to crashing
return null;
}
}
@Deprecated
@RequiresPermission(android.Manifest.permission.READ_PHONE_STATE)
public String getDeviceId() {
try {
ITelephony telephony = getITelephony();
String result = telephony.getDeviceId(mContext.getOpPackageName());
if (telephony == null)
return null;
return "r0ysueIMEI";
} catch (RemoteException ex) {
return null;
} catch (NullPointerException ex) {
return null;
}
}

. build/envsetup.sh

lunch aosp_bullhead-user

m

替换编译生成img到官方镜像包中,重新打包成image-bullhead-opm1.171019.011.zip

./flash-all.sh

指纹识别技术安全分析

对抗 不检测root,检测aosp,正常人不会用aosp,App可以通过判断java.net.NetworkInterface.getName()是否等于“tun0”或“ppp0”来判断是否存在VPN。Bypass也很简单,hook该api使其返回“rmnet_data1”,即可达到过vpn检测目的。

风险控制笔记

自制沙箱

检测Android虚拟机的方法和代码实现

2020年安卓源码编译指南

Android 应用多开对抗实践

使用手机连接charles的代理,chsl.pro/ssl安装证书。

1
2
3
4
5
6
cd /data/misc/user/0/cacerts-added  查看新安装的证书a27a90a2.0
cp a27a90a2.0 /sdcard/Download
cd Desktop/asop810r1/system/ca-certificates/files
adb pull /sdcard/Download/a27a90a2.0
. build/envsetup.sh
lunch aosp_bullhead-user 编译,没有root,使用user-debug有root

修改KeyStore.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public final void load(InputStream stream, char[] password)
throws IOException, NoSuchAlgorithmException, CertificateException {
if (password != null) {
String inputPASSWORD = new String(password);
Class logClass = null;
try {
logClass = this.getClass().getClassLoader().loadClass("android.util.Log");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
Method loge = null;
try {
loge = logClass.getMethod("e", String.class, String.class);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
try {
loge.invoke(null, "r0ysueKeyStoreLoad", "KeyStore load PASSWORD is => " + inputPASSWORD);
Exception e = new Exception("r0ysueKeyStoreLoad");
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}

Date now = new Date();
String currentTime = String.valueOf(now.getTime());
FileOutputStream fos = new FileOutputStream("/sdcard/Download/" + inputPASSWORD + currentTime);
byte[] b = new byte[1024];
int length;
while ((length = stream.read(b)) > 0) {
fos.write(b, 0, length);
}
fos.flush();
fos.close();

}

keyStoreSpi.engineLoad(stream, password);
initialized = true;
}

修改SocketOutputStream.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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 {
BlockGuard.getThreadPolicy().onNetwork();
socketWrite0(fd, b, off, len);


if(len>0){
byte[] input = new byte[len];
System.arraycopy(b,off,input,0,len);

String inputString = new String(input);
Class logClass = null;
try {
logClass = this.getClass().getClassLoader().loadClass("android.util.Log");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
Method loge = null;
try {
loge = logClass.getMethod("e",String.class,String.class);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
try {
loge.invoke(null,"r0ysueSOCKETrequest","Socket is => "+this.socket.toString());
loge.invoke(null,"r0ysueSOCKETrequest","buffer is => "+inputString);
Exception e = new Exception("r0ysueSOCKETrequest");
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}

}


} 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();
}
}SocketOutputStream

修改SocketInputStream.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
private int socketRead(FileDescriptor fd,
byte b[], int off, int len,
int timeout)
throws IOException {
int result = socketRead0(fd, b, off, len, timeout);

if(result>0){
byte[] input = new byte[result];
System.arraycopy(b,off,input,0,result);

String inputString = new String(input);
Class logClass = null;
try {
logClass = this.getClass().getClassLoader().loadClass("android.util.Log");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
Method loge = null;
try {
loge = logClass.getMethod("e",String.class,String.class);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
try {
loge.invoke(null,"r0ysueSOCKETresponse","Socket is => "+this.socket.toString());
loge.invoke(null,"r0ysueSOCKETresponse","buffer is => "+inputString);
Exception e = new Exception("r0ysueSOCKETresponse");
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}

}
return result;
}

修改SslWrapper.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
int read(FileDescriptor fd, byte[] buf, int offset, int len, int timeoutMillis)
throws IOException {
int result = NativeCrypto.SSL_read(ssl, fd, handshakeCallbacks, buf, offset, len, timeoutMillis) ;
if(result>0){
byte[] input = new byte[result];
System.arraycopy(buf,offset,input,0,result);
String inputString = new String(input);
Class logClass = null;
try {
logClass = this.getClass().getClassLoader().loadClass("android.util.Log");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
Method loge = null;
try {
loge = logClass.getMethod("e",String.class,String.class);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
try {
loge.invoke(null,"r0ysueSOCKETresponse","SSL is =>"+this.handshakeCallbacks.toString());
loge.invoke(null,"r0ysueSOCKETresponse","buffer is => "+inputString);
Exception e = new Exception("r0ysueSOCKETresponse");
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}

}
return result;
}

void write(FileDescriptor fd, byte[] buf, int offset, int len, int timeoutMillis)
throws IOException {


if(len>0){
byte[] input = new byte[len];
System.arraycopy(buf,offset,input,0,len);

String inputString = new String(input);
Class logClass = null;
try {
logClass = this.getClass().getClassLoader().loadClass("android.util.Log");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
Method loge = null;
try {
loge = logClass.getMethod("e",String.class,String.class);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
try {
loge.invoke(null,"r0ysueSSLrequest","SSL is => "+this.handshakeCallbacks.toString());
loge.invoke(null,"r0ysueSSLrequest","buffer is => "+inputString);
Exception e = new Exception("r0ysueSSLrequest");
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}

NativeCrypto.SSL_write(ssl, fd, handshakeCallbacks, buf, offset, len, timeoutMillis);
}

编译好后,刷机,安装soul使用chales抓包,查看logcat中Keystore加载的Password,下载下来的证书改名为soul.p12安装实现App客户端证书文件和密码自吐

1
2
make update-api  由于修改了文件,更新api
m

甲方风控

实用FRIDA进阶:内存漫游、hook anywhere、抓包

r0capture

git clone https://github.com/r0ysue/r0capture.git

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

python r0capture.py -U cn.soulapp.android -v

python r0capture.py -U -f com.qiyi.video -v

frida -UF -l hookSSLSocket.js

frida -U -f cn.soulapp.android -l saveClientCet.js –no-pause 增加客户端证书dump功能

adb pull /sdcard/Download/ff93e99.p12

证书转换工具 支持bks to p12 把安卓转成Charles支持的p12

charles-Proxy-SSL Proxying Settings-Client Certificates-Create Secure Store-设置自定义密码,配置Host/Port为*对任何IP任何端口使用该证书,Import P12-填入SSL Certificate Password,即抓到的key密码

为沙箱增加调用栈dump证书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
function hook_KeyStore_load() {
Java.perform(function () {
var ByteString = Java.use("com.android.okhttp.okio.ByteString");
var myArray=new Array(1024);
var i = 0
for (i = 0; i < myArray.length; i++) {
myArray[i]= 0x0;
}
var buffer = Java.array('byte',myArray);

var StringClass = Java.use("java.lang.String");
var KeyStore = Java.use("java.security.KeyStore");
KeyStore.load.overload('java.security.KeyStore$LoadStoreParameter').implementation = function (arg0) {
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));

console.log("KeyStore.load1:", arg0);
this.load(arg0);
};
KeyStore.load.overload('java.io.InputStream', '[C').implementation = function (arg0, arg1) {
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));

console.log("KeyStore.load2:", arg0, arg1 ? StringClass.$new(arg1) : null);

if (arg0){
var file = Java.use("java.io.File").$new("/sdcard/Download/"+ String(arg0)+".p12");
var out = Java.use("java.io.FileOutputStream").$new(file);
var r;
while( (r = arg0.read(buffer)) > 0){
out.write(buffer,0,r)
}
console.log("save success!")
out.close()
}
this.load(arg0, arg1);
};

console.log("hook_KeyStore_load...");

// android.content.res.AssetManager$AssetInputStream@9b10ad6 bxMAFPL9gc@ntKTqmV@A

// android.content.res.AssetManager$AssetInputStream@41ce8f6 }%2R+\OSsjpP!w%X

// android.content.res.AssetManager$AssetInputStream@54858e6 cods.org.cn

});
}

重新使用charles即可抓到soul包

1
2
3
4
5
6
7
objection -g cn.soulapp.android explore 
android hooking search classes keystore 将打印的类放到文keystore.txt件中,批量hook,前面加上android hooking watch class
objection -g cn.soulapp.android explore -c keystore.txt
plugin wallbreaker objectsearch java.security.KeyStore$PrivateKeyEntry
plugin wallbreaker objectdump --fullname 0x0123u
android hooking watch class_method java.security.KeyStore$PrivateKeyEntry.getCertificateChain --dump-args --dump-backtrace --dump-return
android hooking watch class_method java.security.KeyStore$PrivateKeyEntry.getPrivateKey --dump-args --dump-backtrace --dump-return

QtScrCpy 手机投屏 Linux版

1
2
apt install libsdl2-2.0-0
./run x

为沙箱增加客户端证书DUMP的功能

1
2
3
4
5
6
7
frida -U -f cn.soulapp.android -l 2021trace.js --no-pause -o traceresult.txt   查看java.security.KeyStore$PrivateKeyEntry的调用栈
objection -g cn.soulapp.android explore
plugin load /root/Desktop/Wallbreaker
plugin wallbreaker objectsearch java.security.KeyStore$PrivateKeyEntry
plugin wallbreaker objectdump --fullname 0x123f 查看KeyStore$PrivateKeyEntry类的privateKey和publicKey
android heap search instances java.security.KeyStore$PrivateKeyEntry
android heap execute 0x2ce7 getPrivateKey() 主动调用

js实现,frida -U- f cn.soulapp.android -l savePrivateKey.js --no-pause 打开app后查看/data/local/tmp/soul下的证书文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
setImmediate(function () {
Java.perform(function () {
console.log("Entering")
Java.use("java.security.KeyStore$PrivateKeyEntry").getPrivateKey.implementation = function () {
console.log("Calling java.security.KeyStore$PrivateKeyEntry.getPrivateKey method ")
var result = this.getPrivateKey()
console.log("toString result is => ", result.toString())
storeP12(this.getPrivateKey(),this.getCertificate(),'/data/local/tmp/soul'+uuid(10,16)+'.p12','hello');
return result;
}
Java.use("java.security.KeyStore$PrivateKeyEntry").getCertificateChain.implementation = function () {
console.log("Calling java.security.KeyStore$PrivateKeyEntry.getCertificateChain method ")
var result = this.getCertificateChain()
storeP12(this.getPrivateKey(),this.getCertificate(),'/data/local/tmp/soul'+uuid(10,16)+'.p12','hello');
return result;
}
})
})
function storeP12(pri, p7, p12Path, p12Password) {
var X509Certificate = Java.use("java.security.cert.X509Certificate")
var p7X509 = Java.cast(p7, X509Certificate);
var chain = Java.array("java.security.cert.X509Certificate", [p7X509])
var ks = Java.use("java.security.KeyStore").getInstance("PKCS12", "BC");
ks.load(null, null);
ks.setKeyEntry("client", pri, Java.use('java.lang.String').$new(p12Password).toCharArray(), chain);
try {
var out = Java.use("java.io.FileOutputStream").$new(p12Path);
ks.store(out, Java.use('java.lang.String').$new(p12Password).toCharArray())
} catch (exp) {
console.log(exp)
}
}
function uuid(len, radix) {
var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
var uuid = [], i;
radix = radix || chars.length;

if (len) {
// Compact form
for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random() * radix];
} else {
// rfc4122, version 4 form
var r;

// rfc4122 requires these characters
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
uuid[14] = '4';

// Fill in random data. At i==19 set the high bits of clock sequence as
// per rfc4122, sec. 4.1.5
for (i = 0; i < 36; i++) {
if (!uuid[i]) {
r = 0 | Math.random() * 16;
uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
}
}
}

return uuid.join('');
}

adb pull /data/local/tmp 使用KeyStore Explorer打开,密码是hello

charles-手机连接代理-SSL Proxying Settings-Client Certificates-Add-Import P12-密码hello-Host和Port配置*,启动Postern,启动soul成功抓包

adb install dida.apk 通过top查看包名cn.ticktick.task

1
objection -g cn.ticktick.task explore -s "android hooking watch class_method java.io.File.\$init --dump-args --dump-return --dump-backtrace"

frida -U -f cn.ticktick.task -l sslpinninghelper.js --no-pause 打印证书路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
setImmediate(function(){
Java.perform(function(){
Java.use("java.io.File").$init.overload('java.io.File', 'java.lang.String').implementation = function(file,cert){
var result = this.$init(file,cert)
var stack = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new());

if(file.getPath().indexOf("cacert")>0 && stack.indexOf("X509TrustManagerExtensions.checkServerTrusted")> 0){
console.log("path,cart",file.getPath(), cert)
console.log(stack);

}
return result;
}
})
})

SSL pinning helper 帮助定位证书绑定的关键代码,在服务器校验客户端的情形下,帮助dump客户端证书,并保存为p12的格式

pm -l | grep -i soul

pm -l | grep -i ticktick

pip install hexdump

python r0capture.py -U -f cn.soulapp.android -v >>sout.txt 重新抓包,frida 14.0.8

python r0capture.py -U -f cn.ticktick.task -v >>tick.txt

框架层抓包

沙箱SslWrapper.java 的修改等同于hook Java.use("com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLInputStream").read.overload('[B', 'int', 'int')实现定位收发包函数的功能。

基于trace的内存漫游确认Java.use("java.security.KeyStore$PrivateKeyEntry").getCertificateChain客户端证书dump导出功能。

增加混淆后的SSLping代码定位功能stack.indexOf("X509TrustManagerExtensions.checkServerTrusted")

抓包沙箱植入根证书绕过客户端校验服务器,cd aosp810r1/system/ca-certificates/files 根证书目录,将charles的证书下载到该文件目录下,编译生成镜像,形成中间人

抓包沙箱导出客户端证书绕过服务器校验客户端,KeyStore.java有个内部方法PrivateKeyEntry

KeyStore.java 去除上面自制沙箱时 public final void load(InputStream stream, char[] password)的修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public PrivateKey getPrivateKey() {

String p12Password = "r0ysue";
Date now = new Date();
String currentTime = String.valueOf(now.getTime());
String p12Path = "/sdcard/Download/tmp" + currentTime + ".p12";

X509Certificate p7X509 = (X509Certificate) chain[0];
Certificate[] mychain = new Certificate[]{p7X509};
// 生成一个空的p12证书
KeyStore myks = null;
try {
myks = KeyStore.getInstance("PKCS12", "BC");
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (NoSuchProviderException e) {
e.printStackTrace();
}
try {
myks.load(null, null);
} catch (IOException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (CertificateException e) {
e.printStackTrace();
}
// 将服务器返回的证书导入到p12中去
try {
myks.setKeyEntry("client", privKey, p12Password.toCharArray(), mychain);
} catch (KeyStoreException e) {
e.printStackTrace();
}
// 加密保存p12证书
FileOutputStream fOut = null;
try {
fOut = new FileOutputStream(p12Path);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
try {
myks.store(fOut, p12Password.toCharArray());
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (CertificateException e) {
e.printStackTrace();
}

return privKey;
}

抓包沙箱之定位(混淆后的)SSLpinning代码,修改File.java,去除上面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public File(File parent, String child) {
if (child == null) {
throw new NullPointerException();
}
if (parent != null) {
if (parent.path.equals("")) {
this.path = fs.resolve(fs.getDefaultParent(),
fs.normalize(child));
} else {
this.path = fs.resolve(parent.path,
fs.normalize(child));
}
} else {
this.path = fs.normalize(child);
}


Class logClass = null;
try {
logClass = this.getClass().getClassLoader().loadClass("android.util.Log");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
Method loge = null;
Method getStackTraceString = null;
try {
// loge = logClass.getMethod("e", String.class, String.class);
getStackTraceString = logClass.getMethod("getStackTraceString",Throwable.class);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
try {
// loge.invoke(null, "r0ysueKeyStoreLoad", "KeyStore load PASSWORD is => " + inputPASSWORD);
String stack = (String)getStackTraceString.invoke(null,new Throwable());
if (parent.getPath().indexOf("cacert") >= 0 &&
stack.indexOf("X509TrustManagerExtensions.checkServerTrusted") >= 0) {
Exception e = new Exception("r0ysueFileSSLpinning");
e.printStackTrace();
}
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}

this.prefixLength = fs.prefixLength(this.path);
}

编译刷机

1
2
lunch aosp_bullhead-user
m 编译完更新system.img到官方镜像bullhead刷机

默认系统报错400 No required SSL certificate was sent,导入证书到SSLProxying Setting才能正确抓到soul包

绕过滴答 frida -U -f cn.ticktick.task -l bypassPinning.js --no-pause默认报错trust the Charles Root Certificate ,客户端收到charles的证书,计算公钥hash后比对结果决定发请求结果。

image-20210528110037286

1
2
3
4
5
6
7
8
9
setImmediate(function(){
Java.perform(function(){
console.log("Bypassing")
Java.use("z1.g").a.implementation = function(){
console.log("called here")
return;
}
})
})

HTTPS客户端证书多重证书绑定

咪咕视频

登录抓包,SSL handshake with client failed: An unknown issue occurred processing the certificate (certificate_unknown)从抓包发现证书 绑定,可能客户端只信任信任的公钥签名,不信任就不允许,停止客户端访问的证书绑定。客户端发了,我们已经绕过了校验,把自己公钥发给charles,charles用自己私钥解开客户端的公钥发现不正常的结果。

1
2
3
4
5
6
dumpsys activity top  查看包名 com.ophone.reader.ui
objection -g com.ophone.reader.ui explore
android sslpinning disable 需要在启动时运行
objection -g com.ophone.reader.ui explore -s "android sslpinning disable" 在点我登录页面触发解绑定,如果崩溃
objection -g com.ophone.reader.ui explore 在点我登录时开始漫游
android sslpinning disable 解绑定后再获取验证码,再抓包

证书绑定的逻辑没有hook掉:”at com.bangcle.andjni.JniLib.cL(Native Method)” → 只有逆代码来过证书绑定

1
python r0capture.py -U com.ophone.reader.ui -v -p migu.pcap  关闭postern抓包,获取验证码没有更新log,说明没有一些底层的框架

通过wireshark查看migu.pcap结果,发现也没有关键性信息,抓包也抓不到。

1
2
3
4
5
6
python r0capture.py -U -f com.ophone.reader.ui -v  尝试导出证书
frida -U -f com.ophone.reader.ui -l script.js --no-pause
adb shell 查看sdcard/Download下的证书
adb pull /sdcard/Download/ophone 下的证书导入到Charles的SSL Proxying Settings中,打开postern抓包
objection -g com.ophone.reader.ui explore 在点我登录时开始漫游
android sslpinning disable 解绑定后再获取验证码抓包即可获取passport.migu.cn:8443的包信息

SSL handshake with server failed - Remote host terminated the handshake
The remote SSL server rejected the connection. The server may require a specific certificate or cipher not supported by Charles.

过客户端证书后发现更多证书绑定,Frida.Android.Practice

1
objection -g com.ophone.reader.ui explore -s "android sslpinning disable"

git clone https://github.com/WooyunDota/DroidSSLUnpinning.git

1
frida -U -f com.ophone.reader.ui -l hooks.js --no-pause   抓发送验证码包依旧有请求失败

FRIDA 使用经验交流分享,git clone https://github.com/deathmemory/FridaContainer.git

1
2
3
4
cd utils/android
frida -U -f com.ophone.reader.ui -l multi_unpinning.js --no-pause
objection -g com.ophone.reader.ui explore -s "android hooking watch class_method java.io.File.\$init --dump-args --dump-backtrace --dump-return" 查看证书
frida -U -f com.ophone.reader.ui -l trace.js --no-pause -o ophone.txt 修改trace.js中traceClass("java.io.File"),在traceClass中修改targets=[]只trace init方法,在traceMethod中打开调用栈android.util.log,发送验证码后查看文件,搜索cacert查看调用栈

image-20210528115202167

北京银行

1
2
3
4
python r0capture.py -U -f com.bankofbeijing.mobilebanking -v
./hluda-server-14.2.1-android-arm64
frida -U -f com.bankofbeijing.mobilebanking -l script.js --no-pause -o bjbank.txt
frida -U -f com.bankofbeijing.mobilebanking -l trace.js --no-pause -o bjbank2.txt 打开traceClass("java.security.KeyStore$PrivateKeyEntry")

加固厂商自定义开发的证书绑定对抗很难被攻克。

文章作者: J
文章链接: http://onejane.github.io/2021/05/06/frida沙箱自吐实现/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 万物皆可逆向
支付宝打赏
微信打赏