安卓逆向——从0学习Frida(配合FridaLab)
Table of Contents
一年前还是会一点的,过了这么久忘了挺多,重新学一下。写一个博客来记录,往后也可以给学弟学妹作为参考。
##
0.基础配置
#
安装配置Android模拟器
本文使用MuMu模拟器-https://mumu.163.com/。
- 在主页下载之后,安装打开。
- 在设置中打开root权限
勾选之后重启模拟器。
- 通过adb连接模拟器
打开文件所在位置,找到自带的adb(就不用去额外下载了)
在这里目录下打开cmd,Shift+鼠标右键进入终端。
直接在cmd调用是没有的,可以把adb加到环境变量里
添加完之后就能在cmd随时随地调用了。
Q:MuMu模拟器如何启动adb调试?
A:官方文档https://mumu.163.com/help/20230214/35047_1073151.html,
从官方文档可以知道,可通过模拟器右上角菜单-问题诊断,获取ADB调试端口。
在终端执行adb connect 127.0.0.1:16385
连接成功之后就可以用adb命令来操作虚拟机了。
默认一般是16384端口,注意自己的配置。
常用的adb命令
adb devices #列出当前连接的所有设备。
adb install <path_to_apk> #将指定的APK文件安装到设备上。
adb shell #进入设备或模拟器的Linux shell环境,可以执行各种Linux命令。
adb push <本机路径> <模拟器路径> #把电脑上的文件传到模拟器上。
adb pull <模拟器路径> <本机路径> #把模拟器上的文件下载到电脑上。
adb push example.txt /sdcard/ #把当前目录的example.txt传到/sdcard/目录下。
先把模拟器打开放一边。
#
Frida的安装和配置
Q: 什么是Frida?
A: Frida 是一款功能强大的动态代码插桩工具(Dynamic Instrumentation Toolkit),主要用于逆向工程、安全分析、漏洞挖掘以及应用程序的运行时调试。它通过注入脚本到目标进程的内存中,允许开发者和安全研究人员动态修改、监控和调试应用程序的行为,支持多种平台(Android、iOS、Windows、macOS、Linux 等)和编程语言(Java、C/C++、Objective-C、Swift、Python 等)。
本文主要是Android逆向。
确保电脑有python环境,在cmd执行
pip install frida
pip3 install frida-tools
看到当前的frida版本。然后去github下载对应版本的frida-server并放到安卓设备上。
https://github.com/frida/frida/releases
这里也是16.7.10。
如果安装其他版本的话,找到其他版本的release,然后下载第一个whl文件,用python安装。然后再下载对应的server。
不知道的可以执行adb shell getprop ro.product.cpu.abi
来查看
下载之后解压出来,这里解压到桌面上。(当然也可以直接传xz文件,然后shell内xz -d进行解压)
回到刚才的adb命令传文件到模拟器上。
用adb push <电脑文件路径> <模拟器文件路径>
我传到了/sdcard/frida。再用adb shell
连接模拟器的shell,安卓底层是linux,所以用linux命令。
因为这个目录有权限问题,所以得复制到另一个目录。
先执行su
命令进入root用户,这里一般模拟器会弹出对话框,点击允许。
cp /sdcard/frida /data/local/tmp
然后cd /data/local/tmp
进入目录
chmod +x frida
然后运行frida-server。
##
运行frida-server
其实之前这里命名为frida不太合适,可以改为frida-server
使用./frida-server -h
查看启动参数
介绍最简单的两种,剩下自行摸索
直接启动:./frida-server
启动到后台:./frida-server -D
直接启动是挂在前台的,没有输出,无法继续执行shell了。Ctrl+C终止进程。
后台启动不影响shell的使用,ps -ef
可以看到,可以使用kill 2771
终止进程。
确保程序在运行就可以开始做题了。
##
版本问题1
如果暂时没有问题可以跳过这部分
本文前面安装的是最新版本的,然而因为太新了有点问题。问题如下,
如果你也遇到Failed to spawn: the connection is closed
可以降个级试试。
https://www.52pojie.cn/thread-2020759-1-1.html
然后删除了之前的进行指定版本降级安装。
pip uninstall frida
pip uninstall frida-tools
pip install frida-tools==12.0.0
pip install frida==16.2.0
然后再在github的release页面上找到16.2.0对应的frida-server文件,跟上面一样。重新传到/data/local/tmp目录。
frida-tools,frida,frida-server三个版本要对应起来。参考下面的文章
https://bbs.kanxue.com/thread-280436.htm
#
Jadx安装
Q: 什么是Jadx
A: JADX 是一款开源的逆向工程工具,主要用于将 Android 应用(APK 文件或 DEX 文件)反编译为可读的 Java 或 Kotlin 源代码。它可以帮助开发者和安全研究人员分析 Android 应用的代码逻辑、查找漏洞或检测恶意行为。
https://github.com/skylot/jadx/releases/tag/v1.5.1,进入releases页面之后下载。
with-jre是带运行环境的。一般是下载这个。
##
1. Frida基础使用
##
常用命令
-
列出当前连接的设备:
frida-ls-devices
-
列出设备上正在运行的进程:
frida-ps -U
-
启动应用并附加(
-f
表示启动应用):frida -U -f com.example.app
-
注入JS脚本:
frida -U com.android.xxx -l /path/to/script.js
##
常用参数
-U
: 连接 USB 设备(Android/iOS)。-l <script.js>
: 加载自定义的 JavaScript 脚本。--no-pause
: 启动应用后立即执行脚本(避免卡在启动界面)。
##
常用脚本片段
###
打印某个类的所有方法
Java.perform(function () {
var className = "com.example.app.MainActivity";
var methods = Java.use(className).class.getDeclaredMethods();
methods.forEach(function (method) {
console.log(method.toString());
});
});
###
Hook 方法并打印参数
Java.perform(function () {
var targetClass = Java.use("com.example.app.Utils");
targetClass.encrypt.implementation = function (input) {
console.log("Input: " + input);
var result = this.encrypt(input); // 调用原方法
console.log("Result: " + result);
return result;
};
});
###
修改函数返回值
Java.perform(function () {
var targetClass = Java.use("com.example.app.Auth");
targetClass.isValidUser.overload('java.lang.String').implementation = function (user) {
console.log("Bypassing validation for user: " + user);
return true; // 强制返回 true
};
});
##
2. Lab题目
我是Linux系统,剩下的操作都是在linux终端下操作的。问题不大,命令是一样的。
克隆仓库git clone https://github.com/DERE-ad2001/Frida-Labs.git
看一个仓库先看README文件。这里是每个挑战的知识点。第一关就是frida的安装和hook一个方法。
每个目录都有apk安装包和一个教程。
#
0x1 Frida setup, Hooking a method
第一关,首先安装apk文件,并用jadx反编译。
使用adb命令安装,adb install [apk文件]
。
观察页面,发现页面由三个组件构成。逻辑就是用户输入文本,然后点击按钮,内容正确就显示flag。
然后jadx反编译apk。
可以看到package和activity,MainActivity就是打开程序后的主界面。
知道了package然后从源代码找到MainActivity的代码。
如何分析MainActivity的代码呢。了解一下Activity的生命周期。
Activity被启动后,会先执行onCreate的内容,然后一般开发者会在onCreate绑定界面上的组建到对应的类中。
分析代码。解法很多!
了解一下https://frida.re/docs/javascript-api/官方api
Java.perform
##
解法1-修改get_random的返回值
需要java基础。一般套模板就差不多了。
Java.perform(fn): 确保当前线程已附加到Java虚拟机(VM)并执行fn函数。(在来自Java的回调中无需显式调用此操作)若应用程序的类加载器尚未就绪,将延迟执行fn函数。若不需要访问应用程序的类,请使用Java.performNow()替代。
使用场景对比:
• perform():需要访问应用类时使用(需等待类加载器初始化)
• performNow():仅需执行通用操作时使用(无需类加载依赖)
有点代码有setImmediate,`setImmediate` 是 Node.js 风格的函数,用于立即执行回调,但 Frida 中通常用 `Java.perform()`。
Java.perform(function () {
var targetClass = Java.use("com.ad2001.frida0x1.MainActivity");
targetClass.get_random.overload().implementation = function () {
return 2; // 返回2
};
});
random=2,那么obj=8时就能获得flag。
使用脚本启动frida -U -f com.ad2001.frida0x1 -l 1.js
##
解法2-从check触发的时候输出随机数或直接修改参数。
Java.perform(function() {
var targetClass = Java.use("com.ad2001.frida0x1.MainActivity");
targetClass.check.overload('int', 'int').implementation = function(a, b) {
console.log("The random number is " + a);
console.log("The user input is " + b);
this.check(a,b) //如果没有这个是不会继续执行的,输出完就没后续了。
}
})
启动之后,必须触发check函数才会提示。先输入1触发。
知道了随机数就可以知道应该输入什么内容了。输入54
就得到flag了。
或者直接修改参数。这时候无论输入什么,参数始终是4和12。都会返回flag。
Java.perform(function() {
var targetClass = Java.use("com.ad2001.frida0x1.MainActivity");
targetClass.check.overload('int', 'int').implementation = function(a, b) {
this.check(4, 12);
}
})
总结一下,发现overload()里的参数和function()里的参数要和原参数对应起来。
#
0x2 Calling a static method
界面很简单,只有一个文本框,HOOK ME!拖入Jadx。
老规矩,先进入AndroidManifest.xml找package和主要的Activity。
只有一个简单的get_flag静态函数,只要传入的参数a是4919就可以处理出来flag并且设置到t1(TextView)上。
这里要注意一个点,执行get_flag必须在onCreate的t1文本框绑定之后。
绑定之后t1才有正确地址,才能够正确的调用setText来显示flag。
Java.perform(function() {
var targetClass = Java.use("com.ad2001.frida0x2.MainActivity");
setTimeout(() => {
targetClass.get_flag(4919);
}, 1000);
})
frida -U -f com.ad2001.frida0x2 -l 1.js
用-l参数脚本执行的话需要setTimeout();
直接frida -U -f com.ad2001.frida0x2之后,在控制台输入不需要加setTimeout()
#
0x3 Changing the value of a variable
在类加载的准备阶段会给所有static 成员在方法区中分配内存
安装完之后打开,点击按钮说是要TRY AGAIN。上jadx。
说是要Checker.code==512,Ctrl+左键点击Checker跳转到对应的类文件。
里面有一个静态属性和一个静态方法。题目是Changing the value of a variable,意思是改变变量的值。
有两个思路,触发increase256次或者直接给code赋值。
Java.perform(function (){
var targetClass = Java.use("com.ad2001.frida0x3.Checker");
targetClass.code.value = 512;
})
frida -U -f com.ad2001.frida0x3 -l 1.js
然后进入程序再点击按钮即可获得flag。
方法二的代码。
Java.perform(function (){
var targetClass = Java.use("com.ad2001.frida0x3.Checker");
for(let i=0;i<256;i++)
{
targetClass.increase();
}
})
#
0x4 : Creating a class instance
还是jadx。BUT MainActivity没啥东西。
还是有个Check的。
思路就是触发get_flag方法,然后获取返回值。但是这个get_flag方法不是静态的,需要去先实例化一个Check对象。
然后在Check对象上调用get_flag(1337)。正好对应起来了题目。
emm,思路有了,不太会写代码,看看Solution。
Java.perform(function() {
var check = Java.use("com.ad2001.frida0x4.Check");
var check_obj = check.$new(); // Class Object
var res = check_obj.get_flag(1337); // Calling the method
console.log("FLAG " + res);
})
可以看到用了check.$new();
这个来实例化一个对象。学会了!
其实也可以不用创建脚本文件,直接输入frida -U -f 包名
,然后再直接输入代码。
#
0x5 Invoking methods on an existing instance
如题所示,在一个存在的实例上调用方法。调用MainActivity的flag方法就可以了。
但是MainActivity是一个界面,实例化起来很麻烦。不能用上一题的方法。
直接获取存在的MainActivity并调用。
从Solution中学习了两个函数。S
-
Java.performNow
:该函数用于在Java运行时上下文中执行代码。 -
Java.choose
:运行时枚举指定Java类(通过第一个参数提供)的实例。该函数会遍历内存中已加载目标类的所有实例。
choose不知道,看看api。
https://frida.re/docs/javascript-api/
最终代码如下:
当匹配到了com.ad2001.frida0x5.MainActivity会出发onMatch然后调用flag函数。
Java.performNow(function() {
Java.choose('com.ad2001.frida0x5.MainActivity', {
onMatch: function(instance) { // "instance" is the instance for the MainActivity
console.log("Instance found");
instance.flag(1337); // Calling the function
},
onComplete: function() {}
});
});
但是Solution给出的代码在控制台启动之后再运行可以获取到flag。
如果写在脚本里就不行了frida -U -f com.ad2001.frida0x5 -l MyScript/1.js
会报错的。然后需要跟第二个题目一样,加个setTimeOut。程序加载后过个1s就会调用Java.choose。然后输出Instance found,
再回到APP就能看到flag了。
但是还要把Java.performNow改成Java.perform。
使用场景对比:
• perform():需要访问应用类时使用(需等待类加载器初始化)
• performNow():仅需执行通用操作时使用(无需类加载依赖)
我的理解是performNow比perfrom执行的要早,类加载器还没初始化就执行导致找不到MainActivity。
Java.perform(function() {
setTimeout(() => {
Java.choose('com.ad2001.frida0x5.MainActivity', {
onMatch: function(instance) { // "instance" is the instance for the MainActivity
console.log("Instance found");
instance.flag(1337); // Calling the function
},
onComplete: function() {}
});
},1000);
});
#
0x6 Invoking a method with an object argument
界面还是Hello,World。
这题结合了0x5和0x4。
需要调用MainActivity的get_flag,然后还得传一个Checker,需要再实例化一个Checker传进来。
Java.perform(function() {
setTimeout(() => {
Java.choose('com.ad2001.frida0x6.MainActivity', {
onMatch: function(instance) {
console.log("Instance found");
var checker = Java.use("com.ad2001.frida0x6.Checker");
var checker_obj = checker.$new();
checker_obj.num1.value=1234;
checker_obj.num2.value=4321;
instance.get_flag(checker_obj);
},
onComplete: function() {}
});
},1000);
});