# 攻擊與防禦 HW2 ###### tags: `攻擊與防禦` ## Task 1 - Heartbleed 此次作業預期找出 CVE-2014-0160 ,為了避免站在上帝視角,這份報告站在以下假設: - heartbeat plugin 被懷疑有 bug 。 - 我將要使用 afl 來尋找可能存在於 heartbeat plugin 的 bug 。 ### Steps 以下假定 openssl 專案放在 /src 下。 1. 編譯 openssl 。 ```bash cd /src CC=afl-clang-fast CXX=afl-clang-fast++ ./config –d AFL_USE_ASAN=1 make ``` 2. 撰寫 harness 。將 harness.cc 放在 /src 下 。 3. 編譯 harness 。 ```bash AFL_USE_ASAN=1 afl-clang-fast++ -g /src/harness.cc \ /src/libssl.a /src/libcrypto.a \ -o /src/harness -I openssl-1.0.1f/include -ldl ``` 4. 準備好 seed ,假設放在 /src/fuzz/in 。 5. 將 server.key 和 server.pem 放在 /src 下。 6. fuzzing 。 ```bash cd /src afl-fuzz -i fuzz/in -o fuzz/out ./harness ``` ### Harness Harness 的功能是將使用者給的 payload 直接拿去握手。因此使用者給的 input 將預期是 binary data 。 使用者需先準備 binary payload 檔案,然後以下列方式執行 harness 。 ```bash cd /src ./harness < /path/to/payload.txt ``` ### Seed 抱持著 seed 應盡量極簡,用來握手的 payload 長度應為 0 。此情況下其 input 為 $233+0=233$ byte 。 ``` 00000000: 1603 0200 dc01 0000 d803 0253 435b 909d ...........SC[.. 00000010: 9b72 0bbc 0cbc 2b92 a848 97cf bd39 04cc .r....+..H...9.. 00000020: 160a 8503 909f 7704 33d4 de00 0066 c014 ......w.3....f.. 00000030: c00a c022 c021 0039 0038 0088 0087 c00f ...".!.9.8...... 00000040: c005 0035 0084 c012 c008 c01c c01b 0016 ...5............ 00000050: 0013 c00d c003 000a c013 c009 c01f c01e ................ 00000060: 0033 0032 009a 0099 0045 0044 c00e c004 .3.2.....E.D.... 00000070: 002f 0096 0041 c011 c007 c00c c002 0005 ./...A.......... 00000080: 0004 0015 0012 0009 0014 0011 0008 0006 ................ 00000090: 0003 00ff 0100 0049 000b 0004 0300 0102 .......I........ 000000a0: 000a 0034 0032 000e 000d 0019 000b 000c ...4.2.......... 000000b0: 0018 0009 000a 0016 0017 0008 0006 0007 ................ 000000c0: 0014 0015 0004 0005 0012 0013 0001 0002 ................ 000000d0: 0003 000f 0010 0011 0023 0000 000f 0001 .........#...... 000000e0: 0118 0302 0003 0100 00 ......... ``` ### CVE-2014-0160 跑 afl-fuzz 大約三鐘內可以發現 crash 。將造成 crash 的 input 餵給 harness 得到 afl 的錯誤訊息。 ``` ==22148==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x629000009748 at pc 0x000000497b17 bp 0x7ffd7c8eccb0 sp 0x7ffd7c8ec478 READ of size 25088 at 0x629000009748 thread T0 #0 0x497b16 in __asan_memcpy (/src/harness+0x497b16) #1 0x4e7517 in tls1_process_heartbeat /src/ssl/t1_lib.c:2586:3 #2 0x5e0d24 in ssl3_read_bytes /src/ssl/s3_pkt.c:1092:4 #3 0x5ea259 in ssl3_get_message /src/ssl/s3_both.c:538:5 #4 0x57db8a in ssl3_get_client_hello /src/ssl/s3_srvr.c:941:4 #5 0x57478f in ssl3_accept /src/ssl/s3_srvr.c:357:9 #6 0x4cc67d in main /src/harness.cc:39:3 #7 0x7fd97ed850b2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x270b2) #8 0x41d50d in _start (/src/harness+0x41d50d) ``` t1_lib.c:2586 在函數 tls1_process_heartbeat ,程式碼如下。 ```c=2583 /* Enter response type, length and copy payload */ *bp++ = TLS1_HB_RESPONSE; s2n(payload, bp); memcpy(bp, pl, payload); bp += payload; /* Random padding */ RAND_pseudo_bytes(bp, padding); ``` 經比對此即 heartbleed 。 ### Discussion #### Harness 撰寫方式 一開始跑 afl-fuzz ,它抱怨說執行速度很慢,約 60/sec 。我將 harness 做了改良,效率提升約 30 倍。以下為改良前與後的 harness 程式碼。 ```c int main() { static SSL_CTX *sslctx = Init(); SSL *ssl = SSL_new(sslctx); BIO *rbio = BIO_new(BIO_s_mem()); BIO *wbio = BIO_new(BIO_s_mem()); SSL_set_bio(ssl, rbio, wbio); SSL_set_accept_state(ssl); char data[4096]; int size = read(0, data, sizeof(data)); BIO_write(rbio, data, size); SSL_do_handshake(ssl); SSL_free(ssl); return 0; } ``` ![](https://i.imgur.com/JKzXxyl.png =540x) ```c int main() { static SSL_CTX *sslctx = Init(); SSL *ssl = SSL_new(sslctx); BIO *rbio = BIO_new(BIO_s_mem()); BIO *wbio = BIO_new(BIO_s_mem()); SSL_set_bio(ssl, rbio, wbio); SSL_set_accept_state(ssl); while (__AFL_LOOP(1000)) { char data[4096]; int size = read(0, data, sizeof(data)); BIO_write(rbio, data, size); SSL_do_handshake(ssl); } SSL_free(ssl); return 0; } ``` ![](https://i.imgur.com/bMzVWCB.png =540x) ## Task 2 - ntpq 此次作業預期找出 CVE-2009-0159 。為了避免站在上帝視角,這份報告站在以下假設: - 函數 cookedprint 被懷疑有 bug 存在。 - 我將要使用 afl 來尋找可能存在於 cookedprint 的 bug 。 - cookedprint 的 parameter `datalen` 必為 `data` 之長度,不會出錯。 - cookedprint 的 parameter `data` 可為任意長度的任意格式資料,其中包含 binary data 的可能性。 ### Steps 以下假定 ntp 專案放在 /src 下。 1. 撰寫 harness 。我直接修改 ntpq.c 作為 harness 。 2. 編譯 ntpq 。 ```bash cd /src CC=afl-clang-fast ./configure AFL_USE_ASAN=1 make -C ntpq ``` 3. 撰寫 seed 和 dicionary ,假設分別放在 /src/fuzz/seed 和 /src/fuzz/dict.txt 。 4. 跑 afl-fuzz 。 ```bash afl-fuzz -i /src/fuzz/seed -o /src/fuzz/out -x /src/fuzz/dict.txt /src/ntpq/ntpq ``` ### Harness 我直接修改 ntpq.c 作為 harness 。以下為我所使用的 main 函數。 ```c=484 #ifndef NO_MAIN_ALLOWED int main(int argc, char *argv[]) { const int status = 1; char buf[1024 * 8]; while (__AFL_LOOP(1000)) { int readed = read(0, buf, sizeof(buf)); for (int datatype = 1; datatype <= 3; datatype++) cookedprint(datatype, readed, buf, status, stdout); } return 0; } #endif ``` 我將 `datatype` 從 1 到 3 各嘗試一遍,然後令 `status` 為常數 1 。這是因為 `status` 僅在 ```c=3040 (void) fprintf(fp, "status=%04x %s,\n", status, statustoa(datatype, status)); ``` 被使用,而這份作業假定 bugs 存在於 cookedprint 及其密切相關的函數,因此 statustoa 不在目標中,故不重視 `status` 之值。 使用方法如下: ```bash /src/ntpq/ntpq < /path/to/seed.txt ``` ### Seed 由於 bugs 可能發生在長字串或極端整數,因此字串與整數都需要被檢驗。我準備兩個 seed ,一個為字串型態,一個為整數型態。 ``` name=wdzeng ``` ``` id=716023 ``` 我嘗試過其他 seed 的實驗(例如將上述兩個測資合而為一 `name=wdzeng,id=716023`) 但對於尋找 path 的速率並沒有顯著的差異。我推測這是因為尋找 path 主要還是要靠命中 cookedprint 函數的 switch-case 。 ### Dictionary 為了拓展 path ,需要想辦法讓 swtich-case 命中。因此可以準備對於命中有利的字串做成字典。事實上 `reach` 就是能夠觸發 CVE 的關鍵字。我使用了助教給的 dictionary 。 以下兩張截圖可以明顯看出使用字典與否,對於尋找 path 的速度有顯著的差距。上圖為不使用字典,下圖為使用。 ![](https://i.imgur.com/oNMqA30.png =560x) ![](https://i.imgur.com/x4an3G5.png =560x) ### Found Bugs 以下僅詳述一些 cookedprint 函數較密切相關的 bugs ,其餘略述。除非特別提及,否則所有程式碼片段之行號皆為原始碼之行號。 #### Bug-1) Empty Key ```c=2704 while (isspace((int)(*(np-1)))) np--; ``` `np` 為 `name` 的 iterator,第 2704 行可能造成 `np` 讀取越界。應該為 ```c=2704 while (np > name && isspace((int)(*(np-1)))) np--; ``` 此錯誤發生在 key 為空字串時。以下給出一個造成此錯誤的測資。 ``` key=value,=emptykeyleadstoerror ``` #### Bug-2) Empty Value 函數 nextvar 在 parse 含有 value 為空字串之 data 時,會令 value 為 NULL 。這會造成後續分析 value 值時發生越界。例如以下函數 decodetime 因為試圖存取指向 NULL 的 `str` 而造成錯誤。此函數為 cookedprint 所呼叫。 ```c=1705 int decodetime(char *str, l_fp *lfp) { return mstolfp(str, lfp); } ``` 以下將 value 賦予空字串之值,可以避免此問題。 ```c=2717 value[0] = '\0'; *vvalue = value; ``` 此錯誤會在 value 為空字串時觸發。以下給出一個造成此錯誤的測資。 ``` emptyvalueleadstoerror=,key=value ``` #### Bug-3) Value Too Long 此錯誤發生在非常多函數中,本質為 stack overflow。以下以 `decodearr` 作為說明。 ```c=1970 char buf[60]; ``` ```c=1983 while (!isspace((int)*cp) && *cp != '\0') *bp++ = *cp++; ``` `bp` 為指向 `buf` 的 iterator ,第 1984 行因沒有檢查而造成 stack overflow 。 此錯誤發生在 value 長度大於 buffer 長度時發生。可以修改 `MAXVALLEN` 為較低的數字來防止。 #### Bug-4) Binary Data cookedprint 在遇到 binary data 時極容易 crash 。測試各種造成 crash 的 input 並檢視原始碼後,發現 cookedprint 似乎假定了 data 為純文字,以至於後續將字串轉為整數、時間或其他資料型態時因未檢查而發生錯誤。 由於修復此 bug 需要修改許多函數的 code ,且修復此 bug 並非作業目標(作業目標為發現 bug 而非修復 bug),故可以在 main 函數中先檢查 data 是否為純文字,以此法繞過。 #### 其他 Bugs - decodeint 第 1933、1934 行 `&val` 應改為 `val` ,此錯誤造成存取記憶體越界。這可能是開發者計概沒學好。 ```c=1931 if (*str == '0') { if (*(str+1) == 'x' || *(str+1) == 'X') return hextoint(str+2, (u_long *)&val); return octtoint(str, (u_long *)&val); } ``` - deocdenetnum.c 第 28 行記憶體越界。應檢查是否配對到右中括號。此程式碼片段為 cookedprint 所用。 ```c=26 for (i = 0; *cp != ']'; cp++, i++) name[i] = *cp name[i] = '\0'; ``` - 常數陣列 `tstflagnames` 實際上為 13 ,第 2987 行誤植為 14 ,可能是開發者粗心數錯。 ```c=2987 for (i = 0; i < 14; i++) { if (val & 0x1) { sprintf(cb, "%s%s", sep, tstflagnames[i]); ``` #### CVE-2009-0159 afl++ 大約跑了 3M 個測資後,找到了 CVE-2009-0159 。這是因為 afl++ 產生了以下測資。 ``` reach=3333333333 ``` 將此測資餵給 ntpq ,產生以下訊息。 ``` ==11517==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffd160f900a at pc 0x00000043a994 bp 0x7ffd160f8b40 sp 0x7ffd160f82d8 WRITE of size 12 at 0x7ffd160f900a thread T0 #0 0x43a993 in vsprintf (/src/ntpq/ntpq+0x43a993) #1 0x43b913 in sprintf (/src/ntpq/ntpq+0x43b913) #2 0x4d3a6b in cookedprint /src/ntpq/ntpq.c:2599:31 #3 0x4cf61c in main /src/ntpq/ntpq.c:475:38 #4 0x7f299ddc50b2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x270b2) #5 0x41c46d in _start (/src/ntpq/ntpq+0x41c46d) ``` 第 2599 行看到 `(void)sprintf(b, "%03lo", uval);` ,此即 CVE-2009-0159 。 原因是因為,將一 unsigned integer 轉為八進位整數,其長度最長為 $\left\lceil\log_82^{32}\right\rceil=11$ ,因此 buffer `b` 至少需要 12 byte 。在本例中, $333333333_{10}=30653520525_{8}$ 需要 12 byte 。 以下為 CVE-2009-0159 造成錯誤的部分。將 `b` 之長度改為 12 即可。(按:此處 line number 並非原始檔案的 line number ,而是被我改過的 ntpq.c 的行號) ```c=2597 char b[10]; (void)sprintf(b, "%03lo", uval); ``` ### Discussion #### 尋找目標 CVE 問題 一開始拿 ntpq 原始碼跑 fuzzing 時,很快就會發現許多 crash ,但這些 crash 中並未找到目標 CVE 。我一開始希望可以先等找到目標 CVE 後再一起檢視這些 bugs ,但發現要等待非常非常久才會找到。 後來我著手先把比較快找到的 bug 做修補,修補完之後,用同樣的 Seed 和 Dictionary 便能很快找到目標 CVE (約省下 95% 時間)。我認為這是因為 afl 會針對能造成程式 crash 的測資做進一步鑽研,以至於新測資的方向偏離目標 CVE 所致。 #### Seed 格式問題 一開始我讓 harness 能夠讀取使用者給定的 datatype 和 status 。 Seed 的格式是第一行與第二行為整數,分別代表該兩變數;餘後所有資料視為 data 。但我發現找到一個 crash 的週期非常高。 後來我將 datatype 從 1 遍歷到 3 ,然後令 status 為常數。這樣做讓找到 crash 的週期降低不少,此舉有助於找到目標 CVE 。