在《APP动态分析系列 – Frida的进阶用法(上)》中,我们对APP动态分析的技术攻坚案例进行了探讨——运用Frida 的进阶用法:Hook native 函数和更改 native 函数的返回值来解决两种典型的复杂分析场景。本篇文章我们将继续分享会在以下两种分析场景中运用到的 Frida 进阶用法:
-
调用 native 函数:例如,在分析隐藏恶意功能时,某些 APP会将发送短信、拦截电话、远程控制等功能实现代码包含在 native 函数中,隐藏其真实目的。我们需要调用 native 函数来观察其隐藏功能的具体实现。
-
使用 ARM64Writer 修改指令:例如,在绕过安全检测时,某些包含安全检测功能的 APP,检测到调试器或者虚拟环境时,会触发特定逻辑导致应用无法正常运行。我们需要使用对应的 ARM64Writer 修改指令绕过安全检测。
使用 Frida 通常需要完成一些前置条件,如安装Python、Frida、安卓模拟器等环境(可参考官方文档:https://frida.re/docs/home/),而基于【大狗涉网线索分析平台】的【云真机操作台】的 Frida 脚本功能,省去了复杂的安装过程,实现一键运行 Frida 脚本,随时在无糖浏览器中对恶意 APP 进行动态调试,大大提高了工作效率。
在这个基础上,要完成上述两种分析场景,我们还需要掌握,Frida 调用 native 函数、修改内存页的保护属性、修改 ARM64 指令等技术和方法。接下来,我们将对这些进阶用法进行讲解和演示。
本篇文章将继续以 Frida-lab 中的题目为例,为大家介绍大狗平台云真机操作台中 Frida 脚本调用 native 函数和使用 ARM64Writer 修改指令的两种进阶用法和相关背景知识。
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)
-
IDA 反汇编工具:ida 是一款功能强大的反汇编工具,用于分析和逆向工程二进制文件,被广泛用于软件漏洞分析、恶意代码分析、逆向工程等领域。(官方网站:https://www.hex-rays.com/products/ida/)
-
JavaScript 脚本:我们将使用 JavaScript API 来完成 Frida 脚本的编写。(API文档地址:https://frida.re/docs/javascript-api/)(值得注意的是,Frida也支持Python。)
-
Frida框架:本次我们使用【大狗涉网线索分析平台】-【云真机操作台】中引入的 Frida 脚本功能。
-
了解 Frida 框架的基本原理和架构。
-
了解 Hook 技术拦截和修改函数或方法的基础知识。
-
掌握使用 jadx 进行逆向工程的基础知识。
-
掌握使用 IDA 进行 x86/ARM64 反汇编的基础知识。
-
具备理解 Java 代码的能力。
-
具备编写小型 JavaScript 代码片段的能力。
APP下载地址:https://github.com/DERE-ad2001/Frida-Labs/tree/main/Frida%200xA
在下载 Challenge 0xA.apk 文件并上传至大狗云真机操作平台后,可以看到 APP 程序只有一个静态页面,没有其他按钮和输入框(图1)。我们使用 jadx 对 APP 进行静态分析。
图1. 应用程序Challenge 0xA 界面
使用 jadx 反编译 APP,查看反编译源代码,找到程序入口(图2)。可以看到应用程序的 MainActivity类中,同样声明了一段native功能:在程序开始时定义了返回值为字符串类型的 native 函数 stringFromJNI,它不接受任何参数;在程序结尾处加载 frida0xa 动态链接库,用于实现 native 函数。MainActivity 类中还定义了onCreate方法,在程序加载时调用 stringFromJNI 函数,将函数返回的 “Hello Hackers”文本设置给 TextView 控件。接下来,我们使用 IDA 对 frida0xa 动态链接库进行分析。
图2. 程序入口
public final native String stringFromJNI();
...
static {
System.loadLibrary("frida0xa");
}
public void onCreate(Bundle savedInstanceState) {
...
activityMainBinding.sampleText.setText(stringFromJNI());
}
将 Challenge 0xA.apk 文件解压后在 .libarm64-v8a 目录下找到 libfrida0xa.so 文件,使用 IDA 进行分析,我们可以在函数列表窗口发现两个重要的函数Java_com_ad2001_frida0xa_MainActivity_stringFromJNI()和get_flag()(图3)。查看stringFromJNI函数的反编译伪代码,可以发现这个函数被调用后会返回一个”Hello Hackers” 文本(图4),用于显示在 TextView 控件中。而 get_flag 函数未在 Java 空间中声明,也没有从库中的任何位置调用,我们可以通过检查其交叉引用来确认,它唯一的引用位于 (Frame Description Entry) FDE 表中(图5)。观察get_flag 函数的反编译伪代码,可以发现,它接受两个int 类型的参数,并检查这两个参数加起来是否等于3,如果等于,就将硬编码形式的 flag 进行解密,然后打印到 Android 日志中(图6)。
图3. 反汇编函数名
图4.stringFromJNI函数的反编译伪代码
图5. get_flag 函数交叉引用
图6. get_flag 函数反编译伪代码
分析完 APP 反编译源代码和 frida0xa 动态链接库反汇编代码,我们可以知道,程序在加载时调用了frida0xa 库中的 stringFromJNI 函数获取”Hello Hackers” 文本,显示在应用程序界面中。frida0xa 库中还有一个未被调用的 get_flag 函数,该函数接收两个 int 类型的参数,如果这两个参数值相加等于3,则会将 flag 解码打印到 Android 日志中。为了获取这个 flag ,我们需要使用 Frida 脚本,调用frida0xa 库中的 native 函数 get_flag,并传入两个加起来等于3的参数。
我们需要使用Frida框架,编写一个 JavaScript 脚本,调用 frida0xa 库中的 get_flag 函数,并为它传入两个加起来等于3的参数。
要使用 Frida 脚本调用 native 函数,我们需要创建一个 NativePointer 对象,将要调用的 native 函数地址传递给 NativePointer 构造函数。然后,我们需要创建一个 NativeFunction 对象,来表示我们要调用的实际 native 函数。NativeFunction 对象的第一个参数应是 NativePointer 对象,第二个参数是 native 函数的返回类型,第三个参数是要传递给 native 函数参数的数据类型列表。
首先,我们获取 get_flag 函数地址,在《APP动态分析系列 – Frida的进阶用法(上)》中我们讲过,可以通过 Module.getBaseAddress API 获取 frida0xa 库的基地址,再加上 get_flag 函数的偏移量来获取 get_flag 函数的地址,我们在IDA中可以观察到 get_flag 函数的偏移量为“0x1DD60”(图7)。因此,获取get_flag 函数的地址的 Frida 脚本可以这样写:
var adr = Module.findBaseAddress("libfrida0xa.so").add(0x1DD60)
图7. get_flag函数偏移量
调用 get_flag native 函数并传入参数的 Frida 脚本如下:
var adr = Module.findBaseAddress("libfrida0xa.so").add(0x1DD60);
// 获取 get_flag() 函数地址。
var get_flag_ptr = new NativePointer(adr);
// 创建一个 NativePointer 对象,用于操作和访问 get_flag() 函数内存地址。
const get_flag = new NativeFunction(get_flag_ptr, 'void', ['int', 'int']);
// 创建一个名为 get_flag 的 NativeFunction 对象,实现对 native 函数的调用和控制。
// get_flag_ptr:表示 使用 get_flag_ptr NativePointer对象;
// 'void':表示函数的返回值类型为viod;
// ['int', 'int']:表示传递给函数的参数类型为两个int类型的参数。
get_flag(1,2);
// 调用 get_flag 函数,传入两个参数1,2。
进入【云真机操作台】启动 APP,在 Frida 脚本功能区点击【新增脚本】,命名为Challenge_0xA_hook,将编写好的代码复制进来,点击【加载脚本】(图8)。脚本运行成功后,可以在【云真机操作台 】的 APP 【运行日志】中看到解密后输出的 flag。
图8. 加载脚本
图9. 获取flag
Frida 调用 native 函数脚本模板:
var native_adr = new NativePointer(<address_of_the_native_function>);
// 创建一个 NativePointer 对象,用于操作和访问 <address_of_the_native_function>内存地址。
const native_function = new NativeFunction(native_adr, '<return type>', ['argument_data_type']);
// 创建一个 NativeFunction 对象,实现对 native 函数的调用和控制。
// native_adr:使用native_adr NativePointer对象;
// '<return type>':函数的返回值类型;
// ['argument_data_type']:传递给函数的参数类型列表。
native_function(<arguments>);
// 调用 native_function 函数,如果需要,可以传递<arguments>参数。
APP下载地址:https://github.com/DERE-ad2001/Frida-Labs/tree/main/Frida%200xB
在下载 Challenge 0xB.apk 文件并上传至大狗云真机操作平台后,可以看到应用程序界面有一个“CLICK ME”按钮,但是点击这个按钮程序没有任何显示(图10)。我们使用 jadx 对 APP 进行静态分析。
图10. 应用程序Challenge 0xB 界面
使用 jadx 反编译 APP,查看反编译源代码,找到程序入口(图11)。可以看到在应用程序的 MainActivity 类中,定义了一个 native 函数 getFlag,该函数不接受任何参数也没有返回值,并在程序结尾处使用 System.loadLibrary 函数加载 frida0xb 动态链接库。MainActivity 类中定义的 onCreate 方法监听按钮的点击,点击按钮时会通过 lambda 表达式调用 onCreate$lambda$0 方法,然后在onCreate$lambda$0 方法中调用 getFlag 函数。接下来,我们使用 IDA 对 frida0xb 动态链接库进行分析,查看 getFlag 函数是如何实现的。
图11. 程序入口
public final native void getFlag();
...
static {
System.loadLibrary("frida0xb");
}
public void onCreate(Bundle savedInstanceState) {
...
btn.setOnClickListener(new View.OnClickListener() { // from class: com.ad2001.frida0xb.MainActivity$$ExternalSyntheticLambda0
@Override // android.view.View.OnClickListener
public final void onClick(View view) {
MainActivity.onCreate$lambda$0(MainActivity.this, view);
}
});
}
/* JADX INFO: Access modifiers changed from: private */
public static final void onCreate$lambda$0(MainActivity this$0, View it) {
Intrinsics.checkNotNullParameter(this$0, "this$0");
this$0.getFlag();
}
图12. getFlag 函数反编译伪代码
图13. getFlag 函数流程控制
分析完代码我们可以发现,程序在点击按钮时调用了 getFlag 函数,该函数在 native 代码实现过程中,有一个永久为假的条件跳转,导致解密 flag 并输出的代码块未被执行。为了获取这个 flag ,我们需要使用 Frida 脚本,修改B.NE(跳转到程序结尾)这条指令,让它不执行,以绕过条件跳转来执行解码和输出 flag 的程序。
我们需要使用Frida 框架,编写一个 JavaScript 脚本,将 native 函数 getFlag 中的 B.NE (条件分支跳转指令)修改为 Nop(空操作指令),在Frida中修改汇编指令需要使用ARM64Writer 类。
Arm64Writer 类是 Frida 中用于编写 ARM64 汇编指令的辅助工具,它提供了一组方法来生成 ARM64 指令,并将其写入到给定地址的内存中。以下是一些 Arm64Writer 类的基础方法:
-
new Arm64Writer(codeAddress):创建一个新的 Arm64Writer 实例,用于编写 ARM64 指令, codeAddress 是要写入的内存地址。
-
putNop():用于向 Arm64Writer 实例中指定的内存地址中添加一条 Nop 指令。
-
flush():将已添加的 ARM64 指令写入内存,该方法必须在所有指令都被添加后调用。
-
dispose():释放与 Arm64Writer 实例关联的资源。
(官方文档地址:https://frida.re/docs/javascript-api/#arm64writer)
首先,我们需要知道 B.NE 指令的地址,该地址同样可以通过Module.getBaseAddress API 得到 frida0xb 库的基地址,再加上 B.NE 指令的偏移量来获取。我们可以在IDA中查看 B.NE 指令地址的偏移量为”0x15248″(图14)。然后创建一个 ARM64Writer 类的实例,将获取到的 B.NE 指令的地址作为传入参数,再调用 ARM64Writer 实例的 putNop 方法在 B.NE 指令的地址上写入一条 Nop 指令,覆盖掉原来的指令。接着调用 ARM64Writer 实例的 flush 方法将修改后的指令写入内存中。
图14. B.NE 指令偏移量
为了绕过应用程序中的内存分页保护机制,成功修改并运行 native 代码中的指令,我们还需要使用 Frida 中的 Memory.protect 函数,将指定内存区域的保护属性修改为”rwx”(可读可写可执行)。
在Frida中,Memory.protect 函数用于修改内存页的保护属性,以控制对内存的访问权限。这个函数可以用来修改目标进程中的内存页,例如将内存页设置为可读、可写、可执行等。Memory.protect 函数的语法如下:
Memory.protect(ptr, size, protection)
-
ptr: 表示要修改保护属性的内存地址,通常是一个指向目标内存区域的指针。
-
size: 表示要修改保护属性的内存区域的大小,以字节为单位。
-
protection: 表示要设置的内存保护属性。
实现上述所有功能的 Frida 脚本如下:
var BNE_adr = Module.getBaseAddress("libfrida0xb.so").add(0x15248);
//获取 B.NE 指令地址。
Memory.protect(BNE_adr,0x1000,"rwx");
// 将对应内存区域的保护属性修改为可读可写可执行。
var writer = new Arm64Writer(BNE_adr);
// 创建 ARM64Writer 类的实例 writer,将 BNE_adr 作为写入的内存地址。
try{
writer.putNop();
// 在原本 B.NE 指令的地址上写入一条 Nop 指令,替换原本的 B.NE 指令
writer.flush();
// 将修改后的指令写入内存中
console.log("Command modification successful.");
// 指令修改完成后打印一条提示语。
}finally {
writer.dispose();
// 释放与 writer 实例关联的资源。
}
进入【云真机操作台】启动APP,在Frida脚本功能区点击【新增脚本】,命名为Challenge_0xB_hook,将编写好的代码复制进来,点击【加载脚本】。脚本运行成功后,getFlag 函数中的 B.NE 指令被修改为 Nop 指令,程序能够在 getFlag 函数调用时执行到解密并输出 flag 的代码(图15)。我们再次点击“CLICK ME”按钮,触发应用程序对 getFlag 函数的调用,此时可以在 APP 运行日志中看到解密后输出的 flag(图16)。
图15. 运行脚本
图16. 点击按钮输出flag
Frida使用ARM64Writer修改指令的脚本模板:
var writer = new ARM64Writer(<address_of_the_instruction>);
// 创建一个Arm64Writer类实例,用于编写ARM64指令,codeAddress是要写入的内存地址。
try {
/*
我们自己的指令实现
*/
writer.flush();
// 将修改后的指令写入内存中
} finally {
writer.dispose();
// 释放与 ARM64Writer 实例关联的资源。
}
无糖浏览器的新注册用户登录即可免费试用【云真机操作台】一天!如需申请更多权限可以在【无糖浏览器】首页 – 打开右下角【问问元芳】控件 – 点击【转人工】,联系在线客服协助您开通试用权限。欢迎相关公检法技术工作者注册安装【无糖浏览器】,自行通过【大狗平台专区】的【云真机操作台】进行复现研究。大家还可以根据参考链接中Frida-lab项目给出的参考答案,尝试更多有趣的解题方法。
以上为本期分享的两个 Frida 进阶用法,这也是Frida 脚本使用系列文章分享的最后两种用法。本系列教程内容与日常技术攻坚案例紧密切合,掌握 Frida 的基础及进阶用法后,大家可以尝试在 APP 动态分析实战中运用起来。如果有非常复杂、难以研判的 APP,可以上传大狗平台【云真机操作台】后,在【联系客服】处添加客服钉钉号码进行技术交流。本系列教程后续还会以视频的形式上架到【知更-反网络犯罪实战训练场】,欢迎大家学习交流~
[1]进阶用法3:https://github.com/DERE-ad2001/Frida-Labs/blob/main/Frida%200xA/Solution/Solution.md
[2]进阶用法4:https://github.com/DERE-ad2001/Frida-Labs/blob/main/Frida%200xB/Solution/Solution.md
无糖浏览器-您身边的办案助手
下载地址(PC端与APP同链接):
http://browser.nosugar.tech
邀请码:注册邀请码可从已认证通过的公安民警处获得,完成注册流程并审核通过可开通完整使用权限。
如有疑问,可以扫描下方二维码进入无糖反网络犯罪研究中心。
原文始发于微信公众号(无糖反网络犯罪研究中心):APP动态分析系列 – Frida的进阶用法(下)