APK加壳【3】通用内存加载dex方案分析
來源
Andorid APK反逆向解決方案:梆梆加固原理探尋
CSDN 作者Jack_Jia
該篇博文中的:“3. 如何使DexClassLoader加載加密的dex文件? ”這部分。
方案
上一篇實現的內存加載dex方案,具有Android系統版本的局限性。為了克服這個問題,在不斷的百度、google下,找到了本文的來源博文,該文章是分析梆梆加固的,作為一個以APK安全為業的公司級產品,實現的加密當然是全面的。在對梆梆加固實現的猜想部分,在“如何使DexClassLoader加載加密的dex文件?”這個技術點下,作者提出了這樣一個方案猜想,步驟如下:
讀取dexFileName文件內容并解密到byte數組。
調用dexFileParse函數解析byte數組為DexFile:\dalvik\libdex\DexFile.c
調用allocateAuxStructures轉換DexFile為DvmDex(由于該方法為static方法,因此需要按照其邏輯自行實現)。\dalvik\vm\DvmDex.c
static DvmDex* allocateAuxStructures(DexFile* pDexFile)添加DvmDex到gDvm.userDexFiles \dalvik\vm\Init.c
struct DvmGlobals gDvm; //gDvm = dlsym(handle, "gDvm");修改MyDexClassLoader中的mDexs對象的mCookie值。mCookie主要用于映射底層DvmDex數據——DexClassLoader.mDexs[0].mCookie值。
分析與實驗
對于一個對底層代碼了解有限的菜鳥而言,對于這個猜想方案只能表示一頭霧水。好在有了之前方案實現的基礎,仔細分析起來還是有跡可循的。在開始實驗之前,針對這個方案提出一個想不明白的問題,然后通過走讀、對比源碼來找到答案,以期能夠快速的吃透這個思路的原理。
這些是在動手實踐之前想不通的問題,那么帶著問題,就開始在源碼、搜索引擎、前兩個方案的實例中尋找答案。在不斷摸索中,慢慢的還是可以形成一些看法的。對整個方案的認識也會更進一步。挨個回答吧。
第一題,答案是同樣的方法根本拿不到dexFileParse這個指針。
根據同目錄下的 Android.mk 文件配置可知:
實際上這貨是個靜態庫,生成的是lib.a,所以不能使用同樣的方法搞出函數指針來用;
第二題,只能捂著臉來答了,因為這屬于C基礎知識范圍的內容,C語言中的靜態函數與java中的靜態函數意義完全不一樣,C中static主要限定該函數只能在該文件中使用。我覺得我該考慮修改一下簡歷里對C語言的熟悉程度了,好久沒碰果然忘得夠快。其實上個方案調用的方法也是靜態的:
static void Dalvik_dalvik_system_DexFile_openDexFile_bytearray(const u4* args, JValue* pResult);不過仔細看函數指針的獲取方法就知道,源碼中是將這個靜態函數相關信息保存在一個常量數組里面的,而上個方案也是先獲取常量數組的標號之后再獲取該函數的指針。
const DalvikNativeMethod dvm_dalvik_system_DexFile[] = { { "openDexFile", "(Ljava/lang/String;Ljava/lang/String;I)I",Dalvik_dalvik_system_DexFile_openDexFile }, { "openDexFile", "([B)I",Dalvik_dalvik_system_DexFile_openDexFile_bytearray },… }此外,對于靜態函數allocateAuxStructures,除了需要模擬相關聯到的結構體之外,摳出來通過編譯并不難;
第三題,去指定的Init.cpp文件看看那個全局變量,基本上能猜測一二。每一個APK的啟動與運行,在底層都會對應一個Android DVM,同時也就是有一個全局變量gDvm,通過dlsym獲取全局變量的地址是沒問題的。問題是不同版本的DVM,擁有不同版本的DvmGlobals結構,只有指針的情況下無法拿到正確的gDvm值。不過這個問題可以先放一放,大不了使用最笨的方法——根據版本選用不同的結構體去解析也可實現;
第四題,cookie。實際上到這一步的時候基本上可以脫離原猜想方案來看問題了。想知道如何獲取cookie,最簡單的辦法就是通過已有的代碼找找這貨究竟是個什么東西。那么,很明顯上個方案中Dalvik_dalvik_system_DexFile_openDexFile_bytearray——它就返回過一個cookie。
static void Dalvik_dalvik_system_DexFile_openDexFile_bytearray(const u4* args, JValue* pResult) {…pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar));pDexOrJar->isDex = true;pDexOrJar->pRawDexFile = pRawDexFile;pDexOrJar->pDexMemory = pBytes;pDexOrJar->fileName = strdup("<memory>"); // Needs to be free()able.…RETURN_PTR(pDexOrJar); }或許到這里還不甚清晰,因為返回類型是void,那么返回的任務肯定是落在輸入參數pResult上了,看看這個RETURN_PTR宏定義基本也就清楚了:
#define RETURN_PTR(_val)do { pResult->l = (Object*)(_val); return; } while(0)好,所謂cookie其實是個DexOrJar類型結構體的指針。JAVA層需要這個cookie值,到最后也就是C/C++中需要這個結構體,有了這個結構體中的信息就能拿到加載dex的內容。反觀猜想方案中的步驟:
通過dexFileParse拿到一個DexFile指針(雖然動態加載無法實現,我們先假設能夠拿到)->以DexFile指針為參數,通過allocateAuxStructures方法轉換一個DvmDex 指針->找到DVM中的gDvm,將DvmDex設置進該全局變量的userDexFiles屬性中(這一步沒有指明具體的方法,但是我們也假設能找到合適的方法)->返回cookie到JAVA層使用。
可以看出cookie在整個方案中出現的很突兀,參照前面幾步用到的函數也找不到任何有cookie的地方。上文所引函數已經很清晰的說明了cookie實際上是個DexOrJar指針(這一點需要通過不同版本的Android源碼來對比驗證,確實如此),指針只是地址,真正起作用的是這個結構:
4.0以前的版本沒有最后一個變量,但這不影響我們對整個流程的理解。很關鍵的一點,該結構需要一個RawDexFile指針,而顯然我們到目前為止只有一個DvmDex指針,這之間明顯缺乏一個轉換的步驟。
同時,我們再看看第五題,會不會產生一個完善的思路出來?沒錯,相關流程中4.0版本與之前的版本相比,除了相關的代碼從C變成C++之外,它多出了實現內存讀取dex的功能,就在Dalvik_dalvik_system_DexFile_openDexFile_bytearray函數中,猜想方案中的步驟也無非應該是在2.3版本中對應可能實現該功能的一種方式,兩者相差不會很大。我們把4.0中實現的方法、流程吃的透徹一點,對于實驗方案的出爐一定有事半功倍的效果。于是,在文件、函數各種跳轉之后,有了下面一個流程簡圖:
這是4.1版本源碼的函數調用簡易流程,去掉了各種容錯、拋異常的部分,只列出了關鍵函數,每個虛線框中表示一個文件。通過代碼可以很明確的了解,所謂內存加載dex也就做了兩件事,第一是加載;第二是注冊(實際操作是寫入全局變量的一個屬性表,我覺得用“注冊”來描述這個動作也蠻貼切)。通過代碼走讀,也完全可以了解到猜測方案中的第三步,使用allocateAuxStructures方法得到DvmDex其實無法達到加載dex的目的,該函數只是做了一個轉換,malloc了一個DvmDex結構而已。也就是說,完全按照猜想方案來,是無法正確加載dex數據的。
這個時候再看第六題就顯得無足輕重,Android 版本問題固然是阻礙通用方案實現的最大障礙之一,但是重點不在猜想方案設計的諸函數中,因為猜想方案本身就略顯骨感,不能實現預期的目的。
綜上,我們可以根據猜想方案衍生出一個預計可行的方案。其實無論是猜想方案還是4.0之后系統中的實現,它們的目的是相同的:加載、注冊。加載實現的重點在于DexPrepare文件中的rewriteDex函數;注冊的實現重點則在于Init文件中的gDvm變量。打通這兩個關節,應該可以算是又向前邁上了一步。拋開不同版本的DVM其相關數據結構、實現過程的差別帶來的阻礙,先考慮在低版本拉通這個過程,那新的思路與實現目標就是:
仿照4.0的Dalvik_dalvik_system_DexFile_openDexFile_bytearray函數實現,在jni中實現內存加載的方法,測試系統為Android2.3。
Jni實現的大體思路如下:
大致類似于:
int mock_dalvik_system_DexFile_openDexFile_bytearray(u1 * olddata, long len) {LOGI("before make pRawDexFile !!");DexOrJar* pDexOrJar = NULL;RawDexFile* pRawDexFile = (RawDexFile*) calloc(1, sizeof(RawDexFile));if (0 == dvmRawDexFileOpenArray(olddata, len, &pRawDexFile)) {LOGV("Unable to open in-memory DEX file");} else {LOGV("open in-memory DEX file success!");}pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar));pDexOrJar->isDex = true;pDexOrJar->pRawDexFile = pRawDexFile;//pDexOrJar->pDexMemory = olddata;pDexOrJar->fileName = strdup("<memory>"); // Needs to be free()able.LOGI("before addToDexFileTable, and pRawDexFile %p", pRawDexFile);addToDexFileTable(pDexOrJar);LOGI("addToDexFileTable success return %p", pDexOrJar);return (int) pDexOrJar; }摳源碼和結構并不是個簡單的活計,當然并不是說很難,準確的說是有點枯燥。測試好能夠正常拿到gDvm和各相關函數指針之后就可以開始驗證了。
臨時結論
在2.3系統的驗證中,依樣畫葫蘆還是出了問題,在摳出來的rewriteDex代碼實現中,最后一步驗證和優化class數據的函數verifyAndOptimizeClasses報錯異常。由于該函數是通過函數指針直接調用底層代碼的,所以無法直接看出出錯原因,項目暫停,沒有時間繼續探索了,暫時記錄在此。
思考一下依樣畫葫蘆的方法,依的是4.0的樣在2.3上畫葫蘆可以預見的是版本間的差別可能會造成不可預期的謬誤。具體如何在2.3中實現內存加載,估計還要從2.3本身如何加載dex入手去分析實現過程,來優化上面的實現。
原文地址: http://taoyuanxiaoqi.com/2015/01/25/apkshell3/
總結
以上是生活随笔為你收集整理的APK加壳【3】通用内存加载dex方案分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: APK加壳【2】内存加载dex实现详解
- 下一篇: Android 开发, Android