Improving Obfuscation
While simple XOR provides a basic level of obfuscation, its predictability, especially with repeating keys, makes it vulnerable to analysis. Even without a key, it is extremely vulnerable to frequency analysis. To overcome some of these weaknesses, we can employ techniques to make the XOR process less static. One common improvement is known as Rolling XOR.
Rolling XOR Concept
The core idea behind Rolling XOR (also sometimes called a keystream cipher based on XOR) is to avoid applying the same repeating key byte sequence directly to the plaintext. Instead, the key byte used for the XOR operation at each position is modified based on its position within the data stream (or other changing factors).
This means that even if the same plaintext byte appears multiple times, it will likely be XORed with a different key byte each time, resulting in different ciphertext bytes. Similarly, identical sequences in the plaintext are less likely to produce identical sequences in the ciphertext. It’s worth noting that though this will obviously improve defence against frequency analysis, it will produce higher levels of entropy. So as always with obfuscation, one needs to strike a balance - too much of a good thing turns out to be bad in a different way.
Purpose: Thwarting Frequency Analysis
The primary goal of rolling XOR is to make frequency analysis significantly harder compared to simple XOR with a repeating key. In simple XOR, if the key K
repeats every 4 bytes, then byte 0 of the plaintext is always XORed with K[0]
, byte 1 with K[1]
, …, byte 4 with K[0]
, byte 5 with K[1]
, and so on. An attacker could potentially analyze every 4th byte of the ciphertext separately to deduce the corresponding key byte based on expected plaintext frequencies.
Rolling XOR disrupts this direct relationship. Since the effective key byte changes at each position i
, the statistical patterns of the plaintext are smeared across the ciphertext, making it much harder to deduce the underlying base key or patterns simply by looking at byte frequencies in the ciphertext.
Example Implementation
One common way to implement rolling XOR is to combine the base key byte with the current position index before performing the XOR operation.
Consider the following function:
// Pseudocode based on the project files
func obfuscatePayload(data []byte, baseKey []byte) []byte {
keyLen := len(baseKey)
result := make([]byte, len(data))
for i := 0; i < len(data); i++ {
// 1. Get the original key byte for this position
originalKeyByte := baseKey[i % keyLen]
// 2. Get a value based on the position (e.g., lower 8 bits of index)
positionByte := byte(i & 0xFF) // Mask 'i' to get a single byte value
// 3. Calculate the 'rolling' key byte by XORing original key and position
rollingKeyByte := originalKeyByte ^ positionByte
// 4. XOR the data with the *rolling* key byte
result[i] = data[i] ^ rollingKeyByte
}
return result
}
baseKey[i % keyLen]
provides the repeating base key byte.byte(i & 0xFF)
creates a modifier byte derived from the current indexi
. Using& 0xFF
ensures this modifier cycles every 256 bytes.- The
rollingKeyByte
is calculated by XORing these two components. This ensures that even ifbaseKey
repeats, therollingKeyByte
changes based oni
. - The actual data byte
data[i]
is then XORed with this uniquerollingKeyByte
.
Example Walkthrough
To really help solidify this concept let’s walk through a very elementary example of how this would play out conceptually.
Input Data (data
): Let’s say our data bytes are data = [0x41, 0x42, 0x43, 0x44]
Base Key (baseKey
): Let’s use a short key, say baseKey = [0x1A, 0x2B]
We’ll follow the logic inside the for
loop for each byte.
Iteration 1: Processing data[0]
(The 1st byte, i = 0
)
- Get
originalKeyByte
:- Index
i
is 0.keyLen
is 2. i % keyLen
=0 % 2
=0
.originalKeyByte = baseKey[0]
=0x1A
.
- Index
- Get
positionByte
:- Index
i
is 0. i & 0xFF
=0 & 0xFF
=0
. (0xFF is binary11111111
.0 &
anything is0
).positionByte = byte(0)
=0x00
.
- Index
- Calculate
rollingKeyByte
:rollingKeyByte = originalKeyByte ^ positionByte
rollingKeyByte = 0x1A ^ 0x00
=0x1A
.- (Binary:
00011010 ^ 00000000 = 00011010
)
- XOR Data with
rollingKeyByte
:result[0] = data[0] ^ rollingKeyByte
result[0] = 0x41 ^ 0x1A
- (Binary:
01000001 ^ 00011010 = 01011011
) result[0] = 0x5B
.
Iteration 2: Processing data[1]
(The 2nd byte, i = 1
)
- Get
originalKeyByte
:- Index
i
is 1.keyLen
is 2. i % keyLen
=1 % 2
=1
.originalKeyByte = baseKey[1]
=0x2B
.
- Index
- Get
positionByte
:- Index
i
is 1. i & 0xFF
=1 & 0xFF
=1
.positionByte = byte(1)
=0x01
.
- Index
- Calculate
rollingKeyByte
:rollingKeyByte = originalKeyByte ^ positionByte
rollingKeyByte = 0x2B ^ 0x01
- (Binary:
00101011 ^ 00000001 = 00101010
) rollingKeyByte = 0x2A
.
- XOR Data with
rollingKeyByte
:result[1] = data[1] ^ rollingKeyByte
result[1] = 0x42 ^ 0x2A
- (Binary:
01000010 ^ 00101010 = 01101000
) result[1] = 0x68
.
Iteration 3: Processing data[2]
(The 3rd byte, i = 2
)
- Get
originalKeyByte
:- Index
i
is 2.keyLen
is 2. i % keyLen
=2 % 2
=0
.originalKeyByte = baseKey[0]
=0x1A
. (Notice the base key repeats)
- Index
- Get
positionByte
:- Index
i
is 2. i & 0xFF
=2 & 0xFF
=2
.positionByte = byte(2)
=0x02
.
- Index
- Calculate
rollingKeyByte
:rollingKeyByte = originalKeyByte ^ positionByte
rollingKeyByte = 0x1A ^ 0x02
- (Binary:
00011010 ^ 00000010 = 00011000
) rollingKeyByte = 0x18
. Crucially, even though theoriginalKeyByte
(0x1A) is the same as in Iteration 1, therollingKeyByte
(0x18) is different because thepositionByte
changed.
- XOR Data with
rollingKeyByte
:result[2] = data[2] ^ rollingKeyByte
result[2] = 0x43 ^ 0x18
- (Binary:
01000011 ^ 00011000 = 01011011
) result[2] = 0x5B
.
Iteration 4: Processing data[3]
(The 4th byte, i = 3
)
- Get
originalKeyByte
:- Index
i
is 3.keyLen
is 2. i % keyLen
=3 % 2
=1
.originalKeyByte = baseKey[1]
=0x2B
. (Base key repeats)
- Index
- Get
positionByte
:- Index
i
is 3. i & 0xFF
=3 & 0xFF
=3
.positionByte = byte(3)
=0x03
.
- Index
- Calculate
rollingKeyByte
:rollingKeyByte = originalKeyByte ^ positionByte
rollingKeyByte = 0x2B ^ 0x03
- (Binary:
00101011 ^ 00000011 = 00101000
) rollingKeyByte = 0x28
. Again, compare to Iteration 2: sameoriginalKeyByte
(0x2B) but differentpositionByte
leads to a differentrollingKeyByte
(0x28 vs 0x2A).
- XOR Data with
rollingKeyByte
:result[3] = data[3] ^ rollingKeyByte
result[3] = 0x44 ^ 0x28
- (Binary:
01000100 ^ 00101000 = 01101100
) result[3] = 0x6C
.
Summary of Results
- Input Data:
[0x41, 0x42, 0x43, 0x44]
- Base Key:
[0x1A, 0x2B]
- Rolling Keys used:
[0x1A, 0x2A, 0x18, 0x28]
- Output Result:
[0x5B, 0x68, 0x5B, 0x6C]
As you can see, even with a short, repeating base key (0x1A, 0x2B
), the actual key used for XORing each byte (0x1A, 0x2A, 0x18, 0x28
) changes progressively because it incorporates the byte’s position (i
). The & 0xFF
ensures the position modifier cycles every 256 bytes, adding another layer to the key variation.
Decryption
Just like simple XOR, rolling XOR implemented this way remains perfectly symmetric. The decryption process uses the exact same logic to generate the rollingKeyByte
at each position i
.
PlaintextByte = CiphertextByte XOR rollingKeyByte
Where rollingKeyByte
is calculated identically using the baseKey
and the position i
. Since (P XOR K') XOR K' = P
, applying the same deterministic rolling key generation process twice recovers the original plaintext.
Conclusion
While rolling XOR is still not cryptographically secure in a formal sense (it’s a form of stream cipher susceptible to certain attacks if parts of the plaintext/key are known), it provides a significant improvement over simple XOR for basic obfuscation purposes by defeating simple frequency analysis. The next challenge, however, remains key management – how do the encrypting and decrypting sides securely agree on the baseKey
?
In the next section we’ll explore more secure key derivation concepts to generate a shared secret and derive the final XOR key.