Rail-Fence Cipher Payload Obfuscation
A custom obfuscation technique observed in XLoader batch-script droppers. The attacker encrypts a binary payload with AES-256-CBC, then applies a rail-fence (zigzag) transposition cipher whose rail count and rail read-order permutation are both derived from a hardcoded seed passphrase via System.Random.
What It Does
- The plaintext payload is written into a zigzag pattern across N rails.
- The rails are then read out of order, where the read order is a permutation generated by a PRNG seeded with the ASCII sum of a passphrase.
- At runtime, the victim script reconstructs the original payload by reversing the permutation and zigzag pattern, using the same deterministic PRNG sequence.
This evades static signature matching on the encrypted payload because the byte order depends on a secret seed, not just a key.
Detection / Fingerprint
- PowerShell or C# code containing "rail", "zigzag", or permutation logic seeded by a string.
- A hardcoded passphrase followed by a
System.Random(seed)call and swap loops. - Base64 blobs whose decoded bytes begin with
ZM(byte-swappedMZ) or other scrambled PE signatures.
Implementation Pattern
Observed in XLoader 5a26fd46:
$masterSeedPhrase = "ofD1*Vc9%Vzh6CMM#s*qG*Mz"
function Derive-RailCountFromSeed {
param ([string]$seedString, [int]$minRailCount = 2, [int]$maxRailVariance = 18)
$cumulativeAscii = 0
foreach ($char in $seedString.ToCharArray()) { $cumulativeAscii += [int]$char }
return ($cumulativeAscii % $maxRailVariance) + $minRailCount
}
function Generate-SeededPermutation {
param ([string]$seedString, [int]$arraySize)
$seedNumeric = 0
foreach ($char in $seedString.ToCharArray()) { $seedNumeric += [int]$char }
$prng = [System.Random]::new($seedNumeric)
$permutationArray = 0..($arraySize - 1)
for ($i = $arraySize - 1; $i -ge 1; $i--) {
$j = $prng.Next(0, $i + 1)
$swapContainer = $permutationArray[$i]
$permutationArray[$i] = $permutationArray[$j]
$permutationArray[$j] = $swapContainer
}
return $permutationArray
}
Reproduce on Your Own VMs
A Python reimplementation:
import random
def rail_fence_scramble(data: bytes, seed: str) -> bytes:
ascii_sum = sum(ord(c) for c in seed)
rail_count = (ascii_sum % 18) + 2
rng = random.Random(ascii_sum)
# Build zigzag rail lengths
rail_lengths = [0] * rail_count
row, direction = 0, 1
for _ in data:
rail_lengths[row] += 1
if row == 0: direction = 1
elif row == rail_count - 1: direction = -1
row += direction
# Seeded permutation of rail read order
read_order = list(range(rail_count))
for i in range(rail_count - 1, 0, -1):
j = rng.randint(0, i)
read_order[i], read_order[j] = read_order[j], read_order[i]
# Split data into rails using read_order
rails = [[] for _ in range(rail_count)]
idx = 0
for r in read_order:
rails[r] = list(data[idx:idx + rail_lengths[r]])
idx += rail_lengths[r]
# Read back in zigzag order
counters = [0] * rail_count
result = bytearray()
row, direction = 0, 1
for _ in data:
result.append(rails[row][counters[row]])
counters[row] += 1
if row == 0: direction = 1
elif row == rail_count - 1: direction = -1
row += direction
return bytes(result)
Defensive Countermeasures
- Script-content analysis: flag custom permutation algorithms seeded by strings.
- Entropy analysis: AES ciphertext already has high entropy; the rail-fence adds no entropy, only reordering. Frequency analysis of byte pairs may reveal the rail structure.
Pages Where Observed
- xloader
5a26fd46^[/intel/analyses/5a26fd462a809c89b0448318591cb98a77a7b96fca658815dc0f547a51958bed.html]