利用Frida破解黑盒環境的Dex函數抽取殼

看雪論壇作者ID:飛翔的貓咪
題目出自看雪高研班2021年10月份作業第二題:
函數抽取殼導致了被保護的函數始終運行在解釋模式下。提供的pixel鏡像已經將默認解釋器修改成了Switch實現,請分析該Switch解釋器,并編寫frida hook腳本,能夠實現對解釋器解釋執行的每一條smali指令的跟蹤記錄。提供的測試apk采用了自解密的函數抽取技術。
分析:
Dex函數抽取殼目前算是比較常見的加殼方式,對此種殼目前來說比較流行的脫殼工具是寒冰大佬的fart工具。當然這種殼的脫殼方式不止一種,就像題目中描述的那樣,由于Dex函數最終還是需要通過art虛擬機來執行的,因此在虛擬機執行引擎內部做手腳也可以直接達成脫殼目標,題目已經給出了解法:直接分析Switch實現的解釋器,即可獲取函數執行指令流。就是如果分析的函數比較多,函數比較復雜,在每一條指令執行時做手腳會一定程度上影響App的性能。
對于題目中的8.0系統,解釋器分為Switch實現和Mterp匯編實現的解釋器,Switch實現要好分析很多,因此手頭上如果有源碼的情況下可以強制解釋器用Switch來實現。
做這道題目可以開擴脫殼的思路,原來就算沒有源碼的情況下,用ida分析so也可以找到脫殼點。了解殼的根本原理以及虛擬機執行的原理才是以不變應萬變的法則,剩下的就靠創造力去發揮即可。
解答:
1.先將題目提供的鏡像刷寫至Pixel手機中,然后將apk安裝上去,發現apk以32位模式運行,那么使用adb pull將/system/lib/libart.so文件拉出來放到ida中分析。
2.鏡像已經帶有了整體脫殼功能,其/data/data下面的8299712_dexfile.dex就是MainActivity類所在的dex文件,可以看到里邊很多的函數都是抽取型函數,我這里選擇分析其中一個函數:com.xgtl.aggregate.activities.MainActivity.a(android.view.View)。
3.在ida的exports中尋找解釋器的實現函數ExecuteSwitchImpl(可以結合有源碼的系統來對比,廠商一般不會改動原生虛擬機),找到取指階段的指令地址,通過查看aosp源碼得知該函數為模板函數,實際編譯器生成的函數是四個,它們的地址分別位于0x2089B6,0x021FA66,0x01FA2E0,0x0215028,接下來嘗試著用frida進行inline hook,總是跑著跑著就崩潰了,不是報SIGSEGV就是報TRAP,試了各種方法,還是無法解決崩潰的問題,只好換其他的方式來解決。
4.通過在源碼中搜索,每一條解釋器指令執行前都會調用PREAMBLE(),因此如果可以hook住PREAMBLE也可以跟蹤每條指令的執行分支。通過觀察可以發現PREAMBLE中會調用shadow_frame.GetThisObject(),因此hook shadow_frame.GetThisObject就可以跟蹤指令的執行,但是必須有個前提:if (UNLIKELY(instrumentation->HasDexPcListeners())) {這個分支必須滿足才會執行下面的shadow_frame.GetThisObject()。
5.通過跟蹤ida中指令發現執行ExecuteSwitchImpl的時候instrumentation對象的地址存放在r4寄存器中,而HasDexPcListeners()函數被內聯了,它是通過r4+480偏移來判斷if (UNLIKELY(instrumentation->HasDexPcListeners()),因此只要可以通過frida來修改r4寄存器+480的值,使得instrumentation->HasDexPcListeners()返回為true,這樣就可以使shadow_frame.GetThisObject()被調用,然后再hook這個函數即可。
frida代碼如下:
var prettyMethodPtr;var prettyMethod; function pretty_method(art_method) { if (!prettyMethodPtr) { prettyMethodPtr = Module.getExportByName("libart.so", "_ZN3art9ArtMethod12PrettyMethodEb"); prettyMethod = new NativeFunction(prettyMethodPtr, 'pointer', ['pointer', 'pointer', 'bool']); } var result = Memory.alloc(0x100); prettyMethod(ptr(result), art_method, 1); return result.add(0x8).readPointer().readCString();} var in_method = false;var target_method_name = "com.xgtl.aggregate.activities.MainActivity.a(android.view.View)"; function hookGetThisObject(base) { var get_this_object_addr = Module.getExportByName("libart.so", "_ZNK3art11ShadowFrame13GetThisObjectEt"); Interceptor.attach(get_this_object_addr, { onEnter: function (args) { console.log("pid :" + Process.getCurrentThreadId() + ", lr:" + ptr(this.context.lr).sub(base).add(0xB000)); console.log(hexdump(ptr(this.context.r11), { length: 32 })); console.log() }, onLeave: function (retval) { } });} function hook() { var libartmodule = Process.getModuleByName("libart.so"); var base = libartmodule.base; libartmodule.enumerateSymbols().forEach(function (symbol) { if (symbol.name.indexOf("ExecuteSwitchImpl") != -1 && symbol.name.indexOf("CodeItem") != -1) { Interceptor.attach(symbol.address, { onEnter: function (args) { var instance = ptr(0x43898C).add(base).sub(0xB000).add(0xca9f4).add(480); instance.writeU8(1); var shadow_frame = args[3]; var art_method = ptr(shadow_frame).add(Process.pointerSize).readPointer(); this.method_name = pretty_method(art_method); if (this.method_name.indexOf(target_method_name) != -1) { console.log("method start : >>>>>>>>>>>" + this.method_name); hookGetThisObject(base, this.method_name); in_method = true; } if (in_method) { console.log("method start : >>>>>>>>>>>" + this.method_name); } }, onLeave: function (retval) { if (this.method_name.indexOf(target_method_name) != -1) { console.log("method end : >>>>>>>>>>>" + this.method_name); Interceptor.detachAll(); in_method = false; } } }); } });}function main() { hook();}setImmediate(main);
6.這里要跟蹤的函數為com.xgtl.aggregate.activities.MainActivity.a(android.view.View),需要app進入以后點擊界面下方的tab觸發。
運行腳本得到的輸出如下:
[AOSP on msm8996::com.xgtl.assistant]-> method start : >>>>>>>>>>>void com.xgtl.aggregate.activities.MainActivity.a(android.view.View)method start : >>>>>>>>>>>void com.xgtl.aggregate.activities.MainActivity.a(android.view.View)pid :16444, lr:0x206a57 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEFc4bce9f8 28 1b 00 00 00 00 00 00 00 00 00 00 00 00 00 00 (...............c4bcea08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ pid :16444, lr:0x203a79 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEFc4bcea2e 14 00 5f 18 00 00 71 10 4e df 00 00 28 e0 02 00 .._...q.N...(...c4bcea3e 02 00 02 00 00 00 00 00 00 00 09 00 00 00 12 01 ................ pid :16444, lr:0x203f87 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEFc4bcea34 71 10 4e df 00 00 28 e0 02 00 02 00 02 00 00 00 q.N...(.........c4bcea44 00 00 00 00 09 00 00 00 12 01 70 20 87 ca 10 00 ..........p .... pid :16444, lr:0x206a57 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEFc4bcea3a 28 e0 02 00 02 00 02 00 00 00 00 00 00 00 09 00 (...............c4bcea4a 00 00 12 01 70 20 87 ca 10 00 0c 01 71 20 50 ca ....p ......q P.
每條smali指令的執行都會打印出一條lr以及指令所在地址處的值。
接下來分析打印:
第一條lr為0x206a57,使用ida跳轉至此地址處,發現該地址處的switch-case對應的case為40,也就是0x28,與c4bce9f8 28 1b這條打印匹配,還原了指令以后就是goto 1b,因此第一條指令為goto 1b。接下來把所有的log按dex格式還原即可,遇到method id或者field id,則在8299712_dexfile.dex中查找對應的簽名:
method start : >>>>>>>>>>>void com.xgtl.aggregate.activities.MainActivity.a(android.view.View)lr:0x206a57 = goto 1blr:0x203a79 = const v0, 0x0000185flr:0x203f87 = invoke-static {v0},kind@df4e = invoke-static {v0},Ls/h/e/l/l/H;->i(I)Vlr:0x206a57 = goto e0 lr:0x2046e7 = iget-object v0,v2,field@8188 = Landroid/support/v4/media/MediaBrowserServiceCompat;->mSession:Landroid/support/v4/media/session/MediaSessionCompat$Token;lr:0x205ccf = const/4 vA, #+B = const/4 v1,#0lr:0x204c49 = invoke-virtual {v0, v1}, kind@61b5method start : >>>>>>>>>>>void android.support.v7.widget.AppCompatImageView.drawableStateChanged()lr:0x2064ad = invoke-super {v0}, kind@614clr:0x2046e7 = iget-object v0,v1,field@32aelr:0x20466d = if-eqz v0,0x0005lr:0x204c49 = invoke-virtual {v0},kind@4b5b = void android.support.v7.widget.AppCompatBackgroundHelper.applySupportBackgroundTint()method start : >>>>>>>>>>>void android.support.v7.widget.AppCompatBackgroundHelper.applySupportBackgroundTint()lr:0x2046e7 = iget-object v0,v3,field@3285lr:0x204c49 = invoke-virtual {v0},kind@5cd5lr:0x206c21 = move-result-object v0lr:0x20466d = if-eqz v0,0x0022lr:0x20c951 = return-voidlr:0x2046e7 = iget-object v0,v1,field@32aflr:0x20466d = if-eqz v0,0x0005lr:0x204c49 = invoke-virtual {v0},kind@4befmethod start : >>>>>>>>>>>void android.support.v7.widget.AppCompatImageHelper.applySupportImageTint()lr:0x2046e7 = iget-object v0,v3,field@32adlr:0x204c49 = invoke-virtual {v0},kind@6150lr:0x206c21 = move-result-object v0lr:0x20466d = if-eqz v0,0x0005lr:0x203f87 = invoke-static {v0}, kind@4e6emethod start : >>>>>>>>>>>void android.support.v7.widget.DrawableUtils.fixDrawable(android.graphics.drawable.Drawable)lr:0x203f09 = sget v0, field@0467lr:0x204023 = const/16 v1, 0x0015lr:0x204301 = if-ne v0,v1,0x0015lr:0x20c951 = return-voidlr:0x20466d = if-eqz v0,0x0022lr:0x206729 = invoke-direct {v3}, kind@4bf8method start : >>>>>>>>>>>boolean android.support.v7.widget.AppCompatImageHelper.shouldApplyFrameworkTintUsingColorFilter()lr:0x203f09 = sget v0, field@0467lr:0x205ccf = const/4 v1,#1lr:0x205ccf = const/4 v2,#0lr:0x204023 = const/16 v3, 0x0015lr:0x204b7b = if-le v0,v3,0x0009lr:0x2046e7 = iget-object v0,v4,field@32ablr:0x20466d = if-eqz v0,0x0003lr:0x205ccf = const/4 v1,#0lr:0x20c9f1 = return v1lr:0x2051d5 = move-result v1lr:0x20466d = if-eqz v1,0x0009lr:0x2046e7 = iget-object v1,v3,field@32aalr:0x20466d = if-eqz v1,0x000clr:0x2046e7 = iget-object v1,v3,field@32ablr:0x20466d = if-eqz v1,0x0003lr:0x20c951 = return-voidlr:0x20c951 = return-voidmethod start : >>>>>>>>>>>void android.support.v7.widget.AppCompatTextHelperV17.applyCompoundDrawablesTints()lr:0x2064ad = invoke-super {v3},kind@4cbbmethod start : >>>>>>>>>>>void android.support.v7.widget.AppCompatTextHelper.applyCompoundDrawablesTints()lr:0x2046e7 = iget-object v0,v3,field@32e5lr:0x206943 = if-nez v0,+0x000elr:0x2046e7 = iget-object v0,v3,field@32e7lr:0x206943 = if-nez v0,+0x000alr:0x2046e7 = iget-object v0,v3,field@32e6lr:0x206943 = if-nez v0,+0x0006lr:0x2046e7 = iget-object v0,v3,field@32e4lr:0x20466d = if-eqz v0,0x0028lr:0x20c951 = return-voidlr:0x2046e7 = iget-object v0,v3,field@32eclr:0x206943 = if-nez v0,+0x0006lr:0x2046e7 = iget-object v0,v3,field@32eblr:0x20466d = if-eqz v0,0x0018lr:0x20c951 = return-voidlr:0x2046e7 = iget-object v0,v2,field@818alr:0x204c49 = invoke-virtual {v0,v1},kind@61b5lr:0x2046e7 = iget-object v0,v2,field@818blr:0x204c49 = invoke-virtual {v0,v1},kind@61b5lr:0x2046e7 = iget-object v0,v2,field@8189lr:0x204c49 = invoke-virtual {v0,v1},kind@61b5lr:0x205ccf = const/4 v0,#1lr:0x204c49 = invoke-virtual {v3,v0},kind@5dabmethod start : >>>>>>>>>>>void android.support.v7.widget.AppCompatImageView.drawableStateChanged()lr:0x2064ad = invoke-super {v1},kind@614clr:0x2046e7 = iget-object v0,v1,field@32aelr:0x20466d = if-eqz v0,+0x0005lr:0x204c49 = invoke-virtual {v0},kind@4b5b = void android.support.v7.widget.AppCompatBackgroundHelper.applySupportBackgroundTint()lr:0x2046e7 = iget-object v0,v1,field@32aflr:0x20466d = if-eqz v0,+0x0005lr:0x204c49 = invoke-virtual {v0},kind@4befmethod start : >>>>>>>>>>>void android.support.v7.widget.AppCompatImageHelper.applySupportImageTint()lr:0x2046e7 = iget-object v0,v3,field@32adlr:0x204c49 = invoke-virtual {v0},kind@6150lr:0x206c21 = move-result-object v0lr:0x20466d = if-eqz v0,0x0005lr:0x203f87 = invoke-static {v0}, kind@4e6emethod start : >>>>>>>>>>>void android.support.v7.widget.DrawableUtils.fixDrawable(android.graphics.drawable.Drawable)lr:0x203f09 = sget v0, field@0467lr:0x204023 = const/16 v1, 0x0015lr:0x204301 = if-ne v0,v1,0x0015lr:0x20c951 = return-voidlr:0x20466d = if-eqz v0,0x0022lr:0x206729 = invoke-direct {v3}, kind@4bf8method start : >>>>>>>>>>>boolean android.support.v7.widget.AppCompatImageHelper.shouldApplyFrameworkTintUsingColorFilter()lr:0x203f09 = sget v0, field@0467lr:0x205ccf = const/4 v1,#1lr:0x205ccf = const/4 v2,#0lr:0x204023 = const/16 v3, 0x0015lr:0x204b7b = if-le v0,v3,0x0009lr:0x2046e7 = iget-object v0,v4,field@32ablr:0x20466d = if-eqz v0,0x0003lr:0x205ccf = const/4 v1,#0lr:0x20c9f1 = return v1lr:0x2051d5 = move-result v1lr:0x20466d = if-eqz v1,0x0009lr:0x2046e7 = iget-object v1,v3,field@32aalr:0x20466d = if-eqz v1,0x000clr:0x2046e7 = iget-object v1,v3,field@32ablr:0x20466d = if-eqz v1,0x0003lr:0x20c951 = return-voidlr:0x20c951 = return-voidmethod start : >>>>>>>>>>>void android.support.v7.widget.AppCompatTextHelperV17.applyCompoundDrawablesTints()lr:0x2064ad = invoke-super {v3},kind@4cbbmethod start : >>>>>>>>>>>void android.support.v7.widget.AppCompatTextHelper.applyCompoundDrawablesTints()lr:0x2046e7 = iget-object v0,v3,field@32e5lr:0x206943 = if-nez v0,+0x000elr:0x2046e7 = iget-object v0,v3,field@32e7lr:0x206943 = if-nez v0,+0x000alr:0x2046e7 = iget-object v0,v3,field@32e6lr:0x206943 = if-nez v0,+0x0006lr:0x2046e7 = iget-object v0,v3,field@32e4lr:0x20466d = if-eqz v0,0x0028lr:0x20c951 = return-voidlr:0x2046e7 = iget-object v0,v3,field@32eclr:0x206943 = if-nez v0,+0x0006lr:0x2046e7 = iget-object v0,v3,field@32eblr:0x20466d = if-eqz v0,0x0018lr:0x20c951 = return-voidlr:0x20c951 = return-voidmethod end : >>>>>>>>>>>void com.xgtl.aggregate.activities.MainActivity.a(android.view.View
由于方法里邊調用了其他方法如void android.support.v7.widget.AppCompatBackgroundHelper.applySupportBackgroundTint(),我這里打印出了執行的每條smali指令。
這里通過查看指令碼就可以大致看出程序的邏輯,如果還想更進一步,就需要分隔各個方法,寫到dex文件中去,用jadx這種工具來打開即可。
由于附件超出了8M,我用split命令將原始文件分割了開了,合并起來:
cat test.apk.split1 test.apk.split2 test.apk.split3 > test.apk