Advanced · Lesson 7/7

Malware Analysis Basics — How Defenders Study Dangerous Code Safely

44 min 1 quiz Graded labs: 3

What you'll learn

By the end of this lesson you will be able to:

  • Explain the legal and ethical frame of malware analysis and the handling rules defenders follow
  • Describe a safe analysis lab: isolated virtual machines, snapshots, a simulated network
  • Follow the defender's workflow: intake → static triage → behavior observation → documentation
  • Extract, defang and share indicators of compromise (IOCs), and judge which ones hurt an attacker most
  • Write simple YARA-style detection rules and compare before/after snapshots of a system
  • Map observed behavior to MITRE ATT&CK and structure a short analysis report

The biosafety lab 🧫

Scientists who study a dangerous virus do not work on a kitchen table. They use a sealed laboratory, protective equipment, strict procedures and decontamination. Their goal is never to spread the virus: it is to understand it well enough to build tests and vaccines.

Malware analysis is the same discipline for software. Defenders in security operations centers, incident-response teams, CERTs and security vendors study malicious programs to answer three questions: What does it do? How do we detect it? How do we clean up and protect others? The output is knowledge: indicators, detection rules and reports. Building or spreading malware is never part of the job.

About the samples in this lesson: every "sample" here is a harmless text, byte dump, log or file list written for teaching. No real malware appears anywhere, and you never need real malware to learn these skills.

The rules before the lab ⚖️

Malware work sits close to the line, so professionals follow explicit rules. They build on the rules of the road from the first lesson.

Work only in an authorized context. You analyze a sample because it is part of your job (an incident at your organization, a vendor's research process) or because a course gave you harmless practice material. Downloading live malware "to see what happens" is how people infect their own networks.

Never create, modify or spread malware. In most countries, writing or distributing malicious code without authorization and with the intent that it be used to commit offences is a crime. For example, Article 6 of the Council of Europe's Budapest Convention on Cybercrime asks its parties to criminalize producing or distributing programs designed to commit offences when this is done intentionally and without right, and it excludes authorized testing and protection of computer systems. Analysis is about understanding and defending, never about improving an attack.

Handle samples like hazardous material.

  • Keep them in password-protected archives (by long convention the password is infected), clearly labeled, never on shared drives or in email bodies
  • Rename files with a harmless extension (.bin) so nobody double-clicks them
  • Transfer them only into the isolated lab, and never back out: what leaves the lab is text, such as hashes, notes and reports
  • Do not upload a confidential file to a public scanning service: files submitted to services such as VirusTotal are shared with the security community. Search by hash instead
  • Defang indicators when you share them (hxxp://example[.]com), so nobody opens them by accident

Never "hack back". Even when a sample reveals the attacker's server, connecting to it, disrupting it or taking data from it is unauthorized access. Report what you found to your incident lead, the hosting provider or the authorities. And as always, this is general education, not legal advice: rules differ by country and by contract.

A safe analysis lab

A typical small lab, built from free tools:

 ┌─────────────────────── your host computer ───────────────────────┐
 │                                                                  │
 │   ┌───────────────────────┐   isolated     ┌──────────────────┐  │
 │   │ analysis VM           │   host-only    │ "fake internet"  │  │
 │   │ (Windows + FLARE-VM   │◀──network ────▶│ VM: INetSim or   │  │
 │   │  or Linux + REMnux)   │   no route out │ FakeNet-NG       │  │
 │   │ clean snapshot taken  │                │                  │  │
 │   └───────────────────────┘                └──────────────────┘  │
 │      no shared folders · no shared clipboard · revert after use  │
 └──────────────────────────────────────────────────────────────────┘
  • FLARE-VM (Windows) and REMnux (Linux) are free collections of analysis tools that turn a fresh VM into a workstation.
  • INetSim and FakeNet-NG pretend to be the internet: they answer DNS, HTTP and other requests locally, so you can see what a program tries to contact without letting it reach anything real.
  • Take a snapshot of the clean analysis VM before every session and revert afterwards.
  • Keep the VM software up to date, keep the lab network away from your home or company network, and treat the whole lab as contaminated.
  • Automated sandboxes such as CAPE run a sample in a disposable VM and produce a behavior report. They save time, but you still interpret the results yourself.

The workflow

  1. Intake. Record where the file came from, compute its hashes (MD5, SHA-1, SHA-256), store it safely. Search the hash in your organization's records and public reputation services.
  2. Static triage first. Everything from the earlier lessons, without running anything: file, strings, imports, sections and permissions, entropy (a hint of packing), embedded URLs or keys after decoding layers. Often this alone answers the main questions.
  3. Behavior observation. Only when needed, in the lab: take a baseline, run the sample, and record processes started, files created or changed, registry changes on Windows, and network activity. Tools: Process Monitor and Sysmon on Windows, strace and auditd on Linux, Wireshark for traffic, Regshot-style before/after comparisons.
  4. Document and share. Indicators, detection rules, an ATT&CK mapping and a short report that someone else can act on.

Static before dynamic is the golden rule: it is safer, cheaper, and tells you what to watch for when you do run the sample.

Indicators of compromise

An indicator of compromise (IOC) is an observable fact that suggests a system has met a particular threat: a file hash, an IP address, a domain or URL, a file path, a registry key, a mutex name, a distinctive string. Defenders search for IOCs in logs, block them at the edge, and share them so others can do the same.

Not all indicators are equally useful. David Bianco's Pyramid of Pain (2013) ranks them by how much it hurts the attacker when you detect them:

                 /\
                /  \        TTPs: tactics, techniques, procedures   (tough)
               /────\       tools                                   (challenging)
              /──────\      network and host artifacts              (annoying)
             /────────\     domain names                            (simple)
            /──────────\    IP addresses                            (easy)
           /────────────\   hash values                             (trivial)

Changing one byte changes a file's hash, and renting a new IP address is cheap, so those indicators are easy to evade. Detecting behavior (how a program persists, how it talks to its server) forces an attacker to change how they work, which is expensive. Collect the cheap indicators, but aim higher.

Defanging makes indicators safe to paste into tickets and chats: http becomes hxxp and dots become [.], so nothing turns into a clickable link. Refang before you feed them to a tool.

Safe example values. Documentation and training should never use real hosts. RFC 5737 reserves 192.0.2.0/24, 198.51.100.0/24 and 203.0.113.0/24 for documentation, and RFC 2606 reserves example.com, example.net, example.org and the top-level names .test, .example, .invalid and .localhost. Every indicator in this lesson uses them.

Sharing. Indicators are exchanged in standard formats such as STIX 2.1 (an OASIS standard), and labeled with the Traffic Light Protocol (TLP 2.0, by FIRST) to say who may receive them: TLP:RED, TLP:AMBER+STRICT, TLP:AMBER, TLP:GREEN or TLP:CLEAR.

Run it: one changed byte produces completely different hashes, which is why a hash identifies one exact file and why hash IOCs sit at the bottom of the Pyramid of Pain. Record SHA-256 in reports; MD5 and SHA-1 are still widely listed for lookups, but practical collisions exist for both, so never rely on them to prove two files are identical. Similarity hashes such as ssdeep and TLSH exist for finding near-duplicates.

import hashlib

sample = b"notes-sync teaching sample v1\n"    # a harmless stand-in for a file under analysis
tweaked = b"notes-sync teaching sample v2\n"   # the same file with a single byte changed

for name in ("md5", "sha1", "sha256"):
    a = hashlib.new(name, sample).hexdigest()
    b = hashlib.new(name, tweaked).hexdigest()
    same = sum(x == y for x, y in zip(a, b))
    print(f"{name:6} {a}")
    print(f"{'':6} {b}")
    print(f"{'':6} {same} of {len(a)} hex digits match, by pure chance\n")

Lab exercise 1

PythonGraded in Code Lab

Extract and defang indicators

Incident notes arrive as free text, sometimes already defanged by another analyst. Write the tool that pulls the indicators out, normalizes them and prints them safely.

Steps

  1. Refang the whole input: replace hxxp (any case) with http and [.] with ..
  2. Find the indicators:
    • ip: four dot-separated decimal numbers, each 0-255, not touching another digit or a dot followed by a digit (so 1.2.3.4.5 and 10.0.0.256 are not addresses, but a sentence may end with 192.0.2.1.)
    • url: http:// or https:// (any case) followed by non-space characters, with trailing ., ,, ;, :, ), ' or " removed
    • domain: the host of each URL, lowercase, without port, path, query or fragment (hosts that are IP numbers are not domains)
    • md5 / sha1 / sha256: a run of hex digits of length 32 / 40 / 64 that does not touch a letter, digit or underscore; print it lowercase
  3. Print each kind in the order ip, domain, url, md5, sha1, sha256, one <kind> <value> line per unique value. Sort IPs numerically (octet by octet), everything else as text. Defang IPs, domains and URLs: every . becomes [.] and a leading http becomes hxxp.
  4. Finish with total: <N> indicators.

Example

Input:

The teaching sample (sha256 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824)
contacted http://update.example.com/check?id=7 and then 203.0.113.42 on port 8443.

Output:

ip 203[.]0[.]113[.]42
domain update[.]example[.]com
url hxxp://update[.]example[.]com/check?id=7
sha256 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
total: 4 indicators
Visible tests (6)
  • The worked example
  • Already-defanged input is refanged first and printed defanged once
  • Look-alikes that are not addresses, and punctuation around URLs
  • Hashes are classified by length; near misses are ignored
  • Duplicates collapse and IPs sort numerically
  • Nothing to report
Example run
Input
The teaching sample (sha256 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824)
contacted http://update.example.com/check?id=7 and then 203.0.113.42 on port 8443.
Expected output
ip 203[.]0[.]113[.]42
domain update[.]example[.]com
url hxxp://update[.]example[.]com/check?id=7
sha256 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
total: 4 indicators
Starter code
import re
import sys

text = sys.stdin.read()

# TODO 1: refang: "hxxp" (any case) -> "http", "[.]" -> ".".

found = {"ip": set(), "domain": set(), "url": set(), "md5": set(), "sha1": set(), "sha256": set()}

# TODO 2: IPv4 addresses. A useful pattern: r"(?<![\d.])\d{1,3}(?:\.\d{1,3}){3}(?![\d])(?!\.\d)"
#         then keep only matches whose four parts are <= 255.
# TODO 3: URLs (r"https?://\S+", ignoring case), strip trailing punctuation, and add the host as a domain.

More lines in Code Lab: 14

Try in Code Lab

Hints and instant feedback are in Code Lab. Optional practice: it does not block finishing the lesson.

JavaScript and Python also run in your browser without an account; other languages use the secure server runner and need a free account.

Detection rules with YARA

Indicators describe one sample; a detection rule describes a family of files. The standard tool is YARA, an open-source pattern-matching engine created at VirusTotal and used across the security industry. A rule has a name, some strings (text or hex bytes) and a condition:

rule Notes_Updater_Config
{
    meta:
        description = "Teaching sample: notes-sync updater configuration"
    strings:
        $host  = "update.example.test"
        $agent = "TeachingAgent/1.0" nocase
        $key   = { DE AD BE EF }
    condition:
        2 of them
}

nocase ignores ASCII letter case, { ... } is a hex string, and the condition says how many strings must be present (any of them, all of them, 2 of them, or boolean logic such as $host and $key). Running yara -s rules.yar file prints every matching rule and, with -s, where each string was found.

Good rules are specific (strings that only this family has, never common words such as http or kernel32.dll), robust (several strings with N of them, so one change does not break the rule), and tested: run them against a collection of clean files to catch false positives before you deploy them. A packer's section names, such as UPX's UPX0 and UPX1, are a useful hint but not a verdict, because legitimate programs are packed too.

Lab exercise 2

JavaScript (Node.js)Graded in Code Lab

A tiny YARA

Implement the core of a YARA-style scanner: parse simple rules, scan sample files given as hex bytes, and report matches like yara -s.

Input: rules, a line ---, then samples.

  • A rule starts with rule NAME, then string lines $id = "text", $id = "text" nocase or $id = { hex bytes }, then condition: any of them, condition: all of them or condition: N of them. Text has no double quotes inside; lines may be indented.
  • A sample starts with sample NAME, followed by zero or more lines of hex bytes.

Matching: a text string matches its ASCII bytes exactly; with nocase, ASCII letters match in either case. A hex string matches those bytes exactly. A rule matches a sample when at least the required number of its strings occur somewhere in the sample (any = 1, all = every string).

Output: for each sample in order, for each matching rule in order, print <rule> <sample>, then one line per string of that rule that occurs, in the rule's order: 0x<offset of its first occurrence, lowercase hex>:$<id>. Print nothing for rules that do not match.

Example

Input:

rule Demo_Greeting
  $hello = "hello" nocase
  $tail = { 00 ff }
  condition: any of them
---
sample a.bin
48 45 4c 4c 4f 00 ff
sample b.bin
62 79 65

Output:

Demo_Greeting a.bin
0x0:$hello
0x5:$tail
Visible tests (5)
  • The worked example
  • Two rules and three teaching samples
  • Case sensitivity, and "3 of them" with only two present
  • Rule order for strings, first occurrence only, and an empty sample
  • A string longer than the sample never matches
Example run
Input
rule Demo_Greeting
  $hello = "hello" nocase
  $tail = { 00 ff }
  condition: any of them
---
sample a.bin
48 45 4c 4c 4f 00 ff
sample b.bin
62 79 65
Expected output
Demo_Greeting a.bin
0x0:$hello
0x5:$tail
Starter code
const lines = require("fs").readFileSync(0, "utf8").split("\n").map((l) => l.trim());

const rules = [];   // { name, strings: [{ id, bytes, nocase }], condition }
const samples = []; // { name, bytes }
let section = "rules";
for (const line of lines) {
  if (!line) continue;
  if (line === "---") { section = "samples"; continue; }
  if (section === "rules") {
    if (line.startsWith("rule ")) rules.push({ name: line.slice(5).trim(), strings: [], condition: "" });
    else if (line.startsWith("condition:")) rules[rules.length - 1].condition = line.slice(10).trim();
    else {

More lines in Code Lab: 24

Try in Code Lab

Hints and instant feedback are in Code Lab. Optional practice: it does not block finishing the lesson.

JavaScript and Python also run in your browser without an account; other languages use the secure server runner and need a free account.

Watching behavior: snapshots and ATT&CK

When static triage leaves questions open, you observe the sample in the lab. The simplest powerful technique is a before/after comparison: record the state of the file system (and on Windows the registry), run the sample, record the state again, and compare. Tools such as Regshot automate this for the registry; on Linux you can hash every file before and after.

Most changes are noise (logs, caches). What deserves attention:

  • Persistence locations, places that make a program start again after a reboot or login: on Windows the ...\CurrentVersion\Run registry keys and Startup folders; on Linux cron directories, systemd unit folders, XDG autostart entries (~/.config/autostart/) and shell start-up files such as ~/.bashrc
  • New executables in temporary or user-writable folders
  • Changes to security-relevant files, such as /etc/hosts
  • Network activity: DNS lookups and connections, captured by the fake-internet VM

MITRE ATT&CK gives these behaviors shared names. It is a public knowledge base of adversary behavior organized as tactics (the goal, such as Persistence, TA0003, or Command and Control, TA0011) and techniques (the method). For example, T1547.001 is Registry Run Keys / Startup Folder, T1053.003 is Scheduled Task/Job: Cron, T1071.001 is Application Layer Protocol: Web Protocols and T1027 is Obfuscated Files or Information. Mapping your observations to these IDs lets other defenders search their own data for the same behavior, which is exactly the top of the Pyramid of Pain.

The point of knowing these locations is to check them: during an investigation, on your own systems, and in your detection rules.

Lab exercise 3

GoGraded in Code Lab

Compare two snapshots

A teaching sample called notes-sync was run in the lab VM. Before and after, the analyst recorded every file (and registry value) with a short fingerprint. Write the comparison tool and flag changes in watched locations.

Input: three sections, each starting with a header line: [before] and [after] contain lines PATH FINGERPRINT; [watch] contains one path per line. Paths contain no spaces. Any section may be empty.

Rules

  • A path only in [after] was created (+), only in [before] was deleted (-), in both with different fingerprints was modified (~). Unchanged paths are not printed.
  • A path is watched when it equals a watch entry, or the watch entry ends with / or \ and the path starts with it (so /etc/hosts does not cover /etc/hosts.allow).

Output: the changed paths sorted by byte value (Go's sort.Strings), one per line: <mark> <path>, with [watched] (two spaces) appended for watched paths. Then:

summary: <c> created, <m> modified, <d> deleted, <w> in watched locations

Example

Input:

[before]
/home/lab/notes.txt 9f2c41aa
/etc/hosts 1a2b3c4d
/tmp/old.log 77e01c2b
[after]
/home/lab/notes.txt 9f2c41aa
/etc/hosts 5e6f7a8b
/home/lab/.config/autostart/notes-sync.desktop c0ffee11
[watch]
/home/lab/.config/autostart/
/etc/hosts

Output:

~ /etc/hosts  [watched]
+ /home/lab/.config/autostart/notes-sync.desktop  [watched]
- /tmp/old.log
summary: 1 created, 1 modified, 1 deleted, 2 in watched locations
Visible tests (6)
  • The worked example
  • A Windows run: registry Run key and a temporary file
  • Exact entries versus folder entries
  • Nothing changed (the order of lines does not matter)
  • Deleted and modified files in watched places count too
  • An empty before-snapshot and no watch list
Example run
Input
[before]
/home/lab/notes.txt 9f2c41aa
/etc/hosts 1a2b3c4d
/tmp/old.log 77e01c2b
[after]
/home/lab/notes.txt 9f2c41aa
/etc/hosts 5e6f7a8b
/home/lab/.config/autostart/notes-sync.desktop c0ffee11
[watch]
/home/lab/.config/autostart/
/etc/hosts
Expected output
~ /etc/hosts  [watched]
+ /home/lab/.config/autostart/notes-sync.desktop  [watched]
- /tmp/old.log
summary: 1 created, 1 modified, 1 deleted, 2 in watched locations
Starter code
package main

import (
	"bufio"
	"fmt"
	"os"
	"sort"
	"strings"
)

func main() {
	before := map[string]string{}

More lines in Code Lab: 35

Try in Code Lab

Hints and instant feedback are in Code Lab. Optional practice: it does not block finishing the lesson.

JavaScript and Python also run in your browser without an account; other languages use the secure server runner and need a free account.

Writing it down: the analysis report

Analysis that is not written down did not happen. A short report that a colleague can act on usually has these parts:

1. Summary        what it is, what it does, how confident you are   (3-5 sentences)
2. Sample         file names, size, type, MD5 / SHA-1 / SHA-256, where it came from
3. Static         format, notable strings, imports, packing, decoded configuration
4. Behavior       processes, files, registry, network, with timestamps from the lab run
5. Indicators     defanged, grouped by type, each with context
6. ATT&CK         technique IDs with the evidence for each
7. Detection      YARA rules, log searches, network signatures
8. Remediation    how to find affected machines and clean them
   Label          TLP marking, author, date, version

Write facts and assessments differently: "the sample creates notes-sync.desktop in ~/.config/autostart/" is a fact you observed; "this is probably meant to survive reboots" is an assessment, and it should say how confident you are. Never pad a report with indicators you did not verify, and remember that the report itself is shared material: defang it, label it with TLP and keep personal data out of it.

Quick quiz

Question 1 of 5

A colleague asks you to upload a suspicious attachment from a confidential customer email to a public scanning service. What is the better first step?

Question 2 of 5

According to the Pyramid of Pain, which detection forces an attacker to change the most?

Question 3 of 5

Why are indicators written as `hxxp://update.example[.]com` in reports?

Question 4 of 5

A YARA rule matches the UPX section names `UPX0` and `UPX1` in a file. What can you conclude?

Question 5 of 5

During a lab run, a sample's IOCs point to a live server on the internet. What should you do with that server?

Summary and what to practice next

  • Malware analysis is defensive work in an authorized context: never create or spread malware, never hack back, handle samples like hazardous material and share only text
  • The lab is isolated: VMs with snapshots, no shared folders or clipboard, a fake internet such as INetSim or FakeNet-NG
  • The workflow is intake → static triage → behavior observation → documentation, and static always comes first
  • IOCs are hashes, IPs, domains, URLs, paths and keys; defang them, use documentation ranges in examples, share them with STIX and TLP, and aim up the Pyramid of Pain
  • YARA rules describe families; before/after snapshots reveal persistence; ATT&CK gives behaviors shared names; a good report separates facts from assessments

Practice next: build a two-VM lab with REMnux or FLARE-VM and INetSim, then take and revert a snapshot until it is a habit. Run your snapshot tool on your own machine before and after installing a normal program, and see how much noise a legitimate installer makes. Write a YARA rule for the teaching sample of this lesson, test it against a folder of your own clean files, and fix every false positive. Then read a public analysis report from a CERT or security vendor and find each part of the report structure above. This is the last lesson of the track: take the rules of the law and ethics lesson into every analysis you do.

Practice

Reading is not learning until you run something. Pick one.

Graded lab exercises

Open an example in Code Lab