众所周知,在安卓项目中,混淆的时候,字符串是不参与混淆的,是以明文的方式打包到dex文件中。App或者sdk被逆向后,很容易就发现原始的字符串信息。很多代码静态扫描工具也会根据字符串来判定代码是否存在风险。举个例子,sdk中有一部分代码是判定应用是否拥有某个权限,这行代码在被静态扫描时,可能被扫出sdk获取敏感权限的风险,如果对权限字符串进行加密,则可以绕过。与此同时,字符串加密也是各安全厂商对代码处理的要求之一。本例中是基于Gradle插件、TransformApi以及字节码注入工具ASM来实现的。
项目已经上传到jcenter,可以快速集成。项目github地址
相关工具
Gradle 插件
基于Gradle build api 实现的工具,可以参与Gradle构建过程,运行插件代码。接口定义在gradleApi中:
org.gradle.api.Plugin
,自定义插件需要实现该接口。
TransformApi
安卓打包过程的Api,定义在tools
包中:com.android.build.api.transform.Transform
,需要注意的是,只支持Gradle1.5.0以上版本,目前Gradle版本已经开发到3.x.x。
ASM字节码注入
效率较高、使用偏复杂的字节码注入工具
Gradle插件、TransformApi以及字节码注入工具ASM在前一篇文章中Gadle插件实现代码插桩与构件时依赖有详细的介绍,不了解的可以参考
插件实现
字符串寻找
插件的核心之一是寻找代码中的字符串常量,这要从字节码指令以及类的加载顺序说起。
在字节码指令中,将字符常量压入操作数栈的指令是:ldc
、ldc_w
两个指令,在ASM对JVM指令集转换中,会对ldc
进行自动转换成ldc_w
,这从接口描述中可以看出:
* Defines the JVM opcodes, access flags and array type codes. This interface
* does not define all the JVM opcodes because some opcodes are automatically
* handled. For example, the xLOAD and xSTORE opcodes are automatically replaced
* by xLOAD_n and xSTORE_n opcodes when possible. The xLOAD_n and xSTORE_n
* opcodes are therefore not defined in this interface. Likewise for LDC,
* automatically replaced by LDC_W or LDC2_W when necessary, WIDE, GOTO_W and
* JSR_W.
因此,我们只需要对ldc指令进行捕获就可以,在ASM的指令类型接口:Opcodes
有如下定义:
int LDC = 18; // visitLdcInsn
很明确的告诉开发者,在ASM中,ldc指令对应的方法是:visitLdcInsn
到这里,其实还有一个问题,就是调用类的初始化方法<clinit>
之前,会对标识为final
+static
的成员变量赋予初始值,从而造成该成员变量在类的初始化以及后续流程中不会触发常量压入操作数栈的问题。举个例子,我们通过ASMified
生成ASM字节码指令文件。
原始文件:
private static final String S1 = "this is static final const variable";
对应ASMified
格式文件为:
fv = cw.visitField(ACC_PRIVATE + ACC_FINAL + ACC_STATIC, "S1", "Ljava/lang/String;", null, "this is static final const variable");
可以看出S1在定义时被赋予了值,并且是final
类型的,后续在方法的调用过程中是不会重新访问的。
对于这个问题,本例中是通过检索访问控制符的类型来对成员变量进行改造。具体是
- 针对
final
+static
的成员变量,将字符串类型的初始值赋值为null
,并以键值对的形式保存,在类的初始化方法<clinit>
中,对变量重新赋值 - 检索字节码中所有的LDC指令
字符串加密
在上一步中,已经查找到了所有的字符串,利用加解密lib,对字符串进行加密,并压入操作数栈,然后注入解密函数即可,可以按照下面的样式编码:
@Override
public void visitLdcInsn(Object cst) {
if (cst instanceof String) {
// 生成随机秘钥和IV
int length = randomLength(config.encType);
String k = CipherUtil.randomString(length);
String iv = CipherUtil.randomString(length);
// 加密原始字符串
String encryption = cipher(config.encType).ee((String) cst,k,iv);
mv.visitLdcInsn(encryption);
mv.visitLdcInsn(k);
mv.visitLdcInsn(iv);
// 注入解密
mv.visitMethodInsn(Opcodes.INVOKESTATIC, STRING_ENC_OWNER, methodString(config.encType),
STRING_ENC_P, false);
} else {
mv.visitLdcInsn(cst);
}
}
到此,注入流程已经结束。
字符串解密
解密函数是在扫描字节码时注入的,插件会将加解密lib添加到集成方的依赖中,这在Plugin
的apply
方法中处理的。
def libImpl = "com.github.box:string:1.0.5@jar"
def list = project.getConfigurations().toList().iterator()
while (list.hasNext()) {
def config = list.next().getName()
if ("implementation" == config) {
project.getDependencies().add(config, libImpl)
println("app implementation:" + libImpl
}
}
插件通过配置选项
stringExt {
encType = "base64"
exclude = ["androidx"]
}
来确定加解密方法,加密时会选择对应的加密函数,并注入对应的解密函数,本例中解密函数为:
public class XxVv {
/**
* base64
*
* @param v 密文
* @param k Key
* @param i Iv
* @return 明文
*/
public static String xr(String v, String k, String i) {
return new Base64StringCipher().dd(v, k, i);
}
/**
* hex
*
* @param v 密文
* @param k Key
* @param i Iv
* @return 明文
*/
public static String rx(String v, String k, String i) {
return new HexStringCipher().dd(v, k, i);
}
/**
* aes
*
* @param v 密文
* @param k Key
* @param i Iv
* @return 明文
*/
public static String vv(String v, String k, String i) {
return new AesStringCipher().dd(v, k, i);
}
/**
* xor
*
* @param v 密文
* @param k Key
* @param i Iv
* @return 明文
*/
public static String vx(String v, String k, String i) {
return new XorStringCipher().dd(v, k, i);
}
}
至此,整个字符串加密流程就已经结束了。可以看下Base64加密的效果。
集成插件前
public class Util {
private static final String S1 = "this is static final const variable";
private static String S2 = "this is static const variable";
private final String S3 = "this is final const variable";
private String S4 = "this is normal variable";
public Util() {
Log.e("wh", "normal block string");
}
public void print() {
Log.e("wh", "S1=this is static final const variable");
Log.e("wh", "S2=" + S2);
Log.e("wh", "S3=this is final const variable");
Log.e("wh", "S4=" + this.S4);
}
static {
Log.e("wh", "this is static block");
}
}
集成插件后后
public class Util {
private static final String S1 = XxVv.xr("dGhpcyBpcyBzdGF0aWMgZmluYWwgY29uc3QgdmFyaWFibGU=");
private static String S2 = XxVv.xr("dGhpcyBpcyBzdGF0aWMgY29uc3QgdmFyaWFibGU=");
private final String S3 = XxVv.xr("dGhpcyBpcyBmaW5hbCBjb25zdCB2YXJpYWJsZQ==");
private String S4 = XxVv.xr("dGhpcyBpcyBub3JtYWwgdmFyaWFibGU=");
public Util() {
Log.e(XxVv.xr("d2g="), XxVv.xr("bm9ybWFsIGJsb2NrIHN0cmluZw=="));
}
public void print() {
Log.e(XxVv.xr("d2g="), XxVv.xr("UzE9dGhpcyBpcyBzdGF0aWMgZmluYWwgY29uc3QgdmFyaWFibGU="));
Log.e(XxVv.xr("d2g="), XxVv.xr("UzI9") + S2);
Log.e(XxVv.xr("d2g="), XxVv.xr("UzM9dGhpcyBpcyBmaW5hbCBjb25zdCB2YXJpYWJsZQ=="));
Log.e(XxVv.xr("d2g="), XxVv.xr("UzQ9") + this.S4);
}
static {
Log.e(XxVv.xr("d2g="), XxVv.xr("dGhpcyBpcyBzdGF0aWMgYmxvY2s="));
}
}
很明显,原先可读的字符串类容被编码了,变成了不可读的字符序列,完成了字符串的加密流程
总结一下
该工具对项目进行构建过程中的侵入,完全不会影响开发流程,集成简单,功能明确。
随机秘钥的同时,增加安全性。解决手动加密的烦劳。
网友评论