Skip to main content

Entropy Analysis

How Batin uses information theory to detect packed, encrypted, and suspicious files.

What is Entropy?

Entropy measures the randomness or "information content" of data. In cybersecurity, it reveals file characteristics invisible to signature detection.

Shannon Entropy Formula

H(X) = -Σ p(xᵢ) × log₂(p(xᵢ))

Where:

  • H(X) = entropy in bits per byte (0.0 to 8.0)
  • p(xᵢ) = probability of byte value i occurring
  • log₂ = logarithm base 2

Intuitive Understanding

EntropyMeaningExample
0.0All bytes identicalAAAAAAAAAA
~4.5English textSource code, documents
~6.0Compiled codeNormal executables
~7.5Compressed dataZIP, JPEG, MP3
~8.0Perfectly randomEncrypted, packed

Why Entropy Matters for Security

Packed Executables

Malware is often "packed" to:

  • Evade signature detection
  • Reduce file size
  • Obfuscate code

Packing compresses the executable, resulting in high entropy.

Encrypted Content

Ransomware, encrypted payloads, and encrypted documents have:

  • Very high entropy (>7.8)
  • Nearly uniform byte distribution

Compression vs Encryption

Both have high entropy, but:

CharacteristicCompressedEncrypted
Entropy7.0-7.87.8-8.0
Chi-squareHigherVery low
Has headersYes (ZIP, GZIP)Often no

Chi-square test distinguishes them: encrypted data has nearly perfect uniform distribution.


Implementation

Single-Pass Algorithm

pub fn calculate_entropy_stats(data: &[u8]) -> EntropyStats {
if data.is_empty() {
return EntropyStats::default();
}

// Step 1: Build frequency table in single pass
let mut frequency: [usize; 256] = [0; 256];
for &byte in data {
frequency[byte as usize] += 1;
}

let len = data.len() as f64;
let mut entropy = 0.0;
let mut chi_square = 0.0;
let expected = len / 256.0; // Expected count for uniform

// Step 2: Calculate both metrics from frequency table
for &count in &frequency {
if count > 0 {
// Shannon entropy
let p = count as f64 / len;
entropy -= p * p.log2();

// Chi-square statistic
let diff = count as f64 - expected;
chi_square += (diff * diff) / expected;
}
}

EntropyStats { frequency, entropy, chi_square }
}

Why Single Pass?

Memory Access Pattern:

Data:      [A, B, C, D, E, F, G, H, ...]
Pass 1: ^ ^ ^ ^ ^ ^ ^ ^ (OLD: build frequency)
Pass 2: ^ ^ ^ ^ ^ ^ ^ ^ (OLD: calculate chi)

Single: ^ ^ ^ ^ ^ ^ ^ ^ (NEW: do both)
  • 50% fewer memory reads
  • Better cache utilization
  • Same result

Chi-Square Test

Purpose

Chi-square measures how much the observed distribution deviates from expected (uniform).

Formula

χ² = Σ (Oᵢ - Eᵢ)² / Eᵢ

Where:

  • Oᵢ = observed count of byte i
  • Eᵢ = expected count (data.len() / 256)

Interpretation

Chi-SquareInterpretation
< 50Very uniform (encrypted)
50-150Somewhat uniform (packed/compressed)
150-500Normal variation (binary)
> 500Non-uniform (text, structured data)

Packed vs Encrypted


Detection Thresholds

pub struct DetectionConfig {
// Packed detection
pub entropy_threshold: f64, // Default: 7.2
pub packed_chi_square_threshold: f64, // Default: 100.0

// Encrypted detection
pub encrypted_entropy_threshold: f64, // Default: 7.8
pub encrypted_chi_square_threshold: f64, // Default: 50.0
}

Detection Rules

EntropyProfile {
is_packed: entropy > 7.2 && chi_square < 100.0,
is_encrypted: entropy > 7.8 && chi_square < 50.0,
...
}

Sliding Window Entropy

Purpose

Global entropy misses localized anomalies:

[Normal data .................. Hidden encrypted blob .... Normal data]
^^^^^^^^^^^^^^^^^^^
Global entropy: 5.5 (looks normal)
Local entropy: 7.9 (suspicious!)

Implementation

pub fn sliding_window_entropy(data: &[u8], window_size: usize) -> Vec<f64> {
if data.len() < window_size {
return vec![calculate_shannon_entropy(data)];
}

let mut entropies = Vec::with_capacity(data.len() - window_size + 1);

for i in 0..=(data.len() - window_size) {
let window = &data[i..i + window_size];
entropies.push(calculate_shannon_entropy(window));
}

entropies
}

Visualization

Offset:     0    100   200   300   400   500
Entropy: |-----|-----|-----|-----|-----|
4.5 4.8 7.9 7.8 4.6 4.5
^^^^^^^^^^^
Hidden encrypted section!

Use Cases

  1. Steganography detection - Hidden data in images
  2. Ransomware analysis - Encrypted file sections
  3. Malware unpacking - Find encrypted payloads

EntropyProfile

pub struct EntropyProfile {
/// Overall file entropy (0.0-8.0)
pub global_entropy: f64,

/// Entropy at each block (for visualization)
pub block_entropies: Vec<f64>,

/// Chi-square statistic
pub chi_square: f64,

/// True if likely packed executable
pub is_packed: bool,

/// True if likely encrypted content
pub is_encrypted: bool,
}

Construction

pub fn analyze_entropy(data: &[u8], threshold: f64) -> Result<EntropyProfile> {
let stats = calculate_entropy_stats(data);

// Block entropies for visualization (optional)
let block_entropies = sliding_window_entropy(data, 256);

Ok(EntropyProfile {
global_entropy: stats.entropy,
block_entropies,
chi_square: stats.chi_square,
is_packed: stats.entropy > threshold && stats.chi_square < 100.0,
is_encrypted: stats.entropy > 7.8 && stats.chi_square < 50.0,
})
}

Real-World Examples

Normal Executable

File: notepad.exe
Entropy: 6.12 bits/byte
Chi-square: 342.5
Is Packed: false
Is Encrypted: false
→ Threat Level: Suspicious (normal exe)

UPX-Packed Malware

File: malware_packed.exe
Entropy: 7.89 bits/byte
Chi-square: 78.2
Is Packed: true
Is Encrypted: false
→ Threat Level: Dangerous

Encrypted Payload

File: encrypted_data.bin
Entropy: 7.99 bits/byte
Chi-square: 23.1
Is Packed: false
Is Encrypted: true
→ Threat Level: Dangerous

Text File

File: readme.txt
Entropy: 4.23 bits/byte
Chi-square: 8547.3 (very non-uniform)
Is Packed: false
Is Encrypted: false
→ Threat Level: Safe

Performance

Time Complexity

OperationComplexity
calculate_entropy_statsO(n)
sliding_window_entropyO(n × w)

Where n = file size, w = window size.

Space Complexity

ComponentSpace
Frequency array256 bytes (fixed)
Block entropies8 × (n/w) bytes

Optimization: Skip for Small Files

if data.len() < 256 {
// Too small for meaningful entropy
return EntropyProfile::default();
}

Key Insight

Entropy is the "second opinion" after magic bytes. A file claiming to be a PDF with 7.9 entropy is almost certainly not a normal PDF. It is likely encrypted, packed, or malicious.

Combined with chi-square, Batin can distinguish:

  • Normal compression (ZIP, JPEG)
  • Suspicious packing (UPX, Themida)
  • Likely encryption (ransomware, encrypted payloads)