how2heap의 예제를 활용한 house of lore 공격 분석
bins
bins은 사용이 끝난 청크들이 저장되는 객체이다.
메모리의 낭비를 막고, 해제된 청크를 빠르게 재사용할 수 있게 한다.
bin에는 unsorted bin, small bin, large bin이 있으며 small bin에서 특정 크기 이하 청크는 fast bin으로 관리된다.
malloc 요청이 들어오면 먼저 tcache를 검색하고 없으면, unsorted bin을 찾으며 unsorted bin에도 없으면
fast, small, large bin순으로 크기 범위에 맞는 bin을 찾아 탐색한다.
이는 해제된 free 상태의 청크가 저장되는 순서와도 동일한데,
free 함수로 할당 받은 청크를 해제하면 청크는 tcache에 우선적으로 저장된다.
해당 크기의 tcache가 가득 찬 경우, fast, small, large bin으로 바로 가는 것이 아닌 unsorted bin으로 먼저 할당된다.
malloc 과정에서 이 unsorted bin에 분류된 청크들을 적절한 bin으로 분류하는 작업도 수행하게 됩니다.
공격에 관련된 small bin과 tcache에 대한 드림핵의 강의 자료는 다음과 같다.
_int_malloc
다음으로 glibc 2.35의 _int_malloc 함수 코드에 대해 알아보자.
요청크기가 small bin에 해당하는 경우
3, 4번 라인을 거처 크기에 맞는 인덱스를 구한 뒤 아레나의 해당 bin 리스트의 주소를 가져온다.
6번 라인 if문에서 해당 bin의 제일 먼저 해제된 청크를 last 메크로 함수를 통해 victim에 저장한다.
여기서 victim에 저장하는 last(bin)은 bin의 bk를 반환한다.
해당 bin의 주소와 같다는 것은 해당 bin에 현재 저장된 청크가 없어 비어 있는 상태임을 의미한다.
이후 8번 라인에서 bck에 victim의 bk를 저장한다.
이는 bin의 bk의 bk를 의미한다.
9번 라인의 if문에서 bck의 fd가 victim과 동일한 지 검사한다.
bck의 fd는 victim의 bk의 fd이므로 당연히 같아야 한다.
if문을 통과하면 12,13 라인에서 bin의 bk와 bck의 fd값을 바꿔주는 unlink 과정을 통해
victim을 해당 bin 리스트에서 제거한다.
unlink가 끝나면 victim 청크를 재할당한다.
tcache가 도입된 이후 malloc 함수에도 이에 맞춰 코드가 추가되었으며 다음과 같다.
21번 라인에서 요청 크기에 맞는 tcache 인덱스를 계산해서 tcache 범위에 해당하는 지 확인한다.
범위에 해당하면 27번 라인의 while문에 들어가게 되는데
tcache가 꽉 차거나 아까 변수 bin에 담았던 small bin이 빈 리스트가 될 때까지
해당 small bin에 존재하는 청크를 차례대로 tcache로 옮긴다.
while 문 내부 36, 37번 라인에서 small bin에 있던 청크에 unlink 작업을 수행하고,
39번 라인에서 tcache로 옮겨주는 작업을 수행한다.
How2Heap
다음은 설명하는 부분과 주소를 출력하는 printf 함수들을 제외하고 가져온 코드이며 3등분 하였다.
해당 예제의 목표는 스택에 청크를 할당하여 리턴 주소를 1번 라인에 선언된 jackpot 함수로 오버라이트 하는 것이다.
코드를 살펴보면 4,5,6번 라인에선 아까 살펴본 malloc 함수의 조건식을 우회하기 위해
stack_buffer_1, stack_buffer_2라는 fake chunk와 fake freelist를 선언한다.
8번 라인에서 small bin에 해당하는 청크 victim을 할당 받는다.
10,11 라인에서 victim과 같은 크기의 더미 청크 7개를 할당 받는다.
이 더미 청크들은 tcache를 채우는데 활용된다.
14번 라인의 victim_chunk는 청크 헤더 사이즈를 뺀 victim의 헤더 주소이다.
17번 라인은 fake_freelist의 bk 위치의 값을 다음 인덱스로 설정해주는데 이 리스트도 tcache를 채우는 용도로 사용된다.
19번 라인에서 fake_freelist의 마지막 요소의 bk는 NULL로 설정된다.
21~27번 라인에서 stack_buffer_1, stack_buffer_2의 fd, bk 위치에 값을 채워 fake chunk로 만들어준다.
위 내용을 그림으로 표현하면 다음과 같다.
victim과 dummy chunk들은 힙에 할당되어 있고 나머지는 스택에 존재하는 fake 청크들이다.
stack_buffer_1, stack_buffer_2, fake freelist가 bk를 통해 연결되어 있으며,
stack_buffer_1, stack_buffer_2는 양방향으로 연결되어 있는 상태이다.
다음으로 29번 라인에서 1000 바이트만큼 malloc 요청하여 크기가 작은 청크가 탑 청크와 병합되는 것을 방지해준다.
32번 라인에서 dummies 배열에 저장해 둔 청크들을 모두 해제시켜 tcache를 채운 다음
victim을 해제시켜 unsorted bin에 넣어준다.
이후 1200 바이트만큼 malloc 요청하여 unsorted bin에 있던 victim을 small bin으로 옮겨준다.
위 내용을 그림으로 표현하면 다음과 같다.
더미 청크들이 해제되면서 tcache에 들어가게 되고
victim이 해제될 때는 tcache에 7개가 모두 채워졌기 때문에 unsorted bin에 들어가게 되고 fd, bk가 설정된다.
이후 1200바이트를 요청하므로 unsorted bin에서 small bin으로 옮겨지게 된다.
참고로 이때 victim의 fd, bk도 재설정된다.
이제 37번 라인에서 victim의 bk를 stack_buffer_1의 주소로 변조시킨다.
39번 라인에서 tcache에 있던 청크들을 다 빼준 뒤,
41번 라인에서 victim을 재할당 받아 p3에 저장한다.
42번 라인에서 한 번 더 0x100만큼 malloc 요청을 해서 p4에 저장하는데 fake freelist의 청크가 할당된다.
이는 37번 라인에서 victim의 bk를 stack_buffer_1로 설정해주어서 small bin 리스트가 변조된 결과이다.
마지막으로 45, 46 라인에서 리턴 주소까지의 오프셋을 계산해서 jackpot 함수를 리턴 주소에 오버라이트 하면 목표를 달성하게 된다.
위 내용을 그림으로 표현하면 다음과 같다.
더미 청크들을 전부 재할당 받으면서 tcache는 비워진다.
이후 malloc을 통해 small bin에 있던 victim이 p3에 저장된다.
여기서 victim에 대해 unlink 과정을 수행할 때 victim의 bk가 stack _buffer_1을 가리키고 있으므로
small bin이 비어 있지 않는 것으로 인식이 되어 버리기 때문에 tcache로 옮겨주는 작업을 진행하게 된다.
그렇게 stack_buffer_1부터 bk를 따라 가면서 tcache에 순차적으로 채워지게 되면
fake freelist 인덱스 4번 요소가 마지막으로 채워지게 된다.
tcache는 last in first out 구조이기 때문에 p4에 저장되는 청크를 malloc 요청하게 되면
tcache의 마지막으로 채워진 fake freelist의 인덱스 4번 요소가 반환되게 되며 스택에 청크가 할당되게 된다.
How2Heap (Debugging)
이제 디버깅을 통해 실제 어떻게 익스플로잇이 되는지 확인해보자.
1~27번 라인까지는 선언 및 할당으로 크게 디버깅으로 볼 부분이 없다.
8번 라인의 설정되는 victim의 주소만 보자.
8번 라인이 실행된 직후의 모습이다.
RAX에 victim의 data 영역 주소인 0x92a0가 저장되어 있다.
헤더 영역의 주소는 0x9290이 될 것이다.
스택 관련 주소는 빼두었던 printf 구문을 다시 넣고 디버깅하여 확인했다.
stack_buffer_1의 주소는 0xded0이고, stack_buffer_2의 주소는 0xdef0이다.
fake freelist 배열의 시작 주소는 0xdf50이다.
(편의상 주소는 끝 자리 4개만 언급함)
29번 라인이 실행된 직후를 보면 작은 청크들이 탑 청크와 병합되지 않도록 1000바이트 만큼 청크를 할당 받은 모습이다.
32번 라인까지 실행되고 난 직후의 모습이다.
tcache에 더미 청크들이 모두 들어갔으며 victim은 unsorted bin에 들어가 있는 모습이다.
34번 라인이 실행되면 unsorted bin에 있던 victim이 small bin으로 옮겨진다.
39번 라인까지 실행된 후의 모습이다.
victim의 bk를 stack_buffer_1의 주소로 변조시킨 뒤 tcache에 존재하는 모든 더미 청크들을 빼낸다.
아래쪽 사진은 heapinfo 명령의 결과이다.
small bin에 대해 경고 문구가 생겼는데 small bin에 청크가 1개일 땐
fd, bk 둘 다 같은 main_arena의 주소를 가지고 있어야 하는데
victim의 bk를 변조시켜서 fd와 bk가 동일하지 않아 발생하는 경고 문구이다.
41번 라인이 실행된 직후의 모습이다.
malloc 요청으로 small bin에 있던 victim을 재할당 받아 p3에 저장한다.
victim이 unlink 및 재할당되면서 small bin은 원래 비어 있는 상태가 되는데
victim의 bk가 변조되어서 small bin 리스트에 아직 청크가 남아있는 것으로 착각하게 된다.
따라서 tcache로 옮기는 작업이 수행되며 victim의 bk였던 stack_buffer_1부터 차례대로 tcache로 들어가게 된다.
이 때부턴 아래쪽 사진처럼 heapinfo 명령으로는 경고 문구들 때문에 tcache의 리스트를 파악하기가 어렵다.
tcahce는 청크를 key로 관리되는데 중앙 오른쪽 사진을 보면 디버거의 명령으로 tcache의 key를 확인할 수 있다.
stack_buffer_1, stack_buffer_2, fake_freelist의 bk 위치에 저장된 값을 키 값과 비교해보면
tcache에 들어갔는지 여부를 판단할 수 있다.
현재는 stack_buffer_1, stack_buffer_2, fake_freelist의 인덱스 4번까지 총 7개가 tcache에 들어가 있음을 확인할 수 있다.
마지막 malloc 요청인 42번 라인이 실행된 직후의 모습이다.
tcache에 마지막으로 저장되었던 fake_freelist의 4번 인덱스 요소가 재할당된다.
중앙 왼쪽 사진의 RAX를 보면 data 영역의 주소인 0xdfe0가 반환되었음을 확인할 수 있다.
중앙 오른쪽 사진을 보면 fake_freelist 4번 인덱스가 재할당되면서 key 값이 사라진 것을 확인할 수 있고
heapinfo 명령을 보면 tcache의 청크가 7개에서 1개 줄어 6개가 된 모습을 볼 수 있다.
정리해보면 victim은 할당되면서 small bin은 원래 비어 있는 상태가 되지만
victim의 bk가 stack_buffer_1으로 변조되었기 때문에 비어 있지 않은 것으로 인식되어
stack_buffer_1이 첫 번째로 tcache에 들어간다.
stack_buffer_1의 bk인 stack_buffer_2가 두 번째로 tcache에 들어가게 되며 7개가 채워질 때까지 순서대로 들어간다.
이후 malloc을 요청하게 되면 가장 마지막에 들어간 fake_freelist 배열의 4번 인덱스가 청크로 스택에 할당된다.
이후 나머지 라인들이 실행되면 main 함수의 리턴 주소를 jackpot 함수의 주소로 변조시킨다.
따라서 종료되기 전 jackpot 함수가 실행된 후 종료된다.
마지막에 Nice jump가 출력되는 것을 확인할 수 있다.
결론적으로 house of lore 공격은 다음과 같이 정리해볼 수 있다.
레퍼런스
https://learn.dreamhack.io/16#66
https://learn.dreamhack.io/98#3
https://github.com/shellphish/how2heap/blob/master/glibc_2.35/house_of_lore.c
https://www.lazenca.net/pages/viewpage.action?pageId=1148020
'background > linux' 카테고리의 다른 글
[how2heap] Fastbin dup (glibc 2.35) (0) | 2024.03.05 |
---|---|
stack pivoting (0) | 2023.07.06 |
[how2heap] Tcache House of Spirit (glibc 2.35) (0) | 2023.05.23 |
[how2heap] House of Force (glibc 2.27) (0) | 2023.03.26 |
Frame Pointer Overwirte (One Byte Overflow) (0) | 2023.03.26 |