what is _IO_buf_base & vulnerability
우리가 흔히 FILE 구조체가
struct _IO_FILE {
int _flags; // 0x00
char* _IO_read_ptr; // 0x08
char* _IO_read_end; // 0x10
char* _IO_read_base; // 0x18
char* _IO_write_base; // 0x20
char* _IO_write_ptr; // 0x28
char* _IO_write_end; // 0x30
char* _IO_buf_base; // 0x38
char* _IO_buf_end; // 0x40
/* skip some save/backups pointers ~0x60 */
struct _IO_marker *_markers; // ~0x60
struct _IO_FILE* _chain; // ~0x68
int _fileno; // ~0x70
int _flags2; // ~0x74
__off_t _old_offset; // ~0x78
unsigned short _cur_column; // ~0x80
signed char _vtable_offset;// ~0x88
char _shortbuf[1]; // ~0x89
_IO_lock_t *_lock; // ~0x90 (approx)
...
};
struct _IO_FILE_plus {
struct _IO_FILE file;
const struct _IO_jump_t *vtable; // at offset ≈0xd8
};다음과 같이 생겨 먹은것은 이제 흔히 알것이다. 우리는 오늘 그중
char* _IO_read_ptr; // 0x08 char* _IO_read_end; // 0x10 char* _IO_read_base; // 0x18 char* _IO_write_base; // 0x20 char* _IO_write_ptr; // 0x28 char* _IO_write_end; // 0x30 char* _IO_buf_base; // 0x38 char* _IO_buf_end; // 0x40
얘내들에 대해 알아볼면서 한다. 특히 _IO_buf_base 얘를 중점으로 알아볼것이다.
_IO_buf_base 얘는 한 프로세스에서 전체적인 하나의 내부 버퍼의 시작을 의미한다. 반대로
_IO_buf_end 얘는 한 프로세스에서 전체적인 하나의 내부 버퍼의 끝을 의미한다.
우리가 stdin이나 stdout , stderr 혹은 그 외의 파일 구조체를 사용하는 파일들은 모두 고유한
내부 버퍼를 가지고 있다. ( 각각 다른 위치의 ) 그리고 file function ( fread , fgets , fwrite , printf ) 들은 이런 내부버퍼를 사용해서 buf에 쓰거나 file 구조체에 쓴다. file function도 read() 함수과와 write() 함수과가 존재한다.
read() 함수과 → stdin 같은 파일 입력을 통해서 프로세스 메모리 내의 buf에 저장하기 ( ex. fread,fgets )
write() 함수과 → 프로세스 메모리 내의 buf 에서 읽어서 stdout와 같은 파일 출력을 통해( ex. fwrite ,fputs , printf , fprintf )
_IO_SYSWRITE , _IO_SYSREAD , _IO_overflow , _IO_underflow , _IO_flush diff 이글을 읽고 오면 편하다.
read 함수과
fgets 함수로 예시를 들겠다.
https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/iofgets.c#L70
fgets 함수가 실행되면 실질적으로
count = _IO_getline (fp, buf, n - 1, '\n', 1);_IO_getline 함수가 실행된다.
여기서 ‘\n’ 이 궁금할수도 있어서 다루어보았다.
_IO_getline 에서는 무한 While 문을 돌면서
while (n != 0)
{
_IO_ssize_t len = fp->_IO_read_end - fp->_IO_read_ptr;
if (len <= 0)
{
int c = __uflow (fp);
if (c == EOF)
{
if (eof)
*eof = c;
...다음과 같은 코드가 실행되는데
fp->_IO_read_end - fp->_IO_read_ptr;
이 말은 다 현재 버퍼에서 읽은 것을 다 읽었으면~ 이라는 의미이다. 혹은, 맨처음에는
_IO_read_end == _IO_read_ptr 이므로 자연스럽게 if (len <= 0) 에 충족되어
__uflow 함수 즉, 언더플로우 함수가 실행된다. 해당 함수는 vtable에 있는 함수이다.
glibc with vtable 해당글을 참고하면 이해가 쉽다.
__uflow 함수는 glibc 상 _IO_new_file_underflow 함수이고
해당 함수에서는,
fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;
fp->_IO_read_end = fp->_IO_buf_base;
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
= fp->_IO_buf_base;
count = _IO_SYSREAD (fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base);기본적인 _IO_read_base _IO_read_ptr _IO_read_end 값을 내부버퍼 시작 (_IO_buf_base) 값으로 초기화 시키고 _IO_SYSREAD 를 통해 내부적으로
read ( fp→fileno , fp->_IO_buf_base , fp->_IO_buf_end - fp->_IO_buf_base )를 실행시켜서
표준 입력으로부터 _IO_buf_base에 입력을 받는다.
fp->_IO_read_end += count;그리고 받은 바이트 수 많큼 _IO_read_end 값을 변경해준다.
그리고 이제 다시 _IO_getline 함수로 돌아와서
char *t;
if ((_IO_size_t) len >= n)
len = n;
t = (char *) memchr ((void *) fp->_IO_read_ptr, delim, len);memchr 함수로 delim 즉, “\n” 까지 자른뒤
memcpy ((void *) ptr, (void *) fp->_IO_read_ptr, len);
fp->_IO_read_ptr += len;
ptr += lenbuf에 fp->_IO_read_ptr 부터 len 만큼의 바이트를 memcpy 하면서 붙여넣게 된다.
write 함수과
fwrite 함수를 예시로 들겠다.
https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/iofwrite.c#L54
_IO_fwrite (const void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
{
_IO_size_t request = size * count;
_IO_size_t written = 0;
CHECK_FILE (fp, 0);
if (request == 0)
return 0;
_IO_acquire_lock (fp);
if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)
written = _IO_sputn (fp, (const char *) buf, request);
_IO_release_lock (fp);
/* We have written all of the input in case the return value indicates
this or EOF is returned. The latter is a special case where we
simply did not manage to flush the buffer. But the data is in the
buffer and therefore written as far as fwrite is concerned. */
if (written == request || written == EOF)
return count;
else
return written / size;
}_IO_sputn 함수로 buf 인자를 가지고 들어간다.
_IO_new_file_xsputn 함수에서는 buf를
const char *s = (const char *) data;s로 넣은뒤
memcpy (f->_IO_write_ptr, s, count);다음과 같이 buf에서 _IO_write_ptr 위치에 복사를 한고
_IO_new_file_overflow 함수에서는
f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
f->_IO_write_ptr = f->_IO_read_ptr;
f->_IO_write_base = f->_IO_write_ptr;
f->_IO_write_end = f->_IO_buf_end;
f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;다음과 같이 기본적인 설정을 한뒤에
_IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);다음과 같은 함수를 실행시킨다.
# define _IO_new_do_write _IO_do_write_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;
}new_do_write 함수를 실행하고
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;
}_IO_SYSWRITE 을 통해 통해 write( fp→fileno , data , to_do ) 로 실질적으로 표준 출력인 터미널에 write를 진행한다.
따라서 다시 돌아와서_IO_new_file_overflow 함수에서 언제 flush가 되어서 출력이 되는지 확인해보면
if (ch == EOF)
return _IO_do_write(...);char이 eof 일때 버퍼에 남은 데이터를 write 하고
if (f->_IO_write_ptr == f->_IO_buf_end)
if (_IO_do_flush(f) == EOF)
return EOF;_IO_write_ptr가 계속 커지다. _IO_buf_end 까지 다다르면 내부버퍼가 꽉 찬거 이므로 _IO_do_flush 를 진행한다.
if ((_IO_LINE_BUF && ch == '\n') || _IO_UNBUFFERED)
_IO_do_write(...);_IO_LINE_BUF → 줄다위 버퍼링인데 char이 “\n” 일때도 flush 를 진행한다.
_IO_buf_base vulnerability
이제 취약점을 알아볼것인데 예를 들어 stdin 파일 구조체의 _IO_buf_base 부분을 덮어쓸 수 있으면,
위에 이 부분에서
fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;
fp->_IO_read_end = fp->_IO_buf_base;
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
= fp->_IO_buf_base;
count = _IO_SYSREAD (fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base);_IO_SYSREAD 의 fp->_IO_buf_base 이 부분을 컨트롤 할 수 있다. 이는
read(0, _IO_buf_base , size) 를 의미하므로 _IO_buf_base 위치에 Arbitrary writable이 가능하게 된다.