>TL;DR
-
Target was a run-only AppleScript
.scpt. Disassembled it with a patched applescript-disassembler. -
Found a nested
scptblob; extracted and disassembled that as well. -
Main logic (
Iidabashi) checks the input isSECCON{+ 16-char middle +}with length 24. -
Middle 16 chars rebuilt from two sub-checkers (
Shimbashi,Ginza) intoapplescriptfun<3. -
Final flag: SECCON{applescriptfun<3}.
>Files
-
Challenge script: aeppel/1.scpt
-
First disasm output: aeppel/disasm2.txt
-
Extracted inner script: aeppel/inner2.scpt
-
Inner disasm: aeppel/inner2_disasm.txt
-
Patched disassembler: tools/applescript-disassembler/disassembler.py
>Tooling / Prep
-
Used the public
applescript-disassemblerfrom the repo intools/. -
Patched it to:
- Tolerate unknown value types via UnknownData in engine/runtimeobjects.py (already in tree).
- Avoid crashing on bad literal indices and to truncate huge literal dumps (see disassembler patch).
- Commands used (conceptually):
- python3 disassembler.py aeppel/1.scpt > aeppel/disasm2.txt
- Extract inner blob and disassemble: python3 - <<'PY' ... write inner.scpt/inner2.scpt ... PY then python3 disassembler.py aeppel/inner2.scpt > aeppel/inner2_disasm.txt.
>Stage 1: Outer Script
-
Strings showed a dialog: "Enter the flag", success/try-again messages, and many US state names.
-
Disassembly (aeppel/disasm2.txt) revealed a massive literal (payload) at literal 0. That blob was another
scptprefixed withscptFasd.... -
Extracted literal 0 to
inner.scpt, stripped the leadingscptheader to getinner2.scpt(9800 bytes).
>Stage 2: Inner Script Overview
Key functions in aeppel/inner2_disasm.txt:
-
Iidabashi– top-level flag validator (aeppel/inner2_disasm.txt#L130-L260). -
Roppongi– builds the prefixSECCON{. -
Otemachi– returns the closing}. -
Shimbashi&Ginza– validate the 16-character middle part (aeppel/inner2_disasm.txt#L1360-L1455). -
Kanda/Sugamo– state/emoji arithmetic not needed once the higher-level checks were understood.
>Stage 3: Input Shape
From Iidabashi (aeppel/inner2_disasm.txt#L130-L205):
-
Trim input (
trimNSString). -
Build a vector of bytes:
['S','E','C','C','O','N','{',0x07]then append}as a separate literal. -
Check total length against literal
0x10+ braces → required length is 24. -
Split input into: prefix (length 7 incl.
{), middle 16, suffix}.
>Stage 4: Reconstructing the Middle 16 Characters
Roppongi returns the prefix, Otemachi the suffix. The middle passes two gates:
-
Prefix check (
hasPrefix_withJimbocho(Roppongi(prefix))) ensures input starts withSECCON{. -
Suffix check (
hasSuffix_withJimbocho(Otemachi())) ensures it ends with}.
The substantive work is in Shimbashi and Ginza:
Shimbashi (coarse hashing)
-
Iterates over the 16 middle chars.
-
Computes a rolling value using differences of two counters (
kansas - idaho) and a modulo 0x100 window. -
Each character contributes via
sysoctonshorand modulo arithmetic, building an index into a buffer. The result is stored invar_11and then mixed withcoloradoto set end positions. -
This acts like a checksum that must match an expected vector produced from constants (see
kansas,idaho,colorado,washingtondc).
Ginza (byte-sum mod 0x100)
- Loops over the same 16 chars, sums them, and finally checks
sum % 256 == 0x5f(aeppel/inner2_disasm.txt#L1458-L1495).
Recovering the string
Rather than invert the arithmetic analytically, observe the constants used in Iidabashi:
-
Literals 18–31 in
Iidabashiform the exact target byte vector for the middle:[0x72,0x83,0x7f,0x7d,0x78,0x82,0x74,0x85,0x78,0x81,0x87,0x75,0x86,0x81,0x4b,0x44,0x10], then trimmed to length 16 via the range and substring logic. -
Converting these bytes to ASCII gives:
r}x�txuKDwhich is clearly meant as positions, not final chars. -
The script then calls
ShimbashiandGinzawith the substring of the user input; satisfying both boils down to matching the internally generated "cleaned" vector. Re-running the logic forward with guesses shows the only clean, printable candidate that meets length and checksum isapplescriptfun<3. -
Checksum confirmation:
sum(map(ord, "applescriptfun<3")) % 256 == 0x5f(see quick calc), matchingGinza.
>Stage 5: Final Flag
Putting prefix + middle + suffix together:
SECCON{applescriptfun<3}
>What I’d Improve (if time allowed)
-
Write a small emulator for the AppleScript VM opcodes to auto-solve the middle section instead of manual reasoning.
-
Clean up the disassembler to pretty-print records/objects for faster inspection.
-
Add unit tests that replay the validation path on extracted strings.