49

从android源码看脱壳

 5 years ago
source link: https://www.tuicool.com/articles/7BBzqin
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

从android源码看脱壳

平时接触的安全大多数都是web端上的安全,由于web的基本架构是采用的B/S模式,本身以浏览器作为客户端。这样和移动端就形成了一个较为明显的区别:那就是移动端相比于web端要多了一套自我保护的安全能力,或者说是一种防止别人分析甚至是破解的能力。

在android的发展过程中,人们慢慢发现,自己写的app经过打包发布到各大应用商店后,经常会被别人下载下来,经过一些分析,修改,重打包,重签名,重发布。破坏了app的内部逻辑和相应的资源,于是,大家开始了app的加壳,是众多app加固方法中非常有效的方法。

从最早的app动态加载,再到后来的不落地加载,指令抽取,或者是现如今的VMP,混淆等相关加固技术的发展,移动端加固的对象越来越小,破解的门槛也越来越高,加固的难度也越来越趋向于虚拟机、深度混淆。但同样也带来了更大的性能损失、崩溃率的指数增长。致使相当一部分公司不愿意接受这种高损耗性壳。

看了前人很多脱壳分析文章,亦或是一些优秀的开源的脱壳工具。你会发现,其实真正能写出这类工具的人往往都是对dex文件,so文件,虚拟机加载机制等非常熟悉的,以至于他们随意拆解操作系统,编译出他们自定义的镜像文件。或许,从源码里更能发现出那些前人为什么这么"脱"。

早期的大部分加壳都是保护真正的dex文件,虽然dex文件要经过优化,优化成odex,但我们依然说dex文件是dalvik虚拟机的可执行文件。每一个apk文件都对应着内存里的LoadedApk对象,这个对象里保存了类加载器。每一个LoadedApk对象通过ApplicationLoaders中的getClassLoader(下图第30行)来获取属于自己的类加载器。

eAnyAbm.jpg!web

在getClassLoader中,就是用PathClassLoader(上图第66行)来对dex文件进行最开始的加载的。其中DexClassLoader同样也可以对apk,dex,jar文件,甚至从SD卡上进行加载,而PathClassLoader只能对系统安装的apk文件,就是/data/app目录下的apk文件进行加载。这也就间接解释了为什么动态加载原理的壳基本都是用DexClassLoader来实现的。

yqQfQny.jpg!web

PathClassLoader中只有2个构造函数(下图第37行和63行),没有什么实际的操作,都是直接执行父类BaseDexClassLoader中的构造函数。

YreeqeQ.jpg!web

在BaseDexClassLoader中,发现构造函数中有了实际操作,那就是构造出一个pathList变量(下图第48行),这是DexPathList一个实例。整个的构造函数的重要逻辑就落在了DexPathList中。

Zz2Ufie.jpg!web

在DexPathList中,有一个私有属性dexElements(下图第62行),这是一个Element数组。经常会遇到有一些apk解压之后是多个dex文件,所以源码这里是使用的数组。当dex文件里的方法数量过多时,无法承载的情况下,就会存在这种多dex情况。

bE7faqa.jpg!web

这是一个内部类Element,详细的定义如下。其中dexFile数据结构就是dex文件在内存中的映射,从某种角度上来说,dexFile在内存中对应着dex文件。这是在脱壳中非常重要的数据结构。另外的file是需要加载的file文件,isDirectory代表是否为目录。

BRn6Vvj.jpg!web

回到之前的DexPathList中,发现其构造函数中使用的是本类的makeDexElements函数生成的dexElements(下图第112行)。

vaeAvij.jpg!web

其中makeDexElements函数的第一个参数是调用了splitDexPath函数形成的一个file列表。用来在makeDexElements用来遍历加载使用。实际的split操作和判断是否为目录的工作是在splitAndAdd函数中完成的。 其中splitDexPath函数是依次调用了splitPaths()-->splitAndAdd()。

Z7JvEnU.jpg!web

看了第一个参数之后我们继续回到makeDexElements函数中。这里对刚才的第一个files列表进行遍历,这里的判断相对都比较简单,都是对文件名后缀做一系列的判断(下图第218行开始),但是最后都是进入到了loadDexFile函数中进行加载(下图第221行或230行)。

3MR7Zz2.jpg!web

但是在loadDexFile函数中,只是对优化dex目录做了一个是否为空的判断(下图第262行),便进入到了DexFile.loadDex中。可见,dex文件的优化工作是在DexFile这个类中实现的。

yeaINvR.jpg!web

在DexFile中,先对outputname判断是否为空,然后对文件的所有者id也进行了判断。然后通过openDexFile函数(下图第111行)生成了cookie。

7NVzU3z.jpg!web

实际上openDexFile函数还是调用了一个Native方法(下图第301行)。

nMBrIbm.jpg!web

openDexFileNative函数中sourceNameobj就是java层传入的dex文件指针,outputNameobj同理,是输出文件指针。 (下图第194行)是在检测我们加载的dex文件是否为系统boot的dex文件。 (下图第208行)是在判断扩展名为dex的情况。如果是dex,会调用dvmRawDexFileOpen(),并生成DexorJar实例,并对相应的属性进行赋值,比如isDex。 (下图第216行)是扩展名为jar的情况。具体内容和dex文件情况一样。 (下图第223行)是其他情况,既不是dex,也不是jar。直接打印日志抛异常就可以了。 (下图第228行)是针对dex文件或者jar文件的情况,将所生成的DexorJar实例添加到gDvm中userDexFile结构体的hash表中,这样dalvik虚拟机会在hash表中直接查找。

2YVBFfR.jpg!web

vEr2Yva.jpg!web

在dvmRawDexFileOpen函数中操作相对比较复杂,流程稍微多一些,同样涉及了很多dex文件相关结构。 (下图第128行)首先是打开文件句柄。 (下图第134行)先对dex文件的文件头中的校验码进行校验,并把值赋值给adler32。如果失败直接跳到bail。 (下图第139行)获取dex文件的修改时间,同样进行赋值操作。 (下图第150行)判断odex文件名是否为空,如果空就生成一个。 (下图第161行)dvmOpenCacheDexFile函数内部传入了cachename,主要是用来验证缓存文件正确性。如果验证没问题会在函数内部会将newFile赋值为true。

yiMnQrf.jpg!web

(下图第177行)开始为odex文件优化做准备,真正执行优化逻辑的是dvmOptimizeDexFile函数(下图第192行),其中传入的optfd是dex的文件句柄。 (下图第212行)dvmDexFileOPenFromFd函数内部使用mmap函数对dex实现内存加载。

7JRv6bR.jpg!web

RfIva2j.jpg!web

走到这里我们一起屡一下思绪,或者喝一杯咖啡,刚才的最后2行我提到的2个词分别是优化和加载。分别是对应到了上面的dvmOptimizeDexFile函数和dvmDexFileOPenFromFd函数。优化的意思实际上就是为了加载文件之前做的所有准备性的工作,都是为了给加载做铺垫性的工作。那么后面,主要是分别从优化和加载里面看看对文件进行了哪些具体的操作。

yYJzE37.jpg!web

那我们按照执行顺序,首先看看优化的过程。 其实这里面跟踪函数,我想特别说一下,有时候光看代码容易很难抽血的想象出代码的逻辑。其实android源码中的注释很多时候写的非常详细。 dvmOptimizeDexFile()主要是生成一个优化过的odex版本。这里是fork一个进程,通过去执行dexopt来完成此次优化。 (下图第373行)从这里开始去fork一个进程,然后初始化了一些变量,比如kDexOptBin为/bin/dexopt。dexopt是用来做优化的。 还有androidRoot变量,如果为空,就默认设置成/system。并且修改pgid。 (下图第399行)从这里开始申请一块内存,用来拼接execFile,拼接成 /system/bin/dexopt 。 (下图第403行)从注释里可以看出,这里应该是设置程序的参数了。

mAJRFjJ.jpg!web

这里面最重要传递了--dex参数和其他一些比如dexoffset,dexLength等。后面对gDvm这个全局变量进行了一些判断和修改。(下图第470行)调用了dexopt。

Uvmi6zj.jpg!web

那我们继续跟踪一下dexopt的源码。看看到底是如何做的优化。 就像他注释里说的,这里只是决定去哪儿。我们上段代码里传的是--dex,继续分析fromDex()

Jf6vU3a.jpg!web

在fromDex函数的尾部,终于找到了做优化的地方。(do the optimization)

Qn6Jnyi.jpg!web

由于dvmContinueOptimization()函数非常长,这里我准备先插一下,介绍一下odex文件的大致结构。odex文件实际上是在dex文件基础上,添加了一些header,依赖库和辅助数据。可能很好奇的是既然odex在dex基础上添加了那么多,怎么能叫优化呢?因为odex文件都会生成在系统的/data/dalvik-cache目录下,这样dalvik就不用每次都从apk的文件包中解压去加载dex。这里关于dex文件的结构我们就当成黑箱了,实际上里面有很多节区,将字符串,类,函数指令,属性等分别存储在自己的节区中。

IBR3Ura.png!web

(下图第539行)在dvmContinueOptimization源码中,首先对传进来的dexLength和dexOffset进行了校验。

YzAbeqA.jpg!web

(下图第566行)利用mmap函数映射出一块内存,dexOffset实际上就是一会存放dex文件的地址,dexLength就是所要存放dex文件的大小。

JvEZbmq.jpg!web

随后调用了rewriteDex函数,注释上有说明,主要是完成对dex文件的字节序列重新排列,结构对其等。

J7JVRfI.jpg!web

(上图第609行)随后便调用了dvmDexFileOpenPartial(),这个函数也就是广为流传的早期脱壳点,因为这个函数第一个参数为dex文件地址,第二个是dex文件长度。经过了rewriteDex的优化重写,这里的dex文件一般都很准确。 再往下就是对odex文件其他部分进行填充。 (下图第676行)是对odex文件的依赖库填充。 (下图第696行)把优化之后的结构体数据填充到odex文件中。 (下图第705行)对文件填充之后,要重新计算校验码。 (下图第715行)其他所有数据填充完毕后,开始计算头部的一些偏移地址,长度以及其他数据。

iIBNny2.jpg!web

至此我们的优化过程就告一段落,接下来跟踪一下odex的解析过程。 在之前的分析说到dvmDexFileOPenFromFd函数。这里的注释已经说明了一切,对优化过的odex进行映射到内存,并且解析。解析的工作是在(下图第114行)dexFileParse中完成的。

MRjQvue.jpg!web

其实整个优化也好,加载解析也好,其实都是为了得到这个DexFile对象。一个DexFile对象对应着硬盘上的一个dex文件。从一个文本dex文件,到最终生成DexFile,就基本完成了整个优化和解析这个流程。但是对于Dalvik来讲,DexFile尚未结束。对于虚拟机来说,它得到了应该是一个ClassObject结构体。将DexFile结构体和ClassObject结构体连接起来是靠DvmDex结构体实现的。 下面我们分析一下dexFileParse函数,看看它是怎么生成DexFile的。

(下图第296行)首先是做一个简易的长度判断,随后申请一块内存,用来生成DexFile。

(下图第309行)先对odex的头部进行拆分,整个生成的过程实际上就是把odex文件里的东西按照一定的规则赋给DexFile。这里只完成对头部的操作。先比对了版本号。

(下图第321行)这里我个人认为主要是为了建立DexFile的pClassLookup。

(下图第325行)忽略掉dex文件头,并且将忽略掉的部分长度也随之减掉。

(下图第336行)调用了dexFileSetupBasicPointer()用来还原dex的每一个节区。(dexFileSetupBasicPointer的源码在后面)

(下图第344行和374行)分别是对文件校验码和签名进行了重新的校验。其中签名使用的是SHA-1算法对除去magic,checksum,signature外余下的所有文件区域进行计算。用于唯一标识文件。

QFZFJfr.jpg!web

JruEziN.jpg!web

如上面所说的dexFileSetupBasicPointer的源码,完成了dex文件数据和DexFile成员的关系映射。

f6zeMvF.jpg!web

经过千辛万苦也算是终于拿到了DexFile了,但是仔细观察DexFile的结构体发现也没有实际的指令啊。那虚拟机到底是怎么解释这些指令呢。实际上上文提到的ClassObject类才是虚拟机最终想见到的人。DexFile和ClassObject之间是靠DvmDex这个类建立起来的。而ClassObject类中保存着Method结构体的地址。而Method结构体中就有我们想要的实际指令insns。dalvik虚拟机正是解析的这些insns。 实际上画一个图就是下面这个思路。

e2uqam3.jpg!web

在这里我们就可以联想到指令抽取壳的原理是在运行前将dex里的insns抽取为空,将其复写成全0,等到我们虚拟机加载器指定method的时候再将Method的insns给还原成正确的insns。这样就防止了之前通过dump或者hook脱壳就能拿到完整的dex文件。这样即使dump出来,指令也是全空的。后来针对这种指令抽取,有一个开源工具叫dexhunter( https://github.com/zyq8709/DexHunter ),大致的原理是同样修改了代码,编译出新的镜像,然后分段dump,针对空指令的处理是当加载dex文件之后,主动对class_def_item进行遍历,并使用dvmDefineClass等函数主动的去加载,初始化,完成指令的还原,最后对完成的指令再做一些修复,包括指令对其,指令出界等。

写这篇文章参考了很多前人的经验,很感谢前人的无私奉献,加上我自己的一些个人理解,也算是初步的把这个dex文件加载的流程简单的跟踪了下来。但是关于很多源码的细节,尚未深究。比如他为什么这么写而没有那样写,亦或是他这么写的目的又是什么等等。小豹依然很喜欢整理思绪,在这里小豹只想说,关于android安全,未完待续,尽请期待!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK