Field note

Secure Malware Analysis with Docker: A SysAdmin's Guide

Author: Mahmoud Ouf min read

As a system administrator dealing with security incidents, you’ll inevitably encounter situations where you need to analyze suspicious files. Running malware on your production systems? Absolutely not. That’s where Docker comes in—providing isolated, disposable environments for safe malware analysis.

Why Docker for Malware Analysis?

Docker containers offer several advantages for malware analysis:

  • Isolation: Containers run in isolated namespaces, separate from the host
  • Disposability: Spin up, analyze, destroy—no traces left
  • Reproducibility: Same environment every time
  • Resource Control: Limit CPU, memory, and disk access
  • Network Isolation: Complete control over network access

⚠️ Important: Docker is NOT a security boundary like a VM. For highly sophisticated malware, use a proper VM with snapshots. Docker is suitable for initial triage and less sophisticated samples.

Setting Up the Analysis Environment

Step 1: Create the Isolated Network

First, create a completely isolated Docker network with no external access:

bash — mahmoud@portfolio
# Create isolated network with no internet access
docker network create \
  --driver bridge \
  --internal \
  --subnet 172.28.0.0/16 \
  malware-net
mahmoud@portfolio ~ $

The --internal flag is critical—it prevents any outbound internet connectivity.

Step 2: Build the Analysis Container

Create a Dockerfile for your analysis environment:

FROM ubuntu:22.04

# Prevent interactive prompts
ENV DEBIAN_FRONTEND=noninteractive

# Install analysis tools
RUN apt-get update && apt-get install -y \
    strace \
    ltrace \
    gdb \
    radare2 \
    file \
    binutils \
    hexedit \
    xxd \
    net-tools \
    tcpdump \
    wireshark-common \
    python3 \
    python3-pip \
    && rm -rf /var/lib/apt/lists/*

# Install Python analysis tools
RUN pip3 install pefile yara-python capstone

# Create non-root analysis user
RUN useradd -m -s /bin/bash analyst
RUN mkdir -p /samples /output
RUN chown analyst:analyst /samples /output

# Drop to non-root user
USER analyst
WORKDIR /samples

CMD ["/bin/bash"]

Build the image:

bash — mahmoud@portfolio
docker build -t malware-sandbox .
mahmoud@portfolio ~ $

Step 3: Run with Maximum Restrictions

Launch the container with strict security constraints:

bash — mahmoud@portfolio
docker run -it --rm \
  --name malware-analysis \
  --network malware-net \
  --cap-drop ALL \
  --security-opt no-new-privileges:true \
  --security-opt seccomp=default \
  --read-only \
  --tmpfs /tmp:rw,noexec,nosuid,size=100m \
  --memory 512m \
  --cpus 1 \
  --pids-limit 100 \
  -v "$(pwd)/samples:/samples:ro" \
  -v "$(pwd)/output:/output:rw" \
  malware-sandbox
mahmoud@portfolio ~ $

Let’s break down these security flags:

FlagPurpose
--cap-drop ALLRemove all Linux capabilities
--no-new-privilegesPrevent privilege escalation
--read-onlyRead-only root filesystem
--tmpfs /tmpWritable temp with noexec
--memory 512mLimit memory to prevent fork bombs
--pids-limit 100Limit process count

Static Analysis Workflow

Once inside the container, start with static analysis—examining the file without executing it:

File Type Identification

bash — mahmoud@portfolio
file suspicious_binary
strings suspicious_binary | head -50
mahmoud@portfolio ~ $

Check for Packing/Obfuscation

bash — mahmoud@portfolio
# Check entropy (high entropy = packed/encrypted)
rabin2 -H suspicious_binary

# Check sections
readelf -S suspicious_binary
mahmoud@portfolio ~ $

Extract Indicators of Compromise (IOCs)

bash — mahmoud@portfolio
# Extract URLs, IPs, emails
strings suspicious_binary | grep -E "(http|https|ftp)://"
strings suspicious_binary | grep -oE "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b"
mahmoud@portfolio ~ $

Dynamic Analysis (Behavioral)

For dynamic analysis, you need execution capabilities. Create a separate, even more isolated container:

bash — mahmoud@portfolio
docker run -it --rm \
  --name malware-dynamic \
  --network none \
  --cap-drop ALL \
  --security-opt no-new-privileges:true \
  --memory 256m \
  --cpus 0.5 \
  -v "$(pwd)/samples:/samples:ro" \
  malware-sandbox
mahmoud@portfolio ~ $

Trace System Calls

bash — mahmoud@portfolio
# Trace all syscalls
strace -f -o /output/strace.log ./suspicious_binary

# Trace library calls  
ltrace -f -o /output/ltrace.log ./suspicious_binary
mahmoud@portfolio ~ $

Monitor File System Activity

bash — mahmoud@portfolio
# Watch for file creation/modification
inotifywait -m -r /tmp /home 2>/dev/null &
./suspicious_binary
mahmoud@portfolio ~ $

Capturing Network Traffic

Even in an isolated network, malware will attempt connections. Capture this for analysis:

bash — mahmoud@portfolio
# In the container
tcpdump -i any -w /output/traffic.pcap &

# Run the sample
./suspicious_binary

# Analyze
tcpdump -r /output/traffic.pcap -n
mahmoud@portfolio ~ $

Automated Analysis Script

Here’s a script to automate basic analysis:

bash — mahmoud@portfolio
#!/bin/bash
# analyze.sh - Automated malware triage

SAMPLE="$1"
OUTPUT_DIR="/output/$(date +%Y%m%d_%H%M%S)"
mkdir -p "$OUTPUT_DIR"

echo "[*] Analyzing: $SAMPLE"
echo "[*] Output: $OUTPUT_DIR"

# Static analysis
echo "[+] File type..."
file "$SAMPLE" > "$OUTPUT_DIR/filetype.txt"

echo "[+] Hashes..."
md5sum "$SAMPLE" > "$OUTPUT_DIR/hashes.txt"
sha256sum "$SAMPLE" >> "$OUTPUT_DIR/hashes.txt"

echo "[+] Strings..."
strings "$SAMPLE" > "$OUTPUT_DIR/strings.txt"

echo "[+] Headers..."
readelf -h "$SAMPLE" > "$OUTPUT_DIR/headers.txt" 2>/dev/null

echo "[+] Sections..."
readelf -S "$SAMPLE" > "$OUTPUT_DIR/sections.txt" 2>/dev/null

# Extract IOCs
echo "[+] Extracting IOCs..."
grep -oE "(http|https|ftp)://[^\"\' ]+" "$OUTPUT_DIR/strings.txt" > "$OUTPUT_DIR/urls.txt"
grep -oE "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b" "$OUTPUT_DIR/strings.txt" > "$OUTPUT_DIR/ips.txt"

echo "[*] Analysis complete: $OUTPUT_DIR"
mahmoud@portfolio ~ $

Case Study: Triage of loader.bin

To make the workflow concrete, here is an illustrative triage of a suspected first-stage loader obtained from a phishing attachment. Hashes, hostnames and IPs below use documentation ranges (198.51.100.0/24, example.com) — they are sample artifacts, not real infrastructure.

1. Fingerprinting

bash — mahmoud@portfolio
$ file loader.bin
loader.bin: ELF 64-bit LSB executable, x86-64, version 1 (SYSV),
             statically linked, no section header, stripped

$ sha256sum loader.bin
9f86d0818a01b1c8e7e6f8c2a44b5d9e0c1f2a3b4c5d6e7f8091a2b3c4d5e6f70  loader.bin
mahmoud@portfolio ~ $

Statically linked + stripped + no section header is the classic signature of a packed/encrypted payload.

2. Entropy & Packing

bash — mahmoud@portfolio
$ rabin2 -H loader.bin | grep -A3 entropy
section   .upx0    entropy 7.94    ; near-max -> encrypted 2nd stage
section   .text    entropy 5.12
section   .data    entropy 4.80
mahmoud@portfolio ~ $

A .upx0 section at 7.94 entropy means the real code is still encrypted in the binary; it is decrypted in-memory by a stub we can catch dynamically.

3. Static IOCs

bash — mahmoud@portfolio
$ strings -n 8 loader.bin | grep -iE "https?://|/tmp/|\.exe|powershell"
hxxp://update-cdn[.]example.com/gate.php      ; mocked C2 endpoint
/tmp/.cache/.sysd                            ; drops & persists here
mahmoud@portfolio ~ $

4. Dynamic Decryption Stub (gdb)

We set a breakpoint at the unpacking routine and dump the decrypted decoder:

gdb — debug session
(gdb) break *0x5555555551b0
(gdb) run
(gdb) x/12i $pc
   0x5555555551b0 <decrypt+0>:   push   %rbp
   0x5555555551b1 <decrypt+1>:   mov    %rsp,%rbp
   0x5555555551b4 <decrypt+4>:   mov    $0x20,%ecx            ; key length = 32
   0x5555555551b9 <decrypt+9>:   lea    0x1234(%rip),%rsi     ; cipher buffer
   0x5555555551c0 <decrypt+16>:  lea    0x2000(%rip),%rdi     ; plaintext buffer
   0x5555555551c7 <decrypt+23>:  xor    (%rsi),%al            ; XOR keystream byte
   0x5555555551c9 <decrypt+25>:  inc    %rsi
   0x5555555551cb <decrypt+27>:  inc    %rdi
   0x5555555551cd <decrypt+29>:  loop   0x5555555551c7        ; repeat ECX times
(gdb) x/s 0x555555557234
0x555555557234: "mZx1k...AABwc2ln"                          ; base64 2nd stage (truncated)

The xor (%rsi),%al + loop pattern is a textbook single-byte XOR string decoder — exactly what we expect from a stager.

5. Behavioral Trace (strace)

bash — mahmoud@portfolio
$ strace -f -e trace=network,execve ./loader.bin 2>&1 | tail -n 6
[pid 1337] socket(AF_INET, SOCK_STREAM, 0)          = 3
[pid 1337] connect(3, {sa_family=AF_INET, sin_port=htons(8443),
                        sin_addr=inet_addr("198.51.100.23")}, 16) = 0
[pid 1337] execve("/tmp/.cache/.sysd", ["/tmp/.cache/.sysd", ...], NULL) = 0
mahmoud@portfolio ~ $

Confirms a beacon to 198.51.100.23:8443 followed by a /tmp dropper execution.

6. Detection (YARA)

rule Suspected_XOR_Loader {
  meta:
    author = "M.Adel"
    sample = "loader.bin"
  strings:
    $xor_loop = { 30 06 48 ff c6 48 ff c7 e2 f7 }   // xor + loop decoder
    $c2       = "update-cdn" nocase
  condition:
    $xor_loop and $c2
}

7. Network Capture

bash — mahmoud@portfolio
$ tcpdump -r traffic.pcap -n 'tcp port 8443'
12:04:11 IP 10.0.0.5.49152 > 198.51.100.23.8443: Flags [P.], seq 1:48, length 47
12:05:11 IP 10.0.0.5.49152 > 198.51.100.23.8443: Flags [P.], seq 1:48, length 47
mahmoud@portfolio ~ $

Two identical 47-byte frames exactly 60 s apart → classic sleep-based beaconing.

This is the full loop: static fingerprint → entropy check → string IOC → in-memory decode → behavioral beacon → signature → pcap correlation.

Cleanup and Forensics

After analysis, always clean up properly:

bash — mahmoud@portfolio
# Stop and remove container
docker stop malware-analysis

# Remove the image if contaminated
docker rmi malware-sandbox

# Prune everything
docker system prune -af --volumes
mahmoud@portfolio ~ $

Best Practices Summary

  1. Never use --privileged - This defeats all isolation
  2. Always use --network none or --internal - Prevent C2 communication
  3. Drop all capabilities - Minimal permissions
  4. Use read-only filesystems - Prevent persistence
  5. Set resource limits - Prevent resource exhaustion
  6. Run as non-root - Additional isolation layer
  7. Destroy after use - Use --rm flag always

When NOT to Use Docker

Docker is not suitable for:

  • Kernel exploits or rootkits
  • Hypervisor escape attempts
  • Container escape exploits
  • Advanced persistent threats (APTs)

For these, use nested VMs with snapshots on an air-gapped analysis machine.

Conclusion

Docker provides a quick and effective way to safely triage malware samples when used correctly. The key is layering multiple security controls: network isolation, capability dropping, resource limits, and read-only filesystems. Remember—Docker isolation is not perfect, but with proper configuration, it’s suitable for most day-to-day malware analysis tasks.

Stay safe, and happy hunting! 🔍