Kernel Exploitに必要な知識のほとんどはLK01で既に説明が完了しているので、ここからはカーネル空間特有の攻撃手法やLinuxカーネルに搭載された機能に対する攻撃などの細かい内容になります。
LK02(Angus)ではカーネル空間におけるNULL Pointer Dereferenceの悪用方法について学びます。まず練習問題LK02のファイルをダウンロードしてください。
本章で扱う脆弱性について
LK02のqemu起動オプションを見ると分かりますが、今回の攻撃対象のマシンではSMAPが無効になっています。本章で扱うNULL Pointer Dereferenceは、SMAPが無効でないとexploitできません。
また、今回のカーネルを起動して次のコマンドを入力してみてください。
1 | $ cat /proc/sys/vm/mmap_min_addr |
mmap_min_addr
はLinuxカーネルの変数で、名前の通りユーザーランドからmmap
でマップできる最も小さいアドレスを制限します。デフォルトでは非ゼロの値ですが、今回の攻撃対象では0に設定されていることに注意してください。この変数は、今回扱うNULL Pointer Dereferenceに対するmitigationとしてLinuxカーネルのバージョン2.6.23から導入されました。
このように、本章の内容はSMAPやmmapのmitigationが回避できる前提での攻撃になるため、最近のLinuxで使える手法にのみ興味がある方は読み飛ばしてもらって構いません。
脆弱性の確認
まずはLK02のソースコードを読んでみましょう。ソースコードはsrc/angus.c
に書かれています。
ioctl
LK01と大きく違う点は、read
,write
が実装されていない代わりにioctl
というシステムコールのハンドラが記述されていることです。ファイルディスクリプタに対してioctl
を呼ぶことで、対応するカーネルやドライバ中のioctl
ハンドラが呼ばれます。
ioctl
はファイルディスクリプタ以外にrequest
, argp
という2つの引数を取ります。
1 | ioctl(fd, request, argp); |
request
には、そのデバイスを操作するリクエストコードを渡します。リクエストコードはドライバが各自で定義した値なので、ソースコードを読んでどのようなリクエストを投げられるかを把握しましょう。
argp
はそのデバイスに渡すデータを入れます。一般的にはここにはユーザー空間のデータのポインタが入り、カーネルモジュール側でcopy_from_user
を使ってリクエスト内容を読み出します。
今回のカーネルモジュールでも、request_t
という構造体をユーザー空間から渡す仕様になっています。
1 | typedef struct { |
また、request
のコードに応じて処理を変えている様子も確認できます。
1 | switch (cmd) { |
ioctl
ハンドラの実装を読む前に、今回使われているprivate_data
について説明します。
file構造体
ユーザー空間からドライバなどを操作するときファイルディスクリプタを使いますが、カーネル側ではfile構造体
として受け取ります。
ファイル構造体には例えばlseek
で設定されたカーソルの位置[1]などファイル固有の情報がありますが、カーネルモジュールが自由に使って良いメンバとしてprivate_data
があります。
1 | struct file { |
private_data
にはどのようなデータを置いても構いませんが、データの確保や解放は当然モジュール側が正しく実装する必要があります。今回のドライバではXorCipher
という独自の構造体を格納するために使っています。
1 | static int module_open(struct inode *inode, struct file *filp) { |
LK01-4 (Holstein v4)でもここにデータを格納すれば競合が起きなかったね。
プログラムの概要
このプログラムはデータをXOR暗号で暗号化・復号できるカーネルモジュールです。
このモジュールはioctl
で操作でき、リクエストコードは次の5つが用意されています。
1 |
まずCMD_INIT
で呼び出すとprivate_data
にXorCipher
構造体が格納されます。
1 | typedef struct { |
XorCipher
構造体は鍵key
とその長さkeylen
、データdata
とその長さdatalen
を持ちます。
次にCMD_SETKEY
で呼び出すと、argp
で渡されたデータを鍵としてコピーします。既に鍵が登録されている場合は先に古い鍵を解放します。
1 | case CMD_SETKEY: |
同様にCMD_SETDATA
ではユーザー空間から暗号化・復号したいデータをコピーします。
1 | case CMD_SETDATA: |
暗号化・復号されたデータはCMD_GETDATA
を使ってユーザー空間へコピーできます。
1 | case CMD_GETDATA: |
最後にCMD_ENCRYPT
とCMD_DECRYPT
では、xor
関数を呼び出します。(XOR暗号なので暗号化も復号も同じアルゴリズムです。)データや鍵が設定されていない場合はエラーとなります。
1 | long xor(XorCipher *ctx) { |
脆弱性の調査
今回のドライバにはバッファオーバーフローやUse-after-Freeといった脆弱性はありません。気づきにくいかもしれませんが、よく読むと暗号化・復号処理にNULL Pointer Dereferenceが存在します。
まずioctl
の最初にprivate_data
のポインタをXorCipher
として取得します。
1 | ctx = (XorCipher*)filp->private_data; |
CMD_SETKEY
などではprivate_data
が初期化済みかチェックされています。
1 | if (!ctx) return -EINVAL; |
しかし、CMD_GETDATA
,CMD_ENCRYPT
,CMD_DECRYPT
にはこのチェックがありません。
1 | long xor(XorCipher *ctx) { |
したがって、データの取得や、暗号化・復号の際に未初期化のXorCipher
(つまりNULLポインタ)を参照してしまう可能性があります。
脆弱性の確認
まずは正しい使い方でこのモジュールを呼び出してみます。各リクエストコードに対応する関数を作ると便利です。
1 | int angus_init(void) { |
例として、"Hello, World!"を"ABC123"という鍵で暗号化・復号してみましょう。
1 | int main() { |
データが暗号化・復号できていたら成功です。
次にXorCipherを初期化せずに暗号化してみましょう。
1 | int main() { |
これを実行すると、次のようにカーネルパニックに陥るかと思います。
BUGの項目を見ると「kernel NULL pointer dereference, address: 0000000000000008」とあり、解析した通りNULLポインタを参照しようとしてクラッシュしていることが分かります。
NULLポインタ参照はユーザー空間のプログラムでも度々発生しますが、このバグは通常exploitableではありません。では、今回はどのようにこのバグを使って権限昇格するのでしょうか。
仮想メモリとmmap_min_addr
Linuxの仕様として、仮想メモリはアドレスによって使用用途が異なります。例えば0000000000000000
から00007fffffffffff
まではユーザー空間が自由に使えます。また、ffffffff80000000
からffffffff9fffffff
まではカーネルデータの領域で、物理アドレス0にマップされています。
Linuxでは48ビットのアドレスを64ビットに符号付き拡張するよ。だから0x800000000000から0xffff7fffffffffffまではアドレスとして不正で、non-canonicalと呼ばれているんだね。
0000000000000000
から00007fffffffffff
まではユーザー空間が使えます。つまり、アドレス0がマップされているとき、NULLポインタ参照はSegmentation Faultを起こさずにデータを読み書きできます。カーネル空間のNULLポインタ参照では、SMAPが無効のときはユーザー空間のデータを読めるので、攻撃者が意図的にアドレス0に用意したデータを使ってしまうのです。
mmap
では通常第一引数が0(NULL)のときは、どのアドレスにマップするかをカーネルに任せます。しかし、MAP_FIXED
フラグを付けてマップすれば必ずそのアドレスにマップする(か失敗する)ようになり、アドレス0にメモリを確保できます。(KPTIが有効なのでMAP_POPULATE
も忘れないようにしましょう。)
1 | mmap(0, 0x1000, PROT_READ|PROT_WRITE, |
今回の攻撃対象のマシンでもこの方法でアドレス0にメモリを確保できますが、みなさんの普段使っているLinuxマシンでは上記コードは失敗するかと思います。
LinuxにはNULL pointer dereferenceに対するmitigationとしてmmap_min_addr
という変数があります。
1 | $ cat /proc/sys/vm/mmap_min_addr |
ユーザー空間からこのアドレスよりも小さいアドレスにメモリをマップすることはできません。そのため通常NULL pointer dereferenceはunexploitableですが、今回の攻撃対象ではこの値が0に設定されているため攻撃可能となります。
権限昇格
XorCipher
構造体をNULLポインタ参照してしまうので、攻撃者はアドレス0に偽のXorCipher
構造体を用意します。
1 | typedef struct { |
data
ポインタとdatalen
を操作すれば、CMD_GETDATA
で任意アドレスからデータを読み出せることが分かります。また、data
ポインタとdatalen
、さらにkey
とkeylen
を適切に設定すれば、任意アドレスのデータを書き換えられます。
したがって、今回の脆弱性ではAAR/AAWという非常に強力なprimitiveが作れます。CMD_GETDATA
ではcopy_to_user
を使ってカーネル空間からユーザー空間にデータを転送します。
1 | if (copy_to_user(req.ptr, ctx->data, req.len)) return -EINVAL; |
copy_to_user
やcopy_from_user
といった関数は、誤ってマップされていないアドレスが渡されてもクラッシュせずに失敗するように設計されています。したがって、KASLRが有効な場合でも、適当にアドレスを決めて総当り的にデータを読んでいけば、いつかcopy_to_user
が成功します。
何はともあれAAR/AAWを作って、ユーザー空間のデータを読み書きすることで実装を確認しましょう。
1 | XorCipher *nullptr = NULL; |
AAR/AAWが成功しました!
あとはカーネルのベースアドレスを探したり、cred
構造体を探したり、自由な手法で権限昇格してみてください。サンプルのexploitコードはここからダウンロードできます。
cred
構造体を見つける方法、カーネルのベースアドレスを見つける方法などを試し、どの手法が平均的に最も速く終わるかを調べましょう。また、それぞれの手法の利点と欠点は何でしょうか。
当然
lseek
のハンドラもカーネルモジュール側が正しく実装する必要があります。 ↩︎