Android第二个绕过签名认证漏洞原理

一、漏洞描述


该漏洞最早由国内“安卓安全小分队”(http://blog.sina.com.cn/u/3194858670)发现并提交给Google,Google迅速修复了该漏洞,对应编号为bug 9695860。该漏洞是即Bluebox Security提报Android 绕过应用签名认证漏洞后又一成功绕过Android签名认证漏洞。该漏洞原理与Bluebox Security漏洞略有不同。

二、影响设备
理论上会影响漏洞修复前所有设备。

三、漏洞原理


了解该漏洞需要对ZIP格式有些许了解(http://www.pkware.com/documents/casestudies/APPNOTE.TXT)。

在每个Zip文件中都有一个Central directory,Central directory中的每一项是一个File header。这个File header的结构对应到Android代码的类就是ZipEntry。File header结构中有一个偏移量指向local file header,local file header后面就紧跟着file data。接下来我们详细看一下local file header的结构:
local file header signature 4 bytes (0x04034b50)
version needed to extract 2 bytes
general purpose bit flag 2 bytes
compression method 2 bytes
last mod file time 2 bytes
last mod file date 2 bytes
crc-32 4 bytes
compressed size 4 bytes
uncompressed size 4 bytes
file name length 2 bytes
extra field length 2 bytes
file name (variable size)
extra field (variable size)


可以看到,除最后2个域以外,local file header的其他域都是定长的。而这两个变长域的长度是由file name length和extra field length所确定。再次说明,紧跟在extra field后面的就是文件的数据file data了。

Android是如何进行apk校验的呢?
Android在进行apk文件校验时,会调到ZipFile的public InputStream getInputStream(ZipEntry entry)函数。这函数中,有这么一段:


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);
} else {
return rafstrm;
}

注意:上述代码中红色部分。localExtraLenOrWhatever就是local file header结构中的extra field length。如果这里的extra filed length的大小是大于2^15,会怎么样?
没错,localExtraLenOrWhatever将会是负值。因此接下来,rafstrm.skip(entry.nameLength + localExtraLenOrWhatever); 这句将无法真正跳过变长域file name (variable size) 和extra field (variable size)。反而有可能呢会跳到file name (variable size)中,甚至file name (variable size)之前。当然为了攻击方便,我们还是期望它跳到file name (variable size)中。

漏洞攻击方式:
要改变一个apk的行为,显然攻击的目标就是apk里的classes.dex文件。对于classes.dex文件在apk文件中的local file header结构,其file name (variable size)域的内容肯定就是“classes.dex”了。注意,这里的后缀名dex,正好和dex文件开头的三个字节完全相同(不理解的,参见dex文件格式)。


a) 利用这一点,从file name (variable size)域“classex.dex”的“.”之后开始我们可以写入一个完整的dex文件。这个dex文件必须是原apk里的classes.dex文件。只有这样才能绕过签名验证。
b) 修改extra field length,使之为0xFFFD。因为这个值刚好为-3。根据漏洞,rafstrm.skip(entry.nameLength + localExtraLenOrWhatever); 这句就会跳到file name (variable size)域中的“.”之后。也就是一个dex文件的开始,这里必须是原dex文件内容。
c) 修改local file header之后的file data数据。在这里写入带有攻击代码的classes.dex内容。
d) 以上的修改会带来apk文件一些结构上的调整,比如扩充extra field域,调整file data大小等。


具体攻击模型,如下图。



总的来说该攻击手段,首先利用了Android在签名验证过程中,对Zip文件相应16位域的读取时,没有考虑到大于2^15的情况。(因为java的int , short, long都是有符号数,而不像C/C++里有无符号数)。
其次利用了Zip文件中的local file header结构的extra field域来存放原classes.dex。但这个域的大小最多只能是2^16-1,因此被攻击的Apk里的classes.dex大小必须在64K以内。否则,就无法对其进行攻击。这算是这种攻击方式的一个限制。
最后还有一个问题补充说明:之所以这种攻击方式能成功,还在于在运行时,系统抽取的是hacked classes.dex,而在签名校验时,验证的是extra域里的classes.dex。前者是在libdex.so中实现,后者在Java层实现。是由Java层跟Native层不一致导致。


四、相关链接

http://www.androidpolice.com/2013/07/11/second-all-access-apk-exploit-is-revealed-just-two-days-after-master-key-goes-public-already-patched-by-google/

更多精彩内容