Android性能优化-Frida工具篇

一、Frida是什么

Dynamic instrumentation toolkit for developers, reverse-engineers, and security researchers.
开发人员,逆向工程和安全研究人员的动态代码插桩工具。

Frida主要用于动态分析和修改运行时的程序行为(支持 Android、iOS、Windows、macOS、Linux 等平台)。它的核心功能是通过注入 JavaScript 或 Python 脚本,实时 Hook 目标应用的函数、修改内存数据或拦截通信,广泛应用于逆向工程、安全测试、漏洞挖掘等领域。

从这个描述来看,Frida主要是逆向和漏洞挖掘用的,和我们应用开发和性能优化有什么关系?事实上,这是一个极其强大,甚至某些程度上可以说无所不能的工具,为什么这么说,我们慢慢来分析。

二、Frida能做什么(基础)?

Frida可以以极低的成本,实现对Java代码的动态Hook,以及实现对Native层代码的动态Hook,举例如下:

2.1 Hook Java代码

Java 复制代码
Java.perform(function () {
    const LottieAnimationView = Java.use('com.airbnb.lottie.LottieAnimationView');
    // Hook onDraw 方法
    LottieAnimationView.onDraw.implementation = function (canvas) {
        // 调用原始方法
        return this.onDraw(canvas);
    };
    console.log("[+] LottieAnimationView.onDraw() Hook 已安装");
});

上面的代码可以实现对LottieAnimView的监听,在LottieAnimView的onDraw方法回调的时候,我们可以得到回调,可以做进一步的处理,例如判断两帧之间的时间间隔。

2.2 Hook Native代码

Java 复制代码
const openAddr = Module.findExportByName("libc.so", "open");
Interceptor.attach(openAddr, {
    onEnter: function (args) {
        const pathPtr = args[0];
        const path = pathPtr.readUtf8String();
        console.log(`[libc] open() 路径: ${path}`);
    },
    onLeave: function (retval) {
        console.log(`[libc] open() 返回值: fd=${retval}`);
    }
});

这段代码会hook libc的open方法,在打开某个文件的时候打印文件路径。

三、环境搭建与使用

3.1 环境搭建

Frida的环境搭建非常简单(需要Root手机)

3.1.1 Window端

安装python环境

复制代码
pip install frida-tools
pip install frida

执行frida --version,得到正常输出即为安装成功 执行

复制代码
adb forward tcp:27042 tcp:27042

3.1.2 Android端

github.com/frida/frida...

  • 1、github仓库的release发行版中,找到与本地frida版本一致的tag,查找此文件:
  • 2、解压得到其中的文件,将其重命名为frida-server(这一步只是用于方便使用)
  • 3、adb push frida-server data/local/tmp/frida-server
  • 4、adb shell chmod +x data/local/tmp/frida-server
  • 5、adb shell "/data/local/tmp/frida-server &"

3.2 基本用法

3.2.1 ps

arduino 复制代码
//得到手机上所有进程以及进程名,-U的意思是使用USB设备,而不是本机
frida-ps -U

记住这里的进程名,后续会用到,这里三方应用一般是中文名,系统服务是英文,是app自己设置的,不同于我们一直使用的包名。

3.2.2 加载脚本

加载脚本有两种方式,一种是直接进程存在的情况,一种是不管进程在不在,都重新拉起。 进程存在:(这种情况使用进程名,一般是中文)

复制代码
frida -U -n 抖音 test.js

重新拉起:(这种情况下,需要使用包名)

复制代码
frida -U -f com.ss.android.ugc.aweme test.js

| 注入模式 | 描述 | 命令或参数|优点|主要用途|

| --- | --- | --- | --- | --- |

| Spawn模式 | 将启动App的权利交由Frida来控制,即使目标App已经启动,在使用Frida注入程序时还是会重新启动App | 在CLI模式中,Frida通过加上 -f 参数指定包名以spawn模式操作App | 适合于需要在App启动时即进行注入的场景,可以在App启动时即捕获其行为 | 当需要监控App从启动开始的所有行为时使用 |

| Attach模式 | 在目标App已经启动的情况下,Frida通过ptrace注入程序从而执行Hook的操作 |在CLI模式中,如果不添加 -f 参数,则默认会通过attach模式注入App |适合于已经运行的App,不会重新启动App,对用户体验影响较小|在App已经启动,或者我们只关心特定时刻或特定功能的行为时使用 |

3.2.3 基础使用

1、在Java环境中运行

Java 复制代码
Java.perform(function () {

}

2、Hook Class

Java 复制代码
const TargetClass = Java.use('com.example.app.MainActivity');
TargetClass.login.implementation = function (username, password) {
    console.log(`用户名: ${username}, 密码: ${password}`);
    return this.login(username, password); // 继续原逻辑
};

3、打印堆栈

Java 复制代码
console.log(Java.use("android.util.Log").getStackTraceString(
    Java.use("java.lang.Exception").$new()
));

4、构造函数

Java 复制代码
Java.use("classname").$init(params)

5、内部类

Java 复制代码
Java.use("classname$innerclassname")

6、取变量里面的值

Java 复制代码
obj.name.value

其中name是变量名,后面需要跟一个value,才能拿到实际的值 7、枚举某个类的实例

Java 复制代码
Java.choose(className, callbacks)

例如我们想拿到Activity的实例,做一些事情,就可以用这个方式来做。

3.2.4 demo

编写一个Js文件,hook某个Class的方法,打印一行日志,将其命名为lottie.js

Java 复制代码
Java.perform(function () {
    const LottieAnimationView = Java.use('com.airbnb.lottie.LottieAnimationView');
    // Hook onDraw 方法
    LottieAnimationView.onDraw.implementation = function (canvas) {
        // 调用原始方法
        return this.onDraw(canvas);
    };
    console.log("[+] LottieAnimationView.onDraw() Hook 已安装");
});

在windows目录下,执行上面的命令即可。

四、Frida的应用

4.1 安全

目前在一些公司里面,配置了安全团队,其中有一项就是扫描应用异常的安全行为。不知道大家有没有想过,他们是如何发现一些异常问题的?例如未同意隐私协议之前不准访问网络、一些敏感信息等?并且他们甚至可以拿到应用的堆栈。可能会有人觉得是他们开发了一些非常复杂的工具、或者系统给他们开了后门。

有一个例子是安全团队扫描到我们有异常的ContentObserver调用,这里由于安全的原因就不贴截图了。大致的堆栈是我们监听了ContentObserver,报的是对媒体库的监听,没有应用的堆栈。首先安全的这个堆栈是怎么拿到的?他们正是使用Frida来实现的,只是他们监控的是实际ContentObserver发生变化的onChange,但是这个对我们没有意义,我们需要的是实际注册的地方,我们可以Hook ContentObserver的registerContentObserver方法,来得到我们实际注册的URI,用于判断URI是否敏感,也就是应用是否有敏感行为。

代码如下:

Java 复制代码
Java.perform(function() {
    const ContentResolver = Java.use('android.content.ContentResolver');
    ContentResolver.registerContentObserver.overload(
        'android.net.Uri',
        'boolean',
        'android.database.ContentObserver'
    ).implementation = function(uri, notifyForDescendants, observer) {
        console.log('  URI: ' + uri.toString());
        return this.registerContentObserver(uri, notifyForDescendants, observer);
    };
});

除了上面的应用之外,在安全领域,Frida被大量使用,检测网络、定位等功能,不一而足。

4.2 逆向

除了安全领域,Frida也广泛的应用于逆向领域,可以用于分析混淆后的代码等。比如我们想要分析某个应用在某个地方的执行结果,例如在混淆代码中,看到某个地方的代码,想要知道执行的结果,通过常规的方式很难达成,但是可以通过Frida来达成,直接Hook这个类的方法, 在收到调用之后,可以执行自己的逻辑,将内容打印出来,并执行原有的方法。

这个实际上也可以应用于我们来debug自己的Release包,有的时候我们会遇到一些非常棘手的问题,只在Release包复现,但是debug包无法复现,这个时候我们就可以通过Frida来编写一些脚本,hook运行时,通过添加日志的形式来进行debug,避免大量没有意义的编译与验证。

举个简单的例子,遇到一个Release包才能复现的问题,我们需要在一些关键方法加日志,判断每次方法调用是否是同个对象(需要考虑混淆),代码如下:

Java 复制代码
Java.perform(function () {
    const HandleBitmap = Java.use('com.xx.xx.xx.xx.xx.xx$handleBitmap$1');
    const UpdateLocation = Java.use('com.xx.xx.xx.xx.xx.xx$updateLocation$1');
    HandleBitmap.invoke.overloads[0].implementation = function () {
        console.log("handle bitmap : " + this.this$0.value.hashCode());
        return this.invoke();
    };
    UpdateLocation.invoke.overloads[0].implementation = function () {
        console.log()
        const view = this.this$0.value;
        if (view.k.getSecond().intValue() != this.$mY.value) {
            console.log("update location : " + this.this$0.value.hashCode());
        }
        return this.invoke();
    };
});

这样我们在updateLocation的时候,可以知道每次调用的是否是同个对象,佐证一些猜想,从而实现我们定位Bug的目的,编写Js脚本的时间非常短,借助AIGC,可能在一两分钟内就可以完成编写与调试。

五、Frida的实现原理

5.1 核心架构

Frida 的架构分为三部分:

  • 主机端(Host):运行 Python/JavaScript 脚本,控制分析逻辑。
  • 设备端(Target):运行 frida-server 或 frida-gadget,负责注入目标进程。
  • 通信桥梁:通过 TCP/WebSocket 在主机和设备间传递数据和指令

​5.​2 进程注入机制​​

​5.2.1 注入方式​​

frida-server(需 Root)

以高权限运行在设备上,通过 ptrace 或 LD_PRELOAD 将 Frida 的运行时库注入目标进程。

流程

主机通过 adb 连接 frida-server。 frida-server 调用 fork() 创建子进程,附加到目标进程(如 zygote 或 App 进程)。 注入 frida-agent.so(核心引擎)到目标进程内存。

frida-gadget(非 Root)

frida-gadget.so 打包进目标 APK(需重签名),随应用启动自动加载。

限制:只能注入当前应用,无法控制系统进程。

5.2.2 注入后的内存布局

目标进程的内存中会加载以下模块:

frida-agent.so:核心引擎,负责脚本解析、Hook 管理和通信。

JavaScript 运行时:基于 V8 引擎(或 Duktape 轻量版),执行用户脚本。

动态生成代码:用于 Hook 的跳板代码(Trampoline)和内存补丁。

5.3 动态插桩原理

5.3.1 Java 层 Hook​​

实现方式 :通过修改 Android ART/Dalvik 虚拟机的运行时结构。 方法替换 :替换 Method 对象的 entry_point 或 code_item,指向 Frida 生成的代理代码。 JNI 桥接:将 Java 方法调用转发到 JavaScript 回调。

​5.3.2 Native 层 Hook​​

Inline Hook :修改函数入口的机器码,跳转到自定义代码。 ARM/Thumb 模式 :处理指令对齐和条件跳转。 指令修复 :保存被覆盖的原始指令,在回调中恢复执行。 PLT/GOT Hook:修改动态链接表的函数地址(适用于 libc.so 等)。

六、总结

Frida是非常强大的动态代码插桩工具,可以用其开发出强大的分析工具和性能优化工具,是我们进行优化的利器。反过来说,我们的应用很容易被Frida进行攻破,如果后续有更高的安全要求,这里是绕不过去的门槛。

参考

1、www.freebuf.com/articles/mo... 2、cloud.tencent.com/developer/a...

相关推荐
安之若素^1 分钟前
启用不安全的HTTP方法
java·开发语言
ruanjiananquan997 分钟前
c,c++语言的栈内存、堆内存及任意读写内存
java·c语言·c++
chuanauc34 分钟前
Kubernets K8s 学习
java·学习·kubernetes
一头生产的驴1 小时前
java整合itext pdf实现自定义PDF文件格式导出
java·spring boot·pdf·itextpdf
YuTaoShao1 小时前
【LeetCode 热题 100】73. 矩阵置零——(解法二)空间复杂度 O(1)
java·算法·leetcode·矩阵
zzywxc7871 小时前
AI 正在深度重构软件开发的底层逻辑和全生命周期,从技术演进、流程重构和未来趋势三个维度进行系统性分析
java·大数据·开发语言·人工智能·spring
YuTaoShao3 小时前
【LeetCode 热题 100】56. 合并区间——排序+遍历
java·算法·leetcode·职场和发展
程序员张33 小时前
SpringBoot计时一次请求耗时
java·spring boot·后端
llwszx6 小时前
深入理解Java锁原理(一):偏向锁的设计原理与性能优化
java·spring··偏向锁
云泽野7 小时前
【Java|集合类】list遍历的6种方式
java·python·list