Skip to main content

YaloisBlog

安卓逆向——从0学习Frida(配合FridaLab)

Table of Contents

一年前还是会一点的,过了这么久忘了挺多,重新学一下。写一个博客来记录,往后也可以给学弟学妹作为参考。

## 0.基础配置

# 安装配置Android模拟器

本文使用MuMu模拟器-https://mumu.163.com/。

  1. 在主页下载之后,安装打开。

image-20250414141049921

  1. 在设置中打开root权限

image-20250414141121775

勾选之后重启模拟器。

image-20250414141149527

  1. 通过adb连接模拟器

image-20250414141234582

打开文件所在位置,找到自带的adb(就不用去额外下载了)

image-20250414141330676

在这里目录下打开cmd,Shift+鼠标右键进入终端。

image-20250414141446085

直接在cmd调用是没有的,可以把adb加到环境变量里

image-20250417151844048

image-20250417151938957

image-20250417152000058

添加完之后就能在cmd随时随地调用了。

image-20250417152143329

Q:MuMu模拟器如何启动adb调试?

A:官方文档https://mumu.163.com/help/20230214/35047_1073151.html,

从官方文档可以知道,可通过模拟器右上角菜单-问题诊断,获取ADB调试端口。

image-20250414141911977

image-20250414141937475

在终端执行adb connect 127.0.0.1:16385

image-20250414142100434

连接成功之后就可以用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

image-20250414140114554

看到当前的frida版本。然后去github下载对应版本的frida-server并放到安卓设备上。

https://github.com/frida/frida/releases

这里也是16.7.10。

如果安装其他版本的话,找到其他版本的release,然后下载第一个whl文件,用python安装。然后再下载对应的server。

image-20250414135935976

image-20250414144538952

不知道的可以执行adb shell getprop ro.product.cpu.abi来查看

image-20250417152413074

image-20250414144852482

下载之后解压出来,这里解压到桌面上。(当然也可以直接传xz文件,然后shell内xz -d进行解压)

回到刚才的adb命令传文件到模拟器上。

adb push <电脑文件路径> <模拟器文件路径>

image-20250414145057718

我传到了/sdcard/frida。再用adb shell连接模拟器的shell,安卓底层是linux,所以用linux命令。

image-20250414145230337

因为这个目录有权限问题,所以得复制到另一个目录。

先执行su命令进入root用户,这里一般模拟器会弹出对话框,点击允许。

cp /sdcard/frida /data/local/tmp

然后cd /data/local/tmp进入目录

chmod +x frida然后运行frida-server

## 运行frida-server

其实之前这里命名为frida不太合适,可以改为frida-server

image-20250417153931829

使用./frida-server -h查看启动参数

image-20250417154008595

介绍最简单的两种,剩下自行摸索

直接启动:./frida-server
启动到后台:./frida-server -D

直接启动是挂在前台的,没有输出,无法继续执行shell了。Ctrl+C终止进程。

后台启动不影响shell的使用,ps -ef可以看到,可以使用kill 2771终止进程。

image-20250417154322044

确保程序在运行就可以开始做题了。

## 版本问题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页面之后下载。

image-20250417160302848

image-20250417160055755

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

image-20250417154826707

看一个仓库先看README文件。这里是每个挑战的知识点。第一关就是frida的安装和hook一个方法。

image-20250417154958699

image-20250417155313743

每个目录都有apk安装包和一个教程。

# 0x1 Frida setup, Hooking a method

第一关,首先安装apk文件,并用jadx反编译。

image-20250417160507316

使用adb命令安装,adb install [apk文件]

image-20250417192723862

观察页面,发现页面由三个组件构成。逻辑就是用户输入文本,然后点击按钮,内容正确就显示flag。

然后jadx反编译apk。

image-20250417165527629

image-20250417165556422

可以看到package和activity,MainActivity就是打开程序后的主界面。

知道了package然后从源代码找到MainActivity的代码。

image-20250417165737514

如何分析MainActivity的代码呢。了解一下Activity的生命周期。

image-20250417170024490

Activity被启动后,会先执行onCreate的内容,然后一般开发者会在onCreate绑定界面上的组建到对应的类中。

分析代码。解法很多!

image-20250417171843593

了解一下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

image-20250417173409611

## 解法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触发。

image-20250417174314687

知道了随机数就可以知道应该输入什么内容了。输入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

image-20250417193207674

界面很简单,只有一个文本框,HOOK ME!拖入Jadx。

老规矩,先进入AndroidManifest.xml找package和主要的Activity。

image-20250417193426022

image-20250417193451462

只有一个简单的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()

image-20250417194215182

# 0x3 Changing the value of a variable

类加载的准备阶段会给所有static 成员在方法区中分配内存

image-20250417200513676

安装完之后打开,点击按钮说是要TRY AGAIN。上jadx。

image-20250417200627024

说是要Checker.code==512,Ctrl+左键点击Checker跳转到对应的类文件。

image-20250417200750527

里面有一个静态属性和一个静态方法。题目是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。

image-20250417201257047

方法二的代码。

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

image-20250417202017445

还是jadx。BUT MainActivity没啥东西。

image-20250417202116692

image-20250417202206843

还是有个Check的。

image-20250417202220661

思路就是触发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 包名,然后再直接输入代码。

image-20250417202545469

# 0x5 Invoking methods on an existing instance

如题所示,在一个存在的实例上调用方法。调用MainActivity的flag方法就可以了。

但是MainActivity是一个界面,实例化起来很麻烦。不能用上一题的方法。

直接获取存在的MainActivity并调用。

image-20250417202749634

从Solution中学习了两个函数。S

  • Java.performNow:该函数用于在Java运行时上下文中执行代码。

  • Java.choose:运行时枚举指定Java类(通过第一个参数提供)的实例。该函数会遍历内存中已加载目标类的所有实例。

choose不知道,看看api。

https://frida.re/docs/javascript-api/

image-20250417204812143

最终代码如下:

当匹配到了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。

image-20250417205647373

如果写在脚本里就不行了frida -U -f com.ad2001.frida0x5 -l MyScript/1.js

image-20250417205718101

会报错的。然后需要跟第二个题目一样,加个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。

image-20250417211719635

image-20250417211822748

这题结合了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);
});

image-20250417212655417

# 0x7 Hooking the constructor

# 0x8 Introduction to native hooking

# 0x9 Changing the return value of a native function

# 0xA Calling a native function

# 0xB Patching instructions using X86Writer and ARM64Writer

# 未完待续