Android应用的加固和逆向一直以来都是大家研究的热点问题之一,加密与破解之间的攻防更是战得如火如荼。虽然其间诞生出了Dex加壳、res混淆等技术,但是实际上应用并不广泛,一是由于大部分防逆向服务都是收费的,二是性能影响较大,三是打包流程操作复杂。市场上大部分的App都是没有做任何的逆向防御,在Jadx、ApkTool等逆向工具面前,几乎同没穿衣服的女人一样毫无隐私。当然,具体的逆向技术我们不再深入讨论,还是切入本篇博客的正题:对Dex中字符串加密。
在绝大多数的Android应用当中,很多隐私信息都是以字符串的形式存在的,比如接入的第三方平台的AppId、AppSecret,又比如接口地址字段等等,这些一般都是明文存在的。如果我们能在打包时对Dex中的字符串加密替换,并在运行时调用解密,这样就能够避免字符串明文存在于Dex中。虽然,无法完全避免被破解,但是加大了逆向提取信息的难度,安全性无疑提高了很多。
这一类似技术其实已经有大厂实现并应用了,比如网易云音乐,我们使用Jadx查看应用内容时,发现几乎所有字符串都做了加密处理,情况如下:
对于字符串加密的处理,一般来说有两种思路。
1、在开发阶段开发者使用加密后的字符串然后手动调用解密。这无疑是最简单的方式,不过维护性差,工作量大,而且对于应用中成千上万的字符串如果全部加密人工耗时巨大。
2、编译后修改字节码,动态植入加密后的字符串并自动调用解密。这是最智能的方式,也不影响正常开发,不过实现起来稍有难度。
对于第一种方式,大家或多或少可能都使用过,这里不多讲,本文的重点是研究第二种方式,简称StringFog,源码已经开源至Github,供大家参考:https://github.com/MegatronKing/StringFog
一、加密方式
数据加解密方式有很多种,考虑到性能和实现问题,这里使用对称加密,StringFog使用的是Base64 + XOR算法。
先来看下经典的异或算法,这里通过对待加(解)密数据与一个字符串循环异或达到简单加(解)密的处理,代码如下:
private static byte[] xor(byte[] data, String key) {
int len = data.length;
int lenKey = key.length();
int i = 0;
int j = 0;
while (i < len) {
if (j >= lenKey) {
j = 0;
}
data[i] = (byte) (data[i] ^ key.charAt(j));
i++;
j++;
}
return data;
}
加密时对数据进行异或得到加密数据,解密时对数据再次进行异或得到解密数据。同时考虑到字符编码的特性,需要使用Base64做编(解)码处理:
public static String encode(String data, String key) {
return new String(Base64.encode(xor(data.getBytes(), key), Base64.NO_WRAP));
}
public static String decode(String data, String key) {
return new String(xor(Base64.decode(data, Base64.NO_WRAP), key));
}
这样,既解决了字符编码的问题,又解决了加解密的问题(注意Base64严格意义上来说并非属于加密算法),而且在性能上又得到了可靠的保证。
二、字节码植入
对Dex中的字符串进行查找和替换不难,但是同时还要植入解密调用就不太容易实现了。但是,如果对编译后Dex前的字节码文件进行操作就相对容易多了,而且对此有强大的ASM包可以使用,著名的热修复框架Nuwa在解决类ISPREVERIFIED标记是也是这样处理的,下面我们来看下实现。
1、Gradle Android的transform机制
使用Gradle进行Android项目编译和打包时,为了提供更好的自定义任务操作,Gradle Android插件提供了强大的transform机制,可以对字节码文件和资源文件做自定义操作。比如进行Jar包合并、MultiDex拆分、代码混淆等都是通过这种机制来实现的。比较细心的童鞋会发现,执行编译或者打包时能够看到如下任务流:
:app:transformClassesWithJarMergingForDebug