APK加壳【2】内存加载dex实现详解
來源
本文要實驗的方案同樣來源于CSDN大牛Jack_Jia的一篇翻譯博文:
Android4.0內存Dex數據動態加載技術
原文的地址是 http://2013.hackitoergosum.org/presentations/Day1-05.Nifty%20stuff%20that%20you%20can%20still%20do%20with%20Android%20by%20Xavier%20Martin.pdf
著重參考了看雪論壇的兩篇帖子:
【求助】Dex內存加載的Native實現過程中出現的問題
【原創】DEX文件內存加載實現中的數據構造(C部分)
方案
從上一篇,基礎加殼的思路最后得出的結果是方案還不夠完善。因為使用的系統DexClassLoader提供的接口必須要求源程序保存在文件系統中,對手一旦過了萊茵河馬其諾防線就沒啥意義了。所以在前一篇的基礎上,又有上面來源方案中的思路,即通過jni調用底層接口,在內存中加載dex文件。步驟如下:
方案本身是譯文,而且沒有介紹細節上的實現。不能像上一篇那樣直接copy代碼,那就只能老老實實的先搞清楚原理。通過短短的幾百字譯文,可以總結出一下幾點:
該方案只是針對實現內存加載dex文件,對于加殼來說這只是其中的一部分、最重要的一部分;
方案的技術點在于通過dlopen、dlsym方法,拿到系統動態庫libdvm.so中的內存加載dex文件的方法,該方法位于源碼 :dalvik/vm/native/ dalvik_system_DexFile.cpp 類中,名稱是:Dalvik_dalvik_system_DexFile_openDexFile_bytearray;并且只在4.0以上版本開放;
底層加載dex文件后,會得到一個int型的cookie值,java層的自定義DexClassLoader需要根據該值能夠拿到已加載好的dex內容才能把整個流程拼接起來;
實現
雖然從方案分析上看,這個加載實現是有系統版本局限性的,不過通過dlsym方法拿到系統動態庫函數指針然后來使用的思路對一個中間層認識有限的土錘來說還從來沒嘗試過,并且,通用的方法應該也離不開這種模式,所以完全有理由去實現它,作為一個中間過程。
所有嘗試都是基于上一篇的基礎班加殼的實現上,不要忘記我們的最終目的是實現APK加殼,內存加載dex文件只是其中的一部分。
殼工程的迭代
本地代碼
Jni關鍵代碼基本都在譯文博客中了,我們要做的是讓它通過編譯、得到so庫。本地代碼當然要有與之對應的java代碼去加載才能用,通過上面對因為的總結,可以先這樣定義本地方法:
static native int loadDex(byte[] dex,long dexlen);生成好對應的.h、.c文件之后把譯文中給出的核心代碼填上,下面才是難題,許多類型都是unknown的,ndk編譯器會告訴你它不認識這些亂七八糟的玩意兒。接下來就是挨個補充定義了。
看著u4、u1這些從java程序猿眼中怪怪的類型我不禁長出一口氣——幸虧當年是C出身的。溯本清源,在源碼 /dalvik/vm/Common.h 類中找到了這群貨的宏定義,于是照葫蘆畫瓢,在jni目錄里弄了一個偽造版的Common.h,搜刮了一下所有需要定義的類型之后,這個文件基本上是這個樣子的:
這里面還有個大小端的問題,不過為求實驗先通過就先定義死,過了再說。
還有個值得一提的結構就是最后面的ArrayObject,這玩意定義在源碼的/dalvik/vm/oo/Object.h 中,原本的定義是這樣的:
如果還實實在在的去弄一個ClassObject,那就是java中毒已深的表現,根據看雪里面的相關討論(就是文首提到的兩篇),直接如上定義了。得到最后的C代碼如下:
#include "com_android_dexunshell_NativeTool.h" #include <stdlib.h> #include <dlfcn.h> #include <stdio.h>JNINativeMethod *dvm_dalvik_system_DexFile; void (*openDexFile)(const u4* args,union JValue* pResult);int lookup(JNINativeMethod *table, const char *name, const char *sig,void (**fnPtrout)(u4 const *, union JValue *)) {int i = 0;while (table[i].name != NULL) {LOGI("lookup %d %s" ,i,table[i].name);if ((strcmp(name, table[i].name) == 0)&& (strcmp(sig, table[i].signature) == 0)) {*fnPtrout = table[i].fnPtr;return 1;}i++;}return 0; }/* This function will be call when the library first be load.* You can do some init in the libray. return which version jni it support.*/ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {void *ldvm = (void*) dlopen("libdvm.so", RTLD_LAZY);dvm_dalvik_system_DexFile = (JNINativeMethod*) dlsym(ldvm,"dvm_dalvik_system_DexFile");if(0 == lookup(dvm_dalvik_system_DexFile, "openDexFile", "([B)I",&openDexFile)){openDexFile = NULL;LOGE("method does not found ");}else{LOGI("method found ! HAVE_BIG_ENDIAN");}LOGI("ENDIANNESS is %c" ,ENDIANNESS );void *venv;LOGI("dufresne----->JNI_OnLoad!");if ((*vm)->GetEnv(vm, (void**) &venv, JNI_VERSION_1_4) != JNI_OK) {LOGE("dufresne--->ERROR: GetEnv failed");return -1;}return JNI_VERSION_1_4; }JNIEXPORT jint JNICALL Java_com_android_dexunshell_NativeTool_loadDex(JNIEnv * env, jclass jv, jbyteArray dexArray, jlong dexLen) {// header+dex contentu1 * olddata = (u1*)(*env)-> GetByteArrayElements(env,dexArray, NULL);char* arr;arr=(char*)malloc(16+dexLen);ArrayObject *ao=(ArrayObject*)arr;ao->length=dexLen;memcpy(arr+16,olddata,dexLen);u4 args[] = { (u4) ao };union JValue pResult;jint result;LOGI("call openDexFile 33..." );if(openDexFile != NULL){openDexFile(args,&pResult);}else{result = -1;}result = (jint) pResult.l;LOGI("Java_com_android_dexunshell_NativeTool_loadDex %d" , result);return result; }ArrayObject之后的數據拷貝是從看雪上抄來的,剛開始不求甚解,后來看了源碼中的調用方法就慢慢明白了:
Java層
底層代碼基本了然,也就是說譯文提供的思路基本實現,剩下其他加殼的事兒還要自己動腦筋補上。現在java層我們有一個可以使用的以byte數組為參數的加載dex的接口了:
static native int loadDex(byte[] dex,long dexlen);
要知道我們花這么大力氣實現的這個方法,實際意義在于讓源程序的dex數據在內存中傳遞,而不是必須保存在某個地方、以文件的方式。也就是說,我們需要一個新的DexClassLoader,去替換在上一篇提到的基礎加殼方案中自定義Application—— ProxyApplication 類,通過反射設置到”android.app.LoadedApk”中mClassLoder屬性的那個系統DexClassLoader,即至少那一段應該改成這樣:
沒錯,DynamicDexClassLoder 它的構造參數中應當去接收源程序的dex數據,以byte數組的形式,這樣、相關把dex數組保存為文件那段代碼可以刪除,/data/data 中相關目錄就找不到緩存dex文件的身影了;
替換DexClassLoader,要知道相對于系統版本的加載器我們的少了什么,又多出了什么,在一一對接上,就沒問題了。少了什么呢?是dex文件路徑、多出了什么呢?是dex byte數組,考慮到已經實現的jni庫,那就是多了一個加載好的dex文件對應的cookie值。那么,這個
Cookie 是否能夠完成替換呢?這需要到源碼中找答案。
源碼路徑:libcore/dalvik/src/main/java/dalvik/system ,生成類圖,取出DexClassLoader相關的一部分:
走讀幾遍代碼基本就能了解,對于dex文件加載而言,DynamicDexClassLoder需要做的實際上只有一件事,復寫findClass方法,使APK運行時能夠找到和加載源程序dex中的類,至于如何實現,從類圖上就可以看出,最后實際上追溯到DexFile類,可以利用到jni加載到的cookie,通過反射DexFile中的方法,實現我們的預期,具體實現如下:
package com.android.dexunshell;import java.io.IOException; import java.net.URL; import java.util.Enumeration;import com.eebbk.mingming.k7utils.ReflectUtils;import android.content.Context; import android.util.Log; import android.view.LayoutInflater;import dalvik.system.DexClassLoader; import dalvik.system.DexFile;public class DynamicDexClassLoder extends DexClassLoader {private static final String TAG = DynamicDexClassLoder.class.getName();private int cookie;private Context mContext;/*** 原構造** @param dexPath* @param optimizedDirectory* @param libraryPath* @param parent*/public DynamicDexClassLoder(String dexPath, String optimizedDirectory,String libraryPath, ClassLoader parent) {super(dexPath, optimizedDirectory, libraryPath, parent);}/*** 直接從內存加載 新構造** @param dexBytes* @param libraryPath* @param parent* @throws Exception*/public DynamicDexClassLoder(Context context, byte[] dexBytes,String libraryPath, ClassLoader parent, String oriPath,String fakePath) {super(oriPath, fakePath, libraryPath, parent);setContext(context);setCookie(NativeTool.loadDex(dexBytes, dexBytes.length));}private void setCookie(int kie) {cookie = kie;}private void setContext(Context context) {mContext = context;}private String[] getClassNameList(int cookie) {return (String[]) ReflectUtils.invokeStaticMethod(DexFile.class,"getClassNameList", new Class[] { int.class },new Object[] { cookie });}private Class defineClass(String name, ClassLoader loader, int cookie) {return (Class) ReflectUtils.invokeStaticMethod(DexFile.class,"defineClass", new Class[] { String.class, ClassLoader.class,int.class }, new Object[] { name, loader, cookie });}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {Log.d(TAG, "findClass-" + name);Class<?> cls = null;String as[] = getClassNameList(cookie);for (int z = 0; z < as.length; z++) {if (as[z].equals(name)) {cls = defineClass(as[z].replace('.', '/'),mContext.getClassLoader(), cookie);} else {defineClass(as[z].replace('.', '/'), mContext.getClassLoader(),cookie);}}if (null == cls) {cls = super.findClass(name);}return cls;}@Overrideprotected URL findResource(String name) {Log.d(TAG, "findResource-" + name);return super.findResource(name);}@Overrideprotected Enumeration<URL> findResources(String name) {Log.d(TAG, "findResources ssss-" + name);return super.findResources(name);}@Overrideprotected synchronized Package getPackage(String name) {Log.d(TAG, "getPackage-" + name);return super.getPackage(name);}@Overrideprotected Class<?> loadClass(String className, boolean resolve)throws ClassNotFoundException {Log.d(TAG, "loadClass-" + className + " resolve " + resolve);Class<?> clazz = super.loadClass(className, resolve);if (null == clazz) {Log.e(TAG, "loadClass fail,maybe get a null-point exception.");}return clazz;}@Overrideprotected Package[] getPackages() {Log.d(TAG, "getPackages sss-");return super.getPackages();}@Overrideprotected Package definePackage(String name, String specTitle,String specVersion, String specVendor, String implTitle,String implVersion, String implVendor, URL sealBase)throws IllegalArgumentException {Log.d(TAG, "definePackage" + name);return super.definePackage(name, specTitle, specVersion, specVendor,implTitle, implVersion, implVendor, sealBase);}}
加密工具的跟進
加密工具需要變化的是,加入殼程序dex的加密數據不再是整個源程序的APK,而是源程序中的dex文件。這一點修改加密代碼中的目標文件、并修改操作腳本即可,無需多說。
小結
結合譯文方案,實現了內存加載dex文件,并通過自定義DexClassLoader的方式,鞏固了之前的加殼方案,使源程序不在以文件的形式出現。殼的意義也在于此,至于防止內存中獲取dex這種高級的破解方法,殼似乎略顯無力,所以先放到后面考慮。目前的問題是,內存加載dex所依賴的底層方法,只在4.0以上幾個版本存在,5.0沒有查詢還是未知數,還沒能滿足通用性的要求,要需要進一步尋找方案。
原文地址: http://taoyuanxiaoqi.com/2015/01/16/apkshell2/
總結
以上是生活随笔為你收集整理的APK加壳【2】内存加载dex实现详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: APK加壳【1】初步方案实现详解
- 下一篇: APK加壳【3】通用内存加载dex方案分