DEX文件混淆加密

移动开发 Android
混淆加密主要是为了隐藏 dex 文件中关键的代码,力度从轻到重包括:静态变量的隐藏、函数的重复定义、函数的隐藏、以及整个类的隐藏。混淆后的 dex 文件依旧可以通过 dex2jar jade 等工具的反编译成 Java 源码,但是里面关键的代码已经看不到了。

现在部分 app 出于安全性(比如加密算法)或者用户体验(热补丁修复bug)会考虑将部分模块采用热加载的形式 Load。所以针对这部分的 dex 进行加密是有必要的,如果 dex 是修复的加密算法,你总不想被人一下就反编译出来吧。当然也可以直接用一个加密算法对 dex 进行加密,Load 前进行解密就可以了,但是最好的加密就是让人分不清你是否加密了。一般逆向过程中拿到一个可以直接反编译成 java 源码的 dex 我们很可能就认为这个 dex 文件是没有加密可以分析的。

原文地址: DEX文件混淆加密

0x00 前言

混淆加密主要是为了隐藏 dex 文件中关键的代码,力度从轻到重包括:静态变量的隐藏、函数的重复定义、函数的隐藏、以及整个类的隐藏。混淆后的 dex 文件依旧可以通过 dex2jar jade 等工具的反编译成 Java 源码,但是里面关键的代码已经看不到了。

效果图: 

 

 

 

源码地址和使用说明在 github 上 hidex-hack

0x01 dex格式分析

dex 文件格式在上一篇有进行了比较详细的介绍,具体可看dex文件格式分析,这里简单的介绍一下整个 dex 文件的布局。

1.header(dex头部)

header 概述了整个 dex 文件的分布情况,包括了:magic, checksum, signature, file_size, header_size, endian_tag, link, map, string_ids, type_ids, proto_ids, field_ids, method_ids, class_defs, data。

  • checksum 和 signature 是校验值,修改后需要对其进行修复
  • string_ids, type_ids, proto_ids, field_ids, method_ids 作为类型数组节区(我瞎起的)保存了不同类型的值
  • class_defs 存储了类的定义也是我们修改的重点
  • data 是数据存储区,包括所有的数据

2.类型数组节区

类型数组节区包括了string_ids, type_ids, proto_ids, field_ids, method_ids。分别表示:字符串,类型,函数签名,属性,函数。每个节区都保存了对应类型数据数组,可以用 010Editor 分析二进制文件数据。

属性示例: 

 

 

 

3.类定义

类定义是修改的重点,这里保存了所有类的结构,也是整个 dex 文件中结构最复杂的部分。其中包括了:静态属性变量、成员数形变量,虚函数,直接函数,静态函数等数据。

0x02 实现功能

通过分析 dex 文件格式,现在可以实现的混淆加密主要包括四种:

  1. 静态变量隐藏
  2. 函数重复定义
  3. 函数隐藏
  4. 类定义隐藏

四种混淆加密的实现方式都是通过修改 class_def 结构体中字段实现的。可以通过 json 格式了解一下 class_def 的结构(这里只列出来要用到的字段):

  1.   "class_def": { 
  2.     "class_idx": 01 
  3.     "static_values_off": 000,        
  4.     "class_data_off": 001,           
  5.     "class_data": {                  
  6.         "direct_methods_size": 001,   
  7.         "virtual_methods_size": 002,  
  8.         "virtual_methods":[           
  9.             { 
  10.                 "code_off": 003  
  11.             }, 
  12.             { 
  13.                 "code_off": 004 
  14.             } 
  15.         ] 
  16.     } 
  17.   } 
  18.  

字段含义:

  • class_idx: 类名序号,值是type_ids的一个index
  • class_def: 类定义结构体
  • static_values_off: 静态变量值偏移
  • class_data_off: 类定义偏移
  • class_data: 类定义结构体
  • direct_methods_size: 直接函数个数
  • virtual_methods_size: 虚函数个数
  • virtual_methods: 虚函数结构体
  • code_off: 函数代码偏移

通过上面的字段介绍其实很容易得到四个功能的实现方案,下面一个一个介绍。

1.静态变量隐藏

static_vaules_off 保存了每个类中静态变量的值的偏移量,指向 data 区里的一个列表,格式为 encode_array_item,如果没有此项内容,该值为0。所以要实现静态变量赋值隐藏只需要将 static_values_off 值修改为0。

实现效果: 

 

 

 

这里的静态数组数据没有成功隐藏,因为我也不知道怎么搞。😶

2.函数重复定义

class_def -> class_data -> virtual_methods -> code_ff 表示的是某个类中某个函数的代码偏移地址。这里需要提到一个概念:Java 中所有函数实现都是虚函数,这一点和 C++ 是不一样的,所有这里修改的都是 virtual_methods 中 code_off。 

 

 

 

实现方式:读取第一个函数的代码偏移地址,将接下来的函数偏移地址都修改为第一的值。

实现效果: 

 

 

 

3.函数隐藏

class_def -> class_data -> virtual_methods_size 和 class_def -> class_data -> direct_methods_size 记录了类定义中函数的个数,如果没有定义函数则该值为0。所以只要将该值改为0,函数定义就会被隐藏。

实现效果: 

 

 

 

4.类定义隐藏

class_def -> class_data_off 保存了具体类定义的偏移地址,也就是 class_def -> class_data 的地址,如果该值为0则所有实现将被隐藏。隐藏后会把类定义的所有东西都隐藏包括成员变量,成员函数,静态变量,静态函数。

实现效果:

 

0x03 数据读取

上面一个章节主要介绍了功能实现的原理,接下来要介绍具体实现了。要实现修改 class_def 中字段,首先要把整个 dex 文件结构解析出来,当然可以只是我们需要的字段。在工具中我定义的 dex 结构如下,因为 class_def 结构比较复杂所以独立了一个包定义:

  1. → tree -L 2 
  2. ├── DexFile.java 
  3. ├── FieldIds.java 
  4. ├── Header.java 
  5. ├── MapList.java 
  6. ├── MethodIds.java 
  7. ├── ProtoIds.java 
  8. ├── StringIds.java 
  9. ├── TypeIds.java 
  10. └── cladef 
  11.     ├── ClassData.java 
  12.     ├── ClassDefs.java 
  13.     ├── Code.java 
  14.     ├── EncodedField.java 
  15.     ├── EncodedMethod.java 
  16.     ├── EncodedValue.java 
  17.     └── StaticValues.java  

也许你可能会疑问,我们功能实现时候只需要修改 class_def 为什么还需要读取 string_ids 这些区段。这是因为像上面提到的 class_def -> class_idx 保存的其实是 type_ids 中的序号,而 type_ids 中保存的是 string_ids 的序号。

为了灵活配置,运行工具的时候我们只需要配置好要隐藏的类名,比如需要隐藏某个类的实现 hack_me_size: cc.gnaixx.samp.core.EntranceImpl, 配置文件的具体实现下个章节介绍。

DexFile.java 定义了整个 dex 文件结构, 实现比较简单只有一个 read(byte[] dexBuff) 函数读取整个 dex 文件格式。

DexFile.java:

  1. public class DexFile { 
  2.     public static final int HEADER_LEN = 0x70; 
  3.  
  4.     public Header header; 
  5.     public StringIds stringIds; 
  6.     public TypeIds typeIds; 
  7.     public ProtoIds protoIds; 
  8.     public FieldIds fieldIds; 
  9.     public MethodIds methodIds; 
  10.     public ClassDefs classDefs; 
  11.     public MapList mapList; 
  12.  
  13.     //reader dex 
  14.     public void read(byte[] dexBuff){ 
  15.         //read header 
  16.         byte[] headerbs = subdex(dexBuff, 0, HEADER_LEN); 
  17.         header = new Header(headerbs); 
  18.  
  19.         //read string_ids 
  20.         stringIds = new StringIds(dexBuff, header.stringIdsOff, header.stringIdsSize); 
  21.  
  22.         //read type_ids 
  23.         typeIds = new TypeIds(dexBuff, header.typeIdsOff, header.typeIdsSize); 
  24.  
  25.         //read proto_ids 
  26.         protoIds = new ProtoIds(dexBuff, header.protoIdsOff, header.protoIdsSize); 
  27.  
  28.         //read field_ids 
  29.         fieldIds = new FieldIds(dexBuff, header.fieldIdsOff, header.fieldIdsSize); 
  30.  
  31.         //read method_ids 
  32.         methodIds = new MethodIds(dexBuff, header.methodIdsOff, header.methodIdsSize); 
  33.  
  34.         //read class_defs 
  35.         classDefs = new ClassDefs(dexBuff, header.classDefsOff, header.classDefsSize); 
  36.  
  37.         //read map_list 
  38.         mapList = new MapList(dexBuff, header.mapOff); 
  39.     } 
  40.  

第一步要先读取 header 因为它保存了其他节区的偏移地址和个数。

Header.java:

  1. public class Header { 
  2.  
  3.     public byte[]  magic           = new byte[MAGIC_LEN]; 
  4.     public int     checksum; 
  5.     public byte[]  signature       = new byte[SIGNATURE_LEN]; 
  6.     public int     fileSize; 
  7.     public int     headerSize; 
  8.     public int     endianTag; 
  9.     public int     linkSize; 
  10.     public int     linkOff; 
  11.     public int     mapOff; 
  12.     public int     stringIdsSize; 
  13.     public int     stringIdsOff; 
  14.     public int     typeIdsSize; 
  15.     public int     typeIdsOff; 
  16.     public int     protoIdsSize; 
  17.     public int     protoIdsOff; 
  18.     public int     fieldIdsSize; 
  19.     public int     fieldIdsOff; 
  20.     public int     methodIdsSize; 
  21.     public int     methodIdsOff; 
  22.     public int     classDefsSize; 
  23.     public int     classDefsOff; 
  24.     public int     dataSize; 
  25.     public int     dataOff; 
  26.  
  27.     public Header(byte[] headerBuff) { 
  28.         Reader reader = new Reader(headerBuff, 0); 
  29.         this.magic = reader.subdex(MAGIC_LEN); 
  30.         this.checksum = reader.readUint(); 
  31.         this.signature = reader.subdex(SIGNATURE_LEN); 
  32.         //...... 
  33.     } 
  34.  
  35.     public void write(byte[] dexBuff){ 
  36.         Writer writer = new Writer(dexBuff, 0); 
  37.         writer.replace(magic, MAGIC_LEN); 
  38.         writer.writeUint(checksum); 
  39.         writer.replace(signature, SIGNATURE_LEN); 
  40.         //..... 
  41.     } 
  42.  

知道了各个节区的偏移地址和个数接下来的读取就比较简单了,比如 string_ids 节区的读取。

StringIds.java:

  1. public class StringIds { 
  2.  
  3.     class StringId { 
  4.         int dataOff;            //字符串偏移位置 
  5.         Uleb128 utf16Size;      //字符串长度 
  6.         byte data[];            //字符串数据 
  7.  
  8.         public StringId(int dataOff, Uleb128 uleb128, byte[] data) { 
  9.             this.dataOff = dataOff; 
  10.             this.utf16Size = uleb128; 
  11.             this.data = data; 
  12.         } 
  13.     } 
  14.  
  15.     StringId stringIds[]; 
  16.  
  17.     public StringIds(byte[] dexBuff, int offint size) { 
  18.         this.stringIds = new StringId[size]; 
  19.  
  20.         Reader reader = new Reader(dexBuff, off); 
  21.         for (int i = 0; i < size; i++) { 
  22.             int dataOff = reader.readUint(); 
  23.             Uleb128 utf16Size = getUleb128(dexBuff, dataOff); 
  24.             byte[] data = subdex(dexBuff, dataOff + 1, utf16Size.getVal()); 
  25.             StringId stringId = new StringId(dataOff, utf16Size, data); 
  26.             stringIds[i] = stringId; 
  27.         } 
  28.     } 
  29.  
  30.     public String getData(int id) { 
  31.         //return "(" + id + ")" + new String(stringIds[id].data); 
  32.         return new String(stringIds[id].data); 
  33.     } 
  34.  

其他节区的读取和 string_ids 类似,但是 class_def 节区结构比较复杂,读取起来可能比较麻烦。但是其实我们要用的值并不是很多,只需要关注那几个字段就好了。

ClassDefs.java:

  1. public class ClassDefs { 
  2.  
  3.     public class ClassDef { 
  4.         public int          classIdx;       //class类型,对应type_ids 
  5.         public int          accessFlags;    //访问类型,enum 
  6.         public int          superclassIdx;  //supperclass类型,对应type_ids 
  7.         public int          interfacesOff;  //接口偏移,对应type_list 
  8.         public int          sourceFileIdx;  //源文件名,对应string_ids 
  9.         public int          annotationsOff; //class注解,位置位于data区,对应annotation_direcotry_item 
  10.         public HackPoint    classDataOff;   //class具体用到的数据,位于data区,格式为class_data_item,描述class的field,method,method执行代码 
  11.         public HackPoint    staticValueOff; //位于data区,格式为encoded_array_item 
  12.  
  13.         public StaticValues staticValues;  // classDataOff不为0时存在 
  14.         public ClassData    classData;     // staticValueOff不为0存在 
  15.  
  16.         public ClassDef(int classIdx, int accessFlags, 
  17.                         int superclassIdx, int interfacesOff, 
  18.                         int sourceFileidx, int annotationsOff, 
  19.                         HackPoint classDataOff, HackPoint staticValueOff) { 
  20.             this.classIdx = classIdx; 
  21.             this.accessFlags = accessFlags; 
  22.             this.superclassIdx = superclassIdx; 
  23.             this.interfacesOff = interfacesOff; 
  24.             this.sourceFileIdx = sourceFileidx; 
  25.             this.annotationsOff = annotationsOff; 
  26.             this.classDataOff = classDataOff; 
  27.             this.staticValueOff = staticValueOff; 
  28.         } 
  29.  
  30.         public void setClassData(ClassData classData){ 
  31.             this.classData = classData; 
  32.         } 
  33.  
  34.         public void setStaticValue(StaticValues staticValues){ 
  35.             this.staticValues = staticValues; 
  36.         } 
  37.     } 
  38.  
  39.     int      offset; //偏移位置 
  40.     int      size;   //大小 
  41.  
  42.     public ClassDef classDefs[]; 
  43.  
  44.     public ClassDefs(byte[] dexBuff, int offint size) { 
  45.         this.offset = off
  46.         this.size = size
  47.  
  48.         Reader reader = new Reader(dexBuff, off); 
  49.         classDefs = new ClassDef[size]; 
  50.         for (int i = 0; i < size; i++) { 
  51.             int classIdx = reader.readUint(); 
  52.             int accessFlags = reader.readUint(); 
  53.             int superclassIdx = reader.readUint(); 
  54.             int interfacesOff = reader.readUint(); 
  55.             int sourcFileIdx = reader.readUint(); 
  56.             int annotationOff = reader.readUint(); 
  57.  
  58.             HackPoint classDataOff = new HackPoint(HackPoint.UINT, reader.getOff(), reader.readUint()); 
  59.             HackPoint staticValueOff = new HackPoint(HackPoint.UINT, reader.getOff(), reader.readUint()); 
  60.  
  61.             ClassDef classDef = new ClassDef( 
  62.                     classIdx, accessFlags, 
  63.                     superclassIdx, interfacesOff, 
  64.                     sourcFileIdx, annotationOff, 
  65.                     classDataOff, staticValueOff); 
  66.  
  67.             if(staticValueOff.value != 0){ 
  68.                 Reader reader1 = new Reader(dexBuff, staticValueOff.value); 
  69.                 Uleb128 staticSize = reader1.readUleb128(); 
  70.                 StaticValues staticValues = new StaticValues(staticSize); 
  71.                 classDef.setStaticValue(staticValues); 
  72.             } 
  73.  
  74.             if(classDataOff.value != 0){ 
  75.                 classDef.setClassData(new ClassData(dexBuff, classDataOff.value)); 
  76.             } 
  77.             classDefs[i] = classDef; 
  78.         } 
  79.     } 
  80.  
  81.     public void write(byte[] dexBuff){ 
  82.         Writer writer = new Writer(dexBuff, offset); 
  83.         for(int i=0; i<size; i++){ 
  84.             ClassDef classDef = classDefs[i]; 
  85.             writer.writeUint(classDef.classIdx); 
  86.             writer.writeUint(classDef.accessFlags); 
  87.             writer.writeUint(classDef.superclassIdx); 
  88.             writer.writeUint(classDef.interfacesOff); 
  89.             writer.writeUint(classDef.sourceFileIdx); 
  90.             writer.writeUint(classDef.annotationsOff); 
  91.  
  92.             writer.writeUint(classDef.classDataOff.value); 
  93.             if(classDef.classDataOff.value != 0){ 
  94.                 classDef.classData.write(dexBuff, classDef.classDataOff.value); 
  95.             } 
  96.  
  97.             writer.writeUint(classDef.staticValueOff.value); 
  98.             if(classDef.staticValueOff.value != 0){ 
  99.                 //暂时不做处理 
  100.             } 
  101.         } 
  102.     } 
  103.  

这里需要介绍一下 dex 特有的一种数据类型 LEB128 官方介绍如下:

LEB128 ("Little-Endian Base 128") is a variable-length encoding for arbitrary signed or unsigned integer quantities. The format was borrowed from the DWARF3 specification. In a .dex file, LEB128 is only ever used to encode 32-bit quantities.

Each LEB128 encoded value consists of one to five bytes, which together represent a single 32-bit value. Each byte has its most significant bit set except for the final byte in the sequence, which has its most significant bit clear. The remaining seven bits of each byte are payload, with the least significant seven bits of the quantity in the first byte, the next seven in the second byte and so on. In the case of a signed LEB128 (sleb128), the most significant payload bit of the final byte in the sequence is sign-extended to produce the final value. In the unsigned case (uleb128), any bits not explicitly represented are interpreted as 0. 

 

也就是说 LEB128 是基于 1 个 Byte 的一种不定长度的编码方式 。若第一个 Byte 的最高位为 1 ,则表示还需要下一个 Byte 来描述 ,直至最后一个 Byte 的最高 位为 0 。每个 Byte 的其余 Bit 用来表示数据。

代码中用 ULeb128.java(unsigned 无符号) 表示是该结构,通过分析Android源码 Leb128.h可以知道 LEB128 虽然表示的是不定长格式,但是在 Android 中只用到了4 个byte,所以只需要用int表示就可以了。

ULeb128.java:

  1. public class Uleb128 { 
  2.     byte[] realVal; //存储的byte数据 
  3.     int val; //表示的整型数据 
  4.  
  5.     public Uleb128(byte[] realVal, int val){ 
  6.         this.realVal = realVal; 
  7.         this.val = val; 
  8.     } 
  9.  
  10.     public int getSize(){ 
  11.         return this.realVal.length; 
  12.     } 
  13.  
  14.     public int getVal(){ 
  15.         return this.val; 
  16.     } 
  17.  
  18.     public byte[] getRealVal(){ 
  19.         return this.realVal; 
  20.     } 
  21.  

Bytes to ULEB128: 

  1. //Reader.java 
  2. public Uleb128 readUleb128() { 
  3.         int value = 0; 
  4.         int count = 0; 
  5.         byte realVal[] = new byte[4]; 
  6.         boolean flag = false
  7.         do { 
  8.             flag = false
  9.             byte seg = buffer[offset]; 
  10.             if ((seg & 0x80) == 0x80) { //高8位为1 
  11.                 flag = true
  12.             } 
  13.             seg = (byte) (seg & 0x7F); 
  14.             value += seg << (7 * count); 
  15.             realVal[count] = buffer[offset]; 
  16.             count++; 
  17.             offset++; 
  18.         } while (flag); 
  19.         return new Uleb128(BufferUtil.subdex(realVal, 0, count), value); 
  20.     }  

Integer to ULEB128:

  1. //Trans.java 
  2. public static Uleb128 intToUleb128(int val) { 
  3.         byte[] realVal = new byte[]{0x00, 0x00, 0x00, 0x00}; //int 最大长度为4 
  4.         int bk = val; 
  5.         int len = 0; 
  6.         for (int i = 0; i < realVal.length; i++) { 
  7.             len = i + 1; //最少长度为1 
  8.             realVal[i] = (byte) (val & 0x7F); //获取低7位的值 
  9.             if (val > (0x7F)) { 
  10.                 realVal[i] |= 0x80; //高位为1 加上去 
  11.             } 
  12.             val = val >> 7; 
  13.             if (val <= 0) break; 
  14.         } 
  15.         Uleb128 uleb128 = new Uleb128(BufferUtil.subdex(realVal, 0, len), bk); 
  16.         return uleb128; 
  17.     }  

0x04 HackPoint格式

HackPoint 表示修改后的数据结构,代码中把所有要修改的的字段都用 HackPoint 类型表示。HackPoint 类型有三个字段 type、offset、value,都是 int 类型分别表示:类型、偏移地址、原始值。类型主要有三种 uint(unsigned int)、ushort(unsigned short 2byte)、uleb128。这三种数据用 int 存储都足够了。

HackPoint.java:

  1. public class HackPoint implements Cloneable { 
  2.  
  3.     public static final int UINT = 0x01; 
  4.     public static final int USHORT = 0x02; 
  5.     public static final int ULEB128 = 0x03; 
  6.  
  7.     public int type;        //数据类型 
  8.     public int offset;      //偏移地址 
  9.     public int value;       //原始值 
  10.  
  11.     public HackPoint(int type, int offset, int val) { 
  12.         this.type = type; 
  13.         this.offset = offset; 
  14.         this.value = val; 
  15.     } 
  16.  
  17.     @Override 
  18.     public HackPoint clone() { 
  19.         HackPoint hp = null
  20.         try { 
  21.             hp = (HackPoint) super.clone(); 
  22.         } catch (CloneNotSupportedException e) { 
  23.             e.printStackTrace(); 
  24.         } 
  25.         return hp; 
  26.     } 
  27.  

在修改完后会把所有的 HackPoint 数据写在 dex 文件的末尾。本来 dex 文件末尾是 map_list 区段,数据格式是 :

  1. struct map_list{ 
  2.     ushort type; 
  3.     ushort unused; 
  4.     uint size
  5.     uint offset; 
  6. };  

刚好是 12 byte, 所以 HackPoint 写入 dex 文件的格式为: 

 

 

 

0x05 配置文件

配置文件的定义比较简单看一下示例就知道了:

  1. #################################################### 
  2. #    hack_class:      隐藏类定义 
  3. #    hack_sf_val:     隐藏静态变量 
  4. #    hack_me_size:    隐藏methods 
  5. #    hack_me_def:     重复函数定义(以第一个为准) 
  6. ##################################################### 
  7.  
  8.  
  9. #隐藏静态变量值 
  10. hack_sf_val: cc.gnaixx.samp.core.EntranceImpl 
  11.  
  12. #重复函数定义(以第一个为准) 
  13. hack_me_def: cc.gnaixx.samp.core.EntranceImpl 
  14.  
  15. #隐藏函数实现 
  16. hack_me_size: cc.gnaixx.samp.core.EntranceImpl 
  17.  
  18. #隐藏整个类实现 
  19. hack_class: cc.gnaixx.samp.core.EntranceImpl cc.gnaixx.samp.BuildConfig  

当多个类需要实现同一个功能的时候只需要用空格分隔就可以了

配置文件读取代码: 

  1. public static Map<String, List<String>> readConfig(String path) { 
  2.     try { 
  3.         Map<String, List<String>> config = new HashMap<>(); 
  4.         FileReader fr = new FileReader(path); 
  5.         BufferedReader br = new BufferedReader(fr); 
  6.         String line; 
  7.         while ((line = br.readLine()) != null) { 
  8.             if (!line.startsWith("#") && !line.equals("")) { 
  9.                 String conf[] = line.split(":"); 
  10.                 if (conf.length != 2) { 
  11.                     log("warning""error config at :" + line); 
  12.                     System.exit(0); 
  13.                 } 
  14.  
  15.                 String key = conf[0]; 
  16.                 String values[] = conf[1].split(" "); 
  17.                 List<String> valueList = new ArrayList<>(); 
  18.                 for (int i = 0; i < values.length; i++) { 
  19.                     if (values[i] != null && !values[i].equals("")) { 
  20.                         valueList.add(values[i]); 
  21.                     } 
  22.                 } 
  23.                 config.put(key, valueList); 
  24.             } 
  25.         } 
  26.         fr.close(); 
  27.         br.close(); 
  28.         return config; 
  29.     } catch (Exception e) { 
  30.         e.printStackTrace(); 
  31.     } 
  32.     return null
  33.  

0x06 dex混淆隐藏

dex 文件混淆隐藏主要包括三个步骤:

  1. 修改 HackPoint 并保存到 dex 文件末尾
  2. 修复Header

1.修改 HackPoint

通过取得的配置文件中的配置类遍历 class_def_item: 

  1. //查找配置文件所在类位置 
  2. private void seekHP(ClassDefs.ClassDef[] classDefItem, List<String> conf, String type, SeekCallBack callBack){ 
  3.     if (conf == null) { 
  4.         return
  5.     } 
  6.     for (int i = 0; i < conf.size(); i++) { 
  7.         String classname = conf.get(i); 
  8.         boolean isDef = false
  9.         for (int j = 0; j < classDefItem.length; j++) { 
  10.             String className = dexFile.typeIds.getString(dexFile, classDefItem[j].classIdx); //查找顺序 class_idx => type_ids => string_ids 
  11.             className = pathToPackages(className); //获取类名 
  12.             if (className.equals(classname)) { 
  13.                 callBack.doHack(classDefItem[j], this.hackPoints); //具体操作 
  14.                 log(type, conf.get(i)); 
  15.                 isDef = true
  16.             } 
  17.         } 
  18.         if (isDef == false) { 
  19.             log("warning""con't find class:" + classname); 
  20.         } 
  21.     } 
  22.  
  23. //具体操作回调处理 
  24. interface SeekCallBack { 
  25.     void doHack(ClassDefs.ClassDef classDefItem, List<HackPoint> hackPoints); 
  26.  

隐藏静态变量值: 

  1. //隐藏静态变量初始化 
  2. private void hackSfVal(ClassDefs.ClassDef[] classDefItem, List<String> conf) { 
  3.     seekHP(classDefItem, conf, Constants.HACK_SF_VAL, new SeekCallBack() { 
  4.         @Override 
  5.         public void doHack(ClassDefs.ClassDef classDefItem, List<HackPoint> hackPoints) { 
  6.             HackPoint point = classDefItem.staticValueOff.clone();  //获取静态变量数据偏移 
  7.             hackPoints.add(point);                          //添加修改点 
  8.             classDefItem.staticValueOff.value = 0;          //将静态变量的偏移改为0(隐藏赋值) 
  9.         } 
  10.     }); 
  11.  

函数重复定义:

  1. //重复函数定义 
  2. private void hackMeDef(ClassDefs.ClassDef[] classDefItem, List<String> conf){ 
  3.     seekHP(classDefItem, conf, Constants.HACK_ME_DEF, new SeekCallBack() { 
  4.         @Override 
  5.         public void doHack(ClassDefs.ClassDef classDefItem, List<HackPoint> hackPoints) { 
  6.             //以第一个为默认值 
  7.             int virtualMeSize = classDefItem.classData.virtualMethodsSize.value; 
  8.             int virtualMeCodeOff = 0; 
  9.             for (int i = 0; i < virtualMeSize; i++) { 
  10.                 if (i == 0) { 
  11.                     virtualMeCodeOff = classDefItem.classData.virtualMethods[i].codeOff.value; 
  12.                 }else
  13.                     HackPoint point = classDefItem.classData.virtualMethods[i].codeOff.clone(); 
  14.                     hackPoints.add(point); 
  15.                     classDefItem.classData.virtualMethods[i].codeOff.value = virtualMeCodeOff; 
  16.                 } 
  17.             } 
  18.         } 
  19.     }); 
  20.  

函数隐藏:

  1. //隐藏函数定义 
  2. private void hackMeSize(ClassDefs.ClassDef[] classDefItem, List<String> conf){ 
  3.     seekHP(classDefItem, conf, Constants.HACK_ME_SIZE, new SeekCallBack() { 
  4.         @Override 
  5.         public void doHack(ClassDefs.ClassDef classDefItem, List<HackPoint> hackPoints) { 
  6.             HackPoint directPoint = classDefItem.classData.directMethodsSize.clone(); //同时需改虚函数和直接函数 
  7.             HackPoint virtualPoint = classDefItem.classData.virtualMethodsSize.clone(); 
  8.             hackPoints.add(directPoint); 
  9.             hackPoints.add(virtualPoint); 
  10.             classDefItem.classData.directMethodsSize.value = 0; 
  11.             classDefItem.classData.virtualMethodsSize.value = 0; 
  12.         } 
  13.     }); 
  14.  

隐藏类:

  1. //隐藏静态变量初始化 
  2. private void hackSfVal(ClassDefs.ClassDef[] classDefItem, List<String> conf) { 
  3.     seekHP(classDefItem, conf, Constants.HACK_SF_VAL, new SeekCallBack() { 
  4.         @Override 
  5.         public void doHack(ClassDefs.ClassDef classDefItem, List<HackPoint> hackPoints) { 
  6.             HackPoint point = classDefItem.staticValueOff.clone();  //获取静态变量数据偏移 
  7.             hackPoints.add(point);                          //添加修改点 
  8.             classDefItem.staticValueOff.value = 0;          //将静态变量的偏移改为0(隐藏赋值) 
  9.         } 
  10.     }); 
  11.  

添加 HackPoint 数据到 dex 文件: 

  1. //保留修改信息 
  2. private void appendHP() { 
  3.     byte[] pointsBuff = new byte[]{}; 
  4.     for (int i = 0; i < hackPoints.size(); i++) { 
  5.         byte[] pointBuff = hackpToBin(hackPoints.get(i)); 
  6.         pointsBuff = BufferUtil.append(pointsBuff, pointBuff, pointBuff.length); 
  7.     } 
  8.     dexBuff = BufferUtil.append(dexBuff, pointsBuff, pointsBuff.length); 
  9.  
  10. //hackPoint 转 二进制 
  11. public static byte[] hackpToBin(HackPoint point) { 
  12.     ByteBuffer bb = ByteBuffer.allocate(4 * 3); 
  13.     bb.put(intToBin_Lit(point.type)); 
  14.     bb.put(intToBin_Lit(point.offset)); 
  15.     bb.put(intToBin_Lit(point.value)); 
  16.     return bb.array(); 
  17.  
  18. //小端二进制 
  19. public static byte[] intToBin_Lit(int integer){ 
  20.     byte[] bin = new byte[]{ 
  21.             (byte) ((integer >> 0) & 0xFF), 
  22.             (byte) ((integer >> 8) & 0xFF), 
  23.             (byte) ((integer >> 16) & 0xFF), 
  24.             (byte) ((integer >> 24) & 0xFF) 
  25.     }; 
  26.     return bin; 
  27.  

dex 文件都是以小端数据保存

2.修复Header

Header 中修复的数据有三个:

  1. 文件长度
  2. checksum
  3. signature

修改代码: 

  1. //修改header 
  2. private void hackHeader() { 
  3.     //修改文件长度 
  4.     Header header = dexFile.header; 
  5.     header.fileSize = this.dexBuff.length; 
  6.     header.write(dexBuff); //需要先修改文件长度,才能计算signature checksum 
  7.     //修复 signature 校验 
  8.     log("old_signature", binToHex(dexFile.header.signature)); 
  9.     byte[] signature = signature(dexBuff, SIGNATURE_LEN + SIGNATURE_OFF); 
  10.     header.signature = signature; 
  11.     log("new_signature", binToHex(signature)); 
  12.     header.write(dexBuff); //需要先写sinature,才能计算checksum,凸 
  13.     //修复 checksum 校验 
  14.     log("old_checksum", intToHex(dexFile.header.checksum)); 
  15.     int checksum = checksum_Lit(dexBuff, CHECKSUM_LEN + CHECKSUM_OFF); 
  16.     header.checksum = checksum; 
  17.     log("new_checksum", intToHex(checksum)); 
  18.     header.write(dexBuff); 
  19.  
  20. //计算signature 
  21. public static byte[] signature(byte[] data, int off) { 
  22.     int len = data.length - off
  23.     byte[] signature = SHA1(data, off, len); 
  24.     return signature; 
  25. //sha1算法 
  26. public static byte[] SHA1(byte[] decript, int offint len) { 
  27.     try { 
  28.         MessageDigest digest = MessageDigest.getInstance("SHA-1"); 
  29.         digest.update(decript, off, len); 
  30.         byte messageDigest[] = digest.digest(); 
  31.         return messageDigest; 
  32.     } catch (NoSuchAlgorithmException e) { 
  33.         e.printStackTrace(); 
  34.     } 
  35.     return null
  36.  
  37. //计算checksum 值 
  38. public static int checksum_Lit(byte[] data, int off) { 
  39.     byte[] bin = checksum_bin(data, off); 
  40.     int value = 0; 
  41.     for (int i = 0; i < UINT_LEN; i++) { 
  42.         int seg = bin[i]; 
  43.         if (seg < 0) { 
  44.             seg = 256 + seg; 
  45.         } 
  46.         value += seg << (8 * i); 
  47.     } 
  48.     return value; 
  49. //计算checksum 
  50. public static byte[] checksum_bin(byte[] data, int off) { 
  51.     int len = data.length - off
  52.     Adler32 adler32 = new Adler32(); 
  53.     adler32.reset(); 
  54.     adler32.update(data, off, len); 
  55.     long checksum = adler32.getValue(); 
  56.     byte[] checksumbs = new byte[]{ 
  57.             (byte) checksum, 
  58.             (byte) (checksum >> 8), 
  59.             (byte) (checksum >> 16), 
  60.             (byte) (checksum >> 24)}; 
  61.     return checksumbs; 
  62.  

该部分代码地址: HidexHandle.java

0x07 dex还原

相对于加密解密过程简单了很多,只要根据 HackPoint 数据一一修复就好了。这里简单的说下修复步骤:

  1. 读取 Header 中 map_list 的偏移地址和个数,因为 HackPoint 数据保存在 map_list 之后
  2. 读取 HackPoint 数据并修复 dex 文件
  3. 修复 Header 中的 file_size、checksum、signature

java 实现

修复关键源码:

  1. //修复dex文件 
  2. public byte[] redex() { 
  3.     int mapOff = getUint(dexBuff, MAP_OFF_OFF); //获取map_off 
  4.     int mapSize = getUint(dexBuff, mapOff); //获取map_size 
  5.     int hackInfoStart = mapOff + UINT_LEN + (mapSize * MAP_ITEM_LEN); //获取 hackinfo 开始地址 
  6.     int hackInfoLen = dexBuff.length - hackInfoStart; //获取hackinfo 长度 
  7.     hackInfoBuff = subdex(dexBuff, hackInfoStart, hackInfoLen); //获取hack数据 
  8.     int dexLen = dexBuff.length - hackInfoLen; 
  9.     dexBuff = subdex(dexBuff, 0, dexLen); //截取原始dex长度 
  10.     HackPoint[] hackPoints = Trans.binToHackP(hackInfoBuff);  //修复hack点 
  11.     for (int i = 0; i < hackPoints.length; i++) { 
  12.         log("hackPoint", JSON.toJSONString(hackPoints[i])); 
  13.         recovery(hackPoints[i]); 
  14.     } 
  15.     byte[] fileSize = intToBin_Lit(dexLen); //修复文件长度 
  16.     replace(dexBuff, fileSize, FILE_SIZE_OFF, UINT_LEN); 
  17.     byte[] signature = signature(dexBuff, SIGNATURE_LEN + SIGNATURE_OFF); //修复signature校验 
  18.     replace(dexBuff, signature, SIGNATURE_OFF, SIGNATURE_LEN); 
  19.     byte[] checksum = checksum_bin(dexBuff, CHECKSUM_LEN + CHECKSUM_OFF); //修复checksum校验 
  20.     replace(dexBuff, checksum, CHECKSUM_OFF, CHECKSUM_LEN); 
  21.     log("fileSize", dexLen); 
  22.     log("signature", binToHex(signature)); 
  23.     log("checksum", binToHex_Lit(checksum)); 
  24.     return this.dexBuff; 
  25. //还原原始值 
  26. private void recovery(HackPoint hackPoint) { 
  27.     Writer writer = new Writer(this.dexBuff, hackPoint.offset); 
  28.     if (hackPoint.type == HackPoint.USHORT) { 
  29.         writer.writeUshort(hackPoint.value); 
  30.     } 
  31.     else if (hackPoint.type == HackPoint.UINT) { 
  32.         writer.writeUint(hackPoint.value); 
  33.     } 
  34.     else if (hackPoint.type == HackPoint.ULEB128) { 
  35.         Uleb128 uleb128 = Trans.intToUleb128(hackPoint.value); 
  36.         writer.writeUleb128(uleb128); 
  37.     } 
  38.  

c++ 实现

工具本身就是为了实现安全加固,那么用 java 实现意义就小了很多,所以工具包里面的实现我是用 NDK 开发的。

修复关键源码: 

  1. //解密dex 
  2. void recode(char* source, uint sourceLen, char* target, uint* targetLen){ 
  3.     uint mapOff = readUint(source, MAP_OFF_OFF); //获取map_off 
  4.     uint mapSize = readUint(source, mapOff); //获取map_size 
  5.     LOGD("mapInfo: {map_off:%d, map_size:%d}", mapOff, mapSize); 
  6.  
  7.     uint hackInfoOff = mapOff + UINT_LEN + (mapSize * MAP_ITEM_LEN); //定位hackInfo位置 
  8.     uint hackInfoLen = sourceLen - hackInfoOff; //hackInfo长度 
  9.     char* hackInfo = (char *) calloc(hackInfoLen, sizeof(char)); 
  10.     memcpy(hackInfo, source + hackInfoOff, hackInfoLen); //复制hackInfo 
  11.     LOGD("hackInfo: {hackInfo_off:%d, hackInfo_len}", hackInfoOff, hackInfoLen); 
  12.  
  13.     uint hackPointSize = hackInfoLen / sizeof(HackPoint); //获取hackPoint结构体 
  14.     HackPoint* hackPoints = (HackPoint *) calloc(hackPointSize, sizeof(HackPoint)); 
  15.     initHP(hackPoints, hackInfo, hackPointSize); //将hockInfo 转化为结构体 
  16.  
  17.     *targetLen = hackInfoOff; 
  18.     memcpy(target, source, *targetLen); //恢复原始长度 
  19.  
  20.     //恢复数据 
  21.     for(int i=0; i<hackPointSize; i++){ 
  22.         recoverHP(target, hackPoints[i]); 
  23.     } 
  24.     LOGD("Recover HackPoint success"); 
  25.  
  26.     //修复hearder 
  27.     recoverHeader(target, *targetLen); 
  28.  
  29.     free(hackInfo); 
  30.     free(hackPoints); 
  31.  

完整源码地址: hidex.cpp

0x09 总结

整体功能还是比较简单,实现的代码也不是很复杂,但是这些都需要基于对 dex 文件格式的了解的前提下。

另外该工具存在一个缺点,dex 的加载问题。Android中加载 dex 的 DexClassLoad 只支持文件路径加载,不像 java 中的 ClassLoad 可以支持二进制流加载,所以在加载 dex 是就存在加密后的 dex 缓存,这是非常危险的。所以下个研究的点也就是自定义 DexClassLoad 实现不落地加载。(很多安全加固厂商老早就实现了🙄)。

虽然功能不算强大,也有不少缺点,不过也花了自己不少时间研究,对 dex 文件格式也有点了解,也算值得了。

原文地址: DEX文件混淆加密

责任编辑:庞桂玉 来源: segmentfault
相关推荐

2016-12-01 14:47:20

2010-02-04 09:55:46

Android DEX

2023-12-14 08:22:57

classdexarsc

2024-03-17 19:14:28

2023-08-04 17:52:22

2015-11-27 18:32:02

Dex优化分包方案

2012-11-22 10:39:37

漏洞PDF文件

2013-04-23 14:25:28

gnupg文件加密

2016-09-29 08:36:38

2013-11-11 11:12:17

EFS加密

2021-05-07 16:23:54

鸿蒙HarmonyOS应用

2011-03-21 14:48:00

Debian派生版合作

2021-05-08 05:56:15

加密OpenSSL密钥

2009-12-09 13:54:42

PHP Zend加密文

2010-07-22 14:02:44

远程文件服务器加密EFS

2020-12-13 17:57:16

加密密码Python

2024-02-28 07:40:13

ConfuserEx代码工具

2023-04-18 08:00:35

DexKubernetes身份验证

2018-09-17 11:12:14

Linux文件管理器加密文件

2009-02-09 12:52:32

文件加密Oracle数据库
点赞
收藏

51CTO技术栈公众号