想要完成的功能:我想简单的对一个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文件
待续。。。