今年は行ってきました RubyKaigi 2025!
そこで STORES さんブースで irb 宝探しをやっていて、今年の RubyKaigi はこれに全力を尽くしました。
https://ruby-quiz-2025.storesinc.tech
あまりにも面白かったので、どうやって解いていったか記録しておきます。でも、読むのに分かりやすいようにある程度編集しています。
もちろんネタバレ全開なので、嫌な人は回れ右ですよー。
このゲームに気づいたのは2日目のこと。面白そうなので早速手を出します。wasm で作られたWeb版 irb にコマンドを打ち込んでいくことで進めていくゲームのようです。
TreasureHunt game on IRB まず最初に puts "Hello STORES" とあったので、素直に打ち込んでみると、一つ目のお宝ゲット。
次に現在のワークスペースのメソッドや定数、変数等が見れる ls コマンドを打ち込んでみると、インスタンス変数に @stores を発見。試しにこれの中身を見てみると、二つ目のお宝を見つけました。
そしてとりあえず help コマンド打ってみると、TreasureHunt の項目に、th-search コマンドを発見。"Search for treasures in the world." とあるので、このコマンドを打ってみます。出てきたヒントは ls コマンドを打ってみろってことだったので、もうやったなぁと思いもう一度 th-search 。次はこのゲームは誰が提供してるのかというヒント。それもさっき @stores で見つけたなぁ。もう一度 th-search 。そうすると、今度は川に行け、とあるので、 cd River で ls 。すると、River クラスのメソッドなどが見えます。その中に treasure という如何にもなメソッドがあったので実行し、三つ目のお宝をゲット。
この River には色々とあり、次に encoded メソッドを動かしてみると、エンコードされた文字列が。これどうやればデコードできるかなぁと思いつつ、もう一つのメソッド bottom を動かしてみると、今度は Tablet.read を実行しろと。そうしたらあるコードと共に Tablet.interpreter を動かせと。それを実行してみると、先ほどのコードの実行方法とサンプルコードが書いてある。でも、これを機械的に実行する方法がわからないので、まだ与しやすそうな encoded メソッドで出てきた文字列のデコードに取り掛かります。パッとみて解らなかったのが今からすると悔しいですが、あれこれ試して Base64 デコードを試し、四つ目のお宝をゲット。
そして Tablet.read に戻りますが、やっぱり機械的にどうすればいいのかよく分からない。どうコマンドを打てばこのコードが実行できるのかが謎だと思い、これもまだしばらく放置。 th-search を打ってみます。Base64が便利と出てきて、それはさっきやったなと思いもう一度 th-search 。そうしたらアスキーアートと思しき文字列が。IRBで実行できるっぽいのでコピペすると、綺麗な五つ目のお宝をゲット。
それから Tablet の問題に戻ります。で、ふと気づきます。この read と interpreter で出てくる謎コード、 interpreter で書いてある通りに手で演算すればええやん、と。で、そう思ってよくみると Numbers are in base 36 (0-9, a-z) とある。これがまた謎なわけですが、足し算をするからにはアルファベットを数字に直さねば。というわけで Ruby リファレンスの String のページをみて base でページ検索すると to_i メソッドが引っかかります。これかな、と思いつつ、以下のように interpreter にあったコードを実行。
memory = 0
memory += " z " .to_i(36 )
memory += " z " .to_i(36 )
memory += " 2 " .to_i(36 )
print memory.chr
ここまでやって H と出てきたことで、当たりを掴んだっぽいと確信します。このノリで手動インタープリターを実行し、 interpreter にあったサンプルコードが HELLO にデコードできたのを確認して、 read にあったコードを実行。六つ目のお宝をゲットしました。
次は th-search のヒントに従って Keynote へ。ls を打つと、CP290_TABLE と LOCATION という定数が見えます。CP290_TABLE は0から255までの数字と文字の対照表。 LOCATION を打つと treasure.txt へのパスが書いてあるので、とりあえず File.read を使ってファイルの中身を確認します。
すると、\x で区切られた二桁の16進数が見える。なので、とりあえず16進数のところだけもらうか、と思い、以下を実行。
f = File .read(" /home/me/treasure.txt " )
s = f.to_s.split("\x" )
ところがこれがなぜかエラー。上手くいきません。
これは相当苦戦しましたが、最終的にはなんとなく Ruby リファレンスの File のページを眺め、each_byte を見つけてひらめきます。f.each_byte{ |x| p x } とやってみると255までっぽい数字の並びが!ここまで来ればあとは簡単。以下のスクリプトを実行します。
File .open(" /home/me/treasure.txt " ) do |io|
ans = []
io.each_byte { |x| ans << CP290_TABLE [x] }
ans.join
end
これで七つ目のお宝を見つけました。
それから、これは本当にたまたまだったのですが、このゲーム時々ハングするわけです。なのでリロードをするのですが、見つけたお宝はそのまま状態が保存されています。だったらコードの実行履歴も保存されてないかなぁ、と思ってリロード後に上キーを押してみると…。八つ目のお宝ゲット。これを見つけたのは運がよかったです。
そして th-search は何度も実行するとヒントがループするわけですが、最後のヒントとして th-search --more をやってみろとあるわけです。それをやってみると、TreasureDetector というクラスを発見。早速 cd して ls します。すると hint というメソッドが。実行すると TreasureDetector クラスに nop というクラスメソッドを実装してくれというヒントが。これは何もしないよ、ってことだったので、以下のメソッドを実装。
def self .nop
end
そしてから use コマンドを打ってみると、九つ目のお宝ゲット。
そして他になんかないかなー、と今まで巡ってきたところもよーく見てみると、River クラスに trigger という謎メソッドがあります。そのまま実行してみると引数が必要な様子。でも引数がどうにも分かりません。show_doc してから trigger としても何もドキュメントが出てこない。仕方ないので ls して、今度は @hooks というインスタンス変数に目をつけました。これの中身はハッシュのよう。explore をキーとして値には何かの Proc の配列が入っています。なので、 @hooks[:explore][0].call で十個目のお宝を見つけました。
そして今度は Tablet に移動し、ReverseSide という何かを見つけます。そこへ移動して ls してみると、read と sentence というメソッドが。両方を確認して、これはやるべきことが割とすぐにわかったので、 String#match を使って treasure の部分だけコピペして th-capture に渡しました。これで11個目。今思うと、これにも "You found a treasure!: "って表示する何かがあったのかも知れないですが、それは見つけてないです。
それから、cd IRB で IRB クラス自体も覗いてみました。そこに @irbrc_files というインスタンスメソッドが。これを見てみると .irbrc へのパスがあります。なので、
f = File .read(" /home/me/.irbrc " )
puts f
してみると、なんかファイルが壊れてるように見えます。なので、これを直すのかなぁと思って手元のエディタで直して File.write してみるのですが何も変わらず。でも、IRBのメソッドを動かしてみると時々動かないものがあって、エラーメッセージの最後に "Maybe IRB bug!" ってあるんです。これを直すのかなぁ、でも irbrc ファイル直してもエラーは変わらんなぁ、って頭を抱えながらKaigi二日目の晩は過ぎて行きました。
翌朝3日目の RubyKaigi 、朝イチで STORES さんのブースに突撃します。そして、ブースにいた方に11個までは自力で見つけたけどあとがわからん、ということで、現在見つけている treasure を見せながらお話をしました。
まず River.trigger の謎ですが、これは引数に :explore を渡してみると、すでに見つけていた @hooks の中身の treasure が出てくるというものでした。
そしてこれ見つけてないみたいですね、と教えてもらったのが、 th-search で出てくるヒントのうち、この問題は誰が提供しているかというやつ。これはもう @stores で見つけたのでは?と思いながら、 STORES と打ち込んでみると…12個目のお宝発見。マジかー!!ってなりました。
最後の一つは irb についてとても詳しくないと見つからないという話だったのですが、 .irbrc が壊れてるのを直しても変わらないという話をしたら、それが壊れて見えるのは irb の表示の問題ですという返答でした。え、じゃあ13個目のお宝はどこに?となったのですが、そこは自分で探してみてください、ちなみにゲーム中でもノーヒントです、というご返答をいただきました。
なので頑張ってみた結果、その日の夜になってようやくコマンドのサジェストから cd TreasureHunt ができることを発見し、TreasureHunt クラスへ行ってみたところ、TREASURE_DIGESTS という定数を発見。みてみると、13個の配列に暗号化された文字列が入っています。これが答えか、最後の一つをデコードするだけだ!と思ってツイートし、就寝。ちなみに、正当な見つけ方はこの時点でもう諦めてます。
そしてDay4、帰りの新幹線でまたデコードに取り組みます。暗号化された文字列は0からfまでの16個の数字が使われていて、どれも同じ桁数で64桁です。で、ネットであれこれ調べて64桁は SHA256 ですね、ということを発見。なので、最初に見つけていたお宝、 ST-HELLO を使って Digest::SHA256.hexdigest("ST-HELLO") とやってみると TREASURE_DIGESTS の最初の文字列に一致します。おっし、ということで、ついでにあれこれやっているうちに発見していた手法、 game = new をして、変数 game に TreasureHunt のインスタンスを代入します。そこから、game.treasures とやると今までに見つけたお宝たちが配列で読み取れるので、以下のスクリプトを実行します。
gems = game.treasures.dup
TREASURE_DIGESTS .reject{|td| gems.map{|gem | Digest ::SHA256 .hexdigest(gem )}.include?(td)}
そうすると、最後のお宝の暗号化文字列が手に入ります。
あとは、フォーマットを意識しつつブルートフォースをして答えを見つけ出すだけです。
もしお宝の文字列に絵文字なんかが絶対に入っていたら非常に辛かったですがそれはないだろうと予想して、AA-AAA みたいな文字列から順々に SHA256 エンコードして最後の暗号化文字列と比較していって、無事に最後のお宝も見つけました。ここのコードは割愛。
そうして現れた、Congratulations!
Congratulations! 最後はズルもしましたが、ゲームクリアはやっぱり嬉しいですね。
STORES さんの解説ブログ、楽しみにしています。