When Game Values Are Encoded
Learn the difference between representation and security, trace small encode/decode paths, and implement a reversible value plus integrity check.
Cheatsheet for Lesson 14.9
A one-page summary of “When Game Values Are Encoded” you can keep beside your notes. Plain Markdown.
Lesson 14.9 — When Game Values Are Encoded: cheatsheet
- Remember: An encoded value looks different in memory, but the program must decode it to use it. So the decoding code is where the real value shows up.
- Also useful: Picture a message re-written each day with a new shift. The meaning stays the same and only the writing changes, so track the key and the meaning separately.
Key terms
- obfuscation: This is obfuscation: it makes a value less obvious.
Review
- Q: What property should a reversible toy value transform satisfy over many inputs? A: decode(encode(value, key), key) gives back value
- Q: What does obfuscation do to a game's stored health value? A: It changes the representation so the value is less obvious, yet running code can decode it
Obfuscation changes the representation
Section titled “Obfuscation changes the representation”A game does not have to store health as the obvious integer 125. It might XOR the value with a key, rotate the bits, split it across fields, or store both a value and a check value.
This is obfuscation: it makes a value less obvious. If the running program can decode it, an analyst can usually trace that decoding logic too. Obfuscation is not the same as encryption, and neither one automatically makes a program fair or secure.
Predict first
Health is 125, but the game stores 125 XOR 90. A memory scan for 125 finds nothing.
Which number does memory hold?
Answer: 39. 125 is 0111 1101 and 90 is 0101 1010. XOR puts a 1 only where the two bits differ, giving 0010 0111 = 32 + 4 + 2 + 1 = 39.
125 XOR 90 = 39
XOR: 1 where the bits differ
Stored: 39XOR with the same key
Decoded: 39 XOR 90 = 125Memory holds 39, so a scan for 125 finds nothing. XOR with 90 again gives back 125.
We use the book’s own toy type. Do not use this lesson to defeat protection in a third-party competitive game.
Start at where meaning appears
Section titled “Start at where meaning appears”Searching memory for the number 125 may fail because that representation never exists for long. A better question is:
Which instruction produces the value that the health bar displays or the damage calculation reads?
In a debug build, place a breakpoint near the rendering or gameplay function, then step backward through the data flow. A small decode might compile into operations like:
mov eax, [rcx+28h] ; read encoded fieldror eax, 7 ; undo a left rotationxor eax, edx ; remove the keyFollow edx too. A transform without its key is only half the formula.
Write the formula in both directions
Section titled “Write the formula in both directions”The lab uses:
The inverse relationship is easy to test:
const ROTATION: u32 = 7;
fn encode(value: u32, key: u32) -> u32 { (value ^ key).rotate_left(ROTATION)}
fn decode(encoded: u32, key: u32) -> u32 { encoded.rotate_right(ROTATION) ^ key}
for value in [0, 1, 100, u32::MAX] { assert_eq!(decode(encode(value, 0xA1B2_C3D4), 0xA1B2_C3D4), value);}That last property is the important one: decoding the encoded value must recover the starting value for many inputs, not only one lucky example.
Follow the loop’s 100 case with its key 0xA1B2_C3D4. The changing numbers
below are all 32-bit values; the final result must return to the starting one:
Undo representation changes in reverse order. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
value = selected_value
mixed = value XOR key
stored = rotate_left(mixed, rotation)
read the same stored bits
unmixed = rotate_right(stored, rotation)
decoded = unmixed XOR keyResult: decoded 100, matching the original 100. Reversibility does not establish integrity.
Final values: value = 100; rotation = 7; key = 0xA1B2C3D4; mixed = 0xA1B2C3B0; stored = 0xD961D850; bytes = 50 D8 61 D9; unmixed = 0xA1B2C3B0; decoded = 100.
Read every step of the lesson’s example
Step 1, line 1. Start with the selected inputs and follow how the next operation changes the state.
value = 100; rotation = 7.
Step 2, line 2. XOR flips exactly the bit positions selected by the key.
value = 100; rotation = 7; key = 0xA1B2C3D4; mixed = 0xA1B2C3B0.
Step 3, line 3. Rotation carries bits across the word boundary without discarding them.
value = 100; rotation = 7; key = 0xA1B2C3D4; mixed = 0xA1B2C3B0; stored = 0xD961D850; bytes = 50 D8 61 D9.
Step 4, line 4. The bytes list is little endian: the lowest byte comes first.
value = 100; rotation = 7; key = 0xA1B2C3D4; mixed = 0xA1B2C3B0; stored = 0xD961D850; bytes = 50 D8 61 D9.
Step 5, line 5. Rotate the stored word back by the same distance.
value = 100; rotation = 7; key = 0xA1B2C3D4; mixed = 0xA1B2C3B0; stored = 0xD961D850; bytes = 50 D8 61 D9; unmixed = 0xA1B2C3B0.
Step 6, line 6. Result: decoded 100, matching the original 100. Reversibility does not establish integrity.
value = 100; rotation = 7; key = 0xA1B2C3D4; mixed = 0xA1B2C3B0; stored = 0xD961D850; bytes = 50 D8 61 D9; unmixed = 0xA1B2C3B0; decoded = 100.
A row of 32 bits starts as the value 100. XOR with the key 0xA1B2C3D4 flips the bits above the key’s ones, giving 0xA1B2C3B0. Rotating left by seven carries the first seven bits round to the end, giving 0xD961D850. Rotating right by seven and XORing with the key again return the bits to 100.
Read the steps
- The value 100 is 0x00000064 as 32 bits. The key, 0xA1B2C3D4, sits below it. encode will XOR with the key and then rotate; decode has to undo those two steps in the opposite order.
- XOR with the key: wherever the key has a 1, the bit above it flips. 100 XOR 0xA1B2C3D4 is 0xA1B2C3B0.
- Rotate left by seven: the first seven bits, 1010000, are carried round to the other end. The stored value is 0xD961D850, which looks nothing like 100.
- To decode, undo the rotation first: rotate right by seven, and the same seven bits go back round. That recovers 0xA1B2C3B0.
- Then XOR with the same key again. The same bits flip back, and the result is 0x00000064: the original 100.
Encoding applies XOR then rotation; decoding reverses the order. This reversible toy representation is not a security guarantee.
An integrity value is another clue
Section titled “An integrity value is another clue”The lab also stores a toy tag calculated from the encoded value and key. When one bit changes, the tag no longer agrees:
The code makes the two exits explicit. It recomputes the tag from the stored
encoded value and the supplied key; a mismatch returns IntegrityError
without decoding. A match permits the inverse transformation and returns the
logical value in Ok.
pub fn read(self, key: u32) -> Result<u32, IntegrityError> { if self.tag != make_tag(self.encoded, key) { return Err(IntegrityError); }
Ok(decode(self.encoded, key))}This tag is educational, not cryptographically secure. An attacker who understands make_tag can calculate a matching tag. The useful lesson is structural: if a changed value is immediately rejected, look for another field or function that validates it.
Dynamic keys change over time
Section titled “Dynamic keys change over time”Some programs change a key when a level loads, an object is created, or a value is written. Reading the same field at several moments shows which events change the key:
| Moment | Logical value | Encoded bytes | Suspected key source |
|---|---|---|---|
| spawn | 125 | E9 88 … |
constructor argument |
| damage | 100 | 75 84 … |
same object field |
| reload | 125 | different | new session state |
Do not assume a value has “random encryption” merely because its bytes change. The cause may be an ordinary per-object key, a pointer-relative cookie, a checksum, or unrelated neighboring data.
Run the complete lab
Section titled “Run the complete lab”The full implementation lives in advanced-memory-labs/src/obfuscation.rs, with a small executable in src/bin/obfuscation_lab.rs:
cargo run --manifest-path advanced-memory-labs/Cargo.toml --bin obfuscation_labcargo test --manifest-path advanced-memory-labs/Cargo.toml obfuscationThe demo prints a transformed value, decodes it, flips one bit, and shows the integrity failure. Its tests prove both the round trip and the failure case.
The analysis, step by step
Section titled “The analysis, step by step”- Start from the exact build and one object address.
- Trigger one controlled change in your own target.
- Capture the encoded field before and after.
- Find the code that reads the field for a meaningful decision.
- Trace every operand feeding the decoded result.
- Express the operations as a small pure function.
- Test the inverse over many inputs.
- Look for validation or duplicate fields.
What not to conclude
Section titled “What not to conclude”- ❌ “XOR means this is secure encryption.”
- ✅ XOR can be one reversible step; security depends on a complete, reviewed construction and key handling.
- ❌ “The value changed, so my offset is wrong.”
- ✅ The representation or key may have changed.
- ❌ “One successful decode proves the formula.”
- ✅ Test several values, sessions, and objects.
- ❌ “An integrity field is impossible to reproduce.”
- ✅ First determine whether it is a checksum, a keyed MAC (message authentication code, which needs a secret key), or an authenticated-encryption tag.
The next lesson returns to game text and its byte encoding. For the deeper distinction between checksums, signatures, encryption, and authenticated formats, continue to Chapter 9’s integrity and protection lessons.
Test the transformation
What property should a reversible toy value transform satisfy over many inputs?
A round-trip property checks that the proposed inverse really undoes the complete transform rather than fitting one coincidence.
Finished Lesson 14.9?
Mark it done and the lesson list shows a beside it. A chapter whose lessons are all done is marked done too. Progress is saved in this browser only.
Plain Markdown, saved in this browser.
This book grew out of the original Game Hacking Academy by attilathedud and has been rewritten as a beginner-first course. Like the original repository, its source is published under the Apache License 2.0.