From d30dfc89bf1b673b2fdc0638766b930adaec228c Mon Sep 17 00:00:00 2001
From: nsfisis
$ echo "#iwillblog" | php Q1.png >/dev/null
+ $ echo "#iwillblog" | php Q1.png >/dev/null
+ 無事に実行できていれば「#ModernPHPisStaticallyTypedLanguage」というトークンが得られる。 @@ -151,7 +152,9 @@ まずは素直に画像として見てみよう。全体は QR コードになっている。適当な QR コードリーダで読み込むと、次のようなテキストが表示されるはずだ。
-Guess password. $ echo "password" | php Q1.png >/dev/null
+ Guess password. $ echo "password" | php Q1.png >/dev/null
+ メッセージは、この画像の実行方法とこの問題でやるべきこと (パスワードの推測) を示している。 @@ -168,8 +171,10 @@ 不正なパスワードを使って実行してみると、次のようなエラーメッセージが表示される。
-$ echo "foo" | php Q1.png >/dev/null
-401 Unauthorized
+ $ echo "foo" | php Q1.png >/dev/null
+401 Unauthorized
+
すでに「解き方」の節で示したように、パスワードである PHPer トークンは「#iwillblog」である。これを与えて実行すると正解のトークンが得られる。
@@ -258,23 +263,27 @@
strings コマンドを使うと、隠されたデータを簡単に閲覧できる。
IHDR
--HHc
-<PLTE
-IDATx
-IEND
-<?php
-error_reporting(-1);
-$b = unpack('C*', file_get_contents(__FILE__));
-$w = $b[20]+2;
-$h = $b[24]+2;
-// (以下略)
+ IHDR
+-HHc
+<PLTE
+IDATx
+IEND
+<?php
+error_reporting(-1);
+$b = unpack('C*', file_get_contents(__FILE__));
+$w = $b[20]+2;
+$h = $b[24]+2;
+// (以下略)
+
IHDR や IEND が PNG 画像の一部で、<?php からが実際のプログラムになっている。もちろんこれを PHP プログラムとして動かすと、PHP タグより前にある PNG 画像としてのデータはそのまま標準出力へと出力されてしまう。それを防ぐため、QR コードを読み込んだときの実行方法
Guess password. $ echo "password" | php Q1.png >/dev/null
+ Guess password. $ echo "password" | php Q1.png >/dev/null
+
には標準出力を捨てるよう >/dev/null と指定されている。
@@ -291,107 +300,109 @@ $h = $b[24]+2;
画像の正体がわかったところで、画像に隠されていた PHP プログラムについて見ていこう。先ほどは一部しか記載しなかったので、全体を載せる。なお、ある程度ゴルフしながら書いたので、空白こそ残しているものの可読性は非常に低いことと思う。
<?php
-error_reporting(-1);
-$b = unpack('C*', file_get_contents(__FILE__));
-$w = $b[20]+2;
-$h = $b[24]+2;
-$cs = [];
-for ($y = 0; $y < $h; $y++)
- for ($x = 0; $x < $w; $x++)
- $cs[$y*$w + $x] = ($x*$y === 0 || $x === $w-1 || $y === $h-1)
- ? 0
- : $b[122+($y-1)*($w-1)+$x-1];
-$i = stream_isatty(STDIN)
- ? []
- : array_map(ord(...), str_split(trim((string) fgets(STDIN))));
-$m = [];
-$pc = 1*$w+1;
-$dp = 0;
-$cc = 1;
-$c0 = 1;
-$b = 0;
-$ns = 0;
-$o = '';
-while (true) {
- $ns++;
- if ($ns > 1e5) {
- echo "infinite loop detected\n";
- break;
- $c1 = $cs[$pc];
- $y = (6 + intdiv($c1-2, 3) - intdiv($c0-2, 3)) % 6;
- $x = (3 + $c1%3 - $c0%3) % 3;
- match (($c0 !== 1) * ($c1 !== 1) * ($y*3 + $x)) {
- 1 => $m[] = $b,
- 2 => array_pop($m),
- 3 => $m[] = array_pop($m) + array_pop($m),
- 4 => $m[] = (fn($x, $y) => $y - $x)(array_pop($m), array_pop($m)),
- 5 => $m[] = array_pop($m) * array_pop($m),
- 8 => $m[] = array_pop($m) === 0 ? 1 : 0,
- 11 => $cc *= pow(-1, array_pop($m)),
- 12 => $m[] = $m[count($m)-1],
- 13 => $m = (fn($n, $d, $m, $l) => [
- ...array_slice($m, 0, $l-$d),
- ...array_reverse([
- ...array_reverse(array_slice($m, $l-$d, $d-$n)),
- ...array_reverse(array_slice($m, $l-$n)),
- ]),
- ])(array_pop($m), array_pop($m), $m, count($m)),
- 15 => !empty($i) and $m[] = array_shift($i),
- 16 => $o .= sprintf('%d', array_pop($m)),
- 17 => $o .= sprintf('%c', array_pop($m)),
- default => 'nop',
- };
- $c0 = $c1;
- for ($j = 0; $j < 8; $j++) {
- $v = [];
- if ($c1 === 1) {
- $x = $pc % $w;
- $y = intdiv($pc, $w);
- $e = [($y+1)*$w-1, ($h-1)*$w+$x, $y*$w, $x][$dp];
- $z = [1, $w, -1, -$w][$dp];
- for ($ep = $pc; $ep !== $e; $ep += $z)
- if ($cs[$ep] !== 1) break;
- $ep -= $z;
- $pc = $ep;
- } else {
- $q = [$pc];
- $ep = $pc;
- while (!empty($q)) {
- $qq = array_pop($q);
- $v[$qq] = true;
- foreach ([$qq+1, $qq+$w, $qq-1, $qq-$w] as $qp) {
- if ($cs[$qp] !== $c1) continue;
- if (isset($v[$qp])) continue;
- $q[] = $qp;
- $qx = $qp % $w;
- $qy = intdiv($qp, $w);
- $x = $ep % $w;
- $y = intdiv($ep, $w);
- if (
- ($dp === 0 && ($x < $qx || ($x === $qx && ($y<=>$qy) === $cc)))
- || ($dp === 1 && ($y < $qy || ($y === $qy && ($qx<=>$x) === $cc)))
- || ($dp === 2 && ($qx < $x || ($qx === $x && ($qy<=>$y) === $cc)))
- || ($dp === 3 && ($qy < $y || ($qy === $y && ($x<=>$qx) === $cc)))
- )
- $ep = $qp;
- }
- }
- }
- $np = $ep + [1, $w, -1, -$w][$dp];
- if ($cs[$np] !== 0) {
- $b = count(array_keys($v));
- $pc = $np;
- break;
- }
- if ($j === 7) break 2;
- if ($j % 2 === 0) $cc = -$cc;
- if ($j % 2 === 1) $dp = ($dp+1) % 4;
-// The original Piet image is wrong: it outputs 403 error for invalid passwords.
-// Failure of authentication should be notified by 401, not 403.
-// I noticed that one month before PHPerKaigi, but I could not read or write (paint)
-// Piet any longer at that time.
-fwrite(STDERR, str_replace('403 Forbidden', '401 Unauthorized', $o));
+ <?php
+error_reporting(-1);
+$b = unpack('C*', file_get_contents(__FILE__));
+$w = $b[20]+2;
+$h = $b[24]+2;
+$cs = [];
+for ($y = 0; $y < $h; $y++)
+ for ($x = 0; $x < $w; $x++)
+ $cs[$y*$w + $x] = ($x*$y === 0 || $x === $w-1 || $y === $h-1)
+ ? 0
+ : $b[122+($y-1)*($w-1)+$x-1];
+$i = stream_isatty(STDIN)
+ ? []
+ : array_map(ord(...), str_split(trim((string) fgets(STDIN))));
+$m = [];
+$pc = 1*$w+1;
+$dp = 0;
+$cc = 1;
+$c0 = 1;
+$b = 0;
+$ns = 0;
+$o = '';
+while (true) {
+ $ns++;
+ if ($ns > 1e5) {
+ echo "infinite loop detected\n";
+ break;
+ $c1 = $cs[$pc];
+ $y = (6 + intdiv($c1-2, 3) - intdiv($c0-2, 3)) % 6;
+ $x = (3 + $c1%3 - $c0%3) % 3;
+ match (($c0 !== 1) * ($c1 !== 1) * ($y*3 + $x)) {
+ 1 => $m[] = $b,
+ 2 => array_pop($m),
+ 3 => $m[] = array_pop($m) + array_pop($m),
+ 4 => $m[] = (fn($x, $y) => $y - $x)(array_pop($m), array_pop($m)),
+ 5 => $m[] = array_pop($m) * array_pop($m),
+ 8 => $m[] = array_pop($m) === 0 ? 1 : 0,
+ 11 => $cc *= pow(-1, array_pop($m)),
+ 12 => $m[] = $m[count($m)-1],
+ 13 => $m = (fn($n, $d, $m, $l) => [
+ ...array_slice($m, 0, $l-$d),
+ ...array_reverse([
+ ...array_reverse(array_slice($m, $l-$d, $d-$n)),
+ ...array_reverse(array_slice($m, $l-$n)),
+ ]),
+ ])(array_pop($m), array_pop($m), $m, count($m)),
+ 15 => !empty($i) and $m[] = array_shift($i),
+ 16 => $o .= sprintf('%d', array_pop($m)),
+ 17 => $o .= sprintf('%c', array_pop($m)),
+ default => 'nop',
+ };
+ $c0 = $c1;
+ for ($j = 0; $j < 8; $j++) {
+ $v = [];
+ if ($c1 === 1) {
+ $x = $pc % $w;
+ $y = intdiv($pc, $w);
+ $e = [($y+1)*$w-1, ($h-1)*$w+$x, $y*$w, $x][$dp];
+ $z = [1, $w, -1, -$w][$dp];
+ for ($ep = $pc; $ep !== $e; $ep += $z)
+ if ($cs[$ep] !== 1) break;
+ $ep -= $z;
+ $pc = $ep;
+ } else {
+ $q = [$pc];
+ $ep = $pc;
+ while (!empty($q)) {
+ $qq = array_pop($q);
+ $v[$qq] = true;
+ foreach ([$qq+1, $qq+$w, $qq-1, $qq-$w] as $qp) {
+ if ($cs[$qp] !== $c1) continue;
+ if (isset($v[$qp])) continue;
+ $q[] = $qp;
+ $qx = $qp % $w;
+ $qy = intdiv($qp, $w);
+ $x = $ep % $w;
+ $y = intdiv($ep, $w);
+ if (
+ ($dp === 0 && ($x < $qx || ($x === $qx && ($y<=>$qy) === $cc)))
+ || ($dp === 1 && ($y < $qy || ($y === $qy && ($qx<=>$x) === $cc)))
+ || ($dp === 2 && ($qx < $x || ($qx === $x && ($qy<=>$y) === $cc)))
+ || ($dp === 3 && ($qy < $y || ($qy === $y && ($x<=>$qx) === $cc)))
+ )
+ $ep = $qp;
+ }
+ }
+ }
+ $np = $ep + [1, $w, -1, -$w][$dp];
+ if ($cs[$np] !== 0) {
+ $b = count(array_keys($v));
+ $pc = $np;
+ break;
+ }
+ if ($j === 7) break 2;
+ if ($j % 2 === 0) $cc = -$cc;
+ if ($j % 2 === 1) $dp = ($dp+1) % 4;
+// The original Piet image is wrong: it outputs 403 error for invalid passwords.
+// Failure of authentication should be notified by 401, not 403.
+// I noticed that one month before PHPerKaigi, but I could not read or write (paint)
+// Piet any longer at that time.
+fwrite(STDERR, str_replace('403 Forbidden', '401 Unauthorized', $o));
+ これは一体なんなのか。ずばり、難解プログラミング言語の一つ Piet のインタプリタである。Piet はピエト・モンドリアン (『赤・青・黄のコンポジション』などで知られる抽象画家) の作品にインスピレーションを受けて作られた、画像をソースコードとするプログラミング言語である。インタプリタは画像の各ピクセルの上を進みながら、色等に応じて特定の処理をおこなっていく。ここでは詳しい言語仕様については解説しないので、気になる方は Wikipedia の記事「Piet」 などを参照してほしい。 @@ -401,7 +412,9 @@ $h = $b[24]+2; プログラムの冒頭にあるこの箇所
-$b = unpack('C*', file_get_contents(__FILE__));
+ $b = unpack('C*', file_get_contents(__FILE__));
+
で __FILE__ つまりこの画像ファイルを読み込んでいる。先ほど Piet は画像をソースコードにしていると説明した。そう、今回の問題の画像ファイル Q1.png は、PHP 製 Piet インタプリタであると同時に、Piet のソースコード画像でもあるのだ。QR コード中央のカラフルな部分が Piet の命令になっている。
@@ -460,11 +473,13 @@ $h = $b[24]+2;
ところで、先ほど掲載した Piet のインタプリタのソースコード末尾には次のような箇所がある。
// The original Piet image is wrong: it outputs 403 error for invalid passwords.
-// Failure of authentication should be notified by 401, not 403.
-// I noticed that one month before PHPerKaigi, but I could not read or write (paint)
-// Piet any longer at that time.
-fwrite(STDERR, str_replace('403 Forbidden', '401 Unauthorized', $o));
+ // The original Piet image is wrong: it outputs 403 error for invalid passwords.
+// Failure of authentication should be notified by 401, not 403.
+// I noticed that one month before PHPerKaigi, but I could not read or write (paint)
+// Piet any longer at that time.
+fwrite(STDERR, str_replace('403 Forbidden', '401 Unauthorized', $o));
+ コメントにも書かれているが、この Piet のソースコード画像には誤りがあった。本来 HTTP のステータスコードを真似るのなら、認証の失敗には 401 を返さなければならない。しかし、Piet のソースは 403 を返すように書いてしまっていた。そのことに私が気付いたのは PHPerKaigi 2023 が開催されるひと月前で、その時点で私はこの Piet のソースコードを (ちょうどこの記事でそうなっているのと同じように) 読解できなくなっていた。さらに悪いことに、正しいメッセージ「401 Unauthorized」は元の「403 Forbidden」よりも3文字長い。3文字出力が長くなるということは、それだけ Piet で塗るべきピクセルが増えることを意味する。もはや3文字追加で出力するだけの余白はこの画像に残されていなかった (と思う。腕ききの Piet プログラマならできるかもしれないので挑戦してみてほしい)。 -- cgit v1.2.3-70-g09d2