はじめに
PHPerによるPHPerのためのお祭り
PHPerチャンレンジ
開催期間中に公式ページ、スポンサーブログ、会場、twitterなどにシャープから始まる文字列が記載されており、それを探し出して専用のフォームに投稿して点数を稼いで準備を競うゲームです。
過去にも開催していて大変盛り上がりました。
僕も過去に参加したことがありましてその時の記事はこちらです。
今年は健闘し4位まで上がることができました!
基本的には目grep力、勘所の勝負となります。
実際何度も見返したはずのパンフレットにまだまだトークンが残っていて絶望しました。
目grep力を鍛えなければ…!
プログラミング問題
スポンサーブログにてトークンを表示していて、それをコピペで貼り付けることが多かったんですが
以下の企業様はプログラミングの問題を作成しており、それを解くことでトークンを得られる仕組みになっておりました。
本記事ではそちらを解説しようと思います。
一部解いてない問題や正攻法でない解き方が含まれます。
デジタルサーカス様
brainf_ck.php
こちらは単純にプログラムを実行するだけで答えが出ます。
ただしPHP8.1以上で実行しないとコケるので注意。
自分はPHP8.0ではじめ実行してエラーになりました。
PHP8.1のimageをdocker hubから持ってきてそこで実行したら無事にトークンが表示されました。
riddle.php
こちらはちょっと頭を使います。
問題としては適切なNの値を入力して実行するとトークンが表示されるよ、というものでした。
ヒントとしては「トークンの最初の文字は#(シャープ)だよ」というもの。
つまり1文字目が#(シャープ)の形になるようなNの値を探せば良い、ということになります。
# #
#####
# #
#####
# #
になる文字列は
# # ##### # # ##### # #
これなので1文字目がこれになるNをループ回して見つければOK。(空白の数に注意)
なのでこんな感じのコードを組んでNを探しました。
for($n=0;$n<9999999999;$n++){
$x = 0x14B499C;
$x = $x ^ $n;
$x = sprintf('%025b', $x);
$x = str_replace(search: ['0', '1'], replace: [' ', '#'], subject: $x);
if($x === " # # ##### # # ##### # # ") echo $n;
}
exit;
結果Nが「31777398」だと最初の文字がシャープになることが分かりました。
なのでこの値にして実行してみると…
縦長で見づらいですが「#WELOVEPHP8.1」と出てくるのでこれが答えです。
toquine.php
こちらも実行するだけですが実行方法に少し工夫が必要です。
まずそのまま実行すると
シャープのみが表示されることが分かります。
これをコメントに書いてあるようにパイプで繋いで実行すると…
php toquine.php |php|php|php|php|php|php|php|php|php|php|php|php|php|php|php|php|php|php|php|php|php|php|php|php|php
「#PHP:HYPERTEXTPREPROCESSOR」と出てくるのでこれが答えです。(見づらいのでスクショを横向きにしています。)
Fusic様
最短経路問題でした。
すみません、こちらはプログラムで解いた訳ではありません。
解こうと思ってエディタに貼り付けてシャープをハイライトしたところスタートとゴールまでの経路が数パターンしかなく
これならプログラム書かずに目で見た方が早いだろうってことで目で見ました。
結果この経路が最短っぽいな、ということになりました。
#★Ph9_i5_pR0f3z$1oN4l_h07y_P0w3r!
ただこのトークン何度やっても正解しない…。
色々試した結果はじめの「★」と末尾の「!」が不要だったようでした。(ちょっと分かりにくかったです。)
#Ph9_i5_pR0f3z$1oN4l_h07y_P0w3r
が正解でした。
ちなみにこれ文字列っぽいんですがなんて書いてあるんでしょうね?
php_is_professional_holy_power
かな?
トラーナ様
5問ありました。
本家様より解説記事があがっております。
トラーナさんの5問はかなり歯ごたえがありました。
問1
<?php
$bytes = [0x61, 0x4c, 0x45, 0x45, 0x46, 0x79, 0x61, 0x79, 0x4c, 0x5b, 0x62, 0x48, 0x40, 0x4e, 0x40, 0x7e, 0x4c, 0x68, 0x5b, 0x4c, 0x7d, 0x46, 0x5b, 0x48, 0x47, 0x48];
$string = '';
for ($i = 0; $i < count($bytes); $i++) {
$string .= chr($bytes[$i] ^ <1>);
}
if (md5($string) !== 'a8f101dec277521c969386effb2c8397') {
echo "ハズレです!";
return;
}
echo "#{$string}";
これはシンプルに<1>に当てはまる整数を見つければ良いので先程と同様に愚直にループを回すと見つけられます。
<?php
$bytes = [0x61, 0x4c, 0x45, 0x45, 0x46, 0x79, 0x61, 0x79, 0x4c, 0x5b, 0x62, 0x48, 0x40, 0x4e, 0x40, 0x7e, 0x4c, 0x68, 0x5b, 0x4c, 0x7d, 0x46, 0x5b, 0x48, 0x47, 0x48];
for($n=0;$n<256;$n++){
$string = '';
for ($i = 0; $i < count($bytes); $i++) {
$string .= chr($bytes[$i] ^ $n);
}
if (md5($string) !== 'a8f101dec277521c969386effb2c8397') {
//echo "${n}: ハズレです!\n";
//return;
continue;
}
echo "#{$string}\n";
}
「#HelloPHPerKaigiWeAreTorana」が答えです。
問2
<?php
$string = '<1>';
$sum = array_sum(
array_map(
static fn (string $char) => ord($char),
<2>($string)
)
);
$additional = implode(
array_map(
static fn (string $numberValue) => chr(0x63 + ((int) $numberValue)),
<2>((string) $sum)
)
);
$string = "{$string}-{$additional}";
if (md5($string) !== '2fa885e32bc24b26f9dff3a47efb0d08') {
echo "ハズレです!";
return;
}
echo "#{$string}";
こちらは1問目の答えを使い、さらに正しい関数を入れろという問題です。
なのでまず<1>には1問目の答えなので「HelloPHPerKaigiWeAreTorana」が入ります。
次に<2>について。
ヒントとしてはarray_mapの第2引数であるという点です。
array_mapの第2引数はarrayをとりますが、入れようとしているのはstringです。
なのでstringからarrayに変換する関数が入ることが推測されます。
最初arrayにキャストしてみたんですがハズレになりました。
じゃあ文字列を1文字ずつのarrayに変換したらどうかということで「str_split」を入れたところ動きました。
<?php
$string = 'HelloPHPerKaigiWeAreTorana';
$sum = array_sum(
array_map(
static fn (string $char) => ord($char),
str_split($string)
)
);
$additional = implode(
array_map(
static fn (string $numberValue) => chr(0x63 + ((int) $numberValue)),
str_split((string) $sum)
)
);
$string = "{$string}-{$additional}";
if (md5($string) !== '2fa885e32bc24b26f9dff3a47efb0d08') {
echo "ハズレです!\n";
return;
}
echo "#{$string}\n";
「#HelloPHPerKaigiWeAreTorana-ehdf」こちらが答えです。
問3
<?php
$string = '<1>-<2>';
[$part1, $part2, $part3] = <3>('-', $string);
$calculator = fn (string $stringValue) => array_sum(
array_reverse(
array_map(
fn (string $numberValue) => crc32($numberValue),
str_split($stringValue)
)
)
);
$string = substr(md5($calculator($part1) + $calculator($part2) + $calculator($part3)), 0, 10);
if (md5($string) !== '8037d28b5a754eeacd1ee90fb1246610') {
echo "ハズレです!";
return;
}
echo "#{$string}";
<1>に入るのは2問目の答えなので「HelloPHPerKaigiWeAreTorana-ehdf」
<2>に入るのは「nfc」です(プロポーザルはこちら)
<3>について。
こちらも先程と同様に関数を埋める問題です。
引数にハイフンと文字列をとり、返り値が配列になる関数です。
$stringが「HelloPHPerKaigiWeAreTorana-ehdf-nfc」で第1引数がハイフンなのでハイフンで千切った配列にするんだろうな、という予想がつきます。
よって<3>に入るのは「exlode」です。
<?php
$string = 'HelloPHPerKaigiWeAreTorana-ehdf-nfc';
[$part1, $part2, $part3] = explode('-', $string);
$calculator = fn (string $stringValue) => array_sum(
array_reverse(
array_map(
fn (string $numberValue) => crc32($numberValue),
str_split($stringValue)
)
)
);
$string = substr(md5($calculator($part1) + $calculator($part2) + $calculator($part3)), 0, 10);
if (md5($string) !== '8037d28b5a754eeacd1ee90fb1246610') {
echo "ハズレです!";
return;
}
echo "#{$string}\n";
「#11e7eb18b2」こちらが答えです。
問4
<?php
$string = '<1>';
$string = substr(md5(str_replace(<2>('a', 'f'), '', $string)), 0, 10);
if (md5($string) !== 'b310309130966447075369fb9d56b437') {
echo "ハズレです!";
return;
}
echo "#{$string}";
<1>に入るのは3問目の答えなので「11e7eb18b2」
<2>について。
こちらも関数を埋める問題。
str_replaceの第1引数に使っているもの。
str_replaceの第1引数にはstring or arrayが入ります。
$string
から第1引数にきたものを空文字に変換する、という処理になります。
$string
の中身は「11e7eb18b2」なので何となく「あーじゃあaからfを空文字に置換するのかなー」という予想がつきます。
aからfという範囲を返す関数は「range」です。
<?php
$string = '11e7eb18b2';
$string = substr(md5(str_replace(range('a', 'f'), '', $string)), 0, 10);
if (md5($string) !== 'b310309130966447075369fb9d56b437') {
echo "ハズレです!";
return;
}
echo "#{$string}\n";
「#6458e00ac0」こちらが答えです。
問5
<?php
$string = '<1>';
$string .= implode(array_reverse(str_split($string))) . '<2>';
$map = [
'e971773f' => 0x7d,
'cbea68d7' => 0x46,
'7f8abdb1' => 0x5b,
'a42a75c2' => 0x48,
'29d01a37' => 0x47,
'4a2414ee' => 0x48,
'1be678b5' => 0x61,
'a347b1db' => 0x40,
'00f56a27' => 0x5b,
'35497491' => 0x40,
'23a0dee4' => 0x47,
'baa98f5e' => 0x4e,
'cdaebfc8' => 0x08,
];
$split = str_split($string, 2);
$result = '';
for ($i = 0; $i < count($split); $i++) {
$result .= chr($map[<3>(<4>, crc32($split[$i]))] ^ <5>);
}
if (md5($result) !== 'd5ff5cec18c54a4fdc90f8ce1462e6b4') {
echo "ハズレです!";
return;
}
echo "#{$result}";
これが難問で未だに解けていません。
が力技で解いて答えだけは分かっています。
ので僕が解いた力技を解説します。
まず<1>は4問目の答えなので「6458e00ac0」
<2>は予測すらつかず。
<3>,<4>に関しては結果が$mapのキーになることから8文字に変換する何か、だと予想しています。
hash('crc32', xxx)
みたいな感じかな〜と思って以下を試しがヒットせず。
$hoge = hash_algos();
foreach($hoge as $alg){
if(isset($map[hash($alg, 3916527423)])){
var_dump('hit');
var_dump($alg);
}
}
exit;
考えが及ばず力技を思いつく。
そもそも<3>,<4>の部分の変換が分からなくても結局$mapのキー名になりその値を
^<5>
した値を使っていく。
$mapは13個なので並び順を考慮すると13の階乗で6227020800通り。
これに対して<5>の0~255を全て試せば答えが一意に出る、ということに気付く。
試しに以下のコードを実行
<?php
$map = [
0x7d,
0x46,
0x5b,
0x48,
0x47,
0x48,
0x61,
0x40,
0x5b,
0x40,
0x47,
0x4e,
0x08,
];
for ($n = 0; $n < 256; $n++) {
$result = '';
for ($i = 0; $i < count($map); $i++) {
$result .= chr($map[$i] ^ $n);
}
if (md5($result) !== 'd5ff5cec18c54a4fdc90f8ce1462e6b4') {
//echo "ハズレです!\n";
//return;
continue;
}
echo "#{$result}\n";
}
なんと解けてしまいましたw
結局キーの情報はさほど重要ではなく、その値を変換した結果がどうなるか、というところに絞るとこれで解けます。
今回は運良く$mapが正解の順に並んでいたので13の階乗分を全て試さなくても答えにたどり着くことができました。
仮に正解の順でなかったとしても13の階乗分をループで回せば力技で解けていたと思います。
結果として5問目は1~4の答えもいらずに単体で解ける問題となっていました。
ただ明らかにズルなので正攻法の解説は本家様におまかせします。
「#ToranaHiring!」こちらが答えです。
まとめ
実際全問解けて気持ちよかったですがトークン自体の価値はそこまで高くなかったため
PHPerチャンレンジを本気でやるなら問題解くより穴が空くほど公式パンフレット見た方が良さそうです。
来年もがんばります!