In this section

0.6 Following Along

Module 0

Four specimens, none of them malware

Everything this course teaches is demonstrated on files you build yourself, from a compiler and software you already have. Twenty minutes now, and every module afterwards has something concrete to work on.

This is not a safety compromise. It is better teaching in three specific ways. You know with certainty what is in a file you compiled, so a false positive is diagnosable rather than mysterious. You can rebuild it with one thing changed, which is the only way to actually demonstrate that a pattern survives a recompile. And you sidestep the legal question entirely, which is the same position this platform takes elsewhere: whether you may hold live malware depends on your jurisdiction and your employer, and a course is the wrong place to decide it for you.

There is one thing built specimens cannot give you, and it is worth naming so the limitation is not a surprise. They are not adversarial. Nobody wrote them to evade your rules, so they will not teach you what deliberate obfuscation looks like from the inside. Module 4 covers obfuscation using public samples and documented techniques, and the specimens carry everything else.

S1 and S2: a binary and its packed twin

The pair that Module 2 and Module 3 keep returning to.

mkdir -p ~/yara/specimens && cd ~/yara/specimens
#
# S1: any small binary you have. On Linux, borrow one.
cp /bin/ls s1.bin
#
# S2: the same file, packed
cp s1.bin s2.bin && upx -q s2.bin
#
ls -l s1.bin s2.bin
sha256sum s1.bin s2.bin

The point of the pair is the contrast. Same program, same behavior, and almost nothing in common at the byte level. Run strings over both and count the lines; Module 2 uses that gap, and Module 3 explains what survives it.

If upx is not installed it is in most package managers, and any packer works for the purpose. What matters is having one file in two forms, because that pair is what makes the packing sections demonstrable rather than assertable.

S3: a family of three, which is the important one

If you build only one specimen, build this one. It is what makes the difference between a course you read and a course you can test claims in.

Three near-identical programs sharing a mutex GUID and differing only in a version string. That is a realistic shape: an actor rebuilds, the version bumps, and the functional identifiers stay because the program needs them.

The mutex is the realistic part. A program that wants to avoid running twice needs a name to check for, and that name has to stay the same across builds or the check stops working. The author can randomize it, but doing so breaks their own duplicate-instance logic unless they are careful, so in practice these values persist. Module 2 returns to that reasoning properly.

cat > fam.c.template <<'EOF'
#include <stdio.h>
static const char *MUTEX = "Global\\{8f2c41d9-7b03-4e15-9a62-c8017fe4b2a1}";
static const char *BUILD = "VERSION";
int main(void){ printf("%s %s\n", MUTEX, BUILD); return 0; }
EOF
#
for v in 1.0.4 1.0.5 1.1.0; do
  sed "s/VERSION/$v/" fam.c.template > fam_$v.c
  gcc -o fam_$v fam_$v.c
done
#
sha256sum fam_1.0.4 fam_1.0.5 fam_1.1.0 | cut -c1-16
fed9993036645f9e
c9328299d3a5dcd4
ead7eb13c8865b8a

Three different hashes for three builds of what is effectively the same program. That single fact is the entire argument for writing rules rather than distributing hashes, and now you have it in front of you rather than as a claim.

Nothing adversarial produced that. No packer, no obfuscation, no attempt to evade anything. Three characters changed in a string constant and the file hash became useless. An actor who never thinks about detection at all defeats hash-based coverage simply by shipping an update.

What the family proves, in two rules

Worth running now, because it is the course's central point in about a minute.

rule Keyed_On_Version
{
    strings:
        $v = "1.0.4"
    condition:
        $v
}
rule Keyed_On_Mutex
{
    strings:
        $m = "Global\\{8f2c41d9-7b03-4e15-9a62-c8017fe4b2a1}"
    condition:
        $m
}

Scan all three builds with each. Predict the results before you run them.

The prediction matters more than the result. If you expect both rules to match all three, the measurement is about to teach you something; if you expect the version rule to match one, you have already understood the module.

Keyed_On_Version   matches 1 of 3   [fam_1.0.4]
Keyed_On_Mutex     matches 3 of 3   [fam_1.0.4, fam_1.0.5, fam_1.1.0]
same mutex rule against 400 files from /usr/bin   false positives: 0

Both rules are syntactically correct, both fire on the sample they were written from, and one of them is worthless. Nothing in the rule's appearance distinguishes them. The only thing that separated them was testing against files the rule did not come from, which is Module 5's entire argument arriving in Section 0.6.

Note which of the two a person reading the sample would more likely have picked. A version string is specific, it looks like an identifier, and it is exactly the sort of thing that stands out when you run strings over a binary. The GUID looks like noise. Visual distinctiveness pointed at the wrong pattern, which is Section 0.1's warning arriving as a measurement rather than a claim.

S4: a script and a document

Module 4's material, and the least work of the four.

# A PowerShell file with a base64-encoded command, which is
# exactly the shape Module 4 teaches you to match
cat > s4.ps1 <<'EOF'
$e = "SQBFAFgAIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAKQA="
powershell -EncodedCommand $e
EOF
#
# An Office document with a macro: create it in Word or LibreOffice
# and save as .docm. It is a ZIP, which is the point.
unzip -l s4.docm | head

The unzip line is not a detour. An Office document is a ZIP container, so the macro you want to match is compressed and no text pattern will ever see it in the raw file. Module 4 is largely about file formats that defeat naive rules, and this is the first one.

The PowerShell file has the same property from a different direction. The interesting content is base64 inside a string, so a rule looking for the decoded command finds nothing and a rule looking for the encoded form has to know which of several encodings was used.

The clean corpus

You cannot test a rule without files it should not match, and Section 0.3 covered where to get them. Confirm you have them:

✓ Verify

Run: ls ~/yara/specimens and the two rules above against all three family builds

Expected: six specimen files, three distinct hashes for the family, and the version rule matching exactly one build while the mutex rule matches all three

If not: if all three family hashes are identical, the version substitution did not take and you have built the same program three times. Check that strings fam_1.1.0 | grep 1.1.0 returns something before continuing, because the whole family exercise depends on the builds actually differing.

Windows specimens, if you can

Most rules in this course target PE files, and the three specimens above are ELF if you built them on Linux. Everything still works, and Module 3 in particular is about PE structure specifically.

If you have a Windows machine or VM, build the family there with any compiler and copy the results across. If you do not, Module 3 supplies PE files to work on and the exercises are written so that both routes work.

The family exercise in particular transfers cleanly. The reasoning is about what survives a rebuild, and that is a property of how programs are built rather than of a file format.

That is the environment. Section 0.7 summarizes the module, and then Module 1 starts on the language.