APK加殼【2】內存加載dex實現詳解

本文引自個人博文 APK加殼【2】內存加載dex實現詳解php

來源

方案

從上一篇,基礎加殼的思路最後得出的結果是方案還不夠完善。由於使用的系統DexClassLoader提供的接口必需要求源程序保存在文件系統中,對手一旦過了萊茵河馬其諾防線就沒啥意義了。因此在前一篇的基礎上,又有上面來源方案中的思路,即經過jni調用底層接口,在內存中加載dex文件。步驟以下:java

  • 獲取 Dalvik_dalvik_system_DexFile_openDexFile_bytearray 方法指針;
  • 調用 Dalvik_dalvik_system_DexFile_openDexFile_bytearray 方法解析Dex數據;
  • 實現JAVA層Dex ClassLoader完成類的加載;

方案自己是譯文,並且沒有介紹細節上的實現。不能像上一篇那樣直接copy代碼,那就只能老老實實的先搞清楚原理。經過短短的幾百字譯文,能夠總結出一下幾點:android

  • 該方案只是針對實現內存加載dex文件,對於加殼來講這只是其中的一部分、最重要的一部分;
  • 方案的技術點在於經過dlopen、dlsym方法,拿到系統動態庫libdvm.so中的內存加載dex文件的方法,該方法位於源碼:dalvik/vm/native/ dalvik_system_DexFile.cpp 類中,名稱是:

    • Dalvik_dalvik_system_DexFile_openDexFile_bytearray 而且只在4.0以上版本開放、4.4又被刪掉;
    • 底層加載dex文件後,會獲得一個int型的cookie值,java層的自定義DexClassLoader須要根據該值可以拿到已加載好的dex內容才能把整個流程拼接起來;

實現

雖然從方案分析上看,這個加載實現是有系統版本侷限性的,不過經過dlsym方法拿到系統動態庫函數指針而後來使用的思路對一箇中間層認識有限的土錘來講還歷來沒嘗試過,而且,通用的方法應該也離不開這種模式,因此徹底有理由去實現它,做爲一箇中間過程。shell

全部嘗試都是基於上一篇的基礎班加殼的實現上,不要忘記咱們的最終目的是實現APK加殼,內存加載dex文件只是其中的一部分。數組

本地代碼

Jni關鍵代碼基本都在譯文博客中了,咱們要作的是讓它經過編譯、獲得so庫。本地代碼固然要有與之對應的java代碼去加載才能用,經過上面對由於的總結,能夠先這樣定義本地方法:緩存

static native int loadDex(byte[] dex,long dexlen);

生成好對應的 .h.c 文件以後把譯文中給出的核心代碼填上,下面纔是難題,許多類型都是unknown的,ndk編譯器會告訴你它不認識這些亂七八糟的玩意兒。接下來就是挨個補充定義了。cookie

看着u四、u1這些從java程序猿眼中怪怪的類型我不由長出一口氣——幸好當年是C出身的。溯本清源,在源碼 /dalvik/vm/Common.h 類中找到了這羣貨的宏定義,因而照葫蘆畫瓢,在jni目錄里弄了一個僞造版的Common.h,搜刮了一下全部須要定義的類型以後,這個文件基本上是這個樣子的:app

#ifndef DALVIK_COMMON_H_
#define DALVIK_COMMON_H_

#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <assert.h>

static union { char c[4]; unsigned long mylong; }endian_test = {{ 'l', '?', '?', 'b' } };
#define ENDIANNESS  ((char)endian_test.mylong)

//#if ENDIANNESS == "l"
#define HAVE_LITTLE_ENDIAN
//#else
//#define HAVE_BIG_ENDIAN
//#endif

#if defined(HAVE_ENDIAN_H)
# include <endian.h>
#else /*not HAVE_ENDIAN_H*/
# define __BIG_ENDIAN 4321
# define __LITTLE_ENDIAN 1234
# if defined(HAVE_LITTLE_ENDIAN)
#  define __BYTE_ORDER __LITTLE_ENDIAN
# else
#  define __BYTE_ORDER __BIG_ENDIAN
# endif
#endif /*not HAVE_ENDIAN_H*/

#if !defined(NDEBUG) && defined(WITH_DALVIK_ASSERT)
# undef assert
# define assert(x) \
((x) ? ((void)0) : (ALOGE("ASSERT FAILED (%s:%d): %s", \
__FILE__, __LINE__, #x), *(int*)39=39, (void)0) )
#endif

#define MIN(x,y) (((x) < (y)) ? (x) : (y))
#define MAX(x,y) (((x) > (y)) ? (x) : (y))

#define LIKELY(exp) (__builtin_expect((exp) != 0, true))
#define UNLIKELY(exp) (__builtin_expect((exp) != 0, false))

#define ALIGN_UP(x, n) (((size_t)(x) + (n) - 1) & ~((n) - 1))
#define ALIGN_DOWN(x, n) ((size_t)(x) & -(n))
#define ALIGN_UP_TO_PAGE_SIZE(p) ALIGN_UP(p, SYSTEM_PAGE_SIZE)
#define ALIGN_DOWN_TO_PAGE_SIZE(p) ALIGN_DOWN(p, SYSTEM_PAGE_SIZE)

#define CLZ(x) __builtin_clz(x)

/*
 * If "very verbose" logging is enabled, make it equivalent to ALOGV.
 * Otherwise, make it disappear.
 *
 * Define this above the #include "Dalvik.h" to enable for only a
 * single file.
 */
/* #define VERY_VERBOSE_LOG */
#if defined(VERY_VERBOSE_LOG)
# define LOGVV  ALOGV
# define IF_LOGVV() IF_ALOGV()
#else
# define LOGVV(...) ((void)0)
# define IF_LOGVV() if (false)
#endif


/*
 * These match the definitions in the VM specification.
 */
typedef uint8_t u1;
typedef uint16_tu2;
typedef uint32_tu4;
typedef uint64_tu8;
typedef int8_t  s1;
typedef int16_t s2;
typedef int32_t s4;
typedef int64_t s8;

/*
 * Storage for primitive types and object references.
 *
 * Some parts of the code (notably object field access) assume that values
 * are "left aligned", i.e. given "JValue jv", "jv.i" and "*((s4*)&jv)"
 * yield the same result.  This seems to be guaranteed by gcc on big- and
 * little-endian systems.
 */

#define OFFSETOF_MEMBER(t, f) \
  (reinterpret_cast<char*>(   \
 &reinterpret_cast<t*>(16)->f) -  \
   reinterpret_cast<char*>(16))

#define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0])))

union JValue {
#if defined(HAVE_LITTLE_ENDIAN)
    u1  z;
    s1  b;
    u2  c;
    s2  s;
    s4  i;
    s8  j;
    float   f;
    double  d;
    void* l;
#endif
#if defined(HAVE_BIG_ENDIAN)
    struct {
        u1_z[3];
        u1z;
    };
    struct {
        s1_b[3];
        s1b;
    };
    struct {
        u2_c;
        u2c;
    };
    struct {
        s2_s;
        s2s;
    };
    s4  i;
    s8  j;
    float   f;
    double  d;
    void*   l;
#endif
};

/*
 * Array objects have these additional fields.
 *
 * We don't currently store the size of each element.  Usually it's implied
 * by the instruction.  If necessary, the width can be derived from
 * the first char of obj->clazz->descriptor.
 */
typedef struct   {
   void*clazz;
   u4  lock;
   u4  length;
   u1*  contents;
}ArrayObject ;

#endif  // DALVIK_COMMON_H_

這裏面還有個大小端的問題,不過爲求實驗先經過就先定義死,過了再說。ide

還有個值得一提的結構就是最後面的ArrayObject,這玩意定義在源碼的/dalvik/vm/oo/Object.h 中,本來的定義是這樣的:函數

struct Object {
    ClassObject*clazz;
    u4  lock;
};

struct ArrayObject : Object {
    u4  length;
    u8  contents[1];
};

若是還實實在在的去弄一個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 content
    u1 * 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以後的數據拷貝是從看雪上抄來的,剛開始不求甚解,後來看了源碼中的調用方法就慢慢明白了:

static void Dalvik_dalvik_system_DexFile_openDexFile_bytearray(const u4* args,
JValue* pResult)
{
    ArrayObject* fileContentsObj = (ArrayObject*) args[0];
    u4 length;
    u1* pBytes;
    …
    length = fileContentsObj->length;
    pBytes = (u1*) malloc(length);
    …
    memcpy(pBytes, fileContentsObj->contents, length);
    …
}

Java層

底層代碼基本瞭然,也就是說譯文提供的思路基本實現,剩下其餘加殼的事兒還要本身動腦筋補上。如今java層咱們有一個可使用的以byte數組爲參數的加載dex的接口了:

static native int loadDex(byte[] dex,long dexlen);

要知道咱們花這麼大力氣實現的這個方法,實際意義在於讓源程序的dex數據在內存中傳遞,而不是必須保存在某個地方、以文件的方式。也就是說,咱們須要一個新的 DexClassLoader,去替換在上一篇提到的基礎加殼方案中自定義Application—— ProxyApplication 類,經過反射設置到 android.app.LoadedApk 中 mClassLoder 屬性的那個系統 DexClassLoader,即至少那一段應該改爲這樣:

DynamicDexClassLoder dLoader = new DynamicDexClassLoder(base,srcdata,
   libPath, (ClassLoader) RefInvoke.getFieldOjbect(
 "android.app.LoadedApk", wr.get(), "mClassLoader"),
 getPackageResourcePath(),getDir(".dex", MODE_PRIVATE).getAbsolutePath() );

RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader",
   wr.get(), dLoader);

沒錯,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 });
     }

     @Override
     protected 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;
     }

     @Override
     protected URL findResource(String name) {
               Log.d(TAG, "findResource-" + name);
               return super.findResource(name);
     }

     @Override
     protected Enumeration<URL> findResources(String name) {
               Log.d(TAG, "findResources ssss-" + name);
               return super.findResources(name);
     }

     @Override
     protected synchronized Package getPackage(String name) {
               Log.d(TAG, "getPackage-" + name);
               return super.getPackage(name);
     }

     @Override
     protected 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;
     }

     @Override
     protected Package[] getPackages() {
               Log.d(TAG, "getPackages sss-");
               return super.getPackages();
     }

     @Override
     protected 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沒有查詢仍是未知數,還沒能知足通用性的要求,要須要進一步尋找方案。

相關文章
相關標籤/搜索