
PoC CVE-2017-5123 - LPE - SMEP/SMAP のバイパス。KASLR なし
この小さな writeup では、root 権限を取得できるカーネル脆弱性を分析します。
このファイルは4つのパートに分かれています:
この CVE の悪用方法は他にもたくさんあります(実際、これはカーネル学習用の_PoC_に過ぎず、_実際の攻撃_では使えません)が、この方法論はカーネルエクスプロイトの入門として役立つと思います。
この脆弱性は_4c48abe91be0_で導入されたため、そのバージョンのカーネルをビルドする必要があります。
これは古いバージョンであり、コードにパッチを当てる必要があるため、少し厄介かもしれません。
私は、既にパッチ済みのカーネルコードを含むリポジトリと、.configファイルを作成しました。これにより、_クローンしてビルド_することができます。
git clone https://github.com/c3r34lk1ll3r/kernel_mirror.git
cd kernel_mirror
git checkout origin/modified_v4.14
wget https://gist.githubusercontent.com/c3r34lk1ll3r/c9c34ae86140cc7a24d0d90141686ee8/raw/52431b577a71e3fe8f89d6ce355ce9c1c54c53b6/.config
make -j 8 --output-sync=recurse
このカーネルは_virtio_ドライバでビルドされるため、_virtio ディスク_を使ってVMとの間でファイルを共有できます。
次に、初期_rootfs_を作成します:
qemu-img create -f raw hda.raw 10G
# Format the disk to ext4
mkfs.ext4 ./hda.raw
# Make a mountpoint for the image
mkdir /tmp/mount1
# Mount the disk
sudo mount -o loop ./hda.raw /tmp/mount1
次に、基本的なLinuxディストリビューションをインストールします。例えば、pacstrap や debootstrap を使用します。
sudo pacstrap /tmp/mount1 base base-devel vim
最後に、システムを変更できます:
# Add a 'test' user
echo 'test:x:1000:1000::/home/test:/bin/bash' | sudo tee -a /tmp/mount1/etc/passwd
# without password
echo 'test::14871::::::' | sudo tee -a /tmp/mount1/etc/shadow
# we can mount a virtio disk in order to share files between host and guest
echo '/transient /home/test/shared 9p trans=virtio,version=9p2000.L,rw,user,exec 0 0' | sudo tee -a /tmp/mount1/etc/fstab
sudo mkdir -p /tmp/mount1/home/test/shared
# It is usefull to have sudo permission
echo '%wheel ALL=(ALL) NOPASSWD: ALL' | sudo tee -a /tmp/mount1/etc/sudoers
echo 'wheel:x:998:test' | sudo tee -a /tmp/mount1/etc/group
sudo chown -R 1000:1000 /tmp/mount1/home/test
sudo umount /tmp/mount1
すべてが整っていれば、qemu でテストシステムを試すことができます:
qemu-system-x86_64 \
-kernel ./kernel_mirror/arch/x86_64/boot/bzImage \
-hda ./hda.raw \
-m 4G \
-cpu "Skylake-Client-IBRS,ss=on,vmx=on,hypervisor=on,tsc-adjust=on,clflushopt=on,umip=on,md-clear=on,stibp=on,arch-capabilities=on,ssbd=on,xsaves=on,pdpe1gb=on,ibpb=on,amd-ssbd=on,skip-l1dfl-vmentry=on,hle=off,rtm=off" \
-smp 4 \
-vga virtio \
-enable-kvm \
-nographic \
-machine type=q35,accel=kvm \
-virtfs "fsdriver=local,id=fs.1,path=./trans_fs,security_model=mapped,writeout=immediate,mount_tag=/transient" \
-append "root=/dev/sda rw noquiet nokaslr console=ttyS0 loglevel=5" \
-chardev "vc,id=vc.0,cols=1920,rows=1080" \
-net "user,hostfwd=tcp::10022-:22" \
-net "nic" \
-s
CVE の説明には、waitid システムコール中に無制限の書き込み操作があると記載されています。
kernel/exit.c を開いてコードを見てみましょう:
SYSCALL_DEFINE5(waitid, int, which, pid_t, upid, struct siginfo __user *,
infop, int, options, struct rusage __user *, ru)
{
struct rusage r;
struct waitid_info info = {.status = 0};
long err = kernel_waitid(which, upid, &info, options, ru ? &r : NULL);
int signo = 0;
if (err > 0) {
signo = SIGCHLD;
err = 0;
if (ru && copy_to_user(ru, &r, sizeof(struct rusage)))
return -EFAULT;
}
if (!infop)
return err;
user_access_begin();
unsafe_put_user(signo, &infop->si_signo, Efault);
unsafe_put_user(0, &infop->si_errno, Efault);
unsafe_put_user(info.cause, &infop->si_code, Efault);
unsafe_put_user(info.pid, &infop->si_pid, Efault);
unsafe_put_user(info.uid, &infop->si_uid, Efault);
unsafe_put_user(info.status, &infop->si_status, Efault);
user_access_end();
return err;
Efault:
user_access_end();
return -EFAULT;
}
この関数は非常に単純です。いくつかのチェックの後、unsafe_put_user(...) の呼び出しがいくつかあり、関数は戻ります。
この関数の主要部分は unsafe_put_user(...) 関数で構成されているので、そこに移動しましょう(arch/x86/include/asm/uaccess.h):
/*
* The "unsafe" user accesses aren't really "unsafe", but the naming
* is a big fat warning: you have to not only do the access_ok()
* checking before using them, but you have to surround them with the
* user_access_begin/end() pair.
*/
#define user_access_begin() __uaccess_begin()
#define user_access_end() __uaccess_end()
#define unsafe_put_user(x, ptr, err_label) \
do { \
int __pu_err; \
__typeof__(*(ptr)) __pu_val = (x); \
__put_user_size(__pu_val, (ptr), sizeof(*(ptr)), __pu_err, -EFAULT); \
if (unlikely(__pu_err)) goto err_label; \
} while (0)
#define unsafe_get_user(x, ptr, err_label) \
do { \
int __gu_err; \
__inttype(*(ptr)) __gu_val; \
__get_user_size(__gu_val, (ptr), sizeof(*(ptr)), __gu_err, -EFAULT); \
(x) = (__force __typeof__(*(ptr)))__gu_val; \
if (unlikely(__gu_err)) goto err_label; \
} while (0)
コメントには重大な警告があります:unsafe_put/get_user を使用したい場合は、最初に access_ok() を呼び出し、それらを user_access_begin/end() で囲む必要があります。
先ほどのコード(waitid)を見ると、access_ok() が呼び出されていないため、このシステムコールはこの_警告_に_違反_しています。
しかし、これらのマクロとは何でしょうか?
SMAP と SMEP は、エクスプロイトを書きにくくするためにカーネルに導入された2つのセキュリティ機能です。これらの機能はCPUによって強制されることに注意してください。
SMEP は、CPUがスーパーバイザーモードのときにユーザースペースコードを実行することを防ぎます。一方、SMAP はユーザーメモリへの読み取り/書き込みアクセスをブロックします。
カーネルはユーザーメモリとの間でデータの読み書きを行う必要があり、これには2つの方法があります:
copy_from_user)を使用する;unsafe_put_user の定義にあるように、この関数は ptr が指すメモリに x の値をコピーするだけです(エラーがあれば err_label にジャンプします)。先ほど、カーネルは SMAP のためにユーザースペースにアクセスできないと言いました。そのため、これらの関数は user_access_begin/end() で囲む必要があります。
#define __uaccess_begin() stac()
#define __uaccess_end() clac()
見ての通り、user_access_begin/end は単に_ASM_命令の stac と clac です。
stac: "EFLAGS レジスタの AC フラグビットを設定します。これにより、ユーザーモードのデータアクセスのアライメントチェックが有効になる場合があります。CR4 レジスタで SMAP ビットが設定されている場合でも、スーパーバイザーモードからユーザーモードページへの明示的なデータアクセスが許可されます。"clac: "EFLAGS レジスタの AC フラグビットをクリアします。これにより、ユーザーモードのデータアクセスのアライメントチェックが無効になります。CR4 レジスタで SMAP ビットが設定されている場合、スーパーバイザーモードからユーザーモードページへの明示的なデータアクセスは許可されません。"基本的に、これらの2つのマクロは SMAP を有効/無効にします。
先ほどの「警告」は access_ok 関数にも言及しています:
/**
* access_ok: - Checks if a user space pointer is valid
* @type: Type of access: %VERIFY_READ or %VERIFY_WRITE. Note that
* %VERIFY_WRITE is a superset of %VERIFY_READ - if it is safe
* to write to a block, it is always safe to read from it.
* @addr: User space pointer to start of block to check
* @size: Size of block to check
*
* Context: User context only. This function may sleep if pagefaults are
* enabled.
*
* Checks if a pointer to a block of memory in user space is valid.
*
* Returns true (nonzero) if the memory block may be valid, false (zero)
* if it is definitely invalid.
*
* Note that, depending on architecture, this function probably just
* checks that the pointer is in the user space range - after calling
* this function, memory access functions may still return -EFAULT.
*/
#define access_ok(type, addr, size) \
({ \
WARN_ON_IN_IRQ(); \
likely(!__range_not_ok(addr, size, user_addr_max())); \
})
ここのコメントは一目瞭然です。このマクロはポインタが有効なユーザースペースポインタであるかをチェックします。