What Is Malware Analysis?
HuntRule Team · · 8 min read

On this page
Four things people call malware analysis
sha256sum sample.bin is minute zero. What you do in minute one splits into four methods, and each one has a wall you hit.
Static triage reads the file without running it. Hashes, PE header, sections, imports, embedded strings, resources, certificate. It stops the moment the file is packed. A dropper with a 7.9 entropy .text section and an import table containing LoadLibraryA and GetProcAddress and nothing else has told you it is packed and nothing more.
Dynamic analysis runs the sample and watches. Procmon for file and registry, Wireshark for the wire, System Informer for handles and memory regions. It stops when the sample refuses to run: missing C2, wrong locale, expired campaign, a GetTickCount delta check that decides your box is a sandbox.
Code analysis is Ghidra or IDA plus x64dbg on the unpacked image. It answers questions the other two cannot: what the config decryption routine actually does, which byte in the beacon is the campaign ID. It costs hours per sample and does not scale to a queue.
Automated sandboxing (CAPEv2, Tria.ge, any.run) gives you a report in three minutes. It stops at the sample that fingerprints the VM, and at the sample whose interesting branch only fires on the second run.
You will use all four. The order is usually static, sandbox, dynamic, then code analysis only if the first three left a question worth hours.
The lab
Nested virtualization off, host-only networking, no shared folders, no clipboard sharing. The VM never touches the corporate LAN and it is never domain-joined. A domain-joined analysis box hands a credential-stealing sample a real Kerberos ticket and a real domain to enumerate, and gives ransomware an SMB path to walk.
Snapshot before every detonation. Not after installing tools, before every single run. Reverting is your undo button and you will use it four times an hour.
Fake internet goes on a second VM. INetSim or FakeNet-NG answers DNS, HTTP, HTTPS, SMTP and IRC so the sample believes it has connectivity and shows you the beacon it would have sent. Without it you get a sample that resolves nothing, exits, and teaches you nothing.

Triage workflow
Identify the file type before anything else. file gets it wrong on some .NET and packed samples, so confirm with the PE header.
file sample.bin
sha256sum sample.bin
strings -n 8 sample.bin | sort -u > strings_ascii.txt
floss -n 8 sample.bin > strings_floss.txt
capa -vv sample.bin > capa.txtFLOSS recovers stack and tightly encoded strings that plain strings misses. CAPA maps the binary to capabilities and prints ATT&CK IDs next to them, which is where a lot of your detection ideas come from.
Section entropy tells you whether static analysis is worth continuing.
python3 -c "import pefile; pe=pefile.PE('sample.bin'); [print(s.Name.decode().rstrip(chr(0)), round(s.get_entropy(),2), hex(s.SizeOfRawData)) for s in pe.sections]"A .text section at 7.8 and above, or a section named UPX0 with SizeOfRawData of zero, means the code you want is not on disk yet. Detect It Easy will name the packer if it is a known one. If it is custom, you unpack it yourself.
What you hit in the first hour
Packing (T1027.002). UPX unpacks with upx -d. Everything else means running to the OEP: set a hardware breakpoint on VirtualAlloc in x64dbg, step until the stub writes and jumps into the new region, then dump with Scylla and fix the IAT.
Anti-debug (T1622). IsDebuggerPresent is the beginner version and is a single byte in the PEB. The one you actually meet is a direct read of BeingDebugged at PEB+0x02 or an NtQueryInformationProcess call with ProcessDebugPort (class 7). ScyllaHide handles most of both. It does not handle a sample that checks its own code for 0xCC bytes.
Timing and sandbox checks (T1497, T1497.003). Two GetTickCount calls around a sleep, comparing the delta against the requested sleep, catches sandboxes that patch Sleep. Environment fingerprinting shows up as GetSystemInfo (core count), GlobalMemoryStatusEx (RAM under 4GB), and registry reads under HKLM\SYSTEM\CurrentControlSet\Services\Disk\Enum looking for the strings VBOX, VMWARE and QEMU.
We have not tested ScyllaHide against packed .NET samples. For those the workflow is different and dnSpy replaces x64dbg.
Analysis output, ranked by how long it survives
A hash is dead the moment the builder recompiles. An IP or domain lasts until the operator rotates infrastructure, which for a commodity loader is days. Those go in your blocklist because they are free, not because they work.
A YARA rule on the packed dropper dies with the next packer build. A YARA rule on the unpacked payload survives, because the payload is the part the operator does not rebuild. That is the whole reason you dumped memory instead of stopping at the dropper.
A behavioural rule is the last thing to break. The attacker can change every byte of the implant and still needs to allocate remote memory, write to it, and start a thread there.
Here is the payload-side rule. Run it against dumped regions, not against the original file.
rule Injection_API_Trio_In_Dumped_Image
{
meta:
author = "huntrule.com"
description = "Classic remote injection API set in an unpacked PE or memory dump"
reference = "https://attack.mitre.org/techniques/T1055/001/"
date = "2026-07-31"
strings:
$api1 = "OpenProcess" ascii fullword
$api2 = "VirtualAllocEx" ascii fullword
$api3 = "WriteProcessMemory" ascii fullword
$api4 = "CreateRemoteThread" ascii fullword
$hollow1 = "NtUnmapViewOfSection" ascii fullword
$hollow2 = "SetThreadContext" ascii fullword
$hollow3 = "ResumeThread" ascii fullword
condition:
uint16(0) == 0x5A4D
and filesize < 8MB
and (
all of ($api*)
or (3 of ($api*) and 1 of ($hollow*))
)
}What it catches: remote thread injection in a dumped image where the import names are resolvable as plaintext (T1055.001), and process hollowing (T1055.012) via the second branch. The branches matter. Hollowing does not call CreateRemoteThread, so a hollowing-only sample would never satisfy the full quartet, and a plain injector imports none of the $hollow set. One condition covering both needs both arms.
What it misses: API hashing, which is common in shellcode loaders and removes every string above. Reflective loading that never lands a full PE with an MZ at offset zero (T1620) fails the uint16(0) check. Delete that line and your false positive rate climbs, so decide deliberately.
Known false positives: debuggers, EDR agents, installer frameworks, and a fair number of legitimate injection-based tools. This rule is a hunting rule, not a blocking rule.
The behavioural half, on Sysmon Event ID 8:
title: Remote thread created into a common injection target
id: 6f3d1c0a-2b41-4a7c-9c2e-8d5a1f7b0e33
status: experimental
logsource:
product: windows
category: create_remote_thread
detection:
selection:
TargetImage|endswith:
- '\explorer.exe'
- '\svchost.exe'
- '\notepad.exe'
- '\rundll32.exe'
filter_signed_paths:
SourceImage|startswith:
- 'C:\Windows\System32\'
- 'C:\Program Files\'
- 'C:\Program Files (x86)\'
condition: selection and not filter_signed_paths
falsepositives:
- Security and management agents that run outside Program Files, commonly
from C:\ProgramData or a vendor directory under C:\Windows
- Installers and update services running from temp paths
level: mediumThe path filter is doing real work here, and it is also the rule's main weakness. An EDR agent installed under C:\Program Files is filtered out entirely, which is the point. The same vendor shipping its injector from C:\ProgramData\<vendor>\ walks straight through, and that path is common enough that you should enumerate what actually injects in your estate before turning this on. Tune the filter list first. The rule is only as good as that list.
ATT&CK mapping
ID | Title | Where it showed up |
|---|---|---|
T1027.002 | Software Packing | High entropy sections, stub import table |
T1055.001 | Dynamic-link Library Injection | VirtualAllocEx and CreateRemoteThread pair |
T1055.012 | Process Hollowing | NtUnmapViewOfSection plus SetThreadContext |
T1497.003 | Time Based Evasion | GetTickCount delta around Sleep |
T1622 | Debugger Evasion | PEB BeingDebugged, NtQueryInformationProcess |
T1620 | Reflective Code Loading | Payload with no MZ header in the dump |
Summary
Malware analysis is four methods stacked in order of cost, and the skill is knowing which wall you just hit. The lab rules exist so that hitting a wall costs you a snapshot revert instead of a domain. Everything you extract has a shelf life, and the shelf life runs from hours for a hash to months for a behavioural rule, so spend your remaining hours on the outputs at the far end. Both rules above are starting points and will need tuning against your own telemetry before they are worth alerting on.
If you would rather start from a rule someone already tuned, the injection and process-access coverage is at /rules?domain=windows.
outputs_by_durability:
- artifact: file_hash
survives: until recompile
- artifact: c2_ip_or_domain
survives: until infrastructure rotation
- artifact: yara_on_packed_dropper
survives: until next packer build
- artifact: yara_on_unpacked_payload
survives: until payload rewrite
- artifact: behavioural_rule
survives: until technique changeRelated articles

What Is Living off the Land (LotL) in Attacks?
certutil.exe ships on every Windows install, is signed by Microsoft, sits in C:\Windows\System32, and will fetch a file from a URL onto disk. That last part is not a bug. It is what the certificate…
2026-01-169 min read

What Is Fileless Malware?
Almost nothing is entirely fileless. The DoublePulsar backdoor that EternalBlue installs ends up in kernel memory with no file written, and that is the rare pure case. Everything else people call…
2025-12-307 min read

What Is a Webshell?
China Chopper's server component is one line of ASPX. <%@ Page Language="Jscript"%><%eval(Request.Item["password"],"unsafe");%> That line, dropped anywhere under a web root that IIS will hand to the…
2025-12-2011 min read