幾天前,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層不一致導致。