Just do it.

3 Frida

Posted on By qfzwy

1.什么是Frida

Frida 是一款开源的动态插桩工具,可以插入一些代码到原生App的内存空间去动态地监视和修改其行为,支持Windows、Mac、Linux、Android或者iOS,从安卓层面来讲,可以实现Java层和NativeHook操作。 项目地址 官网及使用文档

2.Frida原理及重要组件

rida注入的原理就是找到目标进程,使用ptrace跟踪目标进程获取mmap,dlpoen,dlsym等函数库的偏移获取mmap在目标进程申请一段内存空间将在目标进程中找到存放frida-agent-32/64.so的空间启动执行各种操作由agent去实现

组件名称 功能描述
frida-gum 提供了inline-hook的核心实现,还包含了代码跟踪模块Stalker,用于内存访问监控的MemoryAccessMonitor,以及符号查找、栈回溯实现、内存扫描、动态代码生成和重定位等功能
frida-core fridahook的核心,具有进程注入、进程间通信、会话管理、脚本生命周期管理等功能,屏蔽部分底层的实现细节并给最终用户提供开箱即用的操作接口。包含了frida-server、frida-gadget、frida-agent、frida-helper、frida-inject等关键模块和组件,以及之间的互相通信底座
frida-gadget 本身是一个动态库,可以通过重打包修改动态库的依赖或者修改smali代码去实现向三方应用注入gadget,从而实现Frida的持久化或免root
frida-server 本质上是一个二进制文件,类似于前面学习到的android_server,需要在目标设备上运行并转发端口,在Frida hook中起到关键作用

两种启动方式

方式名称 方式说明 CLI下启动方式
spawn 将启动APP的权利交由Frida来控制。不管APP是否启动,都会重新启动APP。 -f 参数指定包名
attach 建立在目标APP已经启动的情况下,Frida通过 ptrace 注入程序从而执行Hook的操作 不加 -f 参数

3.Frida与Xposed的对比

工具 优点 缺点
Xposed 直接编写Java代码,Java层hook方便,可打包模块持久化hook 环境配置繁琐,兼容性较差,难以Hook底层代码。
Frida 配置简单,免重启hook。支持Java层和Native层的hook操作 持久化hook相对麻烦

4.Frida环境配置

pip install frida==16.3.1
pip install frida-tools
adb push 

Frida 12.0.0到12.1.0 对应 Frida-tools 1.0.0到1.2.2

Frida 12.3.0到12.4.0 对应 Frida-tools 1.3.0到1.3.2

Frida 12.5.3到12.6.21 对应 Frida-tools 2.0.0到4.1.0

Frida 12.7.3到12.8.12 对应 Frida-tools 5.0.1到7.2.2

Frida 12.10.4到13.0.0 对应 Frida-tools 8.0.0到8.2.0

Frida 14.0.0到15.0.0 对应 Frida-tools 9.0.0到9.2.5

Frida 15.0.0到16.0.0 对应 Frida-tools 10.0.0到10.8.0

Frida 16.0.0到17.0.0 对应 Frida-tools 12.0.0到12.3.0

5.Frida常用Api

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) 枚举指定类的所有方法。

Hook普通方法,打印参数和修改返回值

hook这个a方法

image-20250114191829774

function hookTest1(){
    var utils = Java.use("com.zj.wuaipojie.Demo");
    utils.a.overload("java.lang.String").implementation = function(str){
        // a = 123;
        // b = 456;
        //接收返回值
        var retval = this.a(str);
        console.log(str,retval);
        return retval;
    }
}


function main(){
    Java.perform(function(){
        hookTest1()
    });
}
setImmediate(main);

Hook重载函数

使用overload(“java.lang.String”),指定参数类型

涉及到复杂参数例如如下图

image-20250114192953046

需要看对应的smali代码

image-20250114193139577

utils.Inner.overload('com.zj.wuaipojie.Demo$Animal','java.lang.String').implementation
function hookTest1(){
    var utils = Java.use("com.zj.wuaipojie.Demo");
    utils.Inner.overload("com.zj.wuaipojie.Demo$Animal","java.lang.String").implementation = function(Animal,str){
        // a = 123;
        // b = 456;
        //接收返回值
        var retval = this.Inner(Animal,str);
        console.log(Animal,str);
        // return retval;
    }
}


function main(){
    Java.perform(function(){
        hookTest1()
    });
}
setImmediate(main);

Hook构造函数

function hookTest1(){
    var utils = Java.use("com.zj.wuaipojie.Demo");
    utils.$init.overload("java.lang.String").implementation = function(str){
        str='hooked by frida'
        var retval = this.$init(str);
        console.log(str);
        // return retval;
    }
}


function main(){
    Java.perform(function(){
        hookTest1()
    });
}
setImmediate(main);

Hook字段

修改静态变量

function hookTest1(){
    var utils = Java.use("com.zj.wuaipojie.Demo");
    utils.staticField.value = "我是被修改的静态变量";
    console.log(utils.staticField.value);
    
}

修改非静态变量

function hookTest1(){
    var utils = Java.use("com.zj.wuaipojie.Demo");
    Java.choose("com.zj.wuaipojie.Demo",{
        onMatch: function (obj) {
            // obj._privateInt.value = "123456"; //字段名与函数名相同 前面加个下划线
            obj.privateInt.value = 9999;
            console.log("hookTest1 obj: ", obj.privateInt.value);
        },
        onComplete: function () {

        }
    })

}


function main(){
    Java.perform(function(){
        hookTest1()
    });
}
setImmediate(main);

Hook内部类

function hookTest6(){
    Java.perform(function(){
        //内部类
        var innerClass = Java.use("com.zj.wuaipojie.Demo$innerClass");
        console.log(innerClass);
        innerClass.$init.implementation = function(){
            console.log("eeeeeeee");
        }

    });
}

构造数组

Log.d("SimpleArraysToString", Arrays.toString(arr[i]));

替换整行

var charArray = Java.array('char',['一','二','三','四','五'])
var result = this.toString(charArray);

强制类型转换

当前有两个类

Water和Juice,Juice继承于Water

public class Water {
    public static String flow(Water W) { // 水 的方法
        // SomeSentence
        Log.d("2Object", "water flow: I`m flowing");
        return "water flow: I`m flowing";
    }

    public String still(Water W) { // 水 的方法
        // SomeSentence
        Log.d("2Object", "water still: still water runs deep!");
        return "water still: still water runs deep!";
    }
}

public class Juice extends Water { // 果汁 类 继承了水类

    public String fillEnergy(){
        Log.d("2Object", "Juice: i`m fillingEnergy!");
        return "Juice: i`m fillingEnergy!";
    }

    public static void main() {

        Water w1 = new Water();
        flow(w1) ; //

        Juice J = new Juice(); // 实例化果汁类对象
        flow(J) ; // 调用水的方法  向上转型 J → W

        Water w2 = new Juice();
        ((Juice) w2).fillEnergy();
    }
}

类型转换代码 子类可转父类

function hookTest(){
    let juiceHandle = null;
    Java.choose("com.example.lesson5.Juice",{
        onMatch: function (instance) {
            console.log("instance:",instance);
            console.log("call fillEnergy ",instance.fillEnergy());
            console.log("Juice still",instance.still(instance))
             juiceHandle =  instance;
        },
        onComplete: function () {
            console.log("completed")
            setTimeout(() => {
                if (juiceHandle !== null) {
                    let waterHandle = Java.cast(juiceHandle, Java.use("com.example.lesson5.Water"));
                    console.log("call still", waterHandle.still(waterHandle));
                } else {
                    console.log("No Juice instance found, skipping Java.cast");
                }
            }, 100); // 等待 100ms
        },
    })
}

核心

let waterHandle = Java.cast(juiceHandle, Java.use("com.example.lesson5.Water"));

实现类

当前有如下类,继承Water,实现liquid接口

public class Milk extends Water implements liquid{
    @Override
    public String flow(int time) {
        return "flowing"+time;
    }
}

现在在js中实现一个Beer类

function hookTest(){
    const Water = Java.use('com.example.lesson5.Water');
    const liquid = Java.use('com.example.lesson5.liquid');
    const Beer = Java.registerClass({
      name: 'com.example.lesson5.Peer',
      superClass:Water,
      implements: [liquid],
      methods: {
          flow: function (time) {
              let result =  "Beer flowing"+time;
              return Java.use("java.lang.String").$new(result);
          }
      }
    });
    let BeerInstance = Beer.$new();
    console.log("beer flow", BeerInstance.flow(3));
    console.log("beer-water-still",BeerInstance.still(BeerInstance))
}

image-20250201124436755

枚举所有的类与类的所有方法

function hookTest7(){
    Java.perform(function(){
        //枚举所有的类与类的所有方法,异步枚举
        Java.enumerateLoadedClasses({
            onMatch: function(name,handle){
                    //过滤类名
                if(name.indexOf("com.zj.wuaipojie.Demo") !=-1){
                    console.log(name);
                    var clazz =Java.use(name);
                    console.log(clazz);
                    var methods = clazz.class.getDeclaredMethods();
                    console.log(methods);
                }
            },
            onComplete: function(){}
        })
    })
}

枚举所有方法

function hookTest8(){
    Java.perform(function(){
        var Demo = Java.use("com.zj.wuaipojie.Demo");
        //getDeclaredMethods枚举所有方法
        var methods =Demo.class.getDeclaredMethods();
        for(var j=0; j < methods.length; j++){
            var methodName = methods[j].getName();
            console.log(methodName);
            for(var k=0; k<Demo[methodName].overloads.length;k++){
                Demo[methodName].overloads[k].implementation = function(){
                    for(var i=0;i<arguments.length;i++){
                        console.log(arguments[i]);
                    }
                    return this[methodName].apply(this,arguments);
                }
            }
        }
    })
}

主动调用

静态方法

var ClassName=Java.use("com.zj.wuaipojie.Demo"); 
ClassName.privateFunc("传参");

非静态

    var ret = null;
    Java.perform(function () {
        Java.choose("com.zj.wuaipojie.Demo",{    //要hook的类
            onMatch:function(instance){
                ret=instance.privateFunc("aaaaaaa"); //要hook的方法
            },
            onComplete:function(){
                    //console.log("result: " + ret);
            }
        });
    })
    //return ret;

打印日志

adb shell
logcat | grep "D.zj2595"

打印调用栈

console.log(Java.user("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));

打印map

Map<String, String> mapr0ysue = new HashMap<>(); // 创建Map集合对象
mapr0ysue.put("ISBN 978-7-5677-8742-1", "Android项目开发实战入门"); // 向Map集合中添加元素
mapr0ysue.put("ISBN 978-7-5677-8741-4", "C语言项目开发实战入门");
mapr0ysue.put("ISBN 978-7-5677-9097-1", "PHP项目开发实战入门");
mapr0ysue.put("ISBN 978-7-5677-8740-7", "Java项目开发实战入门");

Log.d("5map", "key值toString"+mapr0ysue.toString());

frida打印

function hookTest(){
    Java.choose("java.util.HashMap",{
        onMatch: function (instance) {
            if (instance.toString().indexOf("ISBN") !== -1) {
                console.log("found instance", instance);
                console.log("hashMap result ", instance.toString());
            }
        },
        onComplete: function () {
            console.log("completed");
        },
    });
}

frida-Hook

function hookTest(){

    Java.use("java.util.HashMap").put.implementation = function (x, y) {
        var result = this.put(x, y);
        if (x.toString().indexOf("ISBN")!== -1) {
            console.log("x,y,result", x.toString(), y.toString(), result);
        }
        return result;

    };
}

image-20250201131032596

生成字符串

Java.use('java.lang.String').$new("NIHAO");

6.objection

objection是基于frida的命令行hook集合工具, 可以让你不写代码, 敲几句命令就可以对java函数的高颗粒度hook, 还支持RPC调用。可以实现诸如内存搜索、类和模块搜索、方法hook打印参数返回值调用栈等常用功能,是一个非常方便的,逆向必备、内存漫游神器。

6.1 环境安装

pip install frida==15.2.2
pip install frida-tools==10.3.0
pip3 install -U objection
#启动frida,查找包名
frida-ps -Uia
#启动Objection
objection -g <包名> explore

● 指定IP端口
objection -N -h <手机ip地址> -p <端口> -g <包名> explore

● spawn 启动前 hook
objection -N -h 192.168.1.3 -p 9999 -g 包名 explore --startup-command "android hooking watch class '包名.类名'"
 
● spawn 启动前 Hook 并打印参数、返回值、函数调用栈
objection -N -h 192.168.1.3 -p 9999 -g 包名 explore --startup-command "android hooking watch class_method '包名.类名.方法'  --dump-args --dump-return --dump-backtrace"

6.2 内存漫游

hook:objection -g com.android.settings explore

image-20250127111327217

6.2.1 提取内存信息

查看内存中加载的库memory list modules

image-20250127111841664

查看库的导出函数memory list exports libbase.so

image-20250127113323021

保存结果到json,memory list exports libbase.so --json ./libbase.json

6.2.2 内存堆搜索与执行

在堆上搜索实例

查看AOSP源码关于设置里显示系统设置的部分,发现存在着DisplaySettings类。

image-20250127114113699

android heap search instances com.android.settings.DisplaySettings

image-20250127114125290


调用实例方法

DisplaySettings.java中有如下方法

image-20250127114543438

调用该方法:android heap execute 7019706 getMetricsCategory

image-20250127114732958

在实例上执行js代码

android heap evaluate 7019706进入编辑器环境

输入console.log("evaluate result:"+clazz.getMetricsCategory()),按ESC退出编辑器,然后按回车,即会开始执行这串脚本,输出结果

image-20250127120548566

6.2.3 启动activity或service

直接启动activity

进入显示设置android intent launch_activity com.android.settings.DisplaySettings

image-20250127120830526

查看当前可用的activity

android hooking list activities

image-20250127120930974

直接启动service

android hooking list services查看可供开启的服务

android intent launch_service com.android.settings.bluetooth.BluetoothPairingService命令来开启服务

6.3 hook

hook类的所有方法

android hooking watch class android.bluetooth.BluetoothDevice

image-20250127121906986

使用蓝牙功能会跟踪到调用的方法

image-20250127122153132

hook方法的参数、返回值和调用栈

android hooking watch class_method android.bluetooth.BluetoothDevice.getName --dump-args --dump-return --dump-backtrace

image-20250127122746796

最后加上的三个选项--dump-args --dump-return --dump-backtrace,为我们成功打印出来了我们想要看的信息,其实返回值Return Value就是getName()方法的返回值,我的蓝牙耳机的型号名字OnePlus Bullets Wireless 2;从调用栈可以反查如何一步一步调用到getName()这个方法的

hook 类的构造方法

android hooking watch class_method com.android.settings.bluetooth.BluetoothPairingService.$init

hook 方法的所有重载

android hooking watch class_method 类名.方法名

6.4 Wallbreaker

安装和加载

git clone https://github.com/hluwa/Wallbreaker
#加载插件
#方法1
objection -g com.example.androiddemo explore  -P ~/.objection/plugins/Wallbreaker(插件路径)
#方法2
objection -g com.example.androiddemo explore
plugin load ~/.objection/plugins/Wallbreaker(插件路径)
windows注意路径:plugin load F:/Penetration/android/objection/Wallbreaker

搜索类

` plugin wallbreaker classdump –fullname com.android.settings.bluetooth.BluetoothPairingService `

image-20250127124531382

6.4 FRIDA-DEXDump

git clone https://github.com/hluwa/frida-dexdump
plugin load F:/Penetration/android/objection/frida-dexdump
or
pip3 install frida-dexdump

打开待脱壳的App

image-20250127132504565

获取包名com.cz.babySister

image-20250127132640816

frida-dexdump -U -f com.cz.babySister

合并dex

import os
import zipfile
import argparse

def rename_class(path):
    files = os.listdir(path)
    dex_index = 0
    if path.endswith('/'):
        path = path[:-1]
        print(path)
    for i in range(len(files)):
        if files[i].endswith('.dex'):
            old_name = path + '/' + files[i]
            if dex_index == 0:
                new_name = path + '/' + 'classes.dex'
            else:
                new_name = path + '/' + 'classes%d.dex' % dex_index
            dex_index += 1
            if os.path.exists(new_name):
                continue
            os.rename(old_name, new_name)
    print('[*] 重命名完毕')

def extract_META_INF_from_apk(apk_path, target_path):
    r = zipfile.is_zipfile(apk_path)
    if r:
        fz = zipfile.ZipFile(apk_path, 'r')
        for file in fz.namelist():
            if file.startswith('META-INF'):
                fz.extract(file, target_path)
    else:
        print('[-] %s 不是一个APK文件' % apk_path)

def zip_dir(dirname, zipfilename):
    filelist = []
    if os.path.isfile(dirname):
        if dirname.endswith('.dex'):
            filelist.append(dirname)
    else:
        for root, dirs, files in os.walk(dirname):
            for dir in dirs:
                # if dir == 'META-INF':
                # print('dir:', os.path.join(root, dir))
                filelist.append(os.path.join(root, dir))
            for name in files:
                # print('file:', os.path.join(root, name))

                filelist.append(os.path.join(root, name))

    z = zipfile.ZipFile(zipfilename, 'w', zipfile.ZIP_DEFLATED)
    for tar in filelist:
        arcname = tar[len(dirname):]

        if ('META-INF' in arcname or arcname.endswith('.dex')) and '.DS_Store' not in arcname:
            # print(tar + " -->rar: " + arcname)
            z.write(tar, arcname)
    print('[*] APK打包成功,你可以拖入APK进行分析啦!')
    z.close()

if __name__ == '__main__':
    args = {
        'dex_path': '脱壳后dex路径',
        'apk_path': '原始带壳apk路径',
        'output': '脱壳后apk路径'
    }

    rename_class(args['dex_path'])
    extract_META_INF_from_apk(args['apk_path'], args['dex_path'])
    zip_dir(args['dex_path'], args['output'])

6.4 zentrace

pip install PyQt5
git clone https://github.com/hluwa/ZenTracer
cd ZenTracer
python ZenTracer.py

action->Match RegEx,输入要跟踪的包名

image-20250127141812327

image-20250127141839230

点击start

image-20250127144008518

6.5 r0trace

frida -U 大 -l r0tracer.js -o Log.txt

7.frida-rpc

7.1 远程调用

已此安卓app为例

package com.example.lees4;

import android.os.Bundle;

import com.google.android.material.snackbar.Snackbar;

import androidx.appcompat.app.AppCompatActivity;

import android.util.Log;
import android.view.View;

import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI;

import com.example.lees4.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {
    private static String total = "@@@@####@@@@";



    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        while(true){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            int m = fun(50,80);
            Log.d("kanxue m = ", String.valueOf(m));
            Log.d("kanxue tolowercase", fun("LOWERCASEME!"));
        }
    }
    String fun(String x ){
        total +=x ;
        return x.toLowerCase();

    }

    int fun(int x ,int y){
        Log.d("kanxue", String.valueOf((x+y)));
        return x+y;
    }


    String secret(){
        return total;
    }

    public static String secret2(){
        return total;
    }


}

hook.js

function invoke(){
    Java.perform(function (){
        Java.choose("com.example.lees4.MainActivity",{
            onMatch:function(instance){
                console.log("found instance ",instance);
                console.log("invoke instance.secret ",instance.secret());
            },onComplete:function(){console.log("search completed !")}
        })
    })


}
rpc.exports = {
    invokefunc:invoke
};

rpc.py

# 错误处理
import frida
def my_message_handler(message, payload):
    print(message)
    print(payload)


device = frida.get_usb_device()
session = device.attach("lees4")
with open("hook.js") as f:
    script = session.create_script(f.read())
script.on("message", my_message_handler)
script.load()

command = " "
while True:
    command = input("input command:")
    if command == "2":
        script.exports_sync.invokefunc()

7.2 多端混连

以非标准端口启动fr

./fr16 -l 0.0.0.0:6666

验证是否连接成功

device = frida.get_device_manager().add_remote_device('192.168.0.100:6666')
print(device.enumerate_applications())

7.3 动态修改

示例app如下

package com.example.myapplication;

import android.annotation.SuppressLint;
import android.os.Bundle;
import android.util.Base64;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;

import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;


public class MainActivity extends AppCompatActivity {
    EditText username_et;
    EditText password_et;
    TextView message_tv;
    @SuppressLint("MissingInflatedId")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        password_et = (EditText) this.findViewById(R.id.editText2);
        username_et = (EditText) this.findViewById(R.id.editText);
        message_tv = ((TextView) findViewById(R.id.textView));

        this.findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                if (username_et.getText().toString().compareTo("admin") == 0) {
                    message_tv.setText("You cannot login as admin");
                    return;
                }
                //hook target
                message_tv.setText("Sending to the server :" + Base64.encodeToString((username_et.getText().toString() + ":" + password_et.getText().toString()).getBytes(), Base64.DEFAULT));

            }
        });
    }
}

image-20250205123152367

目的是绕过代码中对admin的检查,将账号admin,以及对应密码发到server,需要hook的函数为setText

首先要确定setText用的是哪个重载

image-20250205124327477

android.widget.TextView.setText(java.lang.CharSequence)

进行hook


Java.perform(function (){
    Java.use("android.widget.TextView").setText.overload("java.lang.CharSequence").implementation =
        function (x){
            let recv_msg ;
            let send_msg =  x.toString();
            send(send_msg);
            recv_msg(function (data){
                recv_msg = data.res;
            }).wait();
            let Java_recv_msg = Java.use("java.lang.String").$new(recv_msg);
            let result = this.setText(Java_recv_msg);
            return result;
        }
})

py

# 错误处理
import base64

import frida


def my_message_handler(message, payload):
    print(message)
    print(payload)
    if message["type"] == "send":
        print(message["payload"])
        data = message["payload"].split(":")[1].strip()
        data = str(base64.b64decode(data))
        print(data)
        usr, pw = data.split(":")
        print('pw', pw)
        data = str(base64.b64encode(("admin" + ":" + pw).encode()))
        print("encode data", data)
        script.post({"res": data})
        print("modified send")


device = frida.get_usb_device()
session = device.attach("My Application")
with open("hook.js") as f:
    # 读取 JavaScript 脚本 lesson7sec.js
    script = session.create_script(f.read())

# 注册 my_message_handler 作为消息处理函数,并加载 Frida 脚本。
script.on("message", my_message_handler)
script.load()
input()

8.综合案例

拿到app,界面如下

image-20250203211049110

8.1 登录

问题很好解决,直接r0trace一把梭

image-20250203214625548

b5224c158c901c0d730ff02bf5ab721363099fcac5af372f6724790134cb6e25

从静态代码分析也可以得知,a(obj,0bj)返回的结果和输入的密码一致才进入下一关,那么trace得到的返回值就是密码。

image-20250203214653296

8.2 第一关

点击进入下一关会返回check failed

image-20250203212254388

trace调用栈如下

image-20250203215221477

这里的a方法返回值如下,如静态代码中的密钥不一致

image-20250203215624986

image-20250203215648880

所以hook该方法,修改返回值

let FridaActivity1 = Java.use("com.example.androiddemo.Activity.FridaActivity1");
FridaActivity1["a"].implementation = function (bArr) {
    console.log(`FridaActivity1.a is called: bArr=${bArr}`);
    let result = this["a"](bArr);
    console.log(`FridaActivity1.a result=${result}`);
    return "R4jSLLLLLLLLLLOrLE7/5B+Z6fsl65yj6BgC6YWz66gO6g2t65Pk6a+P65NK44NNROl0wNOLLLL=";
};

image-20250203220015263

8.3 第二关

image-20250203222806324

从代码得知,一开始static_bool_var和bool_var都是false,需要调用对应的set方法才可以

let FridaActivity2 = Java.use("com.example.androiddemo.Activity.FridaActivity2");
let static_bool_var = FridaActivity2.static_bool_var.value;
console.log(`static_bool_var=${static_bool_var}`);

FridaActivity2.setStatic_bool_var();
Java.choose("com.example.androiddemo.Activity.FridaActivity2",{
    onMatch: function (instance) {
        let bool_var = instance.bool_var.value;
        console.log(`bool_var=${bool_var}`);
        instance.setBool_var();
    },
    onComplete: function () {
        console.log("completed")
    },
})

image-20250203223625844

8.4 第三关

image-20250203223905949

与上一关不同的是没有修改变量的方法,需要我们自己修改

image-20250203224043154

如果变量名与函数同名需要加_

function hookTest3(){
    let FridaActivity3 = Java.use("com.example.androiddemo.Activity.FridaActivity3");
    let static_bool_var = FridaActivity3.static_bool_var.value;
    console.log(`static_bool_var=${static_bool_var}`);
    FridaActivity3.static_bool_var.value = true;
    Java.choose("com.example.androiddemo.Activity.FridaActivity3",{
        onMatch: function (instance) {
            console.log(`bool_var=${instance.bool_var.value}`);
            // 如果变量名与函数同名需要加_
            console.log(`_same_name_bool_var=${instance._same_name_bool_var.value}`);
            instance.bool_var.value = true;
            instance._same_name_bool_var.value = true
        },
            onComplete: function () {
            console.log("completed")
        },
    })
}

8.5 第四关

image-20250203224859150

从静态代码看需要Hook内部类

image-20250203224959277

function hookTest4(){
    let InnerClasses = Java.use("com.example.androiddemo.Activity.FridaActivity4$InnerClasses");
    InnerClasses.check1.implementation = function () {
        return true;
    };
    InnerClasses.check2.implementation = function () {
        return true;
    };
    InnerClasses.check3.implementation = function () {
        return true;
    };
    InnerClasses.check4.implementation = function () {
        return true;
    };
    InnerClasses.check5.implementation = function () {
        return true;
    };
        InnerClasses.check6.implementation = function () {
        return true;
    };
}

8.6 第五关

image-20250204111925609

首先getDynamicDexCheck!=null,然后check()为true

CheckInterface接口相关代码如下

public interface CheckInterface {
    boolean check();
}

实现

private CheckInterface DynamicDexCheck = null;
public CheckInterface getDynamicDexCheck() {
    if (this.DynamicDexCheck == null) {
    	loaddex();
    }
    return this.DynamicDexCheck;
}

声明一个变量,调用getDynamicDexCheck进行实例化,首先使用loaddex()加载dex文件,如果文件不存在,则调用copyFiles,将写好的汇编代码放入文件中,然后利用反射获取实例。

private void loaddex() {
    File filesDir = getFilesDir();
    if (!filesDir.exists()) {
        filesDir.mkdir();
    }
    String str = filesDir.getAbsolutePath() + File.separator + "DynamicPlugin.dex";
    File file = new File(str);
    try {
        if (!file.exists()) {
            file.createNewFile();
            copyFiles(this, "DynamicPlugin.dex", file);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    try {
        this.DynamicDexCheck = (CheckInterface) new DexClassLoader(str, filesDir.getAbsolutePath(), null, getClassLoader()).loadClass("com.example.androiddemo.Dynamic.DynamicCheck").newInstance();
        if (this.DynamicDexCheck == null) {
            Toast.makeText(this, "loaddex Failed!", 1).show();
        }
    } catch (Exception e2) {
        e2.printStackTrace();
    }
}

其加载的类如下

package com.example.androiddemo.Dynamic;

/* loaded from: assets/DynamicPlugin.dex */
public class DynamicCheck implements CheckInterface {
    @Override // com.example.androiddemo.Dynamic.CheckInterface
    public boolean check() {
        return false;
    }
}

这里涉及一个接口多态的知识:接口本身不能直接实例化,但可以通过实现接口的类来创建对象,然后用接口类型的变量引用这个对象。在这段代码中使用接口多态而不是具体类,这样支持动态加载。


那么这里通关的核心就是hook到动态加载的类的check方法,修改返回值为true

function hookTest5(){
    Java.enumerateClassLoaders({
        onMatch: function (loader) {
            try {
                // noinspection JSConstantReassignment
                if (loader.findClass("com.example.androiddemo.Dynamic.DynamicCheck")) {
                    Java.classFactory.loader = loader;
                    console.log("this loader",loader)
                }
            } catch (e){
                console.log(e);
            }
        }, onComplete: function () {},
    })
    let app = Java.use("com.example.androiddemo.Dynamic.DynamicCheck");
        console.log("class", app);
        app.check.implementation = function () {
            let result = this.check();
            console.log(result);
            return true;
        };
}
  • Java.enumerateClassLoaders() 用于遍历所有的类加载器,尝试找到能够加载 com.example.androiddemo.Dynamic.DynamicCheck 这个类的加载器。
  • 对于每个加载器 loader,尝试使用 loader.findClass("com.example.androiddemo.Dynamic.DynamicCheck") 查找目标类。
  • 修改 Frida 的类加载器Java.classFactory.loader = loader,使用这个 loader 来加载 Java 类,否则可能找不到目标类

8.7 第六关

image-20250204121754113

image-20250204121806738

这关很简单,Hook类直接调用静态方法即可

function hootTest6(){
    for (let i = 0; i < 3; i++) {
        Java.use("com.example.androiddemo.Activity.Frida6.Frida6Class"+i).check.implementation = function (){return true};
    }
}

但是还有一种方法,找已经加载好的类

//方法二
Java.enumerateLoadedClasses({
    onMatch: function (name, handle) {
        if (name.toString().indexOf("com.example.androiddemo.Activity.Frida6.Frida6")>=0) {
            console.log("name", name);
            Java.use(name).check.implementation = function (){return true};
        }
    },onComplete:function (){console.log("completed")}
})