安卓字符串加密插件實(shí)現(xiàn)

眾所周知,在安卓項(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)行改造。具體是

  1. 針對(duì)final+static的成員變量,將字符串類型的初始值賦值為null,并以鍵值對(duì)的形式保存,在類的初始化方法<clinit>中,對(duì)變量重新賦值
  2. 檢索字節(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添加到集成方的依賴中,這在Pluginapply方法中處理的。

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)加密的煩勞。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容