frida被常用于android应用测试中,很多时候要对应用中的java代码进行hook,此时,常用的对象就是内置的Java对象,各种操作离不开这个内置对象,但是,除了官方网站的javascript API外,关于它的文档并不多。
该对象实质对应的代码在这个项目中:https://github.com/frida/frida-java-bridge/
在对java对象进行操作时,我们可以直接使用javascript中的数据类型,这方便了不少工作。然而有人会好奇,这是怎么实现的呢?
本文将对frida-java-bridge的数据类型封装进行简单的分析。
frida-java-bridge中对于对象类型的处理位于type.js。
type来完成对象实例和js对象的转换,有两个函数:fromJni
和toJni
,负责将对象在内存中的值和js中的值进行转换。在对对象进行操作,或者涉及函数调用的参数和返回值等时,转换会被调用。
比如说在hook到方法调用,转交给设置的implementation时,会这么处理
// class-factory.js 1617 // handleMethodInvocation中对于参数的处理 for (let i = 0; i !== numArgs; i++) { const t = argTypes[i]; const value = t.fromJni(jniArgs[2 + i], env, false); args.push(value); ownedObjects.push(value); } // class-factory.js 1628 // handleMethodInvocation中对于implementation返回结果的处理 if (!retType.isCompatible(retval)) { throw new Error(`Implementation for ${methodName} expected return value compatible with ${retType.className}`); } let jniRetval = retType.toJni(retval, env);
再比如说在主动调用某个方法时,会这么处理
// class-factory.js 1538 // methodPrototype中invoke对应的函数中对参数的处理 for (let i = 0; i !== numArgs; i++) { jniArgs.push(argTypes[i].toJni(args[i], env)); } // class-factory.js 1556 // methodPrototype中invoke对应的函数中对返回值的处理 return retType.fromJni(jniRetval, env, true);
我们再来看type中进行的对象转换,举个例子:
fromJni (h, env, owned) { if (h.isNull()) { return null; } if (typeIsDefaultString() && unbox) { return env.stringFromJni(h); } return factory.cast(h, factory.use(typeName), owned); }, toJni (o, env) { if (o === null) { return NULL; } if (typeof o === 'string') { return env.newStringUtf(o); } return o.$h; }
再看一些基本类型:
boolean: { name: 'Z', type: 'uint8', size: 1, byteSize: 1, defaultValue: false, isCompatible (v) { return typeof v === 'boolean'; }, fromJni (v) { return !!v; }, toJni (v) { return v ? 1 : 0; }, read (address) { return address.readU8(); }, write (address, value) { address.writeU8(value); } },
可见frida-java-bridge在调用前后对于javascript对象进行了双向的处理,以便符合JNI调用的格式。
另外,一个方法的各类信息被保存在它的_p
属性中,如果要获取某个方法的参数类型和返回值类型,那么可以使用以下代码:
var [methodName, classWrapper, type, retType, argTypes, handler, fallback, pendingCalls] = method._p
如果要获取某个指定Class的类型来做转换,那么可以通过Java._getType(typeName)
来获得
一般情况下,这些数据类型转换的封装极大的方便了代码的编写,但是在一些容易被忽略的角落里,这些数据类型与Java中的数据类型并不一致,这就带来了一些诡异的坑。
比如,我们知道在java中,对象数组也被认为是java.lang.Object对象。然而,在frida-java-bridge中并不是。当你尝试去做类型转换,并把对象数组塞到一个接受Object类型的函数中去时,你会发现,类型转换居然失败了。
因为java.lang.Object的type是这样的:
{ name: 'Ljava/lang/Object;', type: 'pointer', size: 1, defaultValue: NULL, isCompatible (v) { if (v === null) { return true; } if (v === undefined) { return false; } const isWrapper = v.$h instanceof NativePointer; if (isWrapper) { return true; } return typeof v === 'string'; }, fromJni (h, env, owned) { if (h.isNull()) { return null; } return factory.cast(h, factory.use('java.lang.Object'), owned); }, toJni (o, env) { if (o === null) { return NULL; } if (typeof o === 'string') { return env.newStringUtf(o); } return o.$h; } };
而一个js数组对象是没有.$h
的,于是你会得到莫名其妙的报错。而Java.cast时会检查isCompatible,结果还是因为同样的原因,报错,没有办法进行转换。坑,frida认为对象数组不是对象……
所以……怎么做呢,只有找到对象数组对应的type,做一次toJni,然后再用目标类型的type,做一次fromJni,比如说我在XposedFridaBridge中做的:
var env = Java.vm.getEnv() var retType = fridaMethod._p[4] var hhmRetType = XposedBridge.handleHookedMethod.overloads[0]._p[4] return retType.fromJni(hhmRetType.toJni(xposedResult, env), env, false)
还有什么坑呢?就是基本类型直接是javascript类型,这些类型也是没有.$h
的,所以,他们也不是Object……只能手动用java.lang.Integer这样的对象进行转换了。
Frida中Java对象对应的是frida-java-bridge,其中数据类型的转换是由type.js负责的,很不幸在映射的时候与java中对象并不一致。