HITCON CTF 2017で出題された Secret Server Revenge を海外のWriteupを参考に解いてみたので、自分なりの言葉でまとめてみます。
本番中はSecret Serverに着手し、フラグ半分まで得ましたが、方針が間違っており時間内に全て特定できず。。。後日、時間内に解いたチームメイトにチーム内の勉強会で方針を聞いて解くことができたので、その拡張の問題であるSecret Server Revengeを解いてみました。
問題概要
Secret Server同様、Proof of workの後、"Welcome!!"という文字列をAESで暗号化した結果のBase64エンコード文字列が送られてくる。それ以降はこちらから暗号文を送ると、その復号結果の文字列に応じた処理が行われる。
AESの暗号化の際、平文の文字数がブロック長の倍数でない場合はPKCS7という方式でパディングされる。
PKCS7パディング "Welcome!!" (9文字なので, ブロック長16文字に<b>7文字</b>足りない) "Welcome!!\x07\x07\x07\x07\x07\x07\x07" つまり、パディングとしては、"\x01"*1, "\x02"*2, ... , "\x10"*16 が考えられる。
サーバ側で任意のコマンドを実行するには、各コマンドで始まる文字列の暗号文を得る必要がある。
本問題の場合、任意の平文の暗号文を得ることはできず、最初はEnc(pad("Welcome"))しか得ることはできない。しかしながら、「暗号文の先頭に付与された文字列を復号の際のIVに使用する」ということを利用し、暗号文の1ブロック目だけは復号結果を自由に操作することが可能である。
従って、Enc(pad("Welcome"))という暗号文を得られている以上、get-md5やget-tokenというコマンドを実行するのは容易である。
unpad関数の脆弱性
unpad関数が脆弱である。先に述べたように、パディングはPKCS7という方式が利用されている。
def unpad(msg): return msg[:-ord(msg[-1])]
この処理は間違ってはおらず、パディングが正当なものであれば正当なunpad処理がなされる。しかし、復号結果の末尾がパディングでなくともその文字の数値に応じたunpad処理が行われてしまう。
(本来なら、復号後にパディングの正当性を確認し、末尾がPKCS7形式のパディングでない場合はErrorとする必要があるだろう。)
従って、暗号文の復号結果の末尾を自由に操作することができれば、unpadの脆弱性により復号結果を任意の文字数削ることが可能である。
(これは Secret Serverでも同様の脆弱性だった。)
ここで、tokenの文字数は56文字であり、get-tokenコマンドで暗号化される平文の長さは len("token: "+token) = 63文字 であることから、get-tokenコマンドで得られる暗号文Enc(token)の復号結果の末尾は "\x01" であることが分かっていることを頭に入れておきたい。
方針
Secret Server Revenge は、Secret Serverと比較して大きく二つの違いがある。
- 1点目は、解読しなければならない暗号文の平文が32文字のフラグ(可読文字)から56文字のトークン(os.urandom)となった。
- 2点目は、1回のコネクションにおけるリクエスト数に340回までという制限がついた。
1点目の変更により、Secret Server よりも多くのリクエスト数が必要になると考えられるが、2点目の変更によりSecret Serverよりも少ないリクエスト数でトークンを解読する必要がある。
また今後扱う表記について以下に述べる。
Enc(a): aを本問題のAESで暗号化した値 md5(a): aのMD5ハッシュの値 token: 56文字のトークン、またはパディングである"\x01"も含んだトークンの値(適宜読み替える) a|b : 文字列aと文字列bの連結 その他、pythonの記法を使用。
要約
本問題で必要なステップをまとめる。
Step1. (1 リクエスト)
最初に得られるEnc("Welcome")をそのまま送り返し、 Enc("Command not found") を得る
Step2. (1 リクエスト)
IVを操作してget-tokenを実行し、 Enc(token) を得る
Step3. (57 リクエスト)
IV操作とパディング操作により
get-md5|token[:1] get-md5|token[:2] ... get-md5|token[:56] get-md5|token[:57]("\x01"を含む)
を実行させ、
Enc(md5(token[:1])) Enc(md5(token[:2])) ... Enc(md5(token[:56])) Enc(md5(token[:57])) ("\x01"を含む)
を得る
Step4. (Step3の値をうまく使うと 176 リクエスト)
Enc(token)(4ブロック) | "\x00"*16*11(11ブロック) | ("¥x00"*15 + i ) (1ブロック) | Enc(token)[48:](1ブロック) 合計17ブロック(272文字)
の暗号ブロックを用意し、i を変化させることで
32文字unpadされた時のget-md5の結果 33文字unpadされた時のget-md5の結果 ... 255文字unpadされた時のget-md5の結果
を得る。
1文字unpad ~ 31文字unpad のパターンを取得しない理由は、この範囲にある暗号ブロックはunpad操作の為に値が変化しているので、取得していても以降の検証処理で役に立たない為である。
Step5. (56 リクエスト + α)
Step4の暗号ブロックの一部を変化させ、
(1) Enc(token)(4ブロック) | "\x00"*16*11(11ブロック) | "¥x00"*16 (1ブロック) | Enc(md5(token[:1])) あるいは、 (2) Enc(token)(4ブロック) | "\x00"*16*11(11ブロック) | IV (1ブロック) | Enc(md5(token[:1]))
をサーバに送り、返ってきた値とStep4で得た224個の値を比較することで、何文字unpadされたかをチェックし、
md5(token[:1])[-1]
を得る。この時、(2)の暗号ブロックを利用した場合はunpadの値がそのまま md5(token[:1])[-1] となるが、(1)の場合は md5(token[:1])[-1] = "\x00" ^ unpad ^ IV[-1] という計算が必要なことに注意。
次にローカルで、
md5(chr(0))[-1] == md5(token[:1])[-1] md5(chr(1))[-1] == md5(token[:1])[-1] ... md5(chr(255))[-1] == md5(token[:1])[-1]
をチェックし、トークンの1文字目の候補を得る。
以降1文字目の候補を利用して同様の処理を、Step2で得た値の数(56回)だけ繰り返す。
つまり、
Enc(token)(4ブロック) | "\x00"*16*11(11ブロック) | "¥x00"*16 (1ブロック) | Enc(md5(token[:2])) ... Enc(token)(4ブロック) | "\x00"*16*11(11ブロック) | "¥x00"*16 (1ブロック) | Enc(md5(token[:56]))
を送りStep5を繰り返す。
もしもStep4のどの値にもならなかった場合はunpadが1~31の間に入ってしまっている。その場合はunpad操作を行いunpadが32~255の間に入るように調整すると良い。
(ちなみに、unpadが0の時はmsg[:-0](空文字)となるため、command not foundとなる。つまり、返ってきた値がstap4のどの値にもならずにstep1で得た値になった場合、unpadは0になったと言えるが、その場合も上記のようにunpad操作で32~255の間に入るようにやり直しても構わない。)
Step6. (1 リクエスト)
Enc(token)(4ブロック) | "\x00"*16*11(11ブロック) | "¥x00"*16 (1ブロック) | Enc(md5(token[:57])) 最後の暗号ブロックはEnc(md5(token+"\x01"))である
をサーバに送り、返ってきた値とStep4で得た224個の値を比較することで、何文字unpadされたかをチェックし、
md5(token[:57])[-1]
を得る。この57文字目は"\x01"であると分かっているので、
md5(cand[0]+"\x01")[-1] == md5(token[:57])[-1] md5(cand[1]+"\x01")[-1] == md5(token[:57])[-1] ... md5(cand[n]+"\x01")[-1] == md5(token[:57])[-1]
をチェックする。運が悪くない限り、この処理でトークンの候補を一意に絞ることが可能。
感想
AES問の解法は、暗号ブロックの操作やパディングオラクルなど、発想の勝負であることが多い。本番でも頭を柔らかくして解いていきたい。
また、この手の問題は解法を文章化するのが非常に難しい。時間があればビジュアル的な説明も追記したい。