一:代码混淆
Proguard是一个代码优化和混淆工具。能够提供对Java类文件的压缩、优化、混淆,和预校验。压缩的步骤是检测并移除未使用的类、字段、方法和属性。优化的步骤是分析和优化方法的字节码。混淆的步骤是使用短的毫无意义的名称重命名剩余的类、字段和方法。压缩、优化、混淆使得代码更小,更高效。该加密方式只是对工程提供了最小的保护,并不是说不能逆向破解;只是说难度增加,需要耐心。混淆规则很简单,就不在这里详细说明了。
二:Dex文件加密
一个打包好的APK文件源代码是放在dex文件中的,反编译dex文件可以得到源代码。所以我们需要做的是将dex文件进行加密然后生成一个壳dex给系统去加载,那么别人反编译出来也看不到真正的代码内容。
思路:将源APK文件解压得到里面的所有dex文件,然后将这些文件加密,然后生成一个壳类型的dex文件交给系统去加载,这个文件就算被反编译也暴露不了我们真正的代码,然后在这个壳dex文件的代码中先得到加密了的Apk文件,将APK解压到system/appName文件夹下面,这个文件夹需要有root权限才能访问。解密之前所有加密了的dex文件,将解密的文件存到一个数组里面,然后将这些解密后的文件加载到系统中去
1,将dex文件加密
2,生成一个壳dex文件
3,将它打包成一个新的APK,并将新的APK执行对齐,签名操作
开始编写代码
步骤一:
新建一个命名proxy_core的AndroidLibrary的module,主项目引用这个module,并且将主项目的Application设置为module的ProxyApplication类。我们在这个类里面做解密APK操作,并加载到系统。在后面详细分析
步骤二:
新建一个命名proxy_tools的JavaLibrary的module,这个module是一个工具库,我们在这个库里进行APK加密操作。
操作一:加密源apk文件中所有的dex文件
File apkFile=new File("app/build/outputs/apk/debug/app-debug.apk");
File apkTemp=new File("app/build/outputs/apk/debug/temp");
Zip.unZip(apkFile,apkTemp);
//只要dex文件拿出来加密
File[] dexFiles=apkTemp.listFiles(new FilenameFilter() {
@Override
public boolean accept(File file, String s) {
return s.endsWith(".dex");
}
});
//AES加密了
AES.init(AES.DEFAULT_PWD);
for (File dexFile : dexFiles) {
byte[] bytes = Utils.getBytes(dexFile);
byte[] encrypt = AES.encrypt(bytes);
FileOutputStream fos=new FileOutputStream(new File(apkTemp,
"secret-"+dexFile.getName()));
fos.write(encrypt);
fos.flush();
fos.close();
dexFile.delete();
}
操作二:生成壳dex文件。
获取proxy_core壳工程的aar文件:选中proxy_core,点击Build->Make Module 'proxy_core',就会在proxy_core下面的build目录中生成aar文件
解压aar文件,得到里面的classes.jar文件。然后使用androidSDK工具中的dx命令将class或者jar打包成dex文件。
File aarFile=new File("proxy_core/build/outputs/aar/proxy_core-debug.aar");
File aarTemp=new File("proxy_tools/temp");
Zip.unZip(aarFile,aarTemp);
File classesJar=new File(aarTemp,"classes.jar");
File classesDex=new File(aarTemp,"classes.dex");
//dx --dex --output out.dex in.jar
Process process=Runtime.getRuntime().exec("cmd /c dx --dex --output "+classesDex.getAbsolutePath() +" " + classesJar.getAbsolutePath());
process.waitFor();
if(process.exitValue()!=0){
throw new RuntimeException("dex error");
}
PS:dx命令是在androidsdk的buidtools下面任意一个版本里面,比如我的是(D:\android-sdk\build-tools\26.0.3),需要设置环境变量。
操作三:将操作二中生成的classes.dex文件放到第一步的temp文件夹下。
classesDex.renameTo(new File(apkTemp,"classes.dex"));
操作四:将temp文件夹重新压缩成一个APK文件
File unSignedApk=new File("app/build/outputs/apk/debug/app-unsigned.apk");
Zip.zip(apkTemp,unSignedApk);
对新生成的APK进行对齐,然后签名
//通过命令执行对齐
File alignedApk=new File("app/build/outputs/apk/debug/app-unsigned-aligned.apk");
process=Runtime.getRuntime().exec("cmd /c zipalign -v -p 4 "+unSignedApk.getAbsolutePath()
+" "+alignedApk.getAbsolutePath());
process.waitFor();
if(process.exitValue()!=0){
throw new RuntimeException("zipalign error");
}
//通过命令执行签名
File signedApk=new File("app/build/outputs/apk/debug/app-signed-aligned.apk");
//自己创建签名文件
File jks=new File("proxy_tools/proxy2.jks");
process=Runtime.getRuntime().exec("cmd /c apksigner sign --ks "+jks.getAbsolutePath()
+" --ks-key-alias jett --ks-pass pass:123456 --key-pass pass:123456 --out "
+signedApk.getAbsolutePath()+" "+alignedApk.getAbsolutePath());
process.waitFor();
if(process.exitValue()!=0){
throw new RuntimeException("apksigner error");
}
如果runtime执行命令一直等待,可以使用cmd命令行来执行。经过上面的操作我们已经完成了APK加固,下一步就是分析如何解密Apk并且可以正常加载到系统中去。
proxy_core(接上面步骤一)
//代理Applicaiton
public class ProxyApplication extends Application {
//定义好解密后的文件的存放路径
private String app_name;
private String app_version;
/**
* ActivityThread创建Application之后调用的第一个方法
* 可以在这个代理APPlication中进行解密dex,
* 然后再把解密后的dex交给原来的APPlication去加载
* @param base
*/
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
//获取用户填入的metadata
getMetaData();
//得到当前加密了的APK文件
File apkFile = new File(getApplicationInfo().sourceDir);
//把apk解压 app_name+"_"+app_version目录中的内容需要boot权限才能用
File versionDir = getDir(app_name + "_" + app_version, MODE_PRIVATE);
File appDir = new File(versionDir, "app");
File dexDir = new File(appDir, "dexDir");
//得到我们需要加载的Dex文件
List dexFiles = new ArrayList<>();
//进行解密(最好做MD5文件校验)
if (!dexDir.exists() || dexDir.list().length == 0) {
//把apk解压到appDir
Zip.unZip(apkFile, appDir);
//获取目录下所有的文件
File[] files = appDir.listFiles();
for (File file : files) {
String name = file.getName();
//壳dex文件不需要解密
if (name.endsWith(".dex") && !TextUtils.equals(name, "classes.dex")) {
try {
AES.init(AES.DEFAULT_PWD);
//读取文件内容
byte[] bytes = Utils.getBytes(file);
//解密
byte[] decrypt = AES.decrypt(bytes);
//写到指定的目录
FileOutputStream fos = new FileOutputStream(file);
fos.write(decrypt);
fos.flush();
fos.close();
dexFiles.add(file);
} catch (Exception e) {
e.printStackTrace();
}
}
}
} else {
for (File file : dexDir.listFiles()) {
dexFiles.add(file);
}
}
try {
//把解密后的文件加载到系统[见下面loadDex()]
loadDex(dexFiles, versionDir);
} catch (Exception e) {
e.printStackTrace();
}
}
private void getMetaData() {
try {
ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo(
getPackageName(), PackageManager.GET_META_DATA);
Bundle metaData = applicationInfo.metaData;
if (null != metaData) {
if (metaData.containsKey("app_name")) {
app_name = metaData.getString("app_name");
}
if (metaData.containsKey("app_version")) {
app_version = metaData.getString("app_version");
}
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
}
查看源码分析一下Dex文件的加载过程。
Android主要有两个ClassLoader,分别是PathClassLoader,DexClassLoader。PathClassLoader只会加载系统类和已经安装的APK的dex。DexClassLoader支持加载APK、DEX和JAR,也可以从SD卡进行加载。一般我们都是用这个DexClassLoader来作为动态加载的加载器。
package dalvik.system;
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
package dalvik.system;
import java.io.File;
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
他们两个本身没什么好分析的,现在我们来看他们的父类BaseDexClassLoader
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
List suppressedExceptions = new ArrayList();
//调用的是DexPathList的findClass方法
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
[--->DexPathList.java]
public Class findClass(String name, List suppressed) {
//dexElements 从这个数组里面拿出来所有需要加载的dex文件。然后去加载
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
我们来看看dexElements是在哪里初始化的
final class DexPathList {
private Element[] dexElements;
public DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory) {
...
this.definingContext = definingContext;
ArrayList suppressedExceptions = new ArrayList();
// save dexPath for BaseDexClassLoader
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext);
....
}
}
(loadDex)这样我们就知道只需要反射得到数组dexElements,并且将我们需要添加的dex文件加入到这个数组中去就完成了。代码如下
private void loadDex(List dexFiles, File versionDir) throws Exception {
//getClassLoader() 获取的是PathClassLoader对象
//pathListField 是指PathClassLoader的父类BaseDexClassLoader的pathList字段
Field pathListField = Utils.findField(getClassLoader(), "pathList");
//DexPathList类对象
Object pathList = pathListField.get(getClassLoader());
//获取到DexPathList类的dexElements字段
Field dexElementsField = Utils.findField(pathList, "dexElements");
Object[] dexElements = (Object[]) dexElementsField.get(pathList);
//反射得到初始化dexElements的方法
Object[] addElements;
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){//7.0 makeDexElements
Method makeDexElements = Utils.findMethod(pathList, "makeDexElements", List.class, File.class, List.class,ClassLoader.class);
ArrayList suppressedExceptions = new ArrayList();
addElements = (Object[]) makeDexElements.invoke(pathList,dexFiles,versionDir,suppressedExceptions,getClassLoader());
}else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){//6.0 makePathElements
Method makeDexElements = Utils.findMethod(pathList, "makePathElements", List.class, File.class, List.class);
ArrayList suppressedExceptions = new ArrayList();
addElements = (Object[]) makeDexElements.invoke(pathList, dexFiles, versionDir, suppressedExceptions);
}else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){//5.0 makeDexElements
Method makeDexElements = Utils.findMethod(pathList, "makeDexElements", ArrayList.class, File.class, ArrayList.class);
ArrayList suppressedExceptions = new ArrayList();
addElements = (Object[]) makeDexElements.invoke(pathList, dexFiles, versionDir, suppressedExceptions);
}else{
return;
}
//合并数组
Object[] newElements = (Object[]) Array.newInstance(dexElements.getClass().getComponentType(), dexElements.length + addElements.length);
System.arraycopy(dexElements, 0, newElements, 0, dexElements.length);
System.arraycopy(addElements, 0, newElements, dexElements.length, addElements.length);
//替换classloader中的element数组
dexElementsField.set(pathList, newElements);
}
至此,我们就完成了APK的解密操作,并且也能够正常加载APK应用了。但是我们发现了一个新的问题,那就是我们只能在ProxyApplication这个应用入口做各种初始化操作,不能在主项目中定义我们自己的MyApplicaiton来进行各种初始化工作。所以我们需要想办法将我们主项目的MyApplicaiton替换成真正的applicaiton入口。我在通过源码分析Activity和Applicaiton的启动流程(7.0源码)中分析过了APPlication的加载过程,所以我们可以从源码入手,分析想要替换成真正的MyApplication需要做哪些操作。
源码分析(结合通过源码分析Activity和Applicaiton的启动流程(7.0源码))
[--->ActivityThread.java]
private void handleBindApplication(AppBindData data) {
...
Application app = data.info.makeApplication(data.restrictedBackupMode, null);
[第六个要替换的]
mInitialApplication = app;
...
}
[--->ActivityThread.java]
public Application makeApplication(boolean forceDefaultAppClass,
Instrumentation instrumentation) {
Application app = null;
[第一个要替换的]
String appClass = mApplicationInfo.className;
if (forceDefaultAppClass || (appClass == null)) {
appClass = "android.app.Application";
}
...
java.lang.ClassLoader cl = getClassLoader();
...
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
[第二个要替换的]
appContext.setOuterContext(app);
...
[第四个要替换的]
mActivityThread.mAllApplications.add(app);
[第五个要替换的]
mApplication = app;
...
return app;
}
[--->ContextImpl.java ]
static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
return new ContextImpl(null, mainThread,
packageInfo, null, null, 0, null, null, Display.INVALID_DISPLAY);
}
private ContextImpl(ContextImpl container, ActivityThread mainThread,
LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags,
Display display, Configuration overrideConfiguration, int createDisplayWithId) {
//ContextImpl类对象
mOuterContext = this;
...
//ActivityThread类对象
mMainThread = mainThread;
mActivityToken = activityToken;
mFlags = flags;
if (user == null) {
user = Process.myUserHandle();
}
mUser = user;
//LoadedApk类对象
mPackageInfo = packageInfo;
...
}
[--->Instrumentation.java ]
static public Application newApplication(Class> clazz, Context context)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
Application app = (Application)clazz.newInstance();
[第三个要替换的]
app.attach(context);
return app;
}
[--->Application.java]
/**
* @hide
*/
/* package */ final void attach(Context context) {
attachBaseContext(context);
mLoadedApk = ContextImpl.getImpl(context).mPackageInfo;
}
第一个替换的位置:String appClass = mApplicationInfo.className;
//反射得到 LoadedApk.mApplicationInfo字段
Class> loadedApkClass = Class.forName("android.app.LoadedApk");
Field mApplicationInfoField = loadedApkClass.getDeclaredField("mApplicationInfo");
mApplicationInfoField.setAccessible(true);
//LoadedApk实体类对象从通过反射ContextImpl.mPackageInfo得到
Class> contextImplClass = Class.forName("android.app.ContextImpl");
Field mPackageInfoField = contextImplClass.getDeclaredField("mPackageInfo");
mPackageInfoField.setAccessible(true);
Object mPackageInfo = mPackageInfoField.get(baseContext);
ApplicationInfo mApplicationInfo = (ApplicationInfo) mApplicationInfoField.get(mPackageInfo);
mApplicationInfo.className = app_name;
第二个需要替换的位置:appContext.setOuterContext(app)
Field mOuterContext = contextImplClass.getDeclaredField
("mOuterContext");
mOuterContext.setAccessible(true);
mOuterContext.set(baseContext, application);
第三个需要替换的位置:app.attach(context);
//创建用户真实的application(MyApplication)
Class> delegateClass = Class.forName(app_name);
application = (Application) delegateClass.newInstance();
//调用Application.attach(context)方法
Method declaredMethod = Application.class.getDeclaredMethod("attach", Context.class);
//设置可用
declaredMethod.setAccessible(true);
//得到Application.attach(Context context)传入的context对象
Context baseContext = getBaseContext();
declaredMethod.invoke(application,baseContext);
第四个需要替换的位置: mActivityThread.mAllApplications.add(app);
// ActivityThread类对象通过反射ContextImpl.mMainThread得到
Field mMainThreadField = contextImplClass.getDeclaredField("mMainThread");
mMainThreadField.setAccessible(true);
//得到的是ActivityThread类对象
Object mMainThread = mMainThreadField.get(baseContext);
Class> activityThreadClass = Class.forName("android.app.ActivityThread");
Field mAllApplicationsField = activityThreadClass.getDeclaredField("mAllApplications");
mAllApplicationsField.setAccessible(true);
ArrayList mAllApplications =(ArrayList) mAllApplicationsField.get(mMainThread);
// mAllApplications.remove(this);
mAllApplications.add(application);
第五个需要替换的位置:mApplication = app;
Field mApplicationField = loadedApkClass.getDeclaredField("mApplication");
mApplicationField.setAccessible(true);
mApplicationField.set(mPackageInfo, application);
第六个需要替换的位置:mInitialApplication = app;
Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
mInitialApplicationField.setAccessible(true);
mInitialApplicationField.set(mMainThread, application);
开始替换
/**
* 开始替换APPlication,加载真正应用的Application
*/
@Override
public void onCreate() {
super.onCreate();
try {
bindRealApplicatin();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 这个方法主要作用是创建其他程序的Context,
* 通过这个Context可以访问该软件包的资源,甚至可以执行其他软件包的代码。
* 这个代码不写的话,主项目中ContentProvider使用的context就没办法换回来,还是用的ProxyApplication
* @param packageName
* @param flags
* @return
* @throws PackageManager.NameNotFoundException
*/
@Override
public Context createPackageContext(String packageName, int flags) throws PackageManager.NameNotFoundException {
if (TextUtils.isEmpty(app_name)) {
return super.createPackageContext(packageName, flags);
}
try {
bindRealApplicatin();
} catch (Exception e) {
e.printStackTrace();
}
return application;
}
private void bindRealApplicatin() throws Exception {
if (isBindReal) {
return;
}
if (TextUtils.isEmpty(app_name)) {
return;
}
//创建用户真实的application(MyApplication)
Class> delegateClass = Class.forName(app_name);
application = (Application) delegateClass.newInstance();
//调用Application.attach(context)方法
Method declaredMethod = Application.class.getDeclaredMethod("attach", Context.class);
//设置可用
declaredMethod.setAccessible(true);
//得到Application.attach(Context context)传入的context对象
Context baseContext = getBaseContext();
declaredMethod.invoke(application, baseContext);
//第一处: String appClass = mApplicationInfo.className;
Class> loadedApkClass = Class.forName("android.app.LoadedApk");
Field mApplicationInfoField = loadedApkClass.getDeclaredField("mApplicationInfo");
mApplicationInfoField.setAccessible(true);
//LoadedApk实体类对象从通过反射ContextImpl.mPackageInfo得到
Class> contextImplClass = Class.forName("android.app.ContextImpl");
Field mPackageInfoField = contextImplClass.getDeclaredField("mPackageInfo");
mPackageInfoField.setAccessible(true);
Object mPackageInfo = mPackageInfoField.get(baseContext);
ApplicationInfo mApplicationInfo = (ApplicationInfo) mApplicationInfoField.get(mPackageInfo);
mApplicationInfo.className = app_name;
//第二处:appContext.setOuterContext(app);
Field mOuterContext = contextImplClass.getDeclaredField("mOuterContext");
mOuterContext.setAccessible(true);
mOuterContext.set(baseContext, application);
//第三处:mActivityThread.mAllApplications.add(app);
// ActivityThread类对象通过反射ContextImpl.mMainThread得到
Field mMainThreadField = contextImplClass.getDeclaredField("mMainThread");
mMainThreadField.setAccessible(true);
//得到的是ActivityThread类对象
Object mMainThread = mMainThreadField.get(baseContext);
Class> activityThreadClass = Class.forName("android.app.ActivityThread");
Field mAllApplicationsField = activityThreadClass.getDeclaredField("mAllApplications");
mAllApplicationsField.setAccessible(true);
ArrayList mAllApplications = (ArrayList) mAllApplicationsField.get(mMainThread);
// mAllApplications.remove(this);
mAllApplications.add(application);
//第四处:mApplication = app;
Field mApplicationField = loadedApkClass.getDeclaredField("mApplication");
mApplicationField.setAccessible(true);
mApplicationField.set(mPackageInfo, application);
//第五处:mInitialApplication = app;
Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
mInitialApplicationField.setAccessible(true);
mInitialApplicationField.set(mMainThread, application);
//调用Application的oncreat方法
application.onCreate();
isBindReal = true;
}
附上项目代码
[https://github.com/games2sven/ReinForceApk]