Obfuscated Values and Reversible Transforms
Reversible transforms can hide a value in memory without providing encryption; round trips and controlled changes reveal the rule.
Cheatsheet for Lesson 14.10
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 ofvalueare zero, and XOR with 0 changes nothing, so the key's own digits5A3C96pass straight through.u32::wrapping_add:u32::wrapping_addis 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
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.
Why a stored value can look like noise
Section titled “Why a stored value can look like noise”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:
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.
Follow one complete toy transform
Section titled “Follow one complete toy transform”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:
- rotate right by the same count;
- 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.
Encode, store and decode
Encode
XOR each bit with the key
XOR with the key 0x5A3C96E1rotate left 7
Rotate left 7: the high 7 bits wrap to the low endwrite the bytes lowest first
Stored in memory, lowest byte first (little-endian)Decode
undo the rotation
Rotate right 7undo the XOR
XOR with the same key: back to 100XOR 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:
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 0100key 0x5A3C96E1 0101 1010 0011 1100 1001 0110 1110 0001value ^ key 0x5A3C9685 0101 1010 0011 1100 1001 0110 1000 0101The 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 1101hex digits 1 E 4 B 4 2 A D = 0x1E4B42ADx86 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:
0x00000064, as it would sit in memory: lowest byte first.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 keyResult: 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
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 = 0x5A3C96E1; mixed = 0x5A3C9685.
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.
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.
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.
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.
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
- 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.
- XOR with the key flips every bit that sits above a 1 in the key. 0x00000064 XOR 0x5A3C96E1 is 0x5A3C9685.
- 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.
- 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.
- 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.
- 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 = 0100 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.
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.
Verify the recovered model
Section titled “Verify the recovered model”The verification should prove:
decode(encode(value, key), key) == valueover 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.
Glossary terms introduced here
Section titled “Glossary terms introduced here”- 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.
Invert the transform
The encoder computes (value ^ key).rotate_left(7). Which decoder restores the value?
Invert a composition in reverse order: undo the left rotation with a right rotation, then undo XOR by applying the same key again.
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.
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.