Kprobesで実行可能メモリの読み出しを検知してみる
Dynamic ROP(JIT-ROP)はlibcなどの実行可能メモリを読み出し、その中にあるgadgetを利用することでROPを行う。 これに対し、「x64で実行可能なメモリアドレスに対する入出力システムコールを検知してみる」では、ptraceでreadシステムコールを監視することで実行可能メモリの読み出しの検知を試みた。 ここでは、カーネルモジュールにてKprobesを利用し、システム全体に対しカーネル関数の呼び出しを監視することでこれの検知をやってみる。
環境
Ubuntu 14.04.1 LTS 64bit版
$ uname -a Linux vm-ubuntu64 3.13.0-48-generic #80-Ubuntu SMP Thu Mar 12 11:16:15 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 14.04.1 LTS Release: 14.04 Codename: trusty $ gcc --version gcc (Ubuntu 4.8.2-19ubuntu1) 4.8.2
Kprobesによるカーネル関数probe
KprobesはLinuxカーネルが提供するデバッグ機構であり、カーネル関数の実行を監視することができる。 Kprobesが提供するprobe機構にはkprobes、jprobes、kretprobesの3つがあり、それぞれ次のような違いがある。
- kprobes: ハンドラ関数の引数としてレジスタの値が渡される
- jprobes: ハンドラ関数の引数として対象となる関数の引数が渡される
- kretprobes: 呼び出し時とリターン時のそれぞれにハンドラを登録でき、それぞれの時点でのレジスタの値が渡される
kretprobesでは呼び出し時とリターン時でデータを共有することができ、実行時間の計測などに利用することができる。
Linuxカーネルにおけるメモリの読み出しについて調べてみる
まず最初に、ユーザ空間のプログラムで特定のメモリアドレスに対しwriteシステムコールが呼ばれる状況を想定し、probeの対象となるカーネル関数について調べてみる。
writeシステムコールが呼ばれたとき、カーネル空間では次の関数が実行される。
514 SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, 515 size_t, count) 516 { 517 struct fd f = fdget(fd); 518 ssize_t ret = -EBADF; 519 520 if (f.file) { 521 loff_t pos = file_pos_read(f.file); 522 ret = vfs_write(f.file, buf, count, &pos); 523 if (ret >= 0) 524 file_pos_write(f.file, pos); 525 fdput(f); 526 } 527 528 return ret; 529 }
スレッドセーフ版であるpwrite64システムコールも合わせて見てみると、vfs_write関数が共通して呼ばれていることがわかる。
551 SYSCALL_DEFINE4(pwrite64, unsigned int, fd, const char __user *, buf, 552 size_t, count, loff_t, pos) 553 { 554 struct fd f; 555 ssize_t ret = -EBADF; 556 557 if (pos < 0) 558 return -EINVAL; 559 560 f = fdget(fd); 561 if (f.file) { 562 ret = -ESPIPE; 563 if (f.file->f_mode & FMODE_PWRITE) 564 ret = vfs_write(f.file, buf, count, &pos); 565 fdput(f); 566 } 567 568 return ret; 569 }
vfs_write関数の内容は次のようになっている。
457 ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos) 458 { 459 ssize_t ret; 460 461 if (!(file->f_mode & FMODE_WRITE)) 462 return -EBADF; 463 if (!file->f_op->write && !file->f_op->aio_write) 464 return -EINVAL; 465 if (unlikely(!access_ok(VERIFY_READ, buf, count))) 466 return -EFAULT; 467 468 ret = rw_verify_area(WRITE, file, pos, count); 469 if (ret >= 0) { 470 count = ret; 471 file_start_write(file); 472 if (file->f_op->write) 473 ret = file->f_op->write(file, buf, count, pos); 474 else 475 ret = do_sync_write(file, buf, count, pos); 476 if (ret > 0) { 477 fsnotify_modify(file); 478 add_wchar(current, ret); 479 } 480 inc_syscw(current); 481 file_end_write(file); 482 } 483 484 return ret; 485 }
上の内容より、vfs_write関数の中では出力先に応じたwrite関数が呼ばれていることがわかる。 そこで、ここではprobeの対象としてvfs_write関数を選ぶことにする。
カーネルモジュールを書いてみる
jprobeを用い、vfs_write関数の呼び出しを監視するカーネルモジュールを書くと次のようになる。
/* detect-x-read.c */ #include <linux/kernel.h> #include <linux/module.h> #include <linux/kprobes.h> #include <linux/mm.h> MODULE_LICENSE("GPL"); /* this line is required, otherwise you will get "Unknown symbol in module" error */ static ssize_t jprobe_vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos) { unsigned long flags; struct vm_area_struct *vma, *vma_end; flags = 0; vma = find_vma(current->mm, (unsigned long)buf); vma_end = find_vma(current->mm, (unsigned long)buf + count); while (1) { flags |= vma->vm_flags; if (vma == vma_end) { break; } vma = vma->vm_next; } if (flags & VM_EXEC) { printk(KERN_INFO "reading executable memory detected: %s (pid %d), %p, %ld bytes\n", current->comm, current->pid, buf, count); if (count > 0x40) { count = 0x40; } print_hex_dump(KERN_INFO, "", DUMP_PREFIX_OFFSET, 16, 1, (char *)buf, count, 1); } jprobe_return(); return 0; } static struct jprobe jp = { .entry = jprobe_vfs_write, .kp = { .symbol_name = "vfs_write" }, }; int init_module(void) { int ret_val; ret_val = register_jprobe(&jp); if (ret_val < 0) { printk(KERN_ALERT "Registering jprobe failed with %d\n", ret_val); return ret_val; } printk(KERN_INFO "Installation jprobe succeeded\n"); return 0; } void cleanup_module(void) { unregister_jprobe(&jp); }
このコードは、読み出し元アドレスとなるbuf
から終端のbuf + count
までのメモリページを調べ、実行可能なページがあれば引数の値と読み出されるデータのhexdumpをカーネルログに出力する。
Makefileを書き、カーネルモジュールをコンパイル、インストールしてみる。
# Makefile obj-m += detect-x-read.o all: [TAB]make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: [TAB]make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
$ make make -C /lib/modules/3.13.0-48-generic/build M=/home/user/tmp/detect-x-read modules make[1]: Entering directory `/usr/src/linux-headers-3.13.0-48-generic' CC [M] /home/user/tmp/detect-x-read/detect-x-read.o Building modules, stage 2. MODPOST 1 modules CC /home/user/tmp/detect-x-read/detect-x-read.mod.o LD [M] /home/user/tmp/detect-x-read/detect-x-read.ko make[1]: Leaving directory `/usr/src/linux-headers-3.13.0-48-generic' $ sudo insmod detect-x-read.ko $ dmesg | tail [ 13.292209] init: plymouth-upstart-bridge main process (167) killed by TERM signal [ 13.398474] init: plymouth-splash main process (1092) terminated with status 1 [ 13.587164] Bridge firewalling registered [ 13.710926] nf_conntrack version 0.5.0 (7951 buckets, 31804 max) [ 14.945538] aufs 3.13-20140303 [ 15.566302] IPv6: ADDRCONF(NETDEV_UP): docker0: link is not ready [ 16.115379] audit_printk_skb: 90 callbacks suppressed [ 16.115382] type=1400 audit(1428159176.976:42): apparmor="STATUS" operation="profile_replace" profile="unconfined" name="docker-default" pid=1277 comm="apparmor_parser" [ 44.297884] detect_x_read: module verification failed: signature and/or required key missing - tainting kernel [ 44.301873] Installation jprobe succeeded
カーネルログにあらかじめ出力するようにしておいたメッセージが現れていることより、カーネルモジュールがインストールできていることがわかる。
Dynamic ROPによるlibcメモリの読み出しをやってみる
次に、カーネルモジュールがインストールされた状態でDynamic ROPをやってみる。 ここでは手順の簡略化のため、roputilsのサンプルコードを用いることにする。
$ git clone https://github.com/inaz2/roputils.git Cloning into 'roputils'... remote: Counting objects: 684, done. remote: Total 684 (delta 0), reused 0 (delta 0), pack-reused 684 Receiving objects: 100% (684/684), 185.40 KiB | 230.00 KiB/s, done. Resolving deltas: 100% (439/439), done. Checking connectivity... done. $ cd roputils/examples/ $ gcc -fno-stack-protector bof.c $ python libc-dynamic-x86-64.py ./a.out 120 [+] read: '1\x04@\x00\x00\x00\x00\x001\x04@\x00\x00\x00\x00\x001\x04@\x00\x00\x00\x00\x001\x04@\x00\x00\x00\x00\x001\x04@\x00\x00\x00\x00\x001\x04@\x00\x00\x00\x00\x001\x04@\x00\x00\x00\x00\x001\x04@\x00\x00\x00\x00\x001\x04@\x00\x00\x00\x00\x001\x04@\x00\x00\x00\x00\x001\x04@\x00\x00\x00\x00\x001\x04@\x00\x00\x00\x00\x001\x04@\x00\x00\x00\x00\x001\x04@\x00\x00\x00\x00\x001\x04@\x00\x00\x00\x00\x00&\x06@\x00\x00\x00\x00\x0030FDevgY\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x18\x10`\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00(\x10`\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x10\x06@\x00\x00\x00\x00\x00RupPECbF\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00 \x10`\x00\x00\x00\x00\x00\x90\x01\x00\x00\x00\x00\x00\x00H\x14`\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x06@\x00\x00\x00\x00\x0036cQGDOm\x00\x00\x00\x00\x00\x00\x00\x00@\x14`\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xce\x05@\x00\x00\x00\x00\x00' [+] len(data) = 199000 got a shell! $ id uid=1000(user) gid=1000(user) groups=1000(user) $
Dynamic ROPによりシェルが起動できていることが確認できたので、カーネルログを調べてみる。
$ dmesg | tail [ 440.300030] 00000000: 0a . [ 443.386455] reading executable memory detected: bash (pid 1332), 00000000004bab69, 1 bytes [ 443.386465] 00000000: 0a . [ 444.129943] reading executable memory detected: bash (pid 1332), 00000000004bab69, 1 bytes [ 444.129952] 00000000: 0a . [ 459.528744] reading executable memory detected: a.out (pid 1757), 00007f4bd5336dd0, 2097152 bytes [ 459.528754] 00000000: 41 56 41 55 41 54 55 48 89 cd 53 48 81 ec 90 00 AVAUATUH..SH.... [ 459.528760] 00000010: 00 00 48 8b 05 4f d1 39 00 48 89 7c 24 18 89 74 ..H..O.9.H.|$..t [ 459.528764] 00000020: 24 14 48 89 54 24 08 48 85 c0 0f 84 cc 00 00 00 $.H.T$.H........ [ 459.528768] 00000030: 8b 00 31 d2 85 c0 0f 94 c2 48 8d 05 70 d2 39 00 ..1......H..p.9.
Dynamic ROPが行われたa.outのプロセスにおいて、2097152(=0x200000)バイトの実行可能メモリの読み出しが検知できていることがわかる。
他のプログラムにおける検知状況を調べてみる
dmesgコマンドで表示されるカーネルログを調べてみると、上の他にも次のような検知ログが出力されていることがわかる。
[ 435.721040] reading executable memory detected: git (pid 1724), 0000000000549174, 13 bytes [ 435.721051] 00000000: 63 61 70 61 62 69 6c 69 74 69 65 73 0a capabilities. [ 436.123260] reading executable memory detected: git (pid 1724), 000000000054928d, 5 bytes [ 436.123270] 00000000: 6c 69 73 74 0a list. [ 438.123169] reading executable memory detected: git (pid 1727), 0000000000542995, 4 bytes [ 438.123180] 00000000: 30 30 30 30 0000 [ 439.327961] reading executable memory detected: git (pid 1727), 0000000000542995, 4 bytes [ 439.327971] 00000000: 30 30 30 30 0000 [ 440.287954] reading executable memory detected: git (pid 1730), 0000000000529311, 1 bytes [ 440.287965] 00000000: 0a . [ 440.295741] reading executable memory detected: git (pid 1724), 0000000000544200, 39 bytes [ 440.295752] 00000000: 23 20 70 61 63 6b 2d 72 65 66 73 20 77 69 74 68 # pack-refs with [ 440.295757] 00000010: 3a 20 70 65 65 6c 65 64 20 66 75 6c 6c 79 2d 70 : peeled fully-p [ 440.295760] 00000020: 65 65 6c 65 64 20 0a eeled . [ 440.300030] reading executable memory detected: git (pid 1724), 0000000000529311, 1 bytes [ 440.300030] 00000000: 0a . [ 443.386455] reading executable memory detected: bash (pid 1332), 00000000004bab69, 1 bytes [ 443.386465] 00000000: 0a . [ 444.129943] reading executable memory detected: bash (pid 1332), 00000000004bab69, 1 bytes [ 444.129952] 00000000: 0a .
これらは主にメモリ属性がr-x
となっている.rodata
セクションに含まれる文字列定数の読み出しにより検知されたものである。
このことから、Dynamic ROPの検知として実際に適用するには.text
セクションなどのコード領域とELFヘッダや.rodata
などのデータ領域を別々のページにロードするよう、コンパイル時に使われるリンカスクリプトを変更する必要があることがわかる。
Execute-no-Read(XnR)
次の論文では、ここでのアプローチをさらに進め、アーキテクチャレベルで実行可能メモリの読み出しを禁止するというコンセプトをExecute-no-Read(XnR)として提案している。
XnRのコンセプトの厳密な実装にはCPUによるハードウェアサポートが必要とされるが、論文中ではpage fault handlerを利用したソフトウェアエミュレーションを行うことでその有効性が検証されている。
Uprobes、SystemTap
Kprobesはカーネル空間でのprobeを行うものであるが、関連するデバッグ機構としてユーザプロセスのprobeを行うUprobesがある。 また、SystemTapはこれらのprobe機構をユーザ空間から簡単に利用できるようにするプログラムである。
関連リンク
- An introduction to KProbes [LWN.net]
- Kernel Probes (Kprobes)
- jprobe - Linuxの備忘録とか・・・(目次へ)
- Kernel instrumentation using kprobes (Phrack 67)
- The Life of I/O in the linux kernel | Scary Reasoner
- register_kprobe undefined symbol on 2.6.11 of X86_64
- Buffer Overflow脆弱性の動的パッチの方法 - Part 2
- Buffer Overflow脆弱性の動的パッチの方法 - Part 3
- 第14回「 SystemTap ノススメ」 | NTTデータ先端技術株式会社