[DH] validator-revenge
Introduction
validator-revenge에 대한 writeup이다. 구체적인 코드는 생략하였고, 문제를 해결하기 위한 접근과 stack 구조만 작성하였다.
Analysis
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
문제에서 있는 input validation은 별 것 없고 "DREAMHACK!\x00zxy..." 이런식으로 어렵지 않게 조건에 맞는 입력을 찾을 수 있다. 이후 canary도 없는 상태에서 평화롭게 ROP를 진행하면 되는 문제이다. ROP gadget도 충분한 상황인데 문제가 어려워진 건 어떻게 libc를 leak할 것인가였다.
GOT 영역을 보아도 출력 함수는 없었고, 그나마 IO 관련 함수는 read
와 fflush
함수가 있었다.
그래서 최근에 집중했던 FSOP로 방향을 잡고 leak을 진행하기로 했다.
Approach
Stack pivoting
stack pivoting이란 stack pointer를 원하는 위치로 옮겨서 그 위치에서 ROP를 진행하는 것을 의미한다.
내가 원하는 위치에서 ROP를 진행할 수 있기에 훨씬 자유도가 높다.
특히 PIE가 적용되지 않은 상황에서 stack pivoting을 통해서 bss 영역 내부에서 ROP를 진행하면 주소를 모두 알고 지정할 수 있기에 편하다. pop rsp
가 없었다면 곤란했겠지만, pop_rsp_r13_r14_r15
가 있어서 쉽게 해결할 수 있었다.
스택 구조는 그림으로 대체한다.
fflush
함수 분석
해당 분석은 문제 버전에 맞는
glibc
2.27 버전을 기준으로 작성되었습니다.
fflush
함수는 아래처럼 버퍼를 비우기 위한 함수이다.
printf("Hello, world");
fflush(stdout); // it gets printed immediately
그리고 FSOP를 통해 파일 구조체를 잘 조작하면 원하는 영역의 데이터를 출력할 수 있다. 확인하기 위해 분석을 진행했다.
일단 fflush
함수의 구현을 살펴보면 아래와 같다. (실제로는 _IO_fflush
함수가 호출된다.)
int
_IO_fflush (_IO_FILE *fp)
{
if (fp == NULL)
return _IO_flush_all ();
else
{
int result;
CHECK_FILE (fp, EOF);
_IO_acquire_lock (fp);
result = _IO_SYNC (fp) ? EOF : 0;
_IO_release_lock (fp);
return result;
}
}
CHECK_FILE
과 _IO_acquire_lock
FILE 구조체에 대한 검사를 하는 것을 추측된다.
GDB를 돌려보았을 때 두 함수 모두 통과되어 바로 _IO_SYNC
함수에 대한 분석으로 넘어갔다.
아래는 _IO_SYNC
함수의 JUMP 테이블에 등록된 함수이다.
int
_IO_new_file_sync (_IO_FILE *fp)
{
_IO_ssize_t delta;
int retval = 0;
/* char* ptr = cur_ptr(); */
if (fp->_IO_write_ptr > fp->_IO_write_base)
if (_IO_do_flush(fp)) return EOF;
delta = fp->_IO_read_ptr - fp->_IO_read_end;
if (delta != 0)
{
#ifdef TODO
if (_IO_in_backup (fp))
delta -= eGptr () - Gbase ();
#endif
_IO_off64_t new_pos = _IO_SYSSEEK (fp, delta, 1);
if (new_pos != (_IO_off64_t) EOF)
fp->_IO_read_end = fp->_IO_read_ptr;
else if (errno == ESPIPE)
; /* Ignore error from unseekable devices. */
else
retval = EOF;
}
if (retval != EOF)
fp->_offset = _IO_pos_BAD;
/* FIXME: Cleanup - can this be shared? */
/* setg(base(), ptr, ptr); */
return retval;
}
libc_hidden_ver (_IO_new_file_sync, _IO_file_sync)
fp->_IO_write_ptr > fp->_IO_write_base
라면 _IO_do_flush(fp)
함수를 호출한다.
그리고 _IO_do_flush(fp)
는 아래 매크로로 정의된다.
_IO_do_write(_f, (_f)->_IO_write_base, \
(_f)->_IO_write_ptr-(_f)->_IO_write_base) \
...
이를 통해서 vtable의 _IO_do_write
함수가 호출되는 것을 확인할 수 있다.
그리고 _IO_do_write
함수는 new_do_write
함수를 호출한다.
static _IO_size_t
new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
_IO_size_t count;
if (fp->_flags & _IO_IS_APPENDING)
/* On a system without a proper O_APPEND implementation,
you would need to sys_seek(0, SEEK_END) here, but is
not needed nor desirable for Unix- or Posix-like systems.
Instead, just indicate that offset (before and after) is
unpredictable. */
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
_IO_off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
count = _IO_SYSWRITE (fp, data, to_do);
if (fp->_cur_column && count)
fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
fp->_IO_write_end = (fp->_mode <= 0
&& (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
? fp->_IO_buf_base : fp->_IO_buf_end);
return count;
}
그리고 우리가 원하는 것을 찾았다. count = _IO_SYSWRITE (fp, data, to_do);
_IO_SYSWRITE
점프 테이블을 통해서 __write
함수를 호출한다.
_IO_ssize_t
_IO_new_file_write (_IO_FILE *f, const void *data, _IO_ssize_t n)
{
_IO_ssize_t to_do = n;
while (to_do > 0)
{
_IO_ssize_t count = (__builtin_expect (f->_flags2
& _IO_FLAGS2_NOTCANCEL, 0)
? __write_nocancel (f->_fileno, data, to_do)
: __write (f->_fileno, data, to_do));
if (count < 0)
{
f->_flags |= _IO_ERR_SEEN;
break;
}
to_do -= count;
data = (void *) ((char *) data + count);
}
n -= to_do;
if (f->_offset >= 0)
f->_offset += n;
return n;
}
fp->_IO_write_ptr > fp->_IO_write_base
,
_IO_IS_APPENDING = 1
로 설정하면
(_f)->_IO_write_base
부터 (_f)->_IO_write_ptr-(_f)->_IO_write_base)
만큼의 데이터를 작성하게 된다.
적용
fs = FileStructure()
fs.flags = 0xFBAD0000 | 0x1000 # _IO_IS_APPENDING = 1
fs._IO_write_base = stdout
fs._IO_write_ptr = stdout + 0x8
fs._IO_write_end = stdout + 0x100
fs._lock = 0x601600 # writeable addr
fs.fileno = 1 # **stdout fileno**
p.send(bytes(fs)[:-8]) # remove vtable pointer
Troubleshooting
-
stack overflow stack pivoting 과정에서
fflush
를 호출하면서(물론 main을 결국 썼지만) 스택 오버플로우가 발생했다.0x601000
주소아래 영역은 권한이 없는데 콜 스택이 아래로 자라면서 문제가 발생했다.0x601500
주소를 사용해서 스택을 위로 올려서 해결했다. -
fflush in main vs plt 처음에는 plt를 통해 바로
fflush
를 호출했지만,mov rax, qword [obj.stdout]
과 같은 포인터 연산 과정이 없어서 plt로 호출하면 stdout이FILE*
이라고 해석해버린다. 그래서main
함수를 통해fflush
를 호출해야 한다.