Post

(UNDEAD) unlink

unlink는 consolidate가 발생할 때 연결되어 있던 bins list에서 chunk를 제거하기 위해 호출된다. PREV\_INUSE를 체크해 호출하기 때문에 fastbin chunk에 PREV\_INUSE unset한다고 해서 회피할 수 있는 것이 아니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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))    // DEAD
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)) {
   

....check routines....

1
2
3
prev\_size            size|flag
P->fd                P->bk

  • P->fd's value + 12위치에 c P->bk에 있는 값이 대입
  • P->bk's value + 8위치에 c P->fd에 있는 값이 대입

Note ) 두 작업이 모두 일어나기 때문에 fd/bk 모두쓰기 권한이 있는 위치여야 한다.

realloc(p, bigger) 요청이 들어와 chunk를 확장해야 할 때 발생.

1
2
3
4
5
6
7
8
9
10
/\* Try to expand forward into next chunk;  split off remainder below \*/
else if (next != av->top &&
!inuse (next) &&
(unsigned long) (newsize = oldsize + nextsize) >=
(unsigned long) (nb))
{
newp = oldp;
unlink (av, next, bck, fwd);
}

free()중 인접 free’d chunk와 merge( consolidate backward / forward )하는 경우 size가 달라져 bins를 옮겨야 할 때 ` unlink`가 호출된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/\* consolidate backward \*/
if (!prev\_inuse(p)) {
prevsize = prev\_size (p);
size += prevsize;
p = chunk\_at\_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}
...
/\* consolidate forward \*/
if (!nextinuse) {
unlink(av, nextchunk, bck, fwd);
size += nextsize;
}

free(A)하면 chunk A의 PREV\_INUSE flag를 확인하고, unset이면 인접 chunk를 unlink → merge → fd/bk 할당 순으로 진행한다. 따라서

  1. merge 및 unlink를 유발하기 위해 chunk A의 PREV\_INUSE flag를 조작할 수 있어야 한다.
  2. unlink되는 것은 free되는 현재 chunk인 A가 아니라 이전 인접 chunk이므로, 이전 chunk의 fd/bk를 조작할 수 있어야 한다. 이전 chunk의 fd/bk를 조작할 수 없는 경우 chunk A의 mchunkptr 위치에 있는 prev\_size field를 조작해 fake chunk를 unlink하도록 하는 방법을 사용할 수 있다. * 현재 chunk를 대상으로는 unlink를 진행할 필요가 없는게, 이제 막 free되었기 때문에 bins list에 속해있지 않은 상태다.
prev_size fake
1
2
PREV chunk's mchunkptr = A's mchunkptr - prev\_size

따라서 prev\_size를 조작하면 PREV chunk 위치를 제어할 수 있다. prev\_size에 음수를 넘기는 것도 가능하기 때문에 음수를 넘겨 PREV chunk를 A chunk 내부에 있는 것 처럼 잡을 수 있다. A chunk에 대한 쓰기 권한이 있는 경우 PREV chunk’s fd/bk를 A chunk에 쓰는 것으로 채울 수 있다.

DEAD

https://github.com/umbum/pwn/blob/master/how2heap/unsafe_unlink.c#L26-L33

check를 통과하기 위해서는 FD->bk = P이면서, BK->fd = P이어야 한다. 때문에 사용할 수 있는 환경이 매우 제한된다.

chunk P의 FD/BK 는 정상적으로 bins에 연결되어 있지 않기 때문에 check를 통과하려면 FD/BK 를 다음과 같이 설정해야만 한다.

1
2
3
FD = &P-(sizeof(uint64\_t)\*3);
BK = &P-(sizeof(uint64\_t)\*2);

  
stack (maybe) 
FD 
BK 
  
&PP
 
heap chunks 
  
P 

그런데 unlink는 다음과 같이 수행되기 때문에

1
2
3
FD->bk = BK;
BK->fd = FD;

결국 P=P->fd를 실행한 것과 같은 결과가 나온다. P->fd의 값은 check를 통과하려면 고정이므로, 사실상 이를 이용해 GOT 등을 수정하는 것은 어렵다.

UNDEAD

https://github.com/umbum/pwn/blob/master/how2heap/unsafe_unlink.c#L58-L70

그러나, P는 이제 heap이 아니라 자기 자신 근처를 가리키고 있다.

  
stack (maybe) 
FD 
BK 
  
&PFD

따라서 P[3] 으로 접근해서 자기 자신이 가리키는 곳을 다른 곳으로 변경할 수 있다.

* 실제로 가리키는 곳을 다른 곳으로 변경하는 작업을 사용자에게 제공하는 경우는 거의 없고, 대부분 가리키는 곳에 있는 값을 변경하는 인터페이스만 제공하기 때문에 단순히 가리키는 곳에 있는 값을 변경하는 것이 가리키는 곳 자체를 다른 곳으로 변경하는 작업이 가능하다면 결국 그 곳에 원하는 데이터를 쓸 수 있다는 얘기가 된다.

This post is licensed under CC BY 4.0 by the author.