- Source: SECCON 14 Quals
- Author: rand0m
AppleScriptのバイナリが渡される。Linux環境では実行できないのでとりあえず逆アセンブルしたい。
applescript-disassemblerというツールを見つけたので、これを使う。
とりあえず雑に実行してみると数万文字のbyte列と僅かな命令が出力された。これでは何も分からないので困った。(SECCON大会中はこのようなbyte列でdisassemblerがエラーを吐いていたためLLMに言われるがまま適当に自分でパッチして実行していたが、今はbyte列を扱えるようにするPRが取り込まれていた)
$ ./applescript-disassembler/disassembler.py 1.scpt
=== data offset 2 ===
Function name : <Value type=object value=<Value type=event_identifier value=b'aevt'-b'oapp'-b'null'-b'\x00\x00\x80\x00'-b'****'-b'\x00\x00\x90\x00'>>
Function arguments: <empty or unknown>
00000 PushLiteral 0 # <Value type=rawdata value=b'scptFasdUAS 1.101.10\x0e\x00\x00\x00\x04\x0f\xff\xff\xff\xfe\x00\x01...(略)...ascr\x00\x01\x00\r\xfa\xde\xde\xad'>
00001 Push0
00002 MessageSend 1 # <Value type=object value=<Value type=event_identifier value=b'syso'-b'dsct'-b'****'-b'\x00\x00\x00\x00'-b'scpt'-b'\x00\x00\x00\x00'>>
00005 GetData
00006 PopGlobal b'res'
00007 StoreResult
00008 PushGlobal b'res'
00009 Return
0000a Return
とりあえずスクリプトを書いてこの謎のbyte列を取り出す。
#!/usr/bin/env python3
import ast
import subprocess
p = subprocess.run(
["./applescript-disassembler/disassembler.py", "1.scpt"],
capture_output=True,
text=True,
)
s = p.stdout
k = s.find("Value type=rawdata value=")
a = s.find("b'", k)
q = "'"
if a == -1:
a = s.find('b"', k)
q = '"'
i = a + 2
esc = False
while True:
c = s[i]
if esc:
esc = False
elif c == "\\":
esc = True
elif c == q:
break
i += 1
raw = ast.literal_eval(s[a : i + 1])
with open("raw.bin", "wb") as f:
f.write(raw)
このraw.binをfileコマンドで読んだが何も分からず。LLMに投げるとAppleScriptのバイナリだと教えてくれたが、disassemblerが認識してくれない。
どうやらヘッダが本来Fasdであるところ、scptFasdになっている。単純にraw.binの先頭4byteを切り捨てるとdisassmblerが認識してくれるようになった。
すると、長いのでここには載せないが1600行ほどのそれっぽい出力が得られた。この結果をLLMに投げてflagを復元するスクリプトを書くよう頼むと、以下のスクリプトが得られた。
どうやらShimbashiという名前の関数で入力を処理し、var_11の値と一致するかを確認しているらしい。具体的な計算式まで教えてくれた。
暗号化された値 = 元の文字コード + 13 + ((13 * (インデックス % 3 + 1)) % 11) とのこと。
def solve_applescript_flag():
# Source 16-17 から抽出したターゲット配列 (var_11)
# 最後の 0x10 は MakeVector の要素数指定のため除外
target_values = [
0x72, 0x83, 0x7f, 0x7d, 0x78, 0x82, 0x74, 0x85,
0x78, 0x81, 0x87, 0x75, 0x86, 0x81, 0x4b, 0x44
]
# Source 14 で定義されているプレフィックス
# [0x53, 0x45, 0x43, 0x43, 0x4f, 0x4e, 0x7b] -> "SECCON{"
prefix = "SECCON{"
# Source 15 で定義されているサフィックス
# 0x7d -> "}"
suffix = "}"
# Shimbashi 関数の逆算
# 暗号化ロジック: target = char_code + 13 + ((13 * (i % 3 + 1)) % 11)
# 復号ロジック: char_code = target - 13 - ((13 * (i % 3 + 1)) % 11)
decrypted_content = ""
for i, val in enumerate(target_values):
# AppleScriptのループインデックスは通常1始まりとして扱われることが多い
# ディスアセンブリの挙動的にも1-based indexとして計算すると意味の通る文字列になる
index = i + 1
# Source 68 の計算部分
# PushVariable [var_5] (13) * index % 3 ... の部分
# var_5 は (6856 - 6843) % 256 = 13
noise = (13 * (index % 3 + 1)) % 11
# 定数加算分 (Source 68: PushVariable [var_1] -> 13)
constant_offset = 13
original_char_code = val - constant_offset - noise
decrypted_content += chr(original_char_code)
flag = prefix + decrypted_content + suffix
print(f"Decrypted Content: {decrypted_content}")
print(f"Full Flag: {flag}")
# 検算: Ginza関数のチェック (合計 % 256 == 95 ('_') かどうか)
# Source 18: var_12 = 0x5f (95)
total = sum(ord(c) for c in decrypted_content)
if total % 256 == 95:
print("Check: Ginza verification passed (Sum % 256 == 95).")
else:
print(f"Check: Ginza verification FAILED (Sum % 256 = {total % 256}).")
if __name__ == "__main__":
solve_applescript_flag()
これを実行するとflagが得られた。
SECCON{applescriptfun<3}