【AIS3 Pre-Exam 2026 Writeup】Reverse - 哇!金色傳說&Hidden-in-the-Cloak
Decompiled the Unity Mono code first.
ilspycmd -p Reverse1_Data/Managed/Assembly-CSharp.dll -o /tmp/golden-legendary-ilspy
rg -n "AIS3|flag|GachaServerUrl|rate|spend|username|gold|score|kills" /tmp/golden-legendary-ilspy
GameManager.cs had the server URL.
public string GachaServerUrl { get; } = "http://chals1.ais3.org:50001";
GachaServer.cs was the useful file. The client sends this JSON to the server.
float num = UnityEngine.Random.Range(0f, 0.3f);
string s2 = $"{{\"spend\":{spend},\"rate\":{num:F4},"
+ "\"username\":\"" + EscapeJson(s) + "\","
+ $"\"gold\":{num2},\"score\":{num3},\"kills\":{num4}}}";
So normal play only sends rate from 0.0000 to below 0.3000. The UI also checks minimum bet and current gold, but that is only client side.
Native reversing was not useful here. Reverse1.exe is just the Unity loader, and the real logic is in Assembly-CSharp.dll. No GOT overwrite idea, no saved RIP, no glibc printf path. I opened gdb once but it only confirmed the process was Unity runtime code, not where the challenge logic lived. No trigger, no path.
First server test used a normal request. It returned normal weapon and armor JSON. The next guess was to send a rate outside the client range.
curl -sS -X POST http://chals1.ais3.org:50001 \
-H 'Content-Type: application/json' \
--data '{"spend":50,"rate":0.3001,"username":"u","gold":0,"score":0,"kills":0}'
The armor name became:
AIS3{At_Least_U_DIDNT_MODIFY_MY_MONEY_RIGHT?}
I was not sure if the second flag was another server condition. I fuzzed spend, gold, score, kills, username, and extra keys like value, secret, golden, legendary. Most values only changed item stats. rate > 1 returned a cheater item. 0.3 < rate <= 1 always returned the same flag.
Port scan showed one suspicious extra service.
nmap -Pn -sV -p50001,50002 chals1.ais3.org
nc chals1.ais3.org 50002
50002 only printed Value: 1337 and closed. HTTP, JSON, raw 1337, and path tricks all got the same output. I went back to local assets later.
UnityPy found the character_main bundle. The texture character.png had readable text fragments inside the Spine atlas. The Spine JSON also had an emote_a animation. At time 0.75, bones b013 to b025 are moved into one line. Their slots map to atlas regions containing the text pieces.
Slot order gave:
AIS3{d0n7_70uch_my_c4p3_0k_b3f1e768}
Full solve script:
#!/usr/bin/env python3
import json
import re
import requests
import UnityPy
URL = "http://chals1.ais3.org:50001"
BUNDLE = "Reverse1_Data/StreamingAssets/bundles/character_main"
r = requests.post(URL, json={
"spend": 50,
"rate": 0.3001,
"username": "u",
"gold": 0,
"score": 0,
"kills": 0,
}, timeout=5)
print(re.search(r"AIS3\{[^}]+\}", r.text).group(0))
env = UnityPy.load(BUNDLE)
texts = {}
for obj in env.objects:
try:
data = obj.read()
except Exception:
continue
if obj.type.name == "TextAsset":
name = getattr(data, "name", "") or getattr(data, "m_Name", "")
script = getattr(data, "script", None) or getattr(data, "m_Script", None)
if isinstance(script, bytes):
script = script.decode("utf-8", "replace")
texts[name] = script
spine = json.loads(texts["character"])
pieces = {
"s013": "AIS3{",
"s014": "d0n7_70",
"s015": "uch_my_",
"s016": "c4p3_0k_",
"s017": "b",
"s018": "3",
"s019": "f",
"s020": "1",
"s021": "e",
"s022": "7",
"s023": "6",
"s024": "8",
"s025": "}",
}
order = [f"s{i:03d}" for i in range(13, 26)]
print("".join(pieces[s] for s in order))