1. 程式人生 > >Android ZIP檔案提取classes.dex檔案簽名校驗繞過漏洞

Android ZIP檔案提取classes.dex檔案簽名校驗繞過漏洞


技術細節:

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層不一致導致。