Skip to content
Lesson 14.10Versioned Evidence, State, and Integrity

Obfuscated Values and Reversible Transforms

Reversible transforms can hide a value in memory without providing encryption; round trips and controlled changes reveal the rule.

Lesson 14.10
Read or listenPrint chapter or book
About 55 minutes

Keyboard shortcuts

?
Open this guide
Alt + B
Show or hide the lesson list
Alt + O
Show or fold the lesson outline
Alt + N
Open or close notes
Alt + H
Toggle semantic colours
Alt + D
Toggle page brightness
Alt + C
Toggle code brightness
← →
Move through a focused code trace
Home End
First or final step in a focused trace
Escape
Close this guide or an open panel

Reader shortcuts pause while you type in a text field.

Cheatsheet for Lesson 14.10Key ideas, reference and questions

A one-page summary of “Obfuscated Values and Reversible Transforms” you can keep beside your notes. Plain Markdown.

Lesson 14.10 — Obfuscated Values and Reversible Transforms: cheatsheet

  • Remember: The order matters. Undo the rotation first, then XOR with the same key. That restores the original bits.
  • Also useful: Use authenticated encryption when secrecy and tamper detection are required. A reversible representation alone supplies neither guarantee.

Key terms

  • value: The top six hexadecimal digits of value are zero, and XOR with 0 changes nothing, so the key's own digits 5A3C96 pass straight through.
  • u32::wrapping_add: u32::wrapping_add is not the same as arbitrary-precision arithmetic, and an 8-bit rotate is not a 32-bit rotate.

Formulas and patterns

  • encode(a) ^ encode(b) = rotl(a ^ key, 7) ^ rotl(b ^ key, 7); = rotl(a ^ key ^ b ^ key, 7); = rotl(a ^ b, 7) because key ^ key = 0

Review

  • Q: The encoder computes (value ^ key).rotate_left(7). Which decoder restores the value? A: encoded.rotate_right(7) ^ key
  • Q: Of compression, serialization, obfuscation, authentication and encryption, which two need a secret? A: Authentication and encryption
Download cheatsheet (.md)

A value that looks random in a scan may be a familiar number after a few reversible operations. Here we follow hexadecimal values through XOR, bit rotation, and wrapping arithmetic, using breakpoints and an instruction trace to check each step instead of guessing from one memory snapshot.

An encoded field often looks like noise in a memory viewer, because the bits being stored are not the number the game displays. That on its own tells you almost nothing about what is being done to it.

Five words get used as though they were interchangeable here, and they are not:

Word What it is actually for Needs a secret?
compression making the bytes smaller no
serialization arranging values for storage or sending no
obfuscation making the bytes awkward to read no
authentication proving nobody altered the bytes yes
encryption hiding the bytes from anyone without the key yes

Only the last two involve a secret. Mistaking obfuscation for encryption sends you hunting for a key that was never there; mistaking encryption for obfuscation wastes days trying to spot a pattern in output designed to have none.

The rule your decoder has to satisfy:

Decoding an encoded value returns exactly the original value, and any integrity check travelling with it is verified before the decoded value is used for anything.

The second half is the part people skip. A value that decodes cleanly is not the same as a value nobody tampered with — decoding will happily produce a plausible-looking number from bytes somebody edited.

Trace both directions:

valid
invalid
Plain game value
Encode transform
Stored bytes
Decode transform
Value used by game rule
Integrity relation
Reject sample

The write path often reveals the forward transform. The read path reveals its inverse and the order of operations.

Obfuscation and encryption answer different questions

Section titled “Obfuscation and encryption answer different questions”
Property Reversible obfuscation Authenticated encryption
Main purpose Change representation Keep data secret and detect modification
Secret required Often no durable secret Yes
Repeated values May reveal obvious patterns Nonces should make ciphertexts differ
Modification detection Optional ad-hoc tag Authentication tag is part of the design
Analysis result Recover the transform Identify algorithm, key/nonce lifecycle, and boundaries

A reversible XOR-and-rotate transform is useful for learning data flow, but it is not a security boundary.

A minimal reversible transform is:

const ROTATION: u32 = 7;
pub const fn encode(value: u32, key: u32) -> u32 {
(value ^ key).rotate_left(ROTATION)
}
pub const fn decode(encoded: u32, key: u32) -> u32 {
encoded.rotate_right(ROTATION) ^ key
}

Read encode left to right. XOR combines the value and key; rotation moves the bit positions. To invert it, reverse both the order and each operation:

  1. rotate right by the same count;
  2. XOR with the same key because x ^ k ^ k == x.

This “reverse order, invert each step” rule also helps when disassembly shows a longer composition.

XOR, rotate, store: and back againChange the value, the key or the rotation and follow the bytes. Every step is reversible, so decoding recovers the value.

Encode, store and decode

Encode

Value 100 as a 32-bit number
00
00
00
64
XOR with the key 0x5A3C96E1
5A
3C
96
85
Rotate left 7: the high 7 bits wrap to the low end
1E
4B
42
AD
Stored in memory, lowest byte first (little-endian)
AD
42
4B
1E

Decode

Rotate right 7
5A
3C
96
85
XOR with the same key: back to 100
00
00
00
64

XOR gives 0x5A3C9685. Rotating left by 7 moves the high 7 bits (0x2D) to the low end, giving 0x1E4B42AD, stored as AD 42 4B 1E.

One value through the transform, bit by bit

Section titled “One value through the transform, bit by bit”

Take value = 100 and key = 0x5A3C96E1. The whole round trip, before each arrow is worked out below:

XOR key 0x5A3C96E1
rotate left 7
rotate right 7
XOR key 0x5A3C96E1
value 100
0x00000064
0x5A3C9685
stored 0x1E4B42AD
bytes AD 42 4B 1E
0x5A3C9685
decoded 100

In hexadecimal, 100 is 0x64, because 6 × 16 + 4 = 96 + 4 = 100; as a 32-bit number it is 0x00000064. Each hexadecimal digit stands for exactly four bits: 6 is 4 + 2 = 0110, 4 is 0100, A (10) is 8 + 2 = 1010, and E (14) is 8 + 4 + 2 = 1110. That lets both numbers be written out bit by bit.

Step 1, XOR with the key. XOR outputs 1 in each bit position where its two inputs differ and 0 where they match:

value 0x00000064 0000 0000 0000 0000 0000 0000 0110 0100
key 0x5A3C96E1 0101 1010 0011 1100 1001 0110 1110 0001
value ^ key 0x5A3C9685 0101 1010 0011 1100 1001 0110 1000 0101

The top six hexadecimal digits of value are zero, and XOR with 0 changes nothing, so the key’s own digits 5A3C96 pass straight through. Only the last byte mixes: 0110 0100 against 1110 0001 differ in the positions that give 1000 0101, which is 0x85.

Step 2, rotate left by 7. Every bit moves seven places toward the high end, and the seven bits pushed off the top re-enter at the bottom. Here are the same 32 bits split after the seventh, before and after the move:

value ^ key 0101101 0001111001001011010000101
(top 7) (remaining 25)
rotated 0001111001001011010000101 0101101
(remaining 25) (top 7, now at the bottom)
regrouped 0001 1110 0100 1011 0100 0010 1010 1101
hex digits 1 E 4 B 4 2 A D = 0x1E4B42AD

x86 stores a 32-bit number lowest byte first, which is called little-endian, so memory holds the four bytes in the reverse of the order they are written in hexadecimal:

+064
+100
+200
+300
The plain value 100, 0x00000064, as it would sit in memory: lowest byte first.
+0AD
+142
+24B
+31E
What memory holds instead: encode(100, 0x5A3C96E1) = 0x1E4B42AD. No byte is 64, so a memory scan for the value 100 never matches this field.

Decoding runs the same steps backward. Rotating 0x1E4B42AD right by 7 carries its bottom seven bits, 0101101, back to the top and restores 0x5A3C9685. XOR with the key then cancels it: the top six digits are identical in both numbers, and anything XORed with itself is 0, while the last byte gives 1000 0101 against 1110 0001 = 0110 0100 = 0x64 = 100.

Encoded words still become ordinary bytes. 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 key

Result: decoded 100, matching the original 100. Reversibility does not establish integrity.

Final values: value = 100; rotation = 7; key = 0x5A3C96E1; mixed = 0x5A3C9685; stored = 0x1E4B42AD; bytes = AD 42 4B 1E; unmixed = 0x5A3C9685; decoded = 100.

Read every step of the lesson’s example
  1. Step 1, line 1. Start with the selected inputs and follow how the next operation changes the state.

    value = 100; rotation = 7.

  2. Step 2, line 2. XOR flips exactly the bit positions selected by the key.

    value = 100; rotation = 7; key = 0x5A3C96E1; mixed = 0x5A3C9685.

  3. Step 3, line 3. Rotation carries bits across the word boundary without discarding them.

    value = 100; rotation = 7; key = 0x5A3C96E1; mixed = 0x5A3C9685; stored = 0x1E4B42AD; bytes = AD 42 4B 1E.

  4. Step 4, line 4. The bytes list is little endian: the lowest byte comes first.

    value = 100; rotation = 7; key = 0x5A3C96E1; mixed = 0x5A3C9685; stored = 0x1E4B42AD; bytes = AD 42 4B 1E.

  5. Step 5, line 5. Rotate the stored word back by the same distance.

    value = 100; rotation = 7; key = 0x5A3C96E1; mixed = 0x5A3C9685; stored = 0x1E4B42AD; bytes = AD 42 4B 1E; unmixed = 0x5A3C9685.

  6. Step 6, line 6. Result: decoded 100, matching the original 100. Reversibility does not establish integrity.

    value = 100; rotation = 7; key = 0x5A3C96E1; mixed = 0x5A3C9685; stored = 0x1E4B42AD; bytes = AD 42 4B 1E; unmixed = 0x5A3C9685; decoded = 100.

Watch it happenThe same 32 bits: XOR, rotate, store in memory, and back

The value 100 as 32 bits is XORed with the key 0x5A3C96E1, giving 0x5A3C9685, then rotated left by seven to 0x1E4B42AD. In memory the four bytes appear lowest first as AD, 42, 4B, 1E. Decoding rotates right by seven and XORs with the key again, returning 100.

Read the steps
  1. The plain value 100 is 0x00000064 as 32 bits. The key 0x5A3C96E1 is below it. The width, 32 bits, and the rotation, 7, stay the same all the way through.
  2. XOR with the key flips every bit that sits above a 1 in the key. 0x00000064 XOR 0x5A3C96E1 is 0x5A3C9685.
  3. Rotate left by seven: the top seven bits wrap round to the bottom. The result is 0x1E4B42AD. No bit is lost; every bit just changes position.
  4. x86 stores the lowest byte first. Reading the 32 bits in bytes from the right, the memory holds AD, 42, 4B, 1E, in that order. A scan for the byte 64 never matches this field.
  5. To decode, read the four bytes back as one little-endian number, 0x1E4B42AD, and undo the rotation first: rotate right by seven. The low seven bits return to the top, restoring 0x5A3C9685.
  6. Then XOR with the same key: the key bits cancel, leaving 0x00000064, which is 100. Decoding reversed both the order of the steps and the direction of the rotation.

It shows a reversible representation; the separate integrity relation must still be checked before a decoded value is used.

Compare encodings of values that differ in one known way

Section titled “Compare encodings of values that differ in one known way”

Encoding a few chosen values with the same key exposes the transform’s parts one at a time. With the key from the example:

Plain value Encoded value Stored bytes
100 0x1E4B42AD AD 42 4B 1E
101 0x1E4B422D 2D 42 4B 1E
0 0x1E4B70AD AD 70 4B 1E

The comparisons work because the key cancels. Rotation only moves bits, and XOR works on each bit position separately, so rotating two numbers and then XORing them gives the same result as XORing them first and rotating afterward. For any two plain values a and b, the key drops out of encode(a) ^ encode(b):

encode(a) ^ encode(b) = rotl(a ^ key, 7) ^ rotl(b ^ key, 7)
= rotl(a ^ key ^ b ^ key, 7)
= rotl(a ^ b, 7) because key ^ key = 0

100 against 101 exposes the rotation. 100 is 0110 0100 and 101 is 0110 0101: adding 1 to an even number flips only bit 0, so 100 ^ 101 = 1. The two encodings must then differ by rotl(1, 7) = 2⁷ = 128 = 0x80, a single bit at position 7. They do. 0xAD is 1010 1101 and 0x2D is 0010 1101, which differ only in their top bit, bit 7 of the lowest byte. A bit that went in at position 0 came out at position 7, and that gives the rotation count without knowing the key.

+080
+100
+200
+300
encode(100) ^ encode(101), byte by byte: AD ^ 2D = 80, and every other byte meets an identical byte and gives 00. The key has cancelled, leaving rotl(1, 7) = 0x00000080.

0 exposes the key. encode(0) is rotl(0 ^ key, 7) = rotl(key, 7), so rotating that stored value right by 7 returns the key itself. The third row also follows from the first without the key. encode(100) ^ encode(0) is rotl(100, 7), and 100 is only seven bits wide, so no bit reaches the top and the rotation is a plain multiplication: 100 × 128 = 12,800 = 0x3200 (check: 3 × 4,096 + 2 × 256 = 12,288 + 512 = 12,800). XORing 0x1E4B42AD with 0x00003200 changes only the byte 42: 0100 0010 against 0011 0010 gives 0111 0000 = 0x70, so the result is 0x1E4B70AD.

One pair on its own cannot pin a transform down, because many formulas turn 100 into 0x1E4B42AD. The pairs above narrow it because each one isolates a single part. If the same plain value encodes differently after a restart, the key is not a constant: it is generated per session or per object, and has to be recovered for each one.

Useful clues in a read path include:

  • XOR, rotate, byte-swap, add, subtract, and multiply by odd constants;
  • a nearby key or generation field;
  • duplicate calculations before a comparison;
  • a branch that rejects the value;
  • conversion to float or clamping immediately after decoding.

Preserve operation width and wrapping behavior

Section titled “Preserve operation width and wrapping behavior”

Machine arithmetic is performed at a width. u32::wrapping_add is not the same as arbitrary-precision arithmetic, and an 8-bit rotate is not a 32-bit rotate. A decoder has to reproduce operand widths, truncations, sign extensions, and byte order exactly. The worked example above comes out right only because the rotation is a 32-bit rotation and the four bytes are read lowest first.

For multiplication modulo 2^32, only an odd multiplier has a multiplicative inverse. That mathematical fact lets a reversible transform undo the multiply; an even multiplier loses information.

A tag verifies a defined relation, not every field

Section titled “A tag verifies a defined relation, not every field”

A small record can store a toy tag beside the encoded value and verify it before decoding. That can detect accidental changes in the example, but the relation is still reproducible by anyone who knows the algorithm and key. Name the claim accurately: it is a toy integrity relation, not cryptographic authentication.

The verification should prove:

decode(encode(value, key), key) == value

over edge values such as 0, 1, u32::MAX, values around a carry boundary, and multiple keys. Mutation tests should flip each encoded bit and confirm that the toy tag check refuses changed samples.

  • Obfuscation: a reversible representation intended to make a value less obvious, not to provide strong secrecy.
  • Plain value: the value before a transform.
  • Encoded value: the stored result of a transform.
  • Round trip: encode followed by decode, ending at the original value.
  • Differential comparison: encoding two inputs that differ in one known way and comparing the outputs; for an XOR-based transform the key cancels.
  • Wrapping arithmetic: fixed-width arithmetic reduced modulo 2^N.

An encoded value is understood only when its read and write paths agree. The inverse must undo operations in reverse order at the same machine widths, and a changed plain input must produce the encoded bytes the model predicts. A value that merely looks plausible is weaker evidence than that round trip.

Knowledge check

Invert the transform

The encoder computes (value ^ key).rotate_left(7). Which decoder restores the value?

Finished Lesson 14.10?

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.

Chapter 14: 10 lessons

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.