为了保证 Android 应用的源码安全性,我们一般会对上线的应用进行代码混淆,然而仅仅做代码混淆还不够,我们还要对我们的应用加固,防止别人通过反编译获取到我们的源码。目前 apk 加固技术比较成熟完善,市面上比较流行的有“360加固”。本文就 apk 加固技术做一个技术探究,希望读者看过后能明白加固的其中原理,并也能自己实现加固方案。
在 Android apk 加固技术探究(一)中,大致介绍了反编译的过程及我们能够获取到源码的原因。下面就来讲解加固的基本流程。
源码地址:https://gitee.com/openjk/apk-steady
加固流程
- 新建一个 Android 工程,在其中建立一个 shell 的 module 用来生成加固的壳 arr 文件
- shell module 中包含一个 Application 的子类 SteadyApplication,其中包含对 dex 文件解密的逻辑
- 编译 shell 生成 shell.aar 文件
- 解压待加固 apk 到 apkUnzip 目录中,拿到其中的所有 dex 文件
- 修改 apkUnzip 下 AndroidManifest.xml 文件中 application 根结点下的 name 属性值为 2 中创建的 SteadyApplication。同时将原 apk 的 Application 的路径保存到 meta-data 节点下,以备在 SteadyApplication 中解析生成
- 使用加密算法将上一步中得到的 dex 文件加密,并删除原 dex 文件
- 解压 3 中生成的 aar 文件,获得到里面的 jar 文件,然后通过 SDK 中提供的 dx 工具将 jar 文件转换成 dex 文件,将生成的 dex 文件放到 apkUnzip 文件
- 压缩 apkUnzip 文件夹生成新的 apk
- 重新签名
这篇文章主要讲解上述1、2、3步骤,如何生成一个 Shell.arr(壳)文件。Shell 最终会打入到原 apk 的class.dex 中,用来解密已经加密的原 apk 中的dex和加载原来的 dex 文件
一、生成 Shell.aar(dex 解密和类加载)
1、解密加固的 dex 文件的流程
- 在 Application 中可以通过 getApplicationInfo().sourceDir 来获取 base APK,这个 apk 就包含了我们应用的所有代码。
- 通过 Application 的getDir() 方法,我们在应用的私有目录创建一个私有文件夹 SteadyDir
- 在 2 中创建的目录里面我们将 bask.apk 解压
- 解压后我们得到 apk 的所有文件,然后过滤出所有以dex为后缀的文件。其中 classes.dex 文件我们不需要因为它已经被加载进系统,所以只需要处理被我们加密的dex 文件
- 将解密后的 dex 文件加载到程序中
- 运行 apk 真实的 application,启动 app
2、如何解压 apk 文件
zip 解压主要用到了 java 中的 ZipFile 类,具体实现直接上代码,代码中包含注释就不多解释。
public static void unZip(File zip, File dir) {
try {
//清空存放解压文件的目录
deleteFile(dir);
ZipFile zipFile = new ZipFile(zip);
//zip文件中每一个条目
Enumeration<? extends ZipEntry> entries = zipFile.entries();
//遍历
while (entries.hasMoreElements()) {
ZipEntry zipEntry = entries.nextElement();
//zip中 文件/目录名
String name = zipEntry.getName();
//原来的签名文件 不需要了
if (name.equals("META-INF/CERT.RSA") || name.equals("META-INF/CERT.SF") || name
.equals("META-INF/MANIFEST.MF")) {
continue;
}
//空目录不管
if (!zipEntry.isDirectory()) {
File file = new File(dir, name);
//创建目录
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
//写文件
FileOutputStream fos = new FileOutputStream(file);
InputStream is = zipFile.getInputStream(zipEntry);
byte[] buffer = new byte[2048];
int len;
while ((len = is.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
is.close();
fos.close();
}
}
zipFile.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private static void deleteFile(File file){
if (file.isDirectory()){
File[] files = file.listFiles();
for (File f: files) {
deleteFile(f);
}
}else{
file.delete();
}
}
3、如何解密 dex 文件
通过第二步中的解压方式,我们可以很轻松的将 base.apk 解压到私有目录下。然后我们通过文件的后缀名.dex 过滤出所有 dex 文件(排除 classes.dex),接着读取每个 dex 到字节数组中,然后对字节数组进行解密操作。
这里加解密使用的是 AES 的方式,为了增加安全性这里将解密的方式用 jni 方式完成。解密方式如下:
jbyteArray decrypt(JNIEnv *env,jbyteArray srcData) {
jstring type = (*env).NewStringUTF("AES");
jstring cipher_mode = (*env).NewStringUTF("AES/ECB/PKCS5Padding");
jbyteArray pwd = (*env).NewByteArray(16);
char *master_key = (char *) "huangdh'l,.AMWK;";
(*env).SetByteArrayRegion(pwd,0,16,reinterpret_cast<jbyte *>(master_key));
jclass secretKeySpecClass = (*env).FindClass("javax/crypto/spec/SecretKeySpec");
jmethodID secretKeySpecMethodId = (*env).GetMethodID(secretKeySpecClass,"<init>", "([BLjava/lang/String;)V");
jobject secretKeySpecObj = (*env).NewObject(secretKeySpecClass,secretKeySpecMethodId,pwd,type);
jclass cipherClass = (*env).FindClass("javax/crypto/Cipher");
jmethodID cipherInitMethodId = (*env).GetMethodID(cipherClass,"init", "(ILjava/security/Key;)V");
jmethodID cipherInstanceMethodId = (*env).GetStaticMethodID(cipherClass,"getInstance", "(Ljava/lang/String;)Ljavax/crypto/Cipher;");
jobject cipherObj = (*env).CallStaticObjectMethod(cipherClass,cipherInstanceMethodId,cipher_mode);
jfieldID decryptModeFieldId = (*env).GetStaticFieldID(cipherClass,"DECRYPT_MODE", "I");
jint mode = (*env).GetStaticIntField(cipherClass,decryptModeFieldId);
(*env).CallVoidMethod(cipherObj,cipherInitMethodId,mode,secretKeySpecObj);
jmethodID doFinalMethodId = (*env).GetMethodID(cipherClass,"doFinal", "([B)[B");
jbyteArray text = (jbyteArray)(*env).CallObjectMethod(cipherObj,doFinalMethodId,srcData);
return text;
}
4、加载 dex 文件
通过上面的解压和解密操作我们得到了原始的 dex 文件,我们将这些dex文件放进一个集合中,接下来使用类加载机制加载已经解密后的 dex 文件。关于类加载机制会在后续文章中讲解。
public static void loadDex(Application application,List<File> dexFiles, File versionDir) throws Exception{
//1.先从 ClassLoader 中获取 pathList 的变量
Field pathListField = ProxyUtils.findField(application.getClassLoader(), "pathList");
//1.1 得到 DexPathList 类
Object pathList = pathListField.get(application.getClassLoader());
//1.2 从 DexPathList 类中拿到 dexElements 变量
Field dexElementsField= ProxyUtils.findField(pathList,"dexElements");
//1.3 拿到已加载的 dex 数组
Object[] dexElements=(Object[])dexElementsField.get(pathList);
//2. 反射到初始化 dexElements 的方法,也就是得到加载 dex 到系统的方法
Method makeDexElements= ProxyUtils.findMethod(pathList,"makePathElements",List.class,File.class,List.class);
//2.1 实例化一个 集合 makePathElements 需要用到
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
//2.2 反射执行 makePathElements 函数,把已解码的 dex 加载到系统,不然是打不开 dex 的,会导致 crash
Object[] addElements=(Object[])makeDexElements.invoke(pathList,dexFiles,versionDir,suppressedExceptions);
//3. 实例化一个新数组,用于将当前加载和已加载的 dex 合并成一个新的数组
Object[] newElements= (Object[]) Array.newInstance(dexElements.getClass().getComponentType(),dexElements.length+addElements.length);
//3.1 将系统中的已经加载的 dex 放入 newElements 中
System.arraycopy(dexElements,0,newElements,0,dexElements.length);
//3.2 将解密后已加载的 dex 放入新数组中
System.arraycopy(addElements,0,newElements,dexElements.length,addElements.length);
//4. 将合并的新数组重新设置给 DexPathList的 dexElements
dexElementsField.set(pathList,newElements);
}
5、加载真实的 application 类,运行 app
1、首先从 AndroidManifest.xml 文件中获取到原 application 的类名。(在下一篇文章中会讲解我们如何将 apk 的原来的 application 类名放到 AndroidManifest.xml 的meta-data 标签下)
/**
* 解析项目中原来的 Application 名称
*/
private void getMateData(){
try{
ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo(getPackageName(),
PackageManager.GET_META_DATA);//获取包信息
Bundle metaData = applicationInfo.metaData;//获取 Meta-data 的键值对信息
if(null != metaData){
if(metaData.containsKey("app_name")){
app_name = metaData.getString("app_name");//获取原来的包名
}
}
}catch (Exception e){
e.printStackTrace();
}
}
2、获取到原 application 的类名后就通过反射获取到 application 的实例。
private void bindRealApplication() throws Exception{
if(isBindReal){
return;
}
if(TextUtils.isEmpty(app_name)){
return;
}
//1、得到 attachBaseContext(context)传入的上下文 ContextImpl
Context baseContext = getBaseContext();
//2、拿到真实 APK Application 的 class
Class<?> delegateClass = Class.forName(app_name);
//反射实例化,
delegate = (Application) delegateClass.newInstance();
//得到 Application attach() 方法 也就是最先初始化的
Method attach = Application.class.getDeclaredMethod("attach",Context.class);
attach.setAccessible(true);
//执行 Application#attach(Context)
attach.invoke(delegate,baseContext);
// ContextImpl---->mOuterContext(app) 通过Application的attachBaseContext回调参数获取
//4. 拿到 Context 的实现类
Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
//4.1 获取 mOuterContext Context 属性
Field mOuterContextField = contextImplClass.getDeclaredField("mOuterContext");
mOuterContextField.setAccessible(true);
//4.2 将真实的 Application 交于 Context 中。这个根据源码执行,实例化 Application 下一个就行调用 setOuterContext 函数,所以需要绑定 Context
// app = mActivityThread.mInstrumentation.newApplication(
// cl, appClass, appContext);
// appContext.setOuterContext(app);
mOuterContextField.set(baseContext, delegate);
// ActivityThread--->mAllApplications(ArrayList) ContextImpl的mMainThread属性
//5. 拿到 ActivityThread 变量
Field mMainThreadField = contextImplClass.getDeclaredField("mMainThread");
mMainThreadField.setAccessible(true);
//5.1 拿到 ActivityThread 对象
Object mMainThread = mMainThreadField.get(baseContext);
// ActivityThread--->>mInitialApplication
//6. 反射拿到 ActivityThread class
Class<?> activityThreadClass=Class.forName("android.app.ActivityThread");
//6.1 得到当前加载的 Application 类
Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
mInitialApplicationField.setAccessible(true);
//6.2 将 ActivityThread 中的 Applicaiton 替换为 真实的 Application 可以用于接收相应的声明周期和一些调用等
mInitialApplicationField.set(mMainThread,delegate);
// ActivityThread--->mAllApplications(ArrayList) ContextImpl的mMainThread属性
//7. 拿到 ActivityThread 中所有的 Application 集合对象,这里是多进程的场景
Field mAllApplicationsField = activityThreadClass.getDeclaredField("mAllApplications");
mAllApplicationsField.setAccessible(true);
ArrayList<Application> mAllApplications =(ArrayList<Application>) mAllApplicationsField.get(mMainThread);
//7.1 删除 ProxyApplication
mAllApplications.remove(this);
//7.2 添加真实的 Application
mAllApplications.add(delegate);
// LoadedApk------->mApplication ContextImpl的mPackageInfo属性
//8. 从 ContextImpl 拿到 mPackageInfo 变量
Field mPackageInfoField = contextImplClass.getDeclaredField("mPackageInfo");
mPackageInfoField.setAccessible(true);
//8.1 拿到 LoadedApk 对象
Object mPackageInfo=mPackageInfoField.get(baseContext);
//9 反射得到 LoadedApk 对象
// @Override
// public Context getApplicationContext() {
// return (mPackageInfo != null) ?
// mPackageInfo.getApplication() : mMainThread.getApplication();
// }
Class<?> loadedApkClass=Class.forName("android.app.LoadedApk");
Field mApplicationField = loadedApkClass.getDeclaredField("mApplication");
mApplicationField.setAccessible(true);
//9.1 将 LoadedApk 中的 Application 替换为 真实的 Application
mApplicationField.set(mPackageInfo,delegate);
//修改ApplicationInfo className LooadedApk
//10. 拿到 LoadApk 中的 mApplicationInfo 变量
Field mApplicationInfoField = loadedApkClass.getDeclaredField("mApplicationInfo");
mApplicationInfoField.setAccessible(true);
//10.1 根据变量反射得到 ApplicationInfo 对象
ApplicationInfo mApplicationInfo = (ApplicationInfo)mApplicationInfoField.get(mPackageInfo);
//10.2 将我们真实的 APPlication ClassName 名称赋值于它
mApplicationInfo.className=app_name;
//11. 执行 代理 Application onCreate 声明周期
delegate.onCreate();
//解码完成
isBindReal = true;
}
至此 apk 的解密便结束了