Frida 是一个知名的 Hook 框架
接下来我们主要以 Android Hook 为主题进行讲解

环境配置

分别在 PC 和目标 Android 上配置 Frida:

1
2
3
4
5
# 我使用 UV 管理 Python,因此没有一个全局的 Python
# uv venv --seed --clear venv
# venv\Scripts\activate

pip install frida frida-tools

记住 Frida 版本:

1
2
3
4
$ pip show frida

Name: frida
Version: 17.5.1

在 ADB shell 中查看 Android 设备架构:

1
2
3
$ uname -m

aarch64
Note

一般来讲,模拟器采用 x86-64,真机采用 arm64(即此处的 aarch64)

Releases · frida/frida 下载对应版本的 Frida-server,例如此处应下 frida-server-17.5.1-android-arm64.xz。

解压得到的文件传输到 Android 上:

1
adb push frida-server-17.5.1-android-arm64 /data/local/tmp/

在 Android 上启动服务端:

1
2
3
4
5
6
adb shell
su # 需要在 Root 下运行,否则 SELinux 会报错
cd /data/local/tmp
chmod +x ./frida-server-17.5.1-android-arm64 # 赋予执行权限
./frida-server-17.5.1-android-arm64
# 无报错即服务端正常运行

现在就可以用 Frida 了 😊

Frida 调试基础

两种模式:Spawn(生成)和 Attach(附加)。Spawn 模式是从头启动应用并调试,Attach 模式是在应用运行时才开始调试。

Spawn 模式:

1
frida -U -f <TARGET> -l <SCRIPT>
  • TARGET:APP 包名或可执行文件名
  • SCRIPT:用于调试的 Python, JS 或 TS 脚本。自己写或者用已有的。如果不附带脚本,则进入交互式调试。

TARGET 需要的包名可以通过 frida-ps -Ua(运行中的应用)/frida-ps -Uai(安装的应用)得到。

Spawn 方式启动后,会立刻暂停运行。需要继续运行,需要手动输入:%resume

旧版本有一个 --no-pause 会自动运行,但新版本已不支持。


Attach 模式:

1
frida -U -N TARGET -l SCRIPT

怎么写好一个调试脚本

脚本主要有三类内容:

  • Interceptor(拦截器):实现 Hook 函数、打印参数等调试操作。每次匹配到(对应函数),都会触发执行代码。
  • ApiResolver(解析器):模糊查找相关函数。只有第一次(解析时)匹配到,才会执行。
  • Stalker(跟踪器):跟踪代码的实际运行的过程。期间可以打印和查看对应的值,便于实现调试真正代码运行的逻辑。

对于 JS 脚本,一个简单的 CTF 示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 所有 Java API 调用必须在 Java.perform() 回调中执行
Java.perform(function() {

// 获取目标类的引用
var Vault = Java.use("com.heroctf.freeda1.utils.Vault");

// 尝试调用目标方法并处理异常。延迟 1000ms 执行以减少 bug
setTimeout(function() {
try {
var flag = Vault.get_flag();
console.log("[SUCCESS] Flag: " + flag);
} catch(e) {
console.log("[ERROR] " + e);
}
}, 1000);
});

这个的脚本的目的是直接获取 Vault 类下的 get_flag() 方法的输出,得到 flag。

一些常用方法:

API 名称 描述
Java.use(className) 获取指定的 Java 类并使其在 JavaScript 代码中可用。
Java.perform(callback) 确保回调函数在 Java 的主线程上执行。
Java.choose(className, callbacks) 枚举指定类的所有实例。
Java.cast(obj, cls) 将一个 Java 对象转换成另一个 Java 类的实例。
Java.enumerateLoadedClasses(callbacks) 枚举进程中已经加载的所有 Java 类。
Java.enumerateClassLoaders(callbacks) 枚举进程中存在的所有 Java 类加载器。
Java.enumerateMethods(targetClassMethod) 枚举指定类的所有方法。

绕过检测

要用 Frida-server 调试的前提就是 Root,但显然大部分应用都不希望自己被 Frida 或者 Root 调戏,因此会做 Root 检测,一检测到特征就停止程序运行。

但是,代码总是有执行顺序的,只要我在检测前就把特征检测方法给 Hook 掉,你不就检查不出来了吗?另一种就是 “打入内部”,将 Frida-gadget 植入程序,可以实现免 Root 的 Hook。

Security
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.heroctf.freeda2.utils;

import android.content.Context;
import com.scottyab.rootbeer.RootBeer;

/* compiled from: r8-map-id-baa4c77f810b701c3077d4ce68a3d5b79ee91034c03e266e8ba1aed7e464c1a1 */
/* loaded from: classes.dex */
public final class Security {
private Security() {
}

public static boolean detectRoot(Context context) {
return new RootBeer(context).isRooted();
}
}
hook.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Java.perform(function() {
console.log("[+] Bypassing root detection...");

// 绕过 RootBeer
var RootBeer = Java.use("com.scottyab.rootbeer.RootBeer");
RootBeer.isRooted.implementation = function() {
console.log("[+] RootBeer.isRooted() bypassed");
return false;
};

// 绕过 Vault 构造函数检测
var Vault = Java.use("com.heroctf.freeda2.utils.Vault");
Vault.$init.implementation = function() {
console.log("[+] Vault constructor bypassed");
// Don't call original constructor
};

setTimeout(function() {
try {
var flag = Vault.get_flag();
console.log("[SUCCESS] Flag: " + flag);
} catch(e) {
console.log("[ERROR] " + e);
}
}, 1000);
});

RootBeer 是一个简单的 Root 权限检查库。作者也在 README 中坦陈 RootBeer 可以被绕过(查看 Bypassing Android’s RootBeer Library — Part 1

本题仅使用 RootBeer 的综合检测方法 isRooted() 来检测 Root,这倒是为我们省去了不少麻烦,因为我们只需要 Hook isRooted() 就行了。

Native Hook

与 Java 层不同,Native 层的函数是分散在 so 文件中的。在 Hook 前,Frida 需要找到目标函数,因为 so 是动态加载的。