前言:上一篇博客已经把Apk加固的思路详细的介绍过了,也开始创建了一个空的demo进行,然后在项目中添加一个代理module(解密,和系统源码交互功能)和tools工具加密Java library 的module ,这里开始接着把整个过程用代码操作一遍,希望对大家有所帮助。
代码用到的工具类请移步:https://download.csdn.net/download/i123456789t/11239056
1、代码中需要用到几个类,AES加解密类,Zip压缩解压类等工具类
首先我先proxy_core代理module下写一个代理application ,然后继承至Application,代码目录结构请看:
接着把我们这个代理的application加到我们最常写的配置文件中AndroidManifest.xml 中,我们是不是每个App都有一个application,然后把它配置到AndroidManifest.xml中,这里唯一不同的是,不是把我们项目中的那个application写到AndroidManifest.xml中,而是把我们在代理的写上。然后把我们app自己用到的application也加上,自己的application写在meta-data中,另一个meta-data按照下面的写就行,写法和位置如下
这个是我们自己项目用到的初始化application,上面的代理只是处理代理操作的。
我们自己的MyApplication里面目前啥也没写,这个使我们项目中用于初始化的,这里先不写东西。
这里开始写代理了,在ProxyApplication 中:
package com.example.proxy_core;
import android.app.Application;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.text.TextUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
public class ProxyApplication extends Application {
//定义好的加密后的文件的存放路径
private String app_name;
private String app_version;
/**
* ActivityThread创建Application之后调用的第一个方法
* 可以在这个方法中进行解密,同时把dex交给Android去加载
* @param base
*/
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
//获取用户填入的metaData
getMetaData();
//得到当前apk文件
File apkFile = new File(getApplicationInfo().sourceDir);
//把apk解压 这个目录中的内容需要root权限才能使用
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();
if (name.endsWith(".dex") && !TextUtils.equals(name,"classes.dex")){
try{
AES.init(AES.DEFAULT_PWD);
//读取文件内容
byte[] bytes = Utils.getBytes(file);
//解密
byte[] decypt = AES.decrypt(bytes);
//写到指定的目录
FileOutputStream fos = new FileOutputStream(file);
fos.write(decypt);
fos.flush();
fos.close();
dexFiles.add(file);
}catch (Exception e){
e.printStackTrace();
}
}
}
}else {
for (File file:dexDir.listFiles()){
dexFiles.add(file);
}
}
try {
loadDex(dexFiles,versionDir);
}catch (Exception e){
e.printStackTrace();
}
}
private void loadDex(List dexFiles,File versionDir) throws Exception{
//1、获取pathList
Field pathListField = Utils.findField(getClassLoader(), "pathList");
Object pathList = pathListField.get(getClassLoader());
//2、获取数组dexElements
Field dexElementsField = Utils.findField(pathList,"dexElements");
Object[] dexElements = (Object[]) dexElementsField.get(pathList);
//3、反射到初始化makePathElements的方法
Method makeDexElements = Utils.findMethod(pathList,"makePathElements",List.class,File.class,List.class);
ArrayList suppressedException = new ArrayList<>();
Object[] addElements = (Object[]) makeDexElements.invoke(pathList, dexFiles, versionDir, suppressedException);
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);
}
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 (Exception e){
e.printStackTrace();
}
}
/**
* 开始替换application
*/
@Override
public void onCreate() {
super.onCreate();
try {
bindRealApplication();
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 让代码走入if的第三段中
* @return
*/
@Override
public String getPackageName() {
if (!TextUtils.isEmpty(app_name)){
return "";
}
return super.getPackageName();
}
@Override
public Context createPackageContext(String packageName, int flags) throws PackageManager.NameNotFoundException {
if (TextUtils.isEmpty(app_name)){
return super.createPackageContext(packageName, flags);
}
try {
bindRealApplication();
} catch (Exception e) {
e.printStackTrace();
}
return delegate;
}
boolean isBindReal;
Application delegate;
//下面主要是通过反射系统源码的内容,然后进行处理,把我们的内容加进去处理
private void bindRealApplication() throws Exception{
if (isBindReal){
return;
}
if (TextUtils.isEmpty(app_name)){
return;
}
//得到attchBaseContext(context) 传入的上下文 ContextImpl
Context baseContext = getBaseContext();
//创建用户真实的application (MyApplication)
Class> delegateClass = null;
delegateClass = Class.forName(app_name);
delegate = (Application) delegateClass.newInstance();
//得到attch()方法
Method attach = Application.class.getDeclaredMethod("attach",Context.class);
attach.setAccessible(true);
attach.invoke(delegate,baseContext);
//获取ContextImpl ----> ,mOuterContext(app); 通过Application的attachBaseContext回调参数获取
Class> contextImplClass = Class.forName("android.app.ContextImpl");
//获取mOuterContext属性
Field mOuterContextField = contextImplClass.getDeclaredField("mOuterContext");
mOuterContextField.setAccessible(true);
mOuterContextField.set(baseContext,delegate);
//ActivityThread ----> mAllApplication(ArrayList) ContextImpl的mMainThread属性
Field mMainThreadField = contextImplClass.getDeclaredField("mMainThread");
mMainThreadField.setAccessible(true);
Object mMainThread = mMainThreadField.get(baseContext);
//ActivityThread -----> mInitialApplication ContextImpl的mMainThread属性
Class> activityThreadClass = Class.forName("android.app.ActivityThread");
Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
mInitialApplicationField.setAccessible(true);
mInitialApplicationField.set(mMainThread,delegate);
//ActivityThread ------> mAllApplications(ArrayList) ContextImpl的mMainThread属性
Field mAllApplicationsField = activityThreadClass.getDeclaredField("mAllApplications");
mAllApplicationsField.setAccessible(true);
ArrayList mApplications = (ArrayList) mAllApplicationsField.get(mMainThread);
mApplications.remove(this);
mApplications.add(delegate);
//LoadedApk -----> mApplicaion ContextImpl的mPackageInfo属性
Field mPackageInfoField = contextImplClass.getDeclaredField("mPackageInfo");
mPackageInfoField.setAccessible(true);
Object mPackageInfo = mPackageInfoField.get(baseContext);
Class> loadedApkClass = Class.forName("android.app.LoadedApk");
Field mApplicationField = loadedApkClass.getDeclaredField("mApplication");
mApplicationField.setAccessible(true);
mApplicationField.set(mPackageInfo,delegate);
//修改ApplicationInfo className LoadedApk
Field mApplicationInfoField = loadedApkClass.getDeclaredField("mApplicationInfo");
mApplicationInfoField.setAccessible(true);
ApplicationInfo mApplicationInfo = (ApplicationInfo) mApplicationInfoField.get(mPackageInfo);
mApplicationInfo.className = app_name;
delegate.onCreate();
isBindReal = true;
}
}
2、下面在proxy_tools中写一个Main类,和一个main方法,直接运行处理,代码如下:
package com.example.proxy_tools;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
public class Main {
public static void main(String[] args) throws Exception{
/**
* 1、制作只包含解密代码的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 classesDex = new File(aarTemp,"classes.dex");
File classesJar = new File(aarTemp,"classes.jar");
//dx --dex --output out.dex in.jar E:\AndroidSdk\Sdk\build-tools\23.0.3
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");
}
/**
* 2、加密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();
}
/**
* 3、把dex放入apk解压目录,重新压成apk文件
*/
classesDex.renameTo(new File(apkTemp,"classes.dex"));
File unSignedApk = new File("app/build/outputs/apk/debug/app-unsigned.apk");
Zip.zip(apkTemp,unSignedApk);
/**
* 4、对其和签名,最后生成签名apk
*/
// zipalign -v -p 4 my-app-unsigned.apk my-app-unsigned-aligned.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());
// System.out.println("signedApkprocess : 11111" + " :-----> " +unSignedApk.getAbsolutePath() + "\n" + alignedApk.getAbsolutePath());
process.waitFor();
// if(process.exitValue()!=0){
// throw new RuntimeException("dex error");
// }
// apksigner sign --ks my-release-key.jks --out my-app-release.apk my-app-unsigned-aligned.apk
// apksigner sign --ks jks文件地址 --ks-key-alias 别名 --ks-pass pass:jsk密码 --key-pass pass:别名密码 --out out.apk in.apk
File signedApk=new File("app/build/outputs/apk/debug/app-signed-aligned.apk");
File jks=new File("proxy_tools/proxy1.jks");
process= Runtime.getRuntime().exec("cmd /c apksigner sign --ks "+jks.getAbsolutePath()
+" --ks-key-alias wwy --ks-pass pass:123456 --key-pass pass:123456 --out "
+signedApk.getAbsolutePath()+" "+alignedApk.getAbsolutePath());
process.waitFor();
if(process.exitValue()!=0){
throw new RuntimeException("dex error");
}
System.out.println("执行成功");
}
}
我们在写好前面的之后,直接运行这个main方法,就可以在我们的app -> build->outputs->apk->debug下面看到生成的几个apk,分别为 app-debug.apk, app-unsigned.apk, app-unsigned-aligned.apk, app-signed-aligned.apk,最终 app-signed-aligned.apk 才是我们最后安装使用的apk,
注意:如果大家直接按照上面的弄完后,运行main 方法,恐怕会出现不可描述的问题,各种错误,异常,哈哈哈,,正常,,因为我也是这么过来的,所以这里在说一下一些要做的工作:
1)、配置电脑的环境变量:
找到你电脑中sdk的路劲,然后在SDK中把build-tools下面的任意一个版本配置到你的电脑的用户变量中,我这里路劲是:F:\androidSDK\Sdk\build-tools\25.0.0 然后我就把这个路劲配置到用户变量中,(注意:在配置这个变量之后,你的AS一定要重新启动哦,否则依然报错,如果重启as不行,那就直接重启电脑再试试,我也遇到过),这个步骤是准备main中第一个Process命令使用的。
第一步会生成两个文件,一个是classes.jar 和 classes.dex。
下面一共有三个Process 命令操作,但是如果我们直接运行main方法时就算上面的环境配置好了,也未必能直接运行成功,为什么呢?因为我具体也不是很清楚,但是不少人都遇到了,什么问题呢?就是当运行到第二个Process 命令的时候,代码运行到process.waitFor();时,就不往下走了,经过咨询别人讲是由于创建的进程导致的,说什么内存缓存区不足,导致进程一直在等待,我不晓得原因,我的代码运行到下图位置就不动了,
不过已经生成了除了签名之后的apk ,其它的都有了,我百思不得其解,最后我在AS中Terminal 中直接通过命令执行最后一个生成签名的apk 才算把签名之后的apk搞出来,很多人也遇到和我一样的问题。你如果也是这样,那也通过命令操作吧。
2)、记住在执行main方法之前,我们可以看到main方法第四部生成签名的apk,说明我们apk是需要签名的,所以,我们要先签名哦,然后记住别名和密码哦,然后把别名和密码写在第四部中,下面给大家看看我的代码目录结构:
上面就是整个的目录结构,运行完,出来在上面找到签名之后的apk,然后直接拿过去运行能运行出来就可以了!在AS中点击签名之后的apk,会发现这个apk是看不到里面任何的文件的,也就实现了我们的加固功能,别人拿到我们的apk也白费,啥玩意都没有,是不是很牛?哈哈哈哈,,,路漫漫其修远兮? 还有很多的路要走。继续努力!
这里带多一句嘴,上面的代理ProxyApplication被我们配置到Mainfest的application 标签中,这个位置经常是我们配置项目使用的application的,其实不用担心,代码中已经处理过了,当代理application处理完之后,会自动把我们配置的app里面的项目用到的MyApplication 类替换过来,所以项目在第一次运行完之后,正式运行还是以我们自己的MyApplication为主,大可放心。
完整的demo下载地址:https://download.csdn.net/download/i123456789t/11239611