新书上市《深入解析Android 5.0系统》
以下内容节选自本书
Android的签名在理论上可以防止别人破坏了软件后(例如加入恶意代码)还能以你的名义发布。但是Android的签名机制最近接连暴露了两个漏洞,导致整个签名机制形同虚设。
第一个漏洞是由国外的安全公司BlueboxSecurity发现的,这个漏洞自Android 1.6以来就一直存在,号称对99%的android设备造成影响。恶意软件制作者可以在不破坏原有APK签名的前提下,利用这个漏洞来修改APK的代码并绕开Android应用的签名验证机制。
这个漏洞的原理是安装APK文件时,若APK包中同时存在着两个classes.dex,解压时读到第二个classes.dex时会覆盖掉第一个。这样实际进行签名检验的是第二个classes.dex。但是在运行时又是执行的第一个classes.dex,所以只要设法在一个APK文件中放置两个classes.dex,并使它们按照恶意classes.dex在前,正常classes.dex在后的顺序出现在文件中,就可以绕开签名检验并安装成功。
这段出问题的代码位于libcore/luni/src/main/java/java/util/zip/ZipFile.java中,让我们一起看看下面这段从版本4.2.2中摘录的代码:
privatevoid readCentralDir() throws IOException {
......
//Seek to the first CDE and read allentries.
RAFStream rafs = new RAFStream(mRaf,centralDirOffset);
BufferedInputStream bin = new BufferedInputStream(rafs,4096);
byte[] hdrBuf = new byte[CENHDR]; // Reuse the same buffer for eachentry.
for(int i = 0; i < numEntries; ++i) {
ZipEntrynewEntry = new ZipEntry(hdrBuf, bin);
mEntries.put(newEntry.getName(),newEntry);
}
}
很明显,最后这段for循环的代码有问题,循环中读压缩包的内容并逐项加入到mEntries中,而mEntries是一个类型为LinkedHashMap的变量,调用put函数时如果有重名的项,会覆盖掉第一项。
下面再看看版本4.4.1中的代码,比较一下就知道Google是如何修复这个漏洞了。
private voidreadCentralDir() throws IOException{
......
RAFStream rafStream = new RAFStream(raf,centralDirOffset);
BufferedInputStream bufferedStream = newBufferedInputStream(rafStream,4096);
byte[] hdrBuf = new byte[CENHDR]; // Reuse the same buffer for eachentry.
for(int i = 0; i < numEntries; ++i){
ZipEntry newEntry = new ZipEntry(hdrBuf,bufferedStream);
if (newEntry.localHeaderRelOffset >= centralDirOffset){
throw new ZipException("Local file header offset isafter
centraldirectory");
}
String entryName =newEntry.getName();
if (entries.put(entryName, newEntry) != null){
throw new ZipException("Duplicate entry name: " +entryName);
}
}
}
新的代码中会先检查entries中是否已经有同名的项,如果有会抛出异常。
可能有人会感兴趣,如何制造一个这样的apk文件呢?其实很简单,这里就不细说了,毕竟这里不是在教大家制造恶意程序。当然检测这种恶意程序也很简单,只要发现一个apk中有两个classes.dex就可以判定,正常的apk文件不会包含两个classes.dex文件。
第二个Andorid签名漏洞最早由国内的安全team发现并提交给Google,Google很快修复了该漏洞。这个漏洞利用了Android在签名验证过程中,对Zip文件中16位数的读取时,没有考虑到大于2^15的情况。因此在将short型表示的块大小转换成int型时,会把大于2^15的数转换成int型的负数。但是在native层执行时,并不会出错。因为java的int , short,long都是有符号数,而不像C/C++里还有无符号书。具体的漏洞原理就不分析了。