Search

House of Orange 분석

Tag
fsop
Create time
2020/04/06
목차

1. 개요

우선 ' House of Orange ' 이름에는 큰 의미없다. 그냥 hitcon 2016 문제 이름인데, 여기서 사용된 기법을 저렇게 이름지은거라고 한다.
House of Orange는 _int_malloc 함수 내부에서 메모리 손상이 발생했을 때, 에러를 출력하게 되는데, 이 과정을 이용하여 정상적인 에러 메시지 출력이 아닌, 원하는 함수가 동작하게 만드는 공격이다.
그럼 _int_malloc 소스코드를 보면서 어떠한 원리인지 자세하게 파악해보자.
참고로 해당 소스코드는 glibc 2.23 2.23 기준이다.

2. _int_malloc() 함수

_int_malloc() 함수를 살펴보자. malloc 요청이 들어오면, 먼저 bin에 요청 사이즈에 맞는 free된 청크들이 존재하는지 검사하게 된다. 요청 사이즈가 fast chunk 사이즈이면 fastbin, small chunk 사이즈면 smallbin, large chunk 사이즈이면 largebin을 뒤진다.
만약 요청 사이즈에 맞는 bin을 뒤졌을때, 재할당해줄 수 있는 청크가 없다면, unsorted bin을 뒤지게 된다. 위 코드가 바로 unsorted bin을 뒤지기 시작하는 시작부분이다. while문을 돌면서 unsorted bin에 현재 청크가 모두 빌때 까지 반복한다.
victim = unsorted_chunks (av)->bk) != unsorted_chunks (av) 이 조건이 바로 unsorted bin이 비여있는지 검사하는 로직이다. fast chunk size 이상 청크가 free되게 되면 우선적으로 unsorted bin에 먼저 들어오게되고, fd, bk에 main_arena+88 주소가 들어간다.
이해하기 쉽게 small chunk size 하나가 현재 free된 상태이다(해당 청크 아래에는 아직 free되지 않은 청크들이 존재하기 때문에 Top 청크와 병합은 안일어남).
fd, bk에 main_arena+88의 주소가 들어가있는데, 이는 현재 main_arena 구조체중에서 top 위치이다.
victim = unsorted_chunks (av)->bk) != unsorted_chunks (av) 이 조건을 다시 봐보자. 현재 unsorted bin에 free된 청크가 하나 존재한다.
unsorted_chunks(av) = 0x7ffff7dd178
unsorted_chunks(av)→bk = 0x7ffff7dd178+0x18 = 0x7ffff7dd1b90에 들어있는 값 = 0x602000
두 값이 다르다. 즉 현재 unsorted bin에 free 청크가 존재한다는 뜻이므로, 해당 청크를 재할당해줄 수 있는지 확인하면서 쭉 진행이 된다. 이제 while 그 다음 라인을 확인해보자
unsorted bin은 FIFO이기 때문에 victim에 가장 먼저 unsorted bin에 들어온 청크 주소를 bck에 저장한다( victim→bk ). 그리고 __builtin_expect를 이용해 에러검사를 한다.
이 에러체크 로직에서 만약 에러가 발생하게 되면, malloc_printerr함수를 호출하여 에러를 출력하게되는데, house of orange는 바로 이 부분을 이용하여 공격하는 기술이다!!!. 그렇다면 이제 malloc_printerr함수를 살펴보자

3. malloc_printerr() 함수

malloc_printerr()에서 출력되는 문장의 형태는 첫번재 인자 값(action)에 따라 달라진다.
10 ~ 11 라인
(action & 5) 값이 5와 같다면 str 변수에 저장된 문자열의 출력을 요청함
12 ~ 23 라인
(action & 1) 값이 설정되어 있다면 "*** Error in `%s': %s: 0x%s ***\n" 이와 같은 형태로 에러 메시지를 출력함. 첫번째 문자열은 프로그램의 경로, 두번째 문자열은 에러 메시지, 세번째 문자열은 메모리의 주소이다
24 ~ 25 라인
action & 2 값이 설정되어 있다면, abort() 함수를 호출한다
25라인으로 abort가 바로 호출되지 않는 이상, __libc_message 함수를 호출하여 에러 메시지 내용을 실제 출력하게 된다. 이 함수를 자세히 살펴보자.

4. __libc_message() 함수

이 함수에서 실제 에러 내용이 출력된다. 위에 ... 부분이 Backtrace, Memory map을 출력하는 부분이라고 하는데 이건 잘 모르겠다. 어쩃든 그리고 이 프로세스를 강제 종료시키기 위해 abort()를 호출한다. abort() 함수를 봐보자

5. abort() 함수

79 ~ 85 라인 : SIGABRT를 unlock 하는 부분
89 ~ 93 라인 : 모든 스트림을 다 비우기 위해 fflush 함수 호출. 해당 함수는 매크로로 설정됨
#define fflush(s) _IO_flush_all_lockp (0)
Markdown
복사
따라서 fflush를 호출하면 실제로는 _IO_fflush_all_lockp 함수가 호출된다. 해당 함수를 또 살펴보자. (참고로 abort.c 소스코드가 수정되어 이제는 해당 함수에서 fflush 함수를 호출하지 않는다고 한다.)

6. _IO_fflush_all_lockp() 함수

_IO_list_all 구조체 변수를 FILE* 구조체 자료형으로 캐스팅하여 fp 에 저장한다. for 문을 돌면서 fp가 NULL일때까지 반복을 하게 되는데 fp→_chain으로 다음 반복문을 계속 돌게 된다. 이는 FILE 포인터가 리스트 처럼 계속 연결되어 원하는 동작을 하게끔하는것으로 보인다.
여기서 중요한 것은 114 라인의 조건문들과 _IO_OVERFLOW함수이다. 해당 조건을 정리해보자
1.
fp →_mode ≤ 0 && ( fp → _IO_write_ptr > fp → _IO_write_base )
2.
_IO_vtable_offset(fp) == 0 && fp →_mode > 0 &&
( fp → _wide_data → _IO_write_ptr > fp → _wide_data→ _IO_write_base )
3.
_IO_OVERFLOW( fp, EOF) == EOF
우선 1번 조건과 2번 조건이 or( || ) 로 묶여있으므로 둘중 하나만 만족해도 된다. 둘 중 하나라도 만족하면 마지막 3번 조건인 _IO_OVERFLOW 함수의 반환값이 EOF인지 확인을 한다. 여기서 중요하다. 우리가 원하는 동작은 해당 함수에서 발생하므로 이 함수가 호출되려면 1번 or 2번 조건이 만족해야 3번 조건을 검사할수 있는 것이다
만약 1번, 2번 조건이 하나라도 만족하지 못한다면, 3번 조건은 확인조자 할 필요가 없기 때문에 호출이 안됀다. 어쨋든 _IO_OVERFLOW 함수는 매크로 형태로 정의되어 있다
/* The 'overflow' hook flushes the buffer. The second argument is a character, or EOF. It matches the streambuf::overflow virtual function. */ typedef int (*_IO_overflow_t) (_IO_FILE *, int); #define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH) ... #define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
Markdown
복사
_IO_OVERFLOW 를 호출하게 되면 실제로 JUMP1 함수가 호출된다. JUMP1 함수는 첫번째 인자로 fp, 두번째 인자로 문자 혹은 EOF를 전달받는다고 한다. 즉, __overflow, fp,ch를 인자로 하여 JUMP1가 호출된다(여기서 fp는 아까 위에서 _IO_list_all 구조체 변수를 FILE* 포인터로 캐스팅한 값이다.)
JUMP1 역시 매크로 형태로 정의되어 있다. 해당 함수는, FP는 THIS로 표시하여 THIS, X1을 인자로 _IO_JUMPS_FUNC(THIS)->FUNC 을 호출한다. 이 함수는 THIS, 즉 fp가 가지고 있는 vtable의 포인터를 반환하게 되고, 여기서 아까 __overflow 가 FUNC에 들어가 있기 때문에 vtable에서 __overflow 함수를 찾아서 호출하게 된다.
#if _IO_JUMPS_OFFSET # define _IO_JUMPS_FUNC(THIS) \ (IO_validate_vtable \ (*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \ + (THIS)->_vtable_offset))) # define _IO_vtable_offset(THIS) (THIS)->_vtable_offset
Markdown
복사
_IO_JUMPS_FUNC 매크로 함수를 살펴보면, 다음과 같이 구성되어 있다. 결론적으로 위에서 말한 것처럼 해당 함수가 호출되면, fp가 가지고 있는 vtable 주소를 반환하게 되고, vtable→FUNC을 호출하여 실제 __overflow 함수가 호출되는 것이다
이 과정이 이해가 잘 안갈수도 있는데 _IO_list_all 구조체 변수가 어떻게 구성되어있는지를 알면 쉽게 이해할 수 있다.

6.1 _IO_list_all 구조

extern struct _IO_FILE_plus *_IO_list_all;
C
복사
_IO_list_all 포인터 변수는 _IO_FILE_plus 구조체 형태로 구성되어 있다.
struct _IO_FILE_plus { _IO_FILE file; const struct _IO_jump_t *vtable; };
C
복사
_IO_FILE_plus 구조체는 _IO_FILE 구조체를 가지는 file 멤버변수와 _IO_jump_t 구조체를 가지는 vtable 포인터 변수가 존재한다. _IO_FILE 구조체는 파일의 입력, 출력에 관련된 모든 포인터 정보들을 가지게 된다.
struct _IO_FILE { int _flags; /* High-order word is _IO_MAGIC; rest is flags. */ #define _IO_file_flags _flags /* 뒤에 나오는 포인터드들은 C++의 streambuf 규약에 대응한다. */ /* Note: Tk는 _IO_read_ptr와 _IO_read_end 값을 바로 사용한다. */ char* _IO_read_ptr; /* 현재 읽기 포인터 */ char* _IO_read_end; /* 읽을 영역의 끝. */ char* _IO_read_base; /* 읽을 영역의 처음. */ char* _IO_write_base; /* 출력할 영역의 시작부분. */ char* _IO_write_ptr; /* 현재 출력 포인터. */ char* _IO_write_end; /* 출력 영역의 끝부분. */ char* _IO_buf_base; /* 예약된 영역의 처음 */ char* _IO_buf_end; /* 예약된 영역의 끝. */ /* 뒤에 오는 값들은 백업과 되돌리기를 위해 사용된다. */ char *_IO_save_base; /* 현재가 아닌 읽을 영역의 포인터 */ char *_IO_backup_base; /* 백업 영역의 첫번째 유효한 문자의 포인터 */ char *_IO_save_end; /* 현재가 아닌 읽을 영역의 끝 */ struct _IO_marker *_markers; struct _IO_FILE *_chain; int _fileno; #if 0 int _blksize; #else int _flags2; #endif _IO_off_t _old_offset; /* 이것은 오프셋으로 쓰이지만 너무 작다. */ #define __HAVE_COLUMN /* 임시 */ /* 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 }; struct _IO_FILE_complete { struct _IO_FILE _file; #endif __off64_t _offset; /* Wide character stream stuff. */ struct _IO_codecvt *_codecvt; struct _IO_wide_data *_wide_data; struct _IO_FILE *_freeres_list; void *_freeres_buf; size_t __pad5; int _mode; /* 우리가 문제에 직면하지 않게 확실히 한다. */ char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)]; };
C
복사
노란 부분이 우리가 이따가 중요하게 봐야할 부분이다. 지금은 _IO_FILE 구조체가 이렇게 생겼구나 정도만 이해하면 될것이다. 이번에는 _IO_FILE_plus 구조체의 두번째 멤버변수인 _IO_jump_t 구조체를 살펴보자
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 };
C
복사
_IO_jump_t 구조체는 JUMP_FIELD 매크로 형태의 함수를 가지고 있는데, 여기서 원하는 오프셋에 있는 JUMP_FIELD 함수를 호출하게 된다. 우리같은 경우 노란색 부분이 호출되므로써 __overflow함수가 실제로 호출되는 것이다.
자 그럼 정리해보자. _IO_fflush_all_lockp() 함수에서 조건문 안에 들어있던 _IO_OVERFLOW 함수가 호출되고, 실제 __overflow 함수가 호출되는 과정을 그림으로 이해해보자.
위 그림에서 2번을 통해 __overflow 함수가 실제 호출되는데 우리가 목표하는 지점은 바로 여기이다. vtable에서 __overflow 함수를 호출하기 위해 JUMP_FILED를 호출하는데, 이 부분을 one_gadget이나 system 함수로 덮어버리면 된다.

7. 결론

이 기법의 핵심은 FILE stream의 구조를 이해하고 있으면 그렇게 어렵지 않는 방법이다. 하지만 이런 방법을 알아낸거 자체가 너무 대단하다..
쨋든 _IO_FILE_plus 가짜 구조체를 만들고, _IO_list_all 포인터 변수에 우리가 만든 가짜 구조체 변수 주소를 넣을수만 있다면, 위에서 설명한 FILE stream 내부 동작 과정이, 정상적인 데이터가 아닌, 우리가 fake로 만든 구조체를 기준으로 동작하고, vtable에서 실제 함수가 들어있는 오프셋에 system함수를 넣으면 쉘이 떨어질 것으로 예상된다.

8. 참고자료