几天前，Bluebox Security刚曝出了Android存在安全漏洞。小分队立刻就掌握了其技术细节。最近几天经过对Android的研究，小分队又发现了一个类似的漏洞。攻击者可以对原apk进行修改，但不修改其原apk的签名。只是原理跟Bluebox Security曝的漏洞不太一样，但效果是一样的。

这次我们讲讲技术细节：

1. 在讲这个漏洞之前，首先需要搞明白java里short类型转int类型的问题。要理解这个漏洞，必须明白这个技术点。

public class JavaTest {

public static void main(String[] args) {

short a = (short)0xFFFF;

int b;

b = a;

System.out.println(b);

b = a & 0xFFFF;

System.out.println(b);

}

}

如果你能很清楚的了解上述代码中两次打印变量b的值有何不同，以及为何不同的话，这部分就可以先跳过了。否则还是要先弄清楚再往下看。

2. Zip文件格式

在每个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了。

3. 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)中。

4. 如何实施攻击

要改变一个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大小等。

具体攻击模型，如下图。







5. 总结

总的来说该攻击手段，首先利用了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层不一致导致。