CyberMap Group
Back to CyberMap
CyberMap
Home
About
Team
Contact
Back to Blog
Writeup

Grand Larceny Auto — Client-Side Trust Bypass to Hardcoded Secret Disclosure

Room: Grand Larceny Auto A locally-installed, single-player crime-simulator game hides a bonus reward behind an in-game vault that the game itself states can only be opened by reaching a police “wanted” reputation level one step higher than the maximum the game ever lets a player reach through normal play. Anyone willing to open the game’s […]

İsmail Özdoğanİsmail Özdoğan
July 26, 202616 Minutes Read

Room: Grand Larceny Auto

A locally-installed, single-player crime-simulator game hides a bonus reward behind an in-game vault that the game itself states can only be opened by reaching a police “wanted” reputation level one step higher than the maximum the game ever lets a player reach through normal play. Anyone willing to open the game’s own installed files and change a single number the game keeps in its own memory — no network access, no account, no special hardware, and no need to actually finish the intended gameplay loop — can push that value past the game’s own ceiling and read the hidden reward directly.

The vault’s unlock check and the encryption key that protects the reward behind it both depend entirely on that same player-controlled value, a client-side trust problem (see Section 3) that heavy code obfuscation did nothing to actually prevent once the game’s compiled logic was decompiled (see Section 10 for the full methodology).

Contents

1. Recon

2. Attack Flow

3. The Vulnerability

3.1 Root Cause

3.2 Why the Encryption Adds No Real Barrier

3.3 How an Apparent Two-Factor Gate Collapses to One Trust Boundary

4. Exploitation Walkthrough

5. Dead Ends

6. Secondary Finding: A Hardcoded Cheat-Code Flag in Plain Sight

7. Real-World Impact

8. Detection Guidance

9. Remediation

10. Methodology Summary

11. Takeaways

1. Recon

The target is the Windows build of Grand Larceny Auto, a small Godot 4 crime-simulator game shipped as a self-contained executable: a top-down open-world where the player steals traffic cones and cars, accrues police “wanted stars,” and is told that a sealed safehouse vault has never been opened by anyone.

$ find "GrandLarcenyAuto-windows" -maxdepth 3
GrandLarcenyAuto-windows/GrandLarcenyAuto.pck
GrandLarcenyAuto-windows/GrandLarcenyAuto.exe
GrandLarcenyAuto-windows/data_GrandLarcenyAuto_windows_x86_64/GodotSharp.dll
GrandLarcenyAuto-windows/data_GrandLarcenyAuto_windows_x86_64/coreclr.dll
GrandLarcenyAuto-windows/data_GrandLarcenyAuto_windows_x86_64/GrandLarcenyAuto.dll
GrandLarcenyAuto-windows/data_GrandLarcenyAuto_windows_x86_64/GrandLarcenyAuto.deps.json
...

The presence of a full embedded .NET runtime (coreclr.dll, GodotSharp.dll) alongside a small GrandLarcenyAuto.dll indicates the game uses Godot’s C# scripting backend rather than GDScript — the actual gameplay and vault logic is compiled .NET IL, not bytecode baked into the .pck resource pack.

Rather than extracting and reverse-engineering the .pck (Godot’s resource pack format, which here would only yield scenes and assets, not logic), GrandLarcenyAuto.dll was decompiled directly with ilspycmd, because compiled C# IL reconstructs to near-original, readable source, while a .pck would require a dedicated Godot resource extractor for a payoff that doesn’t exist in this build.

ClassRolePurpose
PlayerStateDataHolds position, InCar, WantedStars, Cash — plain public-setter fields/properties
WantedSystemGame logicThe only in-game writer of WantedStars; escalates/cools it, hard-capped at 5
SafehouseVaultTargetGates a secret string behind WantedStars >= 6
CryptoUtilCryptoDerives an XOR key from WantedStars via SHA-256 and decrypts the vault’s sealed payload
CheatConsoleUnrelated decoyA hardcoded cheat-code string comparison (see Section 6)
GameControllerGodot glueWires the F key and the in-game cheat UI to the classes above

A baseline test against WantedSystem confirms the feature’s normal, unbroken state — the maximum reachable wanted level through the only legitimate write path, and the vault’s response at that maximum:

WantedStars after 10x EscalateHeat(1): 5
TryOpen() at capped stars: The vault stays shut. A note reads: "GRAND LARCENY AUTO VI - coming
soon, any day now."  (You need SIX stars. Good luck.)

During this same pass over the decompiled source, a second, unrelated hardcoded secret was noticed in CheatConsole — expanded on separately in Section 6, since it turned out to be a decoy unrelated to the vault objective.

2. Attack Flow

The check that gates the vault and the cryptographic key that protects its payload are not two independent controls layered on top of each other — they are two different readers of the exact same unvalidated, client-owned integer, which is why defeating one defeats both simultaneously.

Setting the single underlying value once satisfies the permission check and reproduces the correct decryption key in the same step, with no independent secret standing between the two.

3. The Vulnerability

3.1 Root Cause

Client-controlled state should never be treated as a security boundary — this is the same principle described by CWE-602 (Client-Side Enforcement of Server-Side Security), and it applies even with no network or server in the picture: any value that lives only in a process’s own memory, and is only ever written by that same process’s own (bypassable) code paths, is attacker-controlled by definition, because the attacker fully controls that process.

PlayerState.WantedStars is a plain int property with a public setter. WantedSystem.EscalateHeat is the only code path in the entire assembly that legitimately writes it, and it clamps the value to 5:

public void EscalateHeat(int amount)
{
    int num = player.WantedStars + amount;
    if (num > 5) num = 5;
    player.WantedStars = num;
}

SafehouseVault.TryOpen and CryptoUtil.DeriveKey both read that same property directly, with no check on where the value came from and no separate, trusted source of truth for it.

Normal play (capped at 5):

TryOpen() at capped stars: The vault stays shut. A note reads: "GRAND LARCENY AUTO VI - coming
soon, any day now."  (You need SIX stars. Good luck.)

Value set directly to 6 (outside WantedSystem entirely):

TryOpen() at stars=6 (client-side value set directly): VAULT UNSEALED
THM{*****************************}

Technical nuance: CryptoUtil does perform genuine SHA-256 hashing and a genuine byte-wise XOR stream cipher — this is not a case of an easily-spotted plaintext comparison. The cryptography is real; it simply protects nothing here, because its only key material is a value the same attacker already fully controls (see Section 3.2).

3.2 Why the Encryption Adds No Real Barrier

CryptoUtil.DeriveKey builds its key by hashing a hardcoded salt concatenated with WantedStars:

public static byte[] DeriveKey(int stars)
{
    string s = "GLA::vault::key::v1::stars=" + stars;
    return SHA256.HashData(Encoding.UTF8.GetBytes(s));
}

SHA-256 is a public, unkeyed hash function — hashing a known constant with an attacker-chosen small integer produces nothing an attacker can’t reproduce. WantedStars only ever ranges from 0–5 in legitimate play, so the entire effective keyspace is six values; brute-forcing it requires no cryptanalysis at all. Recomputing the key independently, outside the game entirely, confirms this byte-for-byte:

Game-derived key : BFC516EFF3769D44988EFCDAF0270CECC507B4AEF13AAE98D8CDAC0024BC55A0
Manual SHA256 key: BFC516EFF3769D44988EFCDAF0270CECC507B4AEF13AAE98D8CDAC0024BC55A0
Keys match: True

Technical nuance: the assembly is obfuscated with Confuser.Core 1.6.0 ([module: ConfusedBy("Confuser.Core 1.6.0+447341964f")] in AssemblyInfo.cs), which applies control-flow flattening (the switch/goto state machines seen in TryOpen and Xor) and string encryption (every literal replaced by a call into an LZMA-backed string-table decoder in <Module>). Both raise the cost of reading the code by eye but change nothing about what a decompiler reconstructs or what the CLR actually executes — the flattened control flow and the encrypted strings were recovered by asking the real, running code to decrypt itself (Section 4), not by manually reimplementing the obfuscator’s decoder.

3.3 How an Apparent Two-Factor Gate Collapses to One Trust Boundary

Reconstructed logic (de-flattened for clarity):

public string TryOpen()
{
    if (player.WantedStars >= 6)
    {
        byte[] key = CryptoUtil.DeriveKey(player.WantedStars);
        byte[] plain = CryptoUtil.Xor(SealedBlob, key);
        return "VAULT UNSEALED\n" + Encoding.UTF8.GetString(plain);
    }
    return "The vault stays shut...";
}

This reads, at a glance, like two independent controls: a permission check (if), and a genuinely separate cryptographic unlock behind it — the kind of design where bypassing the if (say, by patching the binary) would still leave the payload encrypted with an unknown key. In reality, both “layers” key off the identical player.WantedStars field, with no server round-trip, no signed value, and no piece of state derived from anything the player doesn’t fully control standing between them.

The vulnerable code — the unguarded setter behind PlayerState.WantedStars — and the affected code — the vault’s supposedly-independent cryptographic unlock — share no relationship beyond both trusting the same unvalidated in-memory integer.

4. Exploitation Walkthrough

Step 1 — Confirm the normal-play ceiling (baseline from Section 1): drive the only legitimate writer of WantedStars past its own cap and confirm the vault still refuses.

WantedStars after 10x EscalateHeat(1): 5
TryOpen() at capped stars: The vault stays shut. A note reads: "GRAND LARCENY AUTO VI - coming
soon, any day now."  (You need SIX stars. Good luck.)

Step 2 — Decompile the game assembly:

dotnet tool install -g ilspycmd --version 8.2.0.7535
ilspycmd -p -o ./decompiled "GrandLarcenyAuto.dll"
GrandLarcenyAuto/PlayerState.cs
GrandLarcenyAuto/WantedSystem.cs
GrandLarcenyAuto/SafehouseVault.cs
GrandLarcenyAuto/CryptoUtil.cs
GrandLarcenyAuto/CheatConsole.cs
GrandLarcenyAuto/GameController.cs

Step 3 — Read the reconstructed vault/crypto logic and confirm both the gate and the key derivation depend solely on PlayerState.WantedStars (full analysis in Section 3.2).

Step 4 — Build a minimal harness that loads the real assembly and drives it directly, rather than hand-simulating the obfuscator’s flattened control flow:

var ctx = new AssemblyLoadContext("gla");
Assembly asm = ctx.LoadFromAssemblyPath(dllPath);

Type playerStateType = asm.GetType("GrandLarcenyAuto.PlayerState")!;
Type vaultType       = asm.GetType("GrandLarcenyAuto.SafehouseVault")!;

object player = Activator.CreateInstance(playerStateType)!;
playerStateType.GetProperty("WantedStars")!.SetValue(player, 6);   // bypasses WantedSystem's cap

object vault = Activator.CreateInstance(vaultType, player)!;
object result = vaultType.GetMethod("TryOpen")!.Invoke(vault, null)!;

Step 5 — Invoke the real unlock logic with the gating value set directly:

{"result": "VAULT UNSEALED\nTHM{*****************************}

Step 6 — Confirm the result independently, rather than trusting the harness alone: recompute CryptoUtil‘s key by hand, outside the loaded assembly, and confirm it matches the game’s own derived key before trusting the decrypted text.

Manual SHA256 key: BFC516EFF3769D44988EFCDAF0270CECC507B4AEF13AAE98D8CDAC0024BC55A0
Keys match: True

The final step depended on one precondition established in Recon: WantedStars has exactly one legitimate writer (WantedSystem, capped at 5), and no code anywhere in the assembly validates that a “6” seen by SafehouseVault actually came from that writer.

5. Dead Ends

Payload/ApproachResult
Grinding wanted stars via normal gameplay (stealing cones/cars)Hard-capped at 5 by WantedSystem.EscalateHeat; the vault’s required 6 is unreachable through intended play
Extracting and reversing the .pck for GDScriptGame logic lives in the C# GodotSharp assembly, not in .pck-bundled GDScript; the .pck holds only scenes/assets
Loading GrandLarcenyAuto.dll under the machine’s installed .NET 6 SDKBadImageFormatException resolving System.Security.Cryptography — the assembly targets net8.0; resolved by side-installing the .NET 8 SDK
Manually reading SafehouseVault/CryptoUtil top-to-bottom by eyeConfuser.Core 1.6.0 control-flow flattening turns both methods into an opaque switch/goto state machine; error-prone to follow manually
Submitting the CheatConsole classic cheat codeReturns a real flag, but an unrelated decoy — not the vault objective (see Section 6)
Reflection harness invoking the real methods directly, for contrastWorks

No memory-scanner or process-attach tooling (e.g. Cheat Engine against the running .exe) was needed: PlayerState, WantedSystem, SafehouseVault, and CryptoUtil are plain, Godot-independent C# classes that instantiate cleanly outside the game engine, so driving them through a standalone .NET host was simpler and more reliable than live memory editing.

6. Secondary Finding: A Hardcoded Cheat-Code Flag in Plain Sight

CheatConsole, unlike every vault-related class, was left completely unobfuscated:

public class CheatConsole
{
    private const string ClassicCheat = "L0SV4NT0S247";

    public string Submit(string code)
    {
        if (code == "L0SV4NT0S247")
            return "THM{****************************}";
        return "Invalid code. (Try harder, tourist.)";
    }
}

This is a hardcoded-secret comparison in the same family as CWE-798 (Use of Hard-Coded Credentials): the “cheat code” and the flag it unlocks are both static literals sitting in cleartext in the decompiled source, recoverable with a plain strings/decompile pass and zero cryptographic or logical work — in sharp contrast to the vault’s obfuscated, SHA-256-gated flag. It aided the assessment only as a distraction: it is a genuine, working flag, but not the one gated behind the vault objective, and a careless pass over the challenge could easily mistake it for the intended answer.

7. Real-World Impact

  • Sony PlayStation 3 firmware-signing key recovery (2010, no CVE — disclosed at Console Hacking 2010 / fail0verflow): Sony’s ECDSA firmware-signing implementation reused a single static value instead of a fresh random nonce per signature; recovering that one shared constant let researchers derive Sony’s private signing key outright — the same pattern as this finding, where collapsing two supposedly independent security properties onto one shared, low-entropy value breaks both simultaneously.
  • Hikvision IP camera hardcoded/undocumented backdoor functionality (CVE-2021-36260): an unauthenticated command-injection path in the device’s web server, traced to undocumented, hardcoded debug functionality, allowed full device takeover — illustrating how a hardcoded shortcut left in shipped code (the same class as Section 6’s CheatConsole) can be far more severe when the surrounding system is network-exposed.
  • Client-authoritative economy/state manipulation in online games (widely reported, e.g. modded Grand Theft Auto Online lobbies): memory-edited or replayed client state used to mint in-game currency or stats is the same CWE-602 pattern generalized to a networked, multiplayer setting, where the consequence scales from a single-player secret to real economic impact across a shared game world.

Generalized, this finding functions as a confused-deputy/trust-boundary primitive: whenever a security-relevant decision and the material backing a supposedly independent second layer both derive from the same untrusted input, defeating that one input defeats both layers at once — regardless of how much code, obfuscation, or genuine cryptography sits on top of it.

8. Detection Guidance

  • Search decompiled or disassembled code for security-relevant reads of mutable local state. Grep for the property or field names that gate a reward or unlock, and check whether their setters are reachable from anywhere outside the intended flow. grep -rn "WantedStars\|>= 6\|TryOpen" decompiled/
  • Treat heavy obfuscation as a signpost pointing at logic worth extracting, not a stop sign. Confuser/ConfuserEx-style control-flow flattening and string encryption dramatically increase manual-reading effort but are fully transparent to a decompiler (ilspycmd, dotPeek, ILSpy) and to the CLR itself at runtime.
  • Flag any “key derivation” whose input overlaps with an already-attacker-controlled value. If the same integer that gates a feature also seeds a hash used as symmetric key material, the effective keyspace is exactly as small as that gating value’s valid range.
  • Load and drive suspect assemblies directly instead of hand-reversing flattened control flow. A small AssemblyLoadContextbased harness that instantiates the real types and calls the real methods reproduces exact runtime behavior — including any embedded string/LZMA decryption — without reimplementing the obfuscator’s decoder. dotnet run # harness sets the gating field directly and invokes the target method via reflection
  • Trace every gating field back to all of its writers, not just the intended one. If only one clamped writer exists in the whole assembly, any other write path — reflection, a save-file edit, a debugger — satisfies the exact same downstream check.
  • Run strings/decompilation across every class before concluding a target has only one secret. A quick pass here surfaced a second, unrelated hardcoded flag (Section 6) in a class the obfuscator never touched.

9. Remediation

Quick fix:

  • Route the unlock check through the same authority that owns the value. Have SafehouseVault ask WantedSystem — the single legitimate writer of WantedStars — for the value, or have WantedSystem own the unlock check itself, so exactly one code path can ever produce a qualifying value.
  • Stop deriving the decryption key from the same value the gate checks. Use a piece of state that is not attacker-influenced and not identical to the gating condition (e.g. a value only ever set by a genuine end-of-game event) as key material, so bypassing the if doesn’t also hand over the correct key.
  • Remove or equally protect the CheatConsole hardcoded flag. As shipped it hands out a working flag for a trivial string comparison, undermining the challenge’s intended difficulty curve for no design reason once found.

Defense in depth:

  • Do not rely on obfuscation as a security control. Confuser.Core-style control-flow flattening and string encryption raise analysis cost but are fully reversible with free, public tooling in minutes; budget them as a speed bump for casual users, never as the actual protection for a secret.
  • If a secret must never leave the client, do not ship it in the client at all. A locally-installed binary that must guard a genuine secret needs either server-side validation of the unlock condition or acceptance that a sufficiently motivated local user will always recover it eventually — obfuscation and clever key derivation only change how long that takes.
  • Note explicitly what integrity/anti-tamper checks would not have caught here. This finding involved no binary patching at all — the same public method was called with an input the checker never validated — so anti-tamper or code-signing checks alone would not have prevented it; they address a different threat model (modified code), not this one (unvalidated legitimate code called with attacker-chosen state).

10. Methodology Summary

  1. Listed the shipped Windows build’s files and noticed a full embedded .NET/GodotSharp runtime alongside a small GrandLarcenyAuto.dll, indicating the game’s logic is compiled C#, not GDScript inside the .pck.
  2. Installed ilspycmd as a dotnet global tool, pinning an older release compatible with the locally available .NET 6 SDK.
  3. Decompiled GrandLarcenyAuto.dll and located PlayerState, WantedSystem, SafehouseVault, and CryptoUtil, plus a suspicious, unobfuscated CheatConsole class.
  4. Confirmed, via WantedSystem.EscalateHeat, that normal gameplay hard-caps WantedStars at 5 — one below the vault’s stated requirement of 6 — establishing the baseline “sealed” behavior.
  5. Read through SafehouseVault.TryOpen and CryptoUtil, despite Confuser.Core control-flow-flattening obfuscation, and determined both the unlock gate and the XOR key derivation depend solely on the same PlayerState.WantedStars integer.
  6. Attempted to instantiate the assembly’s types directly under the machine’s default .NET 6 SDK; hit a BadImageFormatException because the assembly targets net8.0, and side-installed the .NET 8 SDK to resolve it.
  7. Built a minimal AssemblyLoadContextbased reflection harness that loads GrandLarcenyAuto.dll, sets PlayerState.WantedStars to 6 directly — bypassing WantedSystem‘s cap entirely — and invokes SafehouseVault.TryOpen().
  8. Captured the “VAULT UNSEALED” response containing the flag, then independently recomputed CryptoUtil‘s SHA-256-based key by hand and confirmed it matched the game’s own derived key byte-for-byte, confirming the result was genuine rather than a harness artifact.

11. Takeaways

  • A client-side value can never be a security boundary, even fully offline. In a single-player, no-network binary, “client-side” and “attacker-controlled” describe the same bytes; any check reading only local process memory is a check the local user fully owns.
  • Obfuscation buys time, not security. Control-flow flattening and string encryption made the source unreadable by eye but changed nothing about what a decompiler or the running CLR actually does with it; budget it as an inconvenience, never rely on it as a control.
  • Two “layers” that read the same input are one layer. A permission check and a cryptographic key derivation that both key off an identical, unvalidated value provide no more assurance than either alone — defeating the shared input defeats both simultaneously.
  • Driving the real, compiled code beats hand-deobfuscation. Rather than manually simulating a flattened switch/goto state machine, loading the real assembly and calling the real method reproduces exact behavior — including embedded string/LZMA decoders — with far less effort and far less risk of a transcription mistake.
  • Go check your own game or tool’s unlock-gate logic for this exact pattern today. Grep for every place a locally-stored or locally-computed value gates a “special” reward, and confirm that value has no legitimate writer capable of ever reaching the gating threshold — if it does, that gate is decorative.

About the author

İsmail Özdoğan

İsmail Özdoğan

Author

Table of Contents

Contents1. Recon2. Attack Flow3. The Vulnerability3.1 Root Cause3.2 Why the Encryption Adds No Real Barrier3.3 How an Apparent Two-Factor Gate Collapses to One Trust Boundary4. Exploitation Walkthrough5. Dead Ends6. Secondary Finding: A Hardcoded Cheat-Code Flag in Plain Sight7. Real-World Impact8. Detection Guidance9. Remediation10. Methodology Summary11. Takeaways

Subscribe to the newsletter

New cybersecurity articles, research and hardware-lab notes — straight to your inbox.

Related articles

July 26, 2026

WordPress REST API Batch Endpoint Exploit Chain (wp2shell): From Route Confusion to Remote Code Execution

Fools Mate: Revenge — Prototype Pollution to Global Authorization Bypass
July 12, 2026

Fools Mate: Revenge — Prototype Pollution to Global Authorization Bypass

Technically Identical, Legally Opposite: The Fine Line Between Penetration Testing and Cybercrime
February 17, 2026

Technically Identical, Legally Opposite: The Fine Line Between Penetration Testing and Cybercrime

All articles

Let's assess your organization's security posture together.

Start with a no-commitment consultation; we listen to your needs and build a roadmap tailored to you.

Request a consultation
CyberMap Group

A results-driven cybersecurity ecosystem with an adversary mindset: offensive testing, compliance consultancy, corporate training and R&D.

  • info@cybermapgroup.com
  • YDA Center — Kızılırmak Mah. Dumlupınar Bul. No: 9A, Çankaya / Ankara

Services

  • Penetration Testing
  • Hardware Threat Simulation
  • Security & Compliance
  • SPK VII-128.10 Compliance
  • Corporate Training

Products

  • ARQ
  • Corvox
  • Monorisk

Company

  • About
  • Team
  • Hardware Lab
  • Brand Assets
  • CyberMap Blog
  • Contact

Legal

  • Terms of Service
  • Privacy Policy
  • Cookie Policy
  • Disclaimer

© 2026 CyberMap Group

Developed by CyberMap Group.