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 秒ずつ増えている(1733491555 → 1733491556 など)。これは典型的な covert timing channel の手法で、攻撃者が「狙った下位バイトになるような時刻」に実行タイミングを調整して、ASCII を表現する。
整数の LSB(least significant byte)は、単純に 256 で割った余り。つまり 0–255 の 1 バイト値になって、ASCII 文字にそのままマッピングできる。実際にはこう:
low_byte=timestamp% 256
念のためのメモ:
| カテゴリ | 10 進レンジ | 16 進レンジ |
|---|---|---|
| Printable ASCII(全体) | 32-126 | 0x20-0x7E |
| Space | 32 | 0x20 |
| Punctuation / Symbols | 33-47 | 0x21-0x2F |
| Digits | 48-57 | 0x30-0x39 |
| Punctuation / Symbols | 58-64 | 0x3A-0x40 |
| Uppercase letters | 65-90 | 0x41-0x5A |
| Punctuation / Symbols | 0x5B-0x60 | |
| Lowercase letters | 97-122 | 0x61-0x7A |
| Punctuation / Symbols | 123-126 | 0x7B-0x7E |
| Extended ASCII(非標準) | 128-255 | 0x80-0xFF |
ログチャネルが 2 つ(system.log と network.log)ある。ここが、「2 通りのデコード規則があるかも」というヒント。
実際に変換をチェックしてみる:
まずは一番素直に 各タイムスタンプを 256 で割った余り を取る。
system.logはすぐに printable ASCII になる:1733491556% 256 = 100 (0x64)= d1733491561% 256 = 105 (0x69) = i- …
network.logは下位バイトが0x80-0x94の範囲に落ちる。これは printable ではないけど、一貫して 小文字より+0x20上になっている:1733491586% 256 = 130 (0x82) = ??? ; 0x82 - 0x20 = 0x62 = b1733491589% 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
この時点では特に面白くない。なので -eb(encoding=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。
- qword[4] をリトルエンディアンのペイロードとして扱い、qword[5] の上位 1 バイトがタグ:
- その順に断片を連結する。今回のバイナリでは、最終的にこうなる:
"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 の手順はこう:
objc_msgSendにブレークポイントを置き、条件としてセレクタ名が"fileExistsAtPath:isDirectory:"のときだけ止まるようにする- 実行
- 止まったら「狙ったセレクタの
objc_msgSend入口」にいる状態
- 止まったら「狙ったセレクタの
- 何をチェックしているか確認する:
po (NSString*)$x2/Users/jaybird1291/Library/Application Support/.MSDIR
- out-parameter の
isDirectoryを NO にする(フォルダではなくファイルに見せたい):expr -lobjc++ -- *(BOOL*)$x3 = (BOOL)0 - 戻り値を YES にする(ファイルが存在する):
register write w0 1 - Foundation のメソッド自体は呼びたくないので、呼び出しをスキップして return address にジャンプする
- まず LR(
x30)を読んで戻り先のアドレスを得る:register read x30lr = 0x0000000100000da4 secret___lldb_unnamed_symbol141 + 356
- PC をその値にする:
register write pc 0x0000000100000da4
- まず LR(
- 続行してフラグを回収

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