逆向時用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函數參數的時候,參數不外乎這么幾種類型:
- 指針:可以指向字符串、數值、對象(虛函數指針表+成員變量);建議用hexdump打印查看具體內容;
- 正整數:大概率是字符串長度;
- 地址引用:&符號的,可能保存了函數的處理結果,也就是返回值,建議用hexdump打印內容;
(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