如何应对 Android 面试官 ->文件 IO、手写 APK 加固框架核心实现 (下)
- 2024-01-27 北京
本文字数:12896 字
阅读完需:约 42 分钟
前言
本章知识点分为上下两节,上章主要介绍下字节流、字符流、NIO、本章介绍下 Dex 文件加密、手写加固框架核心实现;
反编译
什么是反编译?
利用编译程序从源语言编写的源程序产生目标程序的过程;
如何进行反编译?
解压缩 apk 文件;
META-INF 存放签名文件
classes.dex 需要将这个文件反编译成 jar
res
resources.arsc
dex2jar,将 class.dex 转换成 jar;
使用 JD-GUI 查看 jar;
MAC 下反编译可以查看这篇博客:mac下反编译
加固方案
反模拟器
模拟器运行 APK,可以用模拟器监控到 APK 的各种行为,所以在实际的加固 APK 运行中,一旦发现模拟器在运行该 APK,就停止核心代码的运行;
代码虚拟化
代码虚拟化在桌面平台应用保护中已经是非常的常见了,主要思路是自建一个虚拟执行引擎,把原生的可执行代码转换成自定义的指令进行虚拟执行;
加密
样本的部分可执行代码是以压缩或者加密的形式存在的,比如:被保护过的代码被切割成多个小段,前面的一段代码先把后面的代码片段在内存中解密,然后再去执行解密之后的代码,如此一块一块的迭代执行;(微信就是基于此的一种加固方式)本章的手写 APK 加固的核心实现也是基于此方案实现;
基于加密方案的加固原理
我们所有的 Dex 经过 classloader 加载到内存后,执行运行,但是 Dex 并不是全部都会进行加载,而是需要用之前加载;
所以我们可以将 Dex 文件分成核心代码和非核心代码,将非核心代码展示出来,当非核心代码需要用到核心代码的时候,再把核心代码中的类加载进来,核心代码加固(加密),提升代码安全性(加载的过程中顺带着解密);
源 Dex 进行加密,壳 Dex 不加密,用壳 Dex 对源 Dex 进行解密;
加固框架总体思路
将 APK 文件分为 Dex 一类和其他一类;
文件解压缩(unzip),对文件进行分类(过滤)「一类是 Dex,一类是非 Dex」;
对 Dex 进行加密操作,将 Dex 文件中的二进制进行读取,读取出来之后进行 AES 加密,加密完成之后再写回去「文件 IO 操作」;
创建一个壳 Dex 文件;
将壳 Dex 和加密后的 Dex 文件合并;
将合并后的 Dex 与其他非 Dex 文件进行合并成一个新的 APK 文件;
对新的 APK 文件进行重新签名;
安装运行,验证是否成功;
这是加固前的流程,加固中和加固后的流程呢?
Dex 文件可以随意拼凑吗?
壳 Dex 怎么来的?
如何签名?
如何运行新的 APK(脱壳)?
Dex 文件可以随意拼凑吗?
我们对 Dex 进行操作的时候,需要了解 Dex 的文件格式
Dex 文件基本格式
文件头 header
checksum
signnature
file_size
索引区
字符串的索引 string_ids
类型的索引 type_ids
方法原型的索引 proto_ids
域的索引 field_ids
方法的索引 method_ids
数据区
类的定义区 class_defs
数据区 data
链接数据区 link_data
对 Dex 的修改,我们必须要按照 Dex 的格式来进行,例如我们往数据区的 data 写入了一段数据,那么我们就需要按照格式修改 header 区的 file_size signnature checksum(checksum 是对整个文件的一个校验)
APK 打包流程
资源文件通过 aapt 工具生成 R.java 文件,由 gradle 自动调用构建工具;
.aidl File 通过 aidl 工具自动生成 java 代码;
开发者创建的 java 代码 + aidl 创建的 java 代码以及资源文件生成的 java 代码,通过 java compiler 生成 class「也就是 javac 的操作」;
用 dx.bat 工具 将 class 文件生成 dex 文件;
用 apkbuilder 工具将资源文件和 dex 文件其他文件打包成一个 APK 文件;
用 jarsigner + keystore 对 apk 进行签名,获取签名文件;
zipalign 对齐;
手写加固框架(非商用)
整体分为四个大的流程
第一步 处理原始 APK & 加密 Dex;
原始 APK 的处理,其实就是文件的 IO 操作,解压 APK,并读取其中的 Dex 文件;
APK 解压,并读取 Dex 文件
public class ZipUtils {
public static void unZip(File zip, File dir) {
try {
dir.delete();
// 适用 jdk 提供的 ZipFile API 进行解压读取操作
ZipFile zipFile = new ZipFile(zip);
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry zipEntry = entries.nextElement();
String name = zipEntry.getName();
// 过滤非 dex 文件
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[1024];
int len;
while ((len = is.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
// 关闭相关输入输出流
is.close();
fos.close();
}
}
zipFile.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
对 Dex 文件加密
public static File encryptAPKFile(File srcAPKfile, File dstApkFile) throws Exception {
if (srcAPKfile == null) {
System.out.println("encryptAPKFile :srcAPKfile null");
return null;
}
// File disFile = new File(srcAPKfile.getAbsolutePath() + "unzip");
// ZipUtls.unZip(srcAPKfile, disFile);
// ZipUtils.unZip(srcAPKfile, dstApkFile);
// 获得所有的dex (需要处理分包情况)
File[] dexFiles = dstApkFile.listFiles(new FilenameFilter() {
@Override
public boolean accept(File file, String s) {
return s.endsWith(".dex");
}
});
File mainDexFile = null;
byte[] mainDexData = null;
for (File dexFile: dexFiles) {
// 读数据,dex 转 byte
byte[] buffer = Utils.getBytes(dexFile);
// 加密,对 byte 进行加密
byte[] encryptBytes = AESUtils.encrypt(buffer);
if (dexFile.getName().endsWith("classes.dex")) {
mainDexData = encryptBytes;
mainDexFile = dexFile;
}
// 写数据,替换原来的数据
FileOutputStream fos = new FileOutputStream(dexFile);
fos.write(encryptBytes);
fos.flush();
fos.close();
}
return mainDexFile;
}
public static byte[] encrypt(byte[] content) {
try {
byte[] result = encryptCipher.doFinal(content);
return result;
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
return null;
}
public static byte[] getBytes(File dexFile) throws Exception {
RandomAccessFile fis = new RandomAccessFile(dexFile, "r");
byte[] buffer = new byte[(int)fis.length()];
fis.readFully(buffer);
fis.close();
return buffer;
}
对加密后的 Dex 进行重命名,区分壳 Dex;
if (newApkFile.isDirectory()) {
return;
}
File[] listFiles = newApkFile.listFiles();
for (File file : listFiles) {
if (file.isFile()) {
if (file.getName().endsWith(".dex")) {
String name = file.getName();
System.out.println("rename step1:"+name);
int cursor = name.indexOf(".dex");
String newName = file.getParent() + File.separator + name.substring(0, cursor) + "_" + ".dex";
System.out.println("rename step2: " + newName);
file.renameTo(new File(newName));
}
}
}
第二步、第三步直接在 main 中进行执行即可;
public class HandleProguard {
public static void main(String[] args) throws Exception {
byte[] mainDexData; //存储源apk中的源dex文件
byte[] aarData; // 存储壳中的壳dex文件
byte[] mergeDex; // 存储壳dex 和源dex 的合并的新dex文件
File tempFileApk = new File("src/source/apk/temp");
if (tempFileApk.exists()) {
File[]files = tempFileApk.listFiles();
for(File file: files){
if (file.isFile()) {
file.delete();
}
}
}
File tempFileAar = new File("src/source/aar/temp");
if (tempFileAar.exists()) {
File[]files = tempFileAar.listFiles();
for(File file: files){
if (file.isFile()) {
file.delete();
}
}
}
/**
* 第一步 处理原始apk 加密dex
*
*/
AES.init(AES.DEFAULT_PWD);
File apkFile = new File("src/source/apk/app-debug.apk");
File newApkFile = new File(apkFile.getParent() + File.separator + "temp");
if(!newApkFile.exists()) {
newApkFile.mkdirs();
}
// 解压 apk
Zip.unZip(apkFile, newApkFile);
File mainDexFile = AES.encryptAPKFile(apkFile, newApkFile);
if (!newApkFile.isDirectory()) {
return;
}
File[] listFiles = newApkFile.listFiles();
for (File file : listFiles) {
if (file.isFile()) {
if (file.getName().endsWith(".dex")) {
String name = file.getName();
System.out.println("rename step1:"+name);
int cursor = name.indexOf(".dex");
String newName = file.getParent()+ File.separator + name.substring(0, cursor) + "_" + ".dex";
System.out.println("rename step2:"+newName);
file.renameTo(new File(newName));
}
}
}
}
}
执行的结果:在 src/source 目录下生成 temp 文件夹,以及 APK 解压之后的相关文件以及加密之后的 Dex 文件,重命名是为了区分壳 Dex 和源 Dex;
第二步 处理 AAR 中的 JAR 获得壳 Dex;
/**
* 第二步 处理aar 获得壳dex
*/
File aarFile = new File("src/source/aar/mylibrary-debug.aar");
// 在 aar 目录下创建 temp 目录
File fakeDex = new File(aarFile.getParent() + File.separator + "temp");
System.out.println("jar2Dex: aarFile.getParent(): " + aarFile.getParent());
// 解压 aar 到 fakeDex 目录
ZipUtils.unzip(aarFile, fakeDex);
// 获取 aar 中的 jar 并转换成 dex
File aarDex = DxUtils.jar2Dex(aarFile);
File tempMainDex = new File(newApkFile.getPath() + File.separator + "classes.dex");
if (!tempMainDex.exists()) {
tempMainDex.createNewFile();
}
FileOutputStream fileOutputStream = new FileOutputStream(tempMainDex);
byte[] bytes = ByteUtils.getBytes(aarDex);
fileOutputStream.write(bytes);
fileOutputStream.flush();
fileOutputStream.close();
// jar2dex 的命令
public class DxUtils {
public static File jar2Dex(File aarFile, File fakeDex) throws IOException {
if (fakeDex == null) {
return null;
}
File[] files = fakeDex.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.equals("classes.jar");
}
});
if (files == null || files.length <= 0) {
throw new RuntimeException("the aar is invalidate");
}
File classes_jar = files[0];
File aarDex = new File(classes_jar.getParentFile(), "classes.dex");
DxUtils.dxCommand(aarDex, classes_jar);
return aarDex;
}
public static void dxCommand(File aarDex, File classes_jar) throws IOException {
Runtime runtime = Runtime.getRuntime();
// windows 下 dx.bat 的执行命令
// Process process = runtime.exec("cmd.exe /C dx --dex --output=" + aarDex.getAbsolutePath() + " "
// + classes_jar.getAbsolutePath());
// mac 下 dx.bat 的执行命令
Process process = runtime.exec("dx --dex --output=" + aarDex.getAbsolutePath() + " "
+ classes_jar.getAbsolutePath());
try {
process.waitFor();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (process.exitValue() != 0) {
InputStream inputStream = process.getErrorStream();
int len;
byte[] buffer = new byte[1024];
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
while ((len = inputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, len);
}
System.out.println(new String(byteArrayOutputStream.toByteArray(), "GBK"));
throw new RuntimeException("dx process error");
}
process.destroy();
}
}
mac 下需要配置 dx 的环境变量,配置成功之后,需要重启 idea,然后执行我们的程序即可
执行将 classes.jar 转换成 classes.dex 并复制到 src/source/apk/temp 目录下;
aar 壳 Dex 中主要是 Application 的子类,用来替换我们在 AndroidManifest.xml 中的默认 Application;
因为 壳 Dex 是没有被加密的,当我们安装了这个壳 Dex 之后,启动这个 ShellApplication 的时候,我们需要在 ShellApplication 中进行脱壳(解密)操作,hook dexElements 数组,保证我们的源 Dex 可以正常安装和 load;
第三步 打包签名;
/**
* 合并壳dex 和源dex 打包签名
* */
File unsignedApkFile = new File("src/result/apk-unsigned.apk");
unsignedApkFile.getParentFile().mkdirs();
ZipUtils.zip(newApkFile, unsignedApkFile);
// 签名
File signedApkFile = new File("src/result/apk-signed.apk");
SignatureUtils.signature(unsignedApkFile, signedApkFile);
public class SignatureUtils {
public static void signature(File unsignedApk, File signedApk) throws InterruptedException, IOException {
// windows 平台下的签名命令
// String cmd[] = {"cmd.exe", "/C ","jarsigner", "-sigalg", "MD5withRSA",
// "-digestalg", "SHA1",
// "-keystore", "src/source/keystore/debug.keystore",
// "-storepass", "android",
// "-keypass", "android",
// "-signedjar", signedApk.getAbsolutePath(),
// unsignedApk.getAbsolutePath(),
// "androiddebugkey"};
// mac 平台下的签名命令
String cmd[] = {"jarsigner", "-sigalg", "MD5withRSA",
"-digestalg", "SHA1",
"-keystore", "src/source/keystore/debug.keystore",
"-storepass", "android",
"-keypass", "android",
"-signedjar", signedApk.getAbsolutePath(),
unsignedApk.getAbsolutePath(),
"androiddebugkey"}; Process process = Runtime.getRuntime().exec(cmd);
System.out.println("start sign");
try {
int waitResult = process.waitFor();
System.out.println("waitResult: " + waitResult);
} catch (InterruptedException e) {
e.printStackTrace();
throw e;
}
System.out.println("process.exitValue() " + process.exitValue() );
if (process.exitValue() != 0) {
InputStream inputStream = process.getErrorStream();
int len;
byte[] buffer = new byte[2048];
ByteArrayOutputStream bos = new ByteArrayOutputStream();
while((len=inputStream.read(buffer)) != -1) {
bos.write(buffer,0,len);
}
System.out.println(new String(bos.toByteArray(),"GBK"));
throw new RuntimeException("sign error");
}
System.out.println("finish signed");
process.destroy();
}
}
执行合并打包和签名
生成签名的 APK,运行 adb install apk-signed.apk 执行安装;
配置了 dx 环境变量,理论上 jarsinger 也就不用配置了,它们都在同一个目录下
在 Mac/Linux 系统中,deubg.keystore 文件默认储存在 ~/.android/ 路径下;
release 的一般存放在公司自己的打包服务器上;
我们将签名后的 APK(apk-signed.apk) 直接拖到 Android Studio 的编辑窗口内,可以直接看 Dex 文件
点击源 Dex,我们发现
源 Dex 因为加密,Android Studio 解析失败;
而我们的壳 Dex 是可以解析成功的;
第四步:脱壳
核心就是在运行的时候进行解密操作: ShellApplication 的 attachBaseContext 中获取加密后的 Dex 进行解密,并 Hook dexElements 数组进行加载;
ClassLoader、dexElements 等后面插件化、热修复的时候会详细讲解;
接下里的操作,基本就是热修复的流程(反射,关于 dex 的加载等等);
public class ShellApplication extends Application {
private static final String TAG = "ShellApplication";
public static String getPassword() {
// 这里要和加密的密码保持一致,否则会解密失败;
return "abcdefghijklmnop";
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
AES.init(getPassword());
File apkFile = new File(getApplicationInfo().sourceDir);
// 解压到这个目录下 data/data/包名/files/fake_apk/ 不需要权限
File unZipFile = getDir("fake_apk", MODE_PRIVATE);
File app = new File(unZipFile, "app");
if (!app.exists()) {
ZipUtils.unZip(apkFile, app);
File[] files = app.listFiles();
for (File file : files) {
String name = file.getName();
if (name.endsWith(".dex")) {
try {
byte[] bytes = getBytes(file);
FileOutputStream fos = new FileOutputStream(file);
// 执行解密操作
byte[] decrypt = AES.decrypt(bytes);
fos.write(decrypt);
fos.flush();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
List list = new ArrayList<>();
Log.d(TAG, Arrays.toString(app.listFiles()));
for (File file : app.listFiles()) {
if (file.getName().endsWith(".dex")) {
list.add(file);
}
}
Log.d(TAG, list.toString()); try {
// 这里可以参考 Tinker 热修复,进行不同版本的适配
V19.install(getClassLoader(), list, unZipFile);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
private static Field findField(Object instance, String name) throws NoSuchFieldException {
Class clazz = instance.getClass();
while (clazz != null) {
try {
Field e = clazz.getDeclaredField(name);
if (!e.isAccessible()) {
e.setAccessible(true);
}
return e;
} catch (NoSuchFieldException var4) {
clazz = clazz.getSuperclass();
}
}
throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
}
private static Method findMethod(Object instance, String name, Class... parameterTypes)
throws NoSuchMethodException {
Class clazz = instance.getClass();
while (clazz != null) {
try {
Method e = clazz.getDeclaredMethod(name, parameterTypes);
if (!e.isAccessible()) {
e.setAccessible(true);
}
return e;
} catch (NoSuchMethodException var5) {
clazz = clazz.getSuperclass();
}
}
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[]) ((Object[]) jlrField.get(instance));
Object[] combined = (Object[]) ((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 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 suppressedExceptions = new ArrayList();
Log.d(TAG, "Build.VERSION.SDK_INT " + Build.VERSION.SDK_INT);
// 参考 Tinker 进行不同版本的适配
if (Build.VERSION.SDK_INT >= 23) {
expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList, new
ArrayList(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
} else {
expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new
ArrayList(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
}
if (suppressedExceptions.size() > 0) {
Iterator suppressedExceptionsField = suppressedExceptions.iterator();
while (suppressedExceptionsField.hasNext()) {
IOException dexElementsSuppressedExceptions = (IOException)
suppressedExceptionsField.next();
Log.w("MultiDex", "Exception in makeDexElement",
dexElementsSuppressedExceptions);
}
Field suppressedExceptionsField1 = findField(loader,
"dexElementsSuppressedExceptions");
IOException[] dexElementsSuppressedExceptions1 = (IOException[]) ((IOException[])
suppressedExceptionsField1.get(loader));
if (dexElementsSuppressedExceptions1 == null) {
dexElementsSuppressedExceptions1 = (IOException[]) suppressedExceptions
.toArray(new IOException[suppressedExceptions.size()]);
} else {
IOException[] combined = new IOException[suppressedExceptions.size() +
dexElementsSuppressedExceptions1.length];
suppressedExceptions.toArray(combined);
System.arraycopy(dexElementsSuppressedExceptions1, 0, combined,
suppressedExceptions.size(), dexElementsSuppressedExceptions1.length);
dexElementsSuppressedExceptions1 = combined;
}
suppressedExceptionsField1.set(loader, dexElementsSuppressedExceptions1);
}
}
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 Object[] makePathElements(
Object dexPathList, ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions)
throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Method makePathElements;
try {
makePathElements = findMethod(dexPathList, "makePathElements", List.class, File.class,
List.class);
} catch (NoSuchMethodException e) {
Log.e(TAG, "NoSuchMethodException: makePathElements(List,File,List) failure");
try {
makePathElements = findMethod(dexPathList, "makePathElements", ArrayList.class, File.class, ArrayList.class);
} catch (NoSuchMethodException e1) {
Log.e(TAG, "NoSuchMethodException: makeDexElements(ArrayList,File,ArrayList) failure");
try {
Log.e(TAG, "NoSuchMethodException: try use v19 instead");
return V19.makeDexElements(dexPathList, files, optimizedDirectory, suppressedExceptions);
} catch (NoSuchMethodException e2) {
Log.e(TAG, "NoSuchMethodException: makeDexElements(List,File,List) failure");
throw e2;
}
}
}
return (Object[]) makePathElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions);
}
private byte[] getBytes(File file) throws Exception {
RandomAccessFile r = new RandomAccessFile(file, "r");
byte[] buffer = new byte[(int) r.length()];
r.readFully(buffer);
r.close();
return buffer;
}
}
Tinker 的 SystemClassLoaderAdapter 进行了不同版本的适配,这里可以参考下;
简历润色
简历上可写:深度理解文件 IO,可基于 IO 操作手写 APK 加固核心实现;
下一章预告
带你玩转 JVM 内存管理;
老A说
还未添加个人签名 2018-11-26 加入
还未添加个人简介
评论