No way out picoCTF 2023 Solution

Published: April 26, 2023

Description

A Unity game walls you in with invisible boundaries. The flag is readable with strings, or you can use dnSpy to modify the game assembly and unlock infinite jumping to fly over the wall.

Download and unzip the game.

bash
wget https://artifacts.picoctf.net/c/504/no-way-out.zip && unzip no-way-out.zip

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    Fast path: strings on the level data
    Observation
    I noticed the game is a Unity build with a *_Data/ folder containing binary level files, which suggested that scene text objects like a displayed flag would be stored as plain strings in level0 and could be extracted without ever running the game.
    Unity games store scene data including text objects in the level data files. Running strings on the level0 file reveals the flag directly without running the game at all.
    bash
    strings 'no-way-out_Data/level0' | grep -i 'WELCOME\|picoCTF'
    bash
    strings 'no-way-out_Data/level0'

    Expected output

    picoCTF{WELCOME_TO_UNITY!!}
    What didn't work first

    Tried: Running strings on the main game executable instead of the level data file.

    The executable itself (no-way-out.exe or the Linux binary) contains engine code and startup logic but not scene assets. Scene text objects are serialized into the level0 binary inside no-way-out_Data/, so strings on the wrong file produces thousands of engine symbol names with no flag present.

    Tried: Using grep -i 'picoctf' without also grepping for 'WELCOME' or checking the raw strings output.

    The flag text 'WELCOME_TO_UNITY!!' sits right above the picoCTF wrapper in the file, but a grep pattern that only matches the curly-brace form might miss it if the encoding splits the prefix. Running strings with no filter first to inspect the surrounding lines confirms the full flag string before narrowing the pattern.

    Learn more

    Unity stores scene data for each level in a binary file called level0, level1, etc. inside the *_Data/ folder. Text objects placed in the scene (like 3D text meshes showing the flag) are stored as plain UTF-16 or UTF-8 strings in this file. The strings command extracts any printable run of characters, which surfaces the flag text without running the game.

    This is the fastest CTF approach: run strings on every data file in the game bundle before spending time on dynamic analysis. Unity games are particularly friendly to this because scene assets are not encrypted by default.

  2. Step 2
    Alternative: patch the game with dnSpy
    Observation
    I noticed the game uses Mono/IL2CPP and ships Assembly-CSharp.dll in the Managed/ folder, which suggested the C# player controller logic (including the grounded jump restriction) could be decompiled and patched with dnSpy to enable infinite mid-air jumping over the invisible wall.
    For the more interactive approach, open the game assembly in dnSpy and remove the grounded check from the jump logic so you can jump infinitely and fly over the wall.
    bash
    # Download dnSpy from https://github.com/dnSpy/dnSpy/releases
    bash
    # Open no-way-out_Data/Managed/Assembly-CSharp.dll in dnSpy
    bash
    # Navigate to PlayerController -> Update method
    bash
    # Remove the isGrounded condition from the jump check
    bash
    # File -> Save Module, then run the game
    What didn't work first

    Tried: Opening the game executable directly in dnSpy instead of Assembly-CSharp.dll.

    dnSpy is a .NET tool and cannot parse the native Unity game launcher. The actual C# game logic is compiled into Assembly-CSharp.dll inside no-way-out_Data/Managed/. Opening the wrong file shows either an error or an empty assembly tree with no PlayerController class to edit.

    Tried: Editing the jump height or speed values in dnSpy rather than removing the isGrounded condition.

    Increasing jump height just lets you jump higher from the ground - the invisible wall is tall enough that no finite jump height clears it. The correct fix is to remove the isGrounded boolean check entirely, which enables repeated mid-air jumps and lets you fly up indefinitely until you clear the boundary.

    Learn more

    dnSpy is a .NET assembly editor and debugger. Unity games compile C# scripts to Assembly-CSharp.dll in the Managed/ folder. dnSpy decompiles this DLL back to readable C# and lets you edit the code directly, then save the modified assembly.

    The jump logic in this game checks if (Input.GetButton("Jump") && canMove && isGrounded && !isClimbing). Removing the isGrounded condition lets you jump while airborne, effectively giving infinite flight. After patching, jump over the invisible wall, land outside, turn around, and read the flag displayed as a 3D text object in the enclosed area.

    The flag reads: WELCOME TO UNITY!!

Interactive tools
  • Strings ExtractorPull printable text from any binary, library, or image. ASCII and UTF-16 detection, configurable minimum length, flag-like highlight, no command line needed.
  • Hex ViewerView text or raw hex bytes as a xxd-style hex dump with byte offset, hex columns, and ASCII sidebar. Highlights printable characters and null bytes.

Flag

Reveal flag

picoCTF{WELCOME_TO_UNITY!!}

The simplest approach is strings on the level0 data file. The dnSpy patch is more fun but takes longer.

Key takeaway

Unity games built with IL2CPP or Mono compile game logic to .NET assemblies that are not stripped of metadata, making them trivially decompilable by tools like dnSpy and ILSpy. Client-side security controls (invisible walls, locked doors, hidden flags in scene data) are always defeatable because the attacker controls the runtime, and the same principle applies to any game with client-enforced restrictions, DRM, or license checks. The real-world analogy is any application that trusts client-side validation: the fix is always to move enforcement to the server.

Related reading

Want more picoCTF 2023 writeups?

Useful tools for Reverse Engineering

What to try next