SPYxFAMILY面白いですよね。
連載が開始したときに表紙を見て「TISTAの人だ!」と感動したのを覚えています。5巻くらいで終わってしまったけど好きな漫画だったので、その人が描く新作ということで気になってましたし、過去作が好きという贔屓目なしに面白いと思います。
2020/09/13現在、SPYxFAMILYの謎解きキャンペーンであるSPY×FAMILY SPECIAL MISSIONというサイトが特設されています。ジャンプ+のアプリから読んでる方も話の後の広告枠に二進数っぽい文字列のバナーがあるので気になった方もいるでしょう。
いざサイトを開くと子供騙しのような、いわゆる「たぬきのかたたたき」の問題が表示されます。ページを開いて数秒経つと、ページ下部にこんな文言が出現します。
このページのどこかに さらにこの問題とは別の上級編への入り口があるぞ!! さあどこに隠されているかな? 紙版のコミックスを持っている人は どこかにヒントがあるかも…!?
おもしろFlashと同時期に流行ってた隠しリンクサイトの匂いがしますね。でもリンクは隠れてないし問題ですらないと言える簡単さです。紙の本を持ってようと持ってなかろうと見つかります。
前置きはこれくらいにして、上級編にたどり着いたところでやっと本題です。この上級編で一切謎を解かずに問題を解きます。
上級編の1問目をみて「また”たぬきのかたたたき”かよ…」とそっ閉じしないでください。この問題は簡単ですが問題は1問ではありません。問題は複数あります。2問目を真面目に解こうとするとけっこうな苦労が必要になると思います。まだ謎を解かれてない方はひとまず人力で挑まれてみるといいと思います。
頭脳と根気の代わりに使用するのはJavaScriptです。ただし健全性のために以下のルールを設けます。
まとめるとズルはするけど先方に迷惑はかけないということです。 先に結論を書くとJSの中に判定ロジックがありますし、JSが読める人なら秒で解けます。私が上級編を解き終わるまでにかかった時間は調査を含めても1-2分です。
試しに適当な値を入力してみるとエラーと表示されます。当然ですが成否判定ロジックが存在してます。
つまり入力値を判定するロジックがどこかにあるはずで、「謎を解くのではなく問題の判定ロジックを解き正解となる入力を見つける」ことが方針となります。
言ってしまえばこれはCapture The Flag(CTF)の一種であると捉えることも可能です。歯応えはありませんが、入門としてならいい問題かもしれません。
ちなみに、上級編のページのURLに1st_mission
という文字列が含まれているため、これを2nd_mission
に書き換えたら次の問題に行けると思うかもしれませんが、ルール3に違反するためやりません。うっかり内部管理用のURLを踏んでしまいIPアドレスが見えてしまったり全データを消し飛ばしてしまうなんて事故が起こらないとも言えないですし、仮にそんなことを起こしてしまった場合の責任なんて取れません。こういうズルをするときはあくまで一般ユーザを模倣しておくと安心です。
まずはクイズの仕組みの調査です。入力した文字列がこのサイト上どう扱われているかを外側から見ていきます。
まずは初級編に戻って、Chromeの開発者ツールを起動しネットワークを監視てから正解の文字列を送信します。このサイトはSPAではなく画面遷移を伴うためPreserve logオプションを有効にしておきましょう。正解を入力した結果以下のことがわかります。
サーバは入力値を受け取っていないため回答を判定していないことがわかります。絶対とは言いませんがかなりの高確率でJSで正解を判定しているはずです。
金銭やセッションが絡むわけでもない単発のキャンペーンのためにわざわざサーバを用意し、悪意を持った攻撃やDDOSのリスクに晒すのはナンセンスなので、JSで判定していると考えるのが筋でしょう。
以上から、そのデメリットがあることから初級編と上級編はどちらもJSで判定ロジックが書かれていると推測できます。案件の性質上わざわざ別アーキテクチャを用意するほどコストを割く意味がないですし。
ということでJSに答えが書いてあるという仮説をもとに上級者編のページのJSを読みます。
JSが読める人なら悩むまでもありません。お疲れ様でした。正解を手に入れ満足感を手に入れましょう。
解説するほどでもないですが一応コードを見ていきます。日本語のコメントがたっぷり書かれたjQuery製のコードが出てきます。この中でformのsubmitもしくは送信ボタンに何かしらのイベントを設定しているコードを探します。
$("button").on("click", function() {
var keyword = mission(97);
if ($('.keyword').val() == keyword ) {
window.location.href = '2nd_mission.html';
} else {
$('.red').addClass('error');
}
});
ここですね。mission(97)
を開発者ツールで実行すれば正解の文字列が手に入ります。なお、現在出題されているこれ以降の問題も同様の方法で解けます。当然正解となる文字列は違いますが、正解の文字列の見つけ方は同じです。
(੭ु˙꒳˙)੭ु⁾⁾
本記事ではmission関数の実装は完全にブラックボックスのままでも正解の文字列を得られます。ただ、正解をハードコードせずわざわざmissionという関数を用意し、しかも引数に数値を渡して周りくどいことをしているのが気になります。ということで中身を読んでみました。
すごそう。あまりに長くて全コードを記事に貼るとかさばるので代わりにURLを載せておきます。 https://www.shonenjump.com/p/sp/2020/spyfamily_challenge/_common/mission.js
とは言え難読化の中ではとてもシンプルな部類です。読んでみるともとのコードが小さいこともあり簡単に読めます。変換前のコードはおそらくこんな感じです。
直接的な答えを書かないようにfromCharCodeの部分だけ復元せずにそのまま残してあります。
function mission(key) {
if (key == 94){
return String.fromCharCode(12404, 12540, 12394, 12387, 12388);
}
if (key == 97){
return String.fromCharCode(83, 84, 69, 76, 76, 65);
}
if (key == 921){
return String.fromCharCode(12365, 12363, 12435, 12375, 12419);
}
}
完全に理解したのでこれで終わります。
mission関数の内部実装は難読化されているのになぜ肝心のクイズの判定処理が難読化されていないのか気になりましたが、その心を知るすべはありません。
もしJSが全て難読化されており、もう少し複雑に難読化されていたらもう少し手応えのある問題になっていたかもしれません。