记一次零基础IOT设备与app交互0day漏洞挖掘学习经历

基础知识

apk结构

apk是对一个安卓应用程序所需要的文件进行打包,本质上是一个被签名的压缩包。

通常情况下,apk会含有以下文件:

  • asset文件夹:是不需编译的原始资源目录,包含各种静态的资源,如各种配置文件、JavaScript、字体文件、图片文件等。
  • lib文件夹:动态链接库存放的位置,通常情况下,这个文件夹内部以不同处理器版本还会划分成多个文件夹,如armeabi、armeabi-v7a、x86等,其存放的文件通常为Android Native的代码。
  • META-INF文件夹:用来存放签名信息,通常会有CERT.RSA、CERT.SF和MANIFEST.MF三个文件。是用来保护apk的所有权和防止apk被恶意篡改。
  • r文件夹/res文件夹:存放编译资源文件,与asset文件夹相似,区别是其存放的文件是编译后的。通常会包含drawable 文件夹(图片资源文件)、layout 文件夹(布局文件)和values 文件夹(值资源文件)等。r文件夹通常是res文件夹进行混淆后的结果。
  • AndroidManifest.xml文件:apk的整体配置文件,其中包含了一个apk的各种配置信息,包括包名、应用名、权限、安卓四大组件、版本等重要信息。
  • dex文件:存放字节码的文件,其反汇编后为smali语言,可转化为Java代码,通常dex文件包含一个apk的主要逻辑。
  • resources.arsc文件:用来存放应用程序的资源表,包含了应用程序的资源 ID 和资源类型的映射关系。

使用工具

jadx/jeb:均为Android程序Java层反编译软件,相对来说jeb反编译能力相对较强,可以看到smali层的代码,而且也有一些混淆对抗的能力

frida:Android程序二进制动态插桩工具,用来动态调试Android程序

xposed:动态调试插件,相对于frida更加稳定

mitmproxy:中间人工具,用来监控并解密app端TLS流量

wireshark:对网卡进行抓包,配合mitmproxy使用可以获取tls解密后的流量信息

ida:用来反编译Android native层代码

iptables:配合mitmproxy透明模式,避免app端流量不经过代理导致tls流量无法被解密

手机:已ROOT

adb:用来连接手机的shell

环境搭建

获取手机Root权限

本次使用的手机是一台Pixel 6,首先需要在电脑上安装adb,过程省略

进入手机开发者模式,打开USB调试开关,连接电脑adb,可以使用adb devices查看是否连接成功,连接成功后开始解BL,输入

1
adb reboot bootloader

将手机进入Bootloader界面,此时手机处于fastboot模式下,输入fastboot devices查看是否可以正常连接。输入

1
fastboot flashing unlock

成功解锁BL。

注:Pixel手机如果发现进入fastboot模式adb断开的情况,请检查是否是数据线的原因,可以换个数据线试试,这个坑卡了我好久-_-

解锁BL后,一般都会向手机中安装Magisk,它是用来管理Root权限的工具。来源:topjohnwu/Magisk: The Magic Mask for Android (github.com)

下载好安装包后,可以使用adb install命令来安装Magisk,装好Magisk后,接下来进行镜像修补。

Nexus 和 Pixel 设备的出厂映像 | Google Play services | Google for Developers中下载手机对应的镜像文件,在手机设置 - 关于手机页面的最底端可以看到当前的版本号,在页面中找到对应的镜像下载,下载后将其中的boot.img用adb传输到手机,最后使用Magisk软件进行镜像修补,最后将修补完的镜像文件adb pull出来。

接下来进入最后的刷机步骤,使用adb reboot bootloader再次进入fastboot模式中,使用fastboot boot img地址指令将手机从修补过的img启动,重启后进入Magisk按照步骤安装好即可。

mitmproxy+iptables搭建中间人代理

mitmproxy是一个强大的中间人代理工具,与其他中间人代理工具相比,mitmproxy不仅可以转发http/https流量,还可以转发非http流量,如MQTT等。

在Android设备中抓取https流量,我们需要安装mitmproxy的CA证书,由于Android 7开始,应用会默认忽略用户级别的证书,因此,我们需要将CA证书放入系统级别中。

一般情况下,将证书格式先转化为pem格式,然后通过openssl x509 -subject_hash_old -in certificate.pem|head -1命令读取哈希值,将pem证书名字改为刚刚提取的哈希值加.0,如9a5ba575.0,其中.0是为了防止证书哈希值重复,如果两个证书哈希值重复,那么后面的证书就会被重命名为.1、.2等,最后将/system/etc/security/cacerts/目录可写权限打开,将重命名后的证书放进去即可。

我的手机版本为Android 10以上,无法直接通过更改文件夹写入权限来导入证书(也可能是我没有搞好),我使用了Magisk的Always Trust User Certificates模块,直接将证书装在用户目录下,重启后即可导入到系统证书中。

装好证书后,正常情况下手机端连wifi时配置代理后应该是可以解密https流量了,但是,如果app拒绝代理或想要捕获其他tcp流量时,就需要使用mitmproxy透明模式,透明模式的启动命令为mitmproxy --mode transparent --showhost,开启透明模式后,工作原理如下图:

此时,对于手机来说,mitmproxy相当于一个服务器,对于原服务器来说,mitmproxy相当于设备。

由于透明模式需要对网络层进行转发,因此还需要配置iptables,关于iptables的知识可以看这篇博客iptables-朱双印博客 (zsythink.net),在此贴一张iptables的原理图。

我的iptables规则参考了fwx学长的博客基于mitmproxy+iptables+SSL pinning绕过技术+wireshark的安卓APP流量(包括HTTP、HTTPS和非HTTP)捕获 | 代码鬼才的Blog (fwx2233.github.io),如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
# 写监听的无线网卡的名称
WIRELESS_CARD="wlxc01c30151c62"

# 开启相关的转发服务
sudo sysctl -w net.ipv4.ip_forward=1
sudo sysctl -w net.ipv4.conf.all.send_redirects=0

# 设置iptables的规则
# 将之前的规则清空
sudo iptables -F PREROUTING -t nat -i $WIRELESS_CARD
# 设置端口转发(MQTT: 8883, HTTP: 80, HTTS: 443)
sudo iptables -t nat -A PREROUTING -i $WIRELESS_CARD -p tcp --dport 80 -j REDIRECT --to-port 8080
sudo iptables -t nat -A PREROUTING -i $WIRELESS_CARD -p tcp --dport 443 -j REDIRECT --to-port 8080
sudo iptables -t nat -A PREROUTING -i $WIRELESS_CARD -p tcp --dport 8883 -j REDIRECT --to-port 8080

最开始我一直想要拿win+mitmproxy透明模式进行抓包,想要通过netsh来代替iptables进行流量转发,然而一直没有成功,如果读者有配置成功的经历麻烦评论区分享一下

接下来配置wireshark,通过捕获mitmproxy密钥交换过程中生成的随机数来进行TLS解密,操作方式也可以直接看官方文档Wireshark and SSL/TLS (mitmproxy.org)

如果时间过长或多次抓包导致生成的随机数文件过大,可能会导致wireshark解密失败,可以定时清空生成的随机数文件。

至此基本环境配置完毕,给出我的最终网络拓扑图。

原理

frida进行hook

frida安装过程省略,网上有很多教程可以参考。

frida中有两种操作模式,分别是CLI模式和RPC模式

  • CLI(命令行)模式:通过命令行直接将JavaScript脚本注入进程中,对进程进行操作
  • RPC模式:使用Python进行JavaScript脚本的注入工作,实际对进程进行操作的还是JavaScript脚本,可以通过RPC传输给Python脚本来进行复杂数据的处理

frida有两种注入模式,分别是Spawn和Attach

  • Spawn模式:将启动App的权利交由Frida来控制,即使目标App已经启动,在使用Frida注入程序时还是会重新启动App。在命令行模式中需要加入参数-f,可以对从启动就开始对App进行监控。
  • Attach模式:在目标App已经启动的情况下,Frida通过ptrace注入程序从而执行Hook的操作。如果只关心一个功能时通常会用这种模式。

相关的api可以在官网上查看官方文档Welcome | Frida • A world-class dynamic instrumentation toolkit

给出一个python的框架代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import sys,frida

jscode = """
这里输入你的js代码
"""

def on_message(message,data):
if message["type"] == "send":
print(message["payload"])
else:
print(message)

# process = frida.get_usb_device().spawn("app"))
process = frida.get_usb_device().attach("app")
script = process.create_script(jscode)
script.on("message",on_message)
script.load()
sys.stdin.read()

Java层hook

Java层hook示例代码:

1
2
3
4
5
6
7
8
9
10
11
setImmediate(
Java.perform(function () {
var targetClass = Java.use(className); // 替换为您的类名
targetClass.examplefunction.implementation = function(a,b,c...){//替换为参数
//调用原始方法
var result = this.examplefunction(a, b, c...);
//可以在这里进行各种操作
return result;
}
});
);

hook重载参数:

1
2
3
4
5
6
7
8
9
function hook(){
var utils = Java.use(className);
//overload定义重载函数,根据函数的参数类型填
utils.expfunc.overload('com.example.Demo$Class','java.lang.String').implementation = function(a,b){
b = "aaaaaaaaaa";
this.expfunc(a,b);
console.log(b);
}
}

hook字段修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function hook(){
//静态字段修改
var utils = Java.use(className);
//修改类的静态字段"flag"的值
utils.staticField.value = "我是被修改的静态变量";
console.log(utils.staticField.value);
//非静态字段的修改
//使用`Java.choose()`枚举类的所有实例
Java.choose("com.example.Demo", {
onMatch: function(obj){
//修改实例的非静态字段"_privateInt"的值为"123456",并修改非静态字段"privateInt"的值为9999。
obj._privateInt.value = "123456"; //字段名与函数名相同 前面加个下划线
obj.privateInt.value = 9999;
},
onComplete: function(){
}
});
}

对内部类进行hook

1
2
3
4
5
6
7
8
function hook(){
//内部类
var innerClass = Java.use("com.example.Demo$innerClass");//如果是匿名类需要反编译查看具体标号
console.log(innerClass);
innerClass.$init.implementation = function(){
console.log("hook");
}
}

静态方法主动调用

1
2
3
4
function hook(){
var ClassName=Java.use("com.example.Demo");
ClassName.privateFunc("传参");
}

非静态方法主动调用

1
2
3
4
5
6
7
8
9
10
11
var ret;
function hook() {
Java.choose("com.example.Demo",{ //要hook的类
onMatch:function(instance){
ret=instance.privateFunc("aaaaaaa"); //要hook的方法
},
onComplete:function(){
console.log("result: " + ret);
}
});
}

Native层hook

枚举so库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function hook(){
Process.enumerateModules({
onMatch: function (module) {
send(module.name + " : " + module.base.toString());
if (module.name == "example.so") {
send("example.so found !");
send("hooking...");
send(module.name + " : " + module.base.toString() + " : " + module.size.toString() + " : " + module.path);
}
},
onComplete: function () {
send("end");
}
});
}

hook函数

1
2
3
4
5
6
7
8
9
10
11
12
function hook(){
var base_addr = Module.findBaseAddress("example.so");
Interceptor.attach(base_addr.add(0xabcd), {//偏移值
onEnter: function (args) {
//args是参数数组
console.log(args[0]);
},
onLeave: function (retval) {
console.log(retval);
}
});
}

这里有一个偏移值的计算,安卓里一般32 位的 so 中都是thumb指令,64 位的 so 中都是arm指令,通过IDA里的opcode bytes来判断,arm 指令为 4 个字节(options -> general -> Number of opcode bytes (non-graph) 输入4)

  • thumb 指令,函数地址计算方式: so 基址 + 函数在 so 中的偏移 + 1
  • arm 指令,函数地址计算方式: so 基址 + 函数在 so 中的偏移

IDA对so库逆向

ida的话就是纯看代码环节了,说几个小技巧吧。

首先就是尽量不要从导出表中反过来找函数,因为有一部分导出表对应的函数是fastcall类型,只观察函数内部有时ida无法将参数识别出来,导致后来再去找函数的调用处时代码都是乱的。

ida有一键生成frida的插件,P4nda0s/IDAFrida: IDA Frida Plugin for tracing something interesting. (github.com)AnxiangLemon/MyIdaFrida: Generate Frida Script (github.com),可以直接从ida中生成frida的hook脚本,比较方便(虽然我没用过几次)

后记

关于漏洞细节就不发出来了。历时2个多月的零基础从入门到入土,确实学到了不少东西,也踩了一大堆坑,有的坑也卡的比较久,在此感谢w学长给我一步步梳理思路。


记一次零基础IOT设备与app交互0day漏洞挖掘学习经历
https://blog.lazyforever.top/2023/12/12/2023apkreverse/
作者
lazy_forever
发布于
2023年12月12日
许可协议