メインコンテンツへスキップ

OBTS v8.0 CTF

·9 分· loading · loading · ·
Jaybird1291
著者
Jaybird1291
目次
⚠️ この文章は ChatGPT で作成しました(日本語はまだ N4 レベルです)。

OBTS v8.0 CTF は、3 人編成のチーム戦 CTF だった。自分はチーム “LesStagiaires” で参加して、最終順位は 4 位!

Cryptography
#

Patch Balearic - 10 points
#

あなたは、イビサの超限定ビーチクラブのセキュリティコンサルタント! 彼らのアプリ管理システムに脆弱性があったけど、パッチがリリースされた。

Mission 何が変わったかを突き止めて、隠されたビーチパーティのアクセスコードを見つけられる?

Flag Format IBIZA{.....}

Files: APP_V1.txt, APP_V2.txt

ファイルはこれ: app_v1.txt:

# Application Security Audit Log
[2025-10-15 09:13:42] INFO: Security scan initiated for Beach_Hacking_Portal v2.13.0
[2025-10-15 09:13:47] WARNING: CVE-2024-13579 detected - UNPATCHED
[2025-10-15 09:13:52] CRITICAL: 13 known vulnerabilities found
[2025-10-15 09:13:58] ERROR: Authentication module v1.3 - patch KB2025-013 MISSING
[2025-10-15 09:14:03] WARNING: Last security update: 13 days ago
[2025-10-15 09:14:08] INFO: Encryption: ROT-XIII cipher (DEPRECATED - UNPATCHED)
[2025-10-15 09:14:13] CRITICAL: Session handler CVE-2025-00013 - NO FIX APPLIED
[2025-10-15 09:14:19] ERROR: SSL Certificate expires in 13 days
[2025-10-15 09:14:24] WARNING: Vulnerable dependencies detected: openssl-1.3.13a
[2025-10-15 09:14:29] INFO: Port 8013 exposed - requires patch #XIII
[2025-10-15 09:14:35] CRITICAL: Payload detected: "Hafrpherq_Cngu_Qrgrpgrq_Cngpu_Zvffvat"
[2025-10-15 09:14:40] ERROR: Security patch bundle "Update-XIII" not installed
[2025-10-15 09:14:45] WARNING: Recommended action: Apply patches 1-13 immediately
[2025-10-15 09:14:50] INFO: Total unpatched issues: XIII (HIGH SEVERITY)
[2025-10-15 09:14:55] CRITICAL: System vulnerable. Patch now or rotate cipher by 13.

app_v2.txt:

# Patch Management System - Update Completed
[2025-10-16 13:00:00] INFO: Patch deployment initiated - Update Bundle XIII
[2025-10-16 13:00:13] INFO: Downloading security patches from cdn.patch.server:8013
[2025-10-16 13:01:27] INFO: Verifying patch signatures... MD5: d4e13f2a9b8c7
[2025-10-16 13:02:13] SUCCESS: CVE-2024-13579 - PATCHED
[2025-10-16 13:02:45] SUCCESS: Authentication module v1.3 -> v2.13 upgraded
[2025-10-16 13:03:13] SUCCESS: Applied security patch KB2025-013
[2025-10-16 13:03:58] INFO: Installing encryption update - ROT-XIII module removed
[2025-10-16 13:04:13] SUCCESS: Session handler CVE-2025-00013 - FIXED
[2025-10-16 13:04:47] INFO: SSL Certificate renewed - valid for 365 days
[2025-10-16 13:05:13] SUCCESS: Updated openssl-1.3.13a -> openssl-3.0.13
[2025-10-16 13:05:39] INFO: Port 8013 secured - firewall rules applied
[2025-10-16 13:06:13] SUCCESS: All XIII critical patches installed
[2025-10-16 13:06:52] INFO: Verification hash: 0xD13A7F (13 in hex = 0xD)
[2025-10-16 13:07:13] SUCCESS: System hardened - 13/13 patches applied
[2025-10-16 13:07:48] INFO: Deployment complete. Reboot required in 13 minutes.
[2025-10-16 13:08:13] INFO: Access token generated: VOVMN{ju173_f4aqf}
[2025-10-16 13:08:35] SUCCESS: Beach Hacking Portal v2.13.1 - FULLY PATCHED
[2025-10-16 13:09:13] INFO: Security status: ALL CLEAR ✓
[2025-10-16 13:10:00] INFO: System reboot scheduled...

この 2 つは平文ログなので、ぱっと見で以下が分かる:

  • app v1(パッチ前): いろいろ問題があって、ROT13 への参照がある(“ROT-XIII cipher”, “rotate cipher by 13”)。
  • app v2(パッチ後): 適用内容のログが出ていて、さらに “access token” がある。

重要そうな行:

- v1: INFO: Encryption: ROT-XIII cipher (DEPRECATED - UNPATCHED)
- v1: CRITICAL: Payload detected: "Hafrpherq_Cngu_Qrgrpgrq_Cngpu_Zvffvat"
- v1: CRITICAL: System vulnerable. Patch now or rotate cipher by 13.
- v2: INFO: Installing encryption update - ROT-XIII module removed
- v2: INFO: Access token generated: VOVMN{ju173_f4aqf}

まず、ROT13 っぽいペイロード Hafrpherq_Cngu_Qrgrpgrq_Cngpu_Zvffvat を復号すると Unsecured_Path_Detected_Patch_Missing になる。

次に “access token generated” の VOVMN{ju173_f4aqf} を ROT13 すると IBIZA{wh173_s4nds}

FLAG: IBIZA{wh173_s4nds}


Sunset Time Oclock - 10 points
#

iOS の sysdiagnose ファイルを解析していたところ、攻撃者がタイムスタンプの精度にメッセージを隠していることが分かった。マルウェアは、データを符号化するために非常に特定のマイクロ秒間隔で実行されるよう設計されていた。

Mission フラグを見つけろ。

Flag Format IBIZA{...}

Files*: malware_execution.log

ファイルはこれ:

=== iOS sysdiagnose - Malware Incident Analysis ===
Device: iPhone 15 Pro (iOS 17.2.1)
Incident Date: 2024-12-06
Analysis: Binary timestamp encoding detected

[system.log] Malware process started at: 1733491556
[system.log] Network callback initiated at: 1733491561
[system.log] Data exfiltration detected at: 1733491571
[system.log] Process cleanup executed at: 1733491555
[system.log] Connection closed at: 1733491567
[network.log] Secondary payload deployed at: 1733491586
[network.log] Encryption key exchange at: 1733491589
[network.log] Data tunnel established at: 1733491585
[network.log] Command received from C2 at: 1733491604
[network.log] Session terminated at: 1733491603

チャレンジの注釈には “Binary timestamp encoding detected … microsecond intervals” とある。ここでは秒単位にしか見えないけど、核となるアイデアは同じで、攻撃者はタイムスタンプ自体の 下位 1 バイトにデータを埋めている。

実際、各 UNIX タイムスタンプはイベント間でほぼ 1 秒ずつ増えている(17334915551733491556 など)。これは典型的な covert timing channel の手法で、攻撃者が「狙った下位バイトになるような時刻」に実行タイミングを調整して、ASCII を表現する。

整数の LSB(least significant byte)は、単純に 256 で割った余り。つまり 0–255 の 1 バイト値になって、ASCII 文字にそのままマッピングできる。実際にはこう:

  • low_byte = timestamp % 256

念のためのメモ:

カテゴリ10 進レンジ16 進レンジ
Printable ASCII(全体)32-1260x20-0x7E
Space320x20
Punctuation / Symbols33-470x21-0x2F
Digits48-570x30-0x39
Punctuation / Symbols58-640x3A-0x40
Uppercase letters65-900x41-0x5A
Punctuation / Symbols0x5B-0x60
Lowercase letters97-1220x61-0x7A
Punctuation / Symbols123-1260x7B-0x7E
Extended ASCII(非標準)128-2550x80-0xFF

ログチャネルが 2 つ(system.lognetwork.log)ある。ここが、「2 通りのデコード規則があるかも」というヒント。

実際に変換をチェックしてみる:

まずは一番素直に 各タイムスタンプを 256 で割った余り を取る。

  1. system.log はすぐに printable ASCII になる:
    1. 1733491556 % 256 = 100 (0x64)= d
    2. 1733491561 % 256 = 105 (0x69) = i
  2. network.log は下位バイトが 0x80-0x94 の範囲に落ちる。これは printable ではないけど、一貫して 小文字より +0x20 上になっている:
    1. 1733491586% 256 = 130 (0x82) = ??? ; 0x82 - 0x20 = 0x62 = b
    2. 1733491589 % 256 = 133 (0x85) = ??? ; 0x85 - 0x20 = 0x65 = e
%% Mermaid diagram showing +0x20 offset mapping (network.log -> ASCII)
graph TD
    title["Consistent +0x20 offset: network.log LSB → lowercase ASCII"]

    %% Raw bytes (upper line)
    R1["0x82"]:::raw
    R2["0x85"]:::raw
    R3["0x81"]:::raw
    R4["0x94"]:::raw
    R5["0x93"]:::raw

    %% Adjusted bytes (lower line)
    A1["0x62 = 'b'"]:::adj
    A2["0x65 = 'e'"]:::adj
    A3["0x61 = 'a'"]:::adj
    A4["0x74 = 't'"]:::adj
    A5["0x73 = 's'"]:::adj

    %% Connectors showing -0x20 offset (angled like downward curve)
    R1 -->|-0x20| A1
    R2 -->|-0x20| A2
    R3 -->|−0x20| A3
    R4 -->|−0x20| A4
    R5 -->|−0x20| A5

    %% Align horizontally
    R1 --- R2 --- R3 --- R4 --- R5
    A1 --- A2 --- A3 --- A4 --- A5

    %% Styles
    classDef raw fill:#ffb3b3,stroke:#ff6666,stroke-width:2px,color:#000;
    classDef adj fill:#b3ffd9,stroke:#33cc99,stroke-width:2px,color:#000;
    classDef title fill:none,stroke:none,font-size:20px;

    class R1,R2,R3,R4,R5 raw;
    class A1,A2,A3,A4,A5 adj;

FLAG: IBIZA{discobeats}


Reverse
#

Dub RC4 - 100 points
#

説明なし

Files*: hello

これは AppleScript のコンパイル済みファイル:

そもそも “AppleScript” って何?

macOS のアプリや OS を自動化/操作するために Apple が作ったスクリプト言語。AppleScript アプレットは通常 .app バンドルにこういう形で入っている:

MyApp.app/
  Contents/
    Info.plist
    MacOS/applet          # AppleScript 用の Mach-O スタブローダ
    Resources/
      Scripts/
        main.scpt         # コンパイル済み AppleScript(run-only のことが多い)
      *.icns, RTFD, etc.
  • Run-only スクリプト(配布されるアプレットのデフォルト)は、osadecompile でソースに戻せない。
  • ただし、スクリプトで使われる 文字列リテラル はコンパイル済みオブジェクト内に残るので、strings でわりと簡単に回収できる。
  • AppleScript は do shell script "..." で外部コマンドを叩きがちで、つまり実際に実行される シェルコマンド文字列(暗号コマンド、キー、ペイロードなど)がリソースとして埋まっていることが多い。

自分の方針: まずは逆コンパイルを試す:

jaybird1291🦉OBTS ~/Downloads % osadecompile hello
osadecompile: hello: errOSASourceNotAvailable (-1756).

これで、run-only AppleScript applet だと即分かる。つまりソースには戻せないが、埋め込まれた リテラル文字列が漏れている ので、重要な情報はそこから拾える。

次は strings:

jaybird1291🦉OBTS ~/Downloads % strings -a hello
FasdUAS 1.101.10
.aevtoappnull
****
.aevtoappnull
****
theargs
theArgs
theargs
theArgs
theenc
theEnc
thekey
theKey
.sysoexecTEXT
TEXT
thedec
theDec
ascr
txdl
olddelimiters
oldDelimiters
citm
theitems
theItems
TEXT
thestring
theString
.ascrcmnt****
****
ascr

この時点では特に面白くない。なので -ebencoding=16-bit bigendian)を付ける:

jaybird1291🦉OBTS ~/Downloads % strings -a -eb hello
U2FsdGVkX19qplDGXEwJiqGDOfxJCH99owUsxR0kJ7IACnDv+Fi8lYl/KdULG61qYHCxPNwPi4Ec11+QAxRGLw=
@27860c1670a8d2f3de7bbc74cd75412
echo 
X' | openssl enc -d -base64 -des3 -pass pass
 -pbkdf
Well done
 Nope, try again

コンパイル済み AppleScript バンドルだと、リソース文字列が UTF-16 で格納されていることがよくある。つまり見えている文字の間に NUL バイトが挟まる:

0x00 0x55  0x00 0x32  0x00 0x46  ...  =  "U2F..."

かなり良い感じなんだけど、まだ足りない情報もある。例えば TextEdit で見ると:

ここでようやく、本当に面白い部分が見える。整理するとこう:

U2FsdGVkX19qplDGXEwJiqGDOfxJCH99owUsxR0kJ7IACnDv+Fi8lYl/KdULG61qYHCxPNwPi4Ec11+QAxRGLw== theEnc
...
27860c1670a8d2f3de7bbc74cd754121 theKey
...
echo 'X' | openssl enc -d -base64 -des3 -pass pass -pbkdf

最初の文字列は明らかに base64。デコードすると Salted__j¦PÆ\L Š¡ƒ9üI}£Å$'²pïøX¼•‰Õ ­j`p±<Ü‹×_F みたいなバイナリになって、次の文字列がキー。

さらにコマンドが載っているので、そのまま実行すればフラグが出る:

echo 'U2FsdGVkX19qplDGXEwJiqGDOfxJCH99owUsxR0kJ7IACnDv+Fi8lYl/KdULG61qYHCxPNwPi4Ec11+QAxRGLw==' \
| openssl enc -d -base64 -des3 -pbkdf2 -pass pass:27860c1670a8d2f3de7bbc74cd754121

FLAG: IBIZA{I heard it through the grapevine}


XORed - 100 points
#

説明なし

Files: secret

これは Mach-O:

jaybird1291🦉OBTS ~/Downloads % file secret
secret: Mach-O 64-bit executable arm64

面倒なので、IDA が吐いた疑似コードを読む(動きが分かるように、コメントは ChatGPT に書かせたやつ):

// --- String.Encoding 用の Swift ワークバッファをセットアップ(スタックフレーム周り) ---
v0 = type metadata accessor for String.Encoding(0);
v1 = *(_QWORD *)(v0 - 8);
v2 = (char *)&v51 - ((*(_QWORD *)(v1 + 64) + 15LL) & 0xFFFFFFFFFFFFFFF0LL);

// --- $HOME を NSString として取得して、Swift String にブリッジ ---
v3 = objc_retainAutoreleasedReturnValue(NSHomeDirectory());
v4 = static String._unconditionallyBridgeFromObjectiveC(_:)(); // NSString -> Swift String
v6 = v5;                             // v5/v6 は Swift String を運ぶ
objc_release(v3);
v55 = v4;                            // ブリッジした Swift String を保持
v56 = v6;
swift_bridgeObjectRetain(v6);

// --- $HOME の末尾に "/" を 1 文字だけ追加(Swift の small string literal) ---
v7._countAndFlagsBits = 47;          // 47 == ASCII '/'(small-string の長さ/フラグが畳み込まれてる)
v7._object = (void *)0xE100000000000000LL; // 0xE1 タグ = Swift small-string ストレージ(1バイトペイロード)
String.append(_:)(v7);
swift_bridgeObjectRelease(v6);

// --- 残りのパスを、2 つのテーブルから小さな断片を順に連結して作る ---
for ( i = 32; i != 168; i += 8 )
{
  // off_1000084D8: 次に使う断片のインデックス表
  v9  = *(unsigned __int64 *)((char *)&off_1000084D8 + i);

  // off_1000083A8: 6-qword レコードのプール(各レコードは 1〜3 バイトの断片を表現)
  v10 = &off_1000083A8 + 2 * v9;

  // v10[4] = small-string のペイロード(リトルエンディアンで低位バイトに文字)
  // v10[5] = タグ/フラグ;最上位バイトが 0xE1/0xE2/0xE3 = 1/2/3 バイト採用
  v11 = (__int64)v10[4];
  v12 = v10[5];

  // この断片を Swift String として包んで、作成中のパスに append
  //(decompiler が v13 へのセットアップを省略している)
  String.append(_:)(v13);

  // 断片オブジェクトの retain/release 管理
  swift_bridgeObjectRelease(v12);
}

// ここまでで Swift String は:  $HOME + "/" + "Library/Application Support/.MSDIR"

// --- Objective-C に戻してファイルシステムのチェックを行う ---
v54 = 0;  // BOOL isDirectory の out-param を 0 で初期化
v14 = objc_retainAutoreleasedReturnValue(objc_msgSend(
        (id)objc_opt_self(&OBJC_CLASS___NSFileManager), "defaultManager"));

v15 = v56;
v16 = String._bridgeToObjectiveC()();  // Swift String -> NSString(Cocoa API 用)
swift_bridgeObjectRelease(v15);

LODWORD(v15) = (unsigned int)objc_msgSend(
        v14, "fileExistsAtPath:isDirectory:", v16, &v54);

objc_release(v14);
objc_release(v16);

// 結果: v15(BOOL)が存在判定、v54 がディレクトリかどうか(ここでは NO を期待)

一番大事なのは fragment loop(断片連結ループ)で、やっていることは:

  • off_1000084D8 のインデックス列を順番に読む(ここだと i = 32..160 step 8 = およそ 17 エントリ)。
  • 各インデックス idx について、off_1000083A8 + 16*idx に飛んで 6-qword レコードを読む。
    • qword[4] をリトルエンディアンのペイロードとして扱い、qword[5] の上位 1 バイトがタグ:
      • 0xE1 → 1 バイト、0xE2 → 2 バイト、0xE3 → 3 バイト取り出す。
    • その低位バイトを ASCII にして append。
  • その順に断片を連結する。今回のバイナリでは、最終的にこうなる:
"Lib","ra","ry","/","App","lic","ati","on"," ","Su","ppo","rt","/",".","MS","DI","R"

= Library/Application Support/.MSDIR

つまり、このコードは [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir] で、$HOME/Library/Application Support/.MSDIR をチェックしていて、exists == YES かつ isDir == NO(ディレクトリではない)を期待している。

チェックが通ると、小さな blob を 2 バイト鍵 "12" の繰り返しで XOR 復号する(18 を "%2x" でフォーマットすると "12" になるのでそこから来てる)、UTF-8 にして、フラグを表示する。

一番簡単な解法は、チェック対象のファイルを作って実行すること:

まずファイルが無い状態でどうなるか:

次にファイルを作って再実行:

[18:23:23] jaybird1291🦉OBTS ~/Downloads % mkdir -p "$HOME/Library/Application Support"
[18:23:34] jaybird1291🦉OBTS ~/Downloads % : > "$HOME/Library/Application Support/.MSDIR"

2 つめの解法はもう少し面白くて、LLDB でライブパッチしてファイルチェックをバイパスする方法。

Objective-C のディスパッチャ(objc_msgSend)を、セレクタ fileExistsAtPath:isDirectory: のときだけフックする。ヒットしたら戻り値を YES に固定して、実呼び出しをスキップする。

これは objc_msgSend 入口のレジスタがこうなるから成立する:

  • x0 = self(オブジェクト)
  • x1 = _cmd(セレクタ)
  • x2 = path (NSString *)(第 1 引数)
  • x3 = isDirectory (BOOL *)(第 2 引数)

なので *(BOOL*)x3 = NO(ディレクトリではない)、戻り値 w0 = 1(YES)にして、pc = lr (x30) で呼び出し元に戻せばいい。

LLDB の手順はこう:

  1. objc_msgSend にブレークポイントを置き、条件としてセレクタ名が "fileExistsAtPath:isDirectory:" のときだけ止まるようにする
  2. 実行
    1. 止まったら「狙ったセレクタの objc_msgSend 入口」にいる状態
  3. 何をチェックしているか確認する: po (NSString*)$x2
    1. /Users/jaybird1291/Library/Application Support/.MSDIR
  4. out-parameter の isDirectoryNO にする(フォルダではなくファイルに見せたい): expr -lobjc++ -- *(BOOL*)$x3 = (BOOL)0
  5. 戻り値を YES にする(ファイルが存在する): register write w0 1
  6. Foundation のメソッド自体は呼びたくないので、呼び出しをスキップして return address にジャンプする
    1. まず LR(x30)を読んで戻り先のアドレスを得る: register read x30
      1. lr = 0x0000000100000da4 secret___lldb_unnamed_symbol141 + 356
    2. PC をその値にする: register write pc 0x0000000100000da4
  7. 続行してフラグを回収

FLAG: IBIZA{@noarfromspace says hi!}


Forensic
#

PowerLog 1 - 10 points
#

アンインストールされたアプリの bundle id は?

Files*: powerlog_2025-10-16_13-55_E98FFE1C.PLSQL

SELECT *
FROM PLApplicationAgent_EventForward_ApplicationDidUninstall

FLAG: com.esim.tele


PowerLog 2 - 10 points
#

アンインストールされたアプリのバージョンは?

SELECT *
FROM PLApplicationAgent_EventNone_AllApps
WHERE appBundleID = 'com.esim.tele';

FLAG: 2.5.6


PowerLog 3 - 25 points
#

13:30 頃(イビサのローカル時間)にメディア再生していたアプリの bundle id を答えよ

SELECT 
    datetime(timestamp, 'unixepoch', 'localtime') AS timestamp,
    *
FROM PLAudioAgent_EventPoint_AudioApp

FLAG: com.apple.WebKit.GPU


PowerLog 4 - 15 points
#

2025-10-16 にヘッドホンが使われていた時間は合計何秒?

SELECT 
    datetime(timestamp, 'unixepoch', 'localtime') AS timestamp,
    *
FROM PLAudioAgent_EventForward_Routing;

13:45:20 から 13:53:38 まで = 498 秒

FLAG: 498


PowerLog 5 - 10 points
#

この端末のセルラープロバイダは?

SELECT 
    datetime(timestamp, 'unixepoch', 'localtime') AS timestamp,
    *
FROM PLBBAgent_EventForward_TelephonyRegistration;

FLAG: Orange


PowerLog 6 - 10 points
#

最後にカメラを使ったアプリの bundle id は?

SELECT 
    datetime(timestamp, 'unixepoch', 'localtime') AS timestamp,
    *
FROM PLCameraAgent_EventForward_Camera;

FLAG: com.burbn.instagram


PowerLog 7 - 10 points
#

フラッシュライト(Torch)の使用時間は(秒単位、整数)?

SELECT 
    datetime(timestamp, 'unixepoch', 'localtime') AS timestamp,
    *
FROM PLCameraAgent_EventForward_Torch;

FLAG: 211


PowerLog 8 - 25 points
#

この端末の iCloud 容量は何 GB?

SELECT 
    datetime(timestamp, 'unixepoch', 'localtime') AS timestamp,
    *
FROM PLConfigAgent_EventNone_Config;

FLAG: 5


PowerLog 10-25 points
#

最後に Apple Pay 取引が発生したのはいつ?(YYYY-MM-DD HH:MM:SS)(イビサのローカル時間)

SELECT 
    datetime(timestamp, 'unixepoch', 'localtime') AS timestamp,
    *
FROM PLNfcAgent_EventForward_Transaction;

PLStorageOperator_EventForward_TimeOffset にあるシステムオフセットを使って、タイムスタンプを補正する必要がある:

Timestamp: 1760613928.63204 Drift: -851.035538911819

FLAG: 2025-10-16 13:11:17


PowerLog 11-10 points
#

アプリスイッチャ(Application Switcher)経由で起動された可能性が高いアプリの bundle id は?

SELECT 
    datetime(timestamp, 'unixepoch', 'localtime') AS timestamp,
    *
FROM PLAppTimeService_Aggregate_AppRunTime;
WITH numbered AS (
    SELECT 
        ROW_NUMBER() OVER (ORDER BY timestamp) AS rn,
        datetime(timestamp, 'unixepoch', 'localtime') AS readable_time,
        *
    FROM PLAppTimeService_Aggregate_AppRunTime
),
matches AS (
    SELECT rn
    FROM numbered
    WHERE BundleID = 'com.apple.springboard.app-switcher'
)
SELECT n.*
FROM numbered n
JOIN matches m
  ON n.rn = m.rn + 1
ORDER BY n.rn;

FLAG: com.apple.Maps