前言
這個專題是根據白龍,龍哥的unidbg博客的案例,進行從0開始到逆向的流程,核心部分會借鑑龍哥的unidbg,通過借鑑大佬的思路,完整的分析某個so層的加密參數
各位朋友也可以直接讀龍哥的博客,我只是用我的角度進一步加工一下
原文地址:SO逆向入門實戰教程一:OASIS_so逆向學習路線_白龍~的博客-CSDN博客
分析
首先拿到這個app,安裝啥的就不多說了。
進入到註冊界面:
點擊獲取驗證碼,然後這邊抓包工具抓到的包:
然後,這裏面的【sign】就是今天的重點了。
用神祕的工具脫殼完之後,有如下的dex:
把這些dex,全部選中,拖進jadx
快速定位
接下來開始找sign所在的位置,先看看抓包工具這邊的參數,根據這些鍵名,一頓搜,總能找到一些
先搜下【sign】,雖然搜出來的結果不多,但是感覺有很多幹擾項
進最後那個,到這裏,
這些參數看着很像,用frida hook,得知,發現並沒有走到這裏
搜【ua】
進到這裏,發現很可疑,好多參數都對上了
啥都不說,先用objection hook下,然後app端點擊【重新發送】看看:
可以,這不直接就定位邏輯了
但是我們要找【sign】,所以還得再看看,jadx查看發現,這個方法反編譯效果不太好,問題不大,記住這個dex名,然後用GDA 打開這個dex,這就挺好,基本都反編譯出來了
反正看着確實可疑,但是這三個方法,還不確定到底是是哪個方法裏有【sign】部分,所以這裏直接hook 它這個a,b,c三個方法,然後app點下重新獲取
ok,發現最後的方法裏基於有我們要的sign,這邊再來下,用抓包工具對比下,謹慎一點,別搞半天沒搞對位置:
重新hook下,然後抓包工具打開看看
對上了,ok,就是這裏了【g.a.c.g.c.a】
在jadx裏,反編譯失敗:
問題不大,用GDA看:
這不就越來越接近了嗎,嘻嘻,點進去:
hook下這個c方法看看,ok,對上了
接着一頓分析後再進入這裏
ok,進到了native層,那麼核心的邏輯就在這裏了。
像這種,常規的方法怎麼解決呢?
- 如果你是爲了拿數據,趕工期的話,那你可以直接主動調用這個方法
- 如果你是爲了純算還原的話,那就把這個so文件拖進ida一頓分析了
- 那麼假如,這個方法的純算很複雜,而你又不想主動調用,感覺很low的話,那你就可以嘗試用unidbg了
unidbg,就是本系列文章的重點了。
unidbg簡介
什麼是unidbg
unidbg 是一個基於 unicorn 的逆向工具,可以實現黑盒調用安卓和 iOS 中的 so 文件
unidbg是凱神寫的一個開源的java項目
使用場景
因爲現在的大多數 app 把核心的加密算法放到了 so 文件中(比如本例的app),你要想破解簽名算法,必須能夠破解 so 文件。但C++ 的逆向遠比 Java 的逆向要難得多,有各種混淆啊,ollvm啥的,所以好多時候是沒法純算還原破解的
那麼你是否有過一個想法,能不能把安卓的環境模擬出來,但是又脫離了安卓真機的環境,就可以直接使用so裏的方法呢?
unidbg 就是這樣一個工具,它模擬好了好幾種虛擬環境,他不需要直接運行 app,也無需逆向 so 文件,而是直接找到對應的 JNI 接口,然後用 unicorn 引擎(也不止這一個引擎)直接執行這個 so 文件,所以效率也比較高。
配置unidbg
1.首先,用git 把這個項目clone 下來:
2.再用idea(寫java項目那個編輯器)打開,
首次打開,右下角會下載很多依賴環境,等待即可
然後隨便找一個項目,執行下main方法,有正常輸出,說明環境配置好了:
執行結果:
ok,這就很nice。
frida調試
接下來,先用ida 打開那個目標so文件
等左下角這個數據沒有再變的時候,說明加載好了
接下來找導出表【export】,或者在左邊的欄裏搜【java】
發現並沒有任何東西,那麼這裏就是動態註冊的so方法了。
什麼是動態註冊、靜態註冊
靜態註冊(又叫靜態綁定),就是,so的方法名直接export導出表裏,且命名格式爲【java_app包名_方法名】,比如 java_com_sina_oasxxx_nativeapi_s
動態註冊(又叫動態綁定)就是export到處表裏沒有的就是動態註冊。
那麼這裏我們的目標方法就是動態註冊的了,那咋辦,看看JNI_load方法:
按下【tab】鍵,會由上面的彙編代碼反編譯爲c代碼:
再按下【\】反斜槓,可讀性更強點:
但是這裏發現,一頓while 和if,根據龍哥的博客說的,大概率是ollvm混淆。那咋辦?
用yang神的hook_native腳本來hook出目標函數的偏移地址 ,然後加上so的基址,就可以得到目標函數的地址了(看是thumb還是arm,thumb要加1,arm不用)
frida_hook_libart/hook_RegisterNatives.js at master · lasting-yang/frida_hook_libart (github.com)
用frida hook一下,結果到這就報錯退出了
就很尷尬,看看,他報的哪個class名,用來過濾下試試:
重新運行frida試試,可以,這下直接就定位到我們要的方法的位置了:
這裏有朋友估計會說,臥槽,這不都出來了嗎,這地址,拿着直接用啊,不急,我重啓腳本看看:仔細看,fnptr地址變了,fnoffset和後面的jni-load + 的地址沒變的
多次hook發現確實如此,因爲這裏就是上面說的動態註冊,所以這個fnptr,也就是so的基址,app沒啓動一次就會變,但是目標方法的偏移值是不會變的。ok
用frida hook so看看:
相關的hoo so,有個大佬總結的很好:
ok,這裏我們hook下,拿下入參和返回值看看:
function inline_hook() { var so_addr = Module.findBaseAddress("liboasiscore.so"); console.log("so_addr:", so_addr); if (so_addr) { var sub = so_addr.add(0x116cc); // 不用加1,是arm架構 console.log("The addr_0x116cc:", sub); Java.perform(function () { Interceptor.attach(sub, { onEnter: function (args) { console.log("addr_0x116cc OnEnter :", this.context.PC, this.context.x1, this.context.x5, this.context.x10); }, onLeave: function (retval) { console.log("retval is :", retval) }, }) }) } } setTimeout(inline_hook, 1000)
運行結果:
發現並不可讀,沒事,反正至少是有了
function stringToBytes(str) { return hexToBytes(stringToHex(str)) } function stringToHex(str) { return str .split('') .map(function (c) { return ('0' + c.charCodeAt(0).toString(16)).slice(-2) }) .join('') } function hexToBytes(hex) { for (let bytes = [], c = 0; c < hex.length; c += 2) bytes.push(parseInt(hex.substr(c, 2), 16)) return bytes } function hexToString(hexStr) { let hex = hexStr.toString() let str = '' for (let i = 0; i < hex.length; i += 2) str += String.fromCharCode(parseInt(hex.substr(i, 2), 16)) return str }
function inline_hook() { var so_addr = Module.findBaseAddress("liboasiscore.so"); console.log("so_addr:", so_addr); if (so_addr) { // var sub = so_addr.add(0x116cc); // 不用加1,是arm架構 console.log("The addr_0x116cc:", so_addr); var ss = "aid=01A8SBOtNRVqsR1ywgkR4tHsZEsgXkGDrgKO2OvFBeThKWZDE.&cfrom=28B5295010&cuid=0&noncestr=L83x8Z40132Wan450y736563n3kmWj&phone=138469655665&platform=ANDROID×tamp=1681790293128&ua=Xiaomi-MI6__oasis__3.5.8__Android__Android9&version=3.5.8&vid=2010511512550&wm=20004_90024"; var add_addr = so_addr.add(0x116cc); // 32位需要加1 var add = new NativeFunction(add_addr, 'pointer', ['pointer', 'int']); console.log(add) var result = add(stringToByte(Memory.allocUtf8String(ss)), false) console.log("add2 result is ->" + result.readCString()); } } function stringToByte(str) { var ch, st, re = []; for (var i = 0; i < str.length; i++) { ch = str.charCodeAt(i); st = []; do { st.push(ch & 0xFF); ch = ch >> 8; } while (ch); re = re.concat(st.reverse()); } // return an array of bytes return re; } setTimeout(inline_hook, 2000)
嘗試主動調用,調試了很久,就是不行
突然反應過來,這個so文件有ollvm混淆啊,雖然用yang神的代碼hook到了偏移地址,但是他內部可能並不是這些參數,而我們又沒法直接分析,那沒法了。其實以上的代碼,在其他地方是可以用的,
那接下來咋辦?上unidbg吧
unidbg調試
上面用了frida+ida調試,發現有的時候沒法搞啊,那麼這裏,終於要用unidbg來模擬執行生成上面目標app的sign了
先創建一個文件,把該有的都放進去
然後再oasis裏寫代碼,照着龍哥的搞就完了:注意文件路徑,跟你實際的路徑保持一致
package com.sina; import com.github.unidbg.AndroidEmulator; import com.github.unidbg.Module; import com.github.unidbg.linux.android.AndroidEmulatorBuilder; import com.github.unidbg.linux.android.AndroidResolver; import com.github.unidbg.linux.android.dvm.AbstractJni; import com.github.unidbg.linux.android.dvm.DalvikModule; import com.github.unidbg.linux.android.dvm.VM; import com.github.unidbg.memory.Memory; import java.io.File; public class oasis extends AbstractJni { private final AndroidEmulator emulator; private final VM vm; private final Module module; oasis() { // 創建模擬器實例,進程名建議依照實際進程名填寫,可以規避針對進程名的校驗 emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.sina.oasis").build(); // 獲取模擬器的內存操作接口 final Memory memory = emulator.getMemory(); // 設置系統類庫解析 memory.setLibraryResolver(new AndroidResolver(23)); // 創建Android虛擬機,傳入APK,Unidbg可以替我們做部分簽名校驗的工作 vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\java\\com\\sina\\lvzhou.apk")); // 加載目標SO DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\sina\\liboasiscore.so"), true); // 加載so到虛擬內存 //獲取本SO模塊的句柄,後續需要用它 module = dm.getModule(); vm.setJni(this); // 設置JNI vm.setVerbose(true); // 打印日誌 dm.callJNI_OnLoad(emulator); // 調用JNI OnLoad }; public static void main(String[] args) { oasis test = new oasis(); } }
然後運行一下,沒啥問題,說明架子是搭上了
仔細看這裏,這樣也把我們要的方法的地址拿到了。舒服啊
用前面frida 的對比,好像不太一樣,問題不大
再來看看代碼:
再來仔細看看他這個main幹了啥:
調用
架子搭好了,接下來調用,調用有兩種方式,一種是符號(symbol)調用,一種是地址調用,符號調用對應靜態註冊,地址調用對應動態註冊。那麼根據前面的解析,這裏我們只能選用地址調用了
先看看我們要調用的方法:
有兩個參數,一個byte數組,一個boolean,然後native層的方法,默認前面會自動加兩個參數 ,一個jni env,一個jobject
還是上面的frida腳本,先hook下,看看入參和返回:
ok,直接拿着這個str參數的值去unidbg構造:
注意,如果你用的舊版,也就是龍哥案例的代碼,直接報錯:
新版得這麼用,運行結果:
package com.sina; import com.github.unidbg.AndroidEmulator; import com.github.unidbg.Module; import com.github.unidbg.linux.android.AndroidEmulatorBuilder; import com.github.unidbg.linux.android.AndroidResolver; import com.github.unidbg.linux.android.dvm.AbstractJni; import com.github.unidbg.linux.android.dvm.DalvikModule; import com.github.unidbg.linux.android.dvm.VM; import com.github.unidbg.linux.android.dvm.array.ByteArray; import com.github.unidbg.memory.Memory; import com.sun.jna.Pointer; import java.io.File; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.lang.Number; public class oasis extends AbstractJni { private final AndroidEmulator emulator; private final VM vm; private final Module module; oasis() { // 創建模擬器實例,進程名建議依照實際進程名填寫,可以規避針對進程名的校驗 emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.sina.oasis").build(); // 獲取模擬器的內存操作接口 final Memory memory = emulator.getMemory(); // 設置系統類庫解析 memory.setLibraryResolver(new AndroidResolver(23)); // 創建Android虛擬機,傳入APK,Unidbg可以替我們做部分簽名校驗的工作 vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\java\\com\\sina\\lvzhou.apk")); // 加載目標SO DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\sina\\liboasiscore.so"), true); // 加載so到虛擬內存 //獲取本SO模塊的句柄,後續需要用它 module = dm.getModule(); vm.setJni(this); // 設置JNI vm.setVerbose(true); // 打印日誌 dm.callJNI_OnLoad(emulator); // 調用JNI OnLoad }; public static void main(String[] args) { oasis test = new oasis(); System.out.println(test.getSign()); } public String getSign(){ List<Object> list = new ArrayList<>(10); list.add(vm.getJNIEnv()); // arg1,env list.add(0); // arg2,jobject String keywords = "aid=01A8SBOtNRVqsR1ywgkR4tHsZEsgXkGDrgKO2OvFBeThKWZDE.&cfrom=28B5295010&cuid=0&noncestr=L83x8Z40132Wan450y736563n3kmWj&phone=138469655665&platform=ANDROID×tamp=1681790293128&ua=Xiaomi-MI6__oasis__3.5.8__Android__Android9&version=3.5.8&vid=2010511512550&wm=20004_90024"; byte[] keyB = keywords.getBytes(StandardCharsets.UTF_8); ByteArray inbarr = new ByteArray(vm,keyB); list.add(vm.addGlobalObject(inbarr)); //arg3 list.add(0); //arg4 // Number number = module.callFunction(emulator,0xC365,list.toArray())[0]; Number number = module.callFunction(emulator,0xC365,list.toArray()); String result = vm.getObject(number.intValue()).getValue().toString(); return result; } }
驗證下結果,對上了,舒服:
結語
unidbg的實用之處就在這裏,相信不用我多說,你已經發現了很多妙用之處