いつぞや開催されたCakeCTF 2021。始まってから初めて zer0pts 主催だということを知りました。InterKosen も zer0pts も Cake もやって、やばいですね。ところで、ここ 2 ヶ月ぱそこんを触っていなかったため、ぱそこんの立ち上げ方もブログの書き方も忘れてしまいました。もちろん pwn もすべて忘れました。よって、このエントリは writeup ではなく、チームの人(主にmora さん が)が解いているのを BIG カツを食べながら見ていた感想になります。

hwdbg Link to this heading

/dev/memへの任意の write ができるため、物理アドレスに対して直で書き込みができるよという問題。2 ヶ月ぶりの kernel 問(実際は、semi-kernel 問)だったため、色々と思い出しながら問題を見ました。いくつかの実験の後に、シンボルの物理アドレスは(少なくとも kernelland のシンボルに関しては)実行毎に不変であるという確証が持てたため、下のどれかの作戦で行こうと思いました。

  • kernelland のデータ領域(modprobe_path)を書き換え、root で任意スクリプトを動かす。
  • kernelland の stack を書き換え、制御を奪う。
  • kernelland の UID の変更を行う関数の code を書き換え、任意プロセスが root になれるようにする。
  • kernelland の code を書き換え shellcode を入れる。
  • hwdbg のコードを書き換え、shellcode を入れる。

AAW のため色々と候補はありますが、/dev/memへの書き込みが任意に出来るだけならこのデバイスファイルの権限を変えるだけの問題でも良いはずで、おそらく suid が hwdbg バイナリについてることが本質で、hwdbg 自体のコードを書き換えるのが想定解なのかなぁと思いました。但し、ユーザランドのプロセスは実行された順番によって物理アドレスが多少変わると思うので、多少の bruteforce が必要な気がしたので、ぼくは kernelland の方で解きたいと思っていました。が、その間にmora さん が hwdbg の書き換えによって解いたので無職になりました。

しかし、時間がかかったのにはいくつか理由があって:

  • modprobe_pathが見つからなかった。kallsymsで見えなくて、nmでも見えなかったため見つからなかった。多分存在はしていたと思うけど、CONFIG_KALLSYMS_ALLが無効になっていたのか、text シンボルしか見れなかった。あれ、こういう場合ってシンボルのオフセット探す方法どうやるんでしたっけ、教えてください。いい感じの関数で break して頑張って探すしかない?
  • modprobe_pathは const にすることができるため、今回はその設定だと思った。こういう時に、どのシンボルを書き換えると楽に LPE できるか知らなかった。
  • ktext 領域への書き込みでフォルトが起きる。

【追記 2021.08.30】 modprobe_pathは最近の kernel(5~)で行方不明になっているらしいです。 案の定こいつを使う関数に break を貼っておいて追うのが良いらしいです。 また、modprobe_pathは今回 static になっていなかったらしいです。 因みに、CONFIG_KALLSYMS_ALLが有効な場合はちゃんと/proc/kallsymsから読めることは最近の kernel でも確認済みです。 デフォルトの config がどっかで変わったのかな。 あとで調べます。

それから、フォルトについてはCONFIG_STRICT_DEVMEMというコンフィグが立っていたらしいです。 この場合 PCI/BIOS/data(.data,.bss)へのアクセスのみが許可され、kernelland の text セクション等への書き込みは拒否されるようです。 知りませんでした。 4.x からあるらしいので、勉強不足です。

modprobe_pathについては、オフセットを調べるのは非常に簡単です。 二度と忘れないようにやり方を github にまとめておきました。

【追記おわり】

とりわけ、ktext への書き込みでフォルトが起こるのがよく分からずに時間を溶かしてしまいました。ぼくの認識では物理アドレスへのアクセスはページテーブルとかを介さないため、よってアクセス権限もフォルトも無縁の世界と思っていました。

fault.txt
 1/ # cat /proc/iomem
 200000000-00000fff : Reserved
 300001000-0009fbff : System RAM
 40009fc00-0009ffff : Reserved
 5000a0000-000bffff : PCI Bus 0000:00
 6000c0000-000c99ff : Video ROM
 7000ca000-000cadff : Adapter ROM
 8000cb000-000cb5ff : Adapter ROM
 9000f0000-000fffff : Reserved
10  000f0000-000fffff : System ROM
1100100000-03fdffff : System RAM
12  02600000-03000c36 : Kernel code
13  03200000-033b3fff : Kernel rodata
14  03400000-034e137f : Kernel data
15  035de000-037fffff : Kernel bss
1603fe0000-03ffffff : Reserved
1704000000-febfffff : PCI Bus 0000:00
18  fd000000-fdffffff : 0000:00:02.0
19    fd000000-fdffffff : bochs-drm
20  fe000000-fe003fff : 0000:00:03.0
21    fe000000-fe003fff : virtio-pci-modern
22  feb00000-feb7ffff : 0000:00:03.0
23  feb90000-feb90fff : 0000:00:02.0
24    feb90000-feb90fff : bochs-drm
25  feb91000-feb91fff : 0000:00:03.0
26fec00000-fec003ff : IOAPIC 0
27fed00000-fed003ff : HPET 0
28  fed00000-fed003ff : PNP0103:00
29fee00000-fee00fff : Local APIC
30fffc0000-ffffffff : Reserved
31100000000-17fffffff : PCI Bus 0000:00
32/ # hwdbg mw 8 2600000
33AAAAAAAA
34BUG: unable to handle page fault for address: ffff938c42600000
35#PF: supervisor write access in kernel mode
36#PF: error_code(0x0003) - permissions violation
37PGD 3801067 P4D 3801067 PUD 3802067 PMD 80000000026000e1
38Oops: 0003 [#1] SMP PTI
39CPU: 0 PID: 144 Comm: hwdbg Not tainted 5.10.7 #2
40Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.13.0-1ubuntu1.1 04/01/2014
41RIP: 0010:memset_orig+0x72/0xb0
42Code: 47 28 48 89 47 30 48 89 47 38 48 8d 7f 40 75 d8 0f 1f 84 00 00 00 00 00 89 d1 83 e1 38 74 14 c1 e91
43RSP: 0018:ffffa33780453e30 EFLAGS: 00000246
44RAX: 0000000000000000 RBX: 0000000000000000 RCX: 0000000000000000
45RDX: 0000000000000008 RSI: 0000000000000000 RDI: ffff938c42600000
46RBP: ffffa33780453e50 R08: 4141414141414141 R09: 0000000000000000
47R10: ffff938c42600000 R11: 0000000000000000 R12: ffff938c42600000
48R13: 0000000000000008 R14: ffff938c41e92100 R15: 0000000000000008
49FS:  00000000004076d8(0000) GS:ffff938c42400000(0000) knlGS:0000000000000000
50CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
51CR2: ffff938c42600000 CR3: 0000000001e7a000 CR4: 00000000003006f0
52Call Trace:
53 ? _copy_from_user+0x70/0x80
54 write_mem+0x96/0x140
55 vfs_write+0xc2/0x250
56 ksys_write+0x53/0xd0
57 __x64_sys_write+0x15/0x20
58 do_syscall_64+0x38/0x50
59 entry_SYSCALL_64_after_hwframe+0x44/0xa9
60RIP: 0033:0x403744
61Code: 07 48 89 47 08 48 29 d1 48 01 d7 eb df f3 0f 1e fa 48 89 f8 4d 89 c2 48 89 f7 4d 89 c8 48 89 d6 4c0
62RSP: 002b:00007ffec3e615a8 EFLAGS: 00000246 ORIG_RAX: 0000000000000001
63RAX: ffffffffffffffda RBX: 000000000040136a RCX: 0000000000403744
64RDX: 0000000000000008 RSI: 00007ffec3e61600 RDI: 0000000000000003
65RBP: 00007ffec3e62610 R08: 0000000000000000 R09: 0000000000000000
66R10: 0000000000000000 R11: 0000000000000246 R12: 00007ffec3e62688
67R13: 00007ffec3e626b0 R14: 0000000000000000 R15: 0000000000000000
68Modules linked in:
69CR2: ffff938c42600000
70---[ end trace afbab88ef6185423 ]---
71RIP: 0010:memset_orig+0x72/0xb0
72Code: 47 28 48 89 47 30 48 89 47 38 48 8d 7f 40 75 d8 0f 1f 84 00 00 00 00 00 89 d1 83 e1 38 74 14 c1 e91
73RSP: 0018:ffffa33780453e30 EFLAGS: 00000246
74RAX: 0000000000000000 RBX: 0000000000000000 RCX: 0000000000000000
75RDX: 0000000000000008 RSI: 0000000000000000 RDI: ffff938c42600000
76RBP: ffffa33780453e50 R08: 4141414141414141 R09: 0000000000000000
77R10: ffff938c42600000 R11: 0000000000000000 R12: ffff938c42600000
78R13: 0000000000000008 R14: ffff938c41e92100 R15: 0000000000000008
79FS:  00000000004076d8(0000) GS:ffff938c42400000(0000) knlGS:0000000000000000
80CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
81CR2: ffff938c42600000 CR3: 0000000001e7a000 CR4: 00000000003006f0
82Kernel panic - not syncing: Fatal exception
83Kernel Offset: 0xa800000 from 0xffffffff81000000 (relocation range: 0xffffffff80000000-0xffffffffbffffff)

これを見ると、そもそもに書き込みたいところ以外でフォルトが起きていて、意図しないマッピングになっている感じがします(というか、物理に直接書いてるのにマッピングって何よ)。

自前 kernel でデバッグしてみようとしたところ、以下の感じになりました。

not-write.txt
 1/ # cat /proc/iomem
 200000000-00000fff : Reserved
 300001000-0009fbff : System RAM
 40009fc00-0009ffff : Reserved
 5000a0000-000bffff : PCI Bus 0000:00
 6000c0000-000c99ff : Video ROM
 7000ca000-000cadff : Adapter ROM
 8000cb000-000cb5ff : Adapter ROM
 9000f0000-000fffff : Reserved
10  000f0000-000fffff : System ROM
1100100000-03fdffff : System RAM
12  01000000-01e037d6 : Kernel code
13  02000000-02378fff : Kernel rodata
14  02400000-026b807f : Kernel data
15  02c67000-02dfffff : Kernel bss
1603fe0000-03ffffff : Reserved
1704000000-febfffff : PCI Bus 0000:00
18  fd000000-fdffffff : 0000:00:02.0
19  fe000000-fe003fff : 0000:00:03.0
20  feb00000-feb7ffff : 0000:00:03.0
21  feb90000-feb90fff : 0000:00:02.0
22  feb91000-feb91fff : 0000:00:03.0
23fec00000-fec003ff : IOAPIC 0
24fed00000-fed003ff : HPET 0
25  fed00000-fed003ff : PNP0103:00
26fee00000-fee00fff : Local APIC
27fffc0000-ffffffff : Reserved
28100000000-17fffffff : PCI Bus 0000:00
29/ # hwdbg mw 8 1000000
30AAAAAAAA
31/ # QEMU 4.2.1 monitor - type 'help' for more information
32(qemu) xp/gx 0x1000000
330000000001000000: 0x4801403f51258d48

自前 kernel だとフォルトこそ置きていませんが、最後の QEMU monitor の結果からもわかるように、書き込みがおきていません。kernel を読んでみます。/dev/memへの write は、write_mem()@/drivers/char/mem.cが呼ばれ、書き込みのチェックが行われます。いくら/dev/memへの書き込みとはいえ、本当にどこにでも書き込めるわけではなく、ある程度のチェックは行われるようです。この中でpage_is_allowed()からdevmem_is_allowed()@/arch/x86/mm/init.cが呼ばれるのですが、なんかこいつが code セクションへの write を拒否してきます。因みに data セクションの場合でも同じでした。 理由は今のところ分かっていませんが、あとでもうちょっと深堀してなんか書きます。

というわけで、配布 kernel だとフォルトが起きて、かつ自前だとハンドラ内でアクセスが拒否されるため、kernel コードの書き換えができませんでした。理由が分かってないのでちゃんと調べたいですね。多分すごく初歩的な勘違いをしているような気がするんですが。 まぁ何はともあれ mora さんが解いてくれたので OK です。

author’s writeup によると、core_patternを書き換えて crash させることでmodprob_pathと同様にいけるらしいです。知らなかったので勉強になりました。ところでこいつのオフセットを楽に知るにはどうしたら良いんでしたっけ。

JIT4B Link to this heading

ある関数において変数の値が追跡されるため、ごまかして OOB アクセスさせたら勝ちの問題です。ebpf の verifier みたいなだなぁと思いました。ebpf だとシフトや and/xor 等にこれまでバグが見つかっていましたが、今回は四則演算と min/max のみの演算になっているので関係なさそうです。1 個ずつabstract.hppの range 処理を見ていき、おおよそバグがないように見えましたが、除算のところだけ気になりました。

abstract.hpp
 1  /* Abstract divition */
 2  Range& operator/=(const int& rhs) {
 3    if (rhs < 0)
 4      *this *= -1; // This swaps min and max properly
 5    // There's no function named "__builtin_sdiv_overflow"
 6    // (Integer overflow never happens by integer division!)
 7    min /= abs(rhs);
 8    max /= abs(rhs);
 9    return *this;
10  }

ぼくは range の下限が負に最大だと range を入れ替えた時に上限で overflow するんじゃないかと疑ってしまいましたが、除算内で呼ばれる掛け算ではちゃんと overflow の処理がされており、ちゃんと追跡不能でマークされていました。ここらへんで mora さんが問題を見始めたんですが、一瞬で abs 内では同様の overflow が実現できると気づいたため、瞬殺されました。range 内にだけ(つまり被除数にだけ)気を取られていて、除数の方で overflow が起きることに気づかなかったのが反省点です。因みに、コメントを大量に入れているところは CTF の文脈において本当にバグがないからココはあんまり見ても意味ないことを伝えている場合と、ただのフリでコメントがあるところにバグがある場合があるのですが、今回は後者寄りでした(厳密には同じところではないけど同じ関数内)。

それにしても、2 ヶ月ぶりの CTF で、mora さんが問題を解くのも久々に見たんですが、異常に早いですね。まじで無職で、あまりにもお腹が減ったので皆が取っておいた warmup を貰ってしまいました。

アウトロ Link to this heading

ぼくは何もしていませんが、他の人が強かったため TSG は 3 位だったみたいです。ところでチーム登録をする時に間違えてぼく個人の G-mail で登録してしまったため、swag 関係のメールが個人宛のメールに来てしまいます。ptr さんからメールが来るなんてきゅんきゅんしちゃいますよね。頑張って好きな犬種は何か聞き出そうと思います。因みにぼくは柴とスピッツとハスキーです。猫よりも犬派です。

参考 Link to this heading