(UNDEAD) unlink
unlink
unlink
는 consolidate가 발생할 때 연결되어 있던 bins list에서 chunk를 제거하기 위해 호출된다. PREV\_INUSE
를 체크해 호출하기 때문에 fastbin chunk에 PREV\_INUSE
unset한다고 해서 회피할 수 있는 것이 아니다.
glibc 2.25’s unlink macro
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()’s unlink
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()’s unlink
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 할당 순으로 진행한다. 따라서
- merge 및 unlink를 유발하기 위해 chunk A의
PREV\_INUSE
flag를 조작할 수 있어야 한다. - 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 | |
&P | P |
… | |
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 | |
&P | FD |
따라서 P[3]
으로 접근해서 자기 자신이 가리키는 곳을 다른 곳으로 변경할 수 있다.
* 실제로 가리키는 곳을 다른 곳으로 변경하는 작업을 사용자에게 제공하는 경우는 거의 없고, 대부분 가리키는 곳에 있는 값을 변경하는 인터페이스만 제공하기 때문에 단순히 가리키는 곳에 있는 값을 변경하는 것이 가리키는 곳 자체를 다른 곳으로 변경하는 작업이 가능하다면 결국 그 곳에 원하는 데이터를 쓸 수 있다는 얘기가 된다.