基础知识 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,输入
将手机进入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进行hookfrida安装过程省略,网上有很多教程可以参考。
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,fridajscode = """ 这里输入你的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层hookJava层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学长给我一步步梳理思路。