Search

Unsafe Unlink 분석

Tag
heap
Create time
2020/04/20

1. Unsafe Unlink 사용 조건

heap 영역을 전역변수에서 관리해야함
첫번째 청크를 이용하여 두번째 청크의 헤더를 조작가능 해야함
이러한 환경이 갖춰진다면, Unsafe Unlink를 이용하여 원하는 공간에 원하는 값을 쓸수가 있다.

2. Unlink 란?

Unlink란 일반적으로 bin list에 있는 청크가 재할당되어 해당 bin을 빠져나올때 fd, bk를 인접한 청크에 연결짓고 해당 청크가 빠지는 과정을 말한다. 즉 연결리스트에서 노드가 빠질때 포인터를 연결지어지는 것이라고 이해하면 된다.
unlink가 호출되는 코드를 살펴보자
/* Consolidate other non-mmapped chunks as they arrive. */ else if (!chunk_is_mmapped(p)) { ... if (!prev_inuse(p)) { prevsize = prev_size (p); size += prevsize; p = chunk_at_offset(p, -((long) prevsize)); unlink_chunk (av, p, bck, fwd); } ...
C
복사
해당 코드는 _int_free 함수의 일부 코드이다. 현재 free 시키려는 청크 사이즈가 fastbin이 아닌경우, 그리고 mmap으로 할당된 공간이 아닌경우 else if 에 들어온다.
그다음 코드가 몇개 수행되고, 현재 free하려는 청크의 prev_inuse 비트를 확인한다. 만약 이전 청크가 free되어있다면, 현재 청크와 이전청크를 병합하기 위해, 이전 청크의 주소를 p에 담고, unlink를 호출한다.
unlink 소스코드를 살펴보자
#define unlink(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); else { FD->bk = BK; BK->fd = FD; if (!in_smallbin_range (P->size)&& __builtin_expect (P->fd_nextsize != NULL, 0)) { assert (P->fd_nextsize->bk_nextsize == P); assert (P->bk_nextsize->fd_nextsize == P); 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; } } } }
C
복사
우선 FD에 p→fd 즉, 실제 free 시키려는 이전 청크의 fd에 들어있는 값을 FD에 넣는다. 그리고 p→bk에 들어있는 값은 BK에 넣는다.
1.
첫번째 조건
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
C
복사
p의 청크사이즈, 즉 free 시키려는 청크의 이전청크의 사이즈와 p의 다음 청크, 즉 free 시키려는 현 청크의 prev_size가 같지 않으면, 에러를 뿜는다. 정상적인 상황이라면, free된 청크는 다음 청크의 prev_size에 자기의 사이즈를 넣기 때문에 두 값을 같아야 정상이다
2.
두번째 조건
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
C
복사
그다음 첫번재 조건문이 나온다. FD→bkP가 같지 않거나, BK→fdP 가 같지 않으면 에러를 뿜는다. 정상적인 unlink라면 FD에 p→fd가 들어있었기 때문에 p→fd→bk 는 p를 가리키고 있어야 한다. 똑같이 BK→fd도 p→bk→fd 이기 때문에 p를 가리키고 있어야 한다
이 두 조건이 만족하면
FD->bk = BK; BK->fd = FD;
C
복사
이 부분이 실행이된다. 아래 코드는 해당 청크가 small_bin이 아닐 경우에 실행되는 로직이다.
우리는 이 두 조건을 우회할 것이고, 이를 통해 원하는 공간에 값을 넣을 것이다.

3. 공격 흐름

우선 이해하기 쉽게 그림으로 살펴보자
기본 조건으로 A,B,C 청크가 현재 할당되어 있는 상태라고 가정하자
1.
A 청크는 Free, B청크는 아직 Free 안됌
현재 A청크가 Free된 상태이기 때문에 fd,bk에는 main_arena+88의 값이 들어 있을 것이다. 또한 next chunk 즉, B 청크의 prev_size에 해당 청크의 사이즈가 들어가 있다. 여기서 이제 B가 free 되면 인접한 청크인 A가 현재 free된 상태이기 때문에 unlink가 호출되어 병합이 이루어 질 것이다.
2.
B를 Free 시키는 중!
B를 free 시키면 현재 A가 free된 상태이기 때문에 unlink가 호출될 것이고 이때 인자로 A의 주소가 들어갈 것이다. 첫번째 조건문으로 사이즈를 체크하는데 정상적인 unlink이므로, 0x90으로 동일하여 첫번째 조건문을 통과할 것이다
그리고 두번째 조건문 역시 동일하게 A주소를 가리키어 FD → bk = BK, BK → fd = FD가 들어갈 것이다. 이제 비정상적인 경우를 살펴보자
3.
A청크의 데이터를 수정하여 B 청크의 헤더가 조작됨
현재 A, B, C 이렇게 할당이 되어있고, 아무것도 free가 안되어 있는 상황이다. A청크의 데이터영역에는 AA... 여러개가 들어가 있다. 이 상태에서 데이터영역을 다음과 같이 수정을 한다.
A청크가 free되면 채워지는 fd, bk 다음 영역에 총 16바이트 0으로 채웠고, 그 다음 영역에 현재 청크 주소가 저장된 전역변수배열의 시작주소 -24, -16을 각각 넣었다. 마지막으로 B청크의 헤더를 덮어서, prev_size를 0x80으로 덮고, B 청크의 size를 0x91에서 0x90으로 변경하였다.
이 상태에서 B를 free 시키면 어떻게 될까?
현재 B 청크의 prev_inuse 비트가 0으로 변경되었기 때문에 내부적으로 B 청크 이전 청크가 현재 free된 상태라고 착각을 할 것이고, prev_size를 확인하여 이전청크 사이즈가 0x80이라고 인지할 것이다. 그렇게 되면, 파란색 박스 맨 위의 0 , 0 이부분을 각각 이전 청크의 header로 인지하여 unlink가 진행된다.
우선 첫번째 조건을 확인해보자. 현재 Fake 청크의 사이즈는 0이다. 다음 청크의 주소는 현 청크 주소에서 size를 더함으로써 계산되기 때문에 다음청크 역시 0이 더해져 자기 자신 즉, fake 청크를 가리키고 있다. 따라서 둘다 0x00이므로 첫번째 조건문을 만족한다
그다음 두번째 조건을 확인해보자. 현재 FD가 0x1234-24이다. 따라서 FD→bk의 값은 FD의 주소에서 bk가 위치해 있는 +24를 더한 값이다. 결국 0x1234-24+24가 되어 0x1234 값이 되고 이는 Fake 청크의 주소이다.
똑같이 계산되어 Bk → fd 역시 Fake 청크를 가리키고 있어 두번째 조건문도 만족한다. 이렇게 두개의 조건을 모두 만족하기 때문에 아래 로직이 수행된다
1. FD->bk = BK; => 0x1234가 가리키는 곳에 0x1234-16 값이 들어감 2. BK->fd = FD; => 0x1234가 가리키는 곳에 0x1234-24 값이 들어감
Markdown
복사
위 사진처럼 원래는 전역변수 배열에 힙의 주소가 들어가 있어야 하지만, 현재 0x1234 주소에 0x121C 가 들어가 있어, 인덱스 0 번의 들어있는 힙의 데이터를 변경하게 된다면, 힙이 아닌, 전역변수 0x121C위치에 데이터를 쓸수가 있다.
예를 들어 위 사진상태에서 인덱스 0번을 선택하여 "A"*16 + printf_got 주소 이렇게 입력하면, 0x1234에는 printf_got가 다시 들어가게 될 것이고, 한번더 인덱스 0을 선택하여 값을 수정할수 있다면, 여기에 system함수 주소를 넣으면 된다.
libc 주소, 인자, 등을 적절히 처리해줬다는 가정하에, 위 로직이 수행되면, printf_gotsystem함수 주소로 덮어서 쉘을 얻을수 있을 것이다
그럼 이러한 기법을 이용하여 문제를 해결해보자. 풀이는 다음링크에서 확인 가능하다