admin管理员组文章数量:1585965
目录
- 1.app加固原理
- 2.简单的加固处理
- 1.定义加解密算法
- 2.定义压缩和解压缩的处理方式
- 3.定义Dex文件加载的插入方式
- 4.定义壳的Application
- 5.定义签名和dex操作指令
- 6.定义apk解压和添加壳后重新编译成apk并签名的方法
1.app加固原理
我们在用360加固的时候,会发现目录结构变成了这种格式
会发现我们自己的代码完全看不见了,而这里会多出一个Application
就是StubApp
,在这个StubApp
中会去加载360的so文件,然后我们的应用就可以正确执行了。并且Mainfest
中Application名会修改为StubApp
<application
android:name="com.stub.StubApp"
android:allowBackup="true"
android:appComponentFactory="androidx.core.app.CoreComponentFactory"
android:debuggable="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
......
其实这里的StubApp
就是加固的壳入口,我们知道,app加载的主dex文件是classes.dex
,那么我们只要在这里处理我们的不加密的壳代码,然后在这个壳里去解密我们加密后的核心代码,然后通过ClassLoader
插入类加载器加载数组代码前面了,那么久可以实现基本的加固了。当然360里实现的比较复杂,比如会去处理清单文件的配置信息,加解密的so和加密的文件处理等。
那么我们要实现简单的加固方式的步骤可以是:
1.
定义一个Application去处理dex加密的相关操作,这就是我们的壳文件,我们可以定义为一个library库,然后app清单文件中去注册这个Application,但不把这个library库的代码也打进app中
2.
解压缩原apk文件,把其中的所有代码的dex文件进行加密,然后重新命名,因为主dex需要是我们的壳,所以这些文件都会按其他格式进行命名,名称随意,只要解密时候可以查找到相应的加密的文件即可
3.
把定义的library库的代码文件进行dx
处理为dex文件,然后放到加压缩的文件目录中,作为壳的主dex文件,其中这里的libary库中的Application
中会去查找相应的加密的文件,进行解密后并进行ClassLoader
的pathList
的插入处理
4.
使用zip
流对解密的文件夹进行压缩,注意使用CRC32
的压缩格式,压缩后的文件以apk进行命名
5.
对apk文件进行签名
2.简单的加固处理
1.定义加解密算法
public class AES {
public static final String DEFAULT_PWD = "abcdefghijklmnop";
private static final String algorithmStr = "AES/ECB/PKCS5Padding";
private static Cipher enctyCiper;
private static Cipher decryCiper;
public static void init() {
try {
enctyCiper = Cipher.getInstance(algorithmStr);
decryCiper = Cipher.getInstance(algorithmStr);
byte[] keyBytes = DEFAULT_PWD.getBytes();
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
enctyCiper.init(Cipher.ENCRYPT_MODE, keySpec);
decryCiper.init(Cipher.DECRYPT_MODE, keySpec);
} catch (Exception e) {
e.printStackTrace();
}
}
public static byte[] encry(byte[] data) throws Exception {
return enctyCiper.doFinal(data);
}
public static byte[] decry(byte[] data) throws Exception {
return decryCiper.doFinal(data);
}
public static void encryFile(File srcFile, File descFile) throws Exception {
byte[] buffer = getFileByte(srcFile);
byte[] encryBuffer = encry(buffer);
FileOutputStream fos = new FileOutputStream(descFile);
fos.write(encryBuffer);
fos.flush();
fos.close();
}
public static void decryFile(File srcFile, File descFile) throws Exception {
byte[] buffer = getFileByte(srcFile);
byte[] decryBuffer = decry(buffer);
FileOutputStream fos = new FileOutputStream(descFile);
fos.write(decryBuffer);
fos.flush();
fos.close();
}
public static byte[] getFileByte(File file) throws Exception {
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
byte[] data = new byte[(int) randomAccessFile.length()];
randomAccessFile.read(data);
randomAccessFile.close();
return data;
}
}
这里使用简单的AES对称加密
进行加密和解密,并定义了原文件和加解密后写入到文件位置
2.定义压缩和解压缩的处理方式
public static void unzipBasePathFile(File fromFile, String targetFilName, File targetFile) throws Exception {
ZipFile zipFile = new ZipFile(fromFile);
Enumeration<? extends ZipEntry> enumeration = zipFile.entries();
while (enumeration.hasMoreElements()) {
ZipEntry zipEntry = enumeration.nextElement();
if (zipEntry.getName().equals(targetFilName)) {
FileOutputStream fos = new FileOutputStream(targetFile);
InputStream fis = zipFile.getInputStream(zipEntry);
byte[] data = new byte[1024];
int length;
while ((length = fis.read(data)) != -1) {
fos.write(data, 0, length);
}
fos.close();
break;
}
}
zipFile.close();
}
public static void unzip(File fromFile, File toFileDir) throws Exception {
ZipFile zipFile = new ZipFile(fromFile);
Enumeration<? extends ZipEntry> enumerations = zipFile.entries();
while (enumerations.hasMoreElements()) {
ZipEntry zipEntry = enumerations.nextElement();
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 targetFile = new File(toFileDir, zipEntry.getName());
if (targetFile.getParentFile() == null || !targetFile.getParentFile().exists())
targetFile.getParentFile().mkdirs();
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(targetFile));
BufferedInputStream bis = new BufferedInputStream(zipFile.getInputStream(zipEntry));
byte[] data = new byte[1024];
int length;
while ((length = bis.read(data)) != -1) {
bos.write(data, 0, length);
bos.flush();
}
bis.close();
bos.close();
}
}
zipFile.close();
}
public static void zip(File dir, File zipFile) throws Exception {
if (zipFile.exists()) zipFile.delete();
if (zipFile.getParentFile() == null || !zipFile.getParentFile().exists())
zipFile.getParentFile().mkdirs();
CheckedOutputStream cos = new CheckedOutputStream(new FileOutputStream(zipFile), new CRC32());
ZipOutputStream zos = new ZipOutputStream(cos);
int bashPathLength = dir.getPath().length();
compress(dir, zos, bashPathLength);
zos.flush();
zos.close();
}
private static void compress(File srcFile, ZipOutputStream zos, int bashPathLength) throws Exception {
String targetPath = srcFile.getPath().substring(bashPathLength);
if (targetPath.startsWith(File.separator))
targetPath = targetPath.substring(1);
if (srcFile.isDirectory()) {
File files[] = srcFile.listFiles();
if (files != null && files.length > 0) {
for (File file : files) {
compress(file, zos, bashPathLength);
}
} else {
ZipEntry zipEntry = new ZipEntry(targetPath + File.separator);
zos.putNextEntry(zipEntry);
zos.closeEntry();
}
} else {
targetPath = targetPath.replaceAll("\\\\", "/");
ZipEntry zipEntry = new ZipEntry(targetPath);
zos.putNextEntry(zipEntry);
byte data[] = new byte[1024];
int length;
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(srcFile));
while ((length = bis.read(data)) != -1) {
zos.write(data, 0, length);
}
bis.close();
zos.closeEntry();
}
}
我这里定义了3个方法
unzipBasePathFile
查找和名称匹配的文件,直接写入目标目录,这个是用来把library库中的aar文件的class.jar
输出到解压缩的目录中去的
unzip
把压缩包文件解压到指定目录中,这个主要是用来解压apk文件的,因为apk本身就是一个压缩文件
zip
把指定目录的全部文件压缩成一个文件,这个是最终生成apk文件的方法。这里有几点需要注意。
1.
需要使用CheckOutputStream
进行流校验,并指定CRC32
的校验方式
2.
压缩操作是在电脑上进行处理的,解压操作是在apk运行后处理的
3.
压缩注意文件目录分隔符的操作处理,不能使用File.separator
这个处理,因为电脑上的分割的可能和这个斜杠是相反的方向的
4.
压缩完毕需要关闭zip流,否则是无法完成压缩的
3.定义Dex文件加载的插入方式
public class DexInstall {
public 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();
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) {
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);
}
}
public 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) {
try {
makePathElements = findMethod(dexPathList, "makePathElements", ArrayList.class, File.class, ArrayList.class);
} catch (NoSuchMethodException e1) {
try {
return makeDexElements(dexPathList, files, optimizedDirectory, suppressedExceptions);
} catch (NoSuchMethodException e2) {
throw e2;
}
}
}
return (Object[]) makePathElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions);
}
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);
}
}
这个方法是比较通用的方式,主要是把我们需要加载的dex插入到Classloader
的pathList
的前列,这样在Classloder的双亲委托机制下就会保证这些代码的正常执行。在加载机制中,如果连续加载两个相同的类信息,那么第一次加载的会生效,后续加载的因为判断已经存在了,就不会再去添加到类信息列表中去了,热修复也是基于这个原理实现的。
4.定义壳的Application
public class ShellApplication extends Application {
private static final String TAG = "ShellApplication";
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
try {
AES.init();
List<File> files = new ArrayList<>();
File apkFile = new File(getApplicationInfo().sourceDir);
File unzipFile = getDir("hook_dir", MODE_PRIVATE);
File app = new File(unzipFile, "app");
if (!app.exists()) {
Zip.unzip(apkFile, app);
File file[] = app.listFiles();
for (File itemFile : file) {
String name = itemFile.getName();
if (name.equals("classes.dex")) {
files.add(itemFile);
} else if (name.endsWith(".dex")) {
AES.decryFile(itemFile, itemFile);
files.add(itemFile);
}
}
}
DexInstall.install(getClassLoader(), files, unzipFile);
} catch (Exception e) {
e.printStackTrace();
}
}
}
这里主要是把apk解压缩到一个目录下,然后遍历我们所加密的dex的文件,然后解密后重新写回去,最终调用上面的方法插入到类加载的列表中去
5.定义签名和dex操作指令
public class Command {
public static void dxJar(File fileFromFile, File toFile) throws Exception {
Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec("cmd /C dx --dex --output=" + toFile.getAbsolutePath() + " " +
fileFromFile.getAbsolutePath());
process.waitFor();
process.destroy();
}
public static void signApk(String keystorePath, String alias, String pwd, String srcPath, String desPath) throws Exception {
String[] commends = new String[]{"cmd", "/C", "jarsigner", "-verbose", "-sigalg", "MD5WithRSA",
"-digestalg", "SHA1",
"-keystore", keystorePath,
"-storepass", pwd,
"-keypass", pwd,
"-signedjar", desPath,
srcPath, alias
};
Process process = Runtime.getRuntime().exec(commends);
process.waitFor();
process.destroy();
}
}
这里有两个方法
dxJar
是把jar文件转换成dex文件
signApk
是对指定的apk进行签名,这个方法可能不太好使,可以直接用命令行签名也行
6.定义apk解压和添加壳后重新编译成apk并签名的方法
public static void main(String[] args) throws Exception {
String srcPath = "source/testapp-debug.apk";
String unzipPath = "source/unzip";
String zipPath = "target/rezip.apk";
File srcFile = new File(srcPath);
File unzipFileDir = new File(unzipPath);
if (unzipFileDir.exists()) unzipFileDir.delete();
// 步骤1
Zip.unzip(srcFile, unzipFileDir);
AES.init();
String aarFilePath = "source/mylibrary-release.aar";
String targetAarName = "classes.jar";
String targetFilePath = "source/unzip/classes.jar";
//步骤2
Zip.unzipBasePathFile(new File(aarFilePath), targetAarName, new File(targetFilePath));
//步骤3
Utils.encryApkDexFiles(unzipFileDir);
String targetJarDexFilePath = "source/unzip/classes.dex";
File aarFile = new File(targetFilePath);
File dexFile = new File(targetJarDexFilePath);
//步骤4
System.out.println("aarFile existes ->>>> "+aarFile.exists());
Command.dxJar(aarFile,dexFile);
aarFile.delete();
System.out.println("aarFile existes ->>>> "+aarFile.exists());
//步骤5
File zipFile = new File(zipPath);
Zip.zip(unzipFileDir,zipFile);
String descPath = "target/rezip-signed.apk";
Command.signApk("keystore/key.jks","demo","android",zipPath,descPath);
}
public class Utils {
public static void encryApkDexFiles(File dir) throws Exception {
File[] files = dir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(".dex");
}
});
for (int i = 0; i < files.length; i++) {
File file = files[i];
AES.encryFile(file, file);
String name = file.getName();
String targetName = name.split("[.]")[0] + "_" + i + ".dex";
file.renameTo(new File(file.getPath().replace(name,targetName)));
}
}
}
操作有点多,这里分段解释一下
首先在项目中创建一个source的文件夹,然后把打包完成的apk和我们创建的library所编译生成的aar都添加进去
步骤1.
把apk文件解压缩到unzip
文件里
步骤2.
把aar文件中的classes.jar
文件拷贝到unzip
文件夹中
步骤3.
遍历unzip
文件夹中的dex
后缀的文件,加密后写回去覆盖原文件,然后用一定格式重命名,这个命名规则可以随便定义,只要能找到即可,这个主要是预留出我们存放壳的classes.dex
的文件位置,也就是apk加载的入口
步骤4.
把上面aar拷贝过来的classes.jar
文件用dx
命令生成classes.dex
这个文件,然后删除原文件
步骤5.
把unzip
文件夹重新打包成apk,并用自己的签名文件重新签名
执行完后的文件夹格式大概是这样的
其中rezip-signed.apk就是我们的目标apk了
用jadu查看的文件格式是
实际在模拟器上跑这个apk也是没有问题的。
可以看出这里只有解密相关的代码,没有核心的应用代码,应用代码这里是在classes_0.dex这里,当然这个命名规则是随意的,可以随便命名,只要能找到并解密即可。
真正的加固方案比这个要复杂的多,这里只是抛砖引玉实现一个最简单的加固方式,其他的加固方式都是在这个基础上进行更深层次的处理的。
版权声明:本文标题:安卓app加固的简单实现 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://m.elefans.com/xitong/1727976212a1140686.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论