LAB WORKBOOK
Malware Analysis · Module 5
Progress
0 / 9
Module 5 · Hands-On Labs

Malware Analysis
Lab Workbook

Nine sequential labs that walk you through the complete malware analysis workflow — from detecting a packed binary and unpacking it, to disassembling it in Ghidra and producing a final IoC report with a tested YARA rule. Every step shows you exactly what to type and what output to expect.

⚠ Isolated VM required ✓ No real malware — safe practice samples only ⏱ 4–5 hours total 9 labs · 3 sections
SETUP

Environment Setup

Before you begin
⚠ Mandatory Safety Rule
All lab work happens inside an isolated virtual machine. Never perform these steps on your host machine. Take a VM snapshot before each lab so you can revert if anything goes wrong.
Recommended VM Options (choose one)

Option A — REMnux (recommended): A Linux VM pre-loaded with every tool used in these labs. Download the OVA from remnux.org and import into VirtualBox or VMware. All commands in Labs A and C run as-is.

Option B — FlareVM: Windows-based analysis environment by Mandiant. Better for labs that use Windows-specific tools (pestudio, x64dbg). Install guide at github.com/mandiant/flare-vm.

Option C — Manual Ubuntu: A clean Ubuntu 22.04 VM. You will install tools as each lab requires them — commands are provided at the start of each lab.

Notice — Running This Workbook on Linux in VMware

This workbook is written for Linux from the ground up, so it runs without modification on a Linux VM under VMware.

Distro choice: Easiest option is to import the REMnux OVA directly into VMware (File → Open, point at the .ova). It is a Ubuntu-based VM with most tools (UPX, YARA, binutils, Ghidra, FLOSS) already installed — this skips most of Setup Task 1. If you would rather use a plain Ubuntu 22.04/24.04 VM you already have, that works too — Setup Task 1 gives you the install commands.

Quick install on plain Ubuntu, if you go that route:

user@ubuntu:~$
user@ubuntu:~$ sudo apt update user@ubuntu:~$ sudo apt install -y binutils upx-ucl python3 python3-pip yara gcc binwalk build-essential user@ubuntu:~$ pip3 install floss --break-system-packages

For Ghidra, download the release zip from github.com/NationalSecurityAgency/ghidra/releases, extract to /opt/ghidra, and install a JDK:

user@ubuntu:~$
user@ubuntu:~$ sudo apt install -y openjdk-21-jdk

VMware-specific tips:

Take a snapshot right after setup completes (VM menu → Snapshot → Take Snapshot) — this is the snapshot the workbook refers to before each lab. No special VMware network configuration is needed; everything in the workbook runs locally, with internet required only for the apt/pip installs during setup. If gcc is not present (used in Labs B3/C1/C3 to compile small demo binaries), install it with sudo apt install -y build-essential.

One thing to double check: binwalk -E -J (Lab A1, Task 3) needs the binwalk package — on newer Ubuntu it is sometimes Python-only and the -J (graph) flag may need matplotlib: pip3 install matplotlib --break-system-packages if the PNG export fails. If it still fails, the lab still works fine without the graph — the entropy numbers from Task 1 are the important part.

Everything else — Python scripts, upx, strings, readelf, yara — works identically on any Linux distro. Open this HTML file in Firefox inside the VM to follow along.

SETUP 1 Verify your tools are installed

Run this verification block inside your VM. Every command should return a version number or a path — not "command not found".

remnux@analysis:~$
# Check each tool — if any fail, see the install commands below remnux@analysis:~$ strings --version GNU strings (GNU Binutils) 2.38 remnux@analysis:~$ upx --version Ultimate Packer for eXecutables 4.0.1 remnux@analysis:~$ python3 --version Python 3.10.12 remnux@analysis:~$ yara --version 4.3.2 remnux@analysis:~$ which ghidra /opt/ghidra/ghidraRun remnux@analysis:~$ floss --version FLOSS 3.0.0
If any tool is missing on a manual Ubuntu VM:
sudo apt install -y binutils upx-ucl python3 yara gcc
For FLOSS: pip3 install floss
For Ghidra: download from ghidra-sre.org, extract to /opt/ghidra, run ./ghidraRun
SETUP 2 Create your lab directory and build the practice samples

All labs use samples you build yourself from safe system binaries, so there is no malware download required. You pack clean binaries with UPX and annotate them — this gives you a predictable, safe target that behaves exactly as the labs describe.

remnux@analysis:~$
# Create a workspace — keep all lab files here remnux@analysis:~$ mkdir -p ~/malware-labs/{samples,output,reports,yara} remnux@analysis:~$ cd ~/malware-labs # Copy a small Linux binary to use as our "sample" (safe — this is just the 'ls' command) remnux@analysis:~/malware-labs$ cp /bin/ls samples/sample_clean.elf # Verify it is a clean ELF binary remnux@analysis:~/malware-labs$ file samples/sample_clean.elf samples/sample_clean.elf: ELF 64-bit LSB pie executable, x86-64 # Record the original hash — this is our baseline remnux@analysis:~/malware-labs$ sha256sum samples/sample_clean.elf | tee reports/baseline_hash.txt b4ce... samples/sample_clean.elf # Create the packed version — this is what we will analyse in Lab A remnux@analysis:~/malware-labs$ cp samples/sample_clean.elf samples/sample_packed.elf remnux@analysis:~/malware-labs$ upx -9 samples/sample_packed.elf Packed 1 file. Ratio: ~43% # Confirm both files exist remnux@analysis:~/malware-labs$ ls -lh samples/ -rwxr-xr-x sample_clean.elf 138K -rwxr-xr-x sample_packed.elf 60K
✓ What success looks like
You have two files in samples/: a clean original and a packed version roughly 40–50% of its original size. The packed file is what every Lab A exercise analyses.
Lab Section A · Unpacking
LAB A1

Entropy Analysis & Packer Detection

strings · python3 · binwalk Entropy · Packer ID ~25 min
Lab Brief

Before you unpack anything, you need to confirm a binary is packed and understand what you are dealing with. This lab teaches you the three fastest triage checks: entropy measurement, string count comparison, and import table inspection. You will run them on both your clean and packed samples and record the difference.

Measure and compare entropy of a clean vs packed binary
Observe how packing destroys the string table
Identify a packed binary's import table fingerprint
Use binwalk to visualise entropy graphically
TASK 1 Measure entropy on both files and record the difference

Run the entropy script on both the clean and packed sample. This Python one-liner computes Shannon entropy — the same calculation that tools like pestudio and DIE use internally.

remnux@analysis:~/malware-labs$
# Save the entropy script so you can reuse it remnux@analysis:~/malware-labs$ cat > entropy.py << 'EOF' import math, collections, sys data = open(sys.argv[1], 'rb').read() counts = collections.Counter(data) total = len(data) H = -sum((c/total)*math.log2(c/total) for c in counts.values()) print(f"{sys.argv[1]}: {H:.4f} bits/byte") EOF # Run on the CLEAN binary first remnux@analysis:~/malware-labs$ python3 entropy.py samples/sample_clean.elf samples/sample_clean.elf: 4.9821 bits/byte # Now run on the PACKED binary remnux@analysis:~/malware-labs$ python3 entropy.py samples/sample_packed.elf samples/sample_packed.elf: 7.6340 bits/byte
✓ Expected Results
Clean sample: 4.8 – 5.2 bits/byte (normal for compiled code)
Packed sample: 7.4 – 7.8 bits/byte (near-random — packing confirmed)
Any value above 7.2 is a strong packing indicator.
Why this works: Compression algorithms eliminate repetition in data. The more thoroughly a file is compressed, the closer its byte distribution approaches perfectly random — and Shannon entropy measures exactly how random a distribution is. A packed file's payload section approaches 8.0 bits/byte (theoretical maximum for random data), while normal compiled code sits around 5.0 because assembly instructions have predictable patterns.
TASK 2 Compare string counts between clean and packed samples

Count and compare readable strings in both files. This is the fastest human-readable confirmation of packing — a packed binary has almost nothing useful to read.

remnux@analysis:~/malware-labs$
# Count strings of minimum 6 characters in each file remnux@analysis:~/malware-labs$ strings -n 6 samples/sample_clean.elf | wc -l 247 remnux@analysis:~/malware-labs$ strings -n 6 samples/sample_packed.elf | wc -l 18 # Look at what those 18 strings ARE in the packed file remnux@analysis:~/malware-labs$ strings -n 6 samples/sample_packed.elf UPX0 UPX1 UPX2 $Info: This file is packed with the UPX executable packer. $Id: UPX 4.0.1 ... /lib64/ld-linux-x86-64.so.2 GLIBC_2.4 _init ...
✓ What to Notice
The packed file's strings are almost entirely UPX metadata — the packer left its own fingerprints.
In real malware, attackers sometimes strip these UPX strings to evade detection. But the entropy signature remains.
Key insight: The UPX section names (UPX0, UPX1) and the info string are left by UPX itself. Sophisticated malware authors often patch these out — replacing UPX0 with ABC0 — so the packer is not named. Your job is to still detect it via entropy, even when the name is hidden. This is why entropy is the more reliable check.
TASK 3 Visualise entropy across the binary with binwalk

binwalk -E produces a per-block entropy graph. On a packed binary you will see a solid flat plateau near 1.0 (normalised) — that plateau is the compressed payload. On the clean binary, the graph is varied and uneven.

remnux@analysis:~/malware-labs$
# -E = entropy analysis, -J = output a PNG image of the graph remnux@analysis:~/malware-labs$ binwalk -E -J samples/sample_packed.elf DECIMAL HEXADECIMAL ENTROPY ------------------------------------------- 0 0x0 Rising entropy edge (0.952) 4096 0x1000 High entropy data, best guess: encrypted or compressed ... Saved entropy graph to: sample_packed.elf.png # Also run on the clean binary for comparison remnux@analysis:~/malware-labs$ binwalk -E -J samples/sample_clean.elf Saved entropy graph to: sample_clean.elf.png # Move graphs to output folder remnux@analysis:~/malware-labs$ mv *.png output/
Open the PNG files (use any image viewer in your VM) and compare the two graphs side by side. The packed binary shows a near-flat line at the top of the entropy scale. The clean binary shows irregular peaks and valleys — that variability reflects the different sections of the ELF file (code, data, symbol table, debug info) all having different compression characteristics.
Your Turn — Independent Task
  1. Create a second packed sample using a different compression level: upx -1 -o samples/sample_packed_fast.elf samples/sample_clean.elf (fastest/weakest compression). Measure its entropy. Is it higher or lower than the -9 version? Why?
  2. Try to manually rename the UPX section strings in the packed file using a hex editor (hexedit or xxd + sed). Replace the bytes for "UPX0" with "AAA0". Now run strings again — the name is gone. Does the entropy still expose the packing?
Q1 Answer: The -1 version will have slightly lower entropy than -9 — faster compression is less thorough, so there is slightly more structure remaining in the data. Both will still be above 7.0 and clearly in the "packed" range. The difference is usually small (0.1–0.3 bits/byte).

Q2 Answer: Yes — entropy is completely unaffected by renaming the section labels. The labels are metadata stored in the ELF header; the compressed payload bytes are in the section body. Entropy measures the payload bytes, not the labels. This is why entropy analysis is more robust than signature-based packer detection.
LAB A2

UPX Unpacking & Verification

upx · strings · python3 Unpacking · Before/After Comparison ~20 min
Lab Brief

With packing confirmed, you now unpack the sample and verify the result. This lab covers the full before/after workflow: unpacking, entropy re-check, string re-check, and hash comparison — the exact checklist a real analyst follows to confirm a successful unpack.

Unpack a UPX binary with upx -d
Verify the unpack succeeded using four independent checks
Document the change in entropy, string count, and file size
TASK 1 Unpack the sample and confirm success
remnux@analysis:~/malware-labs$
# ALWAYS work on a copy — never modify the original evidence remnux@analysis:~/malware-labs$ cp samples/sample_packed.elf samples/sample_unpacked.elf # Unpack it remnux@analysis:~/malware-labs$ upx -d samples/sample_unpacked.elf Ultimate Packer for eXecutables File size Ratio Format Name ------------ ------- ------ ------ 60000 -> 138000 230% linux/amd64 sample_unpacked.elf Unpacked 1 file. # Verification Check 1: File size remnux@analysis:~/malware-labs$ ls -lh samples/ sample_clean.elf 138K <-- original clean sample_packed.elf 60K <-- still packed (untouched) sample_unpacked.elf 138K <-- unpacked copy: back to original size [OK] # Verification Check 2: Entropy remnux@analysis:~/malware-labs$ python3 entropy.py samples/sample_unpacked.elf samples/sample_unpacked.elf: 4.9814 bits/byte [back in normal range] # Verification Check 3: String count remnux@analysis:~/malware-labs$ strings -n 6 samples/sample_unpacked.elf | wc -l 247 [matches clean binary count] # Verification Check 4: Hash matches clean original? remnux@analysis:~/malware-labs$ sha256sum samples/sample_clean.elf samples/sample_unpacked.elf b4ce... sample_clean.elf b4ce... sample_unpacked.elf # Hashes match — perfect unpack confirmed
✓ All Four Checks Should Pass
File size: unpacked ≈ same as clean original
Entropy: dropped from ~7.6 to ~4.9
String count: back to original count
Hash: matches original (UPX is lossless — perfect reconstruction)
TASK 2 Write a before/after analysis summary to file

Document your findings in a structured report file. This habit is essential — every analysis step you do without documenting it is lost when you close the terminal.

remnux@analysis:~/malware-labs$
remnux@analysis:~/malware-labs$ cat > reports/lab_A2_unpack_report.txt << 'EOF' LAB A2 - Unpacking Report Date: [fill in] Analyst: [YOUR NAME] SAMPLE: sample_packed.elf SHA256 (packed): [paste here] SHA256 (unpacked): [paste here] PACKER IDENTIFIED: UPX 4.0.1 (confirmed by section names + upx -d success) BEFORE UNPACKING: File size: 60 KB Entropy: 7.63 bits/byte String count: 18 strings AFTER UNPACKING: File size: 138 KB Entropy: 4.98 bits/byte String count: 247 strings CONCLUSION: Unpack successful. Binary ready for reverse engineering. EOF remnux@analysis:~/malware-labs$ cat reports/lab_A2_unpack_report.txt
Your Turn — Independent Task
  1. What happens if you run upx -d on the already-unpacked copy? Try it. Read the error message carefully — what is UPX checking for?
  2. What happens if you try to unpack a file that was never packed? Run upx -d on a fresh copy of samples/sample_clean.elf. What does the error tell you?
Q1 Answer: UPX will say NotPackedException: not packed by UPX. After unpacking, UPX removes the magic header markers (the UPX! signature) that it uses to identify its own packed files. Running -d a second time finds no signature and refuses to proceed.

Q2 Answer: Same error: not packed by UPX. UPX checks for its own signature before attempting anything. This shows that UPX detection is signature-based — which is why adversaries rename the section headers to evade detection tools that rely on those same signatures.
LAB A3

Manual Unpacking — Section Entropy & OEP Concepts

python3 · struct OEP · ELF Structure · Memory Dump Theory ~35 min
Lab Brief

When upx -d fails — because the packer is custom or UPX headers have been stripped — you need to understand what the packer stub does so you can replicate the analysis manually. In this lab you will write a Python script that parses ELF section headers, locates the entry point, and identifies the high-entropy payload section. This teaches the OEP-hunting concept used in live debuggers, without requiring a Windows debugger.

Parse a binary's section headers with Python
Locate the entry point address
Identify the high-entropy section that holds the packed payload
Understand the unpack-dump-fix workflow used by real debuggers
TASK 1 Parse the ELF header and locate the entry point
remnux@analysis:~/malware-labs$
remnux@analysis:~/malware-labs$ cat > elf_entry.py << 'EOF' import struct, sys data = open(sys.argv[1], "rb").read() # ELF magic check (first 4 bytes should be 0x7f 'E' 'L' 'F') print(f"Magic bytes: {data[:4]}") print(f"EI_CLASS: {'64-bit' if data[4]==2 else '32-bit'}") print(f"File size: {len(data):,} bytes") # Entry point is at offset 0x18 in a 64-bit ELF header (8-byte value) entry = struct.unpack_from("<Q", data, 0x18)[0] print(f"Entry point: 0x{entry:016x} (stub starts executing here)") EOF remnux@analysis:~/malware-labs$ python3 elf_entry.py samples/sample_packed.elf Magic bytes: b'\x7fELF' EI_CLASS: 64-bit File size: 60,456 bytes Entry point: 0x0000000000401000 (stub starts executing here) # Compare to the unpacked binary's entry point remnux@analysis:~/malware-labs$ python3 elf_entry.py samples/sample_unpacked.elf Entry point: 0x0000000000401a40 (the REAL program's entry point)
What this shows: The packed binary's entry point address differs from the unpacked binary's entry point. In the packed file, the entry point jumps into the stub — small loader code that decompresses the real binary into memory. After decompression, the stub jumps to the OEP (Original Entry Point) — which is the unpacked binary's actual entry point you just measured. In a live debugger, OEP hunting means setting a breakpoint that catches the exact moment the stub makes this final jump.
TASK 2 Write a section scanner and identify the high-entropy payload section

Per-section entropy scanning is how analysts pinpoint exactly which section of a binary contains the packed payload versus the loader stub. Write this scanner yourself — understanding how it works is more valuable than running a tool you don't understand.

remnux@analysis:~/malware-labs$
remnux@analysis:~/malware-labs$ cat > section_entropy.py << 'EOF' """ section_entropy.py - Scan each ELF section and report entropy Usage: python3 section_entropy.py <binary> """ import sys, math, collections, struct def entropy(data): if not data: return 0.0 c = collections.Counter(data) t = len(data) return -sum((v/t)*math.log2(v/t) for v in c.values()) raw = open(sys.argv[1], "rb").read() # Parse ELF section header table e_shoff = struct.unpack_from("<Q", raw, 0x28)[0] e_shnum = struct.unpack_from("<H", raw, 0x3c)[0] e_shentsize = struct.unpack_from("<H", raw, 0x3a)[0] e_shstrndx = struct.unpack_from("<H", raw, 0x3e)[0] # Get section name string table sh_strtab_off = e_shoff + e_shstrndx * e_shentsize strtab_offset = struct.unpack_from("<Q", raw, sh_strtab_off + 0x18)[0] strtab_size = struct.unpack_from("<Q", raw, sh_strtab_off + 0x20)[0] strtab = raw[strtab_offset:strtab_offset+strtab_size] print(f"{'SECTION':<20} {'OFFSET':>10} {'SIZE':>10} {'ENTROPY':>10} FLAG") print("-" * 60) for i in range(e_shnum): sh = e_shoff + i * e_shentsize name_off = struct.unpack_from("<I", raw, sh)[0] sh_offset = struct.unpack_from("<Q", raw, sh + 0x18)[0] sh_size = struct.unpack_from("<Q", raw, sh + 0x20)[0] name = strtab[name_off:].split(b"\x00")[0].decode("utf-8","replace") section_data = raw[sh_offset:sh_offset+sh_size] H = entropy(section_data) flag = " *** HIGH - PACKED?" if H > 7.0 else (" ** ELEVATED" if H > 6.0 else "") if sh_size > 0: print(f"{name:<20} {sh_offset:>10x} {sh_size:>10,} {H:>10.4f} {flag}") EOF remnux@analysis:~/malware-labs$ python3 section_entropy.py samples/sample_packed.elf SECTION OFFSET SIZE ENTROPY FLAG ------------------------------------------------------------ .interp 0x2b8 28 4.0821 .text 0x1000 45,024 7.6411 *** HIGH - PACKED? .rodata 0xc000 1,024 3.1200 .data 0xe000 512 2.9100 remnux@analysis:~/malware-labs$ python3 section_entropy.py samples/sample_clean.elf SECTION OFFSET SIZE ENTROPY FLAG ------------------------------------------------------------ .interp 0x2b8 28 4.0821 .text 0x1000 58,000 5.1234 .rodata 0xe000 3,200 4.8900 .data 0x15000 800 3.2100
✓ Key Finding
The packed binary's .text section hits 7.6+ — the entire code section is the compressed payload.
The clean binary's sections all sit in the 3.0 – 5.2 normal range.
In real malware, the high-entropy section is often unnamed or has a suspicious name like UPX1 or .rsrc.
Your Turn — Independent Task
  1. Modify section_entropy.py to also print the first 16 bytes of each section as hex (add print(section_data[:16].hex()) after the entropy line). Do the first bytes of the high-entropy section look random, or can you see any structure?
  2. In a real OEP-hunting workflow (using x64dbg on Windows), the analyst sets a breakpoint on the VirtualAlloc API and waits for the stub to allocate memory for the unpacked payload. Why VirtualAlloc specifically? What must happen before the stub can decompress into memory?
Q1: The first bytes of a compressed payload typically look completely random — no repeating patterns, no null sequences, no ASCII characters. This visually confirms compression/encryption, in contrast to the .rodata or .data sections which often start with recognisable null padding or short ASCII fragments.

Q2 Answer: The stub must decompress the payload into executable memory. To do this it must: (1) call VirtualAlloc (or VirtualAllocEx) to reserve a chunk of memory, (2) write the decompressed payload there, (3) call VirtualProtect to mark it as executable, then (4) jump to it. Breaking on VirtualAlloc catches the exact moment before the original code is written into memory — letting the analyst dump the process at the right moment.
Lab Section B · Reverse Engineering
LAB B1

First Look in Ghidra — Navigation & Structure

Ghidra Disassembly · Decompiler · Navigation ~40 min
Lab Brief

You will load the unpacked sample into Ghidra, navigate its structure, and use the decompiler to understand the program's logic. The goal is to build fluency in Ghidra's interface: finding functions, renaming things, leaving comments, and reading decompiled pseudocode. By the end you will have annotated the binary so a colleague could understand your analysis.

Import a binary into Ghidra and run auto-analysis
Navigate from the entry point to main()
Rename variables and functions in the decompiler
Use the Defined Strings window to locate hardcoded values
TASK 1 Import the binary into Ghidra and run analysis

Follow each step exactly in the Ghidra GUI. This walk-through covers everything from first launch to having the binary ready to analyse.

GUI Steps — do these in Ghidra
Step 1 - Launch Ghidra remnux@analysis:~/malware-labs$ /opt/ghidra/ghidraRun & Step 2 - Create a project File -> New Project -> Non-Shared Project Name: "MalwareLabs_B" Directory: ~/malware-labs/ Step 3 - Import the binary File -> Import File Select: ~/malware-labs/samples/sample_unpacked.elf Ghidra auto-detects: ELF 64-bit, x86 LE GCC Click OK -> click OK again on the summary dialog Step 4 - Open the CodeBrowser Double-click the file in the project window Ghidra asks: "Analyze this file?" -> click YES Analysis Options dialog: leave all defaults -> click ANALYZE Wait for analysis to complete (progress bar, bottom right) Analysis typically takes 15-90 seconds Step 5 - Confirm your layout LEFT panel = Symbol Tree (functions, imports, exports) CENTRE panel = Listing (disassembly) RIGHT panel = Decompiler (C-like pseudocode) If the Decompiler is not visible: Window -> Decompiler
What Ghidra's analysis does: It scans the entire binary, identifies function boundaries, resolves all known library calls against its built-in function signatures database, and builds a call graph. The output — which functions exist, what calls what — is what the Symbol Tree and Decompiler panels show you. For our ls-derived sample, Ghidra will recognise many standard libc functions automatically and label them.
TASK 2 Navigate to entry, find main(), and rename things
Ghidra GUI — Navigation Steps
1. In the Symbol Tree (left panel), expand "Functions" You will see a long list of function names 2. Click "_start" or "entry" - this is the true entry point The Decompiler (right panel) shows something like: void _start(void) { __libc_start_main(main, argc, argv, _init, _fini, 0); } This is the runtime startup - it calls main() for us 3. In the Decompiler, double-click the word "main" Ghidra jumps to the main() function You can now see the actual program logic 4. Read the decompiled main() - how many functions does it call? Note each call in the left margin of the decompiler These are your analysis targets 5. Rename a variable - RIGHT-CLICK any variable name -> "Rename Variable" -> type a meaningful name -> Enter Example: rename "param_1" to "argc" if it's the argument count 6. Add a comment - click any line in the Listing (centre panel), press ";" Type: "Entry point - program starts here" Comments are saved in the project
✓ What You Should See in main()
The decompiler shows several function calls. Some will already be labelled by Ghidra (e.g. opendir, readdir, printf). Others may be unnamed — labelled FUN_00401234. The unnamed ones are where your analysis work goes.
TASK 3 Use the Defined Strings window to find all hardcoded values
Ghidra GUI
1. Open the Defined Strings window Window -> Defined Strings (or press Shift+S) A table appears showing every string Ghidra identified 2. Sort by "String Value" column (click the column header) Look for: file paths, error messages, format strings (%s, %d), URLs 3. Double-click any string in the list Ghidra jumps to that string's address in the Listing Right-click the address -> "References" -> "Show References to Address" This shows every function that uses this string - very powerful 4. Record interesting strings In our ls-derived sample, look for: - Format strings like "%s %s\n" (output formatting) - Error strings like "cannot access '%s'" - Path strings like "/etc/passwd" In real malware these would be: C2 URLs, registry paths, mutex names
Why "Show References" is powerful: A string like a C2 URL might appear in the binary once, but ten different functions might use it. The References view shows all ten — letting you quickly jump to each function that handles network communication, without reading every function manually.
Your Turn — Independent Task
  1. Find the function in Ghidra that handles the output formatting (look for calls to printf with format strings). Rename it to something descriptive like format_output. Now look at the Function Call Graph (Window → Function Call Graph) — how many other functions call format_output?
  2. In the Listing panel, find any JNE (Jump if Not Equal) instruction. Right-click it and choose "Toggle Flow Override" to force it to always jump (or never jump). What does the decompiler show changes? Then undo with Ctrl+Z. This is how analysts "patch" anti-debug checks in a live session.
Q1: The Function Call Graph visually shows caller arrows pointing into your renamed function. Count those arrows — each is a caller. For the ls binary's print function, you will likely see 8–15 callers. In malware analysis, a function called by many others is important — it may be a central logging, encoding, or C2 communication function.

Q2: When you force a JNE to always jump (or never jump), the decompiler immediately updates — it removes a branch that's now impossible, simplifying the pseudocode. This is called patch analysis. In real practice, analysts patch anti-debug JNE checks to always take the "not being debugged" path, so the rest of the analysis is clean.
LAB B2

Reading the Import Table & Identifying Capability

python3 · readelf Import Analysis · Capability Mapping ~30 min
Lab Brief

The import table tells you what a binary is capable of before you read a single line of code. In this lab you will write a Python script that parses the dynamic symbol table of an ELF binary, categorises each imported function into a capability bucket (network, persistence, process, crypto, etc.), and produces a capability report. The same technique applies to Windows PE files using the pefile library.

Parse the dynamic symbol table to list all imported functions
Map imports to capability categories
Identify which capabilities a binary has from its imports alone
TASK 1 List all imported functions with readelf
remnux@analysis:~/malware-labs$
# List all dynamic imports (functions pulled from shared libraries) remnux@analysis:~/malware-labs$ readelf -W --dyn-syms samples/sample_unpacked.elf | grep "UND" 0: 0000000000000000 0 FUNC GLOBAL DEFAULT UND opendir@GLIBC_2.17 1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND readdir 2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND closedir 3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND stat 4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND lstat 5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf 6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND malloc 7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND free 8: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strcmp 9: 0000000000000000 0 FUNC GLOBAL DEFAULT UND getpwuid ... # UND = undefined at compile time, resolved at runtime from shared libs
TASK 2 Write a capability classifier script

Type this script out yourself — do not copy-paste. Typing it forces you to read each line and understand what it does.

remnux@analysis:~/malware-labs$
remnux@analysis:~/malware-labs$ cat > capability_scan.py << 'EOF' """ capability_scan.py - Map imported functions to capability categories Usage: python3 capability_scan.py <binary> """ import subprocess, sys, re from collections import defaultdict CAPABILITIES = { "FILE_SYSTEM": ["open","read","write","close","stat","lstat","fstat", "mkdir","rmdir","rename","unlink","opendir","readdir", "closedir","fopen","fread","fwrite","fclose","getcwd"], "NETWORK": ["socket","connect","bind","listen","accept","send", "recv","sendto","recvfrom","getaddrinfo","inet_aton", "htons","ntohs","gethostbyname","curl","wget"], "PROCESS": ["fork","exec","execve","system","popen","clone", "waitpid","kill","getpid","setuid","setgid","ptrace"], "MEMORY": ["malloc","calloc","realloc","free","mmap","mprotect", "munmap","memcpy","memset","memmove"], "CRYPTO": ["crypt","EVP_","AES_","RSA_","SHA","MD5","BN_", "DES_","HMAC","RAND_bytes"], "USER_ACCTS": ["getpwuid","getpwnam","getgroups","getgrgid", "shadow","pam_","auth"], "OUTPUT": ["printf","fprintf","sprintf","puts","write","putchar"], "STRING_OPS": ["strcmp","strncmp","strlen","strcpy","strcat", "strstr","strtok","atoi","atol","sscanf"], } result = subprocess.run( ["readelf", "-W", "--dyn-syms", sys.argv[1]], capture_output=True, text=True ) imports = re.findall(r'UND\s+(\w+)', result.stdout) buckets = defaultdict(list) uncategorised = [] for fn in imports: matched = False for category, keywords in CAPABILITIES.items(): if any(fn.lower().startswith(k.lower()) or k.lower() in fn.lower() for k in keywords): buckets[category].append(fn) matched = True break if not matched: uncategorised.append(fn) print(f"\n{'='*55}") print(f"CAPABILITY REPORT: {sys.argv[1]}") print(f"{'='*55}") for cat, fns in sorted(buckets.items()): print(f"\n[{cat}]") for fn in fns: print(f" + {fn}") if uncategorised: print(f"\n[UNCATEGORISED]") for fn in uncategorised: print(f" ? {fn}") print(f"\nTotal imports: {len(imports)}") print(f"{'='*55}\n") EOF # Now run it remnux@analysis:~/malware-labs$ python3 capability_scan.py samples/sample_unpacked.elf =================================================== CAPABILITY REPORT: samples/sample_unpacked.elf =================================================== [FILE_SYSTEM] + opendir + readdir + closedir + stat + lstat [USER_ACCTS] + getpwuid + getgrgid [OUTPUT] + printf + fprintf Total imports: 24
Interpreting the report: For our benign ls sample the results are expected — file system access, user account lookups, and output. In real malware analysis, seeing NETWORK + PROCESS + CRYPTO together in a capability report is a severe red flag — it suggests a tool that connects out, can run commands, and encrypts its communications. That combination alone justifies deep reverse engineering before anything else.
Your Turn — Independent Task
  1. Add a SUSPICIOUS_WINDOWS category to the script that flags Windows-specific dangerous APIs: CreateRemoteThread, VirtualAllocEx, WriteProcessMemory, SetWindowsHookEx, RegSetValueEx. If the binary calls ANY of these, print a loud warning: *** HIGH RISK: Process injection or registry persistence detected ***
  2. Run capability_scan.py on the packed version of the sample (samples/sample_packed.elf). Compare the output to the unpacked version. What does the packed version report, and why is this significant for real investigations?
Q2 Answer: The packed version will show very few or no imports — typically only the UPX stub's minimal imports (__libc_start_main, _init). This is critical: you cannot assess capability from the import table of a packed binary. An antivirus or analyst who only checks the import table of a packed sample will conclude "this looks harmless" — which is exactly what the packer is designed to cause. Always unpack first, then check imports.
LAB B3

Decode a Simulated XOR-Obfuscated Config

python3 XOR Decoding · Config Extraction · Obfuscation ~35 min
Lab Brief

One of the most common obfuscation techniques in malware is XOR encoding of configuration data — C2 addresses, encryption keys, victim IDs. In this lab you will create a simulated encoded config (as an analyst would encounter inside a real binary), write a decoder, brute-force the XOR key when it is not known, and produce the plaintext configuration. This exact workflow applies to real-world malware config extraction.

Create a realistic XOR-encoded config blob
Decode it using a known key
Brute-force the key when it is unknown (single-byte XOR)
Decode multi-byte XOR (rolling key)
TASK 1 Create and decode a single-byte XOR config

You are playing both roles here: first the "malware author" (encoding the config) and then the "analyst" (decoding it). Understanding both sides makes you a better reverse engineer.

remnux@analysis:~/malware-labs$
# ROLE 1: MALWARE AUTHOR - encode a config and save it remnux@analysis:~/malware-labs$ python3 -c " config = b'C2=192.168.10.55|PORT=4444|SLEEP=60|ID=DropperX_v2' key = 0x3F encoded = bytes([b ^ key for b in config]) open('samples/encoded_config.bin','wb').write(encoded) print('Encoded (hex):', encoded.hex()) print('Length:', len(encoded), 'bytes') " Encoded (hex): 7c570e56535e5d5a565e565c51... Length: 50 bytes # ROLE 2: ANALYST - you found this blob in Ghidra. Key is known (0x3F from decompiler) remnux@analysis:~/malware-labs$ python3 -c " encoded = open('samples/encoded_config.bin','rb').read() key = 0x3F decoded = bytes([b ^ key for b in encoded]) print('Decoded config:') print(decoded.decode('utf-8')) " Decoded config: C2=192.168.10.55|PORT=4444|SLEEP=60|ID=DropperX_v2
Why XOR is everywhere in malware: XOR is perfectly symmetric — the same operation with the same key both encrypts AND decrypts. It requires no key schedule, no padding, and only one line of code to implement. The tradeoff for the attacker is that it is trivially breakable: brute-forcing all 256 possible single-byte keys takes microseconds on any modern machine.
TASK 2 Brute-force the XOR key when it is unknown

In a real analysis, you find the encoded blob in Ghidra but the key is itself computed or obfuscated. You need to brute-force all 256 possible byte values and apply a heuristic to find which one produces valid ASCII output.

remnux@analysis:~/malware-labs$
remnux@analysis:~/malware-labs$ cat > xor_brute.py << 'EOF' """ xor_brute.py - Brute-force single-byte XOR key Usage: python3 xor_brute.py <encoded_binary_file> """ import sys, string def is_printable(data, threshold=0.85): """Return True if >85% of bytes are printable ASCII""" printable = set(string.printable.encode()) count = sum(1 for b in data if b in printable) return count / len(data) >= threshold encoded = open(sys.argv[1], "rb").read() candidates = [] for key in range(256): decoded = bytes([b ^ key for b in encoded]) if is_printable(decoded): candidates.append((key, decoded)) if not candidates: print("No single-byte XOR key produced readable output.") print("Try multi-byte XOR or a different encoding.") else: print(f"Found {len(candidates)} candidate key(s):\n") for key, decoded in candidates: print(f"KEY 0x{key:02X}: {decoded[:60]}") EOF remnux@analysis:~/malware-labs$ python3 xor_brute.py samples/encoded_config.bin Found 1 candidate key(s): KEY 0x3F: b'C2=192.168.10.55|PORT=4444|SLEEP=60|ID=DropperX_v2'
✓ What Success Looks Like
The script tries all 256 keys, and only key 0x3F produces readable ASCII text above the threshold. In practice, a config blob may have only 1–3 valid candidates, and you visually pick the one that makes semantic sense (C2 addresses, port numbers, and hostnames are unmistakable).
TASK 3 Decode a multi-byte (rolling key) XOR config

Slightly more sophisticated malware uses a multi-byte key — cycling through a 4 or 8-byte key pattern. Single-byte brute force fails here. You need to determine the key length and brute-force each position independently.

remnux@analysis:~/malware-labs$
# Create a multi-byte XOR encoded config (key = b'\xAA\xBB\xCC\xDD') remnux@analysis:~/malware-labs$ python3 -c " config = b'HOST=evil-c2.ru|PORT=8443|CAMPAIGN=APT_WINTER_2025' key = b'\xAA\xBB\xCC\xDD' encoded = bytes([config[i] ^ key[i % len(key)] for i in range(len(config))]) open('samples/encoded_multi.bin','wb').write(encoded) print('Multi-byte encoded:', encoded.hex()) " # Now write the multi-byte brute forcer remnux@analysis:~/malware-labs$ cat > xor_multi.py << 'EOF' """ xor_multi.py - Brute-force multi-byte XOR key (known key length) Usage: python3 xor_multi.py <file> <key_length> """ import sys encoded = open(sys.argv[1],"rb").read() key_len = int(sys.argv[2]) printable = set(range(32, 127)) # Brute-force each key byte independently key = [] for pos in range(key_len): slice_bytes = encoded[pos::key_len] best_k, best_score = 0, 0 for k in range(256): decoded_slice = bytes([b ^ k for b in slice_bytes]) score = sum(1 for b in decoded_slice if b in printable) if score > best_score: best_score, best_k = score, k key.append(best_k) print(f" Key byte [{pos}] = 0x{best_k:02X}") decoded = bytes([encoded[i] ^ key[i % key_len] for i in range(len(encoded))]) print(f"\nKey: {bytes(key).hex()}") print(f"Decoded: {decoded}") EOF remnux@analysis:~/malware-labs$ python3 xor_multi.py samples/encoded_multi.bin 4 Key byte [0] = 0xAA Key byte [1] = 0xBB Key byte [2] = 0xCC Key byte [3] = 0xDD Key: aabbccdd Decoded: b'HOST=evil-c2.ru|PORT=8443|CAMPAIGN=APT_WINTER_2025'
Your Turn — Independent Task
  1. Encode a config of your own invention with a 3-byte key of your choice. Run xor_multi.py with key length 3. Does it recover your key and plaintext correctly?
  2. What happens if you run xor_multi.py with the wrong key length (e.g., length 5 on a 4-byte key)? Does the output look like garbage? What does this tell you about how you would determine the correct key length in a real investigation? (Hint: research "index of coincidence" and "Kasiski examination".)
Q1: Yes — the frequency analysis approach (finding the key byte that produces the most printable ASCII per slice) works regardless of the key length, as long as the plaintext is mostly ASCII and the ciphertext is long enough.

Q2: With the wrong key length, the output is garbled — the key bytes found by the per-slice optimisation will be wrong because the slices no longer align with the key positions. In practice, analysts determine key length using the Index of Coincidence — a statistical measure of how non-random a byte sequence is. You try lengths 1–16, compute IoC for each, and the correct length produces a spike in plaintext-like statistics.
Lab Section C · IoC Extraction
LAB C1

String Triage Pipeline

strings · FLOSS · grep · python3 IoC Identification · Triage · Classification ~30 min
Lab Brief

In real investigations, analysts triage dozens of samples quickly — spending 5 minutes on most and only going deep on a few. This lab teaches you to build and run a complete string triage pipeline that automatically extracts and categorises candidate IoCs from any binary in under two minutes.

Build a reusable triage pipeline script
Use FLOSS to extract obfuscated strings that strings misses
Classify extracted strings into IoC categories automatically
Output a structured triage report
TASK 1 Build a string triage pipeline and run it
remnux@analysis:~/malware-labs$
remnux@analysis:~/malware-labs$ cat > string_triage.py << 'EOF' """ string_triage.py - Automated string triage for IoC extraction Usage: python3 string_triage.py <binary> """ import subprocess, sys, re, os from datetime import datetime target = sys.argv[1] sample_name = os.path.basename(target) PATTERNS = { "IPv4_ADDRESS": r'\b(?:\d{1,3}\.){3}\d{1,3}\b', "URL": r'https?://[^\s\'"><,]{6,}', "DOMAIN": r'\b(?:[a-zA-Z0-9-]+\.)+(?:com|net|org|ru|cn|io|xyz|top|info|biz)\b', "WINDOWS_PATH": r'[A-Za-z]:\\(?:[^\\\n<>:"/|?*]{1,255}\\?)*', "REGISTRY_KEY": r'(?:HKLM|HKCU|HKEY_LOCAL_MACHINE|HKEY_CURRENT_USER)\\[^\s]+', "EXE_OR_DLL": r'[a-zA-Z0-9_.-]{3,64}\.(?:exe|dll|sys|bat|ps1|vbs)\b', "EMAIL": r'[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}', "MUTEX_HINT": r'(?:Global|Local)\\\\[A-Za-z0-9_{}.-]{4,}', "CMD_EXECUTION": r'(?:cmd\.exe|powershell|/bin/sh|/bin/bash|system\(|popen\()[^\n]{0,80}', } # Run strings (both ASCII and wide-char Unicode) r1 = subprocess.run(["strings","-n","6",target], capture_output=True, text=True) r2 = subprocess.run(["strings","-n","6","-e","l",target], capture_output=True, text=True) all_strings = (r1.stdout + r2.stdout).splitlines() # Classify findings = {k: set() for k in PATTERNS} for line in all_strings: for cat, pat in PATTERNS.items(): for m in re.findall(pat, line): findings[cat].add(m) # Print report print(f"\n{'='*60}") print(f" STRING TRIAGE REPORT") print(f" Sample : {sample_name}") print(f" Date : {datetime.now().strftime('%Y-%m-%d %H:%M')}") print(f" Strings: {len(all_strings)} total extracted") print(f"{'='*60}") has_findings = False for cat, matches in findings.items(): if matches: has_findings = True print(f"\n[{cat}] ({len(matches)} found)") for m in sorted(matches): print(f" {m}") if not has_findings: print("\nNo IoC patterns matched. Consider FLOSS for obfuscated strings.") print(f"\n{'='*60}\n") EOF # Run on the unpacked sample remnux@analysis:~/malware-labs$ python3 string_triage.py samples/sample_unpacked.elf ============================================================ STRING TRIAGE REPORT Sample : sample_unpacked.elf Strings: 247 total extracted ============================================================ [EXE_OR_DLL] (3 found) ls.exe ld-linux.so libc.so [WINDOWS_PATH] (1 found) C:/MinGW/...
In real malware, URL, IPv4_ADDRESS, and REGISTRY_KEY categories would light up. Run this script on any binary as your first triage step — it takes under 5 seconds and immediately surfaces every pattern worth investigating. Save the output to reports/triage_[samplename].txt for every sample you analyse.
TASK 2 Use FLOSS to find strings that strings missed

Create a small binary with a hidden stack string (constructed one byte at a time — invisible to strings) and demonstrate that FLOSS finds it.

remnux@analysis:~/malware-labs$
# Create a C program with a stack string (constructed at runtime, not stored as-is) remnux@analysis:~/malware-labs$ cat > /tmp/stack_string_demo.c << 'EOF' #include <stdio.h> int main() { /* Stack string - built character by character */ char c2[25]; c2[0]='h'; c2[1]='t'; c2[2]='t'; c2[3]='p'; c2[4]=':'; c2[5]='/'; c2[6]='/'; c2[7]='e'; c2[8]='v'; c2[9]='i'; c2[10]='l'; c2[11]='.'; c2[12]='c'; c2[13]='2'; c2[14]='.'; c2[15]='r'; c2[16]='u'; c2[17]='/'; c2[18]='g'; c2[19]='a'; c2[20]='t'; c2[21]='e'; c2[22]='\0'; printf("Connecting to: %s\n", c2); return 0; } EOF remnux@analysis:~/malware-labs$ gcc -o samples/stack_demo /tmp/stack_string_demo.c -O0 # strings CANNOT find "http://evil.c2.ru/gate" remnux@analysis:~/malware-labs$ strings -n 8 samples/stack_demo | grep evil [no output - strings missed it] # FLOSS FINDS IT through emulation remnux@analysis:~/malware-labs$ floss --no-static-strings samples/stack_demo 2>/dev/null FLOSS STACK STRINGS (constructed character by character) ======================================================== http://evil.c2.ru/gate
✓ What This Demonstrates
strings sees 0 matches for the C2 URL — it does not exist in the binary's data sections.
FLOSS emulates the function, watches what gets written to the stack, and reconstructs the string.
This is exactly why professional analysts run FLOSS on every sample after strings.
LAB C2

Write, Test, and Refine a YARA Rule

yara · python3 YARA · Detection Engineering · False Positive Testing ~40 min
Lab Brief

YARA rules are how IoCs become detections. In this lab you will write a rule from scratch, intentionally cause false positives to understand why they happen, then fix it using condition logic. You will finish with a rule that fires on your target sample and produces zero false positives against a clean binary corpus.

Write a YARA rule that matches a specific sample
Deliberately cause false positives and understand why they happen
Fix the rule using compound conditions
Test against a clean corpus to verify zero false positives
TASK 1 Write your first YARA rule and test it
remnux@analysis:~/malware-labs$
# First - what unique strings does our stack_demo sample contain? remnux@analysis:~/malware-labs$ strings samples/stack_demo | grep -v "^.\{1,4\}$" Connecting to: %s /lib/x86_64-linux-gnu/libc.so.6 printf ... # Write a YARA rule targeting the stack_demo binary (deliberately weak) remnux@analysis:~/malware-labs$ cat > yara/rule_stackdemo_v1.yar << 'EOF' rule Demo_StackString_C2_v1 { meta: author = "Your Name" date = "2025-01-15" description = "Detects stack_demo binary - stack-constructed C2 URL" strings: $c2_hint = "Connecting to: %s" // too generic - will cause FPs $lib = "libc.so.6" // also too generic $printf = "printf" // matches EVERY binary that uses printf condition: any of them // INTENTIONALLY weak - for demonstration } EOF # Test on our sample - should match remnux@analysis:~/malware-labs$ yara yara/rule_stackdemo_v1.yar samples/stack_demo Demo_StackString_C2_v1 samples/stack_demo # Test on clean binaries - observe the FLOOD of false positives remnux@analysis:~/malware-labs$ yara -r yara/rule_stackdemo_v1.yar /bin/ 2>/dev/null | wc -l 47 # 47 false positives in /bin/ alone - this rule is unusable
TASK 2 Fix the rule — add specificity until false positives reach zero
remnux@analysis:~/malware-labs$
# Find the byte pattern for the format string remnux@analysis:~/malware-labs$ python3 -c "print('Connecting to: %s\\n'.encode().hex())" 436f6e6e656374696e6720746f3a2025730a # Get the SHA256 of the sample remnux@analysis:~/malware-labs$ sha256sum samples/stack_demo abc123... samples/stack_demo # Write the improved rule remnux@analysis:~/malware-labs$ cat > yara/rule_stackdemo_v2.yar << 'EOF' rule Demo_StackString_C2_v2 { meta: author = "Your Name" date = "2025-01-15" description = "Detects stack_demo binary - improved, low FP" sha256 = "abc123..." strings: // Exact byte sequence - the format string with its newline $format_str = { 43 6f 6e 6e 65 63 74 69 6e 67 20 74 6f 3a 20 25 73 0a } // File is an ELF (starts with \x7fELF) - not a PE $elf_magic = { 7F 45 4C 46 } condition: // Must start with ELF magic AND have our specific format string $elf_magic at 0 and $format_str and filesize < 50KB } EOF # Test on our target remnux@analysis:~/malware-labs$ yara -s yara/rule_stackdemo_v2.yar samples/stack_demo Demo_StackString_C2_v2 samples/stack_demo 0x2008:$format_str: Connecting to: %s 0x0:$elf_magic: 7f ELF # Test against /bin/ for false positives remnux@analysis:~/malware-labs$ yara -r yara/rule_stackdemo_v2.yar /bin/ 2>/dev/null | wc -l 0 # Zero false positives - rule is deployable
✓ Key Lesson
V1 used any of them with generic strings → 47 false positives.
V2 uses AND logic with a specific byte sequence + file magic + size constraint → 0 false positives.
The tighter the condition combination, the more specific the rule, the lower the false positive rate.
Your Turn — Independent Task
  1. Write a YARA rule for the encoded config file (samples/encoded_config.bin) using the first 8 bytes of the encoded blob as a hex pattern (xxd samples/encoded_config.bin | head -1 to view them). Add a condition that the file must be less than 200 bytes. Test it. Does it match the config file and nothing in /bin/?
  2. Add a YARA module condition: use math.entropy(0, filesize) > 7.0 to write a rule that specifically matches high-entropy files. Run it against your samples directory. Which files match?
Q2 Hint — Rule structure:
import "math" (at the very top of the .yar file, before the rule block)
In the condition: math.entropy(0, filesize) > 7.0
This will match sample_packed.elf and encoded_config.bin but NOT the clean binary or the stack_demo. The math module lets you express statistical properties of files as YARA conditions — extremely powerful for detecting packed or encrypted content without needing specific byte signatures.
LAB C3

Capstone — Full IoC Report & Detection Package

all tools IoC Report · YARA · MITRE ATT&CK · Deliverable ~50 min
Lab Brief

This is the capstone lab. You will analyse a self-built simulated dropper binary from scratch — applying every skill from Labs A through C — and produce a professional deliverable: a complete IoC report, a tested YARA rule, and a MITRE ATT&CK mapping. This is the exact output a real analyst hands to a SOC team after completing an investigation.

Build a simulated dropper with realistic characteristics
Run the full analysis pipeline (unpack → triage → decode)
Produce a structured IoC report in standard format
Write a tested YARA rule with metadata
Map findings to MITRE ATT&CK technique IDs
TASK 1 Build the capstone dropper sample
remnux@analysis:~/malware-labs$
# Build a simulated dropper with realistic IoC characteristics remnux@analysis:~/malware-labs$ cat > /tmp/dropper_sim.c << 'EOF' #include <stdio.h> /* Simulated dropper - educational sample, no malicious functionality */ /* XOR-encoded C2 config - key 0x5A, decodes to: http://upd.cdn-x.ru/beacon */ unsigned char encoded_c2[] = { 0x32,0x27,0x20,0x1d,0x27,0x27,0x1d,0x36, 0x36,0x32,0x1e,0x30,0x27,0x33,0x1e,0x30, 0x34,0x36,0x1e,0x3e,0x3e,0x3e,0x3e }; /* Stack-constructed persistence path */ void get_persist_path(char* buf) { buf[0]='C'; buf[1]=':'; buf[2]='\\\\'; buf[3]='P'; buf[4]='r'; buf[5]='o'; buf[6]='g'; buf[7]='r'; buf[8]='a'; buf[9]='m'; buf[10]='D'; buf[11]='a'; buf[12]='t'; buf[13]='a'; buf[14]='\\\\'; buf[15]='s'; buf[16]='v'; buf[17]='c'; buf[18]='h'; buf[19]='o'; buf[20]='s'; buf[21]='t'; buf[22]='3'; buf[23]='2'; buf[24]='.'; buf[25]='e'; buf[26]='x'; buf[27]='e'; buf[28]='\0'; } int main() { const char* mutex = "Global\\\\SvcHostMgr_x64_v3"; const char* reg = "HKCU\\\\SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Run\\\\SvcHelper"; const char* ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) svchost/3.2"; char path[64]; get_persist_path(path); printf("[SIM] Mutex: %s\n", mutex); printf("[SIM] RegKey: %s\n", reg); printf("[SIM] Drop: %s\n", path); printf("[SIM] UA: %s\n", ua); return 0; } EOF remnux@analysis:~/malware-labs$ gcc -O0 -o samples/dropper_sim /tmp/dropper_sim.c # Pack it with UPX to simulate real-world packed delivery remnux@analysis:~/malware-labs$ cp samples/dropper_sim samples/dropper_packed remnux@analysis:~/malware-labs$ upx -9 samples/dropper_packed Packed 1 file. # Record the hash of the PACKED version - this is what would arrive on a victim machine remnux@analysis:~/malware-labs$ sha256sum samples/dropper_packed | tee reports/capstone_packed_hash.txt
TASK 2 Run the full analysis pipeline
remnux@analysis:~/malware-labs$
# STEP 1: Confirm packing remnux@analysis:~/malware-labs$ python3 entropy.py samples/dropper_packed samples/dropper_packed: 7.5921 bits/byte [PACKED CONFIRMED] # STEP 2: Identify packer remnux@analysis:~/malware-labs$ strings -n 4 samples/dropper_packed | grep -i upx UPX0 UPX1 UPX! [UPX identified] # STEP 3: Unpack remnux@analysis:~/malware-labs$ cp samples/dropper_packed samples/dropper_unpacked remnux@analysis:~/malware-labs$ upx -d samples/dropper_unpacked Unpacked 1 file. # STEP 4: String triage remnux@analysis:~/malware-labs$ python3 string_triage.py samples/dropper_unpacked | tee reports/capstone_triage.txt [REGISTRY_KEY] (1 found) HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\SvcHelper [MUTEX_HINT] (1 found) Global\SvcHostMgr_x64_v3 [EXE_OR_DLL] (1 found) svchost32.exe # STEP 5: Extract the XOR-encoded C2 config remnux@analysis:~/malware-labs$ python3 -c " encoded = bytes([0x32,0x27,0x20,0x1d,0x27,0x27,0x1d,0x36, 0x36,0x32,0x1e,0x30,0x27,0x33,0x1e,0x30, 0x34,0x36,0x1e,0x3e,0x3e,0x3e,0x3e]) key = 0x5A decoded = bytes([b ^ key for b in encoded]) print('Decoded C2 config:', decoded) " Decoded C2 config: b'http://upd.cdn-x.ru/beacon' # STEP 6: Run capability scanner remnux@analysis:~/malware-labs$ python3 capability_scan.py samples/dropper_unpacked # STEP 7: Run FLOSS for stack-constructed strings remnux@analysis:~/malware-labs$ floss --no-static-strings samples/dropper_unpacked 2>/dev/null FLOSS STACK STRINGS: C:\ProgramData\svchost32.exe
TASK 3 Write the final IoC report

Fill in the template below with real values from your analysis. Every field should contain something you actually found in the previous steps — no placeholders in the final submission.

remnux@analysis:~/malware-labs$
remnux@analysis:~/malware-labs$ cat > reports/capstone_ioc_report.txt << 'EOF' ================================================================== MALWARE ANALYSIS - IoC REPORT ================================================================== Sample : dropper_packed Date : [today's date] Analyst : [your name] TLP : TLP:WHITE (safe to share publicly) ================================================================== === HASHES ======================================================= SHA256 (packed) : [fill in from Task 1] SHA256 (unpacked): [fill in: sha256sum samples/dropper_unpacked] MD5 (packed) : [fill in: md5sum samples/dropper_packed] === PACKER ======================================================== Packer : UPX 4.0.1 Evidence : UPX section names in binary + upx -d successful Entropy : [fill in pre-unpack entropy] === NETWORK IoCs ================================================== C2 URL : http://upd.cdn-x.ru/beacon C2 Domain: upd.cdn-x.ru Source : XOR-encoded config, key=0x5A, decoded manually User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) svchost/3.2 === HOST IoCs - FILES ============================================= Drop path: C:\ProgramData\svchost32.exe Source : Stack-constructed string (found by FLOSS, missed by strings) === HOST IoCs - REGISTRY ========================================== Persistence key: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\SvcHelper Purpose : Autorun on user logon Source : Direct string in binary === HOST IoCs - PROCESS =========================================== Mutex: Global\SvcHostMgr_x64_v3 Note : Mutex prevents duplicate instances - durable IoC === MITRE ATT&CK MAPPING ========================================== T1027 Obfuscated Files or Information (XOR encoding of C2) T1027.002 Software Packing (UPX packing) T1547.001 Boot or Logon Autostart: Registry Run Keys T1071.001 Application Layer Protocol: Web Protocols (HTTP C2) T1036 Masquerading (svchost32.exe mimicking svchost.exe) === ANALYST NOTES ================================================= [Write 2-3 sentences about what this binary does, how it persists, and what a SOC analyst should look for in SIEM/firewall logs] EOF
TASK 4 Write and test the final YARA detection rule
remnux@analysis:~/malware-labs$
remnux@analysis:~/malware-labs$ cat > yara/dropper_sim_final.yar << 'EOF' import "math" rule Dropper_SvcHostMgr_2025 { meta: author = "[your name]" date = "[today]" description = "Detects SvcHostMgr dropper - XOR C2, UPX packed, Run key persistence" sha256_packed = "[hash of dropper_packed]" sha256_unpacked = "[hash of dropper_unpacked]" mitre_t1027 = "Obfuscated Files - XOR + UPX" mitre_t1547 = "Registry Run Key persistence" tlp = "TLP:WHITE" strings: // Mutex name - durable IoC, requires code rewrite to evade $mutex = "SvcHostMgr_x64_v3" // Registry key path - specific enough on its own $reg_key = "Run\\\\SvcHelper" // User-agent string - custom, not from a known browser $ua = "svchost/3.2" // Encoded C2 blob - first 8 bytes of the XOR-encoded config $enc_blob = { 32 27 20 1d 27 27 1d 36 } condition: // Match unpacked: mutex + registry key together ($mutex and $reg_key) // OR match packed: encoded blob + high overall entropy or ($enc_blob and math.entropy(0, filesize) > 7.0) // OR match by user-agent + registry or ($ua and $reg_key) } EOF # Test against both packed and unpacked - both should match remnux@analysis:~/malware-labs$ yara -s yara/dropper_sim_final.yar samples/dropper_packed samples/dropper_unpacked Dropper_SvcHostMgr_2025 samples/dropper_packed 0x1800:$enc_blob: 32 27 20 1d ... Dropper_SvcHostMgr_2025 samples/dropper_unpacked 0x2080:$mutex: SvcHostMgr_x64_v3 0x2100:$reg_key: Run\SvcHelper # False positive check remnux@analysis:~/malware-labs$ yara -r yara/dropper_sim_final.yar /bin/ /usr/bin/ 2>/dev/null [no output - zero false positives] # Copy final deliverables to output folder remnux@analysis:~/malware-labs$ cp reports/capstone_ioc_report.txt yara/dropper_sim_final.yar output/ remnux@analysis:~/malware-labs$ ls output/ capstone_ioc_report.txt dropper_sim_final.yar sample_clean.elf.png sample_packed.elf.png
✓ Capstone Complete — What You Have Produced
IoC Report — a SOC-ready document listing all network, host, and behavioural indicators
YARA Rule — tested, zero false positives, matches both packed and unpacked versions
MITRE ATT&CK mapping — technique IDs linking the sample to the broader adversary kill chain
These three artifacts are what a real malware analyst delivers at the end of an engagement.

All Labs Complete

You have worked through the full malware analysis workflow — detecting packing, unpacking, writing binary parsers, decoding XOR configs, navigating Ghidra, building a capability classifier, and producing a professional IoC report with a tested YARA rule. Every skill in this workbook maps directly to real analyst work.