Profile
Back to NewsBack
GitHub Trending 3 min
Reader Mode
Gezine/CivSwitch: Nintendo Switch userland code execution using Civilization 7 game cartridge

Gezine/CivSwitch: Nintendo Switch userland code execution using Civilization 7 game cartridge

7 hours ago

CivSwitch

XSS toolchain for the Switch 1 edition of Civilization 7. Only the 1.0.0 version of the game in physical cartridge is supported.

Every switch 1/2 firmware 20.x.x and up to current version (23.0.1) should work.

Tested at Switch 1 fw 22.5.0, Switch 2 fw 20.4.0 and 23.0.1

Two components:

  • PicoSwitch — clean, compilable Pico firmware (RP2040 + RP2350) that types a chat-XSS payload into the Switch (USB HID macro).
Default payload = the Remote JS Loader bootstrap (exploit core + TCP loader).

Ships with compress.js (chat transport: terser -> deflateRaw -> base-25 chunked img/onerror lines with censor armor).

  • installer-creator — given any JavaScript payload, produces the boot-persistent installer (XSTRIG.Civ7Save trigger written to savedata) to send through the Remote JS Loader.

Layout

core.js at the root is the shared payload both components build from, and send_js.py at the root sends any payload to a running loader.

Workflow

Video Tutorial from switch2hacks reddit

  1. Build + flash the loader UF2 (see PicoSwitch/README.md).
On the Switch, go to "System" menu and change system language to English(US). Go to "Controllers & Accessories" menu and change USB Keyboard to English(US). These are required to type "English" letters (code) using USB keyboard. (Higher fw might not need to change system language as they support typing english without language change) Connect to Internet using 90DNS to block Nintendo servers. Then launch game and navigate main menu -> MULTIPLAYER -> WIRELESS -> CREATE GAME -> HOST LOBBY -> CHAT then plug the Pico in (in docked mode, focus the chat box first to receive Pico input).

The Pico types the chat XSS (~87 lines, ~15 min); the Remote JS Loader comes up on the console (TCP :9026) for that session.

  1. Install the persistent boot trigger from the PC: python installer-creator/install.py — uploads the save template, splices the compressed payload into it on-console, wipes save:/saves/Single/, and writes the trigger.
  2. From then on, every boot: main menu -> Load Game -> the payload runs from savedata (and if the payload is the loader, :9026 is up at boot — and survives console sleep/wake).
  3. You can revert system language after installer finished.

Sending payloads (send_js.py)

python send_js.py <console-ip> [port] file.js

Sends a JS file as-is to the Remote JS Loader (default port 9026), then prints the log output the console streams back (read until close or timeout).

Build (Pico firmware)

On Ubuntu 24.04:

sudo apt install -y git build-essential cmake ninja-build gcc-arm-none-eabi libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib python3 nodejs npm
git clone --branch 2.3.1 --depth 1 --recurse-submodules --shallow-submodules https://github.com/raspberrypi/pico-sdk.git ~/pico-sdk
export PICO_SDK_PATH=$HOME/pico-sdk
cd PicoSwitch
npm install                    # terser for compress.js
node compress.js               # reads ../core.js -> payload.txt
cmake --preset pico2 && cmake --build --preset pico2    # RP2350 -> build-pico2/src/civ7-pico2.uf2
cmake --preset pico1 && cmake --build --preset pico1    # RP2040 -> build-pico1/src/civ7-pico1.uf2

Credits

Chat with me