kernel
ctf
pwn
この記事は,CTF Advent Calendar 2018 の5日目の記事です.
4日目は私の「最近のLinuxカーネルexploit問に対するテクニック集1」でした.
今回は
について書きます.残念ながら新規性はありません.日本にはこういった記事はあまりありませんが,海外では普通に公開されている内容です.
またLinux以外のOS(例えばWindows)には通用しないと思いますので,ご注意ください.
大前提として,Linuxのカーネルexploit(権限昇格)についてある程度知っている方向けの内容となります.
全く知らないよ!って方は,nokia31337の解説資料で図を入れて解説したので,先にそちらで精進したほうが良いかと思います(2014年当時のARMにおけるLinuxカーネルExploit問です).
また私はLinuxカーネルのソースを全て読んだことはなく,必要に応じてチラ見した程度なので,そこまで深い知識は持っていません.もし記事の内容に間違いがありましたら,優しく教えてください.マサカリ投げるのは禁止!
userfaultfd
について本日焦点を当てるのは,userfaultfd()
と呼ばれるシステムコールです.
このシステムコールは,ユーザランドのメモリがページフォルトを起こしたタイミングで,登録しておいたコールバック関数を呼びだす,というものです.監視対象のメモリ範囲を決めておくための面倒な手順があったり,存在しないページの監視はできない(SIGSEGV
を起こすようなアクセスには対処できない)といった制約は有りますが,ページフォールトをユーザランドでハンドリングできる,という点が強みです.
userfaultfd
の使い方userfaultfd()
の使い方の参考までに,まずはコードを貼っておきましょう.簡単のため,エラーハンドリングは全て省略しています.
雰囲気で分かると思いますが,5つのメモリページ(0x1000バイト単位)を監視登録しておき,その各ページに初めて触った瞬間,コールバックがページ内を初期化している,という感じのコードですね.
さて,続いてはこれをカーネルexploitにどう使うかの話をしましょう.
userfaultfd
の悪用userfaultfd
は,カーネルexploitと相性がよく,知っておくと便利なので紹介します.ちなみに元ネタはこちらです.
このテクニックは,一言で言ってしまうと,「Race Conditionの成功率を上げるためのテクニック」です.これ単体で何かできるわけではありません.また,SMAPが有効な環境下では多分使えません.
カーネルのバグ自体は別にあって,任意の構造体を書き換えられるといったケースを考えます.以下は簡単なコードです.
vulnerable()
が呼ばれると,copy_from_user()
によりBOFが発生します.buf
の直下にあるp
は,BOFにより任意の場所を指せますから,例えばユーザランドを指すようにできます(実際は,スタック上の並びとしては配列のほうが下に来るような気もしますけど,これ解説用のコードなので突っ込まないでください).
しかしp
がユーザランドを指してはいるものの,ちゃんと初期化があります.19行目でp->func_ptr()
を呼び出していますが,ちゃんと17行目の&some_func
で初期化されています.つまりコード上,13行目のBOF時にp
の上書きでp->func_ptr
の値をユーザが指定したとしても,17行目で正しい値に書き戻されてしまうため,ユーザが任意に指定できる訳ではないように見えます.
しかし,こういったコードにはごく短い間ですが,Race Conditionが存在します.
例えばユーザランドのスレッドAが,このコードをシステムコール経由などで呼び出した際,運良く2行目~4行目の間でユーザランドへのコンテキストスイッチが発生し,スレッドAと同じメモリ空間を持つスレッドBがp->func_ptr
の指すアドレスを書き換えれば,再度コンテキストスイッチによりカーネルが再開したタイミングで,p->func_ptr
は書き換わっていることになり,RIPを奪うことができます.
しかし,このようなRace Windowの短いコンテキストスイッチが上手く行くことは珍しく,普通は何度もトライする必要があります.バグのあるカーネルのコードを何度も繰り返し呼び出せるような上記のケースではあまり問題になりませんが,一度失敗したらOSを再起動してやり直し,といった条件のバグの場合は無理ゲーになります.こういう時,Race Conditionを安定させるテクニックがあると便利です.
userfaultfd
を使ったテクニックは,まさに上記のようなケースで有効です.userfaultfd
は,ページフォールトを起こしたのがカーネルであっても,ユーザランドのコールバック関数を呼びだしてくれる,といった特徴があります.
カーネルで起きたページフォールトをハンドルした際,メモリを修正して再開すれば,Race Conditionの信頼度を限りなく100%に近づけることができるようになります.テクニックのキモは,p->func_ptr
とp->member
の間にちょうどページ境界が位置するようにしておき,p->member
へのアクセスでページフォールトを強制的に発生させることです.
これにより,p->func_ptr
が呼び出される前に,確実にユーザランドでp->func_ptr
を修正することができます.
実際のexploit例は,元ネタのリンクを参照してみてください.カーネルのheap上で,このテクニックを使っているのがわかると思います.
最近,こんな記事もありました.
こちらで紹介されているテクニックは,setxattr
とuserfaultfd
を使ったテクニックです.以下の3拍子が揃った,素晴らしいテクニックだと思います.
kmalloc
で確保した領域の先頭から書込可能デメリットは,プロセス(正確にはスレッド)毎に1つしかそのようなオブジェクトを作成出来ない点ぐらいですかね.
ざっくり説明しておきましょう.
setxattr
システムコールの第3引数,第4引数を利用してカーネルに任意サイズの任意データを渡すと,カーネル内部ではkmalloc()
で割り当てた領域に対してstrncpy_from_user()
をします.
ユーザ空間からカーネル空間へコピーをしている途中でユーザ空間のページ境界に差し掛かり,userfaultfd
の監視対象領域を初めて触るとページフォルトが発生するため,ユーザランドのコールバックハンドラがページフォルトをハンドリングします.
ユーザランドのページフォルトコールバックハンドラが無限ループに入るなどにより,意図的にカーネルへ復帰させなければ,そのまま永久にカーネル側の処理は再開しない,というテクニックです.
今回はuserfaultfd()
システムコールの悪用について紹介しました.有用なシステムコールも,使い方によってはexploitの助けとなってしまう,というお話でした.海外ではこういうテクニックの情報がガンガン公開されているようで,閉鎖的な日本とは違うなぁと感じたりします.
明日も私が記事を担当します.カーネル内の使える関数テーブルの話(最近のLinuxカーネルexploit問に対するテクニック集3)です.良ければ見てくださいね.