In this section

0.2 The Specimens You Will Build

Module 0

Every technique in this course needs something to run against, and the samples have to behave in a specific way for the teaching to land: you need several builds of the same program, because a rule that matches the one file it was written from proves nothing. This section builds four specimens from software you already have, in about twenty minutes, and they carry the entire course. They are not malware and they are deliberately not downloaded from anywhere. That is partly a legal position, which the course states plainly rather than dancing around, and partly because a file you compiled yourself is one whose contents you know with certainty, which makes every false positive diagnosable rather than mysterious.

Scenario

A rule is written from a sample, tested against that sample, seen to fire, and shipped. Six weeks later the actor recompiles with a bumped version number and the rule stops matching, silently, in the way Section 0.1 described. Nothing about the test was wrong except that it could not fail: the patterns were extracted from that file, so of course they were present in it. The test that would have caught the problem needs a second build of the same program, and that is precisely what nobody has when they are working from one sample.

Whether you may hold live malware is a question of jurisdiction and of your employer's policy, and a course is the wrong place to decide it for you. This one therefore teaches the isolation, provenance and handling discipline where it is relevant and does not recommend a sample source. The specimens below sidestep the question entirely for everything except Module 5, which needs document and script formats and builds those too.

The choice has a teaching benefit beyond the legal one, and it is the reason this course would probably build specimens even if the question did not arise. When a rule fires unexpectedly on a file you compiled, you can open the source and see exactly why. When it fires unexpectedly on a real sample, you have a second investigation on your hands before you can continue with the first.

Why one sample is not enough

The central skill this course teaches is writing a rule that matches files you have not seen, and there is no way to practice that against a single file. A rule tested only against its own source cannot fail: you took the patterns out of that file, so finding them in it again is a tautology dressed as a test.

What you need is a family, meaning several builds of one program differing the way real builds differ. An actor bumps a version, moves infrastructure, changes a compiler flag, occasionally edits an identifier deliberately. Each of those changes some bytes and leaves others alone, and which is which is the whole subject of Module 3. With a family in front of you, "does this rule generalize" stops being a judgment call and becomes a scan you run in ten seconds.

Three builds is the practical minimum and the reason is arithmetic rather than convention. Two builds can only tell you that a pattern survived one change, which a coincidence explains as easily as durability. Three lets you vary two things independently, so you can see a pattern survive one edit and die on another, and that contrast is what makes the result informative rather than merely positive.

It is worth being clear about what the family is not. It is not a sample of a malware family in the intelligence sense, and nothing in it is adversarial. It is a controlled experiment: one program, changes you chose, so that when a rule behaves unexpectedly you can say exactly which edit caused it. Real families give you the same signal with none of the control, which is why practicing on a controlled one first is worth twenty minutes.

The second thing you need is a corpus of files the rule should not match, and it needs to be large enough that a coincidence has room to occur. A rule producing no false positives against ten files has demonstrated almost nothing; against several thousand it has demonstrated something worth recording. Section 0.3 covers assembling one.

The two populations answer questions that look similar and are not, which is why neither substitutes for the other. The family answers whether the rule is broad enough to catch what it should. The corpus answers whether it is narrow enough to leave alone what it should. A rule can pass either comfortably while failing the other badly, and the two failures look nothing alike: the first is a rule that quietly matches one build, the second is a rule that fires four hundred times in an hour.

The four specimens

S1 and S2, a binary and its packed twin. Any small compiled binary, plus the same file run through a packer. The pair matters because packing removes most of what a text-based rule would key on while leaving the file's structure intact, which is the entire argument of Module 4. Building both takes one command each and you keep both, because several sections ask you to compare them.

Keep both is the instruction people ignore, and it costs them later. It is natural to pack in place and move on; then Module 4 asks you to compare the section tables of the original and the packed version and you have only one of them. The comparison is where the teaching is, and it is unavailable from either file alone.

S3, a family of three near-identical builds. The important one. Three compiles of one small C program sharing a mutex-style identifier and differing only in a version string. That shape is realistic: a program that must not run twice needs a name to check for, and that name has to survive rebuilds or the check stops working, while the version number changes because somebody incremented it.

S4, a document and a script. A macro-bearing Office document and a PowerShell file carrying an encoded command. Module 5 works both, and they are worth building early because they demonstrate something the binaries cannot: a file format that hides its own contents from a naive rule.

Building these two takes about five minutes and neither requires anything unusual. A document with a macro is created in any office suite and saved in the macro-enabled format; the PowerShell file is two lines, a base64 string and the command that decodes it. What makes them worth having early is that they fail differently from each other and from anything binary, so meeting all three failure modes on files you constructed removes most of the confusion from Module 5.

Building the family

The family is the specimen to build first if you build only one. Three sources differing in a single string constant, compiled three times.

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 hashes for what is effectively one program. Nothing adversarial produced that difference: no packer, no obfuscation, no attempt to evade anything. Three characters changed in a string constant and the file hash became useless as an identifier.

That result is worth holding onto because it is the honest argument for this whole discipline. Hash-based coverage is defeated not by clever adversaries but by ordinary software maintenance, and an actor who never thinks about detection at all defeats it simply by shipping an update. A rule is the tool that survives that, which is why writing one well is worth seven modules.

What the family shows you in one scan

Two rules against the three builds, one keyed on the version string and one on the mutex identifier. Both are syntactically correct and both fire on the sample they were written from.

yr scan fam.yar fam/
Mutex_Keyed fam/fam2
Mutex_Keyed fam/fam3
Version_Keyed fam/fam1
Mutex_Keyed fam/fam1

Read the rule names down that output rather than the paths. Mutex_Keyed appears three times and Version_Keyed appears once, which is the course's central claim demonstrated on files you built in the last five minutes.

Nothing in the two rules' appearance distinguishes them. Both are four lines, both name one pattern, both compile without a warning from yr check. One of them describes the program and one describes a number somebody incremented, and the only thing that separated them was running both against files the rules did not come from.

It is worth noticing which of the two a person reading a strings dump would more likely have chosen. A version number is short, readable, and looks exactly like an identifier; a long hexadecimal mutex name looks like noise. Visual distinctiveness pointed at the wrong pattern, and Module 3 is largely about why that happens and what to use instead.

The underlying reason is worth stating once here because it recurs throughout the course. A value that exists to be read by a human is maintained by a human, and maintenance means change. A value that exists for the program is left alone, because nobody is looking at it and nobody wants to find out what depends on it. That is why the readable candidate is usually the fragile one, and it is a rule of thumb you can apply to a sample before running anything.

One program, three builds, what survives 1.0.4 1.0.5 1.1.0 file hash fed99930 c9328299 ead7eb13 all different, matches 1 of 3 version string 1.0.4 1.0.5 1.1.0 incremented, matches 1 of 3 mutex identifier Global\{8f2c41d9 · unchanged across all three the program checks it, matches 3 of 3 No packer, no obfuscation, no attempt to evade anything Three characters changed in a string constant, and two of the three indicators died

The identifier the program depends on survives; the one somebody maintains does not.

What built specimens cannot give you

Two limits, stated so they are not a surprise later.

They are not adversarial. Nobody wrote them to evade a rule, so they will not teach you what deliberate obfuscation looks like from the inside. Module 5 covers obfuscation using public samples and documented techniques rather than pretending these files can stand in for it.

They are also cleaner than real files in a way that flatters your rules. Real software carries compiler padding, embedded resources, third-party libraries and years of accumulated cruft, all of which is surface area for a coincidental match. A rule that behaves perfectly against four files you built will meet a messier population the first time it runs anywhere real, which is why the clean corpus in Section 0.3 matters as much as the specimens do.

There is a third limit worth naming because it shapes what Module 4 can demonstrate. These are ELF binaries if you build them on Linux, and a great deal of YARA work in practice targets Windows PE files. Everything about pattern selection and rule structure transfers unchanged, since it is about how programs are built rather than about a file format. What does not transfer is the structural work in Module 4, where the header fields are format-specific, and that module supplies PE files to work on so both routes are covered.

Specimen guide: build the family and prove it generalizes

Step 1, write the template. A short C program with two string constants: a mutex-style identifier that stays fixed, and a VERSION placeholder the loop replaces. Anything that prints them is enough; the program does not need to do anything.

Step 2, compile three builds with the loop above, then hash them and confirm you have three different values.

Step 3, write two rules. One keyed on the version string of the first build, one on the mutex identifier. Run yr check on both before scanning.

Step 4, scan the directory and read the rule names down the output rather than the paths.

Step 5, run the negation on each rule separately with -n, so you see which builds each one missed rather than only which it caught. That is Section 0.1's control applied to a question you now care about.

Do that last step deliberately rather than treating it as a formality. Running Version_Keyed with -n lists the two builds it did not match, and seeing the misses named is different from inferring them from a short list of hits. On a real hunt across thousands of files you will never enumerate the misses, so the habit of asking what a rule did not match is one worth forming while the population is three files and the answer fits on a screen.

✓ Verify

Three distinct hashes: the three builds differ, so any rule that generalizes across them is doing so despite a changed file rather than because the files are identical.

One rule matched three, the other matched one: two correct rules gave completely different coverage on the same three files, and nothing in the rule text predicted which.

The negation named the misses: running each rule with -n listed the builds it did not match, which is how a coverage gap becomes a fact rather than an inference.

Section 0.3 builds the other half of the test set, the corpus of files your rules must leave alone.