st98 の日記帳 - コピー

なにか変なことが書かれていたり、よくわからない部分があったりすればコメントでご指摘ください。匿名でもコメント可能です🙏 Please feel free to ask questions in English if that's more comfortable for you👍

DIVER OSINT CTF 2025 writeup

6/7 - 6/8という日程で開催された。チーム「25時、ディスコードで。」*1の桃井愛莉*2として参加して1位🎉 昨年はどうしても解けない1問に阻まれて2位だったので、今回は雪辱を果たすことができ大変に嬉しい。

今年も前回大会に引き続きOSINT専門のCTFで、Geo, Recon, Transportation, History, Millitary, Hardware, Companyと様々な角度からOSINT問題が出題されていた。忘れてはならないのがReportカテゴリで、なんと今回は記述式の問題が導入されるというアナウンスが前もってなされていた。詳細は後で述べるけれども、お題をもとに調査を行い、最終的にそれをまとめたレポートを提出するというものだった。私は楽しかった。

記述式の問題は難易度がmediumとされている問題をすべて解くと挑戦できるようになっていたけれども、難易度はあくまで運営の主観で設定されていたことから、その難易度から想像される正答チーム数と実際の正答チーム数との間に乖離が見られる問題がいくつかあった。たとえば、[company] expenseは13 solves, [transportation] air2airは24 solves, [recon] 06_leakedは12 solvesだった。このため、記述式の問題に挑戦できたチームの数は少なかったのではないか。

我々はすでに競技は折り返しを過ぎていた午前3時頃にmediumの問題をすべて解くことができ、ようやく挑戦できた。どのようなお題であるかもこの段階でようやく閲覧できたため、眠気と戦いながら調査とレポートの執筆を行った。

ところで、BunkyoWesterns公式アカウントによるポストではあるが、「BunkyoWesternsのインテリジェンスチーム」が参加したというのは大嘘で、実際にはそんなものは存在しない。


タイムライン

私がどんなことをやっていたか、時系列順でまとめる。時刻はいずれも日本時間・24時間表記で記載している。

  • 6/7 (土)
    • 12時: 競技開始
    • 12時10分頃: チームがオンサイトで集まり作業するための場所に到着し、CTFを始める
    • 14時16分: [transportation] 36_years_agoを解く
    • 15時32分: [military] objectを解く
    • 17時32分: [military] workerを解く
    • 18時43分: [geo] what3slashesを解く
    • 19時12分: [history] internmentを解く
    • 21時59分: [geo] elevatorを解く
    • 22時38分: [company] bidを解く
  • 6/8 (日)
    • 1時: 25時、ディスコードで。
    • 3時3分: [recon] 06_leakedを解く
    • 3時3分: [report] unknown_aircraftがようやくオープンし、調査を始める
    • 6時頃: ある程度調査ができたため、文章をまとめ始める
    • 7時41分: この段階でできあがっていたレポートを提出する
    • 8時16分: 新たな情報を入手して焦る。ほかのメンバーに任せて一旦寝る
    • 10時30分頃: 起きる。新たな情報をもとに再度文章をまとめる
    • 11時47分: 最終版のレポートを提出する
    • 12時: 競技終了

解いた問題(自分でフラグを提出しなかったものを含む)

[geo 446] what3slashes (75 solves)

この画像が撮影されたとき、正面に家が3軒、右手に家が1軒あった。
それから少し経った、ある月の初旬にこの場所にもう一度訪れてみた。そのときには、正面左側に家が1軒増えていて、正面右側にもう1軒が建設中であった。その時点で、建設中の家に屋根はなかったが、黒っぽい屋根を作る予定だという。
「ある月の初旬」とは何年の何月のことだろうか。

Flag形式 : Diver25{YYYY/MM} (e.g. Diver25{2025/06})

添付ファイル: what3slashes.jpeg

漠然とした問題文だ。キリル文字の書かれたチケットのようなものを持ち、どこかに立っているらしい写真が与えられる。背景はあまりにもぼやけていてあまり情報が得られなそう。チケットのようなものを文字起こししたいけれども、手作業でやるのは面倒だったのでChatGPTにやらせる。

モンゴル語で、上に書かれている社名のようなものはMongol Postらしい。そして下部の文章は「3つの単語で3m四方の単位で地球上の任意の場所を表せる」という説明、手書きの文字はその3つの単語のようだ。what3wordsだろう。

上部中央のロゴ:
МОНГОЛ ШУУДАН

手書きの文字(赤い /// マークの右):
бумба . цогц . бататгав

下部の印刷された文章:
Дэлхийн гадаргын 3мх3м хэмжээтэй квадрат бүр өөрийн 3 үгт хаягтай.
Захиалга өгөхдөө эсвэл шуудан илгээхдээ өөрийнхөө 3 үгт хаягийг нэмээрэй

what3wordsで бумба.цогц.бататгав を入力すると、ウランバートルのある場所が表示された。これだろう。さて、問題文で求められていたのは、建設中だった家について黒い屋根ができた時期はいつか特定しろというようなものだった。どうやって特定するか。わざわざこんな住宅街のような場所で定点観測し続けているような人はいないだろうし、そもそも家の写真を探すことすら難しい(ストリートビューもない)。ならば衛星写真はどうだろうと考え、Google Earthで過去の画像を見ていった。

すると、2018年11月30日の写真では黒い屋根のある家が見えるところ、2018年10月30日の写真ではまだ建設中だった。ならば2018年11月だ。

Diver25{2018/11}

[geo 462] convenience (63 solves)

青森県内に、公園とコンビニ、スーパーマーケットが互いに約100m圏内に存在する場所がいくつかある。また、これはOpenStreetMapで確認可能である。 この条件を満たす 公園 のうち、最南端 のものについて、OpenStreetMap上での Way Number を答えよ。 なお、「公園」の定義は、OpenStreetMap上で "park" (leisure=park) と分類されているものに準拠する。

Flag形式: Diver25{123456789}

OpenStreetMapでまず公園・コンビニ・スーパーマーケットの場所を列挙し、手作業で…というのはとても現実的ではない。Overpass APIを使って機械的にやっていくべきだろう。

ChatGPTやClaudeにOverpass QLを書かせて探すということを私もsahuangさんも試したものの、出力されたものは通らず。デバッグが億劫だったので、距離の判定部分はPythonでやらせることにした。「ポリゴン間の距離をちゃんと計算してほしい」と言いつつスクリプトを生成させる。

import requests
import json
import math
from geopy.distance import geodesic
from shapely.geometry import Point, Polygon, LineString
from shapely.ops import transform
import pyproj
from functools import partial

def find_southernmost_park_with_nearby_stores():
    """
    青森県内で公園、コンビニ、スーパーが互いに100m圏内に存在する場所の
    最南端の公園のWay Numberを取得(ポリゴン同士の正確な距離で判定)
    """
    
    # Overpass APIのエンドポイント
    overpass_url = "http://(省略)"
    
    # データを取得
    overpass_query = """
    [out:json][timeout:600];
    
    // 青森県全体
    area[name="青森県"]["admin_level"="4"]->.aomori;
    
    // 公園を取得(leisure=parkのみ)
    (
      node(area.aomori)[leisure=park];
      way(area.aomori)[leisure=park];
      relation(area.aomori)[leisure=park];
    );
    out geom meta;
    
    // コンビニを取得
    (
      node(area.aomori)[shop=convenience];
      way(area.aomori)[shop=convenience];
    );
    out geom meta;
    
    // スーパーを取得
    (
      node(area.aomori)[shop=supermarket];
      way(area.aomori)[shop=supermarket];
    );
    out geom meta;
    """
    
    # APIリクエスト実行
    response = requests.get(overpass_url, params={'data': overpass_query})
    data = response.json()
    
    # データを分類
    parks = []
    convenience_stores = []
    supermarkets = []
    
    for element in data['elements']:
        tags = element.get('tags', {})
        
        if tags.get('leisure') == 'park':
            parks.append(element)
        elif tags.get('shop') == 'convenience':
            convenience_stores.append(element)
        elif tags.get('shop') == 'supermarket':
            supermarkets.append(element)
    
    print(f"leisure=park の公園数: {len(parks)}")
    print(f"shop=convenience のコンビニ数: {len(convenience_stores)}")
    print(f"shop=supermarket のスーパー数: {len(supermarkets)}")
    
    # 青森県付近の座標系設定(UTM Zone 54N)
    wgs84 = pyproj.CRS('EPSG:4326')
    utm = pyproj.CRS('EPSG:32654')  # UTM Zone 54N (青森県に適用)
    project_to_utm = pyproj.Transformer.from_crs(wgs84, utm, always_xy=True).transform
    
    def create_geometry(element):
        """要素からShapelyジオメトリを作成"""
        try:
            if element['type'] == 'node':
                # nodeは5m半径の円として扱う
                point = Point(element['lon'], element['lat'])
                utm_point = transform(project_to_utm, point)
                return utm_point.buffer(5)  # 5m半径
                
            elif element['type'] == 'way' and 'geometry' in element:
                geometry = element['geometry']
                if not geometry or len(geometry) < 3:
                    # 線形の場合や点が少ない場合は小さな円として扱う
                    if geometry:
                        point = Point(geometry[0]['lon'], geometry[0]['lat'])
                        utm_point = transform(project_to_utm, point)
                        return utm_point.buffer(5)
                    return None
                
                # ポリゴンとして作成
                coords = [(point['lon'], point['lat']) for point in geometry]
                
                # 閉じたポリゴンにする
                if coords[0] != coords[-1]:
                    coords.append(coords[0])
                
                if len(coords) < 4:  # ポリゴンには最低4点必要
                    return None
                
                try:
                    polygon = Polygon(coords)
                    if not polygon.is_valid:
                        # 無効なポリゴンの場合は中心点から小さな円を作成
                        centroid = polygon.centroid
                        utm_point = transform(project_to_utm, centroid)
                        return utm_point.buffer(10)
                    
                    # UTM座標系に変換
                    utm_polygon = transform(project_to_utm, polygon)
                    return utm_polygon
                    
                except Exception as e:
                    print(f"ポリゴン作成エラー: {e}")
                    return None
                    
            elif element['type'] == 'relation':
                # relationは中心点から大きめの円として扱う
                if 'center' in element:
                    point = Point(element['center']['lon'], element['center']['lat'])
                    utm_point = transform(project_to_utm, point)
                    return utm_point.buffer(25)  # 25m半径
                    
        except Exception as e:
            print(f"ジオメトリ作成エラー (ID: {element.get('id')}): {e}")
            
        return None
    
    def get_center_latlon(element, geometry):
        """中心座標を取得(緯度経度)"""
        if element['type'] == 'node':
            return (element['lat'], element['lon'])
        elif geometry:
            # UTM座標系の中心をWGS84に戻す
            try:
                center_utm = geometry.centroid
                wgs84_center = transform(pyproj.Transformer.from_crs(utm, wgs84, always_xy=True).transform, center_utm)
                return (wgs84_center.y, wgs84_center.x)
            except:
                pass
        return None
    
    # 各要素のジオメトリを事前計算
    print("\n=== ジオメトリ計算中 ===")
    park_geometries = {}
    convenience_geometries = {}
    supermarket_geometries = {}
    
    for park in parks:
        geom = create_geometry(park)
        if geom:
            park_geometries[park['id']] = geom
            name = park.get('tags', {}).get('name', '名前なし')
            area = geom.area if hasattr(geom, 'area') else 0
            print(f"公園: {name} (ID: {park['id']}) - 面積: {area:.1f}m²")
    
    for store in convenience_stores:
        geom = create_geometry(store)
        if geom:
            convenience_geometries[store['id']] = geom
    
    for store in supermarkets:
        geom = create_geometry(store)
        if geom:
            supermarket_geometries[store['id']] = geom
    
    print(f"\n有効な公園: {len(park_geometries)}件")
    print(f"有効なコンビニ: {len(convenience_geometries)}件")
    print(f"有効なスーパー: {len(supermarket_geometries)}件")
    
    # 条件を満たす公園を検索(3つの距離をすべてチェック)
    valid_parks = []
    
    for park in parks:
        if park['id'] not in park_geometries:
            continue
            
        park_geom = park_geometries[park['id']]
        park_name = park.get('tags', {}).get('name', '名前なし')
        park_id = park.get('id')
        park_center = get_center_latlon(park, park_geom)
        
        if not park_center:
            continue
        
        # 100m以内のコンビニをチェック
        nearby_convenience = []
        convenience_geoms_near_park = []
        for store in convenience_stores:
            if store['id'] not in convenience_geometries:
                continue
            store_geom = convenience_geometries[store['id']]
            
            # 公園とコンビニの距離
            distance = park_geom.distance(store_geom)
            
            if distance <= 100:
                store_name = store.get('tags', {}).get('name', '名前なし')
                nearby_convenience.append(f"{store_name}({distance:.1f}m)")
                convenience_geoms_near_park.append((store, store_geom))
        
        # 100m以内のスーパーをチェック
        nearby_supermarket = []
        supermarket_geoms_near_park = []
        for store in supermarkets:
            if store['id'] not in supermarket_geometries:
                continue
            store_geom = supermarket_geometries[store['id']]
            
            # 公園とスーパーの距離
            distance = park_geom.distance(store_geom)
            
            if distance <= 100:
                store_name = store.get('tags', {}).get('name', '名前なし')
                nearby_supermarket.append(f"{store_name}({distance:.1f}m)")
                supermarket_geoms_near_park.append((store, store_geom))
        
        # コンビニとスーパーが互いに100m以内かチェック
        convenience_supermarket_pairs = []
        for conv_store, conv_geom in convenience_geoms_near_park:
            for super_store, super_geom in supermarket_geoms_near_park:
                # コンビニとスーパーの距離
                distance = conv_geom.distance(super_geom)
                if distance <= 100:
                    conv_name = conv_store.get('tags', {}).get('name', '名前なし')
                    super_name = super_store.get('tags', {}).get('name', '名前なし')
                    convenience_supermarket_pairs.append(f"{conv_name}↔{super_name}({distance:.1f}m)")
        
        # デバッグ情報
        print(f"\n公園: {park_name} (ID: {park_id})")
        print(f"  中心: {park_center[0]:.6f}, {park_center[1]:.6f}")
        print(f"  公園→コンビニ 100m以内({len(nearby_convenience)}件): {nearby_convenience}")
        print(f"  公園→スーパー 100m以内({len(nearby_supermarket)}件): {nearby_supermarket}")
        print(f"  コンビニ↔スーパー 100m以内({len(convenience_supermarket_pairs)}組): {convenience_supermarket_pairs}")
        
        # 全ての条件を満たす場合(公園-コンビニ、公園-スーパー、コンビニ-スーパー全てが100m以内)
        if (len(nearby_convenience) > 0 and 
            len(nearby_supermarket) > 0 and 
            len(convenience_supermarket_pairs) > 0):
            
            valid_parks.append({
                'park': park,
                'latitude': park_center[0],
                'longitude': park_center[1],
                'way_id': park_id,
                'name': park_name,
                'convenience_stores': nearby_convenience,
                'supermarkets': nearby_supermarket,
                'convenience_supermarket_pairs': convenience_supermarket_pairs
            })
            print(f"  ✅ 全ての条件を満たします!")
        else:
            missing = []
            if len(nearby_convenience) == 0:
                missing.append("コンビニなし")
            if len(nearby_supermarket) == 0:
                missing.append("スーパーなし")
            if len(convenience_supermarket_pairs) == 0:
                missing.append("コンビニ↔スーパー距離超過")
            print(f"  ❌ 条件不足: {', '.join(missing)}")
    
    if not valid_parks:
        print("\n条件を満たす公園が見つかりませんでした。")
        return None
    
    # 緯度で最南端を特定
    valid_parks.sort(key=lambda x: x['latitude'])
    
    print(f"\n=== 条件を満たす公園一覧(南→北順) ===")
    for i, park_info in enumerate(valid_parks):
        print(f"{i+1}. {park_info['name']} (ID: {park_info['way_id']})")
        print(f"   緯度: {park_info['latitude']:.6f}")
        print(f"   公園→コンビニ: {', '.join(park_info['convenience_stores'])}")
        print(f"   公園→スーパー: {', '.join(park_info['supermarkets'])}")
        print(f"   コンビニ↔スーパー: {', '.join(park_info['convenience_supermarket_pairs'])}")
        print()
    
    southernmost_park = valid_parks[0]
    
    print(f"=== 最終結果 ===")
    print(f"最南端の公園: {southernmost_park['name']}")
    print(f"Way Number: {southernmost_park['way_id']}")
    print(f"緯度: {southernmost_park['latitude']:.6f}")
    print(f"経度: {southernmost_park['longitude']:.6f}")
    
    return southernmost_park['way_id']

# 実行
if __name__ == "__main__":
    try:
        result = find_southernmost_park_with_nearby_stores()
        if result:
            print(f"\n答え: {result}")
    except Exception as e:
        print(f"エラーが発生しました: {e}")
        print("必要なライブラリをインストールしてください:")
        print("pip install geopy requests shapely pyproj")

これを実行すると、上沢巻目公園が出てくる。たしかに公園とコンビニ、コンビニとスーパー、公園とスーパーの距離はそれぞれ100m以内に見える。

$ python3 aomori.py
1. 上沢巻目公園 (ID: 556701681)
   緯度: 40.508948
   公園→コンビニ: ファミリーマート(95.3m)
   公園→スーパー: ユニバース湊高台店(4.5m)
   コンビニ↔スーパー: ファミリーマート↔ユニバース湊高台店(46.9m)
…

この問題には提出回数に制限があり、かつsahuangさんと私で何度かそれを消費してしまっていたので、慎重に進める必要があると考えた。これで問題ないかとsahuangさんに確認の上で提出いただくと、通ったとのことだった。やったあ。

Diver25{556701681}

[geo 472] Talentopolis (53 solves)

記事 / Article: https://www.guineaecuatorialpress.com/noticias/primera_edicion_de_talentopoli

"Primera edición de Talentopolis" という記事内に登場するステージの位置を答えよ。

アクセスすると、音楽とともに赤道ギニアの大統領であるテオドロ・オビアン・ンゲマの誕生日を祝う旨のメッセージが表示された。83歳らしい。それはどうでもよくて、更新すると問題文で言及されていたスペイン語の記事が表示される。首都であるマラボ、San Juanという地区において、Talentopolisというイベントが開催されたらしい。

このイベントが開催された日時や名称等を含めた「San Juan de Malabo septiembre 7, 2024 Talentopolis」というクエリを使いGoogleで検索すると、別の角度から撮影した写真を含む記事がヒットした。なお、ここまでで得られた写真のいずれもEXIF等に位置情報は含まれていなかった。

そもそもSan Juan地区とはどこなのか。そこら中にありそうな名前だ。San Juan de MalaboでググったりGoogleマップで検索したりしてみるものの、位置がわからない。OSMでSan Juan de Malaboを検索してみると、ある程度場所が絞れた。写真に写っている建物や駐車場、坂等の位置関係を整理しつつSatokiさんと悩んでいると、Satokiさんが衛星写真から場所を特定してくれた。

[geo 499] elevator (12 solves)

これらの写真はあるホテルの客室内と、その窓からの景色を撮影したものである(撮影日は2024年)。 このホテルに設置されたエレベーターには、2件の故障記録が存在しているものが1基ある。 故障日と、故障記録があるエレベーターの登録番号を答えよ。

Flag形式: Diver25{YYYYMMDD_YYYYMMDD_1234567}

例えば、登録番号 "1234567" のエレベーターが2000年11月20日と2020年2月13日に故障していた場合、Flagは Diver25{20001120_20200213_1234567} となる。

なにかが解体された跡を撮影した写真と、東横インの部屋内でそのWi-Fiのパスワード等を撮影したらしい写真が与えられている。私が問題を確認した時点でkanonさんとpr0xyさんが頑張って東横インの各ホテルをチェックしていた。

この問題で最終的に答えるべき情報は故障した日と登録番号であるから、逆に言えばその情報がどこかしらから得られるということになる。日本国内では特にそういった情報を記録しており、かつ一般にアクセス可能であるデータベースが見つからなかったことから、実は韓国やドイツといった場所にあるホテルが撮影場所で、かつそういった国ではデータベースが閲覧可能なのではないかと考えた。

東横インが進出しているそれぞれの国にそういったデータベースがないかClaudeにリサーチさせてみたところ、韓国には국가승강기정보센터なるものがあると教えてくれた。たしかに「토요코인」(東横イン)などで検索してみるとエレベーターのリストが出てくるし、それらは故障履歴まで見られるようになっている。さらに、ここでpr0xyさんが写真の情報から釜山の東横インである可能性を見出した。

この問題ではフラグの提出制限はなかった。ただし、あくまで回数に制限がないだけで、3回間違えるとそれ以降は1回間違えるごとに15分ロックがかかってしまうという別の制限があった。しかしながら、これは15分悩むぐらいなら1回それらしいフラグを提出し、少なくともそれが間違っている情報を得る方がよい(つまり、15分悩むのは1回提出権を失うのとほぼ等価である)ということでもある。これに基づいて、釜山にある東横インのうち、2回故障しているエレベーターをリストアップし試していくことにした。最初に試した登録番号 8019148 のものが正解だった。

Diver25{20230810_20241124_8019148}

競技だから許される考え方だとは思う。ブルートフォースに近しいけれども、ルール上はあくまで機械的なアクセス、それもスクレイピングやスクリプトのような「機械的手法」が禁止されているだけで、手動での総当たりは禁止されていなかったから問題ない、またほかのメンバーが並行して絞り込みを進めていたけれども、それによって場所が確定するまで15分以上が必要となるだろうと判断し、このようにした。

[recon 499] 06_leaked (12 solves)

"00_engineer" の問題で見つかったソフトウェアエンジニアのパスワードが漏洩して公開されてしまったらしい。彼はパスワードを変更して事なきを得たようだが、漏洩したパスワードは何だっただろうか。

Flag 形式: Diver25{his_password}

シリーズものの問題だ。私が問題に着手した時点でほかのメンバーによってこのシリーズの問題すべてが解かれており、重要なものとして以下の情報がわかっていた:

  • 調査対象のエンジニアの名前は Kodai Shinonome であり、以下のアカウントを持つ:
    • https://github.com/kodai-sn
    • https://x.com/kodai_sn
    • https://speakerdeck.com/kodaisn
    • kodaisn.development@gmail.com
    • shinonomekodai@gmail.com
  • Kodai Shinonome はmagneightという架空の会社に勤めており、この会社は次のようなWebサイトがある:
    • https://magneight.com/
    • https://gitea.magn8soft.tokyo/

このエンジニアはSpeakerDeckでGitやGitHubの使い方を紹介するスライドを公開していた。その中でGitHubでのフォークのやり方を説明していたが、その図解のために載せている以下のスクリーンショットにおいてユーザ名が白塗りにされていることから、このコミットを特定すべきではないかとSatokiさんが注目していた。

フォーク元のリポジトリは公開されているために、そのフォーク先のリポジトリをいくつか確認したが、該当のコミットは見つからない。GitHub全体で検索しても見当たらない。消されてしまっているのだろうか。

GH Archiveのデータを使ってこのようなコミットがないか、コミットメッセージ等を鍵に探してみると、見つかった。ただ、特に怪しいファイルは見当たらない。また、このコミットを行っているユーザは後に mizuki1206edelweiss に名前を変えていることが、その数値のIDを追跡することでわかるが、このユーザは mizuki を名前に含んでいること、またメールアドレスから、おそらくそのエンジニアでなく勤務先のCEOであるMizuki Sekozakiであろうことが推測されていた。

悩んだ挙げ句に、パスワードの流出といえばPastebin.comだろうという安直な考え方から、ここでエンジニアの持つメールアドレスのひとつである kodaisn.development で検索したところ、なんとパスワード付きでヒットする。そんなあ。

Diver25{1_4m_fr0m_h0kk41d0_4nd_l0v3_54un4}

我々が記述式の問題以外で最後に解いたのがこの問題だった。無駄に考えすぎて、チームメンバー全員で時間を空費していた。意図的にせよそうでないにせよこの問題はrabbit holeがちょっと多いかなあとか、シナリオとしてあまり面白いものとは思えないなあとか思う。

[transportation 347] 36_years_ago (125 solves)

このニュース動画に映っている航空機に、1989年8月時点で割り当てられていたトランスポンダのMode Sコードを16進数表記で答えてください。 https://www.youtube.com/watch?v=OvR2O_Vpwc0

Flag形式: Diver25{1234AB}

content warning: この映像は軽微な航空事故の様子を含みます。

7か月前の2024年10月に、宮城県栗原市の瀬峰飛行場で起きたセスナ機のオーバーランについてのニュースだ。そもそもこれの機体記号はなんなのか。ニュースの動画中では写らない(なお、52秒ごろに機体記号の見えるシーンがあるが、これは別の飛行機だ)。別ソースとしてNHKのニュースを確認したところ、着陸時の動画が含まれており、バッチリ JA4098 と見えた。

この機体記号でググると、いくつか情報が得られる。この中で、Also Registered As として N9768L Deregistered Cancel: 1989-09-22 とある。アメリカにいた頃…かどうかはしらないけれども、1989年以前は N9768L という機体記号も持っていたようだ。

N9768L で検索すると、これに関する情報が得られた。問題で求められている情報である Mode S Code53316552 とある。これは8進数表記であることに注意されたい。

Diver25{AD9D6A}

[history 478] internment (48 solves)

著名な木曜島の真珠採りダイバーであった藤井富太郎氏は、第二次世界大戦中、強制収容されました。彼が釈放された収容所と釈放の年月日を明らかにしてください。

Flag 形式: Diver25{YYYY-MM-DDキャンプ番号キャンプ地名_州名}

例えば、「2025年6月1日に、キャンプ番号7番のDiver州Daibaキャンプ」から釈放されたならば、 Diver25{2025-06-01_7_Daiba_Diver} となります。なお、キャンプ番号がない場合はX(大文字のX)を入れてください。例えば、Diver25{2025-06-01_X_Daiba_Diver} となります。

藤井富太郎氏は最後の真珠貝ダイバー藤井富太郎という本を書いているらしい。この本に書かれている可能性も一応あるし、Kindle版があるのでやろうと思えばすぐにでもできたけれども、最終手段として残しておいた。

氏名で検索していると、藤井氏が住んでいた木曜島に触れているnote記事を見つける。いわく、1941年にニューサウスウェールズ州のヘイ収容所に強制収容されたらしい。釈放の日は触れられていない。

ヘイ収容所について調べてみると、同じく収容所のあったカウラ市の日本人戦争墓地データベースがヒットする。ここでオーストリアに存在した収容所の一覧が説明されており、ヘイ収容所についてはNo.6, 7, 8の3つが存在していたとわかる。No.6については「1943年4月までは単身男性の民間人抑留者を収容」のほか「PWJM」も収容、No.7, 8については「戦争捕虜を収容」ということらしい。このうち藤井氏が当てはまりそうなのはNo.6だが、どうだろう。

次の一手で悩んでいたところ、オーストラリア政府がそういった公文書等を公開していないかと思いつく。収容所名等を英語で検索していたところ、Wartime internment camps in Australiaというページが見つかる。このページの最後にalien registration and internment recordsへのリンクがある。ここからRecordSearchというサービスにたどり着くことができた。ここで[Fujii Tomitaro]で検索する。Item IDが8610767である資料に、筆記体でやや読みづらい箇所があるけれども、1946年12月10日にタツラの収容所から釈放されたということがわかる。

Diver25{1946-12-10_4_Tatura_Victoria}

[company 491] bid (31 solves)

2023年、オマーンにおいて、ある施設に関連するニュースが報じられた。 https://www.youtube.com/watch?v=TFdubskF9Kw

その後、2024年10~11月に、この施設に関連すると推定される、井戸およびパイプラインを建設するための入札が実施された。 このとき、3位の金額で入札した企業のCEOの名前を、Webサイトに掲載されている英語表記で答えよ。

Flag形式: Diver25{Kelly Ortberg}

私が問題を確認した時点で、Satokiさんがニュースの動画(アラビア語!)を字幕等から頑張って読み解いたり、ChatGPTに調べさせたりして、関連するであろう入札のデータを手に入れていた。ただ、このページからは「3位の金額で入札した企業」の情報は得られない。

同プラットフォームのトップページに戻り、Awardedな入札から 1190/2023/MAFWR/DGAWRDK-94- Recall - 1 (先程のページから得られたTender No.)で検索する。以下のメールアイコンを押下すると、GOLDEN SANDS TRANSPORT SERVICES LLCなる会社が我々の探していた企業であるとわかった。この企業のサイトを探すと、CEOの名前がわかる。

[military 359] object (120 solves)

69.216246, 33.378242 には大きな構造物が存在する。この構造物のプロジェクト番号および、構造物の名称(固有名詞)を 現地語 で答えよ。

Flag形式: Diver25{プロジェクト番号名称}(例: Diver25{955АБорей-А})

Googleマップで見に行く。ロシアだ。確かに白くて巨大な何かが存在しており、南東側に大きめの構造物が、北西側にそれよりは少し小さい構造物があるけれども、今回問われているのは後者だ。近くにOlenya Guba Naval Baseなるものが見えるのも怪しい。これでググるとデイリーメールのなかなか怪しげな記事が見つかるけれども、記事中で例の白い構造物が Covered docks for deep-diving submarines とされていることがわかる。

Claudeにこれらのワードを翻訳させつつググっていると、「"Оле́нья губа" док для подводных лодок」というクエリでこれらしきものについてまとめられているWebページを見つけた。翻訳して読むと、問題文で示されている大きめの方の構造物はПД-72であるとわかる。あまり裏取りができていなかったけれども、5回までは提出できるということで以下のフラグを試したところ、通った。確証を持たずに提出するのは理想的ではないが、これは競技なので…

Diver25{13560_ПД-72}

[military 499] worker (12 solves)

"object" の問題で示された構造物で仕事をしていた1964年生まれのある人物は、Akhtubinskに居住しているとされる。 この人物が1987年~2009年にかけて勤務していた組織のOGRNを答えよ。

Flag形式: Diver25{1234567890123}

Akhtubinskのロシア語表記はАхтубинскだとWikipedia等からわかる。わざわざ何年生まれだとか、いつからいつまである組織に勤務していたとか言及しているあたり、それを確認できるようになっているのだろうなあと思う。つまり、たとえばFacebookやLinkedInのようなSNS、あるいはロシア国内で使われているその他のサービスにアカウントがあり、検索や個人ページでの情報の参照ができるのではないかと考えた。

ロシアのSNSといえばVKだ。人物検索では都市や生まれた年でも検索できるようになっている。それぞれАхтубинскと1964年に設定してひとりひとり見ていくと、ちょうどひとりだけ1987年から2009年にかけてある組織で勤務していたとプロフィールに書かれている人物を見つけた。この組織はOpenSanctions登録されており、ここからOGRNがわかった。

Diver25{1023000507936}

[report 300+100] unknown_aircraft (1 solve)

これらは2025年5月10日 19:00UTC ごろに Flightradar24 上で確認された航空機の様子です。 画面上で選択されている航空機(赤いアイコンの航空機)について、公開情報から分かる範囲で、調査を行って説明してください。 記述に関する詳細は添付しているルール(PDF)を参照してください。

これが、最初に述べた記述式の問題であった。渤海上でKL61CEP, B352WHPB, KL61WHPDといったコールサインを持つ航空機が選択されており、これらについて調査しろということらしい。この問題のルールとして、まず基本的なものとして以下のような項目があった。

  • CTFの終了後に採点が行われる
  • プレイヤーの判断とソースを併記せよ
  • ソーシャルメディアの投稿はソースとはみなさない

採点にあたって運営が定めた要素がいくつかあり、それを満たせば加点が行われる。各要素には点数が設定されており、すべて満たすことができれば合計で300点を得られる。さらに、ボーナス点として運営が把握できていなかったものの事実だと確認できた事項がある、かつそれが問題上重要である場合には、300点とは別に100点まで加算される。

この問題がオープンされた時点でこれ以外のすべての問題を解くことができていたため、6人のプレイヤー全員で調査に取り組んでいた。ここではその詳しい手法や調査の結果については述べない*3が、頑張って調査し、頑張ってまとめて、無事に満点の300点およびボーナス点の100点が付与された。基本的には加点方式であろうと考えて得られた情報を書けるだけ書いたが、ソースとして挙げたURL等を除くと5000文字を超えており、若干採点者への申し訳なさを感じた。

*1:BunkyoWesternsのメンバー5名にProject Sekaiのsahuangさんも参加してのチームだったので、今年も引き続きこのようなチーム名とした

*2:チーム名からちょっと離れてるけど、単にプロセカで一番好きなキャラだから… なお、ほかのメンバーは皆もっと離れていた

*3:気が向いたら書くかも