Unsafe Unlink는 doubly linked list에서 청크를 연결 해제하는 매크로인 unlink를 이용해 aaw 할 수 있게 해주는 공격 기법이다.
- 힙 영역을 전역 변수같이 주소를 알고 있는 위치에 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);
}
...
_int_free의 일부이다.
small bin, large bin 같은 doubly linked list들이 여기까지 온다.
mmap으로 할당된 청크가 아니라면, prev_inuse bit를 체크한다.
prev_inuse bit가 세팅되어있다면, 현재 청크과 prev 청크는 병합 대상으로 간주된다.
그리고 병합이 되면서 unlink 매크로가 호출된다.
이때 p에서 prev_size를 뺀 청크, 즉 prev 청크가 p로 들어간다.
unlink 매크로는 doubly linked list에서 노드를 제거하는 것과 같은 동작을 수행한다.
/* 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;
}
}
}
}
unlink 매크로의 소스 코드이다.
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
첫번째 조건문은 P의 chunk size와 next_chunk의 prev_size를 비교한다.
즉, boundary tag가 제대로 들어가있는지 체크한다.
일반적으로 P의 size와 next chunk의 prev_size는 free된 상태라면, size == prev_size
이다.
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
두번째 조건문은 FD->bk(P->fd->bk)
와 BK->fd(P->bk->fd)
가 P인지 검증한다.
일반적으로 fd, bk는 인접한 청크를 가리키기 때문에,
FD->bk
와 BK->fd
는 P
일 수 밖에 없다.
FD->bk = BK;
BK->fd = FD;
앞에 조건문들을 다 통과하면, 실질적인 unlink가 수행된다.
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
첫번째 if문과 두번째 if문을 우회하기 위해서 fake chunk를 만든다.
fake chunk의 prev size와 current size를 0으로 세팅하면 chunksize(P)는 0이 된다.
#define next_chunk(p) ((mchunkptr) (((char *) (p)) + ((p)->size & ~SIZE_BITS)))
next_chunk는 기본적으로 현재 p에서 size를 더해서 계산된다. 이때 하위 3비트 플래그 비트들은 무시된다.
이때 next_chunk(P)는 size가 0이기 때문에 현재 청크를 가리키게 되고, 그 청크의 prev_size는 0으로 세팅되어있으니 결과적으로 0이 된다.
아니면 굳이 prev size와 current size를 0으로 세팅하지 않고도 우회할 수 있다.
prev size와 current size를 잘 맞춰주고, 이후 overflow 등을 이용해서 next chunk의 prev size를 fake chunk의 size로 맞춰주면 우회가 가능하다.
청크를 전역 변수에서 관리한다고 가정하고, FD
가 0x6020b0
라면 FD->bk
는 0x6020b0 + 24
를 가리키게 된다.
그리고 BK
를 0x6020b8
로 세팅해주면, BK->fd
는 0x6020b8 + 16
을 가리킨다.
결국 똑같은 힙 청크를 가리키게 된다.
즉 0x6020c8 - 24
를 FD에 세팅하고, 0x6020c8 - 16
을 BK에 세팅해주면 된다.
그러면 validation check를 우회할 수 있게 된다.
그 다음에는 overflow 등을 이용해서 next chunk 헤더를 조작해야된다.
size의 하위 3비트 플래그 비트와 prev size를 조작해서 fake chunk를 이용할 수 있도록 해주고, size의 PREV_INUSE bit
를 0
으로 세팅해서, free시에 병합 대상으로 간주될 수 있게 해준다.
그리고 마지막으로 prev size를 fake chunk의 size로 바꿔주면,
p = chunk_at_offset(p, -((long) prevsize));
chunk_at_offset 매크로에 의해서 fake chunk를 병합 대상 청크로 인식하게 할 수 있다.
FD->bk = BK;
BK->fd = FD;
이제 free해서 unlink 해주면, 위 로직이 실행된다.
FD->bk = BK;
가 돌아가면 0x6020c8
이 BK
로 덮인다.
BK->fd = FD;
가 돌아가면 0x6020c8
이 최종적으로 FD
, 즉 0x6020b0
으로 덮인다.
그러면 0x6020c8
을 덮을 수 있고, 임의 주소로 돌릴 수 있다.