House of orange 분석

Osori·2021년 3월 23일
1

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을 변조해서 원가젯을 호출하는 문제를 푼 경험은 있다고 생각하겠다.

sysmalloc 동작

일단 house of orange 기법의 (유사한) 문제들은 보통 free함수가 없다. free함수가 없는데 어떻게 leak을 할 수 있냐면, sysmalloc 함수의 동작때문이다. 우리가 탑청크의 사이즈를 적절하게 변조시킨후 top chunk보다 큰 할당을 요청하면 sbrk로 세그먼트를 확장함하면서 이전 topchunk가 free되어 unsorted bin에 들어간다.
(사이즈 변조 ex : 0x20ef1->0xef1 끝3자리가 같아야 한다. 페이지 단위인 000을 검사하기 때문 )
이렇게 free를 한 topchunk를 모종의 방법으로 leak을 하는 것 부터 시작이다.

_int_malloc 동작

아래 코드는 _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") 가 실행될 것이다.

unsorted bin attack 을 통해서 _IO_list_all 변조

일단 top chunk가 free되어있는 상황이라고 가정하자. 여기서 top->bk를 _IO_list_all-0x10 으로 세팅한 이후 할당을 요청하면 _IO_list_allmain_arena+88으로 변조된다. 보통 unsorted bin attack을 할때 요청 사이즈를 해당 청크의 사이즈와 동일하게 하는 반면, 여기서는 오류를 출력해야하기 때문에, 해당 청크보다 큰 사이즈를 요청한다. 그러면 smallbin 이나 largebin에 해당 청크가 들어가고 _IO_list_all에 main arena의 주소가 적힐 것이다.그리고 다음 반복문을 돌 때 변조된 bk를 탐색하면서 에러가날 것이다. 말이 복잡해졌는데 중요한 것을 정리하자면 다음과 같다 .

  • 재할당 기회를 잃은 청크가 small bin[4] 에 들어가도록 사이즈를 0x61로 세팅 할 것.
  • bk가 _IO_list_all-0x10로 변조되있을 것.

이 과정 이후 fflush 함수로 넘어갈 때 상황은
_IO_list_allmain_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 세팅도 해준다.

profile
해킹, 리버싱, 게임 좋아합니다

0개의 댓글