Android 逆向apk的.so動態庫
那麼我們上篇文章中提及了安全性問題, Android apk如何加固防止被破解(防止逆向編譯),那麼本篇文章提及一點,so動態庫的安全性與重要性。
首先我們要知道, .so動態庫是做什麼用的,它不像.smail檔案可修改,它是屬於組合語言,如果直接去修改,檔案會發生錯亂。早上有人來問我,遊戲打入渠道sdk之後發生錯誤,且只有armeabi裡發生錯誤,這種情況可以斷定.so動態庫中有了相容的衝突。
通常我們會看到libs下面有這麼幾個資料夾,mips、armeabi、armeabi-v7a和x86,其實是代表著不同的CPU型別,那麼在arm下有不同的指令,想要了解的可以參考這篇文章 《Android ARM常用的彙編指令合集》
實現步驟:
- 使用apktool命令編譯出來apk的目錄
- 然後用IDA開啟.so檔案,在apk根目錄的lib資料夾下
- 選單中有個Search,可以用text作為入口
- 找到入口,一般為init方法
- 使用動態除錯,給libdvm.so中的函式:dvmDexFileOpenPartial 下斷點,然後得到dex檔案在記憶體中的起始地址和大小,然後dump處dex資料即可
- 分析底層載入dex原始碼,知道有一個函式:dvmDexFileOpenPartial 這個函式有兩個重要引數,一個是dex的其實地址,一個是dex的大小,而且知道這個函式是在libdvm.so中的。所以我們可以使用IDA進行動態除錯獲取資訊
- 雙開IDA開始獲取記憶體中的dex內容,雙開IDA,走之前的動態破解so方式來給dvmDexFileOpenPartial函式下斷點,獲取兩個引數的值,然後使用一段指令碼,將記憶體中的dex資料儲存到本地磁碟中。
- 分析獲取到的dex內容,得到了記憶體中的dex之後,我們在使用dex2jar工具去檢視原始碼,但是發現儲存,以為是dump出來的dex格式有問題,但是最後使用baksmali工具進行處理,得到smali原始碼是可以的,然後我們就開始分析smali原始碼。
Tips:
- debugger模式
- 通過dump出記憶體中的dex資料,其實不管apk如何加固,最後都是會載入到記憶體中的
- 可以嘗試呼叫so中的native方法,在知道了這個方法的定義之後 adb shell input text 命令來輔助我們的輸入
我們以 趣頭條.apk 為例,反向思維來看下這個包是如何加密的,首先我們來看目錄結構:
我們可以看到,smail裡面是沒多少程式碼的,主要的是so動態庫和兩個jar包,開啟jar包來看看:(bdxadsdk.jar 和 gdtadv2.jar )
什麼都沒有,可以肯定的是,這個是加了殼的,加密方式是怎麼樣的呢,我們來參考網上一張圖:
我們先去看下 smail檔案裡面有什麼線索沒
“libjiagu” 顧名思義,加固,我們發現 ,這個包還加固了。來,繼續往下走
“DexOptJobService_DexOptimization” 動態載入了Dex
那麼動態載入dex技術是如何處理的?引用一張圖來看看,順便過下這個知識點
關於PathClassLoader,API中提及: Android uses this class for its system class loader and for its application class loader(s) —->> Android應用就是用它來載入;
DexClass可以載入apk,jar,及dex檔案,但PathClassLoader只能載入已安裝到系統中(即/data/app目錄下)的apk檔案。
子節點都是從繼承BaseDexClassLoader中來的,那我們去看看原始碼:
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
從程式碼中我們發現,當我們需要去找class時,是從pathList中的findClass方法中讀取,檢視原始碼,可以發現pathList是DexPathList類的一個例項,我們接著來看findClass(name, suppressedExceptions);
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
看完之後我們可以發現,它是遍歷一個裝在dex檔案(每個dex檔案實際上是一個DexFile物件)的陣列(Element陣列,Element是一個內部類),然後依次去載入所需要的class檔案,直到找到為止。
public String inject(String libPath) {
boolean hasBaseDexClassLoader = true;
try {
Class.forName("dalvik.system.BaseDexClassLoader");
} catch (ClassNotFoundException e) {
hasBaseDexClassLoader = false;
}
if (hasBaseDexClassLoader) {
PathClassLoader pathClassLoader = (PathClassLoader)sApplication.getClassLoader();
DexClassLoader dexClassLoader = new DexClassLoader(libPath, sApplication.getDir("dex", 0).getAbsolutePath(), libPath, sApplication.getClassLoader());
try {
Object dexElements = combineArray(getDexElements(getPathList(pathClassLoader)), getDexElements(getPathList(dexClassLoader)));
Object pathList = getPathList(pathClassLoader);
setField(pathList, pathList.getClass(), "dexElements", dexElements);
return "SUCCESS";
} catch (Throwable e) {
e.printStackTrace();
return android.util.Log.getStackTraceString(e);
}
}
return "SUCCESS";
}
看到這裡,注入的解決方案也就浮出水面,假如我們將第二個dex檔案放入Element陣列中,那麼在載入第二個dex包中的類時,應該可以直接找到。
OK,這個知識點到底結束,我們知道了這個apk的加固與載入dex方式,那如何逆向分析這個.so動態庫?
等我稍後整理下圖片,先發布了,持續更新。