眾所周知,在安卓項(xiàng)目中,混淆的時(shí)候,字符串是不參與混淆的,是以明文的方式打包到dex文件中。App或者sdk被逆向后,很容易就發(fā)現(xiàn)原始的字符串信息。很多代碼靜態(tài)掃描工具也會(huì)根據(jù)字符串來(lái)判定代碼是否存在風(fēng)險(xiǎn)。舉個(gè)例子,sdk中有一部分代碼是判定應(yīng)用是否擁有某個(gè)權(quán)限,這行代碼在被靜態(tài)掃描時(shí),可能被掃出sdk獲取敏感權(quán)限的風(fēng)險(xiǎn),如果對(duì)權(quán)限字符串進(jìn)行加密,則可以繞過(guò)。與此同時(shí),字符串加密也是各安全廠商對(duì)代碼處理的要求之一。本例中是基于Gradle插件、TransformApi以及字節(jié)碼注入工具ASM來(lái)實(shí)現(xiàn)的。
項(xiàng)目已經(jīng)上傳到j(luò)center,可以快速集成。項(xiàng)目github地址
相關(guān)工具
Gradle 插件
基于Gradle build api 實(shí)現(xiàn)的工具,可以參與Gradle構(gòu)建過(guò)程,運(yùn)行插件代碼。接口定義在gradleApi中:
org.gradle.api.Plugin,自定義插件需要實(shí)現(xiàn)該接口。
TransformApi
安卓打包過(guò)程的Api,定義在tools包中:com.android.build.api.transform.Transform,需要注意的是,只支持Gradle1.5.0以上版本,目前Gradle版本已經(jīng)開發(fā)到3.x.x。
ASM字節(jié)碼注入
效率較高、使用偏復(fù)雜的字節(jié)碼注入工具
Gradle插件、TransformApi以及字節(jié)碼注入工具ASM在前一篇文章中Gadle插件實(shí)現(xiàn)代碼插樁與構(gòu)件時(shí)依賴有詳細(xì)的介紹,不了解的可以參考
插件實(shí)現(xiàn)
字符串尋找
插件的核心之一是尋找代碼中的字符串常量,這要從字節(jié)碼指令以及類的加載順序說(shuō)起。
在字節(jié)碼指令中,將字符常量壓入操作數(shù)棧的指令是:ldc、ldc_w兩個(gè)指令,在ASM對(duì)JVM指令集轉(zhuǎn)換中,會(huì)對(duì)ldc進(jìn)行自動(dòng)轉(zhuǎn)換成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.
因此,我們只需要對(duì)ldc指令進(jìn)行捕獲就可以,在ASM的指令類型接口:Opcodes有如下定義:
int LDC = 18; // visitLdcInsn
很明確的告訴開發(fā)者,在ASM中,ldc指令對(duì)應(yīng)的方法是:visitLdcInsn
到這里,其實(shí)還有一個(gè)問(wèn)題,就是調(diào)用類的初始化方法<clinit>之前,會(huì)對(duì)標(biāo)識(shí)為final+static的成員變量賦予初始值,從而造成該成員變量在類的初始化以及后續(xù)流程中不會(huì)觸發(fā)常量壓入操作數(shù)棧的問(wèn)題。舉個(gè)例子,我們通過(guò)ASMified生成ASM字節(jié)碼指令文件。
原始文件:
private static final String S1 = "this is static final const variable";
對(duì)應(yīng)ASMified格式文件為:
fv = cw.visitField(ACC_PRIVATE + ACC_FINAL + ACC_STATIC, "S1", "Ljava/lang/String;", null, "this is static final const variable");
可以看出S1在定義時(shí)被賦予了值,并且是final類型的,后續(xù)在方法的調(diào)用過(guò)程中是不會(huì)重新訪問(wèn)的。
對(duì)于這個(gè)問(wèn)題,本例中是通過(guò)檢索訪問(wèn)控制符的類型來(lái)對(duì)成員變量進(jìn)行改造。具體是
- 針對(duì)
final+static的成員變量,將字符串類型的初始值賦值為null,并以鍵值對(duì)的形式保存,在類的初始化方法<clinit>中,對(duì)變量重新賦值 - 檢索字節(jié)碼中所有的LDC指令
字符串加密
在上一步中,已經(jīng)查找到了所有的字符串,利用加解密lib,對(duì)字符串進(jìn)行加密,并壓入操作數(shù)棧,然后注入解密函數(shù)即可,可以按照下面的樣式編碼:
@Override
public void visitLdcInsn(Object cst) {
if (cst instanceof String) {
// 生成隨機(jī)秘鑰和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);
}
}
到此,注入流程已經(jīng)結(jié)束。
字符串解密
解密函數(shù)是在掃描字節(jié)碼時(shí)注入的,插件會(huì)將加解密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
}
}
插件通過(guò)配置選項(xiàng)
stringExt {
encType = "base64"
exclude = ["androidx"]
}
來(lái)確定加解密方法,加密時(shí)會(huì)選擇對(duì)應(yīng)的加密函數(shù),并注入對(duì)應(yīng)的解密函數(shù),本例中解密函數(shù)為:
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);
}
}
至此,整個(gè)字符串加密流程就已經(jīng)結(jié)束了??梢钥聪翨ase64加密的效果。
集成插件前
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="));
}
}
很明顯,原先可讀的字符串類容被編碼了,變成了不可讀的字符序列,完成了字符串的加密流程
總結(jié)一下
該工具對(duì)項(xiàng)目進(jìn)行構(gòu)建過(guò)程中的侵入,完全不會(huì)影響開發(fā)流程,集成簡(jiǎn)單,功能明確。
隨機(jī)秘鑰的同時(shí),增加安全性。解決手動(dòng)加密的煩勞。