Frida 实战:Android JNI 数组 (jobjectArray) 操作全流程解析

版权归作者所有,如有转发,请注明文章出处:cyrus-studio.github.io/blog/

前言

在 Android 的 Native 层,Java 中的 Object[] 类型参数会以 jobjectArray 的形式传递到 C/C++ 代码中。

与 JS 数组不同,你不能直接对 jobjectArray 进行索引访问或直接操作其元素。要获取或修改其中的内容,必须借助 JNI 提供的接口,例如获取数组长度、读取单个元素或创建新的数组等操作。

env.js

常用的 JNI 函数在 frida 的 env.js 中都已经封装好了

github.com/frida/frida...

通过下面代码获取 JNIEnv 引用,就可以调用相关的 JNI 函数

bash 复制代码
let env = Java.vm.tryGetEnv()

文档:frida.re/docs/javasc...

获取数组长度

ini 复制代码
let arrLen = env.getArrayLength(objArray)
console.log('array length is: ' + arrLen);

元素类型判断

通过 getObjectClassName 可以获取到对象的类名进而判断该元素的类型。

ini 复制代码
// 获取对象的类名
let className = env.getObjectClassName(objArray)
console.log('className: ' + className);

// 判断是否 jobjectArray
if (className === '[Ljava.lang.Object;') {

}

获取数组元素

ini 复制代码
let element = env.getObjectArrayElement(objArray, i)

Int 元素读取

javascript 复制代码
let intElement = Java.cast(env.getObjectArrayElement(objArray, i), Java.use('java.lang.Integer'))
console.log(`element ${i} value: ${intElement}`);

Long 元素读取

javascript 复制代码
let longElement = Java.cast(env.getObjectArrayElement(objArray, i), Java.use('java.lang.Long'))
console.log(`element ${i} value: ${longElement}`);

Float 元素读取

javascript 复制代码
let floatElement = Java.cast(env.getObjectArrayElement(objArray, i), Java.use('java.lang.Float'))
console.log(`element ${i} value: ${floatElement}`);

Double 元素读取

javascript 复制代码
let doubleElement = Java.cast(env.getObjectArrayElement(objArray, i), Java.use('java.lang.Double'))
console.log(`element ${i} value: ${doubleElement}`);

字符串 元素读取

通过 env.js 中定义的 stringFromJni 函数可以直接获取到字符串对象的值

javascript 复制代码
let stringElement = env.stringFromJni(env.getObjectArrayElement(objArray, i))
console.log(`element ${i} value: ${stringElement}`);

或者

javascript 复制代码
let stringElement = Java.cast(env.getObjectArrayElement(objArray, i), Java.use('java.lang.String'))
console.log(`element ${i} value: ${stringElement}`);

Object 元素读取

javascript 复制代码
let element = env.getObjectArrayElement(objArray, i)

let elementClassName = env.getObjectClassName(element)

// 元素类型转换
let castElement = Java.cast(element, Java.use(elementClassName))

console.log(`element ${i} value: ${castElement}`);

打印 jobjectArray

打印 jobjectArray 中所有元素

javascript 复制代码
/**
 * 打印 jobjectArray 中所有元素
 * 
 * @param objArray
 * @returns {string|null}
 */
function printObjectArray(objArray) {
    if (objArray.isNull()) {
        console.log('Object array is null');
        return null;
    }

    // 获取 JNIEnv
    let env = Java.vm.tryGetEnv();
    let className = env.getObjectClassName(objArray);

    // 不是 jobjectArray,则直接打印类型
    if (!className.startsWith('[L')) {
        return `Argument is not a jobjectArray, actual type: ${className}`;
    }

    let arrLen = env.getArrayLength(objArray);
    let result = `Object array of type ${className}, length: ${arrLen}\n`;

    for (let i = 0; i < arrLen; i++) {
        let element = env.getObjectArrayElement(objArray, i);
        let elementClassName = env.getObjectClassName(element);
        let castElement = Java.cast(element, Java.use(elementClassName));

        result += `  [${i}] ${elementClassName}: ${castElement}\n`;
    }

    return result.trim() + '\n';
}

hook native 函数并打印 jobjectArray 传参

javascript 复制代码
function hook_native_func(targetAddress) {
    // Hook 目标地址
    Interceptor.attach(targetAddress, {
        onEnter: function (args) {
            this.log = 'Entering native function at: ' + targetAddress + '\n';
            this.log += printObjectArray(args[2])
        },

        onLeave: function (retval) {
            // 检查是否包含 "283"
            if (this.log.includes("283") && !retval.isNull()) {
                // 类型转换
                let className = Java.vm.tryGetEnv().getObjectClassName(retval)
                retval = Java.cast(retval, Java.use(className));
            }
            this.log += 'Leaving native function,retval: ' + retval
            console.log(this.log);
        }
    });
}

setImmediate(function () {
    Java.perform(function () {
        var baseAddress = Module.findBaseAddress("libGameVMP.so");
        hook_native_func(baseAddress.add(0xdfa8))
    });
})

执行脚本

r 复制代码
frida -H 127.0.0.1:1234 -F -l hook_native_func.js

输出如下:

vbnet 复制代码
Entering native function at: 0x7802455fa8
Object array of type [Ljava.lang.Object;, length: 3
  [0] java.lang.Integer: 283
  [1] com.shizhuang.duapp.modules.app.DuApplication: com.shizhuang.duapp.modules.app.DuApplication@d3fdf19
  [2] java.lang.String: cipherParamuserNamecountryCode86loginTokenpasswordca85e501ec201e140c97d4480a724cffplatformandroidtimestamp1243532540699typepwduserName4860cc943262bab5ef4712e3bf0db355_1uuidac6abb3d17c8fb63v5.43.0
Leaving native function,retval: dWWoXlbR3K87j2N27Dkv4uOPUnOIN8Kof9Gm2x1kil7S/jpBEVaMS8QgdCHBIMPhVX/bK7s5MFUyLCOl
B7InMGNA682aYZfSsu0VK8TERMuSq3Bg3C3ATNGKaJPVMWtogFXteBS1/CxbFUdhtv0v1U8zrQCT6QLeaQvM8nBmXDKSOvivdG7xhLLNWmSGP8gdVL0CuAKDFH2Xj9Krb/0jNsPgNnA==

jobjectArray 转换 JS 数组

封装一个方法将解析 jobjectArray 并返回元素转换为真实类型后的 JS 数组

javascript 复制代码
/**
 * 解析 jobjectArray 并返回元素转换为真实类型后的 JS 数组
 *
 * @param objArray          jobjectArray
 * @returns {array|null}    数组
 */
function parseObjectArray(objArray) {
    if (objArray.isNull()) {
        console.log('Object array is null');
        return null;
    }

    // 获取 JNIEnv
    let env = Java.vm.tryGetEnv();
    let className = env.getObjectClassName(objArray);

    // 不是 jobjectArray
    if (!className.startsWith('[L')) {
        console.log(`Argument is not a jobjectArray, actual type: ${className}`);
        return null;
    }

    let arrLen = env.getArrayLength(objArray);
    let result = []

    for (let i = 0; i < arrLen; i++) {
        let element = env.getObjectArrayElement(objArray, i);
        let elementClassName = env.getObjectClassName(element);
        let castElement = Java.cast(element, Java.use(elementClassName));

        result.push(castElement)
    }

    return result;
}

比如 jobjectArray 中第一个元素是 java.lang.Integer,经过 parseObjectArray 后可以直接访问元素中的 intValue() 方法判断 值是否等于 283

javascript 复制代码
function hook_native_func(targetAddress) {
    // Hook 目标地址
    Interceptor.attach(targetAddress, {
        onEnter: function (args) {
            let arr = parseObjectArray(args[2])
            // 判断数组中第一个元素是否 283
            if (arr[0].intValue() === 283) {
                this.log = 'Entering native function at: ' + targetAddress + '\n';
                this.log += printObjectArray(args[2])
            }
        },

        onLeave: function (retval) {
            if (this.log) {
                if (!retval.isNull()) {
                    let className = Java.vm.tryGetEnv().getObjectClassName(retval)
                    retval = Java.cast(retval, Java.use(className));
                }
                this.log += 'Leaving native function,retval: ' + retval
                console.log(this.log);
            }
        }
    });
}

执行脚本

r 复制代码
frida -H 127.0.0.1:1234 -F -l hook_native_func.js

输出如下:

vbnet 复制代码
Entering native function at: 0x6f22493fa8
Object array of type [Ljava.lang.Object;, length: 3
  [0] java.lang.Integer: 283
  [1] com.abc.duapp.modules.app.DuApplication: com.abc.duapp.modules.app.DuApplication@b68b4ae
  [2] java.lang.String: cipherParamuserNamecountryCode86loginTokenpassword675d0c16c532e8dc96ab17490beplatformandroidtimestamp2195743174404typepwduserNamef573fa1fa140cf340018011db67c963cd733_1uuid84c3a328fb63819bv5.43.0
Leaving native function,retval: dWWoXlbR3K87j2N27Dkv4uOPUnOsh...

创建 jobjectArray

通过 frida 创建 jobjectArray 填充数据,调用 NativeFunction

ini 复制代码
function NCall_IL() {
    Java.perform(function () {
        let targetAddress = Module.findBaseAddress("libGameVMP.so").add(0xdfa8)

        let IL = new NativeFunction(
            ptr(targetAddress),                 // 函数地址
            'pointer',                          // 返回值类型:jstring
            ['pointer', 'pointer', 'pointer']   // 参数类型列表(JNIEnv* , jclass, jobjectArray)
        );

        // 获取 env 和 jclass
        const env = Java.vm.tryGetEnv();
        const clazz = env.findClass("java.lang.Object");

        // 构造元素
        const arg0 = 283;
        const arg1 = Java.use("com.cyrus.duapp.modules.app.DuApplication").instance.value; // 读取 static 字段 instance
        const arg2 = "cipherParamuserNamecoun...";

        // 获取 java.lang.Object class (jclass)
        const objectClass = env.findClass("java.lang.Object");

        // 创建一个长度为 3 的 jobjectArray(即 Object[])
        const arrayLength = 3;
        const objectArray = env.newObjectArray(arrayLength, objectClass, ptr(0));

        // int 参数
        const intArg = Memory.alloc(4);
        intArg.writeS32(arg0);

        // 填充数据
        env.setObjectArrayElement(objectArray, 0, intArg);
        env.setObjectArrayElement(objectArray, 1, arg1.$handle); //  $handle 是 Java 层对象的 native JNI 指针表示
        env.setObjectArrayElement(objectArray, 2, env.newStringUtf(arg2));

        // 调用
        let result = IL(env.handle, clazz, objectArray);
        console.log("函数返回值:", result);
    })
}

调用输出如下:

vbnet 复制代码
[Remote::CYRUS]-> NCall_IL()
Entering native function at: 0x6f22499fa8
Object array of type [Ljava.lang.Object;, length: 3
  [0] java.lang.Integer: 283
  [1] com.cyrus.duapp.modules.app.DuApplication: com.cyrus.duapp.modules.app.DuApplication@89c9f57
  [2] java.lang.String: appKey1e4e9a461f9b4fb09d6a4ae12c1eca83loginTokenplatformandroidsymbol...
Leaving native function,retval: AC8aG5GIwLORLMYNGmc6BE2c3IgGXgoBn3fqYpySA+...
相关推荐
BD_Marathon5 小时前
【MySQL】函数
android·数据库·mysql
西西学代码6 小时前
安卓开发---耳机的按键设置的UI实例
android·ui
maki07710 小时前
虚幻版Pico大空间VR入门教程 05 —— 原点坐标和项目优化技巧整理
android·游戏引擎·vr·虚幻·pico·htc vive·大空间
千里马学框架11 小时前
音频焦点学习之AudioFocusRequest.Builder类剖析
android·面试·智能手机·车载系统·音视频·安卓framework开发·audio
fundroid14 小时前
掌握 Compose 性能优化三步法
android·android jetpack
TeleostNaCl15 小时前
如何在 IDEA 中使用 Proguard 自动混淆 Gradle 编译的Java 项目
android·java·经验分享·kotlin·gradle·intellij-idea
旷野说16 小时前
Android Studio Narwhal 3 特性
android·ide·android studio
maki0771 天前
VR大空间资料 01 —— 常用VR框架对比
android·ue5·游戏引擎·vr·虚幻·pico
xhBruce1 天前
InputReader与InputDispatcher关系 - android-15.0.0_r23
android·ims
领创工作室1 天前
安卓设备分区作用详解-测试机红米K40
android·java·linux