이전 _IO_FILE vtable Overwrite에 이어 특정영역의 값을 읽을 수 있는 공격
_IO_FILE Arbitrary Read 기법에 대해 정리한 글입니다.
드림핵 자료와 여러 자료를 참고해 정리했으며 다소차이점이 존재할수 있습니다.
(잘못된 부분이나 틀린 부분이 있다면 댓글로 지적해주세요.)
먼저 이전에 작성한 _IO_FILE vtable Overwrite 에서 _IO_FILE의 구조체를 분석해보며
해당 _IO_FILE 구조체를 Overwrite하는 것으로 원하는 함수를 실행시키는 것이 가능하다는 것을 보았습니다.
https://skysquirrel.tistory.com/269
[Pwn] _IO_FILE_Plus & _IO_FILE vtable Overwrite
이전 Linux File System _IO_FILE에 이어서 _IO_FILE_Plus와 _IO_FILE vtable Overwrite에 대한 정리글로 드림핵 자료위주로 정리하였습니다. https://skysquirrel.tistory.com/268 [Linux] Linux File System _IO_FILE 드림핵 로드맵을
skysquirrel.tistory.com
_IO_FILE Arbitrary Read
이번에 정리할 _IO_FILE Arbitrary Read또한 동일하게 _IO_FILE 구조체 멤버 조작을 통해
원하는 영역의 값을 출력하는 공격방식입니다.
예시코드와 glibc를 분석해보면 다음과 같습니다.
#include <stdio.h>
#include <string.h>
int main()
{
char *buf = "THIS IS TEST FILE!\0";
FILE *fp;
fp = fopen("testfile","w");
fwrite(buf, 1, strlen(buf), fp);
return 0;
}
코드에서는 "THIS IS TEST FILE!" 라는 문자열을 testfile에 작성해 저장하려고 합니다.
이전 _IO_FILE vtable Overwrite를 위해 _IO_FILE 구조체를 분석했을때는 fread 함수만을 분석했는데
fread분석했던 아래 내용을 보면
fread함수는 내부적으로 _IO_sgetn 함수를 호출하고 이는 다시
검증코드를 지나 _IO_FILE_xsgetn 을 거쳐
_IO_str_overflow()에 도달하게 됩니다.
overwrite가 아닌 read공격에서도 비슷하게 fwrite()를 호출하면 _IO_fwrite를 지나
_IO_new_file_xsputn를 거쳐 _IO_overflow()에 도달하게 됩니다.
이후 해당 함수에서 _IO_FILE 구조체를 조작할수 있는 new_do_write를 호출하게 됩니다.
(여기서 분석된 glibc는 2.27을 기준으로 하였습니다.)
흐름 분석
glibc를 보면 설명과 동일한 흐름으로 다음과 같습니다.
위와 같은 흐름을 지나 _IO_new_file_xsputn에 오게되면 해당 코드중에서
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
const char *s = (const char *) data;
_IO_size_t to_do = n;
int must_flush = 0;
_IO_size_t count = 0;
if (n <= 0)
return 0;
/* This is an optimized implementation.
If the amount to be written straddles a block boundary
(or the filebuf is unbuffered), use sys_write directly. */
/* First figure out how much space is available in the buffer. */
if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
{
count = f->_IO_buf_end - f->_IO_write_ptr;
if (count >= n)
{
const char *p;
for (p = s + n; p > s; )
{
if (*--p == '\n')
{
count = p - s + 1;
must_flush = 1;
break;
}
}
}
}
else if (f->_IO_write_end > f->_IO_write_ptr)
count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */
/* Then fill the buffer. */
if (count > 0)
{
if (count > to_do)
count = to_do;
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
s += count;
to_do -= count;
}
if (to_do + must_flush > 0)
{
_IO_size_t block_size, do_write;
/* Next flush the (full) buffer. */
if (_IO_OVERFLOW (f, EOF) == EOF)
/* If nothing else has to be written we must not signal the
caller that everything has been written. */
return to_do == 0 ? EOF : n - to_do;
/* Try to maintain alignment: write a whole number of blocks. */
block_size = f->_IO_buf_end - f->_IO_buf_base;
do_write = to_do - (block_size >= 128 ? to_do % block_size : 0);
if (do_write)
{
count = new_do_write (f, s, do_write);
to_do -= count;
if (count < do_write)
return n - to_do;
}
/* Now write out the remainder. Normally, this will fit in the
buffer, but it's somewhat messier for line-buffered files,
so we let _IO_default_xsputn handle the general case. */
if (to_do)
to_do -= _IO_default_xsputn (f, s+do_write, to_do);
}
return n - to_do;
}
_IO_oveflow()를 호출하게 됩니다.
...
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_new_file_overflow가 호출되면서 _IO_new_do_write를 호출하게 되는 것입니다.
(디버깅시 _IO_do_write라고 하는데 해당 함수를 glibc의 define을 보면 _IO_new_do_write로 확장된것을 알수 있습니다.)
_IO_new_do_write
그럼 _IO_new_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;
}
libc_hidden_ver (_IO_new_do_write, _IO_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_FILE Arbitrary Read 공격도 _IO_FILE vtable Overwrite와 마찬가지로 흐름을 따라 호출 과정을 정리하면
fwrite() -> _IO_fwrite() -> _IO_new_file_xsputn() -> _IO_new_file_overflow() -> _IO_new_do_write() 입니다.
이러한 흐름에서 _IO_FILE 구조체 멤버 조작이 가능하다면
_IO_new_do_write()를 통해 원하는 값을 출력하는 Arbitrary Read 공격으로 이어지게 할수 있는 것입니다.
하지만 해당코드가 실행되려면 조건문을 통과해야합니다.
해당 조건문들을 보면 다음과 같습니다.
if (fp->_flags & _IO_IS_APPENDING)
...
else if (fp->_IO_read_end != fp->_IO_write_base){
...}
count = _IO_SYSWRITE (fp, data, to_do);
이는 _IO_FILE 구조체 멤버중 _flasg의 값에 _IO_IS_APPENDING값이 설정되지 않도록 하고,
_IO_read_end값과 _IO_write_base값을 같게 만들면 해당 조건문을 피할 수 있습니다.
이렇게 조건문을 피하게되면 다음 구문인 _IO_SYSWRITE가 실행되면서 임의의 메모리 영역을 볼수 있게되는 것입니다.
추가로 회피해야할 조건문이 존재하는데 이는 현재 예시코드보다
더 좋은 예시코드로 워게임문제가 있어 해당 워게임 문제를 해결하면서 정리하도록 하겠습니다.
(잘못된 부분이 있거나 틀린점이 있다면 댓글로 지적해주세요.)
Reference
1. https://dreamhack.io/learn/11#55
2. https://aidencom.tistory.com/503
3. https://elixir.bootlin.com/glibc/glibc-2.27/source
'Reference > Pwnable_Study' 카테고리의 다른 글
[Pwn] House of Force (2) | 2024.01.10 |
---|---|
[Pwn] _IO_FILE Arbitrary Write (0) | 2023.07.24 |
[Pwn] _IO_FILE_Plus & _IO_FILE vtable Overwrite (0) | 2023.07.01 |
[Pwn] SROP(Sig Return Oriented Programming) (0) | 2023.05.02 |
[Pwn] gdb offset Tip (0) | 2023.04.01 |