這次同樣以T廠的x固加殼為例:為了方便理解,減少不必要的干擾,這里只寫了一個簡單的apk,在界面靜態展示一些字符串,如下:

用x固加殼后,用jadx打開后,先看看AndroidMainfest這個全apk的配置文件:入口是“MyWrapperProxyApplication”;
<application android:theme="@style/AppTheme" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:name="MyWrapperProxyApplication" android:allowBackup="true" android:supportsRtl="true" android:roundIcon="@mipmap/ic_launcher_round" android:appComponentFactory="androidx.core.app.CoreComponentFactory"> <activity android:name="com.example.test.MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application>
進入這個類查看: 先執行attachBaseContext,得到一個context,然后修復名稱,最后初始化proxyApplication;然后執行onCreate,在里面調用了一個名為Ooo0ooO0oO的native方法,這里明顯有問題:正常的開發人員會這樣取名字?
public abstract class WrapperProxyApplication extends Application { static Context baseContext = null; static String className = "android.app.Application"; static ClassLoader mLoader = null; static Application shellApp = null; static String tinkerApp = "tinker not support"; /* access modifiers changed from: package-private */ public native void Ooo0ooO0oO(); /* access modifiers changed from: protected */ public abstract void initProxyApplication(Context context); static Context getWrapperProxyAppBaseContext() { return baseContext; } private synchronized boolean Fixappname() { if (className.startsWith(".")) { className = super.getPackageName() + className; } else if (className.indexOf(".") < 0) { className = super.getPackageName() + "." + className; } return true; } public static void fixAndroid(Context context, Application application) { if (Build.VERSION.SDK_INT == 28) { try { mLoader = AndroidNClassLoader.inject(context.getClassLoader(), application); } catch (Throwable th) { th.printStackTrace(); } } } private static String getVersionCode(Context context) { try { return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName; } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); return "0"; } } /* access modifiers changed from: protected */ public void attachBaseContext(Context context) { super.attachBaseContext(context); baseContext = getBaseContext(); if (shellApp == null) { shellApp = this; } Fixappname(); initProxyApplication(context); } public void onCreate() { super.onCreate(); Ooo0ooO0oO(); } }
進入iniProxyApplication函數:這里先是打開一個文件,如果文件打開失敗直接退出(換句話說文件打開失敗的后果很嚴重,直接沒法運行程序了)!最后加載so庫;從整個java代碼執行的過程看,解密dex大概率就是從加載這個so開始的了!
public void initProxyApplication(Context context) { ZipFile zipFile; try { zipFile = new ZipFile(context.getApplicationInfo().sourceDir); } catch (IOException e) { e.printStackTrace(); zipFile = null; } if (zipFile == null) { Process.killProcess(Process.myPid()); System.exit(0); } Util.PrepareSecurefiles(context, zipFile); try { zipFile.close(); } catch (IOException e2) { e2.printStackTrace(); } if (Util.CPUABI == "x86") { System.load(context.getFilesDir().getAbsolutePath() + "/prodexdir/" + Util.libname); return; } System.loadLibrary(Util.libname); }
libs目錄下面有3個so,很明顯最后一個是解密dex的so, 因為第二個只有1K,哪有這么小的so文件!

用IDA打開,先看看segment的情況:貌似比較正常;

在export這里居然找到了jni_onload函數,用graph view查看,發現幾百個分支,正常人有這樣寫代碼的嘛? 明顯是控制流平坦化了(塊之間的分支)

jni_onload參數是V3,根據V3的值走不同的分支:V3的值只有1個,所以只能走1條分支,其他分支都是干擾靜態分析的:

為了便於靜態跟蹤,先把參數改過來:



先把參數a3改成JNIEnv* 試試了,結果發現不對:

把a3改成char* 試試,因為通過觀察發現,a3(也就是v5)好多次被當成基址,然后加上某個偏移賦值,並且不同偏移的數據類型還不同,如下:

這里大膽猜測:這有可能是個結構體;除此外,再也找不到vm被使用了;接下來怎么繼續分析了? 這里有大量的加密字符串,並在init_array看到了很多異或的解密操作,很有可能是在init_array解密的,所以下一步可以嘗試從內存dump這個so,看看這些字符串到底是啥!

運行起來后查pid:9165

把進程在內存的數據全部dump出來:

dump出來的so看看segment:很正常

最重要的是:字符串都解密了成明文的了!


接下來就好分析多了:這里打開一個文件,直觀感覺要開始解密文件了!

這個文件剛好在asset目錄下,貌似被加密了,而且很小,應該不是重要的文件;

同在asset目錄,另一個文件就很大了,有906K,試試這個了:

文件頭被抹掉了,從文件大小看,像是被加密的dex了。現在靜態分析階段,暫時無法解密,找找其他的突破口;

上面分析了V5有可能是結構體,紅框框這個函數是第一個使用V5的,進去看看了:

這里根據libdvm、libart這些關鍵的字眼,都能猜到是在獲取虛擬機的版本:把版本信息存放在字符串604偏移的地方;

為什么要找這個了?art和dvm是兩種不同的dex文件加載方式,所以必須要先確定虛擬機類型,才會對dex進一步做操作(所以這兩個分支肯定是成雙成對出現的,缺一不可)!所以解密dex的操作可以直接從這里開始分析了,減少了很多需要分析的代碼!整個代碼用到604偏移的只有3個地方,根據取值不同走不同的分支。我用的的是4.4版本(低版本防護功能弱,利於逆向分析),很顯然用的是dvm,所以選擇下面這個分支繼續:

進入每個函數挨個分析,根據字符串、參數個數等特征,大概猜了一下這些函數和變量的作用,標記到下面了:核心就是找openDexFileNative和openDexFile;

接下來就是關鍵的代碼了:decryptDex(名字是我自己改的),里面有很多calloc函數分配內存,一看就知道要加載dex解密了(三代殼涉及到dex映射、修復和還原);

為了便於理解:這里改個名;這個變量被應用了很多次,每次都是加上一個偏移,就得到函數。然后傳入參數就能使用了,疑似JNIEnv* 變量,這里先改成試試:

這里改變量類型失敗,重新把jni.h導入,然后再改類型,這現在看起來舒服多了:

靜態分析到這里基本基本到頭了,再分析也分析不出個啥了,接下來動態調試:找到剛才分析的shell,記住基址,后面會根據偏移定位關鍵函數和變量;

加上函數的FOA=29DC,絕對地址就是8D2D0000+29DC=8D2D29DC

找到了,下個斷點:看紅框框的地方,字符串還沒被解密了:F9繼續執行

字符串被解密了:可以確定init_array肯定在解密字符串:

從這里一路開始F8,來到了分發的地方:這也是這種混淆最頭疼的地方:這里有大量的分支,根據取值不同走不同的分支;

這里有綠色的線,說明那是下一步跳轉的地方。對於這種控制流平坦化的分支,建議每跳一次,就在ida靜態分析時標注一次,方便后續靜態分析時剔除雜音!

然后一路F8,終於來到了另一個很重要的函數:偏移是0x3126(這種情況通過靜態分析時不可能找得到的,只能通過動態調試找到)

繼續動態調試前,先靜態分析一下函數大概是干啥的。看不懂的細節再通過動態調試去理解;這里有點經驗之談:前面這些代碼考前,而且比價“平坦”,沒有較大的分支跳轉,按照一般正向開發經驗來看,大概率做很多基礎性質的工作,比如初始化某些變量,讀取某些關鍵數據,換句話說就是“預處理”;

這里也不像是dex加載到內存:

從這里開始又在判斷虛擬機是dvm還是art,兩個分支都考慮了;同時前面也注入了classloader,所以這里有可能是在映射dex(這里ida反編譯有些小問題,看匯編更直接);

如果android版本不是19,那么只調用sub_ad24一個函數,說明這個函數包含了所有dex的處理邏輯,值得進去看看:這種指針加偏移形式的,很有可能是JNIEnv *,可以轉換變量類型試試:


因為mmap有可能是加載dex的函數,所以可以在函數開始的地方下斷點,但這里現在末尾下斷點,看看此時context的值(尤其時前面幾個傳參的寄存器):好幾次F9后,終於在R0這里看到了希望:

dump到本地看看了:

用jadx打開一看:這又是一個悲傷的故事:關鍵代碼和指令都被抽取了!所以脫殼還未完成,同志仍需努力!說明后面還有指令還原的代碼我們並未執行到,所以繼續往后分析和調試!

重新回到前面幾層:這里有另一個比較關鍵的sub_5110函數,如下:

進入sub_5110,和jni_onload一個鳥樣(甚至更離譜),也被控制流平坦化了,呵呵,又是一個“此地無銀三百兩”!

老規矩:v3有可能是env,先改type,方便理解代碼;

這里明顯是通過反射得到java層的一個installdex的方法:

java層的installdex函數在這里:這就容易看懂了吧?通過classloader加載dex的:

這里實錘了sub_5110就是install dex的方法,先改個名,方便辨認;動態調試時在這里下個斷點,成功斷下;由於1B47C只是dex加載失敗后“善后”的功能,這里直接忽略,看代碼直接跳轉到LABEL_74這里了;

回到sub_CC9C函數,繼續往下走:這里調用fork創建子進程,這里也很可疑:一般一個進程自己跑自己的代碼,有些並行計算的需求就創建線程單獨跑,這里居然新生成一個進程,這種操作不常見,下斷點跟蹤后發現:在sub_10B8C斷下了,這個函數值得進去看看;

本想用老規矩看看有沒有被混淆,結果IDA直接提示這個:不用想了,肯定有問題!

F5的代碼是這樣的,正常人有這么寫代碼的么?

在這個函數繼續下斷點的單步跟蹤:來到了FB64函數的_aeabi_memcpy這里:為啥要重點關注這個函數了? 前面已經把dex解密dump出來了,但是關鍵指令還被抽取掉了;要把指令還原,肯定要copy回去呀!

在_aeabi_memcpy這里下個斷點(動態調試居然沒識別函數名.......),R0就是dex的首地址了,同樣導出來:

成功還原dex:

總結:
1、重要的函數都會被混淆,這是一種典型的“此地無銀三百兩”的行為!所以一旦發現函數被混淆,都建議下個斷點調試一下,看看這個函數到底干了啥!
2、字符串、dex、so這些文件被加密,但是在運行的時候肯定會解密,否則app怎么被cpu、android正確運行了? 所以dump內存是必須的步驟,這個一定不能少(這里抓住了兩個關鍵的函數:mmap和_aeabi_memcpy)!本人以前做windows逆向的,很多時候都是直接用CE搜索內存,找到關鍵數據開始逆向的。android下的逆向也能借鑒類似的思路,后續繼續分享!
3、還有個比較明顯的通用的dex脫殼處:DexFile,不同版本的系統對這個類的定義可能不完全相同,建議從http://androidxref.cn這里查查類的具體定義,這里以7版本的舉例:http://androidxref.cn/android-7.1.2_r39/xref/art/runtime/dex_file.h,頭文件里面有兩個關鍵的成員變量,如下:
// The base address of the memory mapping. 1235 const uint8_t* const begin_; 1236 1237 // The size of the underlying memory allocation in bytes. 1238 const size_t size_;
這兩個分別是dex文件的指針和dex文件的大小,所以只要能得到這個指針,就能得到內存中解密后的dex文件,就可以dump出來!那么脫殼的問題又轉換成了:怎么找到DexFile這個指針了?這個簡單,用ida打開libart.so,函數名用DexFile去搜索,能找到一大堆使用了DexFile作為參數的函數,如下:

這里以LoadMethod方法為例,第二個參數就是DexFile,很容易通過hook這個方法得到內存中的dex;然后在根據dex文件頭得到整個dex的大小,整個過程簡單粗暴,如下:

hook的腳本如下: 這個腳本也能做成通用的dex脫殼方法(注意:4.4-7.0版本的DexFile參數是args[1],8.0-11.0版本的DexFile參數是args[0],其他的都通用);
function getDexFile() { //32 _ZN3art11ClassLinker10LoadMethodERKNS_7DexFileERKNS_21ClassDataItemIteratorENS_6HandleINS_6mirror5ClassEEEPNS_9ArtMethodE //64 _ZN3art11ClassLinker10LoadMethodERKNS_7DexFileERKNS_21ClassDataItemIteratorENS_6HandleINS_6mirror5ClassEEEPNS_9ArtMethodE var loadmethodaddr = Module.getExportByName("libart.so", "_ZN3art11ClassLinker10LoadMethodERKNS_7DexFileERKNS_21ClassDataItemIteratorENS_6HandleINS_6mirror5ClassEEEPNS_9ArtMethodE"); console.log(Process.arch + "--loadmethod:" + loadmethodaddr);//Process.arch:運行模式是32還是64位 Interceptor.attach(loadmethodaddr, { onEnter: function (args) { var dexfileptr = args[1]; console.log("DexFile pointer:" + dexfileptr); var begin_ = ptr(dexfileptr).add(Process.pointerSize).readPointer(); var size_ = ptr(dexfileptr).add(Process.pointerSize * 2).readU32(); console.log(hexdump(ptr(begin_))); console.log("dexfile begin:" + begin_ + "--size:" + size_); //console.log(hexdump(ptr(dexfileptr))); }, onLeave: function (retval) { } }); } setImmediate(getDexFile);
4、個人的一點感悟:windows下3環和0環是嚴格分開的:3環是一般的exe或dll,0環就是驅動下的sys,有嚴格的隔離;要想hook操作系統內核,必須通過驅動進入0環;但是android下貌似簡單一些:只要手機root,就能hook libart這種系統級別的so庫,感覺簡單多了!一旦修改系統級別的so庫,和修改操作系統的源代碼已經沒有本質區別了,利用這一點可以做好多有趣的應用!
參考:
1、https://blog.csdn.net/m0_37344790/article/details/79102031 動態調試脫殼
