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文件。步骤如下:

  1. 获取Dalvik_dalvik_system_DexFile_openDexFile_bytearray方法指针;
  2. 调用Dalvik_dalvik_system_DexFile_openDexFile_bytearray方法解析Dex数据;
  3. 实现JAVA层Dex ClassLoader完成类的加载;

方案本身是译文,而且没有介绍细节上的实现。不能像上一篇那样直接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,搜刮了一下所有需要定义的类型之后,这个文件基本上是这个样子的:

#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_

这里面还有个大小端的问题,不过为求实验先通过就先定义死,过了再说。
还有个值得一提的结构就是最后面的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没有查询还是未知数,还没能满足通用性的要求,要需要进一步寻找方案。

感谢您赏个荷包蛋~