apktool是Android分析中会用到的一个重要的开源工具,但是apktool的使用者的增加,相应的对抗apktool的手段也开始日益增加。 anti apktool的方法有很多,不过一般都是针对.dex的反编译做处理,这次在分析某样本时遇到了比较有意思的针对.arsc文件做处理,造成apktool解包失败的手段。本着学习研究的目的,进行了本次anti apktool的研究。
首先我们需要对arsc文件格式有一定的了解,因为我以前也并未研究过*.arsc文件格式,google后发现相关的资料也比较少,所以参考老罗的博客以及Apktool的解析代码研究整理了下面这段arsc的文件格式,如有错漏请尽量指正。
Resources.arsc文件格式是由一系列的chun k构成,每一个chunk均包含如下结构的ResChunk_header,用来描述这个chunk的基本信息。
struct ResChunk_header { enum { RES_NULL_TYPE = 0x0000, RES_STRING_POOL_TYPE = 0x0001, RES_TABLE_TYPE = 0x0002, RES_XML_TYPE = 0x0003, RES_XML_FIRST_CHUNK_TYPE = 0x0100, RES_XML_START_NAMESPACE_TYPE= 0x0100, RES_XML_END_NAMESPACE_TYPE = 0x0101, RES_XML_START_ELEMENT_TYPE = 0x0102, RES_XML_END_ELEMENT_TYPE = 0x0103, RES_XML_CDATA_TYPE = 0x0104, RES_XML_LAST_CHUNK_TYPE = 0x017f, RES_XML_RESOURCE_MAP_TYPE = 0x0180, RES_TABLE_PACKAGE_TYPE = 0x0200, RES_TABLE_TYPE_TYPE = 0x0201, RES_TABLE_TYPE_SPEC_TYPE = 0x0202 }; //当前这个chunk的类型 uint16_t type; //当前这个chunk的头部大小 uint16_t headerSize; //当前这个chunk的大小 uint32_t size; };
Resources.arsc文件的第一个结构是资源索引表头部。其结构如下,描述了Resources.arsc文件的大小和资源包数量。
struct ResTable_header { struct ResChunk_header header; //被编译的资源包的个数 uint32_t packageCount; };
示例:
图中蓝色高亮的部分就是资源索引表头部。通过解析,我们可以得到如下信息,这个chunk的类型为RES_TABLE_TYPE,头部大小为0XC,整个chunk的大小为1400252byte,有一个编译好的资源包。
紧跟着资源索引表头部的是资源项的值字符串资源池,这个字符串资源池包含了所有的在资源包里面所定义的资源项的值字符串,字符串资源池头部的结构如下。
struct ResStringPool_header { struct ResChunk_header header; //字符串的数量 uint32_t stringCount; //字符串样式的数量 uint32_t styleCount; //字符串的属性,可取值包括0x000(UTF-16),0x001(字符串经过排序)、0X100(UTF-8)和他们的组合值 uint32_t flags; //字符串内容块相对于其头部的距离 uint32_t stringsStart; //字符串样式块相对于其头部的距离 uint32_t stylesStart; };
示例:
图中绿色高亮的部分就是字符串资源池头部,通过解析,我们可以得到如下信息,这个chunk的类型为RES_STRING_POOL_TYPE,即字符串资源池。头部大小为0X1C,整个chunk的大小为369524byte,有8073条字符串,72个字符串样式,为UTF-8编码,无排序,字符串内容块相对于此chunk头部的偏移为0X7F60,字符串样式块相对于此chunk头部的偏移为0X5A054。
紧接着头部的的是两个偏移数组,分别是字符串偏移数组和字符串样式偏移数组。这两个偏移数组的大小分别等于stringCount和styleCount的值,而每一个元素的类型都是无符号整型。整个字符中资源池结构如下。
字符串资源池中的字符串前两个字节为字符串长度,长度计算方法如下。另外如果字符串编码格式为UTF-8则字符串以0X00作为结束符,UTF-16则以0X0000作为结束符。
len = (((hbyte & 0x7F) << 8)) | lbyte;
字符串与字符串样式有一一对应的关系,也就是说如果第n个字符串有样式,则它的样式描述位于样式块的第n个元素。 字符串样式的结构包括如下两个结构体,ResStringPool_ref和ResStringPool_span。 一个字符串可以对应多个ResStringPool_span和一个ResStringPool_ref。ResStringPool_span在前描述字符串的样式,ResStringPool_ref在后固定值为0XFFFFFFFF作为占位符。样式块最后会以两个值为0XFFFFFFFF的ResStringPool_ref作为结束。
struct ResStringPool_ref { uint32_t index; }; struct ResStringPool_span { enum { END = 0xFFFFFFFF }; //指向样式字符串在字符串池中偏移,例如粗体样式<b>XXX</b>,则此处指向b ResStringPool_ref name; //指向应用样式的第一个字符 uint32_t firstChar //指向应用样式的最后一个字符 uin32_t lastChar; };
示例:
图中蓝色高亮的部分就是样式内容块,按照格式解析可以得出,第一个字符串和第二字符串无样式,第三个字符串第4个字符到第7个字符的位置样式为字符串资源池中0X1F88的字符,以此类推。
接着资源项的值字符串资源池后面的部分就是Package数据块,这个数据块记录编译包的元数据,头部结构如下:
struct ResTable_package { struct ResChunk_header header; //包的ID,等于Package Id,一般用户包的值Package Id为0X7F,系统资源包的Package Id为0X01。 uint32_t id; //包名称 char16_t name[128]; //类型字符串资源池相对头部的偏移 uint32_t typeStrings; //最后一个导出的Public类型字符串在类型字符串资源池中的索引,目前这个值设置为类型字符串资源池的元素个数。 uint32_t lastPublicType; //资源项名称字符串相对头部的偏移 uint32_t keyStrings; //最后一个导出的Public资源项名称字符串在资源项名称字符串资源池中的索引,目前这个值设置为资源项名称字符串资源池的元素个数。 uint32_t lastPublicKey; };
示例:
图中紫色高亮的部分就是ResTable_package,按照上面的格式解析数据,我们可以得出,此Chunk的Type为RES_TABLE_PACKAGE_TYPE,头部大小为0X120,整个chunk的大小为1030716byte,Package Id为0X7F,包名称为co.runner.app,类型字符串资源池距离头部的偏移是0X120,有15条字符串,资源项名称字符串资源池0X1EC,有6249条字符串。
Packege数据块的整体结构,可以用以下的示意图表示:
其中Type String Pool和Key String Pool是两个字符串资源池,结构和资源项的值字符串资源池结构相同,分别对应类型字符串资源池和资源项名称字符串资源池。
再接下来的结构体可能是类型规范数据块或者类型资源项数据块,我们可以通过他们的Type来识别,类型规范数据块的Type为RES_TABLE_TYPE_SPEC_TYPE,类型资源项数据块的Type为RES_TABLE_TYPE_TYPE。
类型规范数据块用来描述资源项的配置差异性。通过这个差异性描述,我们就可以知道每一个资源项的配置状况。知道了一个资源项的配置状况之后,Android资源管理框架在检测到设备的配置信息发生变化之后,就可以知道是否需要重新加载该资源项。类型规范数据块是按照类型来组织的,也就是说,每一种类型都对应有一个类型规范数据块。其数据块头部结构如下。
struct ResTable_typeSpec { struct ResChunk_header header; //标识资源的Type ID,Type ID是指资源的类型ID。资源的类型有animator、anim、color、drawable、layout、menu、raw、string和xml等等若干种,每一种都会被赋予一个ID。 uint8_t id; //保留,始终为0 uint8_t res0; //保留,始终为0 uint16_t res1; //等于本类型的资源项个数,指名称相同的资源项的个数。 uint32_t entryCount; };
示例:
图中绿色高亮的部分就是ResTable_typeSpec,按照上面的格式解析数据,我们可以得出,此Chunk的Type为RES_TABLE_TYPE_SPEC_TYPE,头部大小为0X10,整个chunk的大小为564byte,资源ID为1,本类型资源项数量为137。
ResTable_typeSpec后面紧跟着的是一个大小为entryCount的uint32_t数组,每一个数组元素都用来描述一个资源项的配置差异性的。
类型资源项数据块用来描述资源项的具体信息, 这样我们就可以知道每一个资源项的名称、值和配置等信息。 类型资源项数据同样是按照类型和配置来组织的,也就是说,一个具有n个配置的类型一共对应有n个类型资源项数据块。其数据块头部结构如下
struct ResTable_type { struct ResChunk_header header; enum { NO_ENTRY = 0xFFFFFFFF }; //标识资源的Type ID uint8_t id; //保留,始终为0 uint8_t res0; //保留,始终为0 uint16_t res1; //等于本类型的资源项个数,指名称相同的资源项的个数。 uint32_t entryCount; //等于资源项数据块相对头部的偏移值。 uint32_t entriesStart; //指向一个ResTable_config,用来描述配置信息,地区,语言,分辨率等 ResTable_config config; };
示例:
图中红色高亮的部分就是ResTable_type,按照上面的格式解析数据,我们可以得出,RES_TABLE_TYPE_TYPE,头部大小为0X44,整个chunk的大小为4086byte,资源ID为1,本类型资源项数量为137,资源数据块相对于头部的偏移为0X268。
ResTable_type后接着是一个大小为entryCount的uint32_t数组,每一个数组元素都用来描述一个资源项数据块的偏移位置。 紧跟在这个偏移数组后面的是一个大小为entryCount的ResTable_entry数组,每一个数组元素都用来描述一个资源项的具体信息。ResTable_entry的结构如下:
struct ResTable_entry { //表示资源项头部大小。 uint16_t size; enum { //如果flags此位为1,则ResTable_entry后跟随ResTable_map数组,为0则跟随一个Res_value。 FLAG_COMPLEX = 0x0001, //如果此位为1,这个一个被引用的资源项 FLAG_PUBLIC = 0x0002 }; //资源项标志位 uint16_t flags; //资源项名称在资源项名称字符串资源池的索引 struct ResStringPool_ref key; };
ResTable_entry根据flags的不同,后面跟随的数据也不相同,如果flags此位为1,则ResTable_entry是ResTable_map_entry,ResTable_map_entry继承自ResTable_entry,其结构如下。
struct ResTable_map_entry : public ResTable_entry { //指向父ResTable_map_entry的资源ID,如果没有父ResTable_map_entry,则等于0。 ResTable_ref parent; //等于后面ResTable_map的数量 uint32_t count; };
ResTable_map_entry其后跟随则count个ResTable_map类型的数组,ResTable_map的结构如下:
struct ResTable_map { //bag资源项ID ResTable_ref name; //bag资源项值 Res_value value; };
示例:
图中颜色由深到浅就是一个完整的flags为1的资源项,现在就一起来解读这段数据的含义,这个资源项头部的大小为0X10,flags为1所以后面跟随的是ResTable_map数组,名称没有在资源项引用池中,没有父map_entry,有一个ResTable_map。
如果flags此位为0,则ResTable_entry其后跟随的是一个Res_value,描述一个普通资源的值,Res_value结构如下。
struct Res_value { //Res_value头部大小 uint16_t size; //保留,始终为0 uint8_t res0; enum { TYPE_NULL = 0x00, TYPE_REFERENCE = 0x01, TYPE_ATTRIBUTE = 0x02, TYPE_STRING = 0x03, TYPE_FLOAT = 0x04, TYPE_DIMENSION = 0x05, TYPE_FRACTION = 0x06, TYPE_FIRST_INT = 0x10, TYPE_INT_DEC = 0x10, TYPE_INT_HEX = 0x11, TYPE_INT_BOOLEAN = 0x12, TYPE_FIRST_COLOR_INT = 0x1c, TYPE_INT_COLOR_ARGB8 = 0x1c, TYPE_INT_COLOR_ARGB8 = 0x1c, TYPE_INT_COLOR_RGB8 = 0x1d, TYPE_INT_COLOR_ARGB4 = 0x1e, TYPE_INT_COLOR_RGB4 = 0x1f, TYPE_LAST_COLOR_INT = 0x1f, TYPE_LAST_INT = 0x1f }; //数据的类型,可以从上面的枚举类型中获取 uint8_t dataType; //数据对应的索引 uint32_t data; };
示例:
图中画红线的部分就是一个ResTable_entry其后跟随的是一个Res_value的例子,从中我们可以得出以下信息,这个头部大小为8,flags等于0,所以后面跟随的是Res_value,在资源项名称字符串资源池中的索引为150,对应的值是badge_continue_months,Res_value的大小为8,数据的类型是TYPE_STRING,在资源项的值字符串资源池的索引为1912,对应的值是res/drawable-nodpi-v4/badge_continue_months.png。
当我们对arsc的文件格式有了了解过后,我们就可以开始我们的探索之旅了,由于在使用Android studio调试Apktool源码的时候遇到很多障碍,在前辈的指导下才能够顺利进行调试,所以下面简单介绍下设置Android studio调试Apktool源码的方法。
1.拉取apktool源码到本地
git clone https://github.com/iBotPeaches/Apktool
2.将apktool源代码导入android studio,导入后会android studio会自动查找对应Grable并下载,需要比较长的时间等待。
3.选中Grable中的fatjar进行编译
4.增加RunApktool配置
5.开始调试
首先我们从解包失败的错误异常入手,定位崩溃处的代码。
通过错误提示可以定位到apktool崩溃处的代码,源码位置如下:
/git/Apktool/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/data/ResType.java
在崩溃的函数处下断点,我们开始调试。
通过抛出的异常的说明“Multiple res specs”,以及整个调用堆栈,大体上我们可以看出造成这个异常的原因是在同一个类型的资源里,有多个同名称的资源。
我们使用aapt dump一份apk的资源清单进行观察,在这之前我们需要了解一下appt dump resources的基本格式。
resource <Resource ID> <Package Name>:<Type>/<Name>: t=<DataType> d=<Data> (s=<Size> r=<Res0>)
Resource ID R.java中的资源ID Package Name 资源所在的的包 Type 资源的类型 Name 资源名称 DataType 数据类型,按照以下枚举类型取值 Data 资源的值,根据dataType进行解释 Size 一直为0x0008 Res0 固定为0x00
enum { // Contains no data. TYPE_NULL = 0x00, // The 'data' holds a ResTable_ref, a reference to another resource // table entry. TYPE_REFERENCE = 0x01, // The 'data' holds an attribute resource identifier. TYPE_ATTRIBUTE = 0x02, // The 'data' holds an index into the containing resource table's // global value string pool. TYPE_STRING = 0x03, // The 'data' holds a single-precision floating point number. TYPE_FLOAT = 0x04, // The 'data' holds a complex number encoding a dimension value, // such as "100in". TYPE_DIMENSION = 0x05, // The 'data' holds a complex number encoding a fraction of a // container. TYPE_FRACTION = 0x06, // Beginning of integer flavors... TYPE_FIRST_INT = 0x10, // The 'data' is a raw integer value of the form n..n. TYPE_INT_DEC = 0x10, // The 'data' is a raw integer value of the form 0xn..n. TYPE_INT_HEX = 0x11, // The 'data' is either 0 or 1, for input "false" or "true" respectively. TYPE_INT_BOOLEAN = 0x12, // Beginning of color integer flavors... TYPE_FIRST_COLOR_INT = 0x1c, // The 'data' is a raw integer value of the form #aarrggbb. TYPE_INT_COLOR_ARGB8 = 0x1c, // The 'data' is a raw integer value of the form #rrggbb. TYPE_INT_COLOR_RGB8 = 0x1d, // The 'data' is a raw integer value of the form #argb. TYPE_INT_COLOR_ARGB4 = 0x1e, // The 'data' is a raw integer value of the form #rgb. TYPE_INT_COLOR_RGB4 = 0x1f, // ...end of integer flavors. TYPE_LAST_COLOR_INT = 0x1f, // ...end of integer flavors. TYPE_LAST_INT = 0x1f };
从这部分信息中我们可以看到在这份资源索引表中大部分的资源并没有名称,正是因为没有名称,apktool在往包结构中添加数据时,两个同类型的没有名称的资源就产生了冲突,造成了崩溃。
知道了崩溃的原因,那我们就需要确定它具体是使用怎样的方法做到的,知道了方法我们才能想办法去避开它。
通过前面的arsc文件格式的学习,我们知道资源项中ResTable_entry.key是在资源项名称字符串资源池中的索引,那么我们通过16进制编辑器就可以真实的观察到数据真实的对应情况。
图中红框内的就是一串连续的ResTable_entry.key,我们可以看到,他们全部都指向了资源项名称字符串资源池下标为0的字符串。
上图就是资源项名称字符串资源池下标为0的字符串,很不幸,这是一个空字符串,所以所有名称指向它的资源都变成了空字符,这才造成了我们的解析失败。
确定了失败的原因,我们就可以对我们apktool的源码进行改造,让它能处理这种情况,我通过在Apktool.apktool-lib.java 中的 readEntry()函数中增加对ResTable_entry.key的判断,如果指向0则产生的名称则使用当前资源的资源ID作为名称来解决这个问题。
解包没有崩溃,好像是解决了问题,但是当我们打开res中文件查看时就会发现事情并没有这么简单。
首先解包出的文件夹中缺少drawables文件夹,这意味着缺少大量图片文件,第二我们可以看见layout.xml中引用了大量的外部文件,这部分文件也没有解析,全部放到了unknow目录下的r目录中,从这些现象我们可以得出现在资源获取是没有问题,但是在资源解析的时候又出现了问题。
没办法,继续调试,首先定位到资源解析的位置。
解析资源的代码位于Apktool.apktool-lib.java中, 上图中红框这种的两处就是我们解析失败的原因,包括以下两点。
第一:只考虑了解析res目录下的文件。
第二:没有将其他文件目录下的文件归类到需要解析的list中。 下面针对这两点我们对apktool继续进行改造。
首先修改对是否是文件的资源的判断进行修改,支持r目录,不过这里只是粗略的修改,最好的方式还是通过正则表达式来匹配。
然后是修改获取资源的输入路径,增加对r目录的支持。
最后是增加对输入路径的判断。
解析成功,到这里,我们关于这个样本的anti apktool差不多就结束,剩下的就是一些需要调整的小问题,包括名称的确定,多路径的支持等,就不再在这里赘述了,感兴趣的读者可以自行实验。