Skip to main content
Defensive Security · lesson 7 of 8
Defensive Security intermediate Lesson 7 of 8

Static File Analysis with EICAR and YARA: Hashes, file, strings and Rules

Triage a suspicious file safely with sha256sum, file and strings, then write a YARA rule that matches the EICAR test file and not a note that mentions it.

By SkillByExample · Published

defensive-security yara malware-analysis eicar hashes dfir

When a suspicious file turns up, the first questions are what it is, whether anyone has seen it before, and whether it matches anything you already detect. All three can be answered without running it. This lesson uses the EICAR test file, a harmless 68-byte string that every anti-malware product agrees to detect as if it were malware, so you can practise the whole workflow with no risk. You’ll hash it, identify it by content rather than name, and write a YARA rule that catches it without catching a text file that merely mentions it. Tested with Docker Engine 29.8.2, Debian 13.7 (debian:trixie-slim), YARA 4.5.2, file 5.46, GNU strings 2.44 and Python 3.13.5.

This lesson works with EICAR only. It doesn’t handle real malware, and it never executes a sample.

An isolated analysis box

# Dockerfile
FROM debian:trixie-slim
RUN apt-get update && apt-get install -y --no-install-recommends yara file binutils python3 \
 && rm -rf /var/lib/apt/lists/*
docker build -t yara-lab .
docker run -d --name analysis --network none -v "$PWD":/work -w /work yara-lab sleep infinity

--network none gives the container no network at all, the standard precaution when handling a sample. The samples are created inside the container, in /samples, not in the mounted directory. Your own anti-malware software would rightly quarantine an EICAR file written to your disk, which is the detection working, but it would also break the lesson.

The script builds the EICAR file from two halves, so the script itself isn’t a match, and adds three other files: the same bytes under an innocent name, a text note that mentions EICAR, and a clean file.

# make_samples.py: build the EICAR test file and some harmless files in /samples
from pathlib import Path

# The EICAR string is split so this script isn't itself flagged by a scanner.
EICAR = r"X5O!P%@AP[4\PZX54(P^)7CC)7}$" + "EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"

samples = Path("/samples")
samples.mkdir(exist_ok=True)
files = {
    "eicar.com": EICAR,
    "invoice.pdf": EICAR,  # same bytes, misleading name
    "notes.txt": "Analyst notes: the EICAR test file is harmless.\n",
    "readme.txt": "Quarterly report draft, nothing to see here.\n",
}
for name, text in files.items():
    path = samples / name
    path.write_text(text)
    print(f"{name:<12} {path.stat().st_size:>3} bytes")
$ docker exec analysis python3 make_samples.py
eicar.com     68 bytes
invoice.pdf   68 bytes
notes.txt     48 bytes
readme.txt    45 bytes

Hash, identify, read

Start with a hash. It’s the file’s identity: what you search threat-intel sources for, and what goes in the incident record.

$ docker exec analysis sh -c 'cd /samples && sha256sum *'
275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f  eicar.com
275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f  invoice.pdf
a0984f3071ad7b1d3fb974cd3faff55536bc94bc76e9a85397f73d0e9099fb31  notes.txt
cd986a908a9e7a50113d5a522e4d0078dd579b18d5c309259b5f8eaa12ea7060  readme.txt

invoice.pdf has the same hash as eicar.com, so it’s the same file, whatever it’s called. 275a02... is the well-known SHA-256 of the EICAR file; search it on any malware database and you’ll find it labelled as the test file.

$ docker exec analysis sh -c 'cd /samples && file *'
eicar.com:   EICAR virus test files
invoice.pdf: EICAR virus test files
notes.txt:   ASCII text
readme.txt:  ASCII text

file ignores the extension and reads the content, comparing the first bytes against a database of known formats. It even has an entry for EICAR. A real PDF would say PDF document; an “invoice” that file doesn’t call a PDF is worth a closer look.

$ docker exec analysis sh -c "strings -n 8 /samples/invoice.pdf | grep -o 'EICAR[A-Z-]*'"
EICAR-STANDARD-ANTIVIRUS-TEST-FILE

strings prints every run of at least 8 printable characters. On real binaries it turns up URLs, file paths, error messages and command names, often enough to guess what a program does. Here the grep picks out the readable marker in the middle of the EICAR string.

A YARA rule that’s too loose

YARA describes files by their content: named strings, and a condition on them. The obvious first rule looks for the word EICAR.

// eicar_loose.yar: any file that mentions EICAR
rule EICAR_Loose
{
    strings:
        $name = "EICAR"
    condition:
        $name
}
$ docker exec analysis yara eicar_loose.yar /samples
EICAR_Loose /samples/eicar.com
EICAR_Loose /samples/invoice.pdf
EICAR_Loose /samples/notes.txt
$ docker exec analysis yara -s eicar_loose.yar /samples/notes.txt
EICAR_Loose /samples/notes.txt
0x13:$name: EICAR

It catches both copies, and the analyst’s notes. -s shows why: the string EICAR at offset 0x13 (byte 19) of the note. That’s the YARA version of the alert-tuning problem: a rule that matches on a word will match every document that discusses the threat, including your own reports and wiki pages.

A rule that describes the file

The EICAR specification says exactly what the file is: a fixed string at the very start, and a total size of at most 128 bytes. Write the rule to that.

// eicar.yar: the EICAR test file itself, not text about it
rule EICAR_Test_File
{
    meta:
        description = "EICAR anti-malware test file"
        sha256 = "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"
    strings:
        $head = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$"
        $tail = "EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"
    condition:
        $head at 0 and $tail at 28 and filesize < 128
}
$ docker exec analysis yara eicar.yar /samples
EICAR_Test_File /samples/eicar.com
EICAR_Test_File /samples/invoice.pdf
$ docker exec analysis yara -s eicar.yar /samples/invoice.pdf
EICAR_Test_File /samples/invoice.pdf
0x0:$head: X5O!P%@AP[4\PZX54(P^)7CC)7}$
0x1c:$tail: EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*
$ docker exec analysis yara -n eicar.yar /samples
EICAR_Test_File /samples/notes.txt
EICAR_Test_File /samples/readme.txt

Both copies match and the note doesn’t. at 0 and at 28 pin each half to its exact offset (0x1c is 28), and filesize < 128 rules out a large document that happens to begin with the string. In YARA strings, a backslash is an escape character, so the single \ in the EICAR string is written \\. -n prints the files that don’t match, which is the quickest way to check a rule against known-good files. The meta section is free-form; recording the sample’s hash there tells the next analyst where the rule came from.

Writing a rule is the easy half. Before it goes anywhere near production, run it over a large set of known-clean files, as -n just did on a small scale, and treat every match there as a bug in the rule.

The last lesson in this topic examines the other file type every SOC triages daily: a suspicious email.

Practice

  1. Write ioc_check.py, which compares each sample’s SHA-256 with a small dictionary of known-bad hashes and prints a verdict per file.
Solution
# ioc_check.py: compare every sample's SHA-256 with a list of known-bad hashes
import hashlib
from pathlib import Path

KNOWN_BAD = {
    "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f": "EICAR test file",
}

for path in sorted(Path("/samples").iterdir()):
    digest = hashlib.sha256(path.read_bytes()).hexdigest()
    verdict = KNOWN_BAD.get(digest, "no match")
    print(f"{path.name:<14} {digest[:16]}  {verdict}")
$ docker exec analysis python3 ioc_check.py
eicar.com      275a021bbfb6489e  EICAR test file
invoice.pdf    275a021bbfb6489e  EICAR test file
notes.txt      a0984f3071ad7b1d  no match
readme.txt     cd986a908a9e7a50  no match

This is how a hash-based indicator of compromise (IOC) feed is used: an exact lookup, fast and with no false positives.

  1. Make a variant of the file with one extra byte, a trailing newline: { cat /samples/eicar.com; echo; } > /samples/eicar_nl.com. Run ioc_check.py, yara eicar.yar and file on it. Which of the three still recognise it?
Solution
$ docker exec analysis sh -c 'wc -c /samples/eicar_nl.com'
69 /samples/eicar_nl.com
$ docker exec analysis python3 ioc_check.py
eicar.com      275a021bbfb6489e  EICAR test file
eicar_nl.com   131f95c51cc81946  no match
invoice.pdf    275a021bbfb6489e  EICAR test file
notes.txt      a0984f3071ad7b1d  no match
readme.txt     cd986a908a9e7a50  no match
$ docker exec analysis yara eicar.yar /samples
EICAR_Test_File /samples/eicar_nl.com
EICAR_Test_File /samples/eicar.com
EICAR_Test_File /samples/invoice.pdf

One byte gives a completely different hash, so the hash lookup misses it. The YARA rule still matches, because the content it describes hasn’t moved, and file still reports EICAR virus test files. That’s the trade-off: hashes are exact and brittle, content rules are flexible and need testing for false positives. Real detection uses both. Delete eicar_nl.com afterwards so later commands match.

  1. Print the rule’s meta fields alongside the match.
Solution
$ docker exec analysis yara -m eicar.yar /samples/eicar.com
EICAR_Test_File [description="EICAR anti-malware test file",sha256="275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"] /samples/eicar.com

-m prints the metadata, so the person reading a match sees the rule’s own description and reference hash without opening the rule file. Rule sets shared between teams usually add author, date and a reference URL in the same place.

  1. Remove filesize < 128 from the rule. Which harmless file could it then match, and why does the EICAR specification limit the size at all?
Solution

Without the size check, any file that starts with the EICAR string matches, however large: for example a long document someone pasted the string into at the top. The specification allows the 68-byte string plus optional trailing whitespace, up to 128 bytes, precisely so scanners can recognise the test file and not every file containing it. When you write rules for real samples, a filesize bound and fixed offsets are among the cheapest ways to cut false positives.

Clean up

docker rm -f analysis

Removing the container also removes /samples, so no EICAR file is left behind.

Frequently Asked Questions

What is the EICAR test file?
EICAR is a 68-character text string, published by the European Institute for Computer Antivirus Research, that anti-malware products agree to detect as if it were malware. It contains no malicious code. Security teams use it to check that scanners, mail filters and detection rules work, without handling real malware.
What is a YARA rule?
A YARA rule describes a family of files by their content. It lists strings or byte patterns and a condition that combines them, optionally with offsets and file size. The yara tool scans files or directories and reports which rules match, which makes it a standard way to share malware detections between teams.
Why use YARA when you already have file hashes?
A hash identifies one exact file, so changing a single byte produces a completely different hash and the lookup misses it. A YARA rule matches content, so it still catches a variant whose relevant parts are unchanged. Hashes give exact, false-positive-free lookups; YARA rules generalise but need testing against clean files.
How do you test a YARA rule for false positives?
Run it against a collection of known-clean files, such as your own documents, binaries from a clean operating system install, and text that discusses the threat. yara -n lists the files that do not match, and any clean file that does match means the rule is too broad. Tighten it with fixed offsets, more specific strings or a filesize limit.
Is it safe to do static analysis on a suspicious file?
Static analysis means examining a file without running it: hashing it, identifying its type, reading its strings and scanning it with rules. It is much safer than executing the file, but do it in an isolated environment with no network, such as a container started with --network none, and never open the file with the program its extension suggests.