安卓加固apk逆向分解和dex重建方案

现在发布的应用为了防止代码泄漏,除了代码混淆,还会使用加固框架对apk进行特殊处理。加固的方案虽然很多,但目的只有一个,就是保护代码不被工具反编译。当然,加固还有资源保护等其它处理,但我这里主要讨论的是dex加固。
在阅读以下内容前,你需要对dex结构有一定了解,但不需要太全面,也不用了解字节码保存方式和指令功能。首先,要知道dex的头部结构,以及它重要的一块数据,即map区。然后,了解string索引区和string数据区的特点。dex的结构,很多文章都有介绍,所以我这里就不再细说。大家可以搜索一下,了解其基本结构,但是不用细究其它区域的结构,除非你要学得深点。

怎样的apk是被加固的呢?使用d2j工具,对dex进行转换。若发现转换后的jar文件内容很少,重新转换成dex后,大小差别很大,则基本可以肯定,这个dex是加固的,且其它dex可能混合在这个文件。 我这里要分析的就是这种文件。若是其它场景,可以类比处理,本质差别不大,重要的是dex的重建。而对于经过全文件加密处理的dex,则需要对转换的代码进行分析,确认解密过程。对于这类加密的数据逆向分析,我就不讲解,毕竟加密方式很多,逆向级别更高。
我的dex从哪个apk获取的,以及使用的是哪种加固产品?这里不方便说明,毕竟这种加固方案可能在多个产品使用,或者是个人的产品。而且,我这里主要讲解dex重建过程,对于“被破坏的”dex来自哪里,就看大家自己选择。另外,加固方案也会因为安全问题,不断改变加密方式。我这里讲解的方案在一段可预见的未来是有效的,但并不保证别人不会因此升级其它方案。

在做这个dex重建过程中,需要开发一个工具。建议选择java开发,因为涉及的很多功能,例如摘要算法,都已经在java标准库里(毕竟dex主要是从java转换过来的)。对dex的分析和更新逻辑不复杂,大家按照操作需要开发一下。d2j有摘要重建的命令,到时重建摘要时可以使用这个或者参考这个开发。在分析过程中,我阅读了一下d2j源码,有很多接口可以使用,例如字符串处理,这些都是可以借鉴。在转换dex时,d2j可能出现转换失败的问题,这个跟工具版本关系比较大。另外,对于二进制文件的阅读、拆分、拼接和修改,建议选择方便的二进制处理工具。我一直使用的是winhex,大家可以自由选择。

首先,大家先开发一个dex头部解析工具,把头部的字段都打印出来。 不要通过二进制编辑器一个一个看,不方便分析。头部里面重要的信息就是每个分区的偏移和大小。基于这些分区偏移和大小,我们可以确定这个dex实际大小,以及这个dex是否混合了其它数据。
以下是我解析的一块dex。我的工具一方面打印头部信息,另一方面,计算摘要并跟头部记录的摘要进行比较(判断这个dex是否改变)。之后,打印头部指向的map区信息,然后自己从尾部开始逆向分析出map区信息。只要两个map信息一致,说明dex的map区确实在尾部,若不一致,只要map不是在中间,说明dex被混合了其它数据:

magic: 6465780a30333500
checksum: 41cf2e2e
signature: 922f2f5b3720315ef0d6790622c5d17dab59a96e
fileSize: 599d4 (367060)
headerSize: 70 (112)
endianTag: 12345678 (305419896)
linkSize: 0 (0)
linkOff: 0 (0)
mapOff: 59904 (366852)
stringIdsSize: d90 (3472)
stringIdsOff: 70 (112)
typeIdsSize: 291 (657)
typeIdsOff: 36b0 (14000)
protoIdsSize: 3a9 (937)
protoIdsOff: 40f4 (16628)
fieldIdsSize: 520 (1312)
fieldIdsOff: 6ce0 (27872)
methodIdsSize: b0b (2827)
methodIdsOff: 95e0 (38368)
classDefsSize: 133 (307)
classDefsOff: ee38 (60984)
dataSize: 4853c (296252)
dataOff: 11498 (70808)
------------------
map size: 11 (17), bytes: d0 (208), total: 599d4 (367060)
type: 0(HeaderItem), size: 1, offset: 0
type: 1(StringIdItem), size: d90, offset: 70
type: 2(TypeIdItem), size: 291, offset: 36b0
type: 3(ProtoIdItem), size: 3a9, offset: 40f4
type: 4(FieldIdItem), size: 520, offset: 6ce0
type: 5(MethodIdItem), size: b0b, offset: 95e0
type: 6(ClassDefItem), size: 133, offset: ee38
type: 2001(CodeItem), size: 6ed, offset: 11498
type: 2003(DebugInfoItem), size: 42b, offset: 3a19c
type: 1001(TypeList), size: 1fa, offset: 3cbcc
type: 2002(StringDataItem), size: d90, offset: 3de72
type: 2004(AnnotationItem), size: 1b3, offset: 50d37
type: 2000(ClassDataItem), size: 126, offset: 526e1
type: 2005(EncodedArrayItem), size: 11, offset: 5610b
type: 1003(AnnotationSetItem), size: 16f, offset: 5692c
type: 2006(AnnotationsDirectoryItem), size: ef, offset: 57854
type: 1000(MapList), size: 1, offset: 59904
------------------
reversed map:
type: 1000(MapList), size: 1, offset: 59904
type: 2006(AnnotationsDirectoryItem), size: ef, offset: 57854
type: 1003(AnnotationSetItem), size: 16f, offset: 5692c
type: 2005(EncodedArrayItem), size: 11, offset: 5610b
type: 2000(ClassDataItem), size: 126, offset: 526e1
type: 2004(AnnotationItem), size: 1b3, offset: 50d37
type: 2002(StringDataItem), size: d90, offset: 3de72
type: 1001(TypeList), size: 1fa, offset: 3cbcc
type: 2003(DebugInfoItem), size: 42b, offset: 3a19c
type: 2001(CodeItem), size: 6ed, offset: 11498
type: 6(ClassDefItem), size: 133, offset: ee38
type: 5(MethodIdItem), size: b0b, offset: 95e0
type: 4(FieldIdItem), size: 520, offset: 6ce0
type: 3(ProtoIdItem), size: 3a9, offset: 40f4
type: 2(TypeIdItem), size: 291, offset: 36b0
type: 1(StringIdItem), size: d90, offset: 70
type: 0(HeaderItem), size: 1, offset: 0
reversed map size: 11 (17), bytes: d0 (208), total: 599d4 (367060)
------------------

dex的data区最后(文件末尾)一般是map区。 这个map区根据dex的结构可以知道,这是对头部的全部索引的一种校验。它是我们重建dex重要的数据。但是,dex文件末尾并不一定是map区。这个可以通过头部信息确定map区是不是在末尾。若map区不是在末尾,我们就需要基于map区的特征在文件里面查找。这个需要比较大的耐心,但是情况很少,特别是很大的dex(小的dex基本在data区开始)。我分析的dex都是以map区结尾的文件,所以基本可以在尾部确定map区,然后基于这个map区记录的信息,重建其它数据区索引,并再往前查找其它map区,重建其它dex文件。

// map区一般位于末尾,且第一个是头部类别,前面加个长度值(较小,大约16)
// type     size     offset
// 00000000 01000000 00000000
// 第二个是string索引偏移,比较明显是类别(1)和偏移(70)
// type     size     offset
// 01000000 be0800 70000000
// map信息里面,除了索引和定义相关区块(类别编码小于0x1000)在data区前面外,其它区块都是属于data区
// 即从非索引和定义相关区块(类别编码大于0x1000)开始的偏移应该是data区的开始偏移

判断dex里面混合了多少个dex有个简单的方法,就是肉眼扫一遍dex,看里面有字符串区域的个数。 每个dex都会有一块明显的字符串区域,而这个可以确定dex的数量。有人说我的眼睛无法识别,那真的对不起,目前这个识别我还没有写工具,但也是可以实现的,毕竟utf-8的编码结构还是很清楚。我在编写爬虫解析引擎时,倒是写过这类编码识别。不过,我们只是解析dex,相信自己眼睛吧!

然后,通过string索引区和string数据区特征,确定dex文件开始。 我们在重建dex文件时,需要假定头部数据被加密了。若没有加密,则我们只要根据dex特征,拆分文件就行。一般dex第一块数据(头部之后)是string索引区,且数据连续分布在data区(并不一定是开始),所以数值会呈现局部重复。什么意思呢?string索引区是保存string数据区每段数据的索引。若string数据区连续分布(一段字符串后面紧接另一段),则string索引区数据必定是规律性的增长(按照每段字符串长度偏移),里面至少高位字段都是重复的!
首先观察全部数据,找到字符串区域,这块区域有比较明显的文字。借助MTUF-8字符串的格式(头部是长度,尾部是0),不断反推出最初位置。常见第一个字符串是0000,即空字符串。以这个位置往上查找,应该能找到一块局部重复的数据区,这块区域很大概率就是string索引区。之所以会这样,是因为string数据一般连续分布,所以索引很容易局部重复并且连续。然后,确定string索引区第一个数据。配合map区的map区偏移推测文件大小(map区偏移+map区大小=文件大小)。另外string数据区偏移可以在map区的StringDataItem记录获得,可以作为校验推测的开始位置和文件大小。

最后,基于以上map区的偏移记录和推测偏移(map区偏移、string数据区偏移),基本可以确定文件大小(通过偏移差或文件大小比较判断)。 由于推测的string数据区开始位置可能不准(毕竟字符串长度并不一定是字节长度,推测有点困难),所以一般不作为文件大小的计算标准,而是作为校验。结合string索引区和文件大小,可以推测需要往前填充多少数据。之后,获取string索引区数据,检查该数值指向的偏移是否正确指向一个字符串开始位置(并不一定是第一个,因为部分索引可能被加密)。对其它区域数据进行校验,若全部校验有效,那么就可以生成头部(包括修复丢失的string索引区部分数据)。我这次分析的dex文件,刚好dex头部和部分string索引区数据被加密,所以不得不先修复string索引区,再重建dex头部。string索引区如何修复?string索引区是指向string数据区的指针。对string数据区重新扫描并生成对应索引即可。string索引区大小可以在map区确定。一般只要修复到破坏的索引就行,其它可以认为没有问题。

以上分析的核心原则:
(1)通过string索引区和map区特征,基本限定文件开始和结束区间;
(2)通过map区偏移记录和推测偏移,确定文件大小和填充多少数据;
(3)进一步对以上限定的数据进行检验,保证索引都能正确对齐。

以上的说明可能特别抽象,只能作为大家分析的参考。对于分析用到的工具,要是需要,我可以再上传。因为能够做这种分析的人,基本不会有开发困难,只是需要一个参考方案。
对于d2j工具,github上有最新版本,大家可以自己编译。不过,我是用2.0的版本,所以转换过程中出现bug。这个转换bug在我另外一篇文章有说明,大家可以看看。若要自己修改现有d2j的局部功能,记得选择跟你现在编译版本一致的分支,否则编译部分源码时可能会出现错误。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值