カーネルexploitへの緩和策として、Linuxカーネルにはセキュリティ機構がいくつか存在します。ユーザーランドで登場したNXのように、ハードウェアレベルでのセキュリティ機構も存在するため、いくつかの知識はWindowsのカーネルexploitにもそのまま適用できます。

ここで取り上げるのはカーネル特有の保護策で、Stack Canaryのようなセキュリティ機構はデバイスドライバにも存在しますが、それについては特筆すべき点はないためここでは説明しません。

カーネル起動時のパラメータについては公式のドキュメントが分かりやすいです。

SMEP (Supervisor Mode Execution Prevention)

カーネルのセキュリティ機構として代表的なものが、SMEPとSMAPです。
SMEPはカーネル空間のコードを実行中に、突然ユーザー空間のコードを実行するのを禁止するセキュリティ機構です。イメージとしてはNXに似ています。

SMEPは緩和機構で、それ単体で強い防御策という訳ではありません。例えばカーネル空間の脆弱性を利用して攻撃者にRIPを奪われてしまったとします。もしSMEPが無効だと、次のようにユーザー空間に用意したシェルコードを実行されてしまいます。

1
2
3
4
5
char *shellcode = mmap(NULL, 0x1000, PROT_READ|PROT_WRITE|PROT_EXECUTE,
MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
memcpy(shellcode, SHELLCODE, sizeof(SHELLCODE));

control_rip(shellcode); // RIP = shellcode

しかしSMEPが有効の場合、上のようにユーザー空間に用意したシェルコードを実行しようとするとカーネルパニックを引き起こします。これにより、攻撃者はRIPを奪っても権限昇格に繋げられなくなる可能性が上がります。

オオカミくん

カーネル空間のシェルコードでは何を実行すれば良いのかな?
権限昇格の方法はまた別の章で勉強するよ。

SMEPはqemu実行時の引数で有効化できます。次のように-cpuオプションに+smepと付いていればSMEPが有効化されます。

1
-cpu kvm64,+smep

マシン内部からは/proc/cpuinfoを見ることでも確認できます。

1
$ cat /proc/cpuinfo | grep smep

SMEPはハードウェアのセキュリティ機構です。CR4レジスタの21ビット目を立てるとSMEPが有効になります。

SMAP (Supervisor Mode Access Prevention)

ユーザー空間からカーネル空間のメモリを読み書きできないのはセキュリティ上当たり前ですが、実はカーネル空間からユーザー空間のメモリを読み書きできなくするSMAP(Supervisor Mode Access Prevention)というセキュリティ機構が存在します。カーネル空間からユーザー空間のデータを読み書きするには、copy_from_user, copy_to_userという関数を使う必要があります。
しかし、なぜ高い権限のカーネル空間から低い権限のユーザー空間のデータを読み書きできなくするのでしょうか。

歴史的な経緯については知りませんが、SMAPによる恩恵は主に2つあると考えられます。

まず1つ目が、Stack Pivotの防止です。
SMEPで出した例ではRIPを制御できてもシェルコードは実行できなくなりました。しかし、Linuxカーネルは非常に膨大な量の機械語を持っているため、次のようなROP gadgetが必ず存在します。

1
mov esp, 0x12345678; ret;

ESPに入る値が何であれ、このROP gadgetが呼ばれるとRSPはその値に変更されます[1]。一方、このような低いアドレスはユーザーランドからmmapで確保可能ですので、SMEPが有効でも攻撃者はRIPを取るだけで、次のようにROP chainを実行できます。

1
2
3
4
5
6
7
8
void *p = mmap(0x12340000, 0x10000, ...);
unsigned long *chain = (unsigned long*)(p + 0x5678);
*chain++ = rop_pop_rdi;
*chain++ = 0;
*chain++ = ...;
...

control_rip(rop_mov_esp_12345678h);

もしSMAPが有効なら、ユーザー空間でmmapしたデータ(ROP chain)はカーネル空間から見られないので、stack pivotのret命令でカーネルパニックを起こします。
このように、SMEPに加えてSMAPが有効になることで、ROPによる攻撃を緩和できます。

SMAPによる2つ目の恩恵が、カーネルプログラミングで起こしやすいバグの防止です。
これにはデバイスドライバなどのプログラマが起こしてしまうカーネル特有のバグが関係します。ドライバが次のようなコードを書いたとしましょう。(今は関数定義の意味は分からなくて構いません。)

1
2
3
4
5
6
7
8
9
10
char buffer[0x10];

static long mydevice_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
if (cmd == 0xdead) {
memcpy(buffer, arg, 0x10);
} else if (cmd == 0xcafe) {
memcpy(arg, buffer, 0x10);
}
return
}

memcpybufferというグローバル変数にデータを読み書きしていることがイメージできるかと思います。

このモジュールはユーザー空間から次のように利用すると、0x10バイトのデータを記憶してくれます。

1
2
3
4
5
6
7
8
9
int fd = open("/dev/mydevice", O_RDWR);

char src[0x10] = "Hello, World!";
char dst[0x10];

ioctl(fd, 0xdead, src);
ioctl(fd, 0xcafe, dst);

printf("%s\n", dst); // --> Hello, World!

ユーザー空間のプログラミングに慣れていると何ということはありません。memcpyのサイズも固定で、特に問題は無いように思えます。

しかし、もしSMAPが無効だと、次のような呼び出しも許されてしまいます。

1
ioctl(fd, 0xdead, 0xffffffffdeadbeef);

0xffffffffdeadbeefというのはユーザー空間としては無効なアドレスですが、仮にこれがLinuxカーネル中の秘密のデータが入っているアドレスだったとしましょう。するとデバイスドライバは

1
memcpy(buffer, 0xffffffffdeadbeef, 0x10);

を実行してしまい、秘密のデータを読んでしまいます。今回の例のように何のチェックもなしにユーザー空間から受け取ったアドレスでmemcpyを使ってしまうと、ユーザー空間からカーネル空間の任意のアドレスを読み書きできてしまうことになります。
カーネルプログラミングに慣れ親しんでいない方にとっては非常に気づきにくい脆弱性ですが、AAR/AAWができるため影響は重大です。このようなミスを防ぐためにもSMAPは役に立っているのです。

SMAPはqemu実行時の引数で有効化できます。次のように-cpuオプションに+smapと付いていればSMAPが有効化されます。

1
-cpu kvm64,+smap

マシン内部からは/proc/cpuinfoを見ることでも確認できます。

1
$ cat /proc/cpuinfo | grep smap

SMAPもSMEP同様にハードウェアのセキュリティ機構です。CR4レジスタの22ビット目を立てるとSMAPが有効になります。

オオカミくん

Intel CPUではEFLAGS.AC (Alignment Check)というフラグをそれぞれ1,0に変更するSTACCLACという命令があって、ACがセットされている間はSMAPの効力が無効になるよ。

KASLR / FGKASLR

ユーザー空間ではアドレスをランダム化するASLR(Address Space Layout Randomization)が存在しました。これと同様に、Linuxカーネルやデバイスドライバのコード・データ領域のアドレスをランダム化するKASLR(Kernel ASLR)という緩和機構も存在します。
カーネルは一度ロードされたら移動しませんので、KASLRは起動時に1度だけ働きます。何か1つでもLinuxカーネル中の関数やデータのアドレスをリークできれば、ベースアドレスが求まります。

2020年に入ってからFGKASLR(Function Granular KASLR)と呼ばれるさらに強いKASLRが登場しました。2022年現在はデフォルトで無効なようですが、これはLinuxカーネルの関数ごとにアドレスをランダム化するという技術です。たとえLinuxカーネル中の関数のアドレスがリークできても、ベースアドレスは求まりません。
しかし、FGKASLRはデータセクションなどはランダム化しませんので、データのアドレスをリークできればベースアドレスが求まります。もっともベースアドレスから特定の関数のアドレスを求めることもできませんが、後々登場する特殊な攻撃ベクタには利用可能です。

アドレスはカーネル空間で共通という点に注意してください。たとえあるデバイスドライバがKASLRのおかげでexploit不可能だとしても、別のドライバがカーネルのアドレスをリークしてしまうと、アドレスは共通なのでexploit可能になります。

KASLRはカーネルの起動時引数で無効化できます。qemuの-appendオプションにnokaslrと付いていればKASLRは無効化されています

1
-append "... nokaslr ..."

KPTI (Kernel Page-Table Isolation)

2018年にIntel等のCPUでMeltdownと呼ばれるサイドチャネル攻撃が発見されました。この脆弱性については説明しませんが、カーネル空間のメモリをユーザー権限で読めてしまうという重大な脆弱性で、KASLRの回避などが可能でした。近年のLinuxカーネルではMeltdownの対策として、KPTI(Kernel Page-Table Isolation)、あるいは古い名称でKAISERと呼ばれる機構が有効になっています。

仮想アドレスから物理アドレスに変換する際にページテーブルが利用されるのはご存知の通りですが、このページテーブルをユーザーモードとカーネルモードで分離する[2]のがこのセキュリティ機構です。KPTIはあくまでMeltdownを防ぐためのセキュリティ機構なので通常のカーネルexploitにおいては問題になりません。しかし、カーネル空間でROPする場合などにKPTIが有効だと、最後にユーザー空間に戻る際に問題が発生します。具体的な解決方法はKernel ROPの章であらためて説明します。

KPTIはカーネルの起動時引数で有効化できます。qemuの-appendオプションにpti=onと付いていればKPTIは有効化され、pti=offnoptiが付いていれば無効化されます。

1
-append "... pti=on ..."

KPTIは/sys/devices/system/cpu/vulnerabilities/meltdownからも確認できます。次のように「Mitigation: PTI」と書いていればKPTIが有効です。

1
2
# cat /sys/devices/system/cpu/vulnerabilities/meltdown
Mitigation: PTI

無効な場合は「Vulnerable」となります。

KPTIはページテーブルの切り替えなので、CR3レジスタの操作でユーザー・カーネル空間を切り替えられます。LinuxにおいてはCR3に0x1000をORする(すなわちPDBRを変更する)ことでカーネル空間からユーザー空間に切り替わります。この操作はswapgs_restore_regs_and_return_to_usermodeで定義されていますが、詳細は実際にexploitを書く章で説明します。

KADR (Kernel Address Display Restriction)

Linuxカーネルでは、関数の名前とアドレスの情報を/proc/kallsymsから読むことができます。また、デバイスドライバによってはprintk関数などを使い、さまざまなデバッグ情報をログに出力するものもあり、このログはdmesgコマンドなどでユーザーから見ることができます。
このように、カーネル空間の関数やデータ、ヒープなどのアドレス情報のリークを防ぐための機構がLinuxには存在します。正式な名称は無いと思いますが、参考文献ではKADR(Kernel Address Display Restriction)と呼んでいるようなので、このサイトでもその名称を採用します。

この機能は/proc/sys/kernel/kptr_restrictの値により変更できます。kptr_restrictが0である場合、アドレスの表示に制限はかかりません。kptr_restrictが1である場合、CAP_SYSLOG権限を持つユーザーにはアドレスが表示されます。kptr_restrictが2である場合、ユーザーが特権レベルであってもカーネルアドレスは隠されます。
KADRが無効な場合はアドレスリークの必要がなくなるため、最初に確認するとexploitが簡単になる場合があります。


練習問題LK01のカーネルに対して以下の操作を実行しましょう。(前の例題で既にroot権限のシェルを持っている状態から始めてください。)
(1) run.shを読んで、KASLR, KPTI, SMAP, SMEPが有効かどうかを確認してください。
(2) SMAP, SMEP両方を有効にするオプションを付けて起動し、/proc/cpuinfoを見てSMAP, SMEPが有効になっていることを確認してください。(確認後にSMAP, SMEPは再度無効化してください。)
(3) 「head /proc/kallsyms」で最初に現れるアドレスはカーネルのベースアドレスです。KASLRが無効の場合、ベースアドレスがいくつになるか確認してください。(ヒント:KADRに注意)

  1. x64では32-bitのレジスタに対して演算する結果が64-bitに拡張されます。 ↩︎

  2. システムコールの呼び出しだけはカーネル・ユーザー空間で共有されます。 ↩︎