# 基於 C 語言標準研究與系統程式安全議題 contributed by 林靜儀 > email: jennifer710846@gmail.com > blog: https://raagi0w0.wordpress.com ## 目標 1. 藉由研讀漏洞程式碼及 C 語言標準,討論系統程式的安全議題 2. 透過除錯器追蹤程式碼實際運行的狀況,了解其運作原理; 3. 取材自 [dangling pointer](https://en.wikipedia.org/wiki/Dangling_pointer), [CWE-416 Use After Free](https://cwe.mitre.org/data/definitions/416.html), [CVE-2017-16943](https://nvd.nist.gov/vuln/detail/CVE-2017-16943) 以及 [integer overflow](https://en.wikipedia.org/wiki/Integer_overflow) 的議題; ## 研究目的 藉由閱讀 C 語言標準理解規範是研究系統安全最基礎的步驟,但很多人都忽略閱讀規範這點,而正因對於規範的不了解、撰寫程式的不嚴謹,導致漏洞的產生的案例比比皆是,例如 2014 年的 OpenSSL Heartbleed Attack[^1] 便是因為使用 memcpy 之際缺乏對應記憶體範圍檢查,造成相當大的危害。本文重新梳理 C 語言程式設計的細節,並藉由除錯器幫助理解程式的運作。 ## 實驗環境 * 編譯器版本: gcc 8 * 除錯器: [PEDA](https://github.com/longld/peda) (以 GDB 為基礎搭配 Python 的強化除錯器) / [Affinic Debugger (ADG)](http://www.affinic.com/?page_id=109) (以 LLDB 為基礎的視覺化除錯器) * 作業系統: Ubuntu Linux 18.04 / macOS 10.13.6 (17G65) ## 主題 (ㄧ): Integer type 資料處理 ### I. Integer Conversion & Integer Promotion 考慮到以下的程式碼: ```c #include <stdint.h> #include <stdio.h> unsigned int ui = 0; unsigned short us = 0; signed int si = -1; int main() { int64_t r1 = ui + si; int64_t r2 = us + si; printf("%lld %lld\n", r1, r2); } ``` 從結論而言: r1 輸出的結果是十進位 `4294967295`、r2 輸出結果是十進位 `-1`,而探究 C11 標準書[^2]裡提到關於 Integer 的特性有兩個,分別是 Integer Conversion 和 Integer Promotion: #### (1) Integer Conversion Integer Conversion 指的是各種 Integer 型別之間有轉換優先權之分,當編譯器處理多個 Integer 型別資料時,會依照定義的優先順序處理資料儲存的格式。 > **C11 Standard (§ 6.3.1.1)** Every integer type has an integer conversion rank defined as follows: No two signed integer types shall have the same rank, even if they have the same representation. >- The rank of a signed integer type shall be greater than the rank of any signed integer type with less precision. >- The rank of long long int shall be greater than the rank of long int, which shall be greater than the rank of int, which shall be greater than the rank of short int, which shall be greater than the rank of signed char. >- The rank of any unsigned integer type shall equal the rank of the corresponding signed integer type, if any. >- The rank of any extended signed integer type relative to another extended signed integer type with the same precision is implementation-defined, but still subject to the other rules for determining the integer conversion rank. 依照 C11 Standard[^2],把 integer 的 rank 排出來是: - long long int $>$ long int $>$ int $>$ short int $>$ signed char - unsigned int == signed int, if they are both in same precision and same size - rank between extended signed integer types are implementation-defined - rank of standard ones higher than the extended ones - 而這項定義會接下來影響到 Integer Promotion 這個特性 #### (2) Integer Promotion 當 Integer 資料進行算數運算時,例如使用 unary operator (`+`, `-`, `*`, `/`) 時,以 Integer Conversion rank 為基礎,若 rank 比 int 低者提升成 Integer、與 int 等位或高於 signed int 者則提升為 unsigned int 的資料型態: ```c /* In the case that the rank is smaller than int */ char c1, c2; // Both of them are char c1 = c1 + c2; // result of c1 becomes to integer /* In the case that the rank is same as int */ signed int si = -1; /* si & ui are at the same rank */ unsigned int ui = 0; int result = si + ui; // result is unsigned ``` ### II.衍生的資安議題: Integer Overflow[^3] Integer Overflow 發生在進行運算時,結果的數字超過儲存結果的變數的資料型態,例如算出來是 64-bit 的數字,但是 result 是 type of int 。這時會發生一個叫作 wraparound 的現象,當數字超過最大值,多出來的數字會從最小值重新開始,就像時鐘一樣,早上超過 12:00 時,13:00 時指針會指向 1 重新開始。 回到程式來,以 8-bit signed integer 而言,它的範圍介在 -128 ~ 127 之間,如果程式中存了一個數字 127,我們對其 +1,直觀來想答案應該是 128,但是因為 128 超過這個資料型態的範圍,反而會變成 -128。反過來如果數字太小也會有問題,叫作 Underflow,在範圍的最小值的地方發生 wraparound 。 ## 主題 (二): 物件的生命週期 ### I. Dangling Pointer C11 標準中 6.2.4:2 [^2]提到物件的生命週期︰ > **C11 Standard (§ 6.2.4.2)** > > The lifetime of an object is the portion of program execution >during which storage is guaranteed to be reserved for it. An object exists, has a constant address, and retains its last-stored value throughout its lifetime. If an object is referred to outside of its lifetime, the behavior is undefined. The value of a pointer becomes indeterminate when the object it points to (or just past) reaches the end of its lifetime. Dangling pointer 指的是當一個程式中的指標不被使用的時候,例如某個記憶體區段被 free() 之後,free() 本身釋放的是指標指向 heap 的連續記憶體而不是本身指標佔有的記憶體 (`*ptr`),程式並不會自動將指標設為 NULL,但有可能該記憶體區段已經在程式中挪為他用,而這個有指的指標就稱為 dangling pointer。故在使用指標時,安全起見應該要記得將不用的指標在 free() 後設為 NULL。 ### II. CWE-416 Use After Free[^5] 根據 OSWAP 內容定義指出[^4],當持續使用一個已經被釋放的指標時,會產生 Undefined behavior 以及 write-what-where condition,而 UAF 錯誤形成的情況通常發生在當 double free 時或 memory leak 的時候。當已經被釋放的記憶體空間並沒有被清空,仍然保存著舊有資料,則有可能在重新分配記憶體時使用到先前已經被 free 的指標,但指標內仍指向某個區域,藉此若精心設計一段利用的程式碼,配合使用 freed 指標,則可以被任意執行程式碼造成系統不正常的結果產生。 ### III. 案例探討:CVE-2017-16943 Abusing UAF leads to Exim RCE[^6] Exim 為泛 UNIX 系統上常見的收發信程式 (Message Transfer Agent, MTA),且為 Debian GNU/Linux 系統的預設 MTA 程式。於 2017 年由台灣的 DEVCORE 團隊披露兩項 CVE 漏洞 (CVE-2017-16943 UAF Abuse leads to RCE、CVE-2017-16944 DoS vulnerability),並入圍下年度的 Pwnie Award 。 細究漏洞利用手法,原因出在 SMTP protocol 的擴充指令 BDAT,用來傳遞大型 Binary 資料,其中 SMTP 於 RFC 5321[^7] 的 4.1.14 記載,信件內容以一組 `<CRLF>.<CRLF>`代表內容的結束並跳脫 (escape)。 > **RFC 5321 Simple Mail Transfer Protocol** > § 4.1.1.4. DATA (DATA) > > The mail data are terminated by a line containing only a period, >that is, the character sequence "≈", where the > first `<CRLF>` is actually the terminator of the previous line. 而若以 BDAT 伴隨 SIZE 及 LAST 的指令例如: `BDAT 1024` 或`BDAT 1024 LAST` 的指令,信件伺服器便不會檢查跳脫的 `<CRLF>.<CRLF>` ,並利用 UAF 作為放置利用程式碼的注入點,達到 RCE 的目的,而 UAF 的發生位於 receive_msg.c 中,因此這裡以討論 receive_msg.c 為主。 首先討論 Exim 的記憶體配置方式︰在 Exim 中,以一系列 store_ 為首的函數進行動態記憶體分配,包括︰`store_get`, `store_release`, `store_reset`。一開始,Exim 會規劃出一個巨大的 storeblock,預設為 0x2000 bytes,並在呼叫 store_get 時切割後放進 stores 這個結構,並以一個全域指標紀錄尚未使用的剩餘空間大小以及可以切割新區段的起始位址。當當前使用的 block 不需要時,就利用 store_release 將指標指向新的 block 之後,成為一個存放 storeblock 的 chain,而這個 chain 被稱為 store_pool,在 Exim 中總共有三個。記憶體分配流程簡圖如圖一。 ![](https://i.imgur.com/MlSF3k9.png) > 圖一、正常情況下記憶體分配狀態,指標指向的位置都是尚未被釋放的空間 再來看到有問題的 `receive_msg.c`: ```clike if (ptr >= header_size - 4) { int oldsize = header_size; /* header_size += 256; */ header_size *= 2; if (!store_extend(next->text, oldsize, header_size)) { uschar *newtext = store_get(header_size); memcpy(newtext, next->text, ptr); store_release(next->text); next->text = newtext; } } ``` 外表看起來和平常的 malloc, realloc, free 很像,但仔細看到 `store_extend` 這個函數,其作用在於檢查舊且不需要的 storeblock 是否在 current block 中最新的位置上。 若這部判斷錯誤回傳 FALSE,就會促使 Exim 以 store_get 新增一個新的區塊並以 store_release 釋放舊區塊。但是 store_get 是將一個分配得的區段切割、store_release 則是將 chain 第一個區塊,也就是整個 current block 釋放。而要是store_extend 回傳 FALSE 導致指標指向 current block 卻是使用 store_get,會使得 current block 內部被切割一段區塊作為 newtext 使用並釋放以 store_release 釋放在之後的 next->text,導致 current block 以及 newtext 指向的是一個已經 freed 的記憶體,形成可以利用的 UAF, memory layout 示意圖如圖二。 ![](https://i.imgur.com/2KOZ7Ag.png) > 圖二、經過 store_get 切割並 store_release 釋放後,current block 及 next->text 指向已經被釋放的記憶體空間 ## 實驗結果與驗證 ### (ㄧ) Integer Promotion 驗證 參考以下程式碼︰ ```c #include <stdint.h> #include <stdio.h> unsigned int ui = 0; unsigned short us = 0; signed int si = -1; int main() { int64_t r1 = ui + si; int64_t r2 = us + si; printf("%lld %lld\n", r1, r2); } ``` 接者以除錯器觀察運作狀態,畫面如下方圖: ![](https://i.imgur.com/qTrWjbz.png) 斷點是停在進行 int64_t r1 = ui + si ,對照 Linux x86_64 system call register table ,可以發現 RDI 的值是 0x01,這個 flag 代表目前在做 unsigned int 的計算,與前文提到的 promotion 現象相符。 ### (二) 物件生命週期於不同環境與編譯器結果比較 以下方程式為例︰ ```c #include <inttypes.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) { char *p, *q; uintptr_t pv, qv; { char a = 3; p = &a; pv = (uintptr_t) p; } { char b = 4; q = &b; qv = (uintptr_t) q; } if (p != q) { printf("%p is different from %p\n", (void *) p, (void *) q); printf("%" PRIxPTR " is not the same as %" PRIxPTR "\n", pv, qv); } else { printf("Surprise!\n"); } return 0; } ``` #### (1) 在 macOS 的狀況 macOS 在 Lion 之後的版本使用 gdb-8 或直接使用內建的 lldb 會有憑證限制的問題,加上此外 gdb 在 macOS 上有 bug 會導致 crash,可以詳閱相關的 Bugzilla[^8][^9] 。這裡使用 ADG 來驗證。 ![](https://i.imgur.com/gm4Y4RK.png) > 圖三、於 ADG 中顯示的訊息,運行到 breakpoint 19 如圖三,將 breakpoint 設在第11行、第16行以及第19行觀察結果,當運行到第11行時,可以看到 char* p 內存放的是 0x00007ffeefbffbbf “\x03" ,因為我們將 char a 的位址指派給 p,後方的 `\x03` 代表的是 3 的 ASCII code,因為 p 的型別是 `char *`,除錯器預期的是 string,但因為我們存放的 p 並不是 string 而也因此沒有 NULL terminate 所以會一直印到 `\x00` 結束,結果如下︰ ```shell Process 3848 launched: '/Users/raagi/dangling' (x86_64) Process 3848 stopped * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1 frame #0: 0x0000000100000ed6 dangling`main(argc=1, argv=0x00007ffeefbffc10) + 38 at dangling.c:11 (lldb)print p (char *) $2 = 0x00007ffeefbffbbf “\x03" (lldb) reg read sp rsp = 0x00007ffeefbffbb0 (lldb) reg read fp rbp = 0x00007ffeefbffbf0 ``` 繼續運行程式碼,並停在 breakpoint 16 : ```shell (lldb) c Process 3848 resuming Process 3848 stopped * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1 frame #0: 0x0000000100000ee6 dangling`main(argc=1, argv=0x00007ffeefbffc10) + 54 at dangling.c:16 (lldb)print b (char) $5 = '\x04' (lldb) print q (char *) $6 = 0x00007ffeefbffbbe “\x04\x03" (lldb)reg read sp rsp = 0x00007ffeefbffbb0 (lldb) reg read fp rbp = 0x00007ffeefbffbf0 ``` 此時 p 已經離開了有效的 scope,進入 q 有效的 scope,但可以看到很奇怪的現象是 q 存的結果最後面多了本應該是代表 char a 的 `\x03`。這是因為即便離開了有效 scope 原先內容並不會被清空,而記憶體配置一切由各編譯器規範,也就是 implementation-defined。 檢閱其記憶體配置的方式,可以發現 p 和 q 的確在這個範圍內︰ ``` Stack 00007ffeef400000-00007ffeefc00000 [ 8192K 4K 4K 12K] rw-/rwx SM=PRV thread 0 ``` 而往後繼續看 pv 和 qv 兩個值,他們的型別是 uintptr_t,這個型別並不是指標型別,而是 unsigned int 的一種,只是它能存放的數值大小足以存放指標的值,以 uintptr_t 作為指標 cast 的方式是比較安全的,因為在 C11 6.2.5 (28) 及 6.3.2.3 (1)[^2] 內容得知若直接對 `void *` 指標進行處理與運算導致 Undefined behavior,所以得到 `pv = 140732920757182`, `qv = 140732920757182` 只是 p, q 兩者的十進位表示值。而在 macOS 以 clang 編譯的記憶體分配上,離開有效 scope 不會開啟新的 stack frame #### (2) 在 Ubuntu 上的情況以 gdb-peda 除錯,breakpoint 一樣設在 10, 16, 19 觀察運作狀況 ![](https://i.imgur.com/Mx2bE2S.jpg) 可以看到 p 的位置是 0x7fffffffdf97,而 stack pointer 的內容如下: ![](https://i.imgur.com/tRQflU0.png) 繼續往下執行到 breakpoint 16: ![](https://i.imgur.com/UAly4rG.jpg) 可以看到 q 裡面存的也是 `0x7fffffffdf97`,而比較兩階段時的 x86 assembly,發現無論是在做 char a 還是 char b 的宣告,都是使用到 rbp-0x29 的位置,導致 p 、 q 兩者內容一樣。最後程式運行的結果是 `Suprise!`,與 macOS 運行結果不同。 #### (3) 原因探究 兩者結果不同的原因在於編譯器實作定義不同,在 LLVM 規範底下有 stack-use-after-scope detector[^10][^11],當以一組 `{}` 包含起來的部分為一個 scope,當編譯時讀取到第一個 `{` 會呼叫 llvm.lifetime.start(),代表於此之後的指標所指位址有效,之前者無效;遇到結尾 `}` 時呼叫 llvm.liftime.end(),代表在這之前的指標所指記憶體位址無效。但是在 gcc 裡必須在編譯時加入 `-fsanitize-address-use-after-scope` 的參數才能避免 use-after-scope 的問題。[^12] ## 結語和討論 - [ ] Integer Promotion 影響範圍以及避免 Integer Overflow 方法討論 在物聯網裝置上,許多感測器可能因為 Integer Promotion 衍生的問題,致使 Integer Overflow,例如溫度感測器的資料更新。解決辦法之一是檢查輸入值是否高於或低於預期資料的大小,如果超過邊界就恢復成原來的狀態,例如 Michael Denzel 的 self-healing FreeRTOS 裡的實作[^12] PoC 就有利用 Integer Overflow 的假設,並對應此問題進行自我修復,在 self-healing.c 中對預期最大值檢查,如果超過就刪除有害的任務 (malicious task),並初始為前一次 log 所記錄的狀態。 - [ ] Use After Free 影響與改善方式 除了近期的 CVE-2017-16943 之外,在 Linux 上也有 UAF 利用漏洞問題 (CVE-2016-0728)[^13]、以及 Pwn2Own 大賽上 Microsoft Edge 瀏覽器也曾因一樣的漏洞被參賽者攻破數次[^14],使其安全性受到質疑,而這也是因為 UAF 只要是以 C/C++ 或直接以組合語言所撰寫的程式,只要使用時不謹慎就可能發生,而且任何作業系統平台都會受到影響。 改善方式除了程式設計者要注意記得清空釋放記憶體外,設計機制能夠偵測 dangling pointer 也是目前常見且致力發展的方式,以 "Efficiently Detecting All Dangling Pointer Uses in Production Servers"[^15] 這篇論文為例,其中提到最基本的偵測 dangling pointer 可以藉由參考動態分配的物件的 pages,利用現代 MMU 在 run-time 時能夠檢查每個記憶體存取動作的特性,在 run-time 的時候,當物件被釋放時,會呼叫一個 mprotect 的 system call,為這個 page 加上 protection bit,而後若有新物件要存取這個 page 時,就會被存取禁止。 ## 參考文獻 [^1]: ["OpenSSL Heartbleed", Synopsys](http://heartbleed.com) [^2]: ["C11 Standard", ISO/IEC, Chap. 6 Language]() [^3]: ["Basic Integer Overflows", blexim](http://www.phrack.org/issues.html?issue=60&id=10#article) [^4]: [“Using freed memory”, OWASP](https://www.owasp.org/index.php/Using_freed_memory) [^5]: [“CWE-416: Use After Free”, MITRE](https://cwe.mitre.org/data/definitions/416.html) [^6]: [“Road to Exim RCE - Abusing Unsafe Memory Allocator in the Most Popular MTA”,DEVCORE](https://devco.re/blog/2017/12/11/Exim-RCE-advisory-CVE-2017-16943-en/) [^7]: [“RFC 5321 Simple Mail Transfer Protocol”, ISO/OSI, Chap. 4]() [^8]: [“Fixing gdb "signal SIG113" issue following macOS Sierra upgrade (Does not work with 10.12.2)”, T. Salami](https://www.youtube.com/watch?v=ro37tqR9bA4) [^9]: ["ggdb broken in Sierra", Apple Community,](https://discussions.apple.com/thread/7684629) [^10]: [“Stack-use-after-scope detector in AddressSanitizer”, Vitaly Buka, Google]() [^11]: [“Memory Use Markers”, LLVM Language Manual,](https://llvm.org/docs/LangRef.html#memory-use-markers) [^12]: [“Self-healing FreeRTOS”, Michael Denzel,](https://github.com/mdenzel/self-healing_FreeRTOS) [^13]: [“Linux join_session_keyring UAF”, MITRE,](https://cve.mitre.org/cgi-bin/cvename.cgi?name=cve-2016-0728) [^14]: [“The Result - Pwn2Own 2017 Day 2”, Zero Day Initiative,](https://www.thezdi.com/blog/2017/3/16/the-results-pwn2own-2017-day-two) [^15]: [“Efficiently Detecting All Dangling Pointer Uses in Production Servers”, Dinakar Dhurjati, Vikram Adve, University of Illinois at Urbana-Champaign, page 4 - 5]()