Pwn 亂刷合集

浅叶梦缘發表於2024-11-25

SHCTF 2024

3※ No stack overflow2

  • 考點:ret2libclibc 庫查詢整數溢位

  • 題目附件:No stack overflow2

  • 在 linux 下使用 checksec 檢視該程式開啟的保護,發現 Archamd64-64-little,這說明這是一個 64位 的程式,並且採用了 小端 儲存,即低位對應低地址,高位對應高地址。

  • 下方的 RELRO ,這是一種透過設定 重定位相關表 的許可權為 只讀 來防止其被修改的安全機制,我們只關注其對 got 表的影響。Partial RELRO指的是部分開啟,此時 got 表被設定為:每個表項只有在未解析過該函式地址前是可寫,載入地址後改為只讀:

  • 不過我們暫時不關心,把 vuln 檔案拖進 IDA64 開啟,點選左側 main,按 F5 反編譯,發現程式主要功能是先讀入一個長度,接著檢測長度小於等於 256 時再讀入最多這麼長的位元組:

  • 我們看 IDA64 提示的儲存地址,發現 nbytes_4 長度也是 0x100 即 256 個位元組,難道無法溢位嗎:

  • 我們發現關鍵在於傳入引數時,將長度轉為了 unsigned int 無符號整數,這就使得首位的符號位被當做了資料,那麼如果原先輸入長度為 -1,二進位制對應的 0xFFFFFFFF,那麼轉為無符號整數後就變為了 2147483647,就可以繞過上方的長度檢測了:

  • 那麼接下來就是要構造我們的 payload1 了,但是一個個檢視左側函式發現沒有 system 相關的,按 Shift+F12 也沒有發現 /bin/sh 字串。

  • 但是發現左側有 puts 函式,那麼我們可以考慮使用和上一題相同的方法:將 puts函式的 got 表地址洩露出來,然後查詢其在動態庫中的位置,二者相減得到偏移量。 接著就可以先查詢動態庫中的 system 函式和字串 /bin/sh的位置,來計算出它們在程式中的位置,然後便可以構造 payload 模擬執行 system("/bin/sh")了。

  • 那麼我們先開啟 ELF 檔案,查詢puts函式 plt 表,got表所在的地址,以及 puts 結束後返回的 main 函式的地址:

  • 這裡的 process('./vuln') 指的是先不連線伺服器,而是用本地檔案來模擬,方便我們除錯:

  • 然後在第一處輸入 -1 繞過長度檢測:

  • 但是我們忽然發現一個問題,本題與上一題不同的是,上一題是 32位 程式,所有引數均透過棧來傳遞,我們只需要將函式的傳參壓入棧中即可模擬函式執行。但是這題是 64位 程式,函式的前 6個 引數是依次透過 6個 暫存器來傳遞:rdirsirdxrcxr8r9,之後的更多的引數才用棧來傳遞。這樣使得程式執行的速度提升了不少,但是對於我們棧溢位攻擊就不能直接透過棧來傳參了,需要修改暫存器的值。


  • 我們可以想到,pop rdi 指令可以將棧內寫入的內容傳給 rdi這樣就可以修改它了,那麼如果我們將 pop rdi指令所在的地址,放在函式返回的 ret處,就可以修改 rip下一條指令執行 pop rdi 了!

  • 可是僅僅是這樣,rip 跳轉之後就回不來了,程式流程整個被打亂,所以我們需要找一個後面緊跟著 ret 指令的 pop rdi,這樣下一句還會執行 ret,將此時 rsp 自加過後的棧頂的內容給 rip,不就仍然等同於繼續進行棧溢位攻擊了嗎。

  • 同理,如果想要修改 rsirdx,也需要在程式中找到 pop rsi 隨後 ret 的程式碼片段的地址,以此來傳遞引數模擬函式執行。這個過程就是所謂的構造 ROP 鏈。


  • 我們可以透過使用 ropper 工具或是 ROPgadget 工具,在 linux 下快速查詢一個檔案中出現指定字串的位置,我們透過使用管道符來查詢所有pop 開頭到 ret 結尾的字串,再要求其中含有 rdi,寫出以下命令查詢:ropper --file vuln --search "pop|ret" | grep "rdi",發現成功找到一個:

  • 記錄下這段程式所在的地址 pop_rdi_ret = 0x401223,接下來就可以使用了。由於 puts 函式只需要一個引數,即輸出的字串的地址,我們只需要 rdi 來傳參,所以現在可以開始構造 payload1 了:

  • 先填充 0x100即 256個位元組 給 nbytes_4,然後因為這是 64位 程式,要填充 8個位元組 給 srbp,接下來在 r 處填入我們的 pop rdi;ret 程式的地址:p64(pop_rdi_ret),然後填入要修改的 rid 資料,即 puts 的傳參,也就是要輸出的字串的地址:puts_got

  • 然後填充 pop rdi;ret 返回回來後要執行的程式的地址,我們要輸出 puts 函式的 got 表裡的內容,所以這裡填呼叫的輸出函式 puts 的地址:puts_plt

  • 最後填充 puts函式輸出完返回回來後要執行的下一個程式的地址,由於我們需要再次利用這裡的棧溢位來執行 system("/bin/sh"),所以填 main 函式的首地址。

    image

  • 可以看到此時已經將地址輸出出來了,不過都是 \x開頭的 16 進位制 bytes 資料。由於 64位 程式的地址都是以 \x7f 開頭的,並且由於這是個 小端程式,字串地位置儲存在低位置,所以輸出出來是倒序的,所以我們可以用 .recvuntil(b'\x7f') 讀到 \x7f 為止。

  • 又由於雖然我們是 64位 程式雖然應該有 8 位,但使用時的編碼都是以 \x7f 開頭的 6位 編碼地址,所以我們只要讀進來的最後 6個位元組:

    image

  • 然後我們要對這個 16進位制 的 bytes 資料用 u64() 進行解包,但是如果直接使用的話程式會報錯:

    image

  • 這是因為 u64() 每次解包需要輸入 \(8個位元組\),而剛才的地址不足 8位,我們需要在左側用.ljust(8,b'\x00')\x00 將其補滿 8位:

    image

  • 此時再輸出發現就是正確的一個整數地址了:

    image

  • 那麼拿到了一個 puts 函式的 got 表的所在地址,接下來我們就可以透過查詢動態庫中 puts 函式的地址然後計算出偏移量啦。

  • 不過這道題題目並沒有把動態庫檔案直接給我們,我們需要根據洩露出來的 puts 函式的 got 表的所在地址來查詢到系統所使用的動態庫版本。


  • 我們可以下載使用 LibcSearcher 這個 python 庫來開啟對應的動態庫,只需要提供某個已知函式的具體地址即可:

    image

  • 接下來就和上一題一樣了,查詢 puts 函式在動態庫的地址,計算出偏移量,然後查詢 system 函式和 /bin/sh 字串在動態庫的地址,計算出在程式內的地址。不過要注意的是此時使用 LibcSearcher 指令,需要用 .dump("xxx") 來查詢某個函式的地址,用 .dump("str_bin_sh") 來查詢字串 /bin/sh 的位置:

    image

  • 此時執行時我們會發現,查詢到多個匹配的動態庫,程式詢問我們要使用哪一個版本的,這是因為先前我們用的是 process('./vuln') 在本地除錯。而如果連線到伺服器上的時候就不需要我們進行選擇了。

  • 不過現在我們需要根據自己的 ubuntu 版本來選擇對應的動態庫,我們可以先按 Ctrl+C 退出程式,在 linux 中輸入 ldd --version 來查詢版本:

    image

  • 可以看到第二行,我的是 2.39-0ubuntu8.3,那麼再次執行程式,這次就選擇這個版本的動態庫,填入程式提示的版本前方的編號 0 按回車,可以看到下方有一句該版本 be choosed就成功選擇了:

    image

  • 那麼完事具備,我們現在已經執行到第二次 main 函式要求我們輸入長度的位置了,再次輸入 -1,然後開始構造我們第二次的 payload2:

  • 先填充前面 0x108 個字元到 r 處與 payload1 一樣,然後透過 pop di;ret 來傳遞 system 的引數:bin_sh_addr

  • 然後填充要執行的 system 函式的地址: system_addr

  • 最後的返回地址在哪裡都無所謂,因為馬上要得到系統許可權進入互動模式了,並不會返回回來用上,直接不填。

    image

  • 但是!此時執行會發現並沒有得到系統許可權,反而報錯退出了。這是因為這是採用了新的高版本的 gcc 編譯器的 64位 系統,其在呼叫動態庫中的 system 函式時,對 rsp 有額外的要求:

  • 在準備進入 system 函式時,會對此時的 rsp 也就是棧頂進行一次檢驗,要求此時指向的地址必須能被 16 整除,也就是必須以 0 結尾,否則報錯退出不予呼叫。

  • 我們進入 IDA64 的 main 函式,點選 nbytes_4 檢視棧空間,發現我們填充到 r 的位置以 8 結尾:

    image

  • 所以此時放在 r 中的 pop rdi;ret 的地址以 8 結尾,接下來 /bin/sh 字串的地址以 0 結尾,而 system函式的地址以 8 結尾,就無法透過高版本的 rsp 檢驗。

  • 那麼我們需要再呼叫 system 函式之前額外填充一個 某段程式的地址,這樣在執行 system 函式時 rsp就以 0 結尾了。

  • 最簡單的就是找一個只有一句 ret 指令的地址,rip 執行原先函式的ret 跳轉到這裡後,下一句還是將執行 ret,沒有區別,但是此時 rsp 已經自加了一次。

  • 所以我們用 ropper 指令尋找一個只有一句 ret 的程式,在 linux 下輸入 ropper --file vuln --search "ret" 查詢:

    image

  • 記錄下程式的位置 ret = 0x40101a,接下來只需要在呼叫 system 之前多填充一個 ret 的地址即可:

    image

  • 此時執行完程式,在選擇動態庫版本輸入 0 後,我們發現已經進入了互動模式,輸入 ls 可以看到當前目錄下的檔案,大功告成:

    image

  • 最後調整為遠端連線伺服器,ls 一下發現有 flagcat flag 獲取 flag:

    image

  • 最後放上完整 exp(調整了一下順序):

    image

  • 除了使用 LibcSearcher 線上查詢動態庫之外,我們還可以使用一個線上網站將伺服器所使用的動態庫下載下來:https://libc.rip/,使用的時候只需要輸入,洩露的函式的名稱,和洩露出來的函式的地址的後三位(16進位制)即可:

    image

  • 當然,如果用 puts 函式查不到對應的版本的話,可以試著用別的函式查詢,網站的內容有時候明沒有更新到最新(這裡就是,我換成了 read函式 ):

    image

  • 然後可以下載下來,本地進行除錯(當然我們不知道伺服器用的是哪一個,這隻限於本地除錯程式碼用的下載)。



3※ No stack overflow2 pro

  • 考點:libc 靜態連結

  • 題目附件:No stack overflow2 pro

  • 這題題目首先提示了,使用了靜態連結,也就是將動態連結直接寫入了程式中,這樣就沒有 plt 表和 got 表供我們使用了。

  • 首先在 linux 下用 checksec vuln 檢視檔案保護情況:

    image

  • 發現是 64位 小端程式,開了 Partial RELRO,開了 NX 保護,這個就是不允許執行存放在資料段的程式碼,也就是為什麼我們之前,都要費盡心思往棧裡面寫別的程式的地址的原因:程式碼直接放在棧裡面不允許執行。

  • 接著是 Stack:Canary found ,這是指開啟了 Canary保護:在進入函式前生成一個校驗碼壓入棧中,在函式返回時檢測校驗碼是否被修改,若被修改則判斷棧發生了改變收到了溢位攻擊,自動結束程式。這是對棧溢位攻擊的防護。

  • 那麼接下來我們拖入 IDA64 中,發現左邊亂七八糟一大堆,這正是因為靜態連結引起的,將所有動態庫裡的函式全寫進來了,如果檢視過這個檔案的大小的話,會發現它遠遠大於我們之前使用動態庫連線技術的檔案的大小:

    image

  • 我們找到加黑了的 main,點選進入,F5 反編譯,發現和上一題的程式碼一模一樣,都是輸入一個長度,然後轉化為有符號的 int來進行判斷大小,接著往 v9 中存入不超過先前讀入的長度的位元組。很明顯這裡存在著和前幾題一樣的棧溢位:

    image

  • 那麼我們記得先前有提到 Canary保護,點開 v9 檢視棧結構找找 校驗值 存在哪裡,但是發現 v9 下面直接就是 sr 了,並沒有找到 Canary保護的校驗值儲存的位置,那麼就不需要理會了,直接正常溢位即可執行我們想執行的程式,也就是所謂的劫持程式。

  • 我們需要模擬 system("/bin/sh") ,這在動態庫裡本質是輸入指令syscall,所以我們就需要一個寫著 syscall指令的地址,用 ropper --file vuln --search "syscall" 進行查詢:

  • 發現很多個,我們隨便選哪個地址都可以,因為執行完 syscall 指令後我們會獲得系統許可權進入互動模式,就不用管 syscall指令之後還有什麼了,可以選最後一個 syscall_addr = 0x41cbf6

  • 接下來我們要找字串 /bin/sh,按 Shift+F12,按 alt+T 查詢字串 /bin/sh,發現並沒有跳轉,不存在現成的字串:

    image

  • 所以我們只能自己找一個地址,往裡面寫入字串 /bin/sh。首先我們需要找一個有讀和寫許可權的段,因為既要寫進去也要讀出來使用。我們按 Shift+F7 開啟段檢視,一般使用 .bss 段,BSS 段通常是指用來存放程式中 未初始化 的或者 \(初始化為0\) 的 全域性變數 和 靜態變數 也就是說,只要初始值為 0 的型別,都會先放在這裡,等到再次賦值時才會被取出。所以寫在這裡面可以全域性使用。

  • 我們點開 .bss 段,隨便複製一個起始位置,bss_addr = 0x4E72C0

  • 那麼接下來我們要往裡面寫資料,可以呼叫 read 函式,在左側下方輸入 read 查詢函式位置:

  • 點進去,複製函式入口位置,read_addr = 0x44FD90

  • 我們發現 read 函式需要三個引數,由於這是 64位 程式透過暫存器傳參,所以和上一題一樣我們要去尋找 pop rdi;retpop rsi;retpop rdx;ret 的程式的存放位置,來改變暫存器的值為 read函式傳參:

  • 在 linux 下用 ropper --file vuln --search "pop|ret" | grep "rdi" 來找與 rdi 相關的指令,在一大堆結果中找到緊挨著 ret 的程式,pop_rdi_ret = 0x4022bf

  • 在 linux 下用 ropper --file vuln --search "pop|ret" | grep "rsi" 來找與 rsi 相關的指令,同理找緊挨著 ret的程式,pop_rsi_ret = 0x40a32e

  • 在 linux 下用 ropper --file vuln --search "pop|ret" | grep "rdx" 來找與 rsi 相關的指令時,發現沒有緊挨著 ret的程式,我們找一個離 ret最近的程式,中間仍然多了一個 pop rbx,不過也可以用,每次多傳一個 0 給 rbx 即可,pop_rdx_rbx_ret = 0x49D06B

  • 最後在程式內找到 main的起始地址,因為第一次溢位後我們輸入字串 /bin/sh 還需要第二次溢位來執行 system("/bin/sh")main_addr = 0x401B7A

  • 那麼我們可以開始構造第一次溢位的 payload1 了,需要注意的是,這一個程式輸入長度的時候用 (unsigned int) 輸入,判斷的時候轉為 (int) 判斷,所以我們需要輸入 2147483679,對應的二進位制轉化為 (int) 就是 -1:

  • 然後構造 payload1,先用 0x100 + 0x08 個位元組填充到 r 處,然後用暫存器為 read 函式傳參:

  • 第一個參數列示讀取的檔案,為 0 表示從控制檯讀入,我們 pop_rdi_ret 後傳 0

  • 第二個引數為存放的地址,我們 pop_rsi_ret 後傳 bss_addr

  • 第三個引數為最大的寫入長度,可以大一點,我們 pop_rdx_rbx_ret 後傳 0x100,然後傳 0 給多的 pop rbx

  • 最後填充函式結束後返回的地址 main_addr

  • 且慢,這是 64 程式,需要檢驗一下呼叫 main 函式之前,rsp 是否指向的地址末尾為 0,可以簡單數一下 main 函式是第 9 條指令,第一條指令以 8 結尾,此時 main 也以 8 結尾,無法透過檢驗。我們需要再填充一條指令進去,這就是所謂的平衡棧操作。

  • 同上一題,我們再用 ropper --file vuln --search "ret" 找一下 ret 指令的位置,取單獨的指令,ret = 0x454257

  • 那麼我們此時在進入 main之前加一個 ret 指令的地址來平衡棧,構造出 payload1:

  • 執行可以看到此時程式成功執行了 read 函式,在輸入一串字元後重新開始執行 main 函式了:

    image

  • 接下來我們可以往這個 .bss 段裡面寫入 /bin/sh 字串了,需要注意的是字串需要一個結束識別符號 \x00,所以我們往裡面寫的應該是 b'/bin/sh\x00'

  • 接著重新再來一遍 main 函式,還是先輸入 2147483649 繞過長度判斷,然後開始構造 payload2 來執行我們的 system("/bin/sh")


  • syscall 的本質是系統透過呼叫這條指令時 rax 裡面的值,來執行不同的函式,我們執行 system("/bin/sh")時需要令 rax = 0x3b 來執行 execve語句。(在 32位 系統中,是用 int 80h 代替 syscall,同時令 eax = 0x0b),所以我們需要找到修改 rax 暫存器的程式碼段 pop rax;ret,和之前的那些一樣,都是所謂的 gadget。

  • 在 linux 中用 ropper --file vuln --search "pop|ret" | grep "rax" 來查詢,pop_rax_ret = 0x4507f7

  • 那麼接下來就可以繼續構造我們的 payload2 了,先填充 0x108 個字元到 r,然後在 pop_rax_ret 後傳 0x3b 修改 rax

  • 接著為 syscall 傳引數,一共有三個引數:

    第一個引數是字串地址,pop_rdi_ret 後傳 bss_addr

    第二和第三個引數涉及系統核心取參方式,系統空間與使用者空間之間的協議,都設為 0 預設即可。

  • pop_rsi_ret 後傳 0,pop_rdx_rbx_ret 後傳 0 ,再傳 0 給 rbx

  • 最後填入 syscall_addr 的地址,來呼叫系統的 syscall 功能。

  • 此時算一下是否棧平衡,我們數一下 syscall的地址為第 10 個指令,此時 rsp 末位為 0,無需調整。

  • 執行成功獲得系統許可權,進入互動模式,輸入 ls 成功輸出當前目錄下的內容:

  • 轉為遠端連線伺服器再次執行,輸入 cat flag 獲得 flag:

  • 最後附上完整 exp(調整了下順序):



MoeCTF 2024

2※ leak_sth

  • 考點:格式化字串漏洞

  • 題目連結:leak_sth

  • 拖進 IDA64,點左側 main,F5 反彙編,發現存在 printf 格式化字串漏洞:

    image

  • 那麼可以先輸出來看看偏移量是多少,由於 buf 最多隻允許輸入 32個位元組,構造輸入為 b'DDDD'+b'.%x'*9

    image

  • 執行數一下,發現偏移 8 個的時候剛好為 buf 的開頭:

    image

  • 繼續分析題目,要求我們接下來輸入的數字等於 v3,那麼我們只需要利用格式化輸出,將 v3 裡面的值輸出來就好了,檢視一下 main 的棧裡 v3 與 buf 的位置關係,發現就在正上方一個位元組:

    image

  • 那麼我們構造 payload 來格式化 %d 輸出偏移量為 7 的位置:

    image

  • 執行,發現已經把 v3 輸出出來了,我們直接複製輸入即可獲得系統許可權,cat flag 一下獲得 flag:

    image