参考文档
https://api-caller.com/2019/03/30/frida-note/
https://frida.re/docs/javascript-api/#frida
数组打印
测试代码:
private static class Bean {
String a;
int b;
float c;
}
private void test() {
Bean[] beans = new Bean[3];
beans[0] = new Bean();
beans[0].a = "a";
beans[0].b = 1;
beans[0].c = 1f;
beans[1] = new Bean();
beans[1].a = "b";
beans[1].b = 2;
beans[1].c = 2f;
beans[2] = new Bean();
beans[2].a = "c";
beans[2].b = 2;
beans[2].c = 3f;
Arrays.toString(beans);
}
想要 hook Arrays.toString()
方法很简单:
function main() {
Java.perform(function () {
Java.use("java.util.Arrays").toString.overload('[Ljava.lang.Object;').implementation = function (x) {
var result = this.toString(x);
console.log("params=", x);
console.log("result=", result)
return result
}
})
}
setImmediate(main)
输出的结果很多:
可以看到,输出的数组里是一个对象,那有没有什么好办法将对象转成字符串显示出来呢?
答案就是Gson,所以使用开发的思想来做逆向是很重要的。
项目里面不一定引入了Gson,所以我们需要自己编译一个gons库,放到手机里面,然后使用frida加载一下就可以使用了。
加载外部DEX
var dexPath = "/data/local/tmp/r0gson.dex";
Java.openClassFile(dexPath).load();
已经有大佬编译好了的dex,我们可以直接用:
https://github.com/r0ysue/AndroidSecurityStudy/blob/master/FRIDA/r0gson.dex.zip
为了避免类重复,还特意换了包名,具体可看:
https://bbs.kanxue.com/thread-259186.htm
重新写脚本:
function main() {
Java.perform(function () {
Java.use("java.util.Arrays").toString.overload('[Ljava.lang.Object;').implementation = function (x) {
var result = this.toString(x);
if (x.length > 0 && x[0].getClass().getName() == "com.example.demo2.MainActivity$Bean") {
Java.openClassFile("/data/local/tmp/r0gson.dex").load();
const gsonClass = Java.use('com.r0ysue.gson.Gson');
for (var i=0; i<x.length; i++) {
console.log("entry=", gsonClass.$new().toJson(x[i]));
}
}
return result;
}
})
}
setImmediate(main)
看下输出结果:
Spawned `com.example.demo2`. Resuming main thread!
[Pixel::com.example.demo2]-> entry= {"a":"a","b":1,"c":1.0}
entry= {"a":"b","b":2,"c":2.0}
entry= {"a":"c","b":2,"c":3.0}
构造数组
有时候为了替换返回值,需要一个数组类型,可以使用如下方式,构造一个数组:
const values = Java.array('int', [ 1003, 1005, 1007 ]);
const JString = Java.use('java.lang.String');
const str = JString.$new(Java.array('byte', [ 0x48, 0x65, 0x69 ]));
类型转换
测试代码:
Father father = new Father();
Son son = new Son();
Father father2 = new Son();
脚本:
Java.choose('com.example.demo2.Father', {
onMatch: function (instance) {
console.log('found instance', instance);
var son = Java.cast(instance, Java.use('com.example.demo2.Son'))
console.log('cast instance', son.test());
}, onComplete: function () { }
})
Java.choose('com.example.demo2.Son', {
onMatch: function (instance) {
console.log('found instance', instance);
var father = Java.cast(instance, Java.use('com.example.demo2.Father'))
console.log('cast instance', father.test());
}, onComplete: function () { }
})
输出结果:
found instance com.example.demo2.Father@daf1aad
found instance com.example.demo2.Son@c7d41e2
cast instance Son
found instance com.example.demo2.Son@2341973
cast instance Son
Error: Cast from 'com.example.demo2.Father' to 'com.example.demo2.Son' isn't possible
at cast (frida/node_modules/frida-java-bridge/lib/class-factory.js:131)
at cast (frida/node_modules/frida-java-bridge/index.js:270)
at onMatch (/demo2.js:20)
at _chooseObjectsArtPreA12 (frida/node_modules/frida-java-bridge/lib/class-factory.js:298)
at <anonymous> (frida/node_modules/frida-java-bridge/lib/class-factory.js:250)
at vt (frida/node_modules/frida-java-bridge/lib/android.js:573)
符合直觉,子类可以转成父类类型,但是调用的方法还是子类的。
注册一个类
有时候,我们想做一个AOPhook的时候,就需要实现一个接口,我们可以使用 registerClass 方法来做到。
测试代码:
public interface IBook {
String id();
int size();
boolean test(int input);
}
// -------------------------
public class SimpleBook implements IBook {
@Override
public String id() {
return UUID.randomUUID().toString();
}
@Override
public int size() {
return 100;
}
@Override
public boolean test(int input) {
Log.e("SimpleBook", "input = " + input);
return false;
}
}
// -------------------------
IBook book = new SimpleBook();
脚本代码:
Java.registerClass({
name: 'com.example.demo2.SimpleBook2',
implements: [Java.use('com.example.demo2.IBook')],
fields: {
proxy: 'com.example.demo2.IBook',
},
methods: {
'<init>': [{
returnType: 'void',
argumentTypes: ['com.example.demo2.IBook'],
implementation: function (proxy) {
this.$super.$init();
this.proxy.value = proxy;
}
}],
id() {
return this.proxy.value.id();
},
size() {
return this.proxy.value.size();
},
test(input) {
this.proxy.value.test(input);
return true;
},
}
})
Java.choose('com.example.demo2.MainActivity', {
onMatch: function (instance) {
console.log('found MainActivity instance', instance);
var oldBook = instance.book.value;
instance.book.value = Java.use('com.example.demo2.SimpleBook2').$new(oldBook);
console.log('book test id = ', instance.book.value.id());
console.log('book test size = ', instance.book.value.size());
console.log('book test result = ', instance.book.value.test(1));
}, onComplete: function () { }
})
这里我们注册了一个类,com.example.demo2.SimpleBook2,并且实现了一个代理类,将 MainActivity 的字段替换之后,我们就可以让代理类来托管逻辑,做很多操作。
打印Map
与开发时的写法是一样的:
var result = "";
var keyset = map.keySet();
var it = keyset.iterator();
while(it.hasNext()){
var keystr = it.next().toString();
var valuestr = map.get(keystr).toString();
result += valuestr;
}
Non-ASCII 方法名处理
比如说
int ֏(int x) {
return x + 100;
}
甚至有一些不可视, 所以可以先编码打印出来, 再用编码后的字符串去 hook。
var methods = Java.use('com.example.demo2.MainActivity').class.getDeclaredMethods();
for (var i in methods) {
console.log('origin method name -> ' + methods[i].toString());
console.log('encode method name ->' + encodeURIComponent(methods[i].toString().replace(/^.*?\.([^\s\.\(\)]+)\(.*?$/, "$1")));
}
Java.use('com.example.demo2.MainActivity')[decodeURIComponent("%D6%8F")].implementation = function() {
console.log('method invoke');
return 200;
}