Android逆向笔记 —— DEX 文件格式解析

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: Android逆向笔记 —— DEX 文件格式解析

系列第一篇文章就分析过 Class 文件格式,我们都知道 .java 源文件经过编译器编译会生成 JVM 可识别的 .class 文件。在 Android 中,不管是 Dalvik 还是 Art,和 JVM 的区别还是很大的。Android 系统并不直接使用 Class 文件,而是将所有的 Class 文件聚合打包成 DEX 文件,DEX 文件相比单个单个的 Class 文件更加紧凑,可以直接在 Android Runtime 下执行。


对于学习热修复框架,加固和逆向相关知识,了解 DEX 文件结构是很有必要的。再之前解析过 Class 文件和 AndroidManifest.xml 文件结构之后,发现看二进制文件看上瘾了。。后面会继续对 Apk 文件中的其他文件结构进行分析,例如 so 文件,resources.arsc 文件等。


DEX 文件的生成


在解析 DEX 文件结构之前,先来看看如何生成 DEX 文件。为了方便解析,本篇文章中就不从市场上的 App 里拿 DEX 文件过来解析了,而是手动生成一个最简单的 DEX 文件。还是以 Class 文件解析时候用的例子:

public class Hello {
    private static String HELLO_WORLD = "Hello World!";
    public static void main(String[] args) {
        System.out.println(HELLO_WORLD);
    }
}
复制代码


首先 javac 编译成 Hello.class 文件,然后利用 Sdk 自带的 dx 工具生成 DEX 文件:

dx --dex --output=Hello.dex  Hello.class
复制代码


dx 工具位于 Sdk 的 build-tools 目录下,可添加至环境变量方便调用。dx 也支持多 Class 文件生成 dex。


DEX 文件结构


概览

关于 DEX 文件结构的学习,给大家推荐两个资料。

第一个是看雪神图,出自非虫,

image.png

第二个是 Android 源码中对 DEX 文件格式的定义,dalvik/libdex/DexFile.h,其中详细定义了 DEX 文件中的各个部分。


第三个是 010 Editor,在之前解析 AndroidManifest.xml 文件格式解析 也介绍过,它提供了丰富的文件模板,支持常见文件格式的解析,可以很方便的查看文件结构中的各个部分及其对应的十六进制。一般我在代码解析文件结构的时候都是对照着 010 Editor 来进行分析。下面贴一张 010 Editor 打开之前生成的 Hello.dex 文件的截图:

image.png

我们可以一目了然的看到 DEX 的文件结构,着实是一个利器。在详细解析之前,我们先来大概给 DEX 文件分个层,如下图所示:

image.png

文末我放了一张详细的思维导图,也可以对着思维导图来阅读文章。

依次解释一下:

  • header : DEX 文件头,记录了一些当前文件的信息以及其他数据结构在文件中的偏移量
  • string_ids : 字符串的偏移量
  • type_ids : 类型信息的偏移量
  • proto_ids : 方法声明的偏移量
  • field_ids : 字段信息的偏移量
  • method_ids : 方法信息(所在类,方法声明以及方法名)的偏移量
  • class_def : 类信息的偏移量
  • data : : 数据区
  • link_data : 静态链接数据区


headerdata 之间都是偏移量数组,并不存储真实数据,所有数据都存在 data 数据区,根据其偏移量区查找。对 DEX 文件有了一个大概的认识之后,我们就来详细分析一下各个部分。


header

DEX 文件头部分的具体格式可以参考 DexFile.h 中的定义:

struct DexHeader {
    u1  magic[8];           // 魔数
    u4  checksum;           // adler 校验值
    u1  signature[kSHA1DigestLen]; // sha1 校验值
    u4  fileSize;           // DEX 文件大小
    u4  headerSize;         // DEX 文件头大小
    u4  endianTag;          // 字节序
    u4  linkSize;           // 链接段大小
    u4  linkOff;            // 链接段的偏移量
    u4  mapOff;             // DexMapList 偏移量
    u4  stringIdsSize;      // DexStringId 个数
    u4  stringIdsOff;       // DexStringId 偏移量
    u4  typeIdsSize;        // DexTypeId 个数
    u4  typeIdsOff;         // DexTypeId 偏移量
    u4  protoIdsSize;       // DexProtoId 个数
    u4  protoIdsOff;        // DexProtoId 偏移量
    u4  fieldIdsSize;       // DexFieldId 个数
    u4  fieldIdsOff;        // DexFieldId 偏移量
    u4  methodIdsSize;      // DexMethodId 个数
    u4  methodIdsOff;       // DexMethodId 偏移量
    u4  classDefsSize;      // DexCLassDef 个数
    u4  classDefsOff;       // DexClassDef 偏移量
    u4  dataSize;           // 数据段大小
    u4  dataOff;            // 数据段偏移量
};
复制代码


其中的 u 表示无符号数,u1 就是 8 位无符号数,u4 就是 32 位无符号数。

magic 一般是常量,用来标记 DEX 文件,它可以分解为:

文件标识 dex + 换行符 + DEX 版本 + 0
复制代码


字符串格式为 dex\n035\0,十六进制为 0x6465780A30333500

checksum 是对去除 magicchecksum 以外的文件部分作 alder32 算法得到的校验值,用于判断 DEX 文件是否被篡改。


signature 是对除去 magicchecksumsignature 以外的文件部分作 sha1 得到的文件哈希值。


endianTag 用于标记 DEX 文件是大端表示还是小端表示。由于 DEX 文件是运行在 Android 系统中的,所以一般都是小端表示,这个值也是恒定值 0x12345678

其余部分分别标记了 DEX 文件中其他各个数据结构的个数和其在数据区的偏移量。根据偏移量我们就可以轻松的获得各个数据结构的内容。下面顺着上面的 DEX 文件结构来认识第一个数据结构 string_ids


string_ids

struct DexStringId {
    u4 stringDataOff;
};
复制代码


string_ids 是一个偏移量数组,stringDataOff 表示每个字符串在 data 区的偏移量。根据偏移量在 data 区拿到的数据中,第一个字节表示的是字符串长度,后面跟着的才是字符串数据。这块逻辑比较简单,直接看一下代码:

private void parseDexString() {
    log("\nparse DexString");
    try {
        int stringIdsSize = dex.getDexHeader().string_ids__size;
        for (int i = 0; i < stringIdsSize; i++) {
            int string_data_off = reader.readInt();
            byte size = dexData[string_data_off]; // 第一个字节表示该字符串的长度,之后是字符串内容
            String string_data = new String(Utils.copy(dexData, string_data_off + 1, size));
            DexString string = new DexString(string_data_off, string_data);
            dexStrings.add(string);
            log("string[%d] data: %s", i, string.string_data);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}
复制代码


打印结果如下:

parse DexString
string[0] data: <clinit>
string[1] data: <init>
string[2] data: HELLO_WORLD
string[3] data: Hello World!
string[4] data: Hello.java
string[5] data: LHello;
string[6] data: Ljava/io/PrintStream;
string[7] data: Ljava/lang/Object;
string[8] data: Ljava/lang/String;
string[9] data: Ljava/lang/System;
string[10] data: V
string[11] data: VL
string[12] data: [Ljava/lang/String;
string[13] data: main
string[14] data: out
string[15] data: println
复制代码

其中包含了变量名,方法名,文件名等等,这个字符串池在后面其他结构的解析中也会经常遇到。


type_ids

struct DexTypeId {
    u4  descriptorIdx;
};
复制代码


type_ids 表示的是类型信息,descriptorIdx 指向 string_ids 中元素。根据索引直接在上一步读取到的字符串池即可解析对应的类型信息,代码如下:

private void parseDexType() {
    log("\nparse DexTypeId");
    try {
        int typeIdsSize = dex.getDexHeader().type_ids__size;
        for (int i = 0; i < typeIdsSize; i++) {
            int descriptor_idx = reader.readInt();
            DexTypeId dexTypeId = new DexTypeId(descriptor_idx, dexStringIds.get(descriptor_idx).string_data);
            dexTypeIds.add(dexTypeId);
            log("type[%d] data: %s", i, dexTypeId.string_data);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}
复制代码


解析结果:

parse DexType
type[0] data: LHello;
type[1] data: Ljava/io/PrintStream;
type[2] data: Ljava/lang/Object;
type[3] data: Ljava/lang/String;
type[4] data: Ljava/lang/System;
type[5] data: V
type[6] data: [Ljava/lang/String;
复制代码


proto_ids

struct DexProtoId {
    u4  shortyIdx;          /* index into stringIds for shorty descriptor */
    u4  returnTypeIdx;      /* index into typeIds list for return type */
    u4  parametersOff;      /* file offset to type_list for parameter types */
};
复制代码


proto_ids 表示方法声明信息,它包含以下三个变量:

  • shortyIdx : 指向 string_ids ,表示方法声明的字符串
  • returnTypeIdx : 指向 type_ids ,表示方法的返回类型
  • parametersOff : 方法参数列表的偏移量


方法参数列表的数据结构在 DexFile.h 中用 DexTypeList 来表示:

struct DexTypeList {
    u4  size;               /* #of entries in list */
    DexTypeItem list[1];    /* entries */
};
struct DexTypeItem {
    u2  typeIdx;            /* index into typeIds */
};
复制代码


size 表示方法参数的个数,参数用 DexTypeItem 表示,它只有一个属性 typeIdx,指向 type_ids 中对应项。具体的解析代码如下:

private void parseDexProto() {
    log("\nparse DexProto");
    try {
        int protoIdsSize = dex.getDexHeader().proto_ids__size;
        for (int i = 0; i < protoIdsSize; i++) {
            int shorty_idx = reader.readInt();
            int return_type_idx = reader.readInt();
            int parameters_off = reader.readInt();
            DexProtoId dexProtoId = new DexProtoId(shorty_idx, return_type_idx, parameters_off);
            log("proto[%d]: %s %s %d", i, dexStringIds.get(shorty_idx).string_data,
                    dexTypeIds.get(return_type_idx).string_data, parameters_off);
            if (parameters_off > 0) {
                parseDexProtoParameters(parameters_off);
            }
            dexProtos.add(dexProtoId);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}
复制代码


解析结果:

parse DexProto
proto[0]: V V 0
proto[1]: VL V 412
parameters[0]: Ljava/lang/String;
proto[2]: VL V 420
parameters[0]: [Ljava/lang/String;
复制代码


field_ids

struct DexFieldId {
    u2  classIdx;           /* index into typeIds list for defining class */
    u2  typeIdx;            /* index into typeIds for field type */
    u4  nameIdx;            /* index into stringIds for field name */
};
复制代码


field_ids 表示的是字段信息,指明了字段所在的类,字段的类型以及字段名称,在 DexFile.h 中定义为 DexFieldId , 其各个字段含义如下:

  • classIdx : 指向 type_ids ,表示字段所在类的信息
  • typeIdx : 指向 ype_ids ,表示字段的类型信息
  • nameIdx : 指向 string_ids ,表示字段名称


代码解析很简单,就不贴出来了,直接看一下解析结果:

parse DexField
field[0]: LHello;->HELLO_WORLD;Ljava/lang/String;
field[1]: Ljava/lang/System;->out;Ljava/io/PrintStream;
复制代码


method_ids

struct DexMethodId {
    u2  classIdx;           /* index into typeIds list for defining class */
    u2  protoIdx;           /* index into protoIds for method prototype */
    u4  nameIdx;            /* index into stringIds for method name */
};
复制代码


method_ids 指明了方法所在的类、方法声明以及方法名。在 DexFile.h 中用 DexMethodId 表示该项,其属性含义如下:

  • classIdx : 指向 type_ids ,表示类的类型
  • protoIdx : 指向 type_ids ,表示方法声明
  • nameIdx : 指向 string_ids ,表示方法名

解析结果:

parse DexMethod
method[0]: LHello; proto[0] <clinit>
method[1]: LHello; proto[0] <init>
method[2]: LHello; proto[2] main
method[3]: Ljava/io/PrintStream; proto[1] println
method[4]: Ljava/lang/Object; proto[0] <init>
复制代码


class_def

struct DexClassDef {
    u4  classIdx;           /* index into typeIds for this class */
    u4  accessFlags;
    u4  superclassIdx;      /* index into typeIds for superclass */
    u4  interfacesOff;      /* file offset to DexTypeList */
    u4  sourceFileIdx;      /* index into stringIds for source file name */
    u4  annotationsOff;     /* file offset to annotations_directory_item */
    u4  classDataOff;       /* file offset to class_data_item */
    u4  staticValuesOff;    /* file offset to DexEncodedArray */
};
复制代码


class_def 是 DEX 文件结构中最复杂也是最核心的部分,它表示了类的所有信息,对应 DexFile.h 中的 DexClassDef :

  • classIdx : 指向 type_ids ,表示类信息
  • accessFlags : 访问标识符
  • superclassIdx : 指向 type_ids ,表示父类信息
  • interfacesOff : 指向 DexTypeList 的偏移量,表示接口信息
  • sourceFileIdx : 指向 string_ids ,表示源文件名称
  • annotationOff : 注解信息
  • classDataOff : 指向 DexClassData 的偏移量,表示类的数据部分
  • staticValueOff :指向 DexEncodedArray 的偏移量,表示类的静态数据


DefCLassData

重点是 classDataOff 这个字段,它包含了一个类的核心数据,在 Android 源码中定义为 DexClassData ,它不在 DexFile.h 中了,而是在 DexClass.h 中:

struct DexClassData {
    DexClassDataHeader header;
    DexField*          staticFields;
    DexField*          instanceFields;
    DexMethod*         directMethods;
    DexMethod*         virtualMethods;
};
复制代码


DexClassDataHeader 定义了类中字段和方法的数目,它也定义在 DexClass.h 中:

struct DexClassDataHeader {
    u4 staticFieldsSize;
    u4 instanceFieldsSize;
    u4 directMethodsSize;
    u4 virtualMethodsSize;
};
复制代码


  • staticFieldsSize : 静态字段个数
  • instanceFieldsSize : 实例字段个数
  • directMethodsSize : 直接方法个数
  • virtualMethodsSize : 虚方法个数


在读取的时候要注意这里的数据是 LEB128 类型。它是一种可变长度类型,每个 LEB128 由 1~5 个字节组成,每个字节只有 7 个有效位。如果第一个字节的最高位为 1,表示需要继续使用第 2 个字节,如果第二个字节最高位为 1,表示需要继续使用第三个字节,依此类推,直到最后一个字节的最高位为 0,至多 5 个字节。除了 LEB128 以外,还有无符号类型 ULEB128。


那么为什么要使用这种数据结构呢?我们都知道 Java 中 int 类型都是 4 字节,32 位的,但是很多时候根本用不到 4 个字节,用这种可变长度的结构,可以节省空间。对于运行在 Android 系统上来说,能多省一点空间肯定是好的。下面给出了 Java 读取 ULEB128 的代码:

public static int readUnsignedLeb128(byte[] src, int offset) {
    int result = 0;
    int count = 0;
    int cur;
    do {
        cur = copy(src, offset, 1)[0];
        cur &= 0xff;
        result |= (cur & 0x7f) << count * 7;
        count++;
        offset++;
        DexParser.POSITION++;
    } while ((cur & 0x80) == 128 && count < 5);
    return result;
}
复制代码


继续回到 DexClassData 中来。header 部分定义了各种字段和方法的个数,后面跟着的分别就是 静态字段实例字段直接方法虚方法 的具体数据了。字段用 DexField 表示,方法用 DexMethod 表示。


DexField

struct DexField {
    u4 fieldIdx;    /* index to a field_id_item */
    u4 accessFlags;
};
复制代码
  • fieldIdx : 指向 field_ids ,表示字段信息
  • accessFlags :访问标识符


DexMethod

struct DexMethod {
    u4 methodIdx;    /* index to a method_id_item */
    u4 accessFlags;
    u4 codeOff;      /* file offset to a code_item */
46};
复制代码


method_idx 是指向 method_ids 的索引,表示方法信息。accessFlags 是该方法的访问标识符。codeOff 是结构体 DexCode 的偏移量。如果你坚持看到了这里,是不是发现说到现在还没说到最重要的东西,DEX 包含的代码,或者说指令,对应的就是 Hello.java 中的 main 方法。没错,DexCode 就是用来存储方法的详细信息以及其中的指令的。


struct DexCode {
    u2  registersSize;  // 寄存器个数
    u2  insSize;        // 参数的个数
    u2  outsSize;       // 调用其他方法时使用的寄存器个数
    u2  triesSize;      // try/catch 语句个数
    u4  debugInfoOff;   // debug 信息的偏移量
    u4  insnsSize;      // 指令集的个数
    u2  insns[1];       // 指令集
    /* followed by optional u2 padding */  // 2 字节,用于对齐
    /* followed by try_item[triesSize] */
    /* followed by uleb128 handlersSize */
    /* followed by catch_handler_item[handlersSize] */
};
复制代码


我们打开 010 Editor,定位到 main() 方法对应的 DexCode,对照进行分析:

image.png

public class Hello {
    private static String HELLO_WORLD = "Hello World!";
    public static void main(String[] args) {
        System.out.println(HELLO_WORLD);
    }
}
复制代码


main() 方法对应的 DexCode 十六进制表示为 :

03 00 01 00 02 00 00 00 79 02 00 00 08 00 00 00
62 00 01 00 62 01 00 00 6E 20 03 00 10 00 0E 00
复制代码


使用的寄存器个数是 3 个。参数个数是 1 个,就是 main() 方法中的 String[] args。调用外部方法时使用的寄存器个数为 2 个。指令个数是 8 。

终于说到指令了,main() 函数中有 8 条指令,就是上面十六进制中的第二行。尝试来解析一下这段指令。Android 官网就有 Dalvik 指令的相关介绍,链接


第一个指令 62 00 01 00,查询文档 62 对应指令为 sget-object vAA, field@BBBBAA 对应 00 , 表示 v0 寄存器。BBBB 对应 01 00 ,表示 field_ids 中索引为 1 的字段,根据前面的解析结果该字段为 Ljava/lang/System;->out;Ljava/io/PrintStream,整理一下,62 00 01 00 表示的就是:

sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
复制代码


接着是 62 01 00 00。还是 sget-object vAA, field@BBBB, AA 对应 01BBBB 对应 0000, 使用的是 v1 寄存器,field 位 field_ids 中索引为 0 的字段,即 LHello;->HELLO_WORLD;Ljava/lang/String,该句完整指令为:

sget-object v1, LHello;->HELLO_WORLD:Ljava/lang/String;
复制代码


接着是 6E 20 03 00, 查看文档 6E 指令为 invoke-virtual {vC, vD, vE, vF, vG}, meth@BBBB6E 后面一个十六位 2 表示调用方法是两个参数,那么 BBBB 就是 03 00,指向 method_ids 中索引为 3 方法。根据前面的解析结果,该方法就是 Ljava/io/PrintStream;->println(Ljava/lang/String;)V。完整指令为:

invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
复制代码


最后的 0E,查看文档该指令为 return-void,到这 main() 方法就结束了。

将上面几句指令放在一起:

62 00 01 00 : sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
62 01 00 00 : sget-object v1, LHello;->HELLO_WORLD:Ljava/lang/String;
6E 20 03 00 : invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
OE OO : return-void
复制代码


这就是 main() 方法的完整指令了。还记得我之前的一篇文章 Smali 语法解析——Hello World,其实这个解析结果和 Hello.java 对应的 smali 代码是一致的:

.method public static main([Ljava/lang/String;)V
    .registers 3
    .prologue
    .line 6
    sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
    sget-object v1, LHello;->HELLO_WORLD:Ljava/lang/String;
    invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
    .line 7
    return-void
.end method
复制代码


总结


这种文章真的是又臭又长,但是耐下心去看,还是会有很大收货的。最后来一张思维导图总结一下:

image.png

Java 版本 DEX 文件格式解析源码,点我 DexParser



相关文章
|
8天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
28 2
|
1月前
|
Java
Java“解析时到达文件末尾”解决
在Java编程中,“解析时到达文件末尾”通常指在读取或处理文件时提前遇到了文件结尾,导致程序无法继续读取所需数据。解决方法包括:确保文件路径正确,检查文件是否完整,使用正确的文件读取模式(如文本或二进制),以及确保读取位置正确。合理设置缓冲区大小和循环条件也能避免此类问题。
|
1月前
|
自然语言处理 数据处理 Python
python操作和解析ppt文件 | python小知识
本文将带你从零开始,了解PPT解析的工具、工作原理以及常用的基本操作,并提供具体的代码示例和必要的说明【10月更文挑战第4天】
331 60
|
14天前
|
存储
文件太大不能拷贝到U盘怎么办?实用解决方案全解析
当我们试图将一个大文件拷贝到U盘时,却突然跳出提示“对于目标文件系统目标文件过大”。这种情况让人感到迷茫,尤其是在急需备份或传输数据的时候。那么,文件太大为什么会无法拷贝到U盘?又该如何解决?本文将详细分析这背后的原因,并提供几个实用的方法,帮助你顺利将文件传输到U盘。
|
1月前
|
数据安全/隐私保护 流计算 开发者
python知识点100篇系列(18)-解析m3u8文件的下载视频
【10月更文挑战第6天】m3u8是苹果公司推出的一种视频播放标准,采用UTF-8编码,主要用于记录视频的网络地址。HLS(Http Live Streaming)是苹果公司提出的一种基于HTTP的流媒体传输协议,通过m3u8索引文件按序访问ts文件,实现音视频播放。本文介绍了如何通过浏览器找到m3u8文件,解析m3u8文件获取ts文件地址,下载ts文件并解密(如有必要),最后使用ffmpeg合并ts文件为mp4文件。
|
1月前
|
Linux API 开发工具
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
ijkplayer是由B站研发的移动端播放器,基于FFmpeg 3.4,支持Android和iOS。其源码托管于GitHub,截至2024年9月15日,获得了3.24万星标和0.81万分支,尽管已停止更新6年。本文档介绍了如何在Linux环境下编译ijkplayer的so库,以便在较新的开发环境中使用。首先需安装编译工具并调整/tmp分区大小,接着下载并安装Android SDK和NDK,最后下载ijkplayer源码并编译。详细步骤包括环境准备、工具安装及库编译等。更多FFmpeg开发知识可参考相关书籍。
85 0
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
|
1月前
|
开发工具 Android开发 iOS开发
深入解析安卓与iOS开发环境的优劣
【10月更文挑战第4天】 本文将深入探讨安卓和iOS两大主流移动操作系统的开发环境,从技术架构、开发工具、用户体验等方面进行详细比较。通过分析各自的优势和不足,帮助开发者更好地理解这两个平台的异同,从而为项目选择最合适的开发平台提供参考。
25 3
|
23天前
|
安全 5G Android开发
安卓与iOS的较量:技术深度解析
【10月更文挑战第24天】 在移动操作系统领域,安卓和iOS无疑是两大巨头。本文将深入探讨这两个系统的技术特点、优势和不足,以及它们在未来可能的发展方向。我们将通过对比分析,帮助读者更好地理解这两个系统的本质和内涵,从而引发对移动操作系统未来发展的深思。
39 0
|
监控 安全 算法
【Android 安全】DEX 加密 ( Application 替换 | 兼容 ContentProvider 操作 | 源码资源 )(三)
【Android 安全】DEX 加密 ( Application 替换 | 兼容 ContentProvider 操作 | 源码资源 )(三)
148 0
【Android 安全】DEX 加密 ( Application 替换 | 兼容 ContentProvider 操作 | 源码资源 )(三)
|
安全 Android开发 数据安全/隐私保护
【Android 安全】DEX 加密 ( Application 替换 | 兼容 ContentProvider 操作 | 源码资源 )(二)
【Android 安全】DEX 加密 ( Application 替换 | 兼容 ContentProvider 操作 | 源码资源 )(二)
185 0

推荐镜像

更多