找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2375

积分

0

好友

319

主题
发表于 昨天 05:16 | 查看: 6| 回复: 0

Frida是一款基于JS代码编写的Hook框架,易用且可跨平台。本文不赘述其背景,直入实战——聚焦Android平台下Frida在Java层、Native层的深度Hook技巧,以及对动态加载Dex等加固/插件化场景的应对策略。

我们就直接步入正题。环境配置好后,需通过启动命令注入hook脚本。这里补充一个常被忽略但极其实用的命令:

frida -U -F -l hook.js

当你觉得每次都要手动查进程名麻烦时,-F 参数可替代进程名。它代表“附加到前台应用”,只需将目标APP置于最前端,Frida便会自动附加到当前活跃的进程,省去手动确认包名的步骤。


一、Java层Hook核心模式

1. 获取类与执行沙箱

要Hook Java方法,必须先获取目标类的引用,并确保操作在正确的类加载上下文中进行:

Java.perform(function() {
    const Map = Java.use("java.util.Map");
    Map.put.implementation = function(key, value) {
        console.log("[+] put called with key:", key, "value:", value);
        return this.put(key, value);
    };
});

⚠️ 注意:Java.use("类名") 必须包裹在 Java.perform() 回调中。这是Frida操作Java环境的安全沙箱,确保使用APP自身的类加载器,避免因类加载器不一致导致ClassNotFoundException或Hook失效。

2. 处理重载方法

Java方法重载要求明确指定参数类型。例如Hook MMKV的decodeString

MMKV.decodeString.overload('long', 'java.lang.String', 'java.lang.String').implementation = function(handle, key, defaultValue) {
    console.log("
  • decodeString: key =", key);     return this.decodeString(handle, key, defaultValue); };
  • 若需Hook所有重载版本,可遍历overloads数组:

    const ClassName = Java.use("类名");
    for (let i = 0; i < ClassName.MethodName.overloads.length; i++) {
        ClassName.MethodName.overloads[i].implementation = function() {
            console.log("
  • Method called with", arguments.length, "args");         // 使用 arguments 判断实际参数类型与个数     }; }
  • 3. 构造函数与对象实例化

    构造函数通过$init标识:

    Java.perform(function() {
        const TargetClass = Java.use("com.example.Target");
        TargetClass.$init.implementation = function() {
            console.log("[+] TargetClass instantiated");
            return this.$init.apply(this, arguments);
        };
    });

    实例化新对象则使用$new()

    const TargetClass = Java.use("com.example.Target");
    const instance = TargetClass.$new("param1", 123);
    const result = instance.someMethod();

    4. 主动调用静态/非静态方法

    • 静态方法:直接通过类对象调用  

      const Utils = Java.use("com.example.Utils");
      Utils.encrypt("data"); // 无需实例
    • 非静态方法:需先获取运行时实例(两种方式)
      方式一:从Java堆中查找现有实例

      Java.choose("com.example.Manager", {
        onMatch: function(instance) {
            console.log("
    • Found instance:", instance);       const ret = instance.doWork("input");   },   onComplete: function() {} });
    • 方式二:主动创建新实例

      const Manager = Java.use("com.example.Manager");
      const instance = Manager.$new();
      instance.doWork("input");

    5. 字段(Field)Hook

    • 静态字段:直接赋值  

      const Config = Java.use("com.example.Config");
      Config.API_URL.value = "https://test.example.com";
    • 非静态字段:需结合Java.choose  

      Java.choose("com.example.User", {
        onMatch: function(obj) {
            console.log("old name:", obj.name.value);
            obj.name.value = "Hacker";
            // 字段名与方法名冲突时加下划线前缀
            obj._token.value = "fake_token_123";
        },
        onComplete: function() {}
      });

    6. 内部类与匿名类

    内部类名用$连接;匿名类由编译器生成(如Test$1):

    // Hook 内部类
    const InnerClass = Java.use("com.example.Outer$Inner");
    InnerClass.method.implementation = function() { /* ... */ };
    
    // Hook 匿名类(需反编译确认类名)
    const Anonymous = Java.use("com.example.Test$1");
    Anonymous.run.implementation = function() { /* ... */ };

    7. 枚举类与方法(反射增强)

    同步枚举所有已加载类(阻塞式):

    Java.perform(function() {
        const classes = Java.enumerateLoadedClassesSync();
        for (let i = 0; i < classes.length; i++) {
            if (classes[i].indexOf("com.yourapp") !== -1) {
                const clazz = Java.use(classes[i]);
                const methods = clazz.class.getDeclaredMethods();
                methods.forEach(m => console.log("[METHOD]", m.toString()));
            }
        }
    });

    异步枚举(推荐用于大型APP,避免卡顿):

    Java.enumerateLoadedClasses({
        onMatch: function(name, handle) {
            if (name.indexOf("com.yourapp") !== -1) {
                const clazz = Java.use(name);
                console.log("[CLASS]", name);
            }
        },
        onComplete: function() {}
    });

    8. 动态加载Dex的Hook

    动态Dex(如RePlugin、Tinker、ClassLoader加载)中的类不会出现在初始enumerateLoadedClasses中。需枚举所有类加载器并尝试加载:

    function hookDex() {
        Java.perform(function() {
            Java.enumerateClassLoaders({
                onMatch: function(loader) {
                    try {
                        if (loader.loadClass("com.plugin.DynamicClass")) {
                            Java.classFactory.loader = loader;
                            const clazz = Java.use("com.plugin.DynamicClass");
                            clazz.doSomething.implementation = function() {
                                console.log("[+] DynamicClass hooked!");
                            };
                        }
                    } catch (e) {
                        // ignore
                    }
                },
                onComplete: function() {}
            });
        });
    }

    💡 提示:该方法依赖loader.loadClass()触发类加载。若目标类已被加载但未被Java.enumerateLoadedClasses捕获,此方式仍有效。

    9. 特殊类型参数处理(Map、Context等)

    当方法参数为MapContext等复杂类型时,可直接调用其Java方法:

    TargetClass.onWakeMap.implementation = function(map) {
        console.log("[MAP KEYS]");
        const entrySet = map.entrySet();
        const iterator = entrySet.iterator();
        while (iterator.hasNext()) {
            const entry = iterator.next();
            console.log("  Key:", entry.getKey(), "Value:", entry.getValue());
        }
    
        // 修改参数
        const Integer = Java.use("java.lang.Integer");
        map.put("2", Integer.$new(999));
    
        // 调用原方法
        this.onWakeMap(map);
    };

    10. 获取调用堆栈

    • Java层堆栈(推荐):

      function printStackTrace() {
        const stackTrace = Java.use("android.util.Log").getStackTraceString(
            Java.use("java.lang.Throwable").$new()
        );
        console.log("Java Stack:\n" + stackTrace);
      }
    • Native层堆栈(需Interceptor.attach):

      Interceptor.attach(funcAddr, {
        onEnter: function(args) {
            console.log("[STACK TRACE]");
            const bt = Thread.backtrace(this.context, Backtracer.ACCURATE).slice(0, 10);
            bt.forEach((addr, i) => {
                const symbol = DebugSymbol.fromAddress(addr) || "unknown";
                console.log(`  [${i}] ${addr} -> ${symbol}`);
            });
        }
      });

    11. Hook系统API对抗混淆

    当目标方法名被混淆时,可转而Hook其调用的系统API,例如:

    • java.util.HashMap.put → 监控加密参数写入
    • javax.crypto.Cipher.doFinal → 拦截明文/密文
    • okhttp3.Request.Builder.build → 抓取请求体

    常见高价值系统API包括:

    • 集合操作Map.put, ArrayList.add, ConcurrentHashMap.put
    • 字符串/JSONStringBuilder.append, JSONObject.$init, URLEncoder.encode
    • 加密MessageDigest.getInstance, Cipher.doFinal
    • 网络HttpURLConnection.connect, OkHttpClient.newCall
    • 反射Class.forName, Method.invoke
    • Native加载System.loadLibrary, DexClassLoader.loadClass

    二、Native层(so)Hook进阶

    1. 函数地址定位

    • 基址+偏移(需确认指令集):

      const libBase = Module.findBaseAddress("libxxx.so");
      const funcAddr = libBase.add(0x166C); // ARM无需+1;Thumb需+1
    • 导出函数名查找(更稳定):

      const funcAddr = Module.findExportByName("libxxx.so", "target_func");

    2. Interceptor.attach基础用法

    Interceptor.attach(funcAddr, {
        onEnter: function(args) {
            console.log("[+] args[0] =", args[0].toString());
            // this.context.x0, x1... 可读写寄存器
            this.inputPtr = args[0]; // 保存供onLeave使用
        },
        onLeave: function(retval) {
            console.log("[-] return =", retval.toString());
            // 修改返回值(如jstring)
            const env = Java.vm.tryGetEnv();
            if (env) {
                const newStr = env.newStringUtf("modified");
                retval.replace(newStr);
            }
        }
    });

    3. 参数/返回值解析技巧

    • C字符串ptr(args[0]).readCString()
    • 指针解引用Memory.readPointer(ptr(args[0]))(处理二级指针)
    • 内存查看hexdump(ptr(args[0]), {length: 32})
    • 字节数组转换
      function stringToBytes(str, len = 16) {
        const bytes = [];
        for (let i = 0; i < len; i++) {
            bytes.push(i < str.length ? str.charCodeAt(i) : 0);
        }
        return bytes;
      }

    4. JNI关键函数Hook:RegisterNatives

    RegisterNatives是JNI函数注册入口,Hook它可发现所有Native方法绑定关系:

    function find_RegisterNatives() {
        const symbols = Module.enumerateSymbolsSync("libart.so");
        for (let i = 0; i < symbols.length; i++) {
            const sym = symbols[i];
            if (sym.name.includes("art") && 
                sym.name.includes("JNI") && 
                sym.name.includes("RegisterNatives") && 
                !sym.name.includes("CheckJNI")) {
    
                console.log("
  • Found RegisterNatives at:", sym.address, sym.name);             hook_RegisterNatives(sym.address);         }     } } function hook_RegisterNatives(addr) {     Interceptor.attach(addr, {         onEnter: function(args) {             const jclass = args[1];             const class_name = Java.vm.tryGetEnv().getClassName(jclass);             const methods_ptr = ptr(args[2]);             const method_count = parseInt(args[3]);             for (let i = 0; i < method_count; i++) {                 const name_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3));                 const sig_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize));                 const fnPtr_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2));                 const name = Memory.readCString(name_ptr);                 const sig = Memory.readCString(sig_ptr);                 const symbol = DebugSymbol.fromAddress(fnPtr_ptr);                 console.log("[JNI] class:", class_name, "method:", name, "sig:", sig, "addr:", fnPtr_ptr, "symbol:", symbol);             }         }     }); }
  • 🔍 提示:libart.so路径因Android版本而异:

    • Android 12+:/apex/com.android.art/lib64/libart.so
    • Android 10+(64位):/apex/com.android.runtime/lib64/libart.so
    • Android <10:/system/lib64/libart.so

    5. Native主动调用

    const libBase = Module.findBaseAddress("libcrypto.so");
    const funcAddr = libBase.add(0x906c0);
    
    // 声明函数指针:返回pointer,参数为两个pointer
    const aesFunc = new NativeFunction(funcAddr, 'pointer', ['pointer', 'pointer']);
    
    const input = Memory.allocUtf8String("plaintext");
    const key = Memory.allocUtf8String("secretkey");
    const result = aesFunc(input, key).readCString();
    
    console.log("[AES RESULT]:", result);

    6. 文件写入(两种方式)

    • Frida File API(简洁)

      const file = new File("/sdcard/frida_log.txt", "w");
      file.write("Hook triggered at " + new Date().toISOString() + "\n");
      file.flush();
      file.close();
    • libc系统调用(灵活)

      const fopen = new NativeFunction(Module.findExportByName("libc.so", "fopen"), 'pointer', ['pointer', 'pointer']);
      const fputs = new NativeFunction(Module.findExportByName("libc.so", "fputs"), 'int', ['pointer', 'pointer']);
      const fclose = new NativeFunction(Module.findExportByName("libc.so", "fclose"), 'int', ['pointer']);
      
      const filename = Memory.allocUtf8String("/sdcard/frida_libc.txt");
      const mode = Memory.allocUtf8String("w");
      const fp = fopen(filename, mode);
      
      const content = Memory.allocUtf8String("Hello from libc!\n");
      fputs(content, fp);
      fclose(fp);

    7. 寄存器级Hook与修改

    Hook BLR X23指令,动态追踪跳转目标:

    Interceptor.attach(targetAddr, {
        onEnter: function(args) {
            const targetAddr = this.context.x23;
            console.log("[X23] target address: 0x" + targetAddr.toString(16));
    
            const module = Process.findModuleByAddress(targetAddr);
            if (module) {
                console.log("→ Module:", module.name, "base:", module.base, "offset: 0x" + ptr(targetAddr).sub(module.base).toString(16));
            }
        }
    });

    修改寄存器值(如替换字符串指针):

    Interceptor.attach(funcAddr, {
        onEnter: function(args) {
            // 替换x1为自定义字符串
            const newStr = Memory.allocUtf8String("Hacked by Frida!");
            this.context.x1 = newStr;
        }
    });

    三、实战建议与避坑指南

    • 动态Dex场景:优先使用Java.enumerateClassLoaders + loader.loadClass,而非等待类自动加载。
    • 混淆对抗:系统API Hook比业务方法Hook更稳定,尤其适用于加密、网络、存储模块。
    • so层分析:善用Module.enumerateExportsenumerateImportsenumerateSymbols三者互补(见下表):

    Frida模块枚举能力对比表

    • 寄存器Hook:ARM64中X0-X7传参,X0同时为返回值;修改this.context.x0可劫持返回值。
    • 内存操作Memory.writeByteArray写入字节,Memory.allocUtf8String分配C字符串,ptr()转换地址类型。
    • 调试技巧console.log()输出有限,关键数据建议写入文件或用hexdump可视化。

    如需深入学习Android逆向技术栈,可参考云栈社区提供的 安全/渗透/逆向 专题资源库,涵盖反编译、调试、漏洞利用、加固脱壳等完整知识链路。




    上一篇:Cursor套壳Kimi只是表象,AI编程工具的技术选型与行业常态
    下一篇:安全简讯:VoidStealer木马2.0利用调试器绕过Chrome加密,窃取主密钥
    您需要登录后才可以回帖 登录 | 立即注册

    手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

    GMT+8, 2026-3-25 01:25 , Processed in 0.930837 second(s), 39 queries , Gzip On.

    Powered by Discuz! X3.5

    © 2025-2026 云栈社区.

    快速回复 返回顶部 返回列表