想要完成的功能:我想简单的对一个apk加壳,让别人不容易破解原本的apk

第一步:动态加载dex文件,但是odex生成以后要存放在本地(可以在加载完成后立即删除),资源文件不做保护

=================================2015-6-1更新=============================

以前的动态加载dex的方法在有的手机(比如emui3.0的android4.4系统)上不能兼容,会出现空指针或者其他不可预料的异常,是由于反射某个变量没有拿到导致的,虽然可以通过判断的方式规避,但毕竟不是好方法,也不能保证以后别的机子不会出现问题。同时因为是将系统的dexclassloader替换掉,所以so库等文件只能放在assets文件夹下,在开始时copy过去。

今天发现google提供了一个加载分割dex的jar包android-support-multidex.jar,这个jar包是用于一个apk分dex包用的,用于解决应用方法总数超过65k导致不能编译运行的问题。

借助这个jar包的部分源码,可以解决在sdk4版本以上加载dex的问题,不会出现兼容的问题(我只在几部测试机上运行了)。

就是通过反射将第二个dex文件放入第一个dexclassloader的Element数组中,这样的好处是以前加载到内存的dexclassloader中存在so库等库文件,这样就不用copy了。同时,我在想,如果第二个dex不需要程序运行时启动的话,甚至可以在某个后台线程中加载,而在前台页面显示等待框,不过这样就不能叫加壳了。

使用的方法和以前是差不多的,资源res文件和manifest的更改不变,只是不需要copy so库等文件,可以直接放在lib目录下使用。


不过没有解决的问题是在最后一个步骤执行以前的application的onCreate生命周期时,还是要用以前的代码,以前的代码存在兼容问题,在注释里面写了,只能简单的判空跳过了,待解决。


代码:


public class App2 extends Application {

	public void loge(String msg) {
		Log.e("壳App", msg);
	}
	
	@Override
	public void onCreate() {
		// super.onCreate();
		try {
			final String applicationName = "com.test.App";
			
			final File cacheDir = getCacheDir();
			final File fileDir = getFilesDir();
			final File dexFile = new File(cacheDir, "classes.dex");
			final File odexFile = new File(fileDir, "classes.dex");

			// copy assets's dex file to dexFile's path
			long space_time;
			long time = System.currentTimeMillis();
			{
				InputStream is = getAssets().open("classes.dex");
				FileOutputStream fos = new FileOutputStream(dexFile);
				byte[] buffer = new byte[1024];
				int len;
				while ((len = is.read(buffer)) != -1) {
					fos.write(buffer, 0, len);
				}
				fos.flush();
				fos.close();
				is.close();
			}
			space_time = System.currentTimeMillis() - time;
			loge("copy assets's dex file to dexFile's path end,time=" + space_time);

			// load dex file by dexclassloader
			// 这样操作有个好处,可以不用加libpath,因为先前的lib目录已经将so库等加载进去了
			time = System.currentTimeMillis();
			{
				List<File> dexFileList = new ArrayList<File>();
				dexFileList.add(dexFile);
				// 这个方法是从android-support-multidex.jar的MultiDex类中找到的
				installSecondaryDexes(getClassLoader(), fileDir, dexFileList);
			}
			space_time = System.currentTimeMillis() - time;
			loge("load dex file by dexclassloader,time=" + space_time);
			
			// 想办法执行以前的application的onCreate生命周期
			time = System.currentTimeMillis();
			{
				//Log.e("app", "执行原来app的Application" + System.currentTimeMillis());
				// 这里执行原来app的Application的onCreate流程
				Object currentActivityThread = RefInvoke.invokeStaticMethod(
						"android.app.ActivityThread", "currentActivityThread",
						new Class[] {}, new Object[] {});
				Object mBoundApplication = RefInvoke.getFieldOjbect(
						"android.app.ActivityThread", currentActivityThread,
						"mBoundApplication");
				Object loadedApkInfo = RefInvoke.getFieldOjbect(
						"android.app.ActivityThread$AppBindData", mBoundApplication,
						"info");
				RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication",
						loadedApkInfo, null);
				Object oldApplication = RefInvoke.getFieldOjbect(
						"android.app.ActivityThread", currentActivityThread,
						"mInitialApplication");
				ArrayList<Application> mAllApplications = (ArrayList<Application>) RefInvoke
						.getFieldOjbect("android.app.ActivityThread",
								currentActivityThread, "mAllApplications");
				mAllApplications.remove(oldApplication);
				ApplicationInfo appinfo_In_LoadedApk = (ApplicationInfo) RefInvoke
						.getFieldOjbect("android.app.LoadedApk", loadedApkInfo,
								"mApplicationInfo");
				ApplicationInfo appinfo_In_AppBindData = (ApplicationInfo) RefInvoke
						.getFieldOjbect("android.app.ActivityThread$AppBindData",
								mBoundApplication, "appInfo");
				appinfo_In_LoadedApk.className = applicationName;
				appinfo_In_AppBindData.className = applicationName;
				Application app = (Application) RefInvoke.invokeMethod(
						"android.app.LoadedApk", "makeApplication", loadedApkInfo,
						new Class[] { boolean.class, Instrumentation.class },
						new Object[] { false, null });
				RefInvoke.setFieldOjbect("android.app.ActivityThread",
						"mInitialApplication", currentActivityThread, app);

				Map mProviderMap = (Map) RefInvoke.getFieldOjbect(
						"android.app.ActivityThread", currentActivityThread,
						"mProviderMap");
				Iterator it = mProviderMap.values().iterator();
				while (it.hasNext()) {
					Object providerClientRecord = it.next();
					Object localProvider = RefInvoke.getFieldOjbect(
							"android.app.ActivityThread$ProviderClientRecord",
							providerClientRecord, "mLocalProvider");
					// 这个代码获取的provider在我的4.4系统的手机是null
					if(localProvider != null) {
						RefInvoke.setFieldOjbect("android.content.ContentProvider",
								"mContext", localProvider, app);
					}
				}
				app.onCreate();
				//Log.e("app", "执行原来app的Application end" + System.currentTimeMillis());
			}
			space_time = System.currentTimeMillis() - time;
			loge("想办法执行以前的application的onCreate生命周期,time=" + space_time);
			
			// delete dex and odex file
			time = System.currentTimeMillis();
			{
				if(dexFile.exists()) {
					dexFile.delete();
				}
				if(odexFile.exists()) {
					odexFile.delete();
				}
			}
			space_time = System.currentTimeMillis() - time;
			loge("delete dex and odex file,time=" + space_time);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private static void installSecondaryDexes(ClassLoader loader, File dexDir,
			List<File> files) throws IllegalArgumentException,
			IllegalAccessException, NoSuchFieldException,
			InvocationTargetException, NoSuchMethodException, IOException {
		if (!files.isEmpty()) {
			if (Build.VERSION.SDK_INT >= 19) {
				V19.install(loader, files, dexDir);
			} else if (Build.VERSION.SDK_INT >= 14) {
				V14.install(loader, files, dexDir);
			} else {
				V4.install(loader, files);
			}
		}
	}
	
	private static Field findField(Object instance, String name)
			throws NoSuchFieldException {
		for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz
				.getSuperclass()) {
			try {
				Field field = clazz.getDeclaredField(name);
				if (!field.isAccessible()) {
					field.setAccessible(true);
				}
				return field;
			} catch (NoSuchFieldException e) {
			}
		}
		throw new NoSuchFieldException("Field " + name + " not found in "
				+ instance.getClass());
	}

	private static Method findMethod(Object instance, String name,
			Class<?>... parameterTypes) throws NoSuchMethodException {
		for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz
				.getSuperclass()) {
			try {
				Method method = clazz.getDeclaredMethod(name, parameterTypes);
				if (!method.isAccessible()) {
					method.setAccessible(true);
				}
				return method;
			} catch (NoSuchMethodException e) {
			}
		}
		throw new NoSuchMethodException("Method " + name + " with parameters "
				+ Arrays.asList(parameterTypes) + " not found in "
				+ instance.getClass());
	}

	private static void expandFieldArray(Object instance, String fieldName,
			Object[] extraElements) throws NoSuchFieldException,
			IllegalArgumentException, IllegalAccessException {
		Field jlrField = findField(instance, fieldName);
		Object[] original = (Object[]) jlrField.get(instance);
		Object[] combined = (Object[]) Array.newInstance(original.getClass()
				.getComponentType(), original.length + extraElements.length);

		System.arraycopy(original, 0, combined, 0, original.length);
		System.arraycopy(extraElements, 0, combined, original.length,
				extraElements.length);
		jlrField.set(instance, combined);
	}

	private static final class V19 {
		private static void install(ClassLoader loader,
				List<File> additionalClassPathEntries, File optimizedDirectory)
				throws IllegalArgumentException, IllegalAccessException,
				NoSuchFieldException, InvocationTargetException,
				NoSuchMethodException {
			Field pathListField = findField(loader, "pathList");
			Object dexPathList = pathListField.get(loader);
			ArrayList<IOException> suppressedExceptions = new ArrayList();
			expandFieldArray(
					dexPathList,
					"dexElements",
					makeDexElements(dexPathList, new ArrayList(
							additionalClassPathEntries), optimizedDirectory,
							suppressedExceptions));
			if (suppressedExceptions.size() > 0) {
				for (IOException e : suppressedExceptions) {
					Log.w("MultiDex", "Exception in makeDexElement", e);
				}
				Field suppressedExceptionsField = findField(loader,
						"dexElementsSuppressedExceptions");

				IOException[] dexElementsSuppressedExceptions = (IOException[]) suppressedExceptionsField
						.get(loader);
				if (dexElementsSuppressedExceptions == null) {
					dexElementsSuppressedExceptions = (IOException[]) suppressedExceptions
							.toArray(new IOException[suppressedExceptions
									.size()]);
				} else {
					IOException[] combined = new IOException[suppressedExceptions
							.size() + dexElementsSuppressedExceptions.length];

					suppressedExceptions.toArray(combined);
					System.arraycopy(dexElementsSuppressedExceptions, 0,
							combined, suppressedExceptions.size(),
							dexElementsSuppressedExceptions.length);

					dexElementsSuppressedExceptions = combined;
				}
				suppressedExceptionsField.set(loader,
						dexElementsSuppressedExceptions);
			}
		}

		private static Object[] makeDexElements(Object dexPathList,
				ArrayList<File> files, File optimizedDirectory,
				ArrayList<IOException> suppressedExceptions)
				throws IllegalAccessException, InvocationTargetException,
				NoSuchMethodException {
			Method makeDexElements = findMethod(
					dexPathList,
					"makeDexElements",
					new Class[] { ArrayList.class, File.class, ArrayList.class });

			return (Object[]) makeDexElements.invoke(dexPathList, new Object[] {
					files, optimizedDirectory, suppressedExceptions });
		}
	}

	private static final class V14 {
		private static void install(ClassLoader loader,
				List<File> additionalClassPathEntries, File optimizedDirectory)
				throws IllegalArgumentException, IllegalAccessException,
				NoSuchFieldException, InvocationTargetException,
				NoSuchMethodException {
			Field pathListField = findField(loader, "pathList");
			Object dexPathList = pathListField.get(loader);
			expandFieldArray(
					dexPathList,
					"dexElements",
					makeDexElements(dexPathList, new ArrayList(
							additionalClassPathEntries), optimizedDirectory));
		}

		private static Object[] makeDexElements(Object dexPathList,
				ArrayList<File> files, File optimizedDirectory)
				throws IllegalAccessException, InvocationTargetException,
				NoSuchMethodException {
			Method makeDexElements = findMethod(dexPathList, "makeDexElements",
					new Class[] { ArrayList.class, File.class });

			return (Object[]) makeDexElements.invoke(dexPathList, new Object[] {
					files, optimizedDirectory });
		}
	}

	private static final class V4 {
		private static void install(ClassLoader loader,
				List<File> additionalClassPathEntries)
				throws IllegalArgumentException, IllegalAccessException,
				NoSuchFieldException, IOException {
			int extraSize = additionalClassPathEntries.size();

			Field pathField = findField(loader, "path");

			StringBuilder path = new StringBuilder(
					(String) pathField.get(loader));
			String[] extraPaths = new String[extraSize];
			File[] extraFiles = new File[extraSize];
			ZipFile[] extraZips = new ZipFile[extraSize];
			DexFile[] extraDexs = new DexFile[extraSize];
			ListIterator<File> iterator = additionalClassPathEntries
					.listIterator();
			while (iterator.hasNext()) {
				File additionalEntry = (File) iterator.next();
				String entryPath = additionalEntry.getAbsolutePath();
				path.append(':').append(entryPath);
				int index = iterator.previousIndex();
				extraPaths[index] = entryPath;
				extraFiles[index] = additionalEntry;
				extraZips[index] = new ZipFile(additionalEntry);
				extraDexs[index] = DexFile.loadDex(entryPath, entryPath
						+ ".dex", 0);
			}
			pathField.set(loader, path.toString());
			expandFieldArray(loader, "mPaths", extraPaths);
			expandFieldArray(loader, "mFiles", extraFiles);
			expandFieldArray(loader, "mZips", extraZips);
			expandFieldArray(loader, "mDexs", extraDexs);
		}
	}
}



这次加壳的dex和上次大小基本相同,执行完的打印结果:


06-01 15:36:29.272: E/壳App(31045): copy assets's dex file to dexFile's path end,time=193
06-01 15:36:30.607: E/壳App(31045): load dex file by dexclassloader,time=1334
06-01 15:36:30.682: E/壳App(31045): 想办法执行以前的application的onCreate声明周期,time=75
06-01 15:36:30.685: E/壳App(31045): delete dex and odex file,time=3

这里没有做dex的加解密操作


因为我现在运行以前的代码总是莫名的崩掉,没有再调试,所以没有做对比测试,和以前的数据的数据不具有对比性,只能说看着好像快了一点。


=================================2015-6-1更新=============================


1、首先使用apktool反编译apk拿到资源文件,解压拿到class.dex文件

2、新建项目,包名和原apk包名相同,将反编译的资源和manifest文件copy到项目中,class.dex文件copy到项目assets目录下

3、如果原apk包含其他资源比如assets下的文件等,直接复制过来

4、修改manifest的Application name为自己项目新建的App name,后面使用反射的方式调用到之前的Application

5、如果项目存在so库的话,直接放在lib下是不会被找到的,只能讲so放在assets下,运行时将so复制到项目目录,将路径放在DexClassLoader构造的第三个参数就可以

6、项目App代码:

public class App extends Application {

	@Override
	protected void attachBaseContext(Context base) {
		// 在这个方法里面是不能读到assets的文件
		super.attachBaseContext(base);
	}

	@Override
	public void onCreate() {
		// 这里替换的dex文件时没有考虑原来App中的Application的
		try {
			// classes.dex先存放在sdcard上面
			Log.e("app", "copy文件" + System.currentTimeMillis());
			String path = Environment.getExternalStorageDirectory() + "/";
			String filename = "classes.dex";
			System.out.println(path + filename);
			InputStream is = getAssets().open("classes.dex");
			FileOutputStream fos = new FileOutputStream(new File(path
					+ filename));
			byte[] buffer = new byte[1024];
			int len;
			while ((len = is.read(buffer)) != -1) {
				fos.write(buffer, 0, len);
			}
			fos.flush();
			fos.close();
			is.close();
			Log.e("app", "copy文件end" + System.currentTimeMillis());
			String odexPath = "/data/data/com.example.test";

			Log.e("app", "配置动态加载环境" + System.currentTimeMillis());
			// 配置动态加载环境
			Object currentActivityThread = RefInvoke.invokeStaticMethod(
					"android.app.ActivityThread", "currentActivityThread",
					new Class[] {}, new Object[] {});
			String packageName = "com.example.test";
			Map mPackages = (Map) RefInvoke.getFieldOjbect(
					"android.app.ActivityThread", currentActivityThread,
					"mPackages");
			WeakReference wr = (WeakReference) mPackages.get(packageName);
			// 这里应该主要是new一个DexClassLoader,传dex路径,临时的odex路径,lib路径(原来项目的lib文件和so文件),
			// 还有一个是ClassLoader
			DexClassLoader dLoader = new DexClassLoader(path + filename,
					odexPath, null, (ClassLoader) RefInvoke.getFieldOjbect(
							"android.app.LoadedApk", wr.get(), "mClassLoader"));
			RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader",
					wr.get(), dLoader);
			Log.e("app", "配置动态加载环境end" + System.currentTimeMillis());
			Log.e("app", "替换系统dexclassloader 完成");
		} catch (Exception e) {
			Log.e("app", "替换系统dexclassloader 出现异常", e);
		}
		
		Log.e("app", "执行原来app的Application" + System.currentTimeMillis());
		// 这里执行原来app的Application的onCreate流程
		String appClassName = "com.example.test.App";
		Object currentActivityThread = RefInvoke.invokeStaticMethod(
				"android.app.ActivityThread", "currentActivityThread",
				new Class[] {}, new Object[] {});
		Object mBoundApplication = RefInvoke.getFieldOjbect(
				"android.app.ActivityThread", currentActivityThread,
				"mBoundApplication");
		Object loadedApkInfo = RefInvoke.getFieldOjbect(
				"android.app.ActivityThread$AppBindData", mBoundApplication,
				"info");
		RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication",
				loadedApkInfo, null);
		Object oldApplication = RefInvoke.getFieldOjbect(
				"android.app.ActivityThread", currentActivityThread,
				"mInitialApplication");
		ArrayList<Application> mAllApplications = (ArrayList<Application>) RefInvoke
				.getFieldOjbect("android.app.ActivityThread",
						currentActivityThread, "mAllApplications");
		mAllApplications.remove(oldApplication);
		ApplicationInfo appinfo_In_LoadedApk = (ApplicationInfo) RefInvoke
				.getFieldOjbect("android.app.LoadedApk", loadedApkInfo,
						"mApplicationInfo");
		ApplicationInfo appinfo_In_AppBindData = (ApplicationInfo) RefInvoke
				.getFieldOjbect("android.app.ActivityThread$AppBindData",
						mBoundApplication, "appInfo");
		appinfo_In_LoadedApk.className = appClassName;
		appinfo_In_AppBindData.className = appClassName;
		Application app = (Application) RefInvoke.invokeMethod(
				"android.app.LoadedApk", "makeApplication", loadedApkInfo,
				new Class[] { boolean.class, Instrumentation.class },
				new Object[] { false, null });
		RefInvoke.setFieldOjbect("android.app.ActivityThread",
				"mInitialApplication", currentActivityThread, app);

		Map mProviderMap = (Map) RefInvoke.getFieldOjbect(
				"android.app.ActivityThread", currentActivityThread,
				"mProviderMap");
		Iterator it = mProviderMap.values().iterator();
		while (it.hasNext()) {
			Object providerClientRecord = it.next();
			Object localProvider = RefInvoke.getFieldOjbect(
					"android.app.ActivityThread$ProviderClientRecord",
					providerClientRecord, "mLocalProvider");
			RefInvoke.setFieldOjbect("android.content.ContentProvider",
					"mContext", localProvider, app);
		}
		app.onCreate();
		Log.e("app", "执行原来app的Application end" + System.currentTimeMillis());
		// 在原来文章中没有执行super的onCreate方法
		// 我思考应该是上面的app调用了onCreate方法,这个方法会调用super的,所以这里不调用了
		// super.onCreate();
	}
}

结果:

可以正常的运行

开始执行时有一段时间延迟的,copy dex文件大概需要400ms,使用反射的方式加载动态执行环境(即将系统的dexclassloader替换为自己的)时间大概是3800ms,替换为原来的Application时间是8ms。

可以在DexClassLoader生成以后立即删除dex和odex文件,可以正常运行。

弊端:

1、使用的话肯定是要将dex文件加密存储的,但是最后还是要copy到文件中以便DexClassLoader加载,那加密和不加密就没什么区别

2、加载过程生成的odex文件也要存放在文件中,root以后也可以直接拿到

3、每次第一次运行都会有很大的延迟


这样保护只要稍微用心就可以轻松拿到以前的dex文件,反编译获取源码。


第二步:想办法在内存中直接加载odex文件,这个odex文件时DexClassLoader加载dex文件的附加产物,应该可以直接加载odex文件,这样就可以不在文件系统存放临时odex文件

待续。。。