courses

Dynamic Analysis of Windows Malware from Linux

Why analyze Windows malware from Linux?

The obvious way to run a Windows sample is on Windows. The problem with the obvious way is that the sample and your analysis tooling then share an operating system. Every trick the malware has — its persistence, its privilege escalation, its EDR evasion, its process injection — is aimed squarely at the platform you are standing on. Your monitoring tools are Windows processes, and Windows processes can be found, hooked, crashed, or lied to.

Analyzing from Linux inverts that. The sample is foreign code: it does not know how to persist on your host, cannot tamper with your capture, and in the VM case cannot even see the tools watching it, because they are not inside the machine it is running on. That last point is the one that matters most, and it is the theme of this page. The best observation post is outside the system under observation.

This page is about the three ways to make a Windows PE actually execute when your host is Linux, what each one is good for, and — critically — how each one lies to you. It builds on Dynamic Analysis, which covers the general methodology, and Working with Wine, which covers Wine as a tool rather than as an analysis technique.

Three ways to run a Windows binary on Linux

These are not three flavors of the same thing. They differ in what actually executes your sample’s instructions, and that single difference determines what you are allowed to conclude from what you see.

  Wine Emulation Windows VM (KVM)
Instructions executed by Real CPU Software CPU model Real CPU (VT-x)
Windows API provided by Wine’s reimplementation A Python model of the API Actual Windows
Isolation from your host None Strong — nothing really runs Strong — hypervisor boundary
Setup cost Minutes Minutes Hours
Speed Native 10–1000x slower Near native
Kernel drivers (.sys) No Partially (Speakeasy) Yes
Instrumentation Moderate Total Via guest agents or the hypervisor
Fidelity Medium Low Highest
Typical use Explore behavior fast Triage at scale, unpack Confirm findings

The right workflow uses all three, in order of increasing cost and increasing trust:

  1. Emulate for triage. Seconds per sample, no risk, machine-readable output.
  2. Run under Wine to explore. Real execution, full API trace, easy diffing.
  3. Detonate in a VM to confirm anything you intend to write down.

Each step up costs more and lies less. Never report a conclusion that only the first two steps support.

Before you run anything

Two Linux-specific hazards catch people who are careful on Windows and assume Linux is safer. Both are verifiable on your own machine in ten seconds.

Wine maps your entire filesystem as drive Z:

A Wine prefix looks like a container. It is not one. Look at what wineboot creates:

$ ls -l ~/.wine-analysis/dosdevices/
lrwxrwxrwx 1 user user 10 Sep 15 13:47 c: -> ../drive_c
lrwxrwxrwx 1 user user 10 Sep 15 13:47 com1 -> /dev/ttyS0
lrwxrwxrwx 1 user user  1 Sep 15 13:47 z: -> /

Z: is the root of your Linux filesystem. Malware running under Wine can read and write anything your user account can — ~/.ssh, your samples directory, your notes, your browser profile — by opening Z:\home\you\.... It does not need an exploit to do this, and it does not need to know it is on Linux. It just needs to enumerate drives, which is ordinary behavior for a ransomware sample.

You can remove the mapping (rm ~/.wine-analysis/dosdevices/z:), and you should, but do not mistake that for isolation: the prefix is still an ordinary directory owned by your user, and a Wine process runs with your full privileges.

⚠️ Wine provides zero isolation. It is a compatibility layer, not a sandbox. Everything on this page that involves Wine assumes you are already inside a disposable REMnux VM with a snapshot to revert to. Running Wine on your daily-driver host is equivalent to double-clicking the sample.

Typing ./sample.exe runs it

Most Linux distributions register PE files with the kernel’s binfmt_misc handler so that Windows executables “just work”. Check yours:

$ cat /proc/sys/fs/binfmt_misc/DOSWin
enabled
interpreter /usr/bin/wine
flags:
offset 0
magic 4d5a

The magic is 4d5a — the bytes MZ, the first two bytes of every PE file. Any file starting with MZ that has the execute bit set will be handed to Wine when you run it. That includes the sample you just downloaded and absent-mindedly tab-completed. A CLR entry doing the same thing for /usr/bin/mono is commonly present too.

Two defenses, both cheap:

# 1. Never let samples be executable. Do this at download time.
$ chmod -x sample.exe

# 2. Disable the handler entirely on your analysis VM.
$ echo 0 | sudo tee /proc/sys/fs/binfmt_misc/DOSWin

Keeping samples in a password-protected zip until the moment of use — standard practice covered in Malware Triage — solves this as a side effect.

A specimen to calibrate with

Before pointing any of this tooling at real malware, point it at something whose behavior you already know. You cannot tell whether a tool is lying to you if you do not have ground truth, and as the next section shows, one of these tools will lie to you in a way that looks completely plausible.

Build a benign stand-in. This does three things every dropper does — writes a Run key, drops a file, resolves a C2-looking name — and nothing harmful. The hostname uses the .test TLD, which RFC 2606 reserves precisely so it can never resolve to a real host.

/* specimen.c -- a deliberately benign stand-in for a dropper.
 *
 * Built without the C runtime so that an emulator sees our API calls
 * immediately instead of several hundred CRT startup calls first.
 */
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>

#define C2_HOST "updates.example-c2.test"
#define RUN_KEY "Software\\Microsoft\\Windows\\CurrentVersion\\Run"
#define DROP    "C:\\Users\\Public\\dropped.bin"

void __cdecl mainCRTStartup(void)
{
    HKEY   hKey;
    HANDLE hFile;
    DWORD  written;
    WSADATA wsa;
    struct addrinfo *res = NULL;
    static const char value[]   = DROP;
    static const char payload[] = "not actually malicious\r\n";

    /* 1. Persistence */
    if (RegCreateKeyExA(HKEY_CURRENT_USER, RUN_KEY, 0, NULL, 0,
                        KEY_WRITE, NULL, &hKey, NULL) == ERROR_SUCCESS) {
        RegSetValueExA(hKey, "SecurityUpdate", 0, REG_SZ,
                       (const BYTE *)value, sizeof(value));
        RegCloseKey(hKey);
    }

    /* 2. Drop a file */
    hFile = CreateFileA(DROP, GENERIC_WRITE, 0, NULL,
                        CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile != INVALID_HANDLE_VALUE) {
        WriteFile(hFile, payload, sizeof(payload) - 1, &written, NULL);
        CloseHandle(hFile);
    }

    /* 3. Look up the C2 */
    if (WSAStartup(MAKEWORD(2, 2), &wsa) == 0) {
        if (getaddrinfo(C2_HOST, "80", NULL, &res) == 0 && res)
            freeaddrinfo(res);
        WSACleanup();
    }

    ExitProcess(0);
}

Cross-compile it on Linux with MinGW — no Windows machine involved:

$ sudo apt install gcc-mingw-w64-x86-64        # Debian/Ubuntu/REMnux
$ x86_64-w64-mingw32-gcc -O1 -nostartfiles -nostdlib \
      -o specimen.exe specimen.c -lkernel32 -ladvapi32 -lws2_32
$ file specimen.exe
specimen.exe: PE32+ executable for MS Windows 5.02 (console), x86-64 (stripped to external PDB), 5 sections

Ground truth: three observable behaviors, in a 9.7 KB binary with exactly eleven imports. Remember the number three.

Path 1: Wine, with the relay trace

Wine’s value for analysis is not that it runs Windows programs. It is that Wine is the Windows API, in source you can read, and it will narrate every call a sample makes through it. That is the relay debug channel, and it is the single most useful thing on this page.

Set up a throwaway prefix

$ export WINEPREFIX=~/.wine-$(date +%Y%m%d)-specimen
$ WINEDEBUG=-all wine wineboot --init
$ rm -f "$WINEPREFIX/dosdevices/z:"          # unmap the host filesystem

One prefix per sample. They are disposable — rm -rf and start over — and keeping them separate means a registry diff shows one sample’s changes rather than the accumulated sludge of a dozen.

The relay channel, and why you must filter it

WINEDEBUG=+relay logs every API entry point crossed. Unfiltered, it is unusable. On Wine 11.17, simply asking cmd to print its version produces:

$ WINEDEBUG=+relay,+tid wine cmd /c ver > relay.txt 2>&1
$ wc -l relay.txt
406631 relay.txt

Four hundred thousand lines of Windows starting up, before your sample has done anything. The fix is RelayInclude, a registry value under HKCU\Software\Wine\Debug that whitelists the calls you care about. Wildcards work per-DLL:

$ wine reg add 'HKCU\Software\Wine\Debug' /v RelayInclude \
    /d 'advapi32.Reg*;kernel32.CreateFile*;kernel32.WriteFile;ws2_32.*' /f

Same command, same Wine, with the filter in place:

$ WINEDEBUG=+relay,+tid wine cmd /c ver > relay.txt 2>&1
$ wc -l relay.txt
2609 relay.txt

406,631 lines to 2,609 — a 99.4% reduction, and what remains is readable. RelayExclude works the same way for the inverse case (log everything except a noisy DLL). Start with an include list built from the sample’s import table: run rabin2 -i sample.exe first, and trace what it actually imports.

Reading the trace

Now run the specimen:

$ WINEDEBUG=+relay,+tid wine specimen.exe 2>&1 \
    | grep -aiE '(Call|Ret) +(advapi32|kernel32|ws2_32)\.'
018c:Call advapi32.RegCreateKeyExA(ffffffff80000001,140002000 "Software\\Microsoft\\Windows\\CurrentVersion\\Run",00000000,00000000,00000000,00020006,00000000,7ffffe30ff68,00000000) ret=140001060
018c:Ret  advapi32.RegCreateKeyExA() retval=00000000 ret=140001060
018c:Call advapi32.RegSetValueExA(00000034,14000202e "SecurityUpdate",00000000,00000001,1400020a0,0000001c) ret=14000112c
018c:Ret  advapi32.RegSetValueExA() retval=00000000 ret=14000112c
018c:Call KERNEL32.CreateFileA(14000203d "C:\\Users\\Public\\dropped.bin",40000000,00000000,00000000,100000002,00000080,00000000) ret=14000109f
018c:Ret  KERNEL32.CreateFileA() retval=00000034 ret=14000109f
018c:Call KERNEL32.WriteFile(00000034,140002080,00000018,7ffffe30ff64,00000000) ret=1400010cf
018c:Ret  KERNEL32.WriteFile() retval=00000001 ret=1400010cf
018c:Call ws2_32.WSAStartup(00000202,7ffffe30fdc0) ret=1400010e8
018c:Ret  ws2_32.WSAStartup() retval=00000000 ret=1400010e8
018c:Call ws2_32.getaddrinfo(14000205c "updates.example-c2.test",140002059 "80",00000000,7ffffe30fdb8) ret=14000115e
018c:Ret  ws2_32.getaddrinfo() retval=00002af9 ret=14000115e
018c:Call ws2_32.WSACleanup() ret=140001178
018c:Ret  ws2_32.WSACleanup() retval=00000000 ret=140001178

All three behaviors, with their arguments decoded. Note the format:

Wine also genuinely performed the actions, so you can verify them out-of-band:

$ wine reg query 'HKCU\Software\Microsoft\Windows\CurrentVersion\Run'
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run
    SecurityUpdate    REG_SZ    C:\Users\Public\dropped.bin

$ ls -l "$WINEPREFIX/drive_c/users/Public/dropped.bin"
-rw-r--r-- 1 user user 24 Sep 15 13:47 dropped.bin

The dropped file is a normal Linux file. Feed it straight to file, strings, YARA, or Ghidra with no extraction step.

Diffing the prefix

For samples that do more than three things, diff instead of reading:

$ cp -a "$WINEPREFIX" "$WINEPREFIX.before"
$ wine sample.exe
$ diff -r "$WINEPREFIX.before/drive_c" "$WINEPREFIX/drive_c"    # dropped files
$ diff "$WINEPREFIX.before/user.reg"   "$WINEPREFIX/user.reg"   # registry

The registry hives are plain text, which makes this far more pleasant than the equivalent on Windows.

Debugging under Wine

Wine ships winedbg, which speaks two protocols:

$ winedbg specimen.exe               # its own console debugger
$ winedbg --gdb specimen.exe         # GDB remote protocol

The --gdb mode is the useful one: it exposes the Windows process to any GDB frontend, so the pwndbg workflow from Advanced Dynamic Analysis applies unchanged to a PE. You get breakpoints, single-stepping, and memory inspection using the same muscle memory you use on ELF binaries.

x64dbg under Wine is not a reliable option. It is the standard Windows debugger for malware work, and people regularly try to run it under Wine; it has a long history of crashing on unimplemented msvcp120.dll functions and of loading with a blank CPU pane. Do not build a workflow on it. If you want x64dbg, run it inside the Windows VM (Path 3) and drive it remotely — REMnux ships an x64dbg Automate MCP server for exactly this, which fits the tooling described in AI-Assisted Analysis.

How malware detects Wine

Wine is trivially detectable, and plenty of samples check. The canonical test is one GetProcAddress call:

/* Non-NULL means Wine. Verified present in Wine 11.17's ntdll. */
FARPROC p = GetProcAddress(GetModuleHandleA("ntdll.dll"), "wine_get_version");

Wine’s ntdll.dll exports wine_get_version, wine_get_host_version, and wine_get_build_id alongside its 1,479 real exports. There is also a conspicuous HKCU\Software\Wine registry key. Neither is hidden, because Wine is not trying to hide — it is a compatibility layer, and these exports exist so Windows software can adapt to it.

The practical consequence: a sample that exits immediately under Wine has told you something. Do not record it as “does nothing.” Record it as “may detect Wine”, and escalate to Path 3. See Anti-Analysis Techniques for the broader catalog.

What Wine cannot do

Path 2: Emulation — the sample never actually runs

An emulator does not execute the sample on your CPU at all. It interprets the instructions in software and models the Windows API in Python. Nothing touches your filesystem, no syscall reaches your kernel, and a “dropped” file is a buffer in a report. For triage this is close to ideal: it is safe by construction, it is scriptable, and it produces JSON.

Speakeasy

Speakeasy is Mandiant’s Windows user- and kernel-mode emulator. It handles PEs, DLLs, .sys drivers, and raw shellcode, and it runs anywhere Python does.

⚠️ Install from git, not PyPI. As of September 2026 the PyPI release (speakeasy-emulator 1.5.11) pins unicorn==1.0.2, which imports distutils — removed from Python in 3.12. On any current distro the CLI dies at import with ModuleNotFoundError: No module named 'distutils', and upgrading unicorn by hand fails differently (module 'unicorn.unicorn' has no attribute '_uc') because 2.x is API-incompatible. The git tree has been modernized — it requires unicorn>=2.1.4 and Python ≥3.10 — and installs cleanly.

$ python3 -m venv ~/venvs/speakeasy
$ ~/venvs/speakeasy/bin/pip install 'git+https://github.com/mandiant/speakeasy.git'
$ ~/venvs/speakeasy/bin/speakeasy -t specimen.exe --no-mp -o report.json

Useful flags:

Flag Purpose
-t FILE Target binary
-o FILE Write the JSON report
--raw --arch x64 Treat input as shellcode rather than a PE
--raw-offset Start shellcode execution at a hex offset
--dropped-files-path DIR Save files the sample “wrote”
-k Emulate child processes too
--timeout Cap emulation time (default 60s)
--gdb --gdb-port 1234 Expose a GDB stub and step the emulation
--analysis-strings Recover strings decoded at runtime
--no-mp Run in-process; makes tracebacks readable

The report nests behavior under entry_points[].events[]:

$ jq -r '.entry_points[].events[]
         | select(.event=="api")
         | "\(.api_name) -> \(.ret_val)"' report.json
advapi32.RegCreateKeyExA -> 0x6
kernel32.CreateFileA -> 0x80
kernel32.WriteFile -> 0x1
kernel32.CloseHandle -> 0x1
ws2_32.WSAStartup -> 0x0

Non-API events carry extracted content — a file_write event records the path, the size, and a SHA-256 of the data, so you can recover a dropped payload that was never written to a real disk:

{"event": "file_write", "path": "C:\\Users\\Public\\dropped.bin",
 "size": 24, "data_ref": "5317ae95596d23c0f86df8a8a718f0e28b600a7d22343e0195b599505ab5d56d"}

The lesson: count the behaviors

Go back to the ground truth. The specimen does three things. Read the Speakeasy trace again:

advapi32.RegCreateKeyExA -> 0x6          <-- 0x6 is ERROR_INVALID_HANDLE
kernel32.CreateFileA     -> 0x80
kernel32.WriteFile       -> 0x1
kernel32.CloseHandle     -> 0x1
ws2_32.WSAStartup        -> 0x0

RegSetValueExA is missing, and getaddrinfo is missing. Speakeasy’s model of RegCreateKeyExA returned 0x6 where Wine — and Windows — returned 00000000. The specimen checks that return value, so it took the other branch and skipped the persistence write entirely. Emulation then stopped after WSAStartup without ever reaching the name resolution.

A tool reported two of three behaviors, with no error, no warning, and an otherwise perfectly plausible trace. Had this been real malware, the write-up would have said “no persistence observed” — and Wine’s trace on the same binary, three sections up, shows RegSetValueExA writing the Run key and getaddrinfo resolving the C2.

⚠️ An emulator’s API model is a guess about what Windows does. When the guess is wrong, the sample takes a branch it would never take on Windows and the emulator reports the resulting behavior with total confidence. Absence of evidence in an emulator is not evidence of absence. Confirm every negative finding somewhere with higher fidelity.

This is exactly why the specimen exists: it is the control that makes the failure visible. Run it whenever you install or upgrade this tooling.

A second, smaller caution from the same run: the report’s strings.static block listed "Hello World!", MessageBoxA, and USER32.dll — none of which appear anywhere in the specimen, which imports only ADVAPI32, KERNEL32, and WS2_32. The report’s sha256 matched the file, so it was describing the right binary with the wrong strings. Use strings, rabin2 -z, or FLOSS for static strings and treat that block as unreliable until it is fixed upstream.

Qiling

Qiling (v1.4.11, September 2026) is the other mature option, built on the same Unicorn engine but designed as an instrumentable framework rather than a CLI. You write Python, and you can hook any address, any API, or any instruction:

from qiling import Qiling
from qiling.const import QL_VERBOSE

def hook_reg(ql, address, params):
    print(f"[!] persistence attempt: {params}")

ql = Qiling(["specimen.exe"], "rootfs/x8664_windows", verbose=QL_VERBOSE.DEFAULT)
ql.os.set_api("RegSetValueExA", hook_reg)
ql.run()

Qiling needs a rootfs — a directory of real Windows DLLs for the emulated process to load. You must supply those yourself from a licensed Windows install; they are not redistributable, which is the main friction in getting started. Choose Qiling over Speakeasy when you need to intervene in execution — forcing a branch, faking an API result, dumping memory at a chosen instruction — rather than just observe it. The same ideas appear in Symbolic Execution with angr.

Path 3: A Windows VM under KVM, watched from outside

This is the high-fidelity option, and on Linux it has an advantage that is easy to undersell: the hypervisor is yours. Monitoring lives on the Linux side of the boundary, where the malware cannot reach it, cannot see it, and cannot disable it. On a Windows analysis host, ProcMon is a process the sample can find. Here it is not a process at all.

Snapshots are the whole safety model

$ virsh snapshot-create-as win10-analysis clean \
      --description "post-install baseline, no samples" --atomic
$ virsh snapshot-revert win10-analysis clean --running
$ virsh snapshot-list win10-analysis

Revert before each sample, not after. “After” assumes the run completed the way you expected, which is the assumption malware exists to violate.

Capture traffic the sample cannot see

Find the host-side interface for the guest’s NIC, then capture on it:

$ virsh domiflist win10-analysis
 Interface   Type      Source    Model    MAC
---------------------------------------------------------
 vnet3       network   isolated  e1000e   52:54:00:1a:2b:3c

$ sudo tcpdump -i vnet3 -w sample.pcap -s 0

vnet3 is a host interface. There is no capture driver in the guest, nothing in the guest’s process list, and nothing for the sample to tamper with. Compare this with running Wireshark inside the VM, where a sample that enumerates processes finds it instantly.

Give the guest an isolated libvirt network (no <forward> element) so it cannot reach the internet, then answer its requests with a fake one. On REMnux:

$ inetsim                         # simulates DNS, HTTP, SMTP, FTP, IRC...
$ sudo fakenet                    # FakeNet-NG; alternative, same idea
$ accept-all-ips start            # redirect any destination IP to local ports

accept-all-ips is the REMnux-specific piece worth knowing: malware often connects to a hard-coded IP rather than a name, and without it those connections land nowhere. See Malware Network Behavior for what to do with the resulting pcap.

Acquire memory without touching the guest

This is the strongest argument for the Linux-host arrangement. libvirt can dump the guest’s RAM from outside, while it runs, with no agent inside:

$ virsh dump win10-analysis mem.dmp --memory-only --live --format win-dmp

The --format values accepted are kdump-zlib, kdump-lzo, kdump-snappy, win-dmp, and elf. win-dmp is the one you want: it produces a Windows crash dump, which WinDbg opens natively and which Volatility 3 reads without conversion. (elf is the default and needs a conversion step.)

$ vol -f mem.dmp windows.pslist
$ vol -f mem.dmp windows.malfind      # injected/unbacked executable memory
$ vol -f mem.dmp windows.dlllist --pid 4242

Nothing ran inside the guest to produce that image — no WinPMEM, no DumpIt, no driver load, no new process, no file written to the guest’s disk. The sample cannot detect the acquisition because from inside the VM, nothing happened. For what to do with the image, see Advanced Dynamic Analysis, which covers malfind and injection detection in depth.

Time the dump deliberately: pause the guest at an interesting moment (virsh suspend), dump, then resume. Catching a packer just after it has unpacked itself in memory but before it has wiped the buffer is the classic use.

Correlate the guest’s view with yours

Run Procmon inside the guest for the file and registry detail the hypervisor cannot see, export to CSV, and correlate it with your host-side pcap using ProcDOT on REMnux. That combination — guest-side process detail plus host-side ground-truth network capture — renders as a single graph of what the sample did, and neither half is trustworthy alone.

Hardening against VM detection

Out of the box, a KVM guest announces itself. The usual checks, and what to do:

Detection Default Mitigation
CPUID leaf 1, ECX bit 31 Set <kvm><hidden state='on'/></kvm>
Hypervisor vendor string KVMKVMKVM <hyperv mode='custom'><vendor_id state='on' value='GenuineIntel'/></hyperv>
MAC address OUI 52:54:00 (QEMU) Set a MAC from a real vendor’s OUI
SMBIOS / DMI strings QEMU, Bochs <smbios mode='sysinfo'/> plus a <sysinfo> block
CPU brand string QEMU Virtual CPU <cpu mode='host-passthrough'/>
Disk/device model strings QEMU HARDDISK Use virtio or override the product string
<features>
  <acpi/><apic/>
  <kvm><hidden state='on'/></kvm>
  <hyperv mode='custom'>
    <vendor_id state='on' value='GenuineIntel'/>
  </hyperv>
</features>
<cpu mode='host-passthrough' check='none'/>
<os>
  <smbios mode='sysinfo'/>
</os>
<sysinfo type='smbios'>
  <system>
    <entry name='manufacturer'>Dell Inc.</entry>
    <entry name='product'>OptiPlex 7090</entry>
  </system>
</sysinfo>

Test your work with Pafish or al-khaser, which run the same checks malware does and report which ones fire.

⚠️ You will not win this arms race, and you should not plan to. Timing attacks against emulated instructions, and the simple absence of a plausible user — no browser history, two files on the desktop, uptime of four minutes, no mouse movement — remain detectable no matter how the XML is tuned. Hardening raises the cost of detection; it does not eliminate it. Budget more effort for making the guest look lived-in than for hiding CPUID.

Choosing a path

A decision procedure that holds up in practice:

  1. Is it shellcode or a .sys driver? Start with Speakeasy — --raw for shellcode, direct for drivers. Wine cannot load drivers at all.
  2. Do you have a hundred samples and one afternoon? Emulate all of them, script over the JSON, and triage by what the reports contain.
  3. Do you want to watch one sample’s API calls closely? Wine with a filtered relay trace, in a REMnux VM.
  4. Did it exit immediately, or do nothing? Assume detection. Go to the VM.
  5. Does it need real Windows — a service, COM, .NET, a driver, a GUI? VM.
  6. Are you about to write a conclusion down? Confirm it in the VM first.

The failure mode to avoid is stopping at step 2 or 3 because the output looked complete. The specimen above shows how convincing an incomplete trace can be.

What none of this gives you

Be explicit in your write-ups about what the method could not have seen:

Key takeaways

References


Related course pages: Dynamic Analysis · Advanced Dynamic Analysis · Working with Wine · Anti-Analysis Techniques · Malware Network Behavior · Malware Triage · REMnux Installation · Symbolic Execution with angr

🛠️ Maintenance note: the tool-specific details here move faster than the concepts. The Speakeasy PyPI-versus-git problem is a snapshot of September 2026 — check whether a release past 1.5.11 has shipped with the unicorn>=2.1.4 dependency before telling students to install from git, and re-check the strings.static anomaly, which may be fixed upstream. Relay line counts (406,631 / 2,609) were measured on Wine 11.17 with cmd /c ver and will drift with every Wine release; the ratio is the point, not the numbers. virsh dump --format gained win-dmp relatively recently, so verify it against the libvirt shipped in whatever REMnux base release is current. Anti-VM hardening is an arms race and the table will rot — re-run Pafish each term rather than trusting it. The durable content is the three-path split, the emulator-fidelity failure, and observing from outside the guest.