作者 | 百度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 SDK安全加固问题与分析

运用 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 JNICALL
Java_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 解说履行自界说指令。

Android SDK安全加固问题与分析

如图,当 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 加固计划对比

能够看到,加固技能是不断攻防升级的进程,下面咱们将以上加固技能分为五代进行对比:

Android SDK安全加固问题与分析

由以上对比咱们能够看出,在加固技能演进进程中,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 办法对应的指令数据:

Android SDK安全加固问题与分析

能够看到蓝色区域包括的办法所需求的寄存器数,内部参数,外部参数及指令长度。这些都是 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 units
28e588:                                        |[28e588] com.vmp.mylibrary.HelloVMP3.compute:(II)I
28e598: 9000 0404                              |0000: add-int v0, v4, v4
28e59c: 9201 0405                              |0002: mul-int v1, v4, v5
28e5a0: 9102 0405                              |0004: sub-int v2, v4, v5
28e5a4: b354                                   |0006: div-int/2addr v4, v5
28e5a6: b010                                   |0007: add-int/2addr v0, v1
28e5a8: b020                                   |0008: add-int/2addr v0, v2
28e5aa: b040                                   |0009: add-int/2addr v0, v4
28e5ac: 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_END
end:
    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优化实践(三) 资源优化

代码级质量技能之基本框架介绍