在《APP动态分析系列-Frida的基础用(上)》文章中我们讲到,在分析恶意APP样本时需要Hook一个方法、调用静态方法、更改变量的值的三种单靠静态和逆向分析无法解决问题,需要引入基于Frida的动态插桩技术来实现的分析场景。本篇文章将继续探讨类似的以下四个场景:
-
创建类实例:例如,在分析APP加密通信模块时,对于非静态的加密方法,我们需要创建该加密算法类的实例来调用这个加密方法,以深入了解加密机制。
-
在现有实例上调用方法:例如,在绕过APP权限验证时,需要获取权限验证类的实例,并调用验证方法来模拟不同的权限验证场景。
-
使用对象参数调用方法:例如,在分析订单处理功能时,需要使用不同的订单对象参数调用订单处理类的方法,来尝试绕过订单验证步骤。
-
Hook构造函数:例如,在分析APP隐私数据收集合规性时,需要Hook应用程序的构造函数来观察初始化过程中的数据收集操作,以识别APP是否违规收集敏感隐私信息。
使用Frida实现这些功能,我们需要安装前置的Python、Frida、安卓模拟器及其依赖环境(可参考官方文档:https://frida.re/docs/home/),并掌握通过Frida实例化类、调用方法、创建对象参数、Hook构造函数等基础知识。【大狗涉网线索分析平台】的【云真机操作台】引入的Frida 脚本功能,可以省去复杂的安装过程,实现一键运行 Frida 脚本,让大家可以在无糖浏览器中随时随地对恶意 APP 进行动态调试,大大提高工作效率和便利性。
接下来,我们将对以上四个分析场景中涉及到的Frida基础用法进行讲解和演示。
本篇文章将继续以Frida-lab中的题目为例,为大家介绍云真机操作台中Frida脚本基础用法和背景知识的下半篇。Frida-lab是Github上的一个开源项目,是专为学习Frida for Android而设计的一系列挑战,包含多个CTF风格的APP样本,旨在帮助初学者掌握Frida及其常用的API基础知识。
项目链接:https://github.com/DERE-ad2001/Frida-Labs
-
Jadx反编译工具:jadx是一个功能强大、使用简单的Android反编译利器,适合开发者在逆向工程和代码分析时使用。(官方网站:https://github.com/skylot/jadx)
-
JavaScript脚本:我们将使用JavaScript API来完成Frida脚本的编写。(API文档地址:https://frida.re/docs/javascript-api/)(值得注意的是,Frida也支持Python。)
-
Frida框架:本次我们使用【大狗涉网线索分析平台】-【云真机操作台】中引入的Frida脚本功能。
-
了解Frida框架的基本原理和架构。
-
了解Hook技术拦截和修改函数或方法的基础知识。
-
掌握使用jadx进行逆向工程的基础知识。
-
具备理解Java代码的能力。
-
具备编写小型JavaScript代码片段的能力。
APP下载地址:https://github.com/DERE-ad2001/Frida-Labs/tree/main/Frida%200x4
在下载Challenge 0x4.apk文件并上传至大狗云真机操作平台后,可以看到APP程序为一个静态页面,只有一段打招呼的提示词,没有任何输入框和按钮(图1)。我们将APK放入jadx进行静态分析。
图1. 应用程序Challenge 0x4界面
使用jadx反编译APP,查看反编译源代码,找到程序入口。分析代码可以发现,应用程序MainActivity类中只进行了简单的初始化界面操作,没有更多有用信息(图2)。我们再看看com.ad2001.frida0x4包中定义的另一个类Check,Check类中定义了一个get_flag方法,此方法会检查传入的参数是否等于1337,如果是则将flag解码并作为函数的返回值。但是这个类以及类中的方法并没有在程序的任何地方被使用,因此我们仅仅打开程序无法获取到flag的值。
图2. MainActivity类
public class Check {
public String get_flag(int a) {
if (a == 1337) {
byte[] decoded = new byte["I]FKNtW@]JKPFA\[NALJr".getBytes().length];
for (int i = 0; i < "I]FKNtW@]JKPFA\[NALJr".getBytes().length; i++) {
decoded[i] = (byte) ("I]FKNtW@]JKPFA\[NALJr".getBytes()[i] ^ 15);
}
return new String(decoded);
}
return "";
}
}
分析完APP可以知道,我们要找的flag藏在Check类的get_flag方法中,想要得到这个flag我们需要调用get_flag方法并将传入的参数设置为1337。在《APP动态分析系列 - Frida的基础用法(上)》用法2中我们已经讲到如何调用一个静态方法,但是这里的get_flag方法并不是一个静态方法,要调用该方法,我们首先需要创建这个方法所在类的实例,再使用该实例进行调用。让我们来编写Frida脚本实现。
首先明确,我们需要实例化的Check类和需要调用的get_flag方法,位于程序的com.ad2001.frida0x4包中,创建Check类的实例,我们需要使用Frida中的$new()方法来实例化特定类的对象,然后使用创建的实例调用get_flag方法,并将传入参数设置为1337,其代码实现如下:(值得注意的是,这里我们需要获取get_flag方法返回的flag的值,因此还需要在脚本中创建一个变量来保存并输出到运行日志中。)
/***
Java.perform 是Frida中的一个函数,用于为脚本创建特殊上下文,进入此上下文后,可以执行挂钩方法或访问Java类等操作来控制或观察应用程序的行为。
**/
Java.perform(function() {
var check = Java.use("com.ad2001.frida0x4.Check");
// 声明了一个变量check来表示目标Android 应用程序中的Java类。
// Java.use函数指定要使用com.ad2001.frida0x4包的Check类。
var check_obj = check.$new();
// $new()是Frida中的方法,用于实例化特定类的对象;
// 使用$new()方法创建Check类的实例check_obj。
var res = check_obj.get_flag(1337);
// 使用check_obj实例调用get_flag,并传入参数1337。
// 创建一个变量res来保存get_flag方法的返回值。
console.log("FLAG:" + res);
// 将保存的flag输出到脚本运行日志中。
})
进入云真机操作台启动APP,在Frida脚本功能区点击【新增脚本】,命名为Challenge_0x4_hook,将编写好的代码复制进来,点击【加载脚本】。由于创建Check类实例并调用get_flag方法的操作,不需要在程序初始化时执行,因此运行脚本时不需要重启APP。脚本运行成功后,get_flag方法执行后返回的flag将被打印到运行日志中(图3)。
图3. 运行脚本获取flag
Frida 创建类实例的脚本模板:
Java.perform(function() {
var <class_reference> = Java.use("<package_name>.<class>");
var <class_instance> = <class_reference>.$new(); // 创建类的实例
<class_instance>.<method>(); // 调用该实例中的方法
})
APP下载地址:https://github.com/DERE-ad2001/Frida-Labs/tree/main/Frida%200x5
在下载Challenge 0x5.apk文件并上传至大狗云真机操作平台后,可以看到APP程序界面依然没有任何有效信息(图4)。我们将APK放入jadx进行静态分析。
图4. 应用程序Challenge 0x5界面
使用jadx反编译APP,查看反编译源代码,找到程序入口。分析代码可以发现,应用程序MainActivity类中定义了一个未被调用的方法flag,在该方法内部,会对传入的code参数进行检查,如果code的值等于1337,就会则解密flag,设置给TxtView控件(图5)。
图5. MainActivity类
public void flag(int code) {
if (code == 1337) {
try {
SecretKeySpec secretKeySpec = new SecretKeySpec("WILLIWOMNKESAWEL".getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec iv = new IvParameterSpec(new byte[16]);
cipher.init(2, secretKeySpec, iv);
byte[] decodedEnc = Base64.getDecoder().decode("2Y2YINP9PtJCS/7oq189VzFynmpG8swQDmH4IC9wKAY=");
byte[] decryptedBytes = cipher.doFinal(decodedEnc);
String decryptedText = new String(decryptedBytes);
this.t1.setText(decryptedText);
} catch (Exception e) {
e.printStackTrace();
}
}
}
分析完APP我们发现,程序在MainActivity类中定义了一个flag方法,当该方法传入参数code的值等于1337时,会将正确的flag输出到TxtView控件上。但是该方法没有在程序的任何位置被调用,我们需要借助Frida框架来调用这个方法,以获取flag。跟上一用法相同的是我们需要通过flag方法所在的MainActivity类的实例来调用该方法,但在使用Frida脚本创建MainActivity类的实例会出现错误(图6)。
图6.创建MainActivity类实例
原因在于MainActivity是Android组件,由于Android的生命周期和线程规则,Android组件需要依赖于应用程序上下文运行,在Frida中,我们缺少必要的上下文。例如:Android UI组件通常需要具有关联Looper的特定线程,如果涉及UI任务,需要在具有活动Looper的主线程上执行。总之,创建MainActivity的实例可能需要应用处于特定状态,并通过Frida管理整个生命周期,并不建议大家这样做。
我们有一个更好的解决办法。由于创建MainActivity实例是Android应用程序生命周期的一部分,当Android应用程序启动时,系统会创建MainActivity的一个实例(或AndroidManifest.xml文件中指定的启动器活动)。因此,我们可以使用Frida获取MainActivity的实例,然后调用flag方法来获取我们的标志。
我们需要使用Frida框架,编写一个JavaScript脚本,获取程序中MainActivity类的实例,然后调用flag方法来获取标志。
首先明确,我们需要获取的MainActivity类实例和需要调用的flag方法,位于程序的com.ad2001.frida0x5包中。这次我们需要用到Frida的Java.performNow和Java.choose API(分别用于在Java运行环境中执行代码和在运行时枚举指定Java类的实例),获取应用程序创建的MainActivity类实例,然后调用该实例的flag方法,并传入参数1337的代码实现如下:
/*
Java.performNow是Frida中的一个函数,用于在Java运行时环境中执行代码。
*/
Java.performNow(function() {
Java.choose('com.ad2001.frida0x5.MainActivity', {
// Java.choose在运行时枚举作为第一个参数传入的Java类的实例。
// 第二个参数需要传入包含onMatch和onComplete两个回调函数的选项对象。
onMatch: function(instance) {
// 处理匹配到的类实例的回调函数
// instance参数表示MainActivity类的每个匹配实例
console.log("Instance found");
// 找到实例时打印一条提示信息。
instance.flag(1337);
// 调用flag方法,并传入参数1337。
},
onComplete: function() {}
// 完成操作后的回调函数
});
});
下面是关于这段代码的解释:
-
Java.performNow(...):用于在Java运行时环境中执行代码的函数。(Java.performNow和Java.perform的区别在于:Java.performNow是一个同步方法,会立即执行传入的函数,并等待函数执行完毕后才继续执行后续代码;Java.perform是一个异步方法,会将传入的函数放入Frida的队列中,等待下一个Java调用时再执行。)
-
Java.choose('com.ad2001.frida0x5.MainActivity', {...}):在运行时枚举com.ad2001.frida0x5包中MainActivity类的实例,当找到匹配的对象时,会调用onMatch回调函数,在这个回调函数中可以对找到的对象进行操作。当选择操作完成时,会调用onComplete回调函数。
-
onMatch: function(instance){...}:在 Java.choose找到匹配的对象时,用于对每个对象进行操作的回调函数,instance用于表示目标类的每个匹配实例。
-
console.log("Instance found"):在找到MainActivity的实例时,将"Instance found"打印到运行日志中。
-
instance.flag(1337):调用获取到的MainActivity实例的flag方法,并传入参数1337。
-
onComplete: function() {...}:在 Java.choose 完成操作时,用于执行清理操作或输出结束信息的回调函数。这里在搜索完成后不需要执行任何特定操作,我们将其留空。
进入云真机操作台启动APP,在Frida脚本功能区点击【新增脚本】,命名为Challenge_0x5_hook,将编写好的代码复制进来,点击【加载脚本】。虽然MainActivity实例是在程序启动时被创建,但是我们需要在实例创建成功(程序启动完成)之后才能使用Frida脚本获取实例,因此运行脚本时不需要重启APP。脚本运行成功后,成功调用flag方法并将解码后的flag输出到TxtView控件上(图7)。
图7. 运行脚本获取flag
Frida 在现有实例上调用方法的脚本模板:
Java.performNow(function() {
Java.choose('<Package>.<class_Name>', {
onMatch: function(instance) {
// 匹配到类实例时进行的操作
},
onComplete: function() {}
// 完成操作后的回调函数
});
});
03
APP下载地址:https://github.com/DERE-ad2001/Frida-Labs/tree/main/Frida%200x6
在下载Challenge 0x6.apk文件并上传至大狗云真机操作平台后,发现APP程序界面跟之前一样没有任何有效信息(图8)。直接将APK放入jadx进行静态分析。
图8. 应用程序Challenge 0x6界面
使用jadx反编译APP,查看反编译源代码,找到程序入口。分析代码可以发现,MainActivity类中同样有一个没有被调用的方法get_flag(图9)。该方法被传入了一个Checker类型的参数A,方法中会检查A参数的变量num1和num2,如果num1等于1234且num2等于4321,则会将flag解密并设置给TxtView控件。我们进一步查看Checker类中的内容。
图9.MainActivity类
public void get_flag(Checker A) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
if (1234 == A.num1 && 4321 == A.num2) {
Cipher cipher = Cipher.getInstance("AES");
SecretKeySpec secretKeySpec = new SecretKeySpec("MySecureKey12345".getBytes(), "AES");
cipher.init(2, secretKeySpec);
byte[] decryptedBytes = Base64.getDecoder().decode("QQzMj/JNaTblEHnIzgJAQkvWJV2oK9G2/UmrCs85fog=");
String decrypted = new String(cipher.doFinal(decryptedBytes));
this.t1.setText(decrypted);
}
}
Checker类中定义了两个变量,num1和num2,并且这个类在程序中没有实例(图10)。
图10. Checker类
分析完APP我们可以知道,程序中有一个没有被调用的方法get_flag,该方法对传入的Checker类型参数A进行检查,如果参数A的num1变量值等于1234且num2变量值等于4321,就会将flag解码并输出。为了得到方法中的flag,我们需要获取MainActivity类的实例来调用这个方法,同时还需要满足方法内部对传入参数A.num1和A.num2变量值的验证。因此,我们还需要创建一个Checker类的实例,并设置好实例中num1和num2变量的值,在调用get_flag方法时将这个实例作为参数传递。
我们需要使用Frida框架,编写一个JavaScript脚本,完成以下操作:
-
创建一个Check类的实例;
-
将这个实例的num1参数设置为1234,num2参数设置为4321;
-
获取程序启动时创建的MainActivity类的实例;
-
通过MainActivity类实例调用get_flag方法,并使用设置好的Check类实例作为参数。
首先明确MainActivity类和Check类位于程序的com.ad2001.frida0x6包中,上述功能的代码实现如下:
/*
Java.performNow是Frida中的一个函数,用于在Java运行时环境中执行代码。
*/
Java.performNow(function() {
Java.choose('com.ad2001.frida0x6.MainActivity', {
// Java.choose在运行时枚举MainActivity类的实例。
// 第二个参数需要传入包含onMatch和onComplete两个回调函数的选项对象。
onMatch: function(instance) {
// 处理匹配到的类实例的回调函数
console.log("Instance found");
// 处理匹配到的类实例的回调函数
var checker = Java.use("com.ad2001.frida0x6.Checker");
// Java.use函数指定要使用com.ad2001.frida0x6包的Check类。
var checker_obj = checker.$new();
// 使用$new()方法创建Check类的实例check_obj。
checker_obj.num1.value = 1234;
// 将check_obj实例的num1变量值设置为1234
checker_obj.num2.value = 4321;
// 将check_obj实例的num2变量值设置为4321
instance.get_flag(checker_obj);
// 使用check_obj作为参数;
// 通过获取到的MainActivity类的实例instance调用get_flag方法
},
onComplete: function() {}
});
});
进入云真机操作台启动APP,在Frida脚本功能区点击【新增脚本】,命名为Challenge_0x6_hook,将编写好的代码复制进来,点击【加载脚本】。由于我们需要在程序启动之后完成创建Check类实例和获取MainActivity类实例的操作,因此脚本运行时不需要重启APP。运行成功之后,完成get_flag方法调用以及对参数值的检验,flag被输出到程序界面上(图11)。
图11. 运行脚本获取flag
Frida 使用对象参数调用方法的脚本模板:
Java.performNow(function() {
Java.choose('<Package>.<class_Name>', {
onMatch: function(instance) {
var <class_reference> = Java.use("<package_name>.<class>");
var <class_instance> = <class_reference>.$new(); // 创建类的实例
/*
设置实例参数
*/
instance.<method>(class_instance); // 使用对象参数调用方法
},
onComplete: function() {}
// 完成操作后的回调函数
});
});
04
APP下载地址:https://github.com/DERE-ad2001/Frida-Labs/tree/main/Frida%200x7
下载Challenge 0x7.apk文件上传至大狗云真机操作平台,可以看到APP程序界面依旧没有任何有效信息(图12)。我们将APK放入jadx进行静态分析。
图12.应用程序Challenge 0x7界面
使用jadx反编译APP,查看反编译源代码,找到程序入口。分析代码可以发现,MainActivity类中的onCreate方法在程序启动时使用123和321两个参数创建了一个Checker对象ch,并调用flag方法传入该对象。flag方法会检查传入的Checker对象A的属性num1和num2的值,如果满足条件A.num1 > 512 && 512 < A.num2,则执行一系列加密和解密操作,并将解密后的内容设置到TextView控件t1上显示出来(图13)。我们继续分析Checker类的构造。
图13.MainActivity类
Checker类定义了两个整型成员变量num1和num2,分别用于存储整数值。类中包含了一个构造函数Checker(int a, int b),该构造函数会在类对象创建(初始化)的时候自动执行,它接受两个整型参数a和b,并将这两个参数分别赋值给类的成员变量num1和num2(图14)。
图14.Checker类
分析完APP我们发现,程序在启动的时候创建了一个Checker类的对象,并将其初始化参数num1和num2设置为123和321,然后调用flag方法传入该对象。flag方法对传入Checker对象的num1和num2变量进行验证,如果满足num1和num2均大于512,则会将解密flag并设置到TextView控件上。由于程序初始化时设置的参数123和321不满足条件,因此应用程序界面没有显示flag。为了获取这个flag我们可以Hook Checker类对象的构造函数Checker(int a, int b),将参数a,b修改为大于512的值来绕过检验。
我们需要使用Frida框架,编写一个JavaScript脚本,来Hook创建Checker类对象时执行的构造函数,并修改初始化的参数值。
首先明确,我们需要Hook的函数位于程序com.ad2001.frida0x7包中的Checker类中。Hook构造函数需要使用$init关键字,Hook创建Checker类对象执行的构造函数Checker(int a, int b),并将传入参数a,b修改为600,代码实现如下:
/***
Java.perform 是Frida中的一个函数,用于为脚本创建特殊上下文,进入此上下文后,可以执行挂钩方法或访问Java类等操作来控制或观察应用程序的行为。
**/
Java.perform(function (){
var a = Java.use("com.ad2001.frida0x7.Checker");
// 声明了一个变量a来表示目标Android应用程序中的Java类。
// Java.use函数指定要使用com.ad2001.frida0x7包中的Checker类。
a.$init.implementation = function (a,b){
// $init关键字Hook Checker类对象创建时执行的构造函数
console.log("Origin num:",a,b);
// 打印a,b变量初始化时设置的值
this.$init(600,600);
// 调用Checker类构造函数将a,b变量值修改为600
}
})
进入云真机操作台启动APP,在Frida脚本功能区点击【新增脚本】,命名为Challenge_0x7_hook,将编写好的代码复制进来,点击【加载脚本】。由于我们Hook的构造函数在程序初始化时被调用,因此运行脚本时需要【勾选重启APP】选项(图15)。脚本运行成功后构造函数中传入的a,b参数的值已经被修改为600,成功绕过flag函数对num1和num2参数的检验获取到flag(图16)。
图15.点击加载脚本勾选重启APP
图16.脚本运行成功获取flag
Frida Hook构造函数的脚本模板为:
Java.perform(function() {
var <class_reference> = Java.use("<package_name>.<class>");
<class_reference>.$init.implementation = function(<args>){
/*
我们自己的方法实现
*/
}
});
以上为本期分享的四个Frida的基础用法,本系列文章后续还会分上、下两篇介绍Frida的进阶用法。欢迎相关公检法技术工作者注册安装【无糖浏览器】,自行通过【大狗平台专区】的【云真机操作台】复现研究。大家还可以根据Frida-lab项目中的参考答案,尝试更多有趣的解题方法。
[1]用法4:https://github.com/DERE-ad2001/Frida-Labs/blob/main/Frida+0x4/Solution/Solution.md
[2]用法5:https://github.com/DERE-ad2001/Frida-Labs/blob/main/Frida+0x5/Solution/Solution.md
[3]用法6:https://github.com/DERE-ad2001/Frida-Labs/blob/main/Frida+0x6/Solution/Solution.md
[4]用法7:https://github.com/DERE-ad2001/Frida-Labs/blob/main/Frida+0x7/Solution/Solution.md
无糖浏览器-您身边的办案助手
下载地址(PC端与APP同链接):
http://browser.nosugar.tech
邀请码:注册邀请码可从已认证通过的公安民警处获得,完成注册流程并审核通过可开通完整使用权限。
如有疑问,可以扫描下方二维码进入无糖反网络犯罪研究中心。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论