What would you like to learn?

Try PowerShell, networks, XSS, or Step 138

Browse the full curriculum →

Reversing

Step 225. Static Malware Analysis (Isolated Lab Required) — Reading the Insides Without Running It

Step 225Estimated practice · 5 hours

Level 3 — Real-World CTF & Advanced Offensive Skills | Difficulty ★★★★☆ | Estimated time: 5 hours

Prerequisites: Step 168 (malware structure and life cycle), Step 169 (encryption and detection evasion), Step 224 (obfuscation analysis strategies). You can use basic Linux shell commands.

⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime. This chapter additionally requires strict adherence to the "isolated lab rules" below.

  • What you need: a Linux lab (WSL or a dedicated VM, with gcc), a notepad. The analysis target is a harmless fake malware sample you build yourself.
  • Caution: today’s two absolute rules — ① never run a real malware sample; read it only (static analysis). ② handle real samples only inside a dedicated VM with the network physically cut off. Today you practice the procedure on a harmless self-made sample, so WSL is safe.

A malware analyst’s first weapon is not execution — it’s reading. Without opening or running the file, you infer "what is this file trying to do" from its hash, strings, headers, and import list, and extract IoCs (Indicators of Compromise). This is static analysis. Today you’ll establish the isolated-lab rules, build a harmless sample laced with suspicious strings, and complete an analysis report through the same five-step procedure used in the field.


1. Learning Objectives

By the end of this chapter, you will be able to:

  • Explain the three rules of an isolated lab (network isolation, snapshots, dedicated environment) together with their reasons
  • Perform the standard static-analysis procedure (file type → hash → strings → headers/libraries → IoCs) in order
  • Pick meaningful clues out of file, sha256sum, strings, readelf, and objdump/nm output
  • Classify discovered strings into IoCs (domains, paths, mutexes, etc.) and list them
  • Write an analysis report in the three-part structure: "suspected behavior + IoCs + detection ideas"

2. Background Knowledge — Today’s Tools and Concepts

Today’s Tools at a Glance

Category Details
Language/environment Linux lab (measured: WSL Ubuntu 24.04, gcc 13), one self-made C source file
Today’s commands file, sha256sum, md5sum, strings, strings -e l, readelf -h/-S, objdump -p, nm
Concepts needed Static vs dynamic analysis, IoC, hash fingerprint, import table, packer indicators, isolated lab
Today’s deliverable One static-analysis report for the fake malware sample (suspected behavior + IoC list + detection ideas)

2-1. Why Not Run It — The Case for Static Analysis

Wouldn’t "just running it" be faster? It’s dangerous. The moment it executes, infection, propagation, or destruction can occur, and a sample with anti-debugging (Step 219) will detect the analysis environment and play innocent. That’s why the first pass is always static analysis — reading the file purely as data.

Static analysis has clear limits too. If the sample is packed (Step 220) or its strings are encrypted (Step 224), little will be readable. In that case, the static-analysis findings themselves ("packed," "strings encrypted") become the signpost to the next stage (unpacking, dynamic analysis).

2-2. The Three Rules of an Isolated Lab

These are the rules for handling real malware samples (from research repositories like theZoo). Today you practice with a harmless sample, but the rules must be second nature.

  1. Network isolation: the analysis VM uses a host-only network, or the network adapter is disabled entirely. You physically remove any channel through which the sample could phone home to its C2.
  2. Snapshots: take a VM snapshot before analysis and roll back afterward. Even if the sample contaminates the system, you can revert.
  3. Dedicated environment: no personal files, accounts, or SSH keys on the analysis VM. Shared folders and clipboard sharing with the host stay off.

Real samples are usually distributed zipped with a password like infected — a convention that prevents antivirus from deleting them or users from running them by accident. Unzip only inside the isolated VM.

2-3. The Five-Step Static-Analysis Procedure

The first 30 minutes in the field flow roughly like this:

Step 1: file        — what is this file, really (ELF? PE? script?)
Step 2: sha256sum   — take the fingerprint. Look the hash up in public DBs (VirusTotal) → already known?
Step 3: strings     — every visible string. URLs, paths, error messages, mutexes are IoC candidates
Step 4: headers/libraries — readelf/objdump for structure and imports. Hunt for suspicious function combos
Step 5: synthesis   — write the report: suspected behavior + IoC list + detection ideas

2-4. What Makes a String "Suspicious"

Not every string is a clue. What an analyst grabs first:

Type Example Meaning
URL/domain http://.../beacon.php C2 server candidate — top-priority IoC
File path %APPDATA%...xxx.exe Self-copy/drop location
Registry key ...CurrentVersionRun Autorun registration (persistence)
Mutex Global... Anti-duplicate-execution marker — useful as a detection indicator
User-Agent Mozilla/5.0 ... Camouflage for HTTP traffic

Conversely, things like libc.so.6 or compiler strings are noise. In today’s practice you’ll separate the two.


3. Follow Along

3-1. Building the Target — a Harmless "Fake Malware"

You’ll build a sample that has suspicious strings embedded but whose only actual behavior is printing one line. You are not writing working malicious code — the goal is structural analysis, so you only need string material.

// notmalware.c — harmless "fake malware" for static-analysis practice
#include <stdio.h>

const char *c2_url   = "http://c2.evil-example.invalid/beacon.php";
const char *c2_url2  = "https://backup.evil-example.invalid:8443/gate";
const char *drop_path = "%APPDATA%\Microsoft\update\svchost_upd.exe";
const char *reg_key  = "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run";
const char *mutex    = "Global\NotRealMutex_7f3a";
const char *ua       = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)";

int main(void) {
    /* Actually does nothing — no network access, no file creation */
    puts("i am harmless.");
    return 0;
}

.invalid is a reserved domain that can never exist under internet standards (RFC 2606), so nothing happens even if this string leaks anywhere. Compile it — and then forget your role as the "problem author."

mkdir -p ~/lab/s225 && cd ~/lab/s225
gcc -o notmalware notmalware.c
file notmalware
./notmalware        # a single run to confirm harmlessness — absolutely forbidden for a real sample
notmalware: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=39b63c265b0d7d02e280c89f353a195fdc5b4362, for GNU/Linux 3.2.0, not stripped
i am harmless.

(Measured 2026-09-09 on WSL Ubuntu 24.04, gcc 13.3.0. The BuildID and hashes will differ in your environment — that’s normal.)

3-2. Steps 1–2 — File Type and Fingerprint

Read the file result first: ELF 64-bit, x86-64, dynamically linked, not stripped. The last part matters — it means the symbols (function/variable names) are intact, which makes analysis easy. Real-world samples are usually stripped, so you won’t get this luxury.

Next, take the fingerprint.

sha256sum notmalware
md5sum notmalware
9e4d3431d938711bce3ddef1979250f509da48c8abfc18f6667af404c27b6961  notmalware
5159148affcb1772fe80c0e414711490  notmalware

How to read the output: this hash is the file’s ID card — it goes on the first line of the report. In the field, you’d search this SHA-256 on VirusTotal to see "is it a known sample, and how many engines flag it" (today, screen example only — no external lookups):

# Screen example — the typical shape of a VirusTotal hash search result
SHA-256: 9e4d3431...   detections: 0/70   first seen: —   (unregistered sample)

If it’s unregistered, you proceed under "possibly new or a variant"; if it’s registered, you read the existing reports first — there’s no reason to re-dissect a sample someone already dissected.

3-3. Step 3 — strings, the Flower of Analysis

strings notmalware | grep -vE "^(__|_|[.]|GCC|GNU|GLIBC)" | head -40
strings -e l notmalware | head    # UTF-16LE strings (especially important for Windows samples)
/lib64/ld-linux-x86-64.so.2
puts
libc.so.6
http://c2.evil-example.invalid/beacon.php
https://backup.evil-example.invalid:8443/gate
%APPDATA%Microsoftupdatesvchost_upd.exe
HKEY_CURRENT_USERSoftwareMicrosoftWindowsCurrentVersionRun
GlobalNotRealMutex_7f3a
Mozilla/5.0 (Windows NT 10.0; Win64; x64)
i am harmless.
notmalware.c
puts@GLIBC_2.2.5
...

(This sample contains no UTF-16LE strings, so the second command produced empty output — measured.)

How to read the output: between the noise (ld-linux, libc, compiler traces), every type from the table in 2-4 appears. At this point, 80% of the analysis is already done. Two C2 candidates, a drop path, a persistence registry key, a mutex, a camouflage User-Agent — you can estimate this file as "a family that sends HTTP beacons and registers itself in autorun."

What if almost no strings had shown up here? That itself is a result — a sign of packing or string encryption (the array form from Step 224), and your next move is unpacking (Step 220) or dynamic analysis.

3-4. Step 4 — Headers, Libraries, Imports

readelf -h notmalware | grep -E "Class|Type|Machine|Entry"
objdump -p notmalware | grep NEEDED
nm -D notmalware
  Class:                             ELF64
  Type:                              DYN (Position-Independent Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Entry point address:               0x1060
  NEEDED               libc.so.6
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
                 w __cxa_finalize@GLIBC_2.2.5
                 w __gmon_start__
                 U __libc_start_main@GLIBC_2.34
                 U puts@GLIBC_2.2.5

How to read the output: the imports (dynamic symbols in ELF; the Import Table in Windows PE) are this file’s "list of things it can do." This sample has exactly one external function, puts — the strings are suspicious, but the header confirms it has no actual capability. In real malware, names like socket, connect, CreateRemoteThread, or URLDownloadToFile get caught right here. Remember it this way: "strings create suspicion; imports create certainty."

For a Windows PE sample, you’d run the same step with objdump -p sample.exe | grep "DLL Name" and DIE (Detect It Easy), which also detects packers.

3-5. Step 5 — Writing the Report

Organize what you collected into the three-part structure, and the analysis is done.

[Sample info]   notmalware | ELF 64-bit x86-64 | SHA-256 9e4d34... | not stripped
[Suspected behavior]  sends HTTP beacons (2 C2s, backup channel on port 8443),
                achieves persistence via autorun registration, prevents duplicate execution with a mutex.
                However, imports contain only puts — this build has no real communication capability (training sample)
[IoCs]          domains: c2.evil-example.invalid, backup.evil-example.invalid:8443
                path: %APPDATA%Microsoftupdatesvchost_upd.exe
                registry: HKCU...CurrentVersionRun
                mutex: GlobalNotRealMutex_7f3a
[Detection ideas] signature the mutex name and C2 domains; behaviorally monitor Run-key changes

How to read it: explicitly marking things as "suspected" is the report’s integrity. Asserting capabilities from strings alone gets it wrong — as today’s sample shows, strings and actual capability (imports) can disagree. The habit of cross-checking the two is today’s core deliverable.


4. Missions & Exercises

Mission — Complete One Analysis Report on a Modified Sample

  1. Modify the sample from 3-1: change the C2 domain, drop path, and mutex name to your own values (the domain must keep .invalid), and add one more string — for example, one shaped like a task-scheduler command
  2. Looking only at the compiled binary (cover up the source), perform the procedure from 3-2 through 3-4
  3. Complete a report in the 3-5 format — but in "suspected behavior," attach the evidence in parentheses (which string/import each estimate came from)
  4. On the last line, write in two sentences: "If this sample had been packed, what signs would have told me?"

Exercises

Exercise 1. Explain the difference between static and dynamic analysis in terms of "whether it executes" and "what information you get."

Exercise 2. List the three isolated-lab rules, and write one line each on what accident each prevents.

Exercise 3. In 3-3, the strings -e l output was empty. What does that tell you about this sample, and for what kind of sample does this option become important?

Exercise 4. If a sample’s strings are nothing but short meaningless strings and its imports are only LoadLibrary and GetProcAddress, what do you suspect, and what do you do next?


Answers & completion criteria · expand/collapse

5. Model Answers & Completion Criteria

Mission Model Answer

The "suspected behavior" part of the report reads like this (phrase it in your own words):

[Suspected behavior]
- Aims for automatic execution at boot (evidence: string "...CurrentVersionRun")
- Likely sends periodic beacons to a C2 (evidence: 2 domains + User-Agent string)
- However, nm -D shows only puts as an external function → no communication code in this build (evidence: imports)

Model answer for the "if packed" item: "If strings output is abnormally sparse, section names show packer traces like UPX0/UPX1, and entropy measures high, I suspect packing. I confirm with DIE, and the response moves on to Step 220’s manual unpacking."

How to verify: ① does every estimate carry its evidence in parentheses? ② are the IoCs classified by type (domain/path/registry/mutex)? ③ is the hash at the top of the report? ④ did you cross-check "strings ≠ actual capability" using the imports?

Exercise Answers

Answer 1. Static analysis reads the file as data without executing it — you get clues about "what it intends to do": hashes, strings, headers, imports. Dynamic analysis executes it in a sandbox and observes behavior — you get what it "actually did" (files created, packets sent). The field runs static → dynamic in that order, using dynamic work to get past static’s limits (packing, encrypted strings).

Answer 2. ① Network isolation — prevents the sample from talking to its C2 or fetching additional payloads. ② Snapshots — let you roll back even if the system gets contaminated mid-analysis. ③ Dedicated environment — ensures there is no personal data or credentials to steal in the first place, and blocks propagation to the host via shared folders/clipboard.

Answer 3. It means there are no UTF-16LE strings — every string in this sample is ASCII. In Windows PE samples, strings are often stored as UTF-16LE (registry paths, filenames, etc.), so plain strings alone shows you only half. That’s why PE analysis habitually runs -e l alongside.

Answer 4. A strong sign of packing/runtime loading — the LoadLibrary+GetProcAddress combo means "it resolves the functions it needs dynamically at runtime," so the static import list has been deliberately emptied. Acknowledge the limit of static analysis; the next moves are packer identification (DIE) → unpacking (Step 220) or sandbox behavior analysis. The combo itself is also a detection idea that belongs in the report.

Completion Criteria Checklist

  • [ ] I can state the three isolated-lab rules (network isolation, snapshots, dedicated environment) with their reasons
  • [ ] I performed the five static-analysis steps in order
  • [ ] I read analysis-difficulty clues like "not stripped" in the file output
  • [ ] I separated noise from IoC candidates in the strings output
  • [ ] I cross-checked "actual capability" via imports/dynamic symbols
  • [ ] I built an IoC list classified by type
  • [ ] Mission: I completed one report of suspected behavior + IoCs + detection ideas

6. Common Pitfalls & Fixes

Wall 1. strings dumps too much to read

Symptom: hundreds of lines pour out.
Cause: most of it is noise strings inserted by the compiler and libraries.
Fix: you can filter noise with grep -v as in 3-3, but in the field you do the opposite — search only for suspicious patterns: strings file | grep -Ei "http|.exe|hkey|mutex|cmd|powershell". For Windows samples, always run strings -e l alongside.

Wall 2. gcc throws -related errors when compiling

Symptom: warning: unknown escape sequence: 'M' or error: incomplete universal character name u (measured 2026-09-09).
Cause: in C strings, is the escape character, so a raw in a path gets interpreted as the special characters M, u.
Fix: double every backslash in paths (\). Check that the 3-1 source does this.

Wall 3. I get nm: notmalware: no symbols

Symptom: nm can’t show any symbols.
Cause: the binary is stripped (reproducible by running strip notmalware or building with -s — verifiable hands-on).
Fix: this is the normal state of real-world samples. nm -D (dynamic symbols) survives stripping, so read imports there and move to disassembly for the rest. Also check that the file output changed to "stripped."

Wall 4. Analyzing while looking at the source — the "practice" doesn’t work

Symptom: since you know the answer, the strings output isn’t interesting.
Cause: the same person set the problem and solved it.
Fix: do it a day later, or randomize the conditions when building the modified sample (string order, names, count). If that still doesn’t work, exchanging samples with a colleague is closest to the real job.

Wall 5. The temptation to run a real sample "just once"

Symptom: "it’s a VM anyway," and you double-click.
Cause: execution accidents keep happening for real — in VMs that aren’t host-only, VMs without snapshots, VMs with shared folders enabled. Some malware even uses VM-escape vulnerabilities.
Fix: if execution is needed, that’s dynamic analysis, not static — and it comes after dedicated sandbox procedures (network simulation, snapshot verification) are in place. It’s outside this chapter’s scope.


7. Summary

Today’s Concepts

Concept One-line explanation
Static analysis Reading a file as data without executing it — always the first pass
Dynamic analysis Watching behavior by executing in a sandbox — when static hits its limits
IoC Indicator of Compromise — domains, hashes, paths, mutexes, and other clues for detection and tracking
Isolated lab Network isolation + snapshots + dedicated environment — the minimum safety gear for handling samples
Import table The list of functions a file requests from outside — the list of its "actual capabilities"
stripped State with symbols removed — the default for real-world samples; raises analysis difficulty

Today’s Commands

Command What it does
file target Check file type, architecture, stripped status
sha256sum target Take the fingerprint — the report’s ID card, for public-DB lookup
strings target Extract ASCII strings — the IoC goldmine
strings -e l target Extract UTF-16LE strings — mandatory companion for Windows samples
readelf -h target ELF header — type, architecture, entry point
objdump -p target | grep NEEDED List linked libraries
nm -D target Dynamic symbols — the ELF equivalent of imports

An Instinct More Important Than Commands

Static analysis is "reading," but the goal is not a book report — it’s grounds for judgment. Strings create suspicion, imports create certainty, and when the two disagree (as in today’s sample), the disagreement itself is the report’s content. And the premise beneath all of this is the isolated lab — only an analyst who follows procedure gets to keep analyzing tomorrow. When you meet an unreadable sample (packed, encrypted), don’t force it: record "why it won’t read" and hand it to the next stage. That hand-off judgment is analysis too.


Once every box is checked, Step 225 is complete.

ONE STEP FURTHER

Finished this lesson?

Check the completion criteria, then mark your progress.

Something wrong with this page or a link? Let us know.
Next