Android ZIP檔案提取classes.dex檔案簽名校驗繞過漏洞
阿新 • • 發佈:2019-01-22
技術細節:
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層不一致導致。