heap exploit의 끝판왕 house of orange 이다.
이 기법을 설명한 블로그들을 보면 뭔가 엄청나게 복잡하게 설명이 되있는데,
간략하게 정리하면 free함수가 없는데, topchunk까지 overflow가 일어날 때 사용가능하며,
결과적으로는 __libc_message
함수에서 _IO_OVERFLOW
함수가 호출 될 때의 fp가 _IO_list_all
인 것을 이용해서 fp를 변조해 vtable을 변조시켜서 system("/bin/sh")
를 실행시킬 수 있다. 그리고 개발자들의 발빠른 대처 덕분에 ubuntu 18.04 이상부터는 이 기법의 사용이 불가능하다.
사실 별로 어렵지 않다. 일단 기본적으로 vtable을 변조해서 원가젯을 호출하는 문제를 푼 경험은 있다고 생각하겠다.
일단 house of orange 기법의 (유사한) 문제들은 보통 free
함수가 없다. free
함수가 없는데 어떻게 leak을 할 수 있냐면, sysmalloc
함수의 동작때문이다. 우리가 탑청크의 사이즈를 적절하게 변조시킨후 top chunk보다 큰 할당을 요청하면 sbrk로 세그먼트를 확장함하면서 이전 topchunk가 free되어 unsorted bin
에 들어간다.
(사이즈 변조 ex : 0x20ef1->0xef1 끝3자리가 같아야 한다. 페이지 단위인 000을 검사하기 때문 )
이렇게 free를 한 topchunk를 모종의 방법으로 leak을 하는 것 부터 시작이다.
아래 코드는 _int_malloc
함수에서 unsorted bin
에 있는 청크들을 하나씩 꺼내서 검사하는 부분이다. 만약 사용자가 요청한 조건에 맞지 않다면 해당 청크들은 재사용 기회를 잃고small bin
또는 larget bin
으로 들어가게 될 것이다. 여기서 malloc_printerr
함수를 자세히 보도록 하겠다.
for (;; )
{
int iters = 0;
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{
bck = victim->bk;
size = chunksize (victim);
mchunkptr next = chunk_at_offset (victim, size);
if (__glibc_unlikely (size <= 2 * SIZE_SZ)
|| __glibc_unlikely (size > av->system_mem))
malloc_printerr ("malloc(): invalid size (unsorted)");
malloc_printerr
함수는 __libc_message(1,"%s\n",str)
을 호출한다. 해당 함수에서는 abort()
함수가 호출되고 abort
함수에서는 fflush
함수가 호출되는데 이는 _IO_flush_all_lockp (0)
함수이다. _IO_flush_all_lockp (0)
함수의 내부 루틴을 보자.
fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
#endif
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;
if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;
if (last_stamp != _IO_list_all_stamp)
{
/* Something was added to the list. Start all over again. */
fp = (_IO_FILE *) _IO_list_all;
last_stamp = _IO_list_all_stamp;
}
else
fp = fp->_chain;
}
위 코드에서 우리는 fp->_chain
을 탐색하며 해당 조건들을 검색하는 것을 볼 수 있다. 여기서 _IO_OVERFLOW
함수를 오출해주어야 하고 그 조건은 아래 코드와 같다.
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
만약 우리가 fp에 fake file structer 를 구성할 수 있다면 이 조건을 쉽게 우회하여 _IO_OVERFLOW
함수를 호출 할 수 있을 것이다. 또한 fake file structer 를 구성한다는 것은 우리가 vtable 또한 마음대로 할 수 있으니 vtable에서 _IO_OVERFLOW
포지션인 vtable에서 +0x18 에 system
함수를 적어주면 IO_OVERFLOW
대신 system
이 호출될 것이다. 또한 fp가 overflow
의 인자인 것을 이용해서
_IO_FILE
구조체의 첫번째 변수는 flag
이므로 이 flag 대신 "/bin/sh" 문자열을 적어놓으면 system("/bin/sh")
가 실행될 것이다.
일단 top chunk가 free되어있는 상황이라고 가정하자. 여기서 top->bk를 _IO_list_all-0x10
으로 세팅한 이후 할당을 요청하면 _IO_list_all
은 main_arena+88
으로 변조된다. 보통 unsorted bin attack을 할때 요청 사이즈를 해당 청크의 사이즈와 동일하게 하는 반면, 여기서는 오류를 출력해야하기 때문에, 해당 청크보다 큰 사이즈를 요청한다. 그러면 smallbin 이나 largebin에 해당 청크가 들어가고 _IO_list_all
에 main arena의 주소가 적힐 것이다.그리고 다음 반복문을 돌 때 변조된 bk를 탐색하면서 에러가날 것이다. 말이 복잡해졌는데 중요한 것을 정리하자면 다음과 같다 .
_IO_list_all-0x10
로 변조되있을 것. 이 과정 이후 fflush
함수로 넘어갈 때 상황은
_IO_list_all
이 main_arena+88
인데, 파일 구조체에서 _chain
에 해당하는 부분이 main_arena+88
에서는 0x61 사이즈의 청크를 관리하는 smallbin이다. 그리고 여기에는 top chunk가 들어가 있을 것이다! (상당히 재미있는 부분이다. chain으로 구성되있는 부분으로 free 된 topchunk를 참조하는 부분이 핵심 인 것 같다.)
따라서 이제 top chunk에 fake file structer 을 구성해주면 된다.
topchunk가 unsorted bin에 들어간 상태일 때
아래 그림과 같이 topchunk를 세팅해준 뒤 0x61 이상의 사이즈를 malloc으로 요청하면 쉘이 따진다. 설명이 어려웠지만 결론적으로는 쉽다
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
를 우회하기 위해서 write_ptr=3 , write_base=2, mode=0 으로 세팅해준다. 그리고 vtable 세팅도 해준다.