堆利用入门

堆利用入门

arena

内存分配区,可以理解为堆管理器所持有的内存池

操作系统-》堆管理器-》用户

物理内存-》arena-》可用内存

堆管理器与用户的 交易发生在arena中可以理解为堆管理器从操作系统批发的有冗余的内存库存

chunk

用户申请内存的单位,也是堆管理内存的基本单位,malloc返回的指针指向一个chunk的数据区域

chunk的 结构体

1
2
3
4
5
6
7
8
9
10
11
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;
};

chunk的分类

按状态

  • malloced(使用状态
  • free(状态释放

按大小

  • fast
  • small
  • largh
  • tcache

按特定功能

  • top chunk
  • last remainder chunk

使用状态(malloc chunk)的堆结构

有个堆头和一个数据区域

释放状态的堆(free chunk)的堆结构

堆头和指针以及未使用的区域

top chunk

从来没有被使用的区域,申请时从其中切割

last remainder chunk

malloc分割原chunk后剩余的部分

比如说之前释放了一个很大的chunk,后面又申请了一个比它小的chunk,可以从那个大的chunk切割出需要的大小,剩下的部分就叫last remainder chunk

堆的大小对齐

堆的大小必须是2乘SIZE_SZ的整数倍,如果申请的内存不是2*SIZE_SZ的整数倍,会被转成(补齐)到满足的大小。32位中SIZE_SZ=4,64位中SIZE_SZ=8,就是说,32位系统堆的大小为8的倍数,64位系统堆的大小为16的倍数。另外8的二进制是1000,所以不管SIZE_SE的大小是多少,对应的低三位都是0,所以利用这三位作为标志位:

NON_MIAN_ARENA,记录当前chunk是否不属于主线程,1表示不属于,0表示属于。

IS_MAPPED,记录chunk是否是由mmap分配的。

PREV_INUSE,记录着前一个chunk块是否被分配。

注意:第一个堆块的P被设置为1,因为再往前就是非法区域,这个P的作用主要是看两个chunk能不能合并

堆的一些名词

bin

管理arena中空闲chunk的结构,以数组的形式存在,数组元素为相应大小的chunk链表的链表头,存在于malloc_state中

  • unsorted bin
  • fast bins
  • small bins
  • large bins
  • (trache glibc-2.27开始)

prev_size

若前一个物理相邻的chunk是free chunk,则表示其大小,否则用于储存前一个chunk的数据(注释:属于前一个chunk,被前一个chunk使用作为存储数据的空间)

确定前一个堆块是否被释放的标志前面说过,就是图中的p

size

占据一字节

用于表示当前chunk的大小(包括chunk头)低三位地址用作标识位

fd pointer(Forward Pointer)

在bin中指向下一个(非物理相邻)空闲的chunk

bk pointer(Backward Pointer)

在bin中指向上一个(非物理相邻)空闲的chunk

fd_nextsize

在large bin中指向前一个与当前chunk大小不同的第一个空闲块,不包含bin头指针

bk_nextsize

在large bin中指向后一个与当前chunk大小不同的第一个空闲块,不包含bin头指针

一般空闲的large chunk在fd的遍历顺序中按照从大到小的顺序排列。这样就可以避免在寻找合适的chunk时挨个遍历

fast bins

  • fastbinsY[]

  • 单项链表

  • LIFO

  • 管理16、24、32、40、56、64Bytes的free chunks(32位下默认)

  • 其中的chunk的in_use位(下一个物理相邻的chunk的P位)总为1

    下面是一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
//lfastbin unsortedbin smallbin
#include <stdio.h>
#include <malloc.h>
int main(int argc, char const *argv[ ]){
int a = malloc (0x10);
int b = malloc (0x10);
int c = malloc (0x10);
free(b);
free(a); //fastbin
mallopt(1,0);//修改global_max_fast = 0,进入unsorted bin,默认大小是0x80,这里设置为0
malloc(0x50);//进入small bin
return 0;
}

编译之后运行不了,是Ubuntu版本问题,改用16可以,,,

在用gdb调试

这是我的调试之后堆情况,不理解为啥size后面是地址,bk指针却是大小。。。

断点断在最后一次malloc处,根据源代码,应该有两个0x20的,应该被整合到一起了

这是视频的调试堆情况,应该是插件版本的原因,我的pwndbg没有paresheap这个指令

视频说:在fast bin相邻的堆块其实是不会合并的,为了更高效的利用,有时系统会申请小块内存

上面是视频,下面是本地调试

后面的是0x20+1(大小和标识(前一个chunk空闲))

unsorted bin

  • bins[1]

  • 管理刚刚释放还未分类的chunk

  • 可以视为空闲chunk回归其所属bin之前的缓冲区

后来的视频提到,把fast bin关闭后两个小的内存就会合并到unsorted bin

果然尝试断点打在 mallopt(1,0);//修改global_max_fast = 0,进入unsorted bin之前就达到了视频中的结果

small bins

  • bins[2]~bins[63]
  • 62个循环双链表
  • FIFO
  • 管理16、24、32、40、。。。。。。、504 Bytes的free chunks(32位下)
  • 每个链表中存储的chunk大小都一致

large bins

  • bin[64]~bins[126]
  • 63个循环双向链表
  • FIFO
  • 管理大于504Bytes的free chunks(32位下)

malloc

  • 它根据用户申请的内存块大小以及相应大小chunk通常使用的频率(fast bin chunk,small bin chunk,large bin chunk),依次实现了不同的分配方法。(先在fastbin找,有就直接返回这块地址的,fastbin没有去smallbin找,找到了返回找不到去largebin找,找到先切割一下再返回)
  • 当所有空闲chunk都无法满足时,才会考虑top chunk。
  • 当top chunk也无法满足时,堆分配器才会进行内存块申请。

mmap函数申请的内存释放时直接归还操作系统

free

  • 它就用户暂且不用的chunk回收给堆管理器,适当的时候还会归还给操作系统。
  • 它依据chunk大小来优先试图将free chunk链入tcache或者是fast bin。不满足则链入unsort bin中。
  • 除了tcache chunk与fast bin chunk,其他chunk在free时会与其他物理相邻的free chunk合并

tcache bin

tcachebin比fastbin大,作用与fastbin类似,可以说成是扩大版的fastbin,最大是0x410(多了会扔到unsort bin里)但是它只能存七个(),fastbin可以是无限的

tcachebin没有double free的机制(还不知道是啥)

UAF(Use-After-Free)

释放后重用漏洞

程序有漏洞,可以对释放后的堆块进行改写

一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<stdio.h>
#include<string.h>
#include<malloc.h>
#include<stdlib.h>
unsigned long long target[100];
int main()
{
unsigned long long *p=malloc(0x10);
free(p);
p[0]=target;//p_chunk->fd is modified(意味修改),此处是将p的fd指针改为target地址,目的是伪造一个chunk
target[0]=0;//prev_size
target[1]=0x21;//size
malloc(0x10);//must be same as p,因为fastbin里有刚刚进去的p大小一样
char *q=malloc(0x10);//must be same as target,因为我们改了p的fd指针,在p从fastbin分配出去后,下一个指向的空闲的chunk就是我们伪造的chunk
memcpy(q,"hello",6);//用memcpy函数在q写入hello
printf("%s\n",&target[2]);//打印target第三个元素,看看是不是hello,如果是那么伪造成功
return 0;
}

编译运行,发现和我们预测的一样

整个程序都没有修改target[2]处的值,我们只是修改了target[0]和[1]处的值

另外说明,这个程序只适用于,没有tcache的ubuntu16.04即libc2.23环境下,有了tcache之后就不能用攻击fastbin的方式了

从以上示例内容处可以看出来,如果可以使用一个被释放了的堆,就可以把堆分配到任意想分配的位置(包括各种函数指针),把函数指针劫持了就相当于劫持了程序的控制流,就可以以该程序所在的权限去做想做的事情

下面是一道buuctf的堆题

例题:actf_2019_babyheap

下载之后运行,ida反编译

搜索找到了敏感函数和/bin/sh字符串

讲课人说遇到堆题坚定不移先看删除函数

删除函数sub_400BAE()

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

v3 = __readfsqword(0x28u);
puts("Please input list index: ");
read(0, buf, 4uLL);
v1 = atoi(buf);
if ( v1 >= 0 && v1 < dword_60204C )//删除前有检查
{
if ( *(&ptr + v1) )
{
free(*(void **)*(&ptr + v1));//先是释放了指向的堆块
free(*(&ptr + v1));//接着释放了自己本身(自身是一个指针)
}
}//但是删除后没有置空,有问题
else
{
puts("Out of bound!");
}
return __readfsqword(0x28u) ^ v3;
}

删除函数有一点问题

创建函数

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
unsigned __int64 sub_400A78()
{
void **v0; // rbx
int i; // [rsp+8h] [rbp-38h]
int v3; // [rsp+Ch] [rbp-34h]
char buf[24]; // [rsp+10h] [rbp-30h] BYREF
unsigned __int64 v5; // [rsp+28h] [rbp-18h]

v5 = __readfsqword(0x28u);
if ( dword_60204C <= 5 )//一个计数器
{
for ( i = 0; i <= 4; ++i )
{
if ( !*(&ptr + i) )
{
*(&ptr + i) = malloc(0x10uLL);
*((_QWORD *)*(&ptr + i) + 1) = sub_40098A;
puts("Please input size: ");//自己确定大小
read(0, buf, 8uLL);
v3 = atoi(buf);
v0 = (void **)*(&ptr + i);
*v0 = malloc(v3);
puts("Please input content: ");//输入内容
read(0, *(void **)*(&ptr + i), v3);
++dword_60204C;
return __readfsqword(0x28u) ^ v5;
}
}
}
else
{
puts("The list is full");
}
return __readfsqword(0x28u) ^ v5;
}

创建函数没有问题

打印函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned __int64 sub_400C66()
{
int v1; // [rsp+Ch] [rbp-24h]
char buf[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v3; // [rsp+28h] [rbp-8h]

v3 = __readfsqword(0x28u);
puts("Please input list index: ");
read(0, buf, 4uLL);
v1 = atoi(buf);
if ( v1 >= 0 && v1 < dword_60204C )
{
if ( *(&ptr + v1) )
(*((void (__fastcall **)(_QWORD))*(&ptr + v1) + 1))(*(_QWORD *)*(&ptr + v1));
}
else
{
puts("Out of bound!");
}
return __readfsqword(0x28u) ^ v3;
}

但是没看明白是怎么输出的,视频经过逆向处理后函数是这样的

好像是通过指针来输出的,正常一个printf就可以了,但是这里用了指针,有点问题。