作者 | 百度APP技术平台

导读 

在挪动互联网疾速倒退的背景下,爱护Android应用程序的安全性和知识产权变得尤为重要。为了避免歹意攻打和未受权拜访,通常采纳对dex文件进行代码加固来爱护应用程序。随着Android加固技术通过动静加载、不落地加载、指令抽取、java2cpp、VMP等技术一直演进和改良,VMP加固技术成为一种高安全性解决方案。因而,本文将着重介绍一种实现和落地VMP技术的思路,以帮忙大家理解其工作原理和利用场景。

全文8359字,预计浏览工夫21分钟。

01 问题背景

在挪动互联网疾速倒退的背景下,Android 作为寰球最受欢迎的挪动操作系统,吸引了大量开发者和用户。随着利用市场的竞争加剧,爱护应用程序的安全性和知识产权变得越来越重要。

同时,随着公司业务的倒退,百度与内部友商深度单干,须要对外输入了百度业务能力SDK。在这种背景下,对Android代码进行加固成为了一种必要的安全措施。加固能够进步应用程序的安全性,爱护知识产权,避免逆向工程和破解。

02 问题剖析

Android 应用程序是由 Java/Kotlin 语言编写而成,而后打包成 APK 文件。Java 代码被编译成 APK/AAR 中的 dex 文件,dalvik/art 虚拟机解释执行 dex 中的字节码。攻击者能够应用反编译工具很容易的逆向剖析 dex 文件,了解代码要害逻辑,减少恶意代码,再打包回 APK 文件。

能够看到,dex 文件就是代码加固的爱护外围!

03 加固调研

为了解决对 dex文件的代码加固,咱们进行了相干技术调研,其实在Android代码平安畛域,相干技术始终属于一直攻防演进的过程。如下是业界罕用的加固技术计划:比方最后的360加固给APK加壳,通过不落地动静加载实现加固;市场上罕用的类办法抽取指令加固;以及将java办法转native办法jni调用等。

3.1 DexClassLoader 动静加载机制

利用 Android 零碎的 DexClassLoader 动静加载机制,通过将爱护的 dex 文件解压解密后,动静加载到内存中执行。

这种形式无效地抵挡了 APK 文件的动态剖析,使得逆向剖析者无奈在 APK 文件中找到实在的 dex 文件。然而因为动静加载技术次要依赖于java的动静加载机制,所以要求要害逻辑局部必须进行解压,并且开释到文件系统。

这种动静加载技术不足之处在于:1.这一解压开释机制就给攻击者留下间接获取对应文件的机会; 2.能够通过hook虚拟机要害函数,进行dump出原始的dex文件数据。

3.2 Hook 技术

针对 DexClassLoader 动静加载机制的爱护缺点,采纳 Hook 技术来解决问题。

在动静加载过程中,通过替换 DexClassLoader 执行过程中的 dex 内存,将其替换为实在 dex 文件的内存,从而实现了无需将 dex 落地的加载形式。

然而,dex 文件尽管不会解密并保留到文件系统,但它在内存中是残缺存在的。因而,在利用程序运行后,逆向剖析者能够通过内存搜寻的形式将 dex 文件转储进去。

3.3 指令抽取

为了反抗逆向开发通过内存搜寻的形式将 dex 文件转储进去,加固技术采纳了函数抽取的办法,使得 dex 文件在内存中始终处于不残缺的状态。

其实现思路大抵如下:

1、对要爱护的 dex 文件进行预处理,将须要爱护的函数指令抽取进去并进行加密存储,同时在原地位填充 nop 指令。

2、当 dalvik/art 执行到抽取的函数时,利用 hook 技术拦挡 libdalvik.so/libart.so 中的指令读取局部,将函数对应的实在指令解密并填充,使得 dalvik/art 可能持续解释执行。

随着逆向技术的一直倒退,革新 dalvik 并遍历所有 dex 办法,以及内存重组 dex,成为了反抗此种加固爱护的无效办法。其中,dexhunter 是该畛域的次要代表之一。

3.4 java2cpp 技术

随着内存脱壳机的呈现,指令抽取的保护方式逐步失去有效性。为了应答这一问题,java2cpp 技术开始被引入到加固爱护中。

外围是对 dex 中的函数进行解决,将函数中的 dalvik 指令转换成等效的 cpp 代码(基于 JNI),而后编译成本地的动态链接库(native so 库),并将爱护的办法标记为 native 属性。这样,在执行到受爱护的办法时,执行流会转移到本地层执行对应的 cpp 代码。

比方原函数:

public class HelloVMP2 {    public int compute(int a, int b) {        int c = a + a;        int d = a * b;        int e = a - b;        int f = a / b;        int result = c + d + e + f;        return result;    }}

转换后:

public class HelloVMP2 {    static {        System.loadLibrary("hello_vmp2");    }    public native int compute(int a, int b);}
extern "C" JNIEXPORT jint JNICALLJava_com_vmp_mylibrary_HelloVMP2_compute(JNIEnv* env, jobject obj, jint a, jint b) {    jint c = a + a;    jint d = a * b;    jint e = a - b;    jint f = a / b;    jint result = c + d + e + f;    return result;}

这种形式下,仅将 java 转 cpp 编译成动态链接库,然而so代码仍然能够被破解,在此基础上其实还是能够持续进步代码爱护的安全性,那就是 DEX-VMP 技术。

3.5 DEX-VMP

DEX-VMP 原理了解起来比拟容易,其针对的爱护单位也是函数。将办法的 dalvik 指令转换成等价的自定义指令,函数原指令替换成自定义 VM 的调用入口指令,再将函数参数通过 VMP 入口传入到自定义 VM 中执行,自定义 VM 解释执行自定义指令。

如图,当 Dalvik VM 执行到 DEX-VMP 爱护的函数时,执行的是 VMP native 入口函数,开始进入 VMP 的执行流程,VMP 首先会初始化 dex 文件信息,接着获取该爱护办法的一些信息,比方寄存器数量,待执行指令的内存地位等,而后初始化寄存器存储构造,最初进入到解释器中解释执行每一条指令。在解释执行的过程,如果执行到内部函数,就会应用 JNI CallMethod 的模式调用,让其切换回 Dalvik VM,让 Dalvik 去执行真正的函数。

加固过程原函数的代码逻辑替换为 native 办法,同时对 Custom VM 进行初始化,原函数 native 办法负责将参数传入到 Custom VM 中,Custom VM 解释执行原代码的等价指令。

实现 DEX-VMP 总体来说须要两步:

1、对原 dex 解决,找到要爱护的办法,将原指令翻译成等价指令,加密存储,并将原指令替换为 VMP 入口指令

2、实现 VM,解释执行存储的等价指令

3.6 加固计划比照

能够看到,加固技术是一直攻防降级的过程,上面咱们将以上加固技术分为五代进行比照:

由以上比照咱们能够看出,在加固技术演进过程中,VMP计划是倒退到目前,加固平安度最高的形式,本着安全性角度登程,咱们抉择VMP计划重点介绍与剖析,以下是对于我的项目中VMP加固的剖析过程。

04 DEX-VMP加固落地实现

以下是咱们要爱护的一段示例代码:

package com.vmp.mylibrary;public class HelleVMP3 {    public int compute(int a, int b) {        int c = a + a;        int d = a * b;        int e = a - b;        int f = a / b;        int result = c + d + e + f;        return result;    }}

4.1 dex 文件预处理

dex 预处理次要做两方面工作:

1、爱护办法的原指令拷贝进去并存储

2、爱护办法的原指令替换成 VMP 入口办法

将要爱护的 java 代码编译成 dex 文件,放入 010editor 中能够查看 compute 办法对应的指令数据:

能够看到蓝色区域蕴含的办法所须要的寄存器数,外部参数,内部参数及指令长度。这些都是 VM 须要的要害信息,须要存储起来。而后将指令替换为 DEX-VMP 的 native 入口指令。

有一些工具能够帮咱们实现以上操作,比方 dexlib2,应用该工具能够对指定办法结构 dalvik 指令,或获取办法的指令数据。该工具的具体应用办法大家能够自定搜寻。

4.2 寄存器结构设计

通过dexdump 命令查看,原办法二进制构造内容如下:

Virtual methods   -    #0              : (in Lcom/vmp/mylibrary/HelloVMP3;)      name          : 'compute'      registers     : 6      ins           : 3      outs          : 0      insns size    : 11 16-bit code units28e588:                                        |[28e588] com.vmp.mylibrary.HelloVMP3.compute:(II)I28e598: 9000 0404                              |0000: add-int v0, v4, v428e59c: 9201 0405                              |0002: mul-int v1, v4, v528e5a0: 9102 0405                              |0004: sub-int v2, v4, v528e5a4: b354                                   |0006: div-int/2addr v4, v528e5a6: b010                                   |0007: add-int/2addr v0, v128e5a8: b020                                   |0008: add-int/2addr v0, v228e5aa: b040                                   |0009: add-int/2addr v0, v428e5ac: 0f00                                   |000a: return v0

从示例 compute 办法的一些 hex 数据中,能够失去一些要害信息:

compute 办法在执行过程中须要应用到 6 个寄存器,传入参数 3 个, 没有应用 try 构造,指令数据为 16 个字。

Dalvik 寄存器最大长度为 32bit,咱们能够间接申请一段内存来示意寄存器:

regptr_t regs[6];regs[0] = 0;regs[1] = 0;regs[2] = 0;regs[3] = 0;regs[4] = 0;regs[5] = 0;regs[3] = (regptr_t) thiz;regs[4] = p1;regs[5] = p2;u1 reg_flags[6];reg_flags[0] = 0;reg_flags[1] = 0;reg_flags[2] = 0;reg_flags[3] = 0;reg_flags[4] = 0;reg_flags[5] = 0;reg_flags[3] = 1;

regs 示意寄存器,4 个寄存器别离为 regs [0], regs [1], regs [2], regs [3]。regs\_bits\_obj 示意对应寄存器是否是 Object,比方 regs [3] 是 Object,则 regs\_bits\_obj [3] = 1,非 object 的状况均为 0;

每一个爱护办法在进入 VM 后,咱们就像示例这样创立好这样的寄存器单元,供 VM 在解释执行阶段应用,执行结束销毁即可。

留神这个过程的业余的加固工具会在 dex 预处理过程中辨认二进制构造内容进行执行,无需每爱护一个办法独自开发。

4.3 虚拟机实现

咱们就以示例 compute 办法中的 add-int, mul-int, sub-int, div-int 这几条指令来实现一个繁难的解释器

介绍一下这几条指令的作用:add-int、mul-int、sub-int、div-int 对两个源寄存器执行已确定的二元运算,并将后果存储到指标寄存器中。

首先定义自定义虚拟机须要执行的vmCode构造:

typedef struct {    const u2 *insns; // 指令    const u4 insnsSize; // 指令大小    regptr_t *regs; // 寄存器    u1 *reg_flags; // 寄存器数据类型标记,次要标记是否为对象    const u1 *triesHandlers; // 异样表} vmCode;

自定义Opcode:

enum Opcode {    OP_ADD_INT = 0x3a,    OP_MUL_INT = 0xe4,    OP_SUB_INT = 0x77,    OP_DIV_INT_2ADDR = 0x6c,    OP_ADD_INT_2ADDR = 0xcf,    OP_RETURN = 0xde,};

指标办法转化的 native 办法:

static jint Java_com_vmp_mylibrary_HelloVMP3_compute__II_I(JNIEnv *env, jobject thiz , jint p1, jint p2) {    regptr_t regs[6];    regs[0] = 0;    regs[1] = 0;    regs[2] = 0;    regs[3] = 0;    regs[4] = 0;    regs[5] = 0;    regs[3] = (regptr_t) thiz;    regs[4] = p1;    regs[5] = p2;    u1 reg_flags[6];    reg_flags[0] = 0;    reg_flags[1] = 0;    reg_flags[2] = 0;    reg_flags[3] = 0;    reg_flags[4] = 0;    reg_flags[5] = 0;    reg_flags[3] = 1;    static const u2 insns[] = {0x00b3, 0x0404, 0x0120, 0x0504, 0x02ee, 0x0504, 0x546c, 0x10a9, 0x20a9, 0x40a9, 0x00ad,     };    const u1 *tries = NULL;    const vmCode code = {            .insns=insns,            .insnsSize=11,            .regs=regs,            .reg_flags=reg_flags,            .triesHandlers=tries    };    jvalue value = vmInterpret(env,                                &code,                                &dvmResolver);    return value.i;}

执行指令解决逻辑:

#define OP_END#define INST_AA(_inst)      ((_inst) >> 8)#define FETCH(_offset)     (pc[(_offset)])#define SET_REGISTER(_idx, _val)            \DELETE_LOCAL_REF(_idx);                     \(fp[(_idx)] =(u4) (_val));                  \SET_REGISTER_FLAGS(_idx, 0)#define HANDLE_OP_X_INT(_opcode, _opname, _op, _chkdiv)                         HANDLE_OPCODE(_opcode /*vAA, vBB, vCC*/)                                    {                                                                               u2 srcRegs;                                                                 vdst = INST_AA(inst);                                                       srcRegs = FETCH(1);                                                         vsrc1 = srcRegs & 0xff;                                                     vsrc2 = srcRegs >> 8;                                                       ILOGV("|%s-int v%d,v%d", (_opname), vdst, vsrc1);                           ......                                                                  }                                                                           FINISH(2);    #define HANDLE_OP_X_INT(_opcode, _opname, _op, _chkdiv)                     \    HANDLE_OPCODE(_opcode /*vAA, vBB, vCC*/)                                \    {                                                                       \        u2 srcRegs;                                                         \        vdst = INST_AA(inst);                                               \        srcRegs = FETCH(1);                                                 \        vsrc1 = srcRegs & 0xff;                                             \        vsrc2 = srcRegs >> 8;                                               \        ILOGV("|%s-int v%d,v%d", (_opname), vdst, vsrc1);                   \        if (_chkdiv != 0) {                                                 \            s4 firstVal, secondVal, result;                                 \            firstVal = GET_REGISTER(vsrc1);                                 \            secondVal = GET_REGISTER(vsrc2);                                \            if (secondVal == 0) {                                           \                dvmThrowArithmeticException(env,"divide by zero");          \                GOTO_exceptionThrown();                                     \            }                                                               \            if ((u4)firstVal == 0x80000000 && secondVal == -1) {            \                if (_chkdiv == 1)                                           \                    result = firstVal;  /* division */                      \                else                                                        \                    result = 0;         /* remainder */                     \            } else {                                                        \                result = firstVal _op secondVal;                            \            }                                                               \            SET_REGISTER(vdst, result);                                     \        } else {                                                            \            /* non-div/rem case */                                          \            SET_REGISTER(vdst, (s4) GET_REGISTER(vsrc1) _op (s4) GET_REGISTER(vsrc2));     \        }                                                                   \    }                                                                       \    FINISH(2);__attribute__((visibility("default")))jvalue vmInterpret(JNIEnv *env, const vmCode *code, const vmResolver *dvmResolver) {    jvalue args_tmp[5]; // 办法调用时参数传递(参数数量小于等于5)    jvalue retval;    regptr_t *fp = code->regs; // 寄存器    u1 *fp_flags = code->reg_flags; // 寄存器类型标识    const u2 *pc = code->insns;    ......    /* File: c/OP_ADD_INT.cpp */    HANDLE_OP_X_INT(OP_ADD_INT, "add", +, 0)        OP_END    /* File: c/OP_SUB_INT.cpp */    HANDLE_OP_X_INT(OP_SUB_INT, "sub", -, 0)        OP_END    /* File: c/OP_MUL_INT.cpp */    HANDLE_OP_X_INT(OP_MUL_INT, "mul", *, 0)        OP_END    /* File: c/OP_DIV_INT.cpp */    HANDLE_OP_X_INT(OP_DIV_INT, "div", /, 1)        OP_END    /* File: c/OP_REM_INT.cpp */    HANDLE_OP_X_INT(OP_REM_INT, "rem", %, 2)        OP_ENDend:    return 0;}

下面是一个解析自定义 opcode 的解释器,大家能够从其中看到解释器就是 while switch 的程序结构,执行到 return 指令时退出循环。

4.4 总结

通过以上实现,能够发现虚拟机加固外围自定义一套opcode用于对爱护办法的指令替换,同时还须要对替换后的指令辨认后,如果对Java函数的调用交给DVM进行解决,如果是原函数指令则创立寄存器交给机器解决。整个加固过程中分为编译器+解释器两局部。

其中编译器负责对打包的AAR或者APK进行加固,加固过程则是将要爱护的办法转换为JNI调用,同时C++局部依据原办法指令生成须要的寄存器与opcode;而解释器则是在运行过程,当执行到JNI调用时,可能对创立的opcode进行辨认,转化原指令与寄存器交由真正的DVM进行执行。

05 兼容与性能

5.1 兼容性危险

兼容危险:

  • 加固计划次要的兼容问题在于无奈脱离JNI实现,而 VM 中 JNI 实现细节不尽相同。比方 Android 5.0 某个小版本中 JNI 实现会存在一个隐含的 jobject(local reference)遗记 delete 掉,当屡次调用该 JNI 函数时,内存溢出不可避免。这个BUG 在之后的 Android 版本中更正过去,也就是说每个 Android 版本进去之后,咱们都要看看 VMP 会不会存在 JNI 兼容性方面的 BUG。

躲避倡议:

  • 每个Android 版本更新须要重点关注JNI实现的变动,是否存在 JNI 兼容性方面问题。

5.2 性能问题

产生性能耗费的次要有两点:

  • JNI 调用
  • DEX-VMP 与 零碎 VM 的切换

优化倡议:

  • JNI 调用是性能耗费次要因素。对于一些罕用的 java class,能够在初始化时对立获取 jclass 缓存起来,这能够肯定水平上进步性能,相似的还有防止反复查找 class。
  • 尽量避免全量代码爱护(dex 中所有的办法都 DEX-VMP 爱护,蕴含 Android SDK 的根底类库),排除Android根底类库和开源类库,仅将业务本人的外围逻辑代码办法进行爱护。

06 结语

总结来说,虚拟机加固是一种能够进步应用程序安全性的技术,但它也带来了性能、兼容性和保护老本等方面的挑战。

咱们在应用代码虚拟化时,须要依据应用程序的特点和平安需要,正当抉择和优化虚拟化计划。

——END——

举荐浏览:

搜寻语义模型的大规模量化实际

如何设计一个高效的分布式日志服务平台

视频与图片检索中的多模态语义匹配模型:原理、启发、利用与瞻望

百度离线资源治理

百度APP iOS端包体积50M优化实际(三) 资源优化

代码级品质技术之根本框架介绍