逆向时用frida hook java层相对比较简单,找准hook点用objection就行!或则自己写脚本hook java常见的加密/编码也很简单,核心原因就是类名、函数名称得以保留,逆向人员能快速定位!java层常见的加密/编码hook脚本这里有:https://www.cnblogs.com/theseventhson/p/14852458.html ; java层的解决了? so层怎么办了? 因为so层用c/c++写的,编译后的release版本把变量名、函数名都去掉了,加密/编码算法单从名称上很难直接看准,需要逐个查看字符换、变量等,这就涉及到hook了,本文分享一些常用的so层hook脚本,希望对frida的粉丝有用!
1、OLLVM字符串加密
很多APP都会用OLLVM加密关键字段,然后在init_array解密!技术好的同学可以跟踪init_array的函数,看看这些字符串是怎么解密的!对于想"偷懒"的同学来说,可以直接用frida hook字符串的地址来查看解密后的值,脚本如下:
function print_string(addr) {
var base_hello_jni = Module.findBaseAddress("libxxxx.so");
var addr_str = base_hello_jni.add(addr);
console.log("addr:", addr, " ", ptr(addr_str).readCString());
}
使用的时候把so改成字符串所在的so;addr就是字符串相对so的偏移,在IDA中是可以查到的,比如下面这种:
打印结果:
2、registerNative:这个函数的作用就不赘述了;因为从第三个参数能看到jni函数的映射关系,而很多加解密函数都是Java层声明、在so层实现的,所以这个函数格外重要;下面这段代码可以动态获取registerNative函数地址,并且打印第三个参数的内容:
function hook_libart() {
var module_libart = Process.findModuleByName("libart.so");
var symbols = module_libart.enumerateSymbols(); //枚举模块的符号
var addr_GetStringUTFChars = null;
var addr_FindClass = null;
var addr_GetStaticFieldID = null;
var addr_SetStaticIntField = null;
var addr_RegisterNatives = null;
for (var i = 0; i < symbols.length; i++) {
var name = symbols[i].name;
if (name.indexOf("art") >= 0) {//动态获取各个函数的地址
if ((name.indexOf("CheckJNI") == -1) && (name.indexOf("JNI") >= 0)) {
if (name.indexOf("GetStringUTFChars") >= 0) {
console.log(name);
addr_GetStringUTFChars = symbols[i].address;
} else if (name.indexOf("FindClass") >= 0) {
console.log(name);
addr_FindClass = symbols[i].address;
} else if (name.indexOf("GetStaticFieldID") >= 0) {
console.log(name);
addr_GetStaticFieldID = symbols[i].address;
} else if (name.indexOf("SetStaticIntField") >= 0) {
console.log(name);
addr_SetStaticIntField = symbols[i].address;
} else if (name.indexOf("RegisterNatives") >= 0) {
console.log(name);
addr_RegisterNatives = symbols[i].address;
}
}
}
}
if (addr_RegisterNatives) {
Interceptor.attach(addr_RegisterNatives, {
onEnter: function (args) {
console.log("addr_RegisterNatives:", hexdump(args[2])); //打印第三个参数,也就是java和native映射的数组首地址
console.log("addr_RegisterNatives name:", ptr(args[2]).readPointer().readCString())//java层函数名称
console.log("addr_RegisterNatives sig:", ptr(args[2]).add(Process.pointerSize).readPointer().readCString());//函数参数
console.log("addr_RegisterNatives addr:", ptr(args[2]).add(Process.pointerSize+Process.pointerSize));//native函数入口地址
}, onLeave: function (retval) {
}
});
}
}
注意:因为一个jni函数注册只调用一次registerNative,所以这里建议用frida -U --no-pause -f com.xxxx.xxxx -l xxxx.js命令注入js,同时启动目标app;如果人为开启目标app,再运行frida,可能regiserNative函数已经执行过了!
3、 inline hook:想要查看某些函数的临时变量,而这些变量存放在寄存器、不在栈或堆上,没法用第一种方式打印,这可咋整?比如这里我想要查看x13的值(因为保存了异或的结果):
hook代码如下: 关键指令的位置在0x731C,所以这里hook 0x7320,然后调用this.context.x13打印想要看的寄存器值;
//刚注入的时候这个so还没加载,需要hook dlopen
function inline_hook() {
var base_hello_jni = Module.findBaseAddress("libxxxx.so");
console.log("base_hello_jni:", base_hello_jni);
if (base_hello_jni) {
console.log(base_hello_jni);
//inline hook
var addr_07320 = base_hello_jni.add(0x07320);//指令执行的地址,不是变量所在的栈或堆
Interceptor.attach(addr_07320, {
onEnter: function (args) {
console.log("addr_07320 x13:", this.context.x13);//注意这里是怎么得到寄存器值的
}, onLeave: function (retval) {
}
});
}
}
//8.0以下所有的so加载都通过dlopen
function hook_dlopen() {
var dlopen = Module.findExportByName(null, "dlopen");
Interceptor.attach(dlopen, {
onEnter: function (args) {
this.call_hook = false;
var so_name = ptr(args[0]).readCString();
if (so_name.indexOf("libxxxx.so") >= 0) {
console.log("dlopen:", ptr(args[0]).readCString());
this.call_hook = true;//dlopen函数找到了
}
}, onLeave: function (retval) {
if (this.call_hook) {//dlopen函数找到了就hook so
inline_hook();
}
}
});
// 高版本Android系统使用android_dlopen_ext
var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");
Interceptor.attach(android_dlopen_ext, {
onEnter: function (args) {
this.call_hook = false;
var so_name = ptr(args[0]).readCString();
if (so_name.indexOf("libhxxxx.so") >= 0) {
console.log("android_dlopen_ext:", ptr(args[0]).readCString());
this.call_hook = true;
}
}, onLeave: function (retval) {
if (this.call_hook) {
inline_hook();
}
}
});
}
这里有两点需要注意:(1)因为不知道目标so什么时候加载,所以要hook dlopen相关函数;确认目标so加载后才执行hook代码 (2)不同版本的dlopen不同,两种情况都要考虑到!异或后的结果:
4、OLLVM函数混淆/指令替换:不管是控制流平坦化,还是虚假控制流,原有的函数调用关系是不会改变的!所以有这么几种方式可以摸清函数的功能:
(1)一般来说:不在条件里面的函数是比较重要的,建议优先hook观察;比如有些函数在if、while等条件里面,这些函数的重要性是不如在条件外(无条件执行)的函数重要的!
(2)可以根据函数体内非if条件用到的常量大致判断函数加解密的类型,具体可以参考这里:https://www.cnblogs.com/theseventhson/p/14852458.html
(3)hook函数参数的时候,参数不外乎这么几种类型:
(4)还有某些指令替换,加入大量的垃圾指令混淆视听,但函数调用的关系不会变!比如下面这种: 上面红框框都是垃圾指令,下面红框框才是核心的算法函数;
注意:如果无法判断这是正常的算法,还是垃圾指令替换,可以把这些常量google一下,看看到底是啥!
(5)hook某些函数,让这些函数在被调用时执行我们的代码,这个实现很简单,比如这种常见的Java.use,本质是在类函数额外增加一些代码,等到这个类的函数被调用时执行我们插入的代码!
function hook_sign2() {
Java.perform(function () {
var HelloJni = Java.use("com.xxxx.xxxx");
HelloJni.sign2.implementation = function (str, str2) {
var result = this.sign2(str, str2);
console.log("HelloJni.sign2:", str, str2, result);
return result;
};
});
}
但是有些时候我们需要主动调用目标函数,避免挨个手动去点击(避免麻烦,提高自动化测试或逆向的效率,还可以主动使用我们想用的参数),可以用Java.choose在堆内存去找对象实例:其中ins是实例符号,可以直接用ins.xxx来主动调用类成员函数!
function call_sign2() {
Java.perform(function () {
Java.choose("com.xxxx.xxxx", {
onMatch: function (ins) {
var result = ins.sign2("0123456789", "abcdefghakdjshgkjadsfhgkjadshfg");
console.log(result);
}, onComplete: function () {
}
});
});
}
也可以用classloader来找到对象实例,代码如下:这个是模拟抢红包时点击onClick的动作;
Java.perform(function () {
Java.enumerateClassLoadersSync().forEach(function (classloader) {
try {
var receive = classloader.findClass("com.xxxxx");
var view = Java.use('android.view.View').$new();
console.log("com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyNotHookReceiveUI$15.onClick before invoke onclick3!");
receive.onClick(view);
console.log("com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyNotHookReceiveUI$15.onClick after invoke onclick!3");
} catch (e) {
console.log("com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyNotHookReceiveUI$15.onClick exception:"+e);//类找不到
}
})
})
主动调用有一点需要注意: 因为是去堆内存查找类的实例,所以如果类还没生成实例时就调用,会报xxxx class not found error!
上述都是java层函数的主动调用;因为java是完全面向对象的,所有的方法都包含在类里面;要调用方法直接用对象去调用就行了;但是so层可能是用c写的,没有对象实例,怎么主动调用so层的方法了?用new NativeFuntion找到代码实例后再调用,demo代码如下:
// 绑定
var thiscall_func = new NativeFunction(ptr("0x0041153C"), // 函数地址
'int', // 返回值类型
['pointer', 'int'],// 函数参数(__thiscall的第一个参数为this指针)
'thiscall' // 调用约定
);
// 调用并打印返回值
console.log(thiscall_func( ptr('0x00421360'), 0xb ));
(6)还有“二级”指针的打印方式:
var sub_18AB0 = base_hello_jni.add(0x18AB0);
Interceptor.attach(sub_18AB0, {
onEnter: function (args) {
this.arg0 = args[0];
this.arg1 = args[1];
this.arg8 = this.context.x8;
console.log("sub_18AB0 onEnter:", hexdump(args[0]), "\r\n", hexdump(args[1]));
}, onLeave: function (retval) {
//console.log("sub_18AB0 onLeave:", hexdump(retval));
console.log("sub_18AB0 onLeave:", hexdump(this.arg8));
console.log("sub_18AB0 onLeave:", hexdump(ptr(this.arg8).add(Process.pointerSize * 2).readPointer()));//二级指针
// console.log("sub_18AB0 onLeave:", hexdump(this.arg0), "\r\n", hexdump(this.arg1));
}
});
打印结果:
5、Native api的获取方法:Java.vm.tryGetEnv().xxxx
var sign2 = Module.findExportByName("libhello-jni.so", "Java_com_example_hellojni_HelloJni_sign2");
console.log(sign2);
Interceptor.attach(sign2, {
onEnter: function (args) {
//jstring
console.log("sign2 str1:", ptr(Java.vm.tryGetEnv().getStringUtfChars(args[2])).readCString());
console.log("sign2 str2:", ptr(Java.vm.tryGetEnv().getStringUtfChars(args[3])).readCString());
}, onLeave: function (retval) {
console.log("sign2 retval:", ptr(Java.vm.tryGetEnv().getStringUtfChars(retval)).readCString());
}
});
6、其他常见java类hook函数 :
(1)String类:很多关键字符需要通过string类生成,hook代码如下:
function hookjavalangString() {
Java.perform(function () {
var JavaString = Java.use('java.lang.String');
JavaString.$init.overload('java.lang.String').implementation = function (content) {
console.log('JavaString.$init.overload(\'java.lang.String\')->' + content);
var result = this.$init(content);
return result;
};
JavaString.$init.overload('[C').implementation = function (content) {
console.log("JavaString.$init.overload('[C')->" + content);
var result = this.$init(content);
return result;
};
var StringFactory = Java.use('java.lang.StringFactory');
StringFactory.newStringFromString.implementation = function (arg0) {
console.log("java.lang.StringFactory.newStringFromString->" + arg0);
var result = this.newStringFromString(arg0);
return result;
};
var exampleString1 = JavaString.$new('Hello World, this is an example string in Java.');
console.log('[+] exampleString1: ' + exampleString1);
})
}
(2)JSONObject:很多app在拼接关键字段时用的就是这个类的put方法,然后通过toString方法转成String,hook代码如下:
var JSONObject=Java.use('org.json.JSONObject');
JSONObject.toString.overload().implementation = function(){
send("=================org.json.JSONObject.toString====================");
send(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
var data=this.toString();
send("org.json.JSONObject.toString result:"+data);
return data;
}
for(var i = 0; i < JSONObject.put.overloads.length; i++){
JSONObject.put.overloads[i].implementation = function(){
send("=================org.json.JSONObject.put====================");
if(arguments.length == 2){
send(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
send("key:"+arguments[0]);
send("value:"+arguments[1]);
var data=this.put(arguments[0],arguments[1]);
return data;
}
}
}
for(var i = 0; i < JSONObject.$init.overloads.length; i++){
JSONObject.$init.overloads[i].implementation = function(){
send("=================org.json.JSONObject.$init====================");
send(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
if(arguments.length == 1){//只有1个string参数
send("string:"+arguments[0]);
}else if(arguments.length == 2){ //其他构造函数用到的时候可以继续添加
}
}
}
(3)另一个和JSONObject类似的类是hashmap了,hook代码如下:
var linkerHashMap=Java.use('java.util.LinkedHashMap');
linkerHashMap.put.implementation = function(arg1,arg2){
send("=================linkerHashMap.put====================");
var data=this.put(arg1,arg2);
send(arg1+"-----"+arg2);
send(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
return data;
}
(4)以上是java层的字符串拼接相关函数;部分防护很好的app转移到so层拼接字符串了,比如在so层调用java层的stringBuffer拼接,拼接的代码如下:
所以hook的时候也要重点关注这些函数!针对这些jni函数的跟踪,可以用jnitrace(https://github.com/chame1eon/jnitrace),效果如下:
这些jni函数的地址、调用顺序、参数都被看的一清二楚!
7、某些app会做各种检测,比如frida、xpose、模拟器检测,一旦发现这些就直接在so层调用kill、exit等方法退出;为了反检测,可以直接静态NOP掉这些关键代码,也可以通过frida动态打补丁NOP掉这些退出的代码(当然也可以直接hook kill或exit函数,这两个函数一旦被调用啥都不做,直接返回),以32位为例,js代码如下:
function dis(address, number) {
for (var i = 0; i < number; i++) {
var ins = Instruction.parse(address);
console.log("address:" + address + "--dis:" + ins.toString());
address = ins.next;
}
}
//libc->strstr() 从linker里面找到call_function的地址:趁so代码还未执行前就hook
function hook() {
//call_function("DT_INIT", init_func_, get_realpath());
var linkermodule = Process.getModuleByName("linker");
var call_function_addr = null;
var symbols = linkermodule.enumerateSymbols();
for (var i = 0; i < symbols.length; i++) {
var symbol = symbols[i];
//LogPrint(linkername + "->" + symbol.name + "---" + symbol.address);
if (symbol.name.indexOf("__dl__ZL13call_functionPKcPFviPPcS2_ES0_") != -1) {
call_function_addr = symbol.address;
//LogPrint("linker->" + symbol.name + "---" + symbol.address)
}
}
Interceptor.attach(call_function_addr, {
onEnter: function (args) {
var type = ptr(args[0]).readUtf8String();
var address = args[1];
var sopath = ptr(args[2]).readUtf8String();
console.log("loadso:" + sopath + "--addr:" + address + "--type:" + type);
if (sopath.indexOf("libnative-lib.so") != -1) {
var libnativemodule = Process.getModuleByName("xxxx.so");//call_function正在加载目标so,这时就拦截下来
var base = libnativemodule.base;
dis(base.add(0x1234).add(1), 10);
var patchaddr = base.add(0x2345);//改so的机器码,避免待会完全加载后运行时就错过时机了!
Memory.patchCode(patchaddr, 4, patchaddr => {
var cw = new ThumbWriter(patchaddr);
cw.putNop();
cw = new ThumbWriter(patchaddr.add(0x2));
cw.putNop();
cw.flush();
});
console.log("+++++++++++++++++++++++")
dis(base.add(0x1234).add(1), 10);
console.log("----------------------")
dis(base.add(0x2345).add(1), 10);
Memory.protect(base.add(0x8E78), 4, 'rwx');
base.add(0x1234).writeByteArray([0x00, 0xbf, 0x00, 0xbf]);
console.log("+++++++++++++++++++++++")
dis(base.add(0x2345).add(1), 10);
}
}
})
}
function main() {
hook();
}
setImmediate(main);
参考:
1、https://eternalsakura13.com/2020/07/04/frida/ Frida Android hook
2、https://github.com/lasting-yang frida hook
3、https://frida.re/docs/javascript-api/ frida官网api