Android加固原理研究

4,152 阅读8分钟
原文链接: www.apkbus.com

一、DEX文件格式分析

1、文件布局

文档可以参考官方文档: source.android.com/devices/tec…

image

dex 文件可以分为3个模块,头文件(header)、索引区(xxxx_ids)、数据区(data)。头文件概况的描述了整个 dex 文件的分布,包括每一个索引区的大小跟偏移。索引区表示每个数据的标识,主要是指向数据区的偏移。

image

010Editor 中除了数据区(data)没有显示出来,其他区段都有显示,另外 link_data 在模板中被定为 map_list

2、文件头header

image
  • magic: 这个是固定值,用于识别 dex 文件。转化为字符串为 "dex 035", 中间是一个换行,后面035是版本号。

  • checksum: ****文件校验码,使用 alder32 算法校验文件除去 maigc、checksum 外余下的所有文件区域,用于检 查文件错误。

  • signature: ****使用 SHA-1 算法 hash 除去 magic、checksum 和 signature 外余下的所有文件区域, 用于唯一识别本文件 。

  • file_size: dex ****文件大小

  • header_size: header 区域的大小,目前是固定为 0x70

  • endian_tag: 大小端标签,dex 文件格式为小端,固定值为 0x12345678 常量

  • map_off: map_item 的偏移地址,该 item 属于 data 区里的内容,值要大于等于 data_off 的大小,处于 dex 文件的末端。

  • 其他元素都是成对出现的。_off 表示元素的偏移量,_size 表示元素的个数

3、索引区——string_ids

string_ids 区段描述了 dex 文件中所有的字符串。格式很简单只有一个偏移量,偏移量指向了 string_data 区段的一个字符串:

image

其中 data 保存的就是字符串的值。string_ids 是比较关键的,后续的区段很多都是直接指向 string_ids 的 index。在写工具进行比较的时候也需要提取到 string_ids。

4、索引区—— type_ids

type_ids 区索引了 dex 文件里的所有数据类型,包括 class 类型,数组类型(array types)和基本类型(primitive types)。区段里的元素格式为 type_ids_item

image

type_ids_item 里面 descriptor_idx 的值的意思,是 string_ids 里的 index 序号,是用来描述此 type 的字符串。

5、索引区—— proto_ids

proto 的意思是 method prototype 代表 java 语言里的一个 method 的原型 。proto_ids 里的元素为 proto_id_item

image
  • shorty_idx: 跟 type_ids 一样,它的值是一个 string_ids 的 index 号 ,最终是一个简短的字符串描述,用来说明该 method 原型。

  • return_type_idx: 它的值是一个 type_ids 的 index 号 ,表示该 method 原型的返回值类型。

  • parameters_off: 指向 method 原型的参数列表 type_list,若 method 没有参数,值为0。参数列表的格式是 type_list,下面会有描述。

6、索引区—— field_ids

filed_ids 区里面有 dex 文件引用的所有的 field。区段的元素格式是 field_id_item,结构如下:

image
  • class_idx: 表示 field 所属的 class 类型,class_idx 的值是 type_ids 的一个 index,并且必须指向一个 class 类型。

  • type_idx: 表示本 field 的类型,它的值也是 type_ids 的一个 index 。

  • name_idx: 表示本 field 的名称,它的值是 string_ids 的一个 index 。

7、索引区—— method_ids

method_ids 是索引区的最后一个条目,描述了 dex 文件里的所有的 method。method_ids 的元素格式是 method_id_item,结构跟 fields_ids 很相似:

image
  • class_idx: 表示 method 所属的 class 类型,class_idx 的值是 type_ids 的一个 index,并且必须指向一个 class 类型。ushort类型也是为什么我们说一个 dex 只能有 65535 个方法的原因,多了必须分包。

  • proto_idx: 表示 method 的类型,它的值也是 type_ids 的一个 index。

  • name_idx: 表示 method 的名称,它的值是 string_ids 的一个 index。

8、索引区—— class_defs

class_def 区段主要是对 class 的定义,它的结构很复杂,看的我有点晕

image
  • 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: 函数代码偏移

9、索引区—— map_list

map_list 中大部分 item 跟 header 中的相应描述相同,都是介绍了各个区的偏移和大小,但是 map_list 中描述的更加全面,包括了 HEADER_ITEM 、TYPE_LIST、STRING_DATA_ITEM、DEBUG_INFO_ITEM 等信息。

image

map_list 里先用一个 uint 描述后面有 size 个 map_item,后续就是对应的 size 个 map_item 描述。 map_item 结构有 4 个元素: type 表示该 map_item 的类型,Dalvik Executable Format 里 Type Code 的定义; size 表示再细分此 item,该类型的个数;offset 是第一个元素的针对文件初始位置的偏移量; unuse 是用对齐字节的,无用。

二、DEX文件混淆加密

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

四种混淆加密的实现方式都是通过修改 class_def 结构体中字段实现的。

1、静态变量隐藏

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

image

2、函数重复定义

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

image

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

image

3、函数隐藏

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

image

4、类定义隐藏

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

image

三、APK加壳(加固)

1、原理解析

image

在加固的过程中需要三个对象:

  • 需要加密的****Apk(****源****Apk)

  • 壳程序****Apk(****负责解密****Apk****工作****)

  • 加密工具****(****将源****Apk****进行加密和壳****Dex****合并成新的****Dex)

2、主要步骤:

拿 到需要加密的Apk和自己的壳程序Apk,然后用加密算法对源Apk进行加密;再与壳Apk的Dex进行合并得到新的Dex文件;然后替换原壳程序中的 dex文件即可,得到新的Apk。那么这个新的Apk我们也叫作脱壳程序Apk,他已经不是一个完整意义上的Apk程序了,他的主要工作是:负责解密源 Apk,然后加载Apk,让其正常运行起来。

3、 源Apk和壳Dex合并生成新的Dex

image

只要关注上面红色标记的三个部分:

1) checksum

文件校验码 ,使用alder32 算法校验文件除去 maigc ,checksum 外余下的所有文件区域 ,用于检查文件错误 。

2) signature

使用 SHA-1 算法 hash 除去 magic ,checksum 和 signature 外余下的所有文件区域 ,用于唯一识别本文件 。

3) file_size

Dex 文件的大小 。

因为将一个文件(加密之后的源Apk)写入到Dex中,那么肯定需要修改文件校验码(checksum).因为他是检查文件是否有错误。那么signature也是一样,唯一识别文件的算法。还有就是需要修改dex文件的大小。

这里还需要一个操作,就是标注一下加密的Apk的大小,因为在脱壳的时候,需要知道Apk的大小,才能正确的得到Apk。那么这个值放到哪呢?这个值直接放到文件的末尾就可以了。

总结一下:修改****Dex****的三个文件头,将源****Apk****的大小追加到壳****dex****的末尾就可以了。

修改之后得到新的Dex文件样式如下:

image

4、加壳代码

image image image image

四、APK脱壳

image

步骤:

1、从脱壳Apk中获取到Dex文件

2、从脱壳Dex中得到源Apk文件

3、解密源程序Apk

4、加载解密之后的源程序Apk

5、找到源程序的Application程序,让其运行

五、动态加载

预备知识:

android中的类加载器:blog.csdn.net/jiangwei091…

android中的动态加载机制:blog.csdn.net/jiangwei091…

第一个思路: 替换LoadedApk中的mClassLoader

加载Activity的时候,有一个很重要的类:LoadedApk.Java,这个类是负责加载一个Apk程序的

image

内部有一个mClassLoader变量,是负责加载一个Apk程序的,只要获取到这个类加载器就可以了。他不是static的,所以还得获取一个LoadedApk对象。再去看一下另外一个类:ActivityThread.java的源码

image

ActivityThread类中有一个自己的static对象,然后还有一个ArrayMap存放Apk包名和LoadedApk映射关系的数据结构,那么分析清楚了,下面就来通过反射来获取mClassLoader对象吧。

image

第二思路:合并PathClassLoader和DexClassLoader中的dexElements数组

image

PathClassLoader和DexClassLoader类的父加载器是BootClassLoader,他们的父类是BaseDexClassLoader。里面有一个DexPathList对象,在来看一下DexPathList.java源码:

image

首先看一下这个类的描述,还有一个Elements数组,这个变量是专门存放加载的dex文件的路径的,系统默认的类加载器是PathClassLoader,一个程序加载之后会释放一个dex出来,这时候会将dex路径放到里面,

当然DexClassLoader也是一样的,可以将DexClassLoader中的dexElements和PathClassLoader中的dexElements进行合并,然后在设置给PathClassLoader中。

image image

文/Mob开发者平台 Android开发专家 张冰峰