DEX文件混淆加密

时间:2024-04-07 15:28:04

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

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

0x00 前言

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

效果图

DEX文件混淆加密

源码地址和使用说明在 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 分析二进制文件数据。

属性示例

DEX文件混淆加密

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。

实现效果

DEX文件混淆加密

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

2.函数重复定义

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

DEX文件混淆加密

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

实现效果:

DEX文件混淆加密

3.函数隐藏

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

实现效果:

DEX文件混淆加密

4.类定义隐藏

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

实现效果:

DEX文件混淆加密

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.

DEX文件混淆加密 

也就是说 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 文件的格式为:

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 文件格式也有点了解也算值得了。



作者凸一_一凸
来源51CTO