本文引自个人博文 APK加壳【2】内存加载dex实现详解php
从上一篇,基础加壳的思路最后得出的结果是方案还不够完善。由于使用的系统DexClassLoader提供的接口必需要求源程序保存在文件系统中,对手一旦过了莱茵河马其诺防线就没啥意义了。因此在前一篇的基础上,又有上面来源方案中的思路,即经过jni调用底层接口,在内存中加载dex文件。步骤以下:java
Dalvik_dalvik_system_DexFile_openDexFile_bytearray
方法指针;Dalvik_dalvik_system_DexFile_openDexFile_bytearray
方法解析Dex数据;方案自己是译文,并且没有介绍细节上的实现。不能像上一篇那样直接copy代码,那就只能老老实实的先搞清楚原理。经过短短的几百字译文,能够总结出一下几点:android
dalvik/vm/native/ dalvik_system_DexFile.cpp
类中,名称是:Dalvik_dalvik_system_DexFile_openDexFile_bytearray
而且只在4.0以上版本开放、4.4又被删掉;虽然从方案分析上看,这个加载实现是有系统版本局限性的,不过经过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层咱们有一个可使用的以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没有查询仍是未知数,还没能知足通用性的要求,要须要进一步寻找方案。