YAPC::Hakodateでもやります!コードゴルフ企画Perlbatross 前回行われたチートも解説するよ #yapcjapan

こんにちは!!! お元気ですか?????? カヤック技術部の谷脇です。

来たる10/5にYAPC::Hakodate 2024が開催されます! わーい!!

yapcjapan.org

我々カヤックもYAPC::Hakodate 2024にスポンサーしております。弊社からも何人か登壇します。実は私もします!

techblog.kayac.com

そして、前回のYAPCでは椅子スポンサーとしてPerlbatrossという企画しておりました。Perlbatrossとは、お題に対してPerlでいかに短くコードを書くか競うコンテストです。つまりコードゴルフです。

前回は気合を入れておりまして、このために問題を閲覧したり投稿された回答をperlを実行して検証したり、回答の短さをもとにランキングを出すWebアプリケーションを作りました。もちろんPerlで。

前回のランキングの様子

回答投稿ページの様子。Parは基準バイト数だが大幅に下回るスコアが出た

Perlの動かし方がわからない方でも、とりあえず試行錯誤できる作りになっております。

当日は多くの方に参加いただきました。大変ありがとうございます。上位の方のコードの解説編はこちら。

techblog.kayac.com

そして今回YAPC::Hakodate 2024でもPerlbatrossをやります!!!パチパチパチ👏〜。

開催期間は 2024/10/04 15:00 〜 2024/10/07 12:00 です。

問題数ですが、前回は4問ありました。今回は全部で2問にしています。4問は多すぎましたね...。それぞれタイトルは「Portalbress Gramana」と「QuAArterPix」です。どんな問題か想像してお待ちください。

時間になりましたらこちらのサイトがYAPC::Hakodateバージョンに切り替わります。 👉 https://perlbatross.kayac.com/

チート解説編

ここでは前回のPerlbatrossで行われたチートっぽい回答についてご紹介します。ちなみにYAPC::Hakodate 2024バージョンでは仕組みを見直して潰しております。

Test2::V0::is関数を上書き

以下が前回のPerlbatrossのコード検証の仕組みです。

  1. あらかじめ問題ごとに設定してある入力文字列をファイルから読み込んで標準入力に流し込む
  2. 独立した名前空間(package)を作る
  3. ユーザー投稿されたコードを名前空間内で実行する
  4. 実行した後に標準出力に吐かれたコードを対応する出力文字列ファイルと比較する

以上のコードをPerlの標準的なテストファイル内で行なっていました。

この実行方法を利用したチートがこちらです。

*::is=*::ok

11バイトで回答できてしまいました。これは何をやっているかというと特定の名前空間内にインポートされた関数を上書きしています。*は型グロブを表し、型グロブ同士の代入はエイリアスです。またパッケージ名区切りの::の左辺に何もない場合はmainパッケージが省略されたとみなします。なのでこの場合は長く書くと *main::is=*main::okとなりますね。いわゆるシンボルテーブルをいじっているわけですが、Perlはこういうよその名前空間に手を出していける柔軟(?)なところが好きであり、また困らされるところではあります。

じゃあこれらのisokってなんだろうとなりますが、これはTest2::V0内のテストアサート用の関数です。isは1つ目と2つ目の引数が同一であればOKを返し、okは1つ目がtruthyな値であればokを返します。

では4のコードを見てみましょう。

my $expect = $io_dir->child("output.txt")->slurp_utf8;
$stdout = decode_utf8($stdout);
is((diff \$stdout, \$expect), "");

この場合、isで標準出力と実際の正解の出力とで差異がない、つまりdiff関数が空になることを期待していますが、これがokと入れ替わったらどうでしょうか。Perlは空文字ではないかつ"0"でもない文字列をtrueであると判断するため、diffがあった場合はokとなりますね。上記のコードは何も出力しないため、差異が発生し、どんな入力・出力であってもテストが通ってしまいます。

これのどこに穴があったかというと、上記の3の部分で同じプロセス内で投稿されたコードを実行したところにあります。コンテスト期間中ですが、このチート対策として投稿されたコードをdoでプロセス内実行するのではなく、perlコマンドで実行するように変えてこのチートを塞ぎました。YAPC::Hakodate 2024バージョンではさらに対策を入れています。

こちらの手法ですが、id:akiym さんのブログで解説されています。

blog.akiym.com

ブログでは他の手法もいろいろ考えたそうですが、結果的にシンプルかつすぐに「やられた!」と思ったコードでした。ここまでスパッとくるとスッキリしますね。

入力を見て固定出力を出す

Hole4「Pytecode」で1位だったKarakasaDcFdさんのコードを見てみましょう。

use v5.38;my@q=<>;print"Hello World
"if$q[0]=~/H/;my@a=(2,23,6,3,3);$"=$/;print"@a
"if$q[2]=~/\+/;print"true
false
"if$q[1]=~/if/;print"false_false
"if$q[2]=~/if/;@a=1..10;print"@a
"if$q[3]=~/10/;$"=' ';@a=0..9;if($q[3]=~/b/){for(0..9){print"@a
";shift@a}}

PytecodeはForthもどきのコードを実行するインタプリタを作成する問題でした。しかし曲がりにもプログラムなのでちゃんと動作する問題のパターンが限られてしまいました。

最初のprint"Hello World\n"if$q[0]=~/H/;はHを含む行があった場合はHello World\nを出力するというコードですが、問題の入力では以下に該当します。

Hello World
.
cr

今回のプログラミング言語はただの文字列リテラルはスタックに積み、.でprint、crで改行なのですが、他にHが先頭に来るような問題がなかったため、/H/で問題がクリアできてしまいました。

次のmy@a=(2,23,6,3,3);$"=$/;print"@a\n"if$q[2]=~/\+/;ですが、2問目に対応しています。このコードでは四則演算をしてprintしていくものでしたが、+`が3行目に来る問題は他になかったため、これを特徴として使われてしまいました。

そんな感じで残りも他の問題入力とは違う特徴を用いて固定値、もしくは固定値を出力できるようなコードを実行する感じです。お見事でした。

/tmpに正解ファイルを保存

PerlbatrossはAWS Lambda上で動いています。Lambdaは/tmpに読み書きできる領域が用意され、またそれは1回の実行を跨ぐ(リクエストを返した後に削除されない)仕様になっています。それを使ったチートです。

コード実行環境に問題の入力および出力のペアが存在するため、それをcatするだけで通ってしまうってことですね。

これをやった方は別の手法のチートもしておりまして、どちらもPerlや問題とは関係がないものです。またどちらも私がWebアプリケーション上に入れてしまった脆弱性に起因するものです。現在は対策済みです。

私が皆さんにお伝えしたいこととしては、この企画は皆さんにPerlを楽しく書いてもらうために用意したものであって、CTFの問題として公開しているものではありません。実行環境を破壊して他の方に影響が出たり、リーダーボードに影響が出てしまうのは私の落ち度があるとはいえ本意ではありません。なので、もしこれはWebアプリケーションとして問題があるのではないかと思われた方は、そのまま突っ走らず、会場でこっそり私に言ったり、このブログのXアカウントもしくは私のアカウントにDMしていただけると助かります。

その他のお知らせ

今回のPerlbatrossはさらに改善を加えております。

間違ったコードを投稿した後に1個前に戻りたいという声があったのでボタンを追加しました

Perl初心者でも安心!チートシートをコメントに入れてます(消すだけでスコアが上がるボーナスもあります)

また併催としてJS体操もやっております! Perlbatrossとの問題のコラボもあるかも? JS体操で知った方もPerlbatross挑戦してみてね〜。

hubspot.kayac.com

皆様の参加をお待ちしております!