本記事では,HarekazeCTF 2019で出題された問題のうち,私が作問した2問について解法の解説を行う. harekaze.com
本記事には次の2問の解説を含む.
- [Crypto 100] Twenty-five (Solved: 56 Teams)
- [Crypto 200] One Quadrillion (Solved: 7 Teams)
[Crypto 100] Twenty-five
ppencode (あらゆるPerlスクリプトを予約語と空白のみを用いて記述する難読化の一種) によるPerlスクリプトを平文とする古典的な単一換字式暗号に対し頻度分析で換字表を割り出す問題.この問題は56チームが正解した.
問題文
With “ppencode”, you can write Perl code using only Perl reserved words. github.com
想定解法
twenty-five.zip
を展開すると, twenty-five.pl, crypto.txt, reserved.txt
を得る.twenty-five.pl
の内容を次に示す.
use open qw/:utf8/; open(my $F, "<:utf8", 'crypto.txt') or die; my $text; while (my $l = <$F>) { $l =~ s/[\r\n]+/ /g; $text .= $l; } close($F); $text =~ y/abcdefghijklmnopqrstuvwxy/*************************/; eval($text);
また,crypto.txt
の内容を一部抜粋して示す.
m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo o sji m oo ... ... sji ytyw wlnfa ytyw whgrf whidl fdfy gliyl vgdj wlnfa rsf lysi idy tgl o myrgf sji m m m whl pyjxah o o kyw ytda yksp blysq gfyj ygu idy blysq spslr o myrgf sji m m m pyjxah o ol fdfy lywk yswh xyaw uwjap dja o myrgf sji m m m whl pyjxah o o kyw ytda yksp blysq gfyj ygu idy blysq spslr o myrgf sji m m m pyjxah o ol lyu fgf clday uglq ejady ytf cslj o myrgf sji m m m whl pyjxah o o kyw ytda yksp blysq gfyj ygu ... ... sji adry o myrgf sji m m m whl pyjxah o o kyw ytda yksp blysq gfyj ygu idy blysq spslr o myrgf sji m m m pyjxah o ol lyu fgf clday uglq ejady ytf cslj gwa xgag lsji ejiyu gli gwa fldja lyu ejady kyw wrf sji adry whgrf o myrgf sji m m m whl pyjxah o o kyw ytda yksp blysq gfyj ygu idy blysq spslr o myrgf sji m m m pyjxah o o kyw ejadp ejady whidl xlyf ierf whgcj fldja yswh sji ytyw o myrgf sji yksp yksp
reserved.txt
にはPerlの予約語 (関数名および演算子) が数多く列挙されている.
twenty-five.pl
の処理の内容を大まかにいうと,crypto.txt
を読み込み,単一換字を施して eval
するという処理を行う.問題は,換字表が伏せられていることである.そこで,reserved.txt
にPerlの予約語 (関数名および演算子) が列挙されている点, crypto.txt
の内容に換字を施すとPerlスクリプトとして解釈できる点,crypto.txt
が英小文字,空白,改行文字のみからなる点に着目すると,平文はppencodeではないかと推測できる.Perlには200種類余り (バージョンによって異なり, reserved.txt
には236種類を掲載している) の予約語が存在し,これをコーパスとして用いることで頻度分析による暗号文単独攻撃を行うことができる.英語などの自然言語に比べて出現する単語が限られているので頻度分析は易しい.換字表を完成させ,twenty-five.pl
を実行すると, crypto.txt
の内容に換字を施したものがフラグを表示するPerlスクリプトとして解釈され,フラグ HarekazeCTF{en.wikipedia.org/wiki/Frequency_analysis}
が得られる.1
[Crypto 200] One Quadrillion
独自実装によるハッシュとソルトの不適切な運用を咎める問題.この問題は7チームが正解した.
問題文
(問題文には問題サーバへのリンクが書かれていたが問題サーバはすでに停止しているので,代わりにDockerを使って問題サーバを建てられるファイル群へのリンクを掲載する.) github.com
index.php(censored):
<?php error_reporting(0); function h($str) { echo htmlspecialchars($str, ENT_QUOTES, 'UTF-8'); } function baby_hash($msg) { $t = array(5676567, 858051, 5476703, 265259, 4058727, 5112531, 964143, 1099579, 8277687, 8717411, 2022783, 7207499, 1997447, 5864691, 828623, 3917019 ); $v = array(7602007, 2906371, 4037663, 3996139); $n = intdiv(6 + strlen($msg), 7); $msg = str_pad($msg, 7 * $n, "0", STR_PAD_RIGHT); for($i = 0; $i < $n; $i++) { $s = intval(substr($msg, 7 * ($n - $i - 1), 7), 10); $k = $t[$i % 16]; $a = $v[1 + $i % 3]; $b = $v[1 + (1 + $i) % 3]; $c = $v[1 + (2 + $i) % 3]; $d = ($a * $b + $b * $c + $c * $s ^ $k) % 10000000; $v = array(($d + $v[1]) % 10000000, ($d | $v[2]) % 10000000, $d * $v[3] % 10000000, $d); } $r = ""; for($i = 0; $i < 4; $i++) { $r = $r . str_pad($v[$i], 7, "0", STR_PAD_LEFT); } return $r; } function verify($progress, $answer, $salt) { if(preg_match("/^[0-9]{43}$/", $progress) && preg_match("/^[0-9]{1,5}$/", $answer) && intval(substr($progress, 5, 4), 10) + intval(substr($progress, 19, 4), 10) === intval($answer, 10) && baby_hash(intval(substr($progress, 28, 15), 10) . $salt) === substr($progress, 0, 28) ) { return TRUE; } return FALSE; } $salt = CENSORED; usleep(200000); if ($_SERVER['REQUEST_METHOD'] === 'POST') { $progress = $_POST['progress']; $answer = $_POST['answer']; if(verify($progress, $answer, $salt)) { $cleared = intval(substr($progress, 28, 22), 10) + 1; $new_progress = baby_hash($cleared . $salt) . str_pad(strval($cleared), 15, "0", STR_PAD_LEFT); $left = intval(substr($new_progress, 5, 4), 10); $right = intval(substr($new_progress, 19, 4), 10); } else { $cleared = 0; $new_progress = baby_hash("0" . $salt) . "000000000000000"; $left = intval(substr($new_progress, 5, 4), 10); $right = intval(substr($new_progress, 19, 4), 10); } } else { $cleared = 0; $new_progress = baby_hash("0" . $salt) . "000000000000000"; $left = intval(substr($new_progress, 5, 4), 10); $right = intval(substr($new_progress, 19, 4), 10); } ?> <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> </head> <body> <?php if($cleared >= 1000000000000000) { ?> <p> CENSORED </p> <?php } else { ?> <p> Question <?php h($cleared + 1); ?> / 1000000000000000</p> <form action="#" method="post"> <p> <?php h($left) ?> + <?php h($right) ?> = <input type="text" name="answer">. <input type="hidden" name="progress" value="<?php h($new_progress) ?>"> </p> </form> <?php } ?> </body> </html>
想定解法
サーバにアクセスすると1000兆問の足し算が出題される.
たとえサーバとの対話を自動化しても,1000兆回の通信を開催期間内に終えることなどできないことは明らかである.どうにかして1000兆問の足し算を回避してサーバにそれを行ったと誤認させたい.
HTMLのソースを見るとPOST時に隠しパラメータ progress
が送信されるようになっていることがわかる.また,添付ソース (PHP) を見ると足し算の問題に使われている数字も progress
の一部から選ばれていることがわかる. progress
には独自実装のハッシュ関数 baby_hash
により生成されたハッシュ値と現在何問目かを表す情報が含まれている.
baby_hash
の処理の概要は次のようである.
- 長さ16の配列 $t
と長さ4の配列 $v
を用意し,定数で初期化する.
- 引数 $msg
の末尾にパディングを施し,長さを7の倍数にする.
- $msg
を7桁ずつに分け (分けられた7桁のかたまり一つ一つをブロックと呼ぶことにする.),末尾のブロックから順に $v
の各要素および $t
の1つの要素と演算をおこない,その結果によって $v
を更新する.
- $msg
をすべて処理し終わったときの $v
の値をならべたものを十進28桁のハッシュ値とする.
いま, baby_hash("0" . $salt)
の値がわかっているとしよう (これは最初に問題サーバにアクセスしたときの progress
の値からわかる.). baby_hash
の処理をよく見ると,長さが7の倍数であるような文字列 $prefix
に対して, baby_hash($prefix . "0" . $salt)
の値は $prefix
と baby_hash("0" . $salt)
から計算できてしまうことがわかる (Length Extension Attackの一種) .ただし, $salt
の長さは未知なので, $salt
の長さについて何通りかの仮定をおいてハッシュを計算してみる必要がある. 2
以上の内容について $prefix = "99999999999999"
(長さ14) とすると baby_hash("999999999999990" . $salt)
の値がわかるので,1000兆問のうち(1000兆 - 10)問を飛ばして残り10問の状態から足し算を始めることができる.残った10問を自力で足し算して答えを送信するとフラグ HarekazeCTF{The_prefix_representing_one_quadrillion_times_of_a_unit_is_peta.}
が表示される.3
-
ちなみに,問題名の「Twenty-five」はppencodeで生成したperlスクリプトが「z」以外の25種類のアルファベットからなっていたことからついている.「z」が出現していないことに気づく前の問題名の原案は「Twenty-six」だった.↩
-
すでに処理したブロックの個数
$i
の値によって,$k, $a, $b, $c
に代入する$t, $v
の要素の添字は周期48で遷移するので,高々48通りを計算すれば十分である.↩ -
ちなみに,
index.php(censored)
で隠されているソルトの値は$salt = "20988936657440586486151264256610222593863921";
となっている.たぶんテンキーをガチャガチャして出てきた値をそのまま使っている.↩