概述
堆溢出是指程序向某个堆块中写入的字节数超过了堆块本身可使用的字节数(之所以是可使用而不是用户申请的字节数,是因为堆管理器会对用户所申请的字节数进行调整,这也导致可利用的字节数都不小于用户申请的字节数),因而导致了数据溢出,并覆盖到物理相邻的高地址的下一个堆块。
堆溢出漏洞发生的基本前提是
- 程序向堆上写入数据。
- 写入的数据大小没有被良好地控制。
堆上并不存在返回地址等可以让攻击者直接控制执行流程的数据,因此我们一般无法直接通过堆溢出来控制 EIP 。一般来说,我们利用堆溢出的策略是
- 覆盖与其物理相邻的下一个 chunk 的元数据。
- 利用堆中的机制(如 unlink 等 )来实现任意地址写入( Write-Anything-Anywhere)或控制堆块中的内容等效果,从而来控制程序的执行流。
分析
堆溢出中比较重要的几个步骤:
寻找堆分配函数
通常来说堆是通过调用 glibc 函数 malloc 进行分配的,在某些情况下会使用 calloc 分配。calloc 与 malloc 的区别是 calloc 在分配后会自动进行清空,这对于某些信息泄露漏洞的利用来说是致命的。
1 | calloc(0x20); |
除此之外,还有一种分配是经由 realloc 进行的,realloc 函数可以身兼 malloc 和 free 两个函数的功能。
1 | #include <stdio.h> |
realloc的操作并不是像字面意义上那么简单,其内部会根据不同的情况进行不同操作
- 当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掉后一部分
- 如果申请size>原来size
- 当realloc(ptr,size)的size等于0时,相当于free(ptr)
- 当realloc(ptr,size)的size等于ptr的size,不进行任何操作
寻找危险函数
通过寻找危险函数,我们快速确定程序是否可能有堆溢出,以及有的话,堆溢出的位置在哪里。
常见的危险函数如下
- 输入
- gets,直接读取一行,忽略 ‘\x00’
- scanf
- vscanf
- 输出
- sprintf
- 字符串
- strcpy,字符串复制,遇到 ‘\x00’ 停止
- strcat,字符串拼接,遇到 ‘\x00’ 停止
- bcopy
堆中的 Off-By-One
off-by-one 漏洞原理
off-by-one 是指单字节缓冲区溢出,这种漏洞的产生往往与边界验证不严和字符串操作有关,当然也不排除写入的 size 正好就只多了一个字节的情况。其中边界验证不严通常包括
- 使用循环语句向堆块中写入数据时,循环的次数设置错误(这在 C 语言初学者中很常见)导致多写入了一个字节。
- 字符串操作不合适
例如
- 循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18int my_gets(char *ptr,int size)
{
int i;
for(i=0;i<=size;i++)
{
ptr[i]=getchar();
}
return i;
}
int main()
{
void *chunk1,*chunk2;
chunk1=malloc(16);
chunk2=malloc(16);
puts("Get Input:");
my_gets(chunk1,16);
return 0;
}off-by-one after1
2
3
40x602000: 0x0000000000000000 0x0000000000000021 <=== chunk1
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000021 <=== chunk2
0x602030: 0x0000000000000000 0x00000000000000001
2
3
40x602000: 0x0000000000000000 0x0000000000000021 <=== chunk1
0x602010: 0x4141414141414141 0x4141414141414141
0x602020: 0x0000000000000041 0x0000000000000021 <=== chunk2
0x602030: 0x0000000000000000 0x0000000000000000 - 字符串strlen 和 strcpy 的行为不一致却导致了off-by-one 的发生。 strlen 是我们很熟悉的计算 ascii 字符串长度的函数,这个函数在计算字符串长度时是不把结束符 ‘\x00’ 计算在内的,但是 strcpy 在复制字符串时会拷贝结束符 ‘\x00’ 。这就导致了我们向chunk1中写入了25个字节
1
2
3
4
5
6
7
8
9
10
11
12
13
14int main(void)
{
char buffer[40]="";
void *chunk1;
chunk1=malloc(24);
puts("Get Input");
gets(buffer);
if(strlen(buffer)==24)
{
strcpy(chunk1,buffer);
}
return 0;
}off-by-one after1
2
30x602000: 0x0000000000000000 0x0000000000000021 <=== chunk1
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000411 <=== next chunk可以看到 next chunk 的 size 域低字节被结束符 ‘\x00’ 覆盖,这种又属于 off-by-one 的一个分支称为 NULL byte off-by-one,我们在后面会看到 off-by-one 与 NULL byte off-by-one 在利用上的区别。 还是有一点就是为什么是低字节被覆盖呢,因为我们通常使用的CPU的字节序都是小端法的,比如一个DWORD值在使用小端法的内存中是这样储存的1
2
30x602000: 0x0000000000000000 0x0000000000000021
0x602010: 0x4141414141414141 0x4141414141414141
0x602020: 0x4141414141414141 0x00000000000004001
2DWORD 0x41424344
内存 0x44,0x43,0x42,0x41
举一个例子,比如数字0x12 34 56 78在内存中的表示形式为:
1)大端模式:
低地址 —————–> 高地址
0x12 | 0x34 | 0x56 | 0x78
2)小端模式:
低地址 ——————> 高地址
0x78 | 0x56 | 0x34 | 0x12
unlink
内容有点多,单独写了一篇文章。
http://eternalsakura13.com/2018/03/01/unlink1/
fastbin attack
fastbin attack 是一类漏洞的利用方法,是指所有基于 fastbin 机制的漏洞利用方法。这类利用的前提是:
- 存在堆溢出、use-after-free 等能控制 chunk 内容的漏洞
- 漏洞发生于 fastbin 类型的 chunk 中
如果细分的话,可以做如下的分类: - Fastbin Double Free
- House of Spirit
- Alloc to Stack
- Arbitrary Alloc
其中,前两种主要漏洞侧重于利用free函数释放真的chunk或伪造的chunk,然后再次申请chunk进行攻击,后两种侧重于故意修改fd指针,直接利用malloc 申请指定位置chunk进行攻击。
Fastbin Double Free
介绍
Fastbin Double Free 是指 fastbin 的 chunk 可以被多次释放,因此可以在 fastbin 链表中存在多次。这样导致的后果是多次分配可以从 fastbin 链表中取出同一个堆块,相当于多个指针指向同一个堆块,结合堆块的数据内容可以实现类似于类型混淆(type confused)的效果。
Fastbin Double Free 能够成功利用主要有两部分的原因