Skip to main content

OBTS v8.0 CTF

·13 mins· loading · loading · ·
Jaybird1291
Author
Jaybird1291
Table of Contents

The OBTS v8.0 CTF was a team CTF with three participants. I played with the team “LesStagiaires”, and we finished in 4th place!

Cryptography
#

Patch Balearic - 10 points
#

You’re the security consultant for an exclusive Ibiza beach club! Their app management system had a vulnerability, but they’ve released a patch.

Mission Can you find what changed and discover the hidden beach party access code?

Flag Format IBIZA{.....}

Files: APP_V1.txt, APP_V2.txt

Here’s the files: 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...

With these two plaintext logs files we can easily see that:

  • app v1 (pre-patch): lot of problems and a reference to ROT13 (“ROT-XIII cipher”, “rotate cipher by 13”).
  • app v2 (post-patch): the deployment log showing fixes applied and an “access token”.

Key infos:

- 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}

First, let’s decrypt ROT13 payload Hafrpherq_Cngu_Qrgrpgrq_Cngpu_Zvffvat which gives Unsecured_Path_Detected_Patch_Missing.

Then, let’s decrypt the “access token generated” VOVMN{ju173_f4aqf} which gives IBIZA{wh173_s4nds}.

FLAG: IBIZA{wh173_s4nds}


Sunset Time Oclock - 10 points
#

While analyzing an iOS sysdiagnose file, you discovered that an attacker hid a message in the timestamp precision. The malware was designed to execute at very specific microsecond intervals to encode data.

Mission Find the flag.

Flag Format IBIZA{...}

Files*: malware_execution.log

Here’s the file:

=== 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

The challenge note says: “Binary timestamp encoding detected … microsecond intervals”. Even though we only see seconds here, the core idea is the same: the attacker hid data in the low byte of the timestamp itself.

In fact, each UNIX timestamp increases by roughly 1 second between events (1733491555 –> 1733491556 etc.). This is a classic covert timing channel technique: the attacker choose a precise firing time so that the timestamp’s LSB (least significant byte) equals the ASCII code they want.

The LSB of an integer is simply the remainder after division by 256. That perfectly gives a value in the range 0-255 (one byte), which maps directly to a single ASCII character. Practically it gives:

  • low_byte = timestamp % 256.

Here’s a little reminder:

CategoryDecimal RangeHex Range
Printable ASCII (overall)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 (non-standard)128-2550x80-0xFF

Two log channels are present (system.log and network.log). That’s the hint that two different decoding rules may apply.

Let’s check how it translates:

We start with the simplest: take each timestamp modulo 256.

  1. For system.log we immediately get printable ASCII:
    1. 1733491556 % 256 = 100 (0x64)= d
    2. 1733491561 % 256 = 105 (0x69) = i
  2. For network.log, the low byte lands in the 0x80-0x94 range. Those aren’t printable ASCII, but they’re consistently +0x20 above lowercase letters:
    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
#

No description

Files*: hello

This is an AppleScript compiled file:

What is “AppleScript”?

This a scripting language created by Apple to automate and control macOS apps and the operating system. The AppleScript applets are normally packaged in a .app bundle like:

MyApp.app/
  Contents/
    Info.plist
    MacOS/applet          # Mach-O stub loader for AppleScript
    Resources/
      Scripts/
        main.scpt         # compiled AppleScript (often run-only)
      *.icns, RTFD, etc.
  • Run-only scripts (the default for shipped applets) cannot be decompiled with osadecompile.
  • However, string literals used by the script are stored in the compiled object and are easy to recover with strings.
  • AppleScript commonly shells out via do shell script “…”, which means the exact shell command strings (including crypto commands, keys, and payloads) tend to be present in the binary resources.

My approach: First I tried to decompile it:

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

Here we can quickly get that this is a run-only AppleScript applet. So we can’t decompile it back to source, but its embedded literal strings leak the important bits.

Move on to 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

Not particularly interesting. Let’s use the eb argument (which is 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

In fact, in compiled AppleScript bundles lots of resource strings get stored as UTF-16 and not 8-bit ASCII. That means that each visible character is interleaved with a NUL byte like:

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

It’s great but actually we miss quite a lot of things. For example with textedit I could see:

And finally we can see really interesting things! Here’s a cleaned version:

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

The first string is obviously base64 which gives Salted__j¦PÆ\L Š¡ƒ9üI}£Å$'²pïøX¼•‰Õ ­j`p±<Ü‹×_F while the second one is the key.

We then have the command so we can just execute it to get the flag:

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
#

No description

Files: secret

This is a Mach-O

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

Since I’m lazy I’ll just read the pseudocode generated by IDA (with ChatGPT generated comments to explain how it works):

// --- Set up a Swift scratch buffer for String.Encoding (stack frame stuff) ---
v0 = type metadata accessor for String.Encoding(0);
v1 = *(_QWORD *)(v0 - 8);
v2 = (char *)&v51 - ((*(_QWORD *)(v1 + 64) + 15LL) & 0xFFFFFFFFFFFFFFF0LL);

// --- Get $HOME as an NSString, bridge to Swift String ---
v3 = objc_retainAutoreleasedReturnValue(NSHomeDirectory());
v4 = static String._unconditionallyBridgeFromObjectiveC(_:)(); // bridge NSString -> Swift String
v6 = v5;                             // v5/v6 carry the Swift String
objc_release(v3);
v55 = v4;                            // keep the bridged Swift String
v56 = v6;
swift_bridgeObjectRetain(v6);

// --- Append a single "/" to the $HOME path (Swift small string literal) ---
v7._countAndFlagsBits = 47;          // 47 == ASCII '/'  (small-string length/flags folded in)
v7._object = (void *)0xE100000000000000LL; // 0xE1 tag = Swift small-string storage (1-byte payload)
String.append(_:)(v7);
swift_bridgeObjectRelease(v6);

// --- Build the rest of the path by appending tiny fragments from two tables ---
for ( i = 32; i != 168; i += 8 )
{
  // off_1000084D8: index table telling which fragment to use next
  v9  = *(unsigned __int64 *)((char *)&off_1000084D8 + i);

  // off_1000083A8: pool of 6-qword records; each record encodes a 1–3 byte fragment
  v10 = &off_1000083A8 + 2 * v9;

  // v10[4] = small-string payload (little-endian chars in the low bytes)
  // v10[5] = tag/flags; its top byte is 0xE1/0xE2/0xE3 meaning 1/2/3 bytes to take
  v11 = (__int64)v10[4];
  v12 = v10[5];

  // wrap that tiny fragment as a Swift String and append to the growing path
  // (decompiler elided setup as v13)
  String.append(_:)(v13);

  // retain/release management for the fragment object
  swift_bridgeObjectRelease(v12);
}

// At this point the Swift String is:  $HOME + "/" + "Library/Application Support/.MSDIR"

// --- Bridge back to NSString and perform the filesystem check ---
v54 = 0;  // BOOL isDirectory out-param init to 0
v14 = objc_retainAutoreleasedReturnValue(objc_msgSend(
        (id)objc_opt_self(&OBJC_CLASS___NSFileManager), "defaultManager"));

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

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

objc_release(v14);
objc_release(v16);

// Result: v15 (BOOL) says exists or not; v54 says whether it's a directory (expects NO)

The most important part here is the fragment loop which:

  • Read the sequence of indices from off_1000084D8 (here: i = 32..160 step 8 = ~17 entries).
  • For each index idx, go to off_1000083A8 + 16*idx and read a 6-qword record.
    • Take qword[4] as the little-endian payload; the top byte of qword[5] is the tag:
      • 0xE1 → take 1 byte, 0xE2 → 2 bytes, 0xE3 → 3 bytes.
    • Convert those low bytes to ASCII and append.
  • Concatenate fragments in that order. In this binary they decode to:
"Lib","ra","ry","/","App","lic","ati","on"," ","Su","ppo","rt","/",".","MS","DI","R"

= Library/Application Support/.MSDIR

So basically, this code check with [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir] the path $HOME/Library/Application Support/.MSDIR and expects exists == YES and isDir == NO.

If the check passes, it decrypts a tiny blob by XORing it with a repeating 2-byte key "12" (because it formats the integer 18 with "%2x" = "12"), turns the result into UTF-8, and prints the flag.

The first solution is the easiest, create the file it checks and run it:

First, let’s see how it goes without the file:

Let’s now create the file and re-run:

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

The second solution is more interesting and consist of patching it live with LLDB to bypass the file check

We’ll hook the Objective-C dispatcher (objc_msgSend) only for the selector fileExistsAtPath:isDirectory:. When we hit it, we’ll force the result to YES and skip the call.

It works because at the entry of objc_msgSend, registers are:

  • x0 = self (the object)
  • x1 = _cmd (selector)
  • x2 = path (NSString *) (first argument)
  • x3 = isDirectory (BOOL *) (second argument)

So we can set *(BOOL*)x3 = NO (not a directory), set the return value w0 = 1 (YES) and then jump back to the caller by setting pc = lr (x30).

Here’s a step by step with LLDB:

  1. Set a breakpoint on objc_msgSend and add a condition: only stop when the selector name is "fileExistsAtPath:isDirectory:"
  2. Run
    1. When it stops it means that we’re at the entry of objc_msgSend for the right selector
  3. Inspect what it’s checking with po (NSString*)$x2
    1. /Users/jaybird1291/Library/Application Support/.MSDIR
  4. Set the out-parameter isDirectory to NO because we want the program to think the path is a file, not a folder with expr -lobjc++ -- *(BOOL*)$x3 = (BOOL)0
  5. Set the return value to YES which means that “the file exists” with register write w0 1
  6. Skip the call and go back to the caller because we don’t want to run the actual Foundation method; we’re faking it so we directly jump to the return address
    1. First, read LR (x30) to get the numeric address with register read x30
      1. lr = 0x0000000100000da4 secret___lldb_unnamed_symbol141 + 356
    2. Set the program counter (PC) to that value with register write pc 0x0000000100000da4
  7. Continue and get the flag

FLAG: IBIZA{@noarfromspace says hi!}


Forensic
#

PowerLog 1 - 10 points
#

What is the bundle id of an app that was uninstalled?

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

SELECT *
FROM PLApplicationAgent_EventForward_ApplicationDidUninstall

FLAG: com.esim.tele


PowerLog 2 - 10 points
#

What is the version of the application that was uninstalled?

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

FLAG: 2.5.6


PowerLog 3 - 25 points
#

Provide the bundle id of the application that was playing media at around 13:30 (local Ibiza Time)

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

FLAG: com.apple.WebKit.GPU


PowerLog 4 - 15 points
#

How long in seconds were headphones in use on 2025-10-16?

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

13:45:20 to 13:53:38 = 498 seconds

FLAG: 498


PowerLog 5 - 10 points
#

What is the cellular provider for this device?

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

FLAG: Orange


PowerLog 6 - 10 points
#

What is the bundle id for the last application that used the camera?

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

FLAG: com.burbn.instagram


PowerLog 7 - 10 points
#

How long was the Flashlight (or Torch for you proper English speakers) in use for (whole seconds)?

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

FLAG: 211


PowerLog 8 - 25 points
#

How many GBs of iCloud space does this device have?

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

FLAG: 5


PowerLog 10-25 points
#

When did an Apple Pay transaction last occur? (YYYY-MM-DD HH:MM:SS) (Local Ibiza time zone)

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

We need to edit the timestamp with the system offset found in PLStorageOperator_EventForward_TimeOffset:

Timestamp is: 1760613928.63204 Drift is: -851.035538911819

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


PowerLog 11-10 points
#

What is the bundle id of the application that was likely launched via the Application Switcher?

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