Java IO在Android中应用(二):APK加固
内容导读
互联网集市收集整理的这篇技术教程文章主要介绍了Java IO在Android中应用(二):APK加固,小编现在分享给大家,供广大互联网技能从业者学习和参考。文章包含13766字,纯文字阅读大概需要20分钟。
内容图文
![Java IO在Android中应用(二):APK加固](/upload/InfoBanner/zyjiaocheng/622/c9738421fc8c48ac829a1810020bc7e2.jpg)
Java I/O在Android中应用(二):APK加固套壳
前言(废话)
我,有两把键盘,第一把是Poker III(黑轴),第二把是Poker II(红轴)。工作的时候我常用的是红轴的Poker II,但是当我回家,一般我就会使用Poker III黑轴键盘。以前我怎么没有感觉到黑轴键盘是这么的重且难受,我仍记得我第一次买的是Poker III的红轴键盘,然后买来的第二个晚上我就把红轴的键盘退了,然后换了黑轴的键盘。现在回想起来,可能还是因为自己修为不够,体会不到红轴的那种快乐。
这次想理一理如何仅仅通过Java I/O的加密和解密来对一个应用进行加壳操作。当下的商用加壳操作其实挺复杂的,造成这种复杂的关键原因是无论是Java还是Linux都是开源的,都是为了共享而存在的。但是,一旦共享,就意味着没有秘密,一些具有商业价值的秘密我们绝对不能允许他们没有任何加密手段直接暴露在所有人面前,因此就需要对应用进行加固。
现在回想起来真的是很搞笑,为什么呢?我记得以前我在某家公司当实习生的时候,我曾经问过我的导师关于加固的事情,他是这么回答我的:通过加固的方式可以让Apk在解压后,就是损坏不可读取的状态,所以是很安全的。
不知道为什么,可能是我的一种天赋,当我看到一件事情的时候,如果我发现,这件事情不靠谱,我会有一种隐隐约约的违和感。虽然到底哪里不对我说不清楚,但是如果我仔细琢磨,我最后能发现到底哪里不对。上面的说法到底哪里不对呢?
首先,我们的开发永远是基于Linux和Android平台这么一套框架和体系的,框架是通过一套固定的代码和机制来处理和运行我们的所生成的应用安装包的。说的再通俗一些,其实框架本身也是通过解压然后执行这一套逻辑来安装和运行程序的,如果按照上面所说的,一旦解压后,应用就处于损坏的状态,那这个加固有什么意义呢?因为没有办法运行啊。所以,所谓导师,终归只学到了一点点皮毛,他错把加密状态下的文件当成了损坏的文件。通过实验+猜测这样学习东西,真的蠢的很!说到这个我越说越气,下次有机会我会专门写一篇博客来批判这样的学习方式。
今天要出差,博客只能先写套壳,解壳操作还是需要再写一篇啊。
思维导图
概要
加固的必要
对于Java和Android这样的开源环境下,想要持有秘密永远是一种刚需。比如说你的接口调用地址,甚至代码中的一些漏洞,如果不通过一些措施进行保护,其实是很可怕的。单单是知道你的API调用这点,就意味着别人完全不用为相同的业务自己搭建服务器,直接使用你提供的API就行了,这白漂效率,堪称业界楷模。或者别人可以反编译你的应用然后向里面注入各种东西,在以前对于签名并没有那么严格的限制的时候,谷歌提供的很多应用都是收费的,所以,很多人就会选择去网络上去下载应用直接安装。但是从网络上下载下来的应用,轻则肯定有广告,如果没广告,那就更恐怖了,很有可能你的私人信息都在无形间被这些应用窃取并利用。
网络上其实有很多的应用加固方案, 我们找到问题的根源,就是框架过于开放,所以解决问题就有两个思路:
- 既然无法改变框架,那么就改变自己
- 既然框架的安全性不好,那么就搭建一套自己的框架
对应于现在主流的两种加固思路,就是套壳加密
或者虚拟桌面
。
套壳加密
的大致思想是,我将应用分为两块,非核心代码
和核心代码
,什么意思呢?在上文中我们说过,我们对像API调用这样敏感的信息进行加密,从而不希望被轻易地破解出来。但是一些简单的,极为基础的,傻子都会的技术和信息,就作为诱饵放在明面上。他们花时间去破解,破解出来的收获微乎其微,我们利用这个时间去研究新的技术,这样不香吗?
那通过虚拟桌面
的方式进行加固的操作呢?这块我后面找时间专门研究研究再写博客来详细说明。大体思路就是我基于Android虚拟机的基础上再搭建一层虚拟机,重新设计一套规则逻辑,然后我的应用就放在自己的虚拟机上运行,这样,别人又不知道新的虚拟机的规则逻辑。自然,应用也就安全了。
APK加固
零:整体流程
/**
* 材料:aar目标应用,apk shell应用
* 1.将两者分别解压,将aar中的dex文件加密并重命名后放入apk shell解压的文件夹内
* 2.将处理后的shell文件夹压缩成apk文件
* 3.将apk文件进行签名操作大功告成
*
* @param args 工具类参数 p1:aar路径,p2:apk路径,p3:生成apk存储路径
* @throws Exception 抛出类型
*/
public static void main(String[] args) throws Exception {
/*
步骤一:解压壳应用mylibrary-debug.aar和目标应用app-debug.apk
*/
//创建临时文件夹,用于存储apk解压后的文件
File tempApkFileDir = new File("source/apk/temp");
File tempFileAar = new File("source/aar/temp");
//如果文件夹已经存在,则说明可能之前进行过应用,清空其中的文件
FileUtil.clearDirIfNotEmpty(tempApkFileDir, tempFileAar);
/*
步骤二:提取出目标应用中的数据,并拷贝到临时文件夹中,同时将其中的dex类型文件进行AES加密并重命名,
该临时文件夹在之后会进行压缩操作变成套壳的未签名应用包。
*/
AES.init(AES.DEFAULT_PWD);
File apkFile = new File("source/apk/app-debug.apk");
File newApkFile = new File(apkFile.getParent() + File.separator + "temp");
FileUtil.createDirIfNotExist(newApkFile);
AES.encryptAPKFile(apkFile, newApkFile);
FileUtil.listSonFiles(IS_DEX, FileUtil::renameFile, newApkFile);
/*
步骤三:将壳应用中的jar文件转换成dex类型的文件,并拷贝到临时文件夹中作为壳应用中的临时dex文件
*/
File aarFile = new File("source/aar/mylibrary-debug.aar");
File aarDex = Dx.jar2Dex(aarFile);
File tempMainDex = new File(newApkFile.getPath() + File.separator + "classes.dex");
FileUtil.createFileIfNotExist(tempMainDex);
FileOutputStream fos = new FileOutputStream(tempMainDex);
byte[] fbytes = Utils.getBytes(aarDex);
fos.write(fbytes);
fos.flush();
fos.close();
/*
步骤四:压缩临时文件夹生成未签名应用包
*/
File unsignedApk = new File("result/apk-unsigned.apk");
FileUtil.createDirIfNotExist(unsignedApk.getParentFile());
Zip.zip(newApkFile, unsignedApk);
/*
步骤五:为安装包签名
*/
File signedApk = new File("result/apk-signed.apk");
Signature.signature(unsignedApk, signedApk);
}
一. 套壳加密
步骤一:解压壳应用和目标应用
1. 加密目标文件中的dex文件
/**
* 对apk包中的dex文件进行加密,并复制到目标文件夹中
*
* @param srcAPKFile 源APK文件
* @param dstApkFile 目标APK文件
* @return 主dex文件
* @throws Exception 抛出异常
*/
public static File encryptAPKFile(File srcAPKFile, File dstApkFile) throws Exception {
if (srcAPKFile == null) {
System.out.println("encryptAPKFile :source APK file is null");
return null;
}
Zip.unZip(srcAPKFile, dstApkFile);
//过滤出所有dex文件
File[] dexFiles = dstApkFile.listFiles((file, s) -> s.endsWith(".dex"));
File mainDexFile = null;
if (dexFiles != null)
for (File dexFile : dexFiles) {
byte[] buffer = Utils.getBytes(dexFile);
byte[] encryptBytes = AES.encrypt(buffer);
if (dexFile.getName().endsWith("classes.dex")) {
mainDexFile = dexFile;
}
FileOutputStream fos = new FileOutputStream(dexFile);
if (encryptBytes != null)
fos.write(encryptBytes);
fos.flush();
fos.close();
}
return mainDexFile;
}
2. 拷贝目标应用中的文件
/**
* 拷贝除了签名文件之外的所有文件
* @param unZipFrom 待解压文件
* @param unZipTo 解压后文件
*/
public static void unZip(File unZipFrom, File unZipTo) {
try {
unZipTo.delete();
ZipFile zipFile = new ZipFile(unZipFrom);
//获取压缩文件中的文件遍历
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry zipEntry = entries.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 file = new File(unZipTo, 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();
}
}
3. 重命名文件夹中目标应用dex文件
/**
* 遍历当前文件夹下的子文件,并执行对应操作
* @param filter 过滤方式
* @param consumer 对过滤结果执行操作
* @param dirFile 当前文件夹
*/
public static void listSonFiles(Applicable<File, Boolean> filter, Consumer<File> consumer, File dirFile) {
if (dirFile == null || !dirFile.isDirectory() || !dirFile.exists()) return;
File[] files = dirFile.listFiles();
if (files == null || files.length <= 0) return;
for (File file : files) if (filter.apply(file)) consumer.consume(file);
}
4. 将壳aar文件中的jar文件提取出来转换成dex文件置于目标文件夹中
/**
* 将壳aar文件中的jar文件提取出来转换成dex文件
* @param aarFile 壳文件
* @return dex文件
* @throws IOException 抛出的IO异常
* @throws InterruptedException 线程中断异常
*/
public static File jar2Dex(File aarFile) throws IOException, InterruptedException {
File fakeDex = new File(aarFile.getParent() + File.separator + "temp");
System.out.println("jar2Dex: aarFile.getParent(): " + aarFile.getParent());
Zip.unZip(aarFile, fakeDex);
File[] files = fakeDex.listFiles((file, s) -> s.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");
Dx.dxCommand(aarDex, classes_jar);
return aarDex;
}
步骤二:壳应用压缩
public static void zip(File dir, File zip) throws Exception {
zip.delete();
CheckedOutputStream cos = new CheckedOutputStream(new FileOutputStream(
zip), new CRC32());
ZipOutputStream zos = new ZipOutputStream(cos);
compress(dir, zos, "");
zos.flush();
zos.close();
}
/**
* 压缩文件或文件夹
*
* @param srcFile 待压缩源文件
* @param zos {@link ZipOutputStream} 压缩文件输出流
* @param basePath 源文件所在的相对路径
* @throws Exception 抛出异常
*/
private static void compress(File srcFile, ZipOutputStream zos,
String basePath) throws Exception {
if (srcFile.isDirectory()) {
compressDir(srcFile, zos, basePath);
} else {
compressFile(srcFile, zos, basePath);
}
}
/**
* 压缩文件夹
*
* @param dir 待压缩文件
* @param zos {@link ZipOutputStream} 压缩文件输出流
* @param basePath 源文件所在的相对路径
* @throws Exception 抛出异常
*/
private static void compressDir(File dir, ZipOutputStream zos,
String basePath) throws Exception {
File[] files = dir.listFiles();
if (files == null) return;
//如果待压缩文件夹内并没有文件,直接创建压缩节点并输出到压缩文件输出流,否则遍历其中文件输出到输出流
if (files.length < 1) {
ZipEntry entry = new ZipEntry(basePath + dir.getName() + "/");
zos.putNextEntry(entry);
zos.closeEntry();
} else {
for (File file : files) {
compress(file, zos, basePath + dir.getName() + "/");
}
}
}
/**
* 压缩文件
*
* @param file 待压缩文件
* @param zos {@link ZipOutputStream} 压缩文件输出流
* @param dir 源文件所在的相对路径
* @throws Exception 抛出异常
*/
private static void compressFile(File file, ZipOutputStream zos, String dir)
throws Exception {
//当前文件先对于根节点的路径
String dirName = dir + file.getName();
String[] dirNameSplit = dirName.split("/");
StringBuilder buffer = new StringBuilder();
if (dirNameSplit.length > 1) {
for (int i = 1; i < dirNameSplit.length; i++) {
buffer.append("/");
buffer.append(dirNameSplit[i]);
}
} else {
buffer.append("/");
}
ZipEntry entry = new ZipEntry(buffer.substring(1));
zos.putNextEntry(entry);
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
int count;
byte[] data = new byte[1024];
while ((count = bis.read(data, 0, 1024)) != -1) {
zos.write(data, 0, count);
}
bis.close();
zos.closeEntry();
}
步骤三:壳应用签名
public static void signature(File unsignedApk, File signedApk) throws InterruptedException, IOException {
final String[] cmd = {"/bin/sh",
"-c",
"jarsigner", "-sigalg", "MD5withRSA",
"-digestalg", "SHA1",
"-keystore", "/home/ciruy/.android/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("签名执行失败");
}
System.out.println("finish signed");
process.destroy();
}
二. Java执行终端命令
Java执行终端命令归根结底就是新建一个子进程用以运行终端命令。具体执行的命令在不同的平台上不同,windows和linux平台可以参考此处。
如下是我在Ubuntu16.04
环境上执行一些终端命令的相关代码:
执行jar转dex文件的相关代码
public static void dxCommand(File aarDex, File classes_jar) throws IOException, InterruptedException {
Runtime runtime = Runtime.getRuntime();
String[] jar2dexExeCmd = new String[]{"/bin/sh",
"-c",
"dx --dex --output=" + aarDex.getAbsolutePath() + " " +
classes_jar.getAbsolutePath()};
Process process = runtime.exec(jar2dexExeCmd);
try {
process.waitFor();
} catch (InterruptedException e) {
e.printStackTrace();
throw e;
}
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(), StandardCharsets.UTF_8));
throw new RuntimeException("dx run failed");
}
process.destroy();
}
执行签名相关代码
public static void signature(File unsignedApk, File signedApk) throws InterruptedException, IOException {
final String[] cmd = {"/bin/sh",
"-c",
"jarsigner", "-sigalg", "MD5withRSA",
"-digestalg", "SHA1",
"-keystore", "/home/ciruy/.android/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("签名执行失败");
}
System.out.println("finish signed");
process.destroy();
}
内容总结
以上是互联网集市为您收集整理的Java IO在Android中应用(二):APK加固全部内容,希望文章能够帮你解决Java IO在Android中应用(二):APK加固所遇到的程序开发问题。 如果觉得互联网集市技术教程内容还不错,欢迎将互联网集市网站推荐给程序员好友。
内容备注
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 gblab@vip.qq.com 举报,一经查实,本站将立刻删除。
内容手机端
扫描二维码推送至手机访问。