某某某加固系統內核so dump和修復:
某某某加固系統采取了內外兩層native代碼模式,外層主要為了保護內層核心代碼,從分析來看外層模塊主要用來反調試,釋放內層模塊,維護內存模塊的某些運行環境達到防止分離內外模塊,另外由于內層模塊不是通過系統加載的,所以實現了自主的ELF加載,這樣就實現內層模塊的加密處理。這些實現后就可以依賴內層模塊保護dex。從而達到系統保護目的。
對于某某某加固系統的反調試網上已經有很多資料了,就不再作為重點介紹了。下面主要介紹對內層模塊的加載,加密,保護。這些外面資料不多的內容。
外層解密內層核心模塊的解密算法是使用zlib庫的uncompress函數實現的,不過解密函數解密出來的并不是整個的模塊,而是被加密了或者說被移除了四個部分的模塊,包含:
program_header_table、.rel.dyn、.rel.plt、Dynamic Segment 。由于是自己的加載系統加載,所以這些被move的部分依賴父模塊組裝,防止內存直接dump出解密的內層模塊。
下面給出frida?dump這些數據的腳本,并加以說明: Java.perform(function?()?{var?i?=?0;var?phadd?=?0;var?jmprel?=?0;var?rel?=?0;var?dynadd?=?0;var?buff?=?0;console.log("begin")var?fileclass?=?Java.use("java.io.File");var?mysavePath?=?"/data/data/"?+?pkg_name?+?"/myso";var?pathDir?=?fileclass.$new(mysavePath);if?(!pathDir.exists())?{pathDir.mkdirs();}console.log("mysavepath:"+pathDir)Interceptor.attach(Module.getExportByName('libz.so',?'uncompress'),?{onEnter:?function?(args)?{if?(i?==?0)?{if?(args[2]?!=?null)?{var?memcpy_add?=?Module.findExportByName("libc.so",?"memcpy");console.log("memcpy:"?+?memcpy_add);Interceptor.attach(memcpy_add,?{onEnter:?function?(args)?{console.log("begin:memcpy,len:"?+?args[2]);console.log(hexdump(args[1]));if?(args[2]?==?0x100)?{//?program_header_table?的大小一般是固定的phadd?=?args[0];console.log(hexdump(args[1]))}if?(args[2]?==?0x948)?{//.rel.plt?數據;jmprel?=?args[0];console.log(hexdump(args[1]))}if?(args[2]?==?0x4a58)?{//.rel.dyn?數據rel?=?args[0];console.log(hexdump(args[1]))}if?(args[2]?==?0xd8)?{//Dynamic?Segment??數據也是一般固定長度dynadd?=?args[0];console.log(hexdump(args[1]))}if?(args[2]?==?0xbbff4)?{//這個就是某某某加固的加載,加載基地址就是copy到的空間地址,這個數據是經過解密的移除上面部分的模塊數據;console.log(hexdump(args[1]));????????????????????????????????//這個hexdump中可以看到ELF頭,所以是個標記,上面順序就可以確定了。var?new_so_base?=?args[0];console.log("newso_base:"?+?args[0])}if?(args[2]?==?0x38fc)?{//到這里上面得到被move的四個部分數據也已經解密出來了,在這里就可以開始dump了console.log(args[0])var?file_path?=?pathDir?+?"/mydump_"?+phadd+?".dat";console.log("----save?begin1-----");var?file_handle?=?new?File(file_path,?"wb");console.log("----save?begin2-----");if?(file_handle)?{console.log("----save?begin3-----");Memory.protect(ptr(phadd),?0x100,?'rwx');var?libso_buffer?=?ptr(phadd).readByteArray(0x100);file_handle.write(libso_buffer);file_handle.flush();file_handle.close();console.log("[dump]:"+?file_path);}var?file_path?=?pathDir?+?"/mydump_"?+jmprel+?".dat";console.log("----save?begin1-----");var?file_handle?=?new?File(file_path,?"wb");console.log("----save?begin2-----");if?(file_handle)?{console.log("----save?begin3-----");Memory.protect(ptr(jmprel),?0x948,?'rwx');var?libso_buffer?=?ptr(jmprel).readByteArray(0x948);file_handle.write(libso_buffer);file_handle.flush();file_handle.close();console.log("[dump]:"+?file_path);}var?file_path?=?pathDir?+?"/mydump_"?+rel+?".dat";console.log("----save?begin1-----");var?file_handle?=?new?File(file_path,?"wb");console.log("----save?begin2-----");if?(file_handle)?{console.log("----save?begin3-----");Memory.protect(ptr(rel),?0x4a58,?'rwx');var?libso_buffer?=?ptr(rel).readByteArray(0x4a58);file_handle.write(libso_buffer);file_handle.flush();file_handle.close();console.log("[dump]:"+?file_path);}var?file_path?=?pathDir?+?"/mydump_"?+dynadd+?".dat";console.log("----save?begin1-----");var?file_handle?=?new?File(file_path,?"wb");console.log("----save?begin2-----");if?(file_handle)?{console.log("----save?begin3-----");Memory.protect(ptr(dynadd),?0xd8,?'rwx');var?libso_buffer?=?ptr(dynadd).readByteArray(0xd8);file_handle.write(libso_buffer);file_handle.flush();file_handle.close();console.log("[dump]:"+?file_path);}}if?(args[2]?==?0xb4)?{//console.log(JSON.stringify(this.context))console.log(args[0])var?file_path?=?pathDir?+?"/mydump_"?+?buff+".so";console.log("----save?begin1-----");var?file_handle?=?new?File(file_path,?"wb");console.log("----save?begin2-----");if?(file_handle)?{console.log("----save?begin3-----");Memory.protect(ptr(buff),?0xc0000,?'rwx');var?libso_buffer?=?ptr(buff).readByteArray(0xc0000);file_handle.write(libso_buffer);file_handle.flush();file_handle.close();console.log("[dump]:"+?file_path);}}}},onLeave:?function?(retval)?{//?simply?replace?the?value?to?be?returned?with?0}})})
上面代碼只是說明功能,扣出來的,需要自己整理下可能才能執行。
腳本的原理就是根據某某某加固流程,父模塊使用uncompress解壓后會把解壓出來的被偷走的數據重新解密到新的內存地址,在memcpy時得到內存地址和長度,然后等解密出來后dump數據。
另外是根據數據的大小取相關數據的,每個APP可能會不同,需要先跑下看看。
需要說明下,首先跑下hook uncompress后的memcpy hexdump,memcpy加載的新地址數據出現ELF頭數據的,表明加載了。然后向上逆推其他數據,這樣就能確定每個的數據大小,然后更改腳本,獲取數據,并dump下來。
比如本例:
begin:memcpy,len:0xbbff4
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
c9085589 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 .ELF............
c9085599 03 00 28 00 01 00 00 00 00 00 00 00 34 00 00 00 ..(.........4...
c90855a9 bc fa 0b 00 00 00 00 05 34 00 20 00 08 00 28 00 ........4. ...(.
c90855b9 19 00 18 00 11 a6 dd 35 da cf 22 1a 71 b7 8b 08 .......5..".q...
由于父模塊需要內存偏移修正,所以完整的模塊需要在后面的一次才能dump。
拿到了需要的so模塊數據,我們需要修復這個so模塊,否則ida無法分析,用010Editor也會打開失敗。下面進入so修復:
使用的工具有010Editor和ELFfix
ELFfix?修復原理具體請參考:
https://bbs.pediy.com/thread-192874.htm
用010Editor打開dump的so模塊,會提示錯誤。
我們已我們已經知道被移除了填充雜亂數據的幾個關鍵的部分。所以肯定不能正常加載。當然修復是需要理解下ELF的文件格式和內存加載原理。這樣更便于理解為啥需要這樣步驟修復。這個各位自己學習學習了。
首先我們需要還原program_header_table ,這個是系統解析加載的關鍵數據,用010Editor打開我們剛才dump出來的0x100字節的那個數據
復制并覆蓋到dumpso的program_header_table里面,
?
好了,到這里我們完成ELFfix需要的關鍵數據,保存這個文件,然后可以使用ELFfix工具進行修復了
復制到ELFfix目錄下,并執行相關的修復命令會得到修復后的文件:
?
dump_new_full.so??修復后的so
再次用010Editor打開這個修復后的so,還是會提示錯誤。不過打開后section_header_table已經正確了,原來的section_header_table是錯誤的:
來說下為啥先要修復program_header_table 節和這個段里面的Dynamic Segment,根據ELFfix作者文章里面說明,修復是依賴這兩個數據進行解析得到ELF section,所以必須先還原這兩個數據塊。
下面開始還原rel數據,一個是jmprel,一個是rel
我們打開這兩個節:
?
到這里內層的so模塊被修復還原出來了,IDA加載完全沒問題了,得到這個模塊我們就可以用IDA分析調試了。當然也可以脫殼使用了。
注意:目前這個ELFfix不支持64位程序修復。
好了,有了這個就可以詳細的分析某某某內層模塊的功能,當然也可以patch代碼等操作了。
下面來看看某某某加固系統如何來保護內層so功能模塊的。比較有特色
Java層偷Native層代碼
?????????某某某加固為了保護內層so不能脫離外層so環境,做了一些防范措施,這種也是,通過移除關鍵部位的二進制代碼,如果脫離了整個環境,那么就會執行錯誤,被移除的二進制代碼丟失。
1 2 3 4 |
|
內層在Java層使用getByte這個函數獲取被偷走的二進制代碼數據,填充回so的執行中。
被偷的數據:
>dump 0x10004b1
010004B1: 00 00 00 00 00 00 00 00? BD 10 B5 4F F6 75 74 E8? ...........O.ut.
010004C1: BF 10 BD 10 B5 4F F6 76? 74 E8 BF 10 BD 10 B5 4F? .....O.vt......O
?
來看看這個是啥函數,原來是:JNIEnv->CallStaticObjectMethodV
這種Java層偷函數二進制執行代碼的方式還是比較新穎的。
關鍵參數放父模塊,通過導出函數調用
一般模塊的導出函數都是給其他模式引用的接口,某某某這個導出函數卻是調用父模塊的接口,因為子模塊的加載完全是父模塊負責的,所以這個接口的填充是父模塊做的,所以如果脫離了父模塊,這個函數就變成了空的。而且這個函數還是個特別的核心函數,來看看:
獲取key,這個函數是獲取解密算法rc4的key的,如果沒有這個解密key,后面的dex解密都會失敗。所以必須到父模塊中執行。
通過查詢有兩個地方調用:
這個就是某某某自己實現的類似亂序處理過的多功能集成函數。
來看看key計算的時候參數是啥:
======================= Registers =======================
? R0=0x20917ec? R1=0x350? R2=0x2024006? R3=0x2024018
R4=0x2024000? R5=0x2024006? R6=0x0? R7=0xcbcca6e8? R8=0x350
R9=0x350? R10=0x2024018? R11=0x202c350? R12=0x80000000? SP=0x7ffad8
LR=0xcbda5859? PC=0xcbdb08fc
======================= Disassembly =====================
0xcbdb08fc:??? blx?? r7
020917EC: 22 39 52 52 54 52 52 52? 42 52 52 52 13 02 02 19? "9RRTRRRBRRR....
020917FC: 17 0B 60 30 60 31 6A 61? 66 67 33 6A 65 61 64 6A? ..`0`1jafg3jeadj
0209180C: 62 34 22 39 52 52 5A 52? 52 52 53 52 52 52 01 36? b4"9RRZRRRSRRR.6
0209181C: 39 17 3C 26 20 2B 63 22? 39 52 52 5E 52 52 52 4E? 9.<& +c"9RR^RRRN
0209182C: 52 52 52 33 31 26 3B 24? 3B 26 2B 1C 33 3F 37 31? RRR31&;$;&+.3?71
0209183C: 3D 3F 7C 33 3B 35 28 7C? 27 3B 7C 30 33 21 37 7C? =?|3;5(|';|03!7|
執行下這個函數,得到的key:
>dump 0x2024006
02024006: 67 5E 7F 35 70 37 78 2E? 7D 22 75 27 08 56 4A A1? g^.5p7x.}"u'.VJ.
上面的參數哪里來的呢?
分析發現原來是從原始包里面用libz解壓出來的:
Executing syscall openat(ffffff9c, 02029000, 00020000, 00000000) at 0xcbc28be4
path:/data/app/xxxxxxxx-1/base.apk
這個解出來就是原始包里面的classes.dex部分:
?
另外一個地方的調用參數如下:
======================= Registers =======================
? R0=0x7ffbf9? R1=0x0? R2=0x2024040? R3=0x7ffb10
R4=0xcbc761c8? R5=0x7ffbf8? R6=0x202b054? R7=0xcbde56df? R8=0xcbc761c8
R9=0x202c34c? R10=0x0? R11=0x2024000? R12=0x2024040? SP=0x7ffb08
LR=0xcbcca6e8? PC=0xcbdb0982
======================= Disassembly =====================
0xcbdb0982:?? blx? r2
>dump 0x7ffbf9
007FFBF9: 00 DA CB 54 B0 02 02 DF? 56 DE CB C8 61 C7 CB 00? ...T....V...a...
007FFC09: 00 00 00 2D 00 00 00 00? 60 02 02 00 70 02 02 F0? ...-....`...p...
這是個校驗調用,如果脫離父模塊就會死在這個調用里面,堆棧會被破壞掉。
當然還有其他類似的這樣調用父模塊校驗:
DEX文件保護
某某某加固系統最重要的功能應該就是為了對dex文件的保護了,因為一般得到原始的dex后,去掉加固模塊就可以直接運行。相當于完整脫殼了。為了達到這個目的,某某某加固系統對dex的保護特別重要。前面的這些對模塊的保護最終的目的也是為了保護dex文件。下面來看看他的保護是怎么樣的。
某某某加固系統第一次會把原始包中的classes.dex用libz函數解壓出來,然后系統會把他重新編譯成oat文件,這個文件中的dex頭被加密處理了。里面的數據部分也被加密處理。在運行的時候,加固系統直接解內存中加載的classes.oat文件,而不用再次解壓原始文件了。這樣帶來了效率的提升。也保證了沒有明文的dex存在磁盤上。
數據的解密用到了rc4算法,算法的key剛才已經說過了是從父模塊的函數中獲取的,保證了解密的安全性。
rc4算法部分大家可以網上找資料看看,用key生成0x100的解密盒子。然后用這個盒子去解密數據。
nt __fastcall rc4(int result, _BYTE *a2, int a3)
{
? int v3; // lr
? int v4; // r12
? int v5; // r5
? if ( a3 )
? {
??? v3 = 0;
??? v4 = 0;
??? do
??? {
????? --a3;
????? v3 = (v3 + 1) % 256;
????? v5 = *(unsigned __int8 *)(result + v3);
????? v4 = (v4 + v5) % 256;
????? *(_BYTE *)(result + v3) = *(_BYTE *)(result + v4);
????? *(_BYTE *)(result + v4) = v5;
????? *a2++ ^= *(_BYTE *)(result + (unsigned __int8)(*(_BYTE *)(result + v3) + v5));
??? }
??? while ( a3 );
? }
? return result;
}
算法核心部分。
下面是跟蹤的截圖和注釋:這個循環保證解密oat中的所有dex文件:
?
所有的oat文件中的dex都被解密出來后,需要立即保存下來,否則某某某加固系統會把dex的頭再次刪除,我們在這個地方先dump下來oat文件。
?
Dump這個已經解密的oat文件后,我們就可以把其中的dex文件都找出來:
根據oat文件結構知道,dex數據是從0x1000開始的,前面是頭,后面接著就是dex文件,從我們剛才跟蹤的時候知道,這個oat中有三個dex文件。向下找下看看,也可以根據剛才R5中的偏移找到開始的位置,第一個位置偏移是0x1808,這個里面的dex頭保存完整:
從dex結構我們知道,開始的偏移+0x20 后面的4個字節就是長度,第一個長度為:0x60E694,把這個數據保存出來,然后繼續這樣找其他的dex:
全部找到后我們用解析工具打開看看:?
這樣原始的dex就被我們抓出來了。
Dump dex原理:
???????某某某加固通過hook系統的LoadDexFile 在系統加載dex文件之前把修改部分的字節還原后交給系統使用。所以在這個時候獲取的dex文件是正確的。
某某某的VMP保護:
?????? 某某某加固系統的核心保護就是對dex代碼中的onCreate函數進行VMP保護。把原指令用自己實現的指令集替換,并在Native層實現這些指令,達到替換指令集,移除dex代碼,加密指令數據等。并且在vmp的代碼中還可以插入自己的檢測代碼。非常有特色,保護能力也非常強,下面就來仔細的研究下某某某vmp的實現原理和具體實現方法。
來看這個app的例子:
通過frida hook RegisterNatives腳本,獲取加固系統注冊Natives方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
并獲取參數打印如下:
這個APP有兩個地方被某某某加固使用VMP保護:
[RegisterNatives] java_class: com/aigz/ui/base/BBSAppStart name: onCreate sig: (Landroid/os/Bundle;)V fnPtr: 0xd32fb39c ?
[RegisterNatives] java_class: com/aigz/ui/base/BBSAppStart name: onCreate sig: (Landroid/os/Bundle;)V fnPtr: 0xe857c39c
來到這兩個函數的地方看看:
Method表頭中被定義為Native函數:?
通過frida的跟蹤分析,某某某加固系統還隱藏了真實的vmp處理函數地址,真正的函數是在入口函數中通過地址跳轉過去的,可以通過內嵌的capstone引擎反匯編出來,得到真實的函數入口地址:
0xe857c39c : mov r8, r8
0xe857c39e : sub sp, #0xc
0xe857c3a0 : push {r7}
0xe857c3a2 : push {lr}
0xe857c3a4 : sub sp, #4
0xe857c3a6 : mov ip, r0
0xe857c3a8 : add r0, sp, #0xc
0xe857c3aa : stm r0!, {r1, r2, r3}
0xe857c3ac : add r2, sp, #0xc
0xe857c3ae : mov r0, pc
0xe857c3b0 : mov r1, ip
0xe857c3b2 : str r2, [sp]
onCreate_function_address: 0xd2858985 offset: 0x1c985
這樣就定位到真實的函數偏移是0x1c985
通過比較長時間的分析,大概的了解了這個函數的參數如下:
R0:加密函數的key,這個key決定了后面的jump table的跳轉,也決定了后面使用的參數的計算和資源的獲取。也就是說某某某的VMP引擎是同一個,就是通過這個key識別處理不同的函數。
R1:jni_env
R2: 堆棧參數
Vmp函數的大概結構是,通過R0 key值獲取需要處理的函數資源,通過函數的參數值生成后面解密的xor_key,這個key還被一個參數值保護,失去這個參數值就會出現錯誤。另外這個key還被inker:__dl_rtld_db_dlactivity 值處理,如果被調試,inker:__dl_rtld_db_dlactivity值不為0,這個參數也是錯誤的。
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
下面詳細的解釋下這個處理函數的過程:
?
.text&.ARM.extab:CC0B042A???????????????? LDR.W?????????? R0, [R9]
.text&.ARM.extab:CC0B042E???????????????? LDRB.W????????? R1, [R9,#4]
.text&.ARM.extab:CC0B0432???????????????? SUBS??????????? R2, R0, R6
.text&.ARM.extab:CC0B0434 ????????????????LDRH??????????? R0, [R0]
.text&.ARM.extab:CC0B0436???????????????? ORR.W?????????? R1, R1, R1,LSL#8
.text&.ARM.extab:CC0B043A???????????????? ASRS??????????? R4, R2, #1
.text&.ARM.extab:CC0B043C???????????????? EOR.W?????????? R8, R1, R0
.text&.ARM.extab:CC0B0440???????????????? MOV???????????? R0, R9
.text&.ARM.extab:CC0B0442???????????????? UXTH.W????????? R11, R8
.text&.ARM.extab:CC0B0446???????????????? MOV???????????? R1, R11
上面這段就是整個處理的核心部分,其中R9中是參數:
本次VMP運行參數如下:
02029000: EC 9B E5 D2 95 00 00 00? 00 52 C6 E5 50 3B 00 00? .........R..P;..
這個參數的的第一個dword 是被加密函數的加密數據指針,第五個字節是xor_key,被加密數據需要通過xor 這個xor_key來解密。第三個dword是第二參數指針,第二參數包含解密查詢指針的低位值和查詢表地址。
.text&.ARM.extab:CC0B0448? ?BL ????sub_CC0BDEFE? //模擬opcode查詢算法
.text&.ARM.extab:CC0B044C ??MOV?? ?R10, R0?????? //返回值就是opcode_v
.text&.ARM.extab:CC0B044E?? CMP???? R0, #0x7E ; '~'? //后面就是根據這個返回值處理
實現代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
也就是說先把加密函數的兩個字節取出來,然后xor xor_key 然后用解密后的前兩個字節進入查詢函數進行查詢計算。得到查詢的index,根據index獲取模擬的opcode值。
查詢表格每個加密函數都有一個屬于自己的表:
在上面的查詢過程中會使用inker:__dl_rtld_db_dlactivity 值來破壞查詢值,所以必須清零。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
|
從這個運行日志中可以看到,某某某加固有兩種處理,一種就是對Java class的處理,還有一種就是數值處理。
對Java class的處理日志能明確的打印出結果,根據結果基本上能知曉函數的作用。而對數值的處理比較麻煩,某某某加固的vmp是自己模擬實現的,需要跟蹤調試反分析出來函數的作用。
另外opcode的長度是某某某加固vmp自己維護的。也就是根據opcode硬編碼的,所以需要記錄下來。
現在來總結下某某某加固vmp的處理方法:
1.?????用需要加密的onCreate函數的參數+常量參數+anti參數生成的xor_key加密函數數據。
2.?????每個opcode的第一個字節被某某某加固vmp的模擬opcode所替換,并被上面的xor_key加密保護。
3.?????每個opcode類型自己實現功能。
4.?????每個opcode長度在實現代碼中自己維護。硬編碼實現。
5.?????每個函數生成高地位的參數加密,并和密文的opcode 計算獲得查詢index。
6.?????每個函數都擁有一張模擬opcode_v查詢表。模擬的opcode是通過這個表查詢得到的。
也就是說模擬的opcode是兩層加密解密出來的。并且都是自己實現具體功能的。
某某某加固VMP代碼還原:
首先要想還原得有一個官方的opcode文檔,推薦:
Dalvik opcodes
?
從官方的文檔中大致能知道opcode類型和opcode長度定義。
從文檔中我們也知道dex的代碼分為opcode操作碼和后面的操作數組成的。
Opcode 占一個字節。后面都是操作數。
而某某某加固的VMP簡單的加密了整個代碼,就是用xor_key 加密,而重點的保護的就是opcode操作碼,還原代碼其實也就是找回opcode操作碼,替換回去就行。后面的操作數通過xor_key還原出來就可以了。
下面就來重點說說怎么還原操作碼的:
從上面的分析來看就是兩種類型,處理Java class 的通過打印的日志基本就清楚是啥功能的。然后通過正常的代碼結合官方文檔大致就能還原出來。
而對數據的處理比較麻煩,因為都是某某某加固系統自己處理的,所以需要到每個處理單元里面跟蹤分析。
當然最好是在AndroidNativeEm模擬器中運行,這樣能方便跟蹤,如果你能用ida動態跟蹤也可以。
下面是一個demo加固后的跟蹤結果和分析結果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
|
通過上面的日志分析基本上還原出來代碼。
opcode_len_dat = [3,3,3,3,3,1,2,3,1,3,1]
xor_key = 0x9595
opcode_操作碼: ['0x7a', '0x12', '0x1', '0x12', '0x1', '0x21', '0xa6', '0x1', '0x21', '0x1', '0xc5']
使用的opcode種類: ['0x7a', '0x12', '0x1', '0x21', '0xa6', '0xc5'] 合計個數: 6
恢復的代碼: 6f20e70d210014021c000a7f6e20513b210014027e00077f6e204f3b21000c021c0200026e10523b01000c006e20fe0b02000e00
用這個還原的代碼修復dex文件就可以實現剝離某某某加固,還原原程序了。
Dex 二進制修改方法:
用IDA打開dex文件,搜索class名稱,找到要修改的方法的地方:
?
看到Native函數定義的數據了吧。比較下正常的public函數定義,知道類型應該是0004,而不是0284,后面跟著的是code地址,修改這個數據:
直接到IDA的Hex View頁中按F2修改:
?
下面修改code offset,這個地址是LEB128編碼,編碼介紹:
https://berryjam.github.io/2019/09/LEB128(Little-Endian-Base-128)格式介紹/
Andorid系統在Dex文件采用LEB128變長編碼格式,相對固定長度的編碼格式,leb128編碼存儲利用率比較高,能讓Dex文件盡可能的小。對于存儲空間比較緊缺的移動設備,這非常有用。就是在字節的第七位插入1,計算地址時,去掉這個第七位的1再組合。
來的偏移地址:0x1b7478處,從IDA中也可以看到前面0x10是函數頭參數,后面就是代碼了:
?
保存,用IDA重新打開:
除了偏移地址發生改變,基本上一樣。說明還原成功。
某某某加固修正重新打包技術要點如下:
1. 首先需要使用自己編寫的某某某_jiagu_info 腳本跑出oat文件的dex,dump出來oat文件后用winhex把dex頭前面的數據去掉就是dex文件。
2.一般dump 多個dex文件,需要處理所有的oat文件得到所有的dex
3.一般被加固的apk的原始入口在Manifest xml中已經改變,需要找回原來的入口,這個尤其重要,這個坑踩了好久。這個入口中腳本中會打印出來,App_Application_Entry is:
4.由于Java vm并不能運行nop指令,一般也不可能出現這個指令,所以清除某某某加固的SDK時,如果不能進行code對齊,中間有nop 時Java vm會執行錯誤。所以建議保留SDK,但是修改SDK代碼為返回原值就行。
某某某加固一般有三種SDK插入:
?主onCreate函數的 stub->Mark()? 這個少,一般只有一個,反編譯后直接在smile中可以去掉,搜索dex中的mark就能找到地方。
?onCreate 的interface11()???? 這個由于是在函數的開頭插入,所以可以寫代碼清除并進行code對齊。 需要寫代碼修復。先用winhex通過特征碼搜索所有的調用地址,然后導出這個搜索結果給下面修復腳本用就能修復好。
腳本見附件:某某某加固dex嵌入SDK修復腳本。
?invoke-static/range StubApp->getOrigApplicationContext(Context)Context, v0 .. v0?? 這種數量巨大,一般都是在調用getcontext函數的后面,所以建議保留,請修改StubApp里面的函數代碼,直接返回原值即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
5. 用dextools工具dump的dex存在問題,需要用自己寫的某某某_info腳本跑出來的oat修復的dex才可以。
把上面修改后的classes文件和dump的所有其他dex文件一起放到apk包中,用apktool工具反編譯后修改mark的地方,就是去掉就行。然后修改Manifest xml中application 選項中的android:name 為 Python 跑出來的App_Application_Entry
6. 如果存在onCreate VMP 那就復雜,需要修復vmp后才能進行上面的反編譯修改再打包。vmp的修復最好是用AndroidNativeEmu 模擬器修復。修復過程在某某某加固分析文檔中有。參數和數據需要自己到內存中去抓。
附錄1 ?獲取某某某加固信息腳本:
本腳本用來獲取某某某加固系統關鍵數據,包含dump so,查詢apk的原始入口,根據特征碼搜索onCreate vmp處理函數等。
?