💡Heap overflow의 과정
✅예시 프로그램 코드
✅after strcpy(c, ...)
✅after free(b)
chunk B에 fd, bk 포인터가 생기며,
이것이 bin_forward, bin_back과 연결됨으로써, free된 chunk B는 bin에 들어간다.
✅after strcpy(a, ...)
chunk A에, 이것의 크기에 비해 과분한 양의 string이 들어간다.
🚩
따라서 string은 chunk A의 data 공간을 다 채우고 나서 higher address 방향으로 흘러넘쳐 chunk B를 corrupt시킨다.
✅after free(a)
chunk A도 free되어 fd, bk 포인터가 생겼고,
이 포인터들이 기존의 bin (free chunk list)과 연결됨으로써 chunk A도 list에 삽입된다.
🚩
이때, 두 free chunk A와 B는 chunk B의 unlink()를 통해 merge된다.
✅unlink()의 동작을 좀더 자세히 살펴보자.
세 node (chunk 1~3)가 이어진 double linked list가 있다 하면,
chunk 2를 이 list에서 없애는 것은
chunk 1, 3이 이제는 chunk 2를 못본 체하고 서로 연결되게 함으로써 구현된다.
☆
(P->fd)->bk = P->bk
(P->bk)->fd = P->fd
이때 메모리는 아래와 같다.
chunk 1 | chunk 2 | chunk 3 | |||
address | value | address | value | address | value |
lower address | |||||
$ | prev_size | prev_size | @ | prev_size | |
size | size | size | |||
$ + 8 | fd | fd = @ | fd | ||
bk | bk = $ | @ + 12 | bk | ||
higher address |
따라서, 식 ☆은 아래와 같아진다.
🚩
[@ + 12] = P->bk 즉, [@ + 12] = $
그리고
[$ + 8] = P->fd
이제 다시 제대로된 표현으로 써보면...
[(P->fd) + 12] = P->bk
[(P->bk) + 8] = P->fd
💡Exploit
위 과정이 그래서 왜?! 공격에 활용되는 건지 알아보자.
✅by strcpy(a, ...)
chunk B의 fd field, bk field를 원하는 값으로 조작한다.
✅by free(a)
이미 free돼있었던 chunk B의 바로 이전 인접한 chunk A가 free됨으로써, chunk B가 free list로부터 unlink될 수 있게 해준다.
✅by unlinking
[(P->fd) + 12] = P->bk의 관계식이 성립된다.
위에서 chunk B의 fd와 bk를 원하는 값으로 조작한 것은, 이 단계에서 fd와 bk를 원하는 값으로 설정하기 위함이었던 것이다.
✅ 셸코드 실행
🚩
만약, 위 조작을 자알 해서 P->fd를 'printf()의 GOT 주소 - 12', P->bk를 '셸코드의 주소'로 조작한다면,
[printf()의 GOT 주소] = '셸코드의 주소' 가 됨으로써 GOT overwrite가 일어난다.
앞으로 printf()를 호출할 때면 나의 악의적 셸코드가 실행되는 것이다.
(물론, GOT 말고도 다른 위치도 충분히 오염시킬 수 있다: Return address, 함수 포인터, ...)
💡Countermeasures
✅Sanity check of heap metadata
마치 stack 영역의 canary와 비슷한 기능이다.
magic 값이 변경되면 corruption을 감지한다.
✅heap section의 execution 권한 없애기
하지만 ROP, return to libc 공격에는 여전히 취약하다.