# 最近のLinuxカーネルexploit問に対するテクニック集3
###### tags: `kernel` `ctf` `pwn`
# 概要
この記事は,[CTF Advent Calendar 2018](https://adventar.org/calendars/3210) の6日目の記事です.
5日目は私の「[最近のLinuxカーネルexploit問に対するテクニック集2](https://hackmd.io/s/SJ4jQLeJN)」でした.
今回は
- 使える関数テーブルの話
について書きます.残念ながら新規性はありません.日本にはこういった記事はあまりありませんが,海外では普通に公開されている内容です.
またLinux以外のOS(例えばWindows)には通用しないと思いますので,ご注意ください.
# はじめに
大前提として,Linuxのカーネルexploit(権限昇格)についてある程度知っている方向けの内容となります.
全く知らないよ!って方は,[nokia31337の解説資料](https://speakerdeck.com/bata_24/katagaitai-ctf-number-3)で図を入れて解説したので,先にそちらで精進したほうが良いかと思います(2014年当時のARMにおけるLinuxカーネルExploit問です).
また私はLinuxカーネルのソースを全て読んだことはなく,必要に応じてチラ見した程度なので,そこまで深い知識は持っていません.もし記事の内容に間違いがありましたら,優しく教えてください.マサカリ投げるのは禁止!
# 使える構造体について
ユーザランドのexploitでは,glibcなどの構造に依存した攻撃がよく使われますね.知らなくても攻略はできるかもしれませんが,知っていると攻略が非常に楽になる,そういったものはCTF界隈ではいくつか知られています.
良く使われるものとして,以下がすぐ思いつきますね.
- ヒープチャンクの管理構造(`main_arena`や`malloc chunk`の各種メンバ, large/small/unsorted bin, fastbin, tcache, ...)
- 参考: [how2heap](https://github.com/shellphish/how2heap)
- 参考: [ヒープexploitテク総まとめ](https://pastebin.com/mrFNd19w)
- FILE構造体の各種メンバ
- 参考: [Play with FILE Structure - Yet Another Binary Exploit Technique](https://www.slideshare.net/AngelBoy1/play-with-file-structure-yet-another-binary-exploit-technique)
- `__free_hook`/`__malloc_hook`などのフック系関数ポインタ
- One Gadget RCE
- 参考: [Pwning (sometimes) with style](https://j00ru.vexillium.org/slides/2015/insomnihack.pdf)
- `setcontext`の全レジスタ設定ガジェット
- 参考: [Getting a shell on fruits - bkpctf 2014](http://wapiflapi.github.io/2014/04/30/getting-a-shell-on-fruits-bkpctf-2014/)
- リピートコード(無限ループガジェット)
- 参考: [リナックスに置ける様々なリモートエキスプロイト手法](https://www.slideshare.net/mobile/codeblue_jp/seok-halee-japub)
- TLS(Thread Local Storage)のマスターカナリア
- 参考: [Master Canary Forging: 新しいスタックカナリア回避手法の提案](https://www.slideshare.net/codeblue_jp/master-canary-forging-by-code-blue-2015)
- vsyscallのretガジェット
- 参考: [Google CTF - Wiki Writeup](http://gmiru.com/writeups/gctf-wiki/)
さてカーネルexploitにおいても,バグを見つけてから権限昇格に繋げる際に利用可能な,使える構造の知識を持っておくと便利です.
スタックBOFなどであれば攻略も単純で良いのですが,多くの場合はカーネルのヒープ(`kmalloc()`などで確保される領域)で発生しますし,場合によってはそこからkernel Use After Free(以降はkUAFと表記)に繋がります.その際は,既知の特定サイズの構造体と上手く重ね合わせる,と言った攻略が必要になりますからね.
さて,kUAFを権限昇格に持ち込むために知っておくと便利なのが,特殊な構造体の知識です.以下のような条件を満たす構造体は,(kUAFにより)他用途で確保された領域と重ね合わせるだけで直ぐにRIPを奪う事ができるため,攻略の助けになります.
- 構造体を確保する時,ユーザランドからのアクションをトリガにすることができる
- 関数ポインタ,もしくは関数テーブルなど,RIPを奪えるようなメンバを持つ
- そのメンバは読み書き可能である
- ユーザランドからのアクションで簡単にcallすることができる
では,こういった良さげな構造体を紹介しましょう.カーネルv4.17.2の頃に調べたメモに沿っているので,URLが半年くらい古いです.ご注意ください.
## `file->f_op->function`
※注意:現在は`kmalloc-256`からは取得されず,スラブキャッシュ(`filp`)から取得されます.
最も有名な構造体と関数テーブルですね.ファイルを作成したり開いたりすると,カーネル内部で作成される構造体です.当然ですが,`FILE*`とは違いますのでご注意ください(`FILE*`はユーザランドのglibc内で定義される構造体です).
`f_op`と呼ばれる関数テーブルへのポインタを持ちますが,このポインタを改変しておけば,その先にある関数が呼ばれたタイミングでRIPを奪うことが出来ます.
- `file`
- `struct file`型
- 構造 https://elixir.bootlin.com/linux/v4.17.2/source/include/linux/fs.h#L857
- `kmalloc-256`型として確保される.関数テーブルへのポインタ`f_op`を持つ
- `f_op`
- `struct file_operations`型
- 構造 https://elixir.bootlin.com/linux/v4.17.2/source/include/linux/fs.h#L1704
- 確保方法
```C=
int fd = open("/path/to/some_file", O_RDONLY);
```
- 呼出方法
```C=
close(fd); // file->f_op->releaseが呼ばれる
```
尚,`struct file_operations`型の定義には,`__randomize_layout`と呼ばれる修飾子がついています.最近はカーネルのビルド時に構造体のメンバの並びをランダマイズするRANDSTRUCTという防御機構が有効になっているケースがあり,`struct file_operations`型はこの影響を受けます.従って環境によっては,関数テーブル内のメンバ(=関数ポインタ)が,ビルドごとに異なる順番になっている場合があります.
ちゃんと調べたわけではありませんが,私のざっくりとした理解をお伝えすると,例えばカーネルのソース上に
```C=
struct HOGE {
int a;
int b;
int c;
} hoge;
```
という`struct HOGE`型の`hoge`があったとして,実際にカーネルをビルドすると,メモリ上の配置は
```=
hoge:
0x0: c
0x4: a
0x8: b
```
みたいにランダムっぽく見える,ということですね.ちゃんと調べた訳ではありませんが,この並びは多分ビルド毎に変わるはずです.
動作環境が異なれば,メモリ内のメンバの配置に違いが出るため,PoCを流用する系の攻撃を防ぐことができるというメリットがあります.でも一度ビルドしたら並びは変わりません.CTFではカーネルが配布されるはずなので,解析すればこの並びは簡単に特定可能です.ただ,知らないとドツボにハマるので注意しましょう.
## `tty->ops->function`
これも有名な構造体と関数テーブルですね.CTFでは何度もお世話になりました.
標準入出力を持つようなCUIを管理する構造体です.`/dev/ptmx`を開けば新規に作ることが出来ます.あまりにも強力なので`/dev/ptmx`が潰されているケースも何度か遭遇しました.
先に紹介した`file`との違いは,`kmalloc()`で確保される時のサイズですね.
- `tty`
- `struct tty_struct`型
- 構造 https://elixir.bootlin.com/linux/v4.17.2/source/include/linux/tty.h#L282
- `kmalloc-1024`型として確保される.関数テーブルへのポインタ`ops`を持つ
- `ops`
- `struct tty_operations`型
- 構造 https://elixir.bootlin.com/linux/v4.17.2/source/include/linux/tty_driver.h#L253
- 確保方法
```C=
fd = open("/dev/ptmx", O_NOCTTY|O_RDWR);
```
- 呼出方法
```C=
ioctl(fd, 0xdeadbeef, 0xcafebabe); // tty->ops->ioctlが呼ばれる
```
ちなみに関数テーブル(`tty->ops->***`)を書き換えているなら,上記の`fd`に対して何をしても大抵はRIPを乗っ取ることができますが,その中で`ioctl()`を使うことにはメリットがあります.書き換えた関数テーブルを参照してRIPを奪った際に,`ioctl()`の引数(`0xdeadbeef`と`0xcafebabe`)は,他のレジスタに入っているので,stack pivotなどに繋げやすくなるからです.
## `shp->shm_file->f_op->function`
内部に`struct file`を持っている構造体です.関数ポインタの段数が1つ多いので,バグの種類によっては,こちらの方が利用しやすいケースがあります.
また`shp->shm_clist->next`と`shp->shm_clist->prev`というリンクリストを持っており,`shmctl(shmid, IPC_RMID, 0)`でunlinkを使って色々任意アドレスの読み書きに繋げやすいという観点もあります.
- `shp`
- `struct shmid_kernel`型
- 構造 https://elixir.bootlin.com/linux/v4.17.2/source/ipc/shm.c#L51
- `kmalloc-256`型として確保される.`struct file`型の`shm_file`を持ち,その内部に関数テーブルへのポインタ`f_op`を持つ
- `shm_file`
- `struct file`型
- 既に紹介済みなので,これ以降は省略
- 確保方法
```C=
int shmid = shmget(IPC_PRIVATE, 1024, IPC_CREAT);
```
- 呼出方法
```C=
shmctl(shmid, IPC_RMID, 0); // shp->shm_file->f_op->releaseが呼ばれる
```
## `msg`
お次はちょっと違うタイプです.先に挙げた条件のうち「関数ポインタを持つ」という条件にはマッチしませんが,ユーザの指定したサイズで確保でき,中身のデータもヘッダ部分を除き自由に書き換え可能,という点が強みの構造体です.kUAFでは非常に役立ちます.
- `msg`
- `struct msg_msg`型
- 構造 https://elixir.bootlin.com/linux/v4.17.2/source/include/linux/msg.h#L9
- `msg_msg`構造体の直下には,実際のメッセージが続く形で一括確保される.従って`kmalloc-N`(N=任意サイズ)の型を確保出来る.
- 確保方法
- 以下は`kmalloc-1024`として確保する例です.
- 100回確保する前後での`/proc/slabinfo`を比較しています.
```C=
// gcc -masm=intel test.c
#include <stdlib.h>
#include <string.h>
#define IPC_CREAT 00001000
#define SIZE (1024-sizeof(struct msg_msg))
//////////////////////////////////////////
// system call
int msgget(int key, int msgflg) {
__asm__("mov rax, 68");
__asm__("syscall");
}
int msgsnd(int msqid, void *msgp, unsigned long msgsz, int msgflg) {
__asm__("mov rax, 69");
__asm__("syscall");
}
//////////////////////////////////////////
// kernel struct
struct list_head {
struct list_head *next;
struct list_head *prev;
};
struct msg_msg {
struct list_head m_list;
long m_type;
unsigned long m_ts;
void *next;
void *security;
};
//////////////////////////////////////////
// user struct
struct msgbuf{
long mtype;
char mtext[SIZE];
};
//////////////////////////////////////////
int main(void) {
system("cat /proc/slabinfo > a");
int i; struct msgbuf m;
for (i=0; i<100; i++) {
int qid = msgget(i, IPC_CREAT | 0666);
m.mtype = 1;
memset(m.mtext, 0x41, sizeof(m.mtext));
// SIZEを1024から変更すると,確保されるkmalloc-NのNも変わる
msgsnd(qid, &m, SIZE, 0);
}
system("cat /proc/slabinfo > b");
system("diff a b");
}
```
# 使えるグローバルな関数テーブルについて
さて,ここまでは構造体そのものを見てきました.いずれも`kmalloc()`で確保されるものばかりなので,kUAFとの相性は良いですが,そのアドレスは一見ランダムに見えてしまう,という問題があります.
カーネルのヒープにおけるランダム性は厄介なもので,kASLRを突破した(カーネルベースを特定した)としても,まだランダムに見えます(電源が入りOSが起動し始めてからの1アクション毎にカーネルヒープからの確保/解放が発生し得るため,起動からexploit発動までの間に,少しでも動作に差分があると,全く同じ状況を再現できないという意味です.非同期で呼ばれるコールバック関数も多々あるので,それらの呼び出される順番まで完全に同じくすることは,現実的には不可能でしょう).
では逆に,kASLRを突破しさえすれば利用可能な,アドレス固定のグローバル変数で良い感じの関数テーブルは無いでしょうか.glibcでいえば`__free_hook`みたいな感じのポインタがあると非常に便利ですよね.
結論から言うと存在しますので,それを紹介します.但し最近はどんどん改善されて殆どread-onlyにされてしまい,現在私が知っているまともに使える関数テーブルは1つだけです.
尚,ここに挙げたもの以外でもカーネルのソースコードを`static struct .*ops`とかでgrepすると,それなりに良さげなものが出て来たりします.まだ色々あるはずなので,自分で探してみると面白いかもしれません.
## `ptmx_fops->function`
`/dev/ptmx`と関係のある,グローバルな関数テーブルです.
`/dev/ptmx`がオープンされた際にカーネル内で新規に作成される構造体は,デフォルトでこの関数テーブルへのポインタを持つ様です.
少し前まで使えましたが,現在のカーネルでは`__ro_after_init`修飾子がついているため,初期化後にread-onlyにされてしまい,悪用はできなくなりました.
- `ptmx_fops`
- `struct file_operations`型
- 構造 https://elixir.bootlin.com/linux/v4.17.2/source/include/linux/fs.h#L1704
- グローバル変数の定義 https://elixir.bootlin.com/linux/v4.17.2/source/drivers/tty/pty.c#L879
- 呼出方法
```C=
int fd = open("/dev/ptmx", O_NOCTTY|O_RDWR); //
close(fd); // ptmx_fops->releaseが呼ばれる
```
## `perf_fops->function`
次はperf(パフォーマンス測定)と関連のある,グローバルな関数テーブルです.
`perf_event_open()`によってカーネル内で新規に作成される構造体は,デフォルトでこの関数テーブルへのポインタを持つ様です.
`static const`なので本来はrodata領域に配置されるはずですが,昔のARMはrodataも`RWX`だったので,これも使えたっぽいです.
あまり使った記憶がないので,この記事を書いている時に「あれ,これ使えたっけ?」って思ったのですが,使った時の古いメモがありましたので,当時は多分使えたのでしょう.改めて調べたら,[ここのP13](https://www.syscan360.org/slides/2016_SG_Vitaly_Nikolenko_Practical_SMEP_Bypass_Techniques.pdf)では紹介されているので,やはり使える環境はあったらしいですね.私の勘違いじゃなくて良かった.
- `perf_fops`
- `struct file_operations`型
- 構造 https://elixir.bootlin.com/linux/v4.17.2/source/include/linux/fs.h#L1704
- グローバル変数の定義 https://elixir.bootlin.com/linux/v4.17.2/source/kernel/events/core.c#L4986
- 呼出方法
```C=
#include <unistd.h>
#include <linux/perf_event.h>
#include <asm/unistd.h>
struct perf_event_attr attr = {};
attr.type = PERF_TYPE_HARDWARE;
attr.size = sizeof(attr);
attr.config = PERF_COUNT_HW_CPU_CYCLES;
int fd = syscall(__NR_perf_event_open, &attr, 0, -1, -1, 0);
close(fd); // perf_fops->releaseが呼ばれる
```
## `dev_attr_ro->function`
デバイス毎に定義される,グローバルな関数テーブルです.
当該デバイスを開いた際,カーネル内で新規に作成される構造体は,デフォルトでこの関数テーブルへのポインタを持つ様です.
まだread-onlyにされていないため,exploitに利用可能です.
見出しでは`dev_attr_ro`としていますが,`dev_attr_rw`, `dev_attr_wo`も同様に利用可能だと思います(試した訳ではありませんが).
- `dev_attr_ro`
- `struct device_attribute`型
- 構造 https://elixir.bootlin.com/linux/v4.17.2/source/include/linux/device.h#L552
- グローバル変数の定義 https://elixir.bootlin.com/linux/v4.17.2/source/include/linux/device.h#L318 (マクロで定義されるためgrepしても出てこない,注意)
- 呼出方法
```C=
int fd = open("/sys/block/<target_block_device>/ro", 0); //
read(fd, buf, sizeof(buf)); // dev_attr_ro->showが呼ばれる
close(fd);
```
但しこのグローバル変数はマクロの結果として生み出されるものです.定義は以下のようになっています.
```C=
#define DRIVER_ATTR_RW(_name) \
struct driver_attribute driver_attr_##_name = __ATTR_RW(_name)
#define DRIVER_ATTR_RO(_name) \
struct driver_attribute driver_attr_##_name = __ATTR_RO(_name)
#define DRIVER_ATTR_WO(_name) \
struct driver_attribute driver_attr_##_name = __ATTR_WO(_name)
```
これらのマクロは様々なドライバ/モジュールから呼ばれており,つまり似たような関数テーブルがたくさん生成されます.従って,今どれが使われているか分かり辛いというデメリットがあります.
またこれらマクロで生成された関数テーブルを扱う関数は,概して特徴的な文字列が付近になく,カーネルをIDAに食わせても検索し辛いため,利用はお勧めしません.私は,どうしても他の攻撃ができない時に利用する,という感じのテクニックに位置づけています.
## `n_tty_ops->function`
[しゃろさん(@Charo_IT)](https://twitter.com/Charo_IT)が何かのCTF大会で見つけていた関数テーブルです.
tty関連の,グローバルな関数テーブルです.
コンソールやターミナルを開いた際,カーネル内で新規に作成される構造体は,デフォルトでこの関数テーブルへのポインタを持つ様です.
まだread-onlyにされていないため,exploitに利用可能です.
含まれる関数ポインタを単に飛ばしたいアドレスに書き換えればよいため,任意の読み書きができるケースではまずこれを狙うことが多いです.
- `n_tty_ops`
- `struct tty_ldisc_ops`型
- 構造 https://elixir.bootlin.com/linux/v4.17.2/source/include/linux/tty_ldisc.h#L169
- グローバル変数の定義 https://elixir.bootlin.com/linux/v4.17.2/source/drivers/tty/n_tty.c#L2445
- 呼出方法1
```C=
scanf("%c", &a); // n_tty_ops->readが呼ばれる
```
- 呼出方法2
```C=
fgets(buf, sizeof(buf), stdin); // n_tty_ops->ioctlが呼ばれる
```
## `tty_ldiscs[0]->n_tty_ops->function`
グローバルな関数テーブル(`n_tty_ops`)へのグローバルなポインタ(`tty_ldiscs[0]`)です.
`RW`かつグローバルなポインタ(`tty_ldiscs[0]`)が,先に説明した`n_tty_ops`のアドレスを保持しているので,リンクリストのunlinkバグを使っても,RIPを奪うことが出来ます.
- `tty_ldiscs`
- `struct tty_ldisc_ops* [30]`型
- グローバル変数の定義 https://elixir.bootlin.com/linux/v4.17.2/source/drivers/tty/tty_ldisc.c#L46
配列の0番目がtty用途を意味するので,ここを上書きしておけば良い
- `n_tty_ops`
- `struct tty_ldisc_ops`型
- 既に紹介済みなので,これ以降は省略
- 呼出方法
```C=
int fd = open("/dev/ptmx", O_RDWR|O_NOCTTY); // tty_ldiscs[0]->n_tty_ops->openが呼ばれる
```
### 補足
この説明だけだと,メリットが分かり辛いのでもう少し補足しましょう.
リンクリストのunlinkとは,`p->fd->bk = p->bk; p->bk->fd = p->fd;`みたいなやつです.双方向リンクリストから,`p`を外す際の処理です.
ここで`p->fd`や`p->bk`が正しくお互いを指しているか検証せずにunlinkをしている場合(safe unlinkingをしていない場合),悪用可能です.これを悪用する攻撃は昔からunlink attackとして知られており,`p->fd`や`p->bk`をバグなどにより任意のアドレスへ書き換えておくと,`p->fd->bk`の位置に`p->bk`を書き込める,というものです.
しかし残念なことに,この処理は`p->fd`に対する処理と`p->bk`に対する処理が連続で行われます.つまり`p->fd->bk = p->bk;`で書き込むためには,`p->fd`は`RW`な位置のアドレスを指定しなければならず,続けて行われる`p->bk->fd = p->fd;`を通過するためには,`p->bk`も`RW`な位置のアドレスを指定しなければなりません.
まとめると,`p->fd`も`p->bk`も`RW`な領域でなくてはなりません.
さて先に紹介した`n_tty_ops`は関数テーブルであり`RW`な領域にありますが,この関数テーブルへ書き込むべきアドレスはコード,つまり`RX`な領域のアドレスです.`p->fd`を`n_tty_ops`にしておき,`p->bk`を飛ばしたいアドレスにすると,`p->fd->bk = p->bk;`は確かに上手く行きますが,その後の`p->bk->fd = p->fd;`では`RX`な領域に`RW`なポインタを書き込もうとすることになり,コケてしまうわけです.
そこでこの`tty_ldiscs[0]`が使える訳ですね.これは関数テーブルへのポインタで,`RW`な領域にあります.また関数テーブルである`n_tty_ops`も,`RW`な領域にあります.いずれも`RW`なので上手くunlinkが出来ます.これにより`tty_ldiscs[0]`はおかしな所を指してしまい,それを辿って関数テーブルが呼ばれることで,RIPを奪う事ができます.
# 終わりに
最近のカーネルのexploitでよく使われる,関数ポインタや構造体について紹介しました.少し古いテクニックも含まれており,徐々に使えなくなってきていますが,Linuxカーネルのセキュリティが改善方向に歩んでいる点については,個人的には非常に良いことだと考えています.CTFerとしては,知ってるテクニックが潰されて辛いという側面もありますけどね.
明日も私が記事を担当します.これまで紹介したテクニックを組み合わせることでようやく解けた,[WCTF 2018 - klist Write-up](https://hackmd.io/s/ryPu-Ll1V)です.良ければ見てくださいね.