無条件で権限昇格するLinuxカーネルモジュールを書いてみる

Linuxにおけるカーネルエクスプロイトの下準備として、無条件で権限昇格する(安全でない)カーネルモジュールを書いてみる。

環境

Ubuntu 14.04.1 LTS 64bit版

$ uname -a
Linux vm-ubuntu64 3.13.0-44-generic #73-Ubuntu SMP Tue Dec 16 00:22:43 UTC 2014 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

必要なパッケージをインストールする

あらかじめ、カーネルモジュールのコンパイルに必要なコンパイラ一式およびLinuxカーネルのヘッダファイルをインストールしておく。

$ sudo apt-get install build-essential

カーネルモジュールを書いてみる

カーネルモジュールとしてmychardevという名前のキャラクタデバイスを作り、ioctl(2)から権限昇格できるようにしてみる。 ioctl(2)は、ユーザランドからデバイスに対して各種操作を行うためのシステムコールである。

プログラムコードを書くと、次のようになる。

/* mychardev.c */
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cred.h>

#include "mychardev.h"
#define DEVICE_NAME "mychardev"

static int device_open(struct inode *inode, struct file *file)
{
    try_module_get(THIS_MODULE);
    return 0;
}

static int device_release(struct inode *inode, struct file *file)
{
    module_put(THIS_MODULE);
    return 0;
}

static ssize_t device_read(struct file *filp, char *buffer, size_t length, loff_t *offset)
{
    return -EINVAL;
}

static ssize_t device_write(struct file *filp, const char *buffer, size_t length, loff_t * offset)
{
    return -EINVAL;
}

long device_ioctl(struct file *file, unsigned int ioctl_num, unsigned long ioctl_param)
{
    switch (ioctl_num) {
    case IOCTL_GIVE_ME_ROOT:
        commit_creds(prepare_kernel_cred(NULL));
        return 0;
    }
    return -EINVAL;
}

static struct file_operations fops = {
    .open = device_open,
    .release = device_release,
    .read = device_read,
    .write = device_write,
    .unlocked_ioctl = device_ioctl,
};

int init_module(void)
{
    int ret_val;
    ret_val = register_chrdev(MAJOR_NUM, DEVICE_NAME, &fops);
    if (ret_val < 0) {
        printk(KERN_ALERT "Registering char device failed with %d\n", ret_val);
        return ret_val;
    }
    printk(KERN_INFO "try 'sudo mknod %s c %d 0'\n", DEVICE_FILE_NAME, MAJOR_NUM);
    return 0;
}

void cleanup_module(void)
{
    unregister_chrdev(MAJOR_NUM, DEVICE_NAME);
}

また、ユーザ側からも定数を参照できるように次のようなヘッダファイルを書く。

/* mychardev.h */
#ifndef MYCHARDEV_H
#define MYCHARDEV_H

#include <linux/ioctl.h>

#define MAJOR_NUM 200
#define DEVICE_FILE_NAME "mychardev"
#define IOCTL_GIVE_ME_ROOT _IO(MAJOR_NUM, 0)

#endif

上のコードは、ioctl(2)でIOCTL_GIVE_ME_ROOTを呼ぶと、無条件でroot権限への権限昇格が行われるようになっている。 prepare_kernel_cred()は引数としてtask_struct構造体を取るが、NULLポインタを渡すこともでき、この場合initプロセスと同じcredential、すなわちuid=0のcredentialが作られる。

568 /**
569  * prepare_kernel_cred - Prepare a set of credentials for a kernel service
570  * @daemon: A userspace daemon to be used as a reference
571  *
572  * Prepare a set of credentials for a kernel service.  This can then be used to
573  * override a task's own credentials so that work can be done on behalf of that
574  * task that requires a different subjective context.
575  *
576  * @daemon is used to provide a base for the security record, but can be NULL.
577  * If @daemon is supplied, then the security data will be derived from that;
578  * otherwise they'll be set to 0 and no groups, full capabilities and no keys.
579  *
580  * The caller may change these controls afterwards if desired.
581  *
582  * Returns the new credentials or NULL if out of memory.
583  *
584  * Does not take, and does not return holding current->cred_replace_mutex.
585  */

したがって、これをcommit_creds()に渡すことでroot権限に昇格させることができる。

カーネルモジュールをコンパイルし、インストールしてみる

まず、次のようなMakefileを書く([TAB]は水平タブ一文字を表す)。

# Makefile
obj-m += mychardev.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

原理的には最初のobj-mの行だけでも十分だが、ここではコンパイル引数の指定を省力化するためにallターゲットとcleanターゲットも記述してある。

makeコマンドで拡張子koのカーネルモジュールをコンパイルしてみる。

$ make
make -C /lib/modules/3.13.0-44-generic/build M=/home/user/tmp/mychardev modules
make[1]: Entering directory `/usr/src/linux-headers-3.13.0-44-generic'
  CC [M]  /home/user/tmp/mychardev/mychardev.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/user/tmp/mychardev/mychardev.mod.o
  LD [M]  /home/user/tmp/mychardev/mychardev.ko
make[1]: Leaving directory `/usr/src/linux-headers-3.13.0-44-generic'

$ ls *.ko
mychardev.ko

mychardev.koというファイル名でカーネルモジュールが作られていることがわかる。

insmodコマンドでカーネルモジュールをインストールし、lsmodコマンドでインストールされているか確認してみる。

$ sudo insmod mychardev.ko

$ lsmod
Module                  Size  Used by
mychardev              12640  0
(snip)

上の結果から、mychardevモジュールがインストールできていることが確認できる。 なお、insmodコマンドによるインストールはシステムがシャットダウンするまでの間のみ有効である。

コード中のinit_module()関数にてキャラクタデバイスの作り方をカーネルメッセージとして出力するようにしてあるので、dmesgコマンドからこれを確認しキャラクタデバイスを作ってみる。

$ dmesg | tail
[   16.081242] nf_conntrack version 0.5.0 (3919 buckets, 15676 max)
[   16.362820] init: plymouth-upstart-bridge main process ended, respawning
[   16.657533] audit_printk_skb: 66 callbacks suppressed
[   16.657545] type=1400 audit(1426913264.480:34): apparmor="STATUS" operation="profile_replace" profile="unconfined" name="docker-default" pid=1449 comm="apparmor_parser"
[  108.564825] cgroup: systemd-logind (1004) created nested cgroup for controller "memory" which has incomplete hierarchy support. Nested cgroups may change behavior in the future.
[  108.564827] cgroup: "memory" requires setting use_hierarchy to 1 on the root.
[  126.124743] mychardev: module license 'unspecified' taints kernel.
[  126.124749] Disabling lock debugging due to kernel taint
[  126.124780] mychardev: module verification failed: signature and/or  required key missing - tainting kernel
[  126.126577] try 'sudo mknod mychardev c 200 0'

$ sudo mknod mychardev c 200 0

$ ls -l mychardev
crw-r--r-- 1 root root 200, 0 Mar 21 13:50 mychardev

無事キャラクタデバイスが作られていることが確認できた。

ユーザモードのプログラムから権限昇格してみる

作成したキャラクタデバイスに対しioctl(2)を発行し、権限昇格した上でシェルを起動するプログラムを書いてみる。

/* ioctl.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>

#include "mychardev.h"

int main()
{
    int fd;
    int ret_val;

    fd = open(DEVICE_FILE_NAME, 0);
    if (fd < 0) {
        printf("Can't open device file: %s\n", DEVICE_FILE_NAME);
        exit(1);
    }

    ret_val = ioctl(fd, IOCTL_GIVE_ME_ROOT);
    if (ret_val < 0) {
        printf("ioctl failed: %d\n", ret_val);
        exit(1);
    }

    close(fd);

    printf("[+] getuid() = %d\n", getuid());
    execl("/bin/sh", "sh", NULL);
}

上のコードでは、カーネルモジュール作成の際のヘッダファイルを読み込むことにより必要な定数の参照を行っている。 また、execl("/bin/sh", "sh", NULL)execve("/bin/sh", {"sh", NULL}, NULL)と同じ働きをする関数である。

上のコードをコンパイルし、生成されたプログラムを実行してみると次のようになる。

$ gcc ioctl.c

$ ./a.out
[+] getuid() = 0
# id
uid=0(root) gid=0(root) groups=0(root)
#

上の結果から、root権限に権限昇格できていることが確認できた。

カーネルモジュールをアンインストールする

$ sudo rmmod mychardev.ko

関連リンク