Android 中的 FORTIFY

谷歌開發者_發表於2017-04-26

640?wx_fmt=gif


FORTIFY 是 Android 自 2012 年中以來一直配備的一項重要的安全功能。去年初,在將預設的 C/C++ 編譯器從 GCC 遷移為 Clang 後,我們投入大量時間和精力,確保 FORTIFY 在 Clang 中的質量與之前相當。為做到這一點,我們重新設計了某些關鍵的 FORTIFY 功能的工作方式,具體將在下文介紹。

在我們介紹全新 FORTIFY 的一些詳情之前,我們來簡單回顧一下 FORTIFY 的功能及其用法。



什麼是 FORTIFY?

FORTIFY 是 C 標準庫的擴充套件集,用於攔截對 memset、sprintf 和 open 和其他標準函式的錯誤使用。它有三大功能:

  • 如果在編譯時 FORTIFY 檢測到錯誤呼叫標準庫函式,則在錯誤得到修復前將不允許編譯您的程式碼。

  • 如果 FORTIFY 未獲取足夠的資訊,或者如果確定程式碼是安全的,FORTIFY 將不會對任何資訊進行編譯。這意味著,當 FORTIFY 在找不到錯誤的上下文中使用時,它的執行時開銷為 0。

  • 否則,FORTIFY 會進一步進行檢查,動態確定可疑程式碼是否存在錯誤。如果檢測到錯誤,FORTIFY 將輸出部分除錯資訊並中止程式執行。


思考下例,它是 FORTIFY 在真實程式碼中捕獲的一個錯誤:

struct Foo { int val; struct Foo *next; }; void initFoo(struct Foo *f) { memset(&f, 0, sizeof(struct Foo)); }


FORTIFY 發現,我們錯誤地將 &f 作為 memset 的第一個引數進行傳遞,而實際上應為 f。通常,很難追蹤此類錯誤,因為它表面上可能將 8 個位元組的附加 0 寫入任意的堆疊部分,而實際上對 *f 不進行任何操作。因此,取決於您的編譯器優化設定、initFoo 的用法和您的專案的測試標準,此錯誤可能長時間被忽略。有了 FORTIFY,您會收到如下編譯時錯誤:

/path/to/file.c: call to unavailable function 'memset': memset called with size bigger than buffer memset(&f, 0, sizeof(struct Foo)); ^~~~~~


以下列函式為例,說明如何進行執行時檢查:

// 2147483648 == pow(2, 31). Use sizeof so we get the nul terminator, // as well. #define MAX_INT_STR_SIZE sizeof("2147483648") struct IntAsStr { char asStr[MAX_INT_STR_SIZE]; int num; }; void initAsStr(struct IntAsStr *ias) { sprintf(ias->asStr, "%d", ias->num); }


此程式碼適用於所有正數。但是,當您傳入 num <= -1000000 的 IntAsStr 時,sprintf 會將 MAX_INT_STR_SIZE+1 個位元組寫入到 ias->asStr 中。如果不使用 FORTIFY,此差一錯誤(結果會清除 num 中的一個位元組)可能被靜默忽略。而有了它,程式可以輸出堆疊追蹤資訊、記憶體對映,並在中止執行時轉儲核心資訊。

FORTIFY 還可以執行其他幾項檢查,例如確保對 open 的呼叫具有適當的引數,而它的主要用途是捕獲上述與記憶體有關的錯誤。


但是,FORTIFY 並不能捕獲當前與記憶體有關的 所有 錯誤。以下列程式碼為例:

__attribute__((noinline)) // Tell the compiler to never inline this function. inline void intToStr(int i, char *asStr) { sprintf(asStr, “%d”, num); } char *intToDupedStr(int i) { const int MAX_INT_STR_SIZE = sizeof(“2147483648”); char buf[MAX_INT_STR_SIZE]; intToStr(i, buf); return strdup(buf); }


由於 FORTIFY 根據緩衝區的型別及其分配位置(如果可見)確定緩衝區的大小,因此它無法捕獲此錯誤。在本例中,FORTIFY 放棄檢測是因為:

  • 我們無法確定此類指標指向的物件大小,因為 char * 可以指向不定數量的位元組

  • FORTIFY 無法確定指標分配的位置,因為 asStr 可以指向任何物件。


如果您想知道為什麼這裡有非內聯屬性,那是因為,如果 intToStr 內聯到 intToDupedStr 中,FORTIFY 可能可以捕獲此錯誤。這是因為,編譯器可以因此確定 asStr 指向同一個記憶體作為緩衝區,這是一個 sizeof(buf) 位元組大小的記憶體區。



FORTIFY 的工作方式

FORTIFY 的工作原理是:在編譯時截獲對標準庫函式的所有直接呼叫,然後將這些呼叫重定向至經過 FORTIFY 處理的特殊版本的上述庫函式。每個庫函式由發出執行時診斷的部分和發出編譯時診斷的部分(如果適用)組成。下面是一個簡化的示例,說明經過 FORTIFY 處理的 memset(源自 string.h)的執行時部分。實際的 FORTIFY 實現可能包含幾個附加的優化或檢查部分。


_FORTIFY_FUNCTION inline void *memset(void *dest, int ch, size_t count) { size_t dest_size = __builtin_object_size(dest); if (dest_size == (size_t)-1) return __memset_real(dest, ch, count); return __memset_chk(dest, ch, count, dest_size); }


在本例中:

  • _FORTIFY_FUNCTION 擴充套件到幾個特定於編譯器的屬性,使對 memset 的所有直接呼叫均呼叫此特殊包裝器。

  • __memset_real 用於跳過 FORTIFY,以呼叫“正常的”memset 函式。

  • __memset_chk 是經過 FORTIFY 處理的特殊 memset。如果 count > dest_size,__memset_chk 中止程式執行。否則,它會一直呼叫到 __memset_real 為止。

  • 因為 __builtin_object_size,奇蹟發生了:它很像 size sizeof,但是它不會告訴您某個型別的大小,而是在編譯時嘗試計算出給定指標包含的位元組數量。如果失敗,它會返回 (size_t)-1。


__builtin_object_size 可能看上去比較粗略。編譯器究竟如何能計算出某個未知指標指向多少個位元組?其實……它不能。:)因此,_FORTIFY_FUNCTION 必須內聯所有這些函式:內聯 memset 呼叫也許可以使指標指向的分配(例如,本地變數和 malloc 的呼叫結果等等)可見。如果它可見,我們通常可以確定準確的 __builtin_object_size 結果。

編譯時診斷位同樣主要圍繞 __builtin_object_size 進行。事實上,如果您的編譯器能通過某種方式發出是否可以證明某個表示式為 true 的診斷,則您可以將此診斷新增到包裝器中。利用特定於編譯器的屬性,在 GCC 和 Clang 上均可實現這一點,因此,新增診斷與新增正確的屬性一樣簡單。



為什麼不使用 Sanitize 呢?

如果您熟悉 C/C++ 記憶體檢查工具的話,您可能在想,既然有了 Clang 的 AddressSanitizer 等工具,FORTIFY 還有何用處。Sanitizer 非常適用於捕獲和跟蹤與記憶體有關的錯誤,而且可以捕獲許多 FORTIFY 所無法捕獲的問題,但我們建議使用 FORTIFY,原因有兩個:

  • 除了在程式碼執行時檢查其是否存在錯誤以外,FORTIFY 還可以引發明顯的編譯時程式碼錯誤,而 Sanitizer 在出現問題時僅僅中止程式執行。儘管人們公認應儘早捕獲問題,但我們還希望盡我們所能提供編譯時錯誤。

  • FORTIFY 非常輕便,可應用於生產環境。對我們的程式碼啟用 FORTIFY 後發現,最高 CPU 效能下降約 1.5%(平均下降 0.1%),幾乎不產生記憶體開銷,且二進位制檔案大小增幅很小。與之對比的是,Sanitizer 可能使程式碼執行速度下降一半以上,而且往往消耗大量的記憶體和儲存空間。


有鑑於此,我們在 Android 生產版本中啟用 FORTIFY,以減輕某些錯誤可能造成的損失。尤其是,FORTIFY 可以將潛在的遠端程式碼執行錯誤轉化為僅中止受破壞的應用的錯誤。重申一遍,Sanitizer 可以檢測的錯誤數量超過 FORTIFY,因此我們絕對支援在開發/除錯版本中使用 Sanitizer。但是,對提供給使用者的二進位制檔案執行 Sanitizer,其成本過高,不宜在生產版本中啟用它。



FORTIFY 重新設計

FORTIFY 的初始實現用到了 C89 中的幾個技巧,並引入幾個特定於 GCC 的屬性和語言擴充套件。由於 Clang 無法模擬 GCC 的執行方式,無法完全支援初始 FORTIFY 實現,我們對 FORTIFY 進行了很大幅度的重新設計,使其在 Clang 中也能儘可能高效。特別是,Clang 風格的 FORTIFY 實現不僅支援某些函式過載(如果您使用 overloadable 屬性,Clang 可以非常順利地對 C 函式應用 C++ 過載規則),還可以充分利用一些特定於 Clang 的屬性和語言擴充套件。

我們使用這一新的 FORTIFY 對數以億計的程式碼行進行了測試,包括所有 Android 程式碼、所有 Chrome 作業系統程式碼(需要自行重新實現 FORTIFY)、我們的內部程式碼庫以及許多常見的開放原始碼專案。

此項測試表明,我們的方法破壞程式碼的方式層出不窮,比如:

template <typename OpenFunc> bool writeOutputFile(OpenFunc &&openFile, const char *data, size_t len) {} bool writeOutputFile(const char *data, int len) { // Error: Can’t deduce type for the newly-overloaded `open` function. return writeOutputFile(&::open, data, len); }


struct Foo { void *(*fn)(void *, const void *, size_t); } void runFoo(struct Foo f) { // Error: Which overload of memcpy do we want to take the address of? if (f.fn == memcpy) { return; } // [snip] }


還有一個開放原始碼專案曾試圖解析系統標頭檔案(比如 stdio.h),以確定它包含哪些函式。新增 Clang FORTIFY 位會嚴重干擾解析器執行,導致其編譯失敗。


儘管作出上述大幅變更,但我們看到,破壞程式碼的情況非常少。例如,在編譯 Chrome 作業系統時,不到 2% 的程式包出現輕微的編譯時錯誤,只需修復幾個檔案即可解決這些錯誤。儘管能做到這一點已經“相當不錯”,但它還不夠理想,因此我們對方法進行了優化,進一步減少不相容情況的發生。儘管其中部分迭代需要變更 Clang 的工作方式,但 Clang+LLVM 社群非常認同我們提議的調整和補充並予以大力支援:

  • 在 Clang 中新增 pass_object_size、

  • 在 Clang 中新增 alloc_size(在 LLVM 中新增對應的函式),以及

  • 其他各種增強/改進功能,例如啟用在過載解析期間轉化與 C 不相容的指標。


最近,我們將它推廣應用於 AOSP,且從 Android O 開始,Android 平臺將受到 Clang FORTIFY 的保護。我們仍在對 NDK 進行最後的改進,開發者應該不久就會看到升級後的 FORTIFY 實現。此外,如前文所述,Chrome 作業系統現在也採用類似的 FORTIFY 實現,我們希望在接下來的幾個月與開放原始碼社群合作,將類似的實現* 引入 GNU C 庫 glibc 中。


瞭解更多細節,檢視文內所有連結,請點選文末“閱讀原文”。


推薦閱讀:

Android Studio 2.4 Preview 6釋出,支援Java 8語言功能

Android O 中對裝置識別符號所做的變更

介紹Android原生開發工具包r14

TensorFlow使用者有獎徵集


640?wx_fmt=gif

點選「閱讀原文」,檢視文內連結640?wx_fmt=gif

相關文章