67、Android逆向(三):Frida框架使用、动态Hook Java层

好,咱们继续。前两章我们把Android逆向的基础环境、静态分析、Smali语法都过了一遍。说实话,静态分析就像看一本没有目录的书——你能读到内容,但很难快速定位关键逻辑。这时候就需要动态分析登场了。

Frida,就是动态分析领域里的一把瑞士军刀。我个人习惯把它叫做「逆向界的Chrome DevTools」——你可以在运行时注入JavaScript代码,实时观察、修改App的行为。今天我们就来聊聊怎么用Frida Hook Java层的方法。

Frida是什么?为什么选它?

Frida是一个动态插桩框架。说白了,它允许你往正在运行的进程里注入一段JavaScript代码,然后这段代码可以拦截函数调用、修改返回值、打印调用栈……几乎无所不能。

我当年第一次接触Frida时,心里想的是:「这玩意儿合法吗?」后来发现,它确实是安全研究的标准工具。相比Xposed需要重启设备、修改系统分区,Frida完全在用户态工作,灵活得多。

核心优势:

  • 无需root?其实还是需要,但可以通过frida-gadget注入到非root设备
  • 支持热插拔——改代码不用重启App
  • JavaScript编写脚本,上手门槛低
  • 同时支持Java层和Native层Hook

环境搭建

嗯,这里要注意。Frida的环境分两部分:PC端和手机端。

PC端安装很简单:

pip install frida-tools

手机端需要运行一个frida-server。记得版本一定要匹配!我曾经因为版本不一致,折腾了整整一个下午才发现问题。

# 查看手机架构
adb shell getprop ro.product.cpu.abi

# 下载对应版本的frida-server
# 解压后push到手机
adb push frida-server /data/local/tmp/
adb shell chmod 755 /data/local/tmp/frida-server
adb shell /data/local/tmp/frida-server &

避坑指南: 我曾经遇到过frida-server启动后,手机端显示运行正常,但PC端就是连不上。后来发现是端口被占用了。建议用 frida-server -l 0.0.0.0:27042 指定端口。

第一个Hook脚本:Hello World

咱们先来个最简单的——Hook一个Java类的静态方法。假设目标App里有这样一个类:

public class Utils {
    public static String getSecret() {
        return "this_is_secret";
    }
}

我们的目标是让 getSecret() 永远返回 "hooked!"。Frida脚本如下:

// hook.js
Java.perform(function() {
    var Utils = Java.use('com.example.Utils');
    Utils.getSecret.implementation = function() {
        console.log('[+] getSecret() was called!');
        return 'hooked!';
    };
});

运行方式:

frida -U -l hook.js com.example.app

你想想看,这有多强大?不需要修改APK,不需要重新编译,一行代码就能改变App的行为。

Hook实例方法

静态方法搞定了,实例方法呢?其实差不多,但要注意实例的获取方式。

Java.perform(function() {
    var MainActivity = Java.use('com.example.MainActivity');
    
    // Hook实例方法
    MainActivity.onClick.implementation = function(view) {
        console.log('[+] onClick called with view: ' + view);
        // 调用原方法
        return this.onClick(view);
    };
    
    // 也可以直接创建实例来调用
    var instance = MainActivity.$new();
    instance.onClick(null);
});

这里有个小技巧:this 关键字在implementation里指向当前实例。如果你想调用原方法,直接用 this.原方法名() 就行。

构造函数的Hook

构造函数怎么Hook?用 $init

Java.perform(function() {
    var MyClass = Java.use('com.example.MyClass');
    
    MyClass.$init.overload('int', 'java.lang.String').implementation = function(num, str) {
        console.log('[+] Constructor called with: ' + num + ', ' + str);
        // 可以修改参数
        return this.$init(999, 'modified');
    };
});

个人经验: 遇到重载方法时,用 overload() 指定参数类型。如果不确定参数类型,可以用 overload('*') 匹配所有重载,但建议还是精确指定。

动态修改字段值

有时候我们不光要Hook方法,还要修改对象的字段值。比如一个登录状态检查:

Java.perform(function() {
    var UserManager = Java.use('com.example.UserManager');
    
    UserManager.isLoggedIn.implementation = function() {
        // 直接修改字段
        this.mIsLoggedIn.value = true;
        return true;
    };
    
    // 或者通过实例修改
    var instance = Java.choose('com.example.UserManager', {
        onMatch: function(inst) {
            inst.mIsLoggedIn.value = true;
            console.log('[+] Found instance, set logged in');
        },
        onComplete: function() {}
    });
});

Java.choose() 是个好东西——它会在堆上搜索指定类的所有实例。我经常用它来定位关键对象。

实战案例:绕过SSL Pinning

讲个真实案例。有一次我分析一个金融类App,它做了SSL Pinning,抓包工具死活抓不到HTTPS流量。静态分析发现它用了OkHttp的CertificatePinner。

Hook方案很简单:

Java.perform(function() {
    var CertificatePinner = Java.use('okhttp3.CertificatePinner$Builder');
    
    CertificatePinner.add.overload('java.lang.String', 'java.lang.String[]').implementation = function(hostname, pins) {
        console.log('[+] SSL Pinning bypassed for: ' + hostname);
        // 不添加任何证书锁定
        return this;
    };
});

就这么几行代码,SSL Pinning就绕过去了。你想想看,如果没有动态Hook,你得反编译、改Smali、重新打包签名……费时费力。

Frida脚本的调试技巧

写Frida脚本时,难免会遇到问题。我分享几个调试技巧:

  • 打印调用栈: Java.perform(function() { console.log(Java.use('android.util.Log').getStackTraceString(Java.use('java.lang.Exception').$new())); });
  • 查看所有已加载的类: Java.enumerateLoadedClasses({ onMatch: function(c) { console.log(c); }, onComplete: function() {} });
  • 动态加载脚本:%load hook.js 在Frida控制台里热加载

核心要点: Frida的JavaScript环境是独立的,不能直接访问App的变量。所有操作都要通过 Java.use()Java.choose() 等API来桥接。

本章知识体系

下面这张图总结了Frida Hook Java层的核心流程:

Frida Hook Java层核心流程 环境准备 连接目标进程 Java.perform() 入口 Java.use() 获取类 Java.choose() 找实例 重载方法处理 修改 implementation / 读写字段 / 调用原方法 frida-server + frida-tools frida -U -l script.js 所有操作必须在此回调内

常见问题与避坑

最后,我总结几个新手容易踩的坑:

  • 类名写错: 一定要用完整的包名路径,比如 com.example.MyClass,不是 MyClass
  • 忘记Java.perform: 所有Java相关操作必须在 Java.perform() 回调里执行
  • 异步问题: Frida的JavaScript是异步的,但 Java.perform() 内部是同步的。不要在回调里做耗时操作
  • App崩溃: 如果Hook后App闪退,多半是返回值类型不匹配。检查一下原方法的签名

我的习惯: 每次写Hook脚本前,先用 frida -U -f com.example.app --no-pause 启动App,然后在控制台里手动测试API。确认没问题了再写成脚本文件。

好了,这一章的内容就到这里。Frida的Java层Hook是Android逆向的必修课,掌握了它,你就能在运行时随心所欲地操控App逻辑。下一章我们会深入Native层,看看怎么用Frida Hook so文件里的C/C++函数。


公众号:蓝海资料掘金营,微信deep3321