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のハンドラもカーネルモジュール側が正しく実装する必要があります。 ↩︎