前章ではHolsteinモジュールのHeap Overflowを悪用して権限昇格をしました。またもやHolsteinモジュールの開発者は脆弱性を修正し、Holstein v3を公開しました。本章では、改善されたHolsteinモジュールv3をexploitしていきます。
パッチの解析と脆弱性の調査
まずはHolstein v3をダウンロードしてください。
v2との差分は主に2点あります。まず、open
でのバッファ確保時にkzalloc
が使われています。
1 | g_buf = kzalloc(BUFFER_SIZE, GFP_KERNEL); |
kzalloc
はkmalloc
と同じくカーネルのヒープから領域を確保しますが、その後内容が0で埋められるという点が違います。つまり、malloc
に対するcalloc
のような位置付けの関数がkzalloc
です。
次に、read
とwrite
においてHeap Overflowが起きないようにサイズチェックがあります。
1 | static ssize_t module_read(struct file *file, |
したがって、今回のカーネルモジュールではHeap Overflowが起こせません。
ここでclose
の実装を見てみましょう。
1 | static int module_close(struct inode *inode, struct file *file) |
g_buf
が不要になったのでkfree
で解放していますが、g_buf
にはまだポインタが入ったままです。もしclose
した後にg_buf
を使えたら、Use-after-Freeが起きます。
読者の中には「でもclose
したら、そのfdに対してはread
もwrite
もできないからUse-after-Freeは起きない」と考えた方もいることでしょう。たしかにその通りですが、ここでカーネル空間で動作するプログラムの特徴を思い出してみましょう。
カーネル空間では、同じリソースを複数のプログラムが共有できます。Holsteinモジュールも、1プログラムだけがopen
できるのではなく、複数のプログラム(あるいは1つのプログラム)が複数回open
できます。では、もし次のような使い方をしたらどうなるでしょうか。
1 | int fd1 = open("/dev/holstein", O_RDWR); |
最初のopen
でg_buf
が確保されますが、次にまたopen
するため、g_buf
は新しいバッファで置き換えられます。(古いg_buf
は解放されないまま残り、メモリリークが起きます。)次にfd1
をclose
するため、ここでg_buf
が解放されます。close
した段階でfd1
は使えなくなりますが、fd2
はまだ有効なので、fd2
に対して読み書きができます。すると、既に解放したはずのg_buf
が操作できてしまい、Use-after-Freeが発生することが分かります。
このように、カーネル空間のプログラムは複数のプログラムにリソースが共有されるという点に注意して設計しないと、簡単に脆弱性が生まれてしまいます。
closeする時にポインタをNULLで消したり、openする時にg_bufが確保済みなら失敗するような設計にすれば、少なくとも今回のような簡単な脆弱性は防げたね。
本当にそれだけで十分かは次の章で調べるよ。
KASLRの回避
手始めにカーネルのベースアドレスとg_buf
のアドレスをリークしてみましょう。
脆弱性がUse-after-Freeになっただけで、今回もバッファサイズが0x400なのでtty_struct
が使えます。
kROPの実現
これでROPができる状態になりました。偽のtty_operations
を用意してROP chainにstack pivotするだけです。
しかし、前回と違いUse-after-Freeですので、今使える領域がtty_struct
と被っています。当然ioctl
などでtty_operations
を使うとき、tty_struct
にも参照されない変数がたくさんあり、そこをROP chainの領域や偽のtty_operations
として使っても構いません。ただ、これから攻撃に使おうとしている構造体の大部分を破壊してしまうのは後々意図しないバグを生み出してしまう可能性がある上、ROP chainのサイズや構造に大幅な制限が加わってしまうこともあります。なるべくtty_struct
とROP chainは別の領域に確保したいです。
そこで、今回は2回目のUse-after-Freeを起こします。といってもg_buf
は1つなので、まずアドレスが分かっている今のg_buf
にROP chainと偽のtty_operations
を書き込みます。次に別でUse-after-Freeを起こし、そちらのtty_struct
の関数テーブルを書き換えます。こうすればtty_struct
の関数テーブルのみを書き換えるので、安定したexploitが実現できます。
1 | // ROP chain |
権限昇格できていれば成功です。このexploitはここからダウンロードできます。
このように、Heap OverflowやUse-after-Freeといった脆弱性は、カーネル空間では多くの場合ユーザー空間の同じ脆弱性よりも簡単に攻撃可能です。
これはカーネルのヒープが共有されており、関数ポインタなどを持ついろんな構造体を攻撃に利用できるからです。逆に言えば、Heap BOFやUAFが起きるオブジェクトと同じサイズ帯で悪用できる構造体を見つけられなければ、exploitは困難になります。
おまけ:RIP制御とSMEPの回避
今回はすべてのセキュリティ機構を回避しました。
前章でも少しだけ話が出ましたが、SMAPが無効でSMEPが有効なときは今までと少し違う簡単な手法が使えます。RIP制御が実現できたとき、次のようなgadgetを使うとどうなるでしょうか。
1 | 0xffffffff81516264: mov esp, 0x39000000; ret; |
あらかじめユーザー空間の0x39000000をmmapで確保してROP chainを書き込んでおき、このgadgetを呼び出すとstack pivotとしてユーザー空間に設置したROP chainが走ります。つまり、この場合カーネル空間にROP chainを置いたり、そのヒープ領域のアドレスを取得したりといった面倒事が不要になります。
注意として、RSPは8バイト単位でアラインされたアドレスになるようにしてください。スタックポインタがアラインされていないと例外を発生するような命令が実行されてしまうとクラッシュしてしまうからです。
また、commit_creds
やprepare_kernel_cred
等の関数を呼び出す際はスタックが消費されるので、実際には0x39000000より前(0x8000バイト程度余裕を持てば十分)から確保しましょう。
実際にSMAPを無効にして、このようなgadgetでユーザー空間のROP chainにstack pivotして権限昇格してみてください。なお、pivot先のメモリをmmapする際にMAP_POPULATE
フラグを付けるようにしましょう。これを付けることで物理メモリが確保され、KPTIが有効でもこのマップをカーネルから見られるようになります。
modprobe_path
の書き換えやcred
構造体の書き換えなどの、ROPを使わない方法でも権限昇格してみましょう。