Wolfenstein Modloader Buy me a coffeeKaffee spendieren

Wiki / Game systemsSpielsysteme

Save Games and Profiles for Both Wolfenstein Titles

Partly solvedTeilweise gelöst Original page in the GitHub wikiOriginalseite im GitHub-Wiki

In plain wordsIn einfachen Worten

This page looks at where Wolfenstein: The New Order and The New Colossus keep their save games and how they guard them against changes. The 4-byte checksum at the start of each save file is solved: it is a folded MD5 hash, and 82 of 82 files match. The New Order encrypts nothing, so its saves are open for editing. The New Colossus adds encrypted profile files and 40-byte .verify files that cannot be recomputed without a key, but the game text suggests a mismatch makes it load the save in Developer Mode instead of rejecting it.

  • The 4-byte save header is MD5 folded to 32 bits; 82 of 82 files match.
  • The New Order uses no encryption, so its save games can be edited and re-signed.
  • The New Colossus encrypts profile.bin and progression.bin with AES; the key is unknown.
  • Identical save files get different .verify files, so a random value is mixed in.
  • The checksum= value in game.details is still unsolved.

Diese Seite untersucht, wo Wolfenstein: The New Order und The New Colossus ihre Spielstände ablegen und wie sie sie vor Änderungen schützen. Die 4-Byte-Prüfsumme am Anfang jeder Spielstanddatei ist gelöst: ein gefalteter MD5-Hash, und 82 von 82 Dateien passen. The New Order verschlüsselt nichts, seine Spielstände sind also frei bearbeitbar. The New Colossus hat zusätzlich verschlüsselte Profildateien und 40-Byte-.verify-Dateien, die ohne Schlüssel nicht nachzurechnen sind; laut Spieltext lädt es einen abweichenden Spielstand aber im Entwicklermodus, statt ihn abzulehnen.

  • Der 4-Byte-Kopf der Spielstände ist ein auf 32 Bit gefalteter MD5-Wert; 82 von 82 Dateien passen.
  • The New Order nutzt keine Verschlüsselung; seine Spielstände lassen sich bearbeiten und neu signieren.
  • The New Colossus verschlüsselt profile.bin und progression.bin mit AES; der Schlüssel ist unbekannt.
  • Identische Spielstanddateien bekommen verschiedene .verify-Dateien; es fließt also ein Zufallswert ein.
  • Der Wert checksum= in game.details ist noch ungelöst.

Technical detailsTechnische Details

Der technische Teil ist auf Englisch – so, wie er im Wiki steht. Unterstrichene Begriffe haben eine Erklärung auf Deutsch.

Measured using tools/probe_savegame.py (read-only) against existing save games plus string analysis of both game executables. Everything claimed here is measured; where something remained open, it is explicitly noted.

Summary:

The New Order The New Colossus
Storage Location base/savegame/ base/savegame.user/<SteamID64>/SLOT0..2/
Save Game Data Plaintext binary, unencrypted Plaintext binary, unencrypted
Profile/Progression profile.bin, unencrypted profile.bin, progression.bin, AES-encrypted
Integrity Protection 4-byte header + checksum= in game.details Additionally one 40-byte .verify per file
Manipulation Detected? Checksum must match .verify is tamper-proof, but blocks loading only if present

1. Directories

The New Order

C:\Users\<user>\Saved Games\MachineGames\Wolfenstein The New Order\base\
  wolfConfig.cfg                     Plaintext, Cvars
  savegame\profile.bin               3950 B, Binary structure (H=4,08; zlib 30 %)
  savegame\GAME-AUTOSAVE\
    checkpoint0.dat                  26572 B
    mapstart0.dat                    3002 B
    game.details                     143 B, Plaintext

No .verify files, no progression.bin. TNO does not protect save games cryptographically — no cryptographic symbols appear in the Exe at all (Blowfish, AES, SHA, HMAC, BCrypt: zero hits).

The New Colossus

base\savegame.user\<SteamID>\
  profile.bin        3976 B    encrypted      + no .verify
  profile2.bin         72 B    encrypted      + profile2.bin.verify
  migrated.bin         72 B    encrypted
  SLOT0..SLOT2\
    PROFILE\progression.bin   136472 B  encrypted  + .verify
    PROFILE\progression2.bin    9800 B  encrypted  + .verify
    GAME-AUTOSAVE0..2, GAME-HELM, GAME-TRANSITION, FC0-AUTOSAVE0..1\
      checkpoint.dat           up to 564 kB   Plaintext binary  + .verify
      checkpoint_mapstart.dat  ~700 B                       + .verify
      checkpoint_alt.dat         4 B (empty)                 + .verify
      game.details             ~400 B  Plaintext             + .verify
      game_duration.dat        ~9,5 kB                      + .verify
      level_duration.dat        167 B                       + .verify
      level_duration_alt.dat     4 B (empty)                 + .verify
    WOLFORG-SAVEDATA\HIGHSCOR.WL6   508 B  (Wolf3D-Highscores)  + .verify

Total of 99 .verify files, all exactly 40 bytes.


2. game.details — Plaintext

Both games write a pure key=value text file:

checksum=213035449
completionRation=50%
date=1764620045
difficulty=2
gamePlayTime=2265
gameVersion=1145896965
mapName=game/wolf/submarine/submarine
...

gameVersion is a tag constant, not a date:

  • TNO 1145896962 = 0x44490002 = "DI" + Version 2
  • TNC 1145896965 = 0x44490005 = "DI" + Version 5

The same pattern is embedded in the header of .dat files (see below) as 0x444D0005 = "DM" + Version 5.

checksum is a signed 32-bit value and not CRC-32 or Adler-32 over the rest of the file, nor over mapName (verified across all 13 files, no hits). It changes per save game even with the same map, depending on the save game content.


3. .dat Files — 4-Byte Header plus Plaintext Structure

All .dat files from both games begin with 4 bytes, followed by the actual structure:

checkpoint.dat         fd a0 5e f3 | 05 00 4d 44 | 26 00 00 00 | 01 00 | 22 00 00 00 "2018…"
game_duration.dat      7e d1 7e ca | 44 4d 00 05 | 00 00 00 26 | 01    | 1f 00 00 00 "idGameL…"
checkpoint_mapstart.dat 2d c2 d7 7d | 00 60 ff cd | 00 00 00 03 | …
checkpoint_alt.dat     3b 75 65 5e   (header only, body empty)

Notable: checkpoint.dat writes the inner header little-endian (05 00 4d 44 = 0x444D0005), while *_duration.dat writes the same value big-endian (44 4d 00 05). The subsequent strings are length-prefixed with LE lengths in both cases. Thus, two different serializers are used.

The 4-Byte Header — Solved: idLibs MD5_BlockChecksum, big-endian

def savehash(rumpf):
    w = struct.unpack("<4I", hashlib.md5(rumpf).digest())
    return struct.pack(">I", w[0] ^ w[1] ^ w[2] ^ w[3])

In other words: MD5 over the payload (everything from byte 4), reading the four 32-bit words of the digest little-endian, XORing them, and writing the result big-endian to the file. This is literally idLibs MD5_BlockChecksum — the same folding function id Software has used since Quake 3 for block checksums. Only the storage format is big-endian, while everything else in these files is little-endian; it was precisely this that caused the previous search to fail.

The known empty value explains itself naturally: MD5("") is d41d8cd9 8f00b204 e9800998 ecf8427e; folding the four LE words yields 0x3B75655E, which when written big-endian becomes the bytes 3b 75 65 5e — interpreted as a u32 little-endian, this gives the frequently cited 0x5E65753B.

Verified: python tools/savehash.py pruefen recalculates the header for every savegame file on this machine.

82 of 82 files match header, 0 deviations.

This covers both titles combined: 80 .dat files (The New Order and The New Colossus), profile.bin from The New Order, and HIGHSCOR.WL6. Payload lengths range from 0 to 1,165,441 bytes; 45 of them are pairwise distinct (44 non-empty). Neither game.details (pure text, starting with checksum=) nor the AES files from The New Colossus have such a header (the first 8 bytes there are the Nonce) — these are not included in the count and correctly fail.

Regarding robustness: the formula was guessed from one data point (the empty value) and subsequently correctly predicts 44 further, mutually independent 32-bit values. Even generously calculated — say, a total of 2^40 formula variants were tried — the probability that one of them matches all 44 by chance is 2^40 / 2^(32·44) = 2^-1368. The counter-check stands as an assert in tools/savehash.py --selbsttest: flipping a single bit results in a different header, one that none of the existing savegames possess.

What the Header is Provably Not

The path to this solution cleared an entire search space, which remains valuable even if the answer is already known. From the savegames themselves, this can be measured without starting the game: there are ten pairs of equal-length payloads with different content (lengths 402, 450, 735, 5829, 9465, 9717). For equal lengths, the Init and XorOut values of a CRC emerge from the difference, leaving

kopf₁ ⊕ kopf₂  ≡  (rumpf₁ ⊕ rumpf₂)(x) · x³²   (mod P)

This means: the sought polynomial P divides the known number D(x) = (rumpf₁⊕rumpf₂)(x)·x³² + (kopf₁⊕kopf₂)(x) over GF(2). The greatest common divisor of all ten Ds must therefore contain P.

GCD = 1. Degree 0, no common factor of any degree.

Thus, the header is not a CRC-32 — for no polynomial, no start value, no XorOut, and neither of the two bit reflections. The same conclusion holds separately by file type (checkpoint_mapstart.dat, game_duration.dat — which use provably different serializers) and with 16 variants of the data stream (Byte-, 16-, 32-, 64-bit word order × Reflection × Header LE/BE) plus appended secrets of every length 0…512: everywhere degree < 32. The solver is self-tested; it finds 0x04C11DB7 on synthetic data in both normal and reflected forms.

Additionally rejected (Seed calculated from zero, checked against all 42 non-empty hulls, 6 frames each — 0 hits): xxHash32, MurmurHash3 x86_32, MurmurHash2, Jenkins lookup3/hashlittle, Jenkins one-at-a-time, FNV-1, FNV-1a, djb2, djb2-xor, sdbm, SuperFastHash, 32-bit word product, and word rotation XOR.

The CRC tables in the Exes were cleanly extracted from .rdata/.data and recalculated against their polynomials (.text is SteamStub-encrypted in both Exes and remains unreadable):

Exe Address Polynomial
TNO 0x141079F50 (.rdata), 0x1415BC670 (.data) 0xEDB88320 CRC-32
TNO 0x14107A750 (.rdata) 0xE1351B80
TNC 0x14209B400 (.rdata) 0x82F63B78 CRC-32C
TNC 0x1425C9140, 0x1425D0040 (.rdata), 0x142EE0FD0 (.data) 0xEDB88320
TNC 0x1425D0840 (.rdata) 0xE1351B80

So the tables do exist — they are simply not used for the game state header. superscriptx64.dll (TNO, unencrypted .text) contains no CRC table at all.

The Header Depends Only on Content

80 .dat files, 43 different hulls. No hull carries two different headers, and no header belongs to two different hulls. The eleven content-identical level_duration.dat files are located in eleven different folders and carry the same header — the path does not matter, nor does the filename (checkpoint_alt.dat and level_duration_alt.dat are both empty and both carry 0x5E65753B).

A lucky find in the inventory is the pair SLOT0/GAME-AUTOSAVE1/game_duration.dat against SLOT0/GAME-TRANSITION/game_duration.dat: 9465-byte hulls that differ by exactly one byte (Offset 3900, 0x99 vs 0x00), with a header difference of 0x6ACD75CB. This is precisely the "change one byte and observe" experiment without needing to touch a save file.

Content of checkpoint.dat (TNC)

Plain text, easy to read. At the start of the file is a string table with LE-u32 length prefixes, among which:

ammo/player/sp/handgun      weapon/player/sp/handgun
ammo/player/sp/shotgun      weapon/player/sp/shotgun
ammo/player/sp/lasergewehr  ammo/player/sp/ubergewehr_laser
player/perkitem             unlocks/upgradekit          abilities/healthy

Important for expectations: This is the name table, not the values. Ammo counts and upgrade flags are located in the binary hull and are addressed via an index into this table. An ammo value cannot therefore be directly changed by searching for ammo/player/sp/handgun next to it; one must first disassemble the component blocks (idGameLocal_CheckpointInfo:10, idGameMapMemoryManager:20/30, …). This is feasible, but constitutes real format work.


4. The .verify Files — Verified, Not Guessed

The hypothesis "40 bytes = SHA-1 (20) plus remainder" was tested byte-for-byte: for all 99 .verify files, MD5, SHA-1, SHA-256, CRC-32 (LE and BE) were computed over

  • the entire source file
  • the source file without its 4-byte header
  • filename‖content and content‖filename
  • length(u32)‖content

and each of these values was compared against the first n bytes, the last n bytes, both byte-swapped, and as a substring anywhere within the .verify.

Total hits: 0.

More significant is a second finding:

28 byte-identical source files result in 28 different .verify files. (Similarly, 11 identical level_duration.dat → 11 different .verify.)

Same content, different checksum — this rules out every pure hash function and every deterministic encryption. A random value per write operation is embedded within it. This fits: across all 99 files, not a single 16-byte block repeats, and the entropy reaches 100% of the achievable 40 bytes.

What the Exe Says About It

NewColossus_x64vk.exe contains a complete crypto layer over Windows CNG (BCrypt):

idSafeBCryptAlgorithmHandle: BCryptOpenAlgorithmProvider failed: %x idSecureHash::SetFunction: Invalid hashFunction value : %d idSecureHMAC::Initialize: Invalid hmacFunction value : %d idSecureBlockCipher::EncryptWithOSPadding: Not initialized idSecureAEADCipher::Encrypt: nonce must be exactly 12 bytes (bcrypt restriction) idSecureAEADCipher: BCryptSetProperty( BCRYPT_CHAINING_MODE ) failed: %x

UTF-16 algorithm names listed alongside are SHA1, SHA256, SHA384, SHA512, MD5, AES, ChainingModeGCM, and AuthTagLength. File naming patterns include %s.verify, %s.temp.verify, and *.verify.

The sizes confirm the pattern. Every encrypted file is ≡ 8 (mod 16):

File Size −8 in 16-Byte Blocks
*.verify 40 32 2
profile2.bin, migrated.bin 72 64 4
profile.bin 3976 3968 248
progression2.bin 9800 9792 612
progression.bin 136472 136464 8529

Thus: 8-byte header (random salt/nonce) + AES block cipher. For .verify files, this is exactly 2 AES blocks, fitting for a SHA-1 value (20 bytes) or MD5 (16 bytes) with padding. The key resides in the executable; without it, a .verify cannot be recalculated.

And: TNO does not have this

The profile.bin from The New Order is not encrypted — entropy 4.08 bits/byte, zlib compresses it to 30%, with 129 repeated 8-byte blocks and per-row zeros. A normal binary structure.


5. What This Means for Cheats

The most important finding lies not in the files, but in the executable:

02686858  Skip entering developer mode when loading corrupted saves
02686898  devMode_skipCorruptedSaves
02686ed0  Loading Dev Mode or corrupted save game.
026e0b98  SAVEGAME_E_CORRUPTED
02981748  devMode_enable
02981810  devMode_fatalErrorOnEnter
026c12b8  +devMode_
02794d58  DEVMODE_DEVELOPER   /  02794df0  DEVMODE_PRODUCTION

A save game whose .verify does not match is not rejected by The New Colossus, but instead loaded as "corrupted" — and the game enters Developer Mode in the process. The .verify is therefore not a loading barrier, but an Achievement/Dev-Mode flag.

This flips the assessment:

  1. The .verify is not a lock. A modified checkpoint.dat will be loaded. The question is only whether the 4-byte header of the file itself matches — and this is independent of the .verify and calculable since this measurement (tools/savehash.py set).
  2. The actual lever is +devMode_. That the executable recognizes a command-line prefix +devMode_ and exposes devMode_enable as a CVar is more interesting for the CVar lock than any save game hack. This should be pursued as its own thread (see docs/limits.md).
  3. Profile/Progression remain locked. progression.bin and profile.bin (TNC) are AES-encrypted; permanent unlocks cannot be set there without the key.
  4. For The New Order, nothing is encrypted, and the 4-byte header is calculable. Thus, the save game there is open: modify the body, run tools/savehash.py set, and you are done. The work shifts from cracking the checksum to deconstructing the component format within the body.

6. Open Points

  • Calculation method of the checksum= field in game.details. It is not an MD5 fold — tested against all 14 game.details entries against the text without the checksum= line, against mapName, against every sibling file (whole, body only, header only), and against the concatenation of all sibling headers, each folded, byte-rotated, and truncated to 4 bytes: 0 hits.
  • Whether TNO also only warns or rejects the save game on checksum error — untested, as this would require starting the game.
  • AES key and exact payload of the .verify (SHA-1 or MD5).

7. Tools

python tools/probe_savegame.py            # both titles
python tools/probe_savegame.py tnc        # The New Colossus only

python tools/savehash.py pruefen          # recalculate header of each file
python tools/savehash.py kopf <datei>     # expected/actual header of a file
python tools/savehash.py setzen <ein> <aus>   # recalculate header, write to NEW file
python tools/savehash.py --selbsttest

probe_savegame.py opens exclusively for reading and writes nothing. savehash.py also reads only; it writes solely to setzen, always to a new path — the target path must not be in the save game folder nor in the Steam library, otherwise it will abort. A corrupted save game costs real game progress.