Lesson 08 / 16
Anti-Malware Software
Twenty execution items are evaluated in the same set with two detection types: the items signature-based detection catches, misses, and falsely flags; the threshold scan of the behavior score derived from six indicators; how the missed count falls and the false positive count rises as the threshold drops; and the foothold point closed in block mode together with the legitimate work it cuts, versus the same point only made visible in alert mode.
Published Updated
In this lesson
- Runs signature and behavior-based detection over the same event set and extracts the three numbers separately
- Scans the decision threshold and shows that the missed count and the false positive count move in opposite directions
- Separates the same detection's counterpart in two modes: the foothold point that closes versus the point that is only made visible
Contents
The previous lesson could not stop seven of the ten types by closing points, and two of them had no point to close in the first place. The layer that stops those seven asks a different question: once something runs on the endpoint, how is it known that this item is a problem? There are two separate answers, and both can be measured over the same execution set.
The first answer relies on recognition: I have seen this item before and flagged it as a problem. The second relies on behavior: what this item does resembles what problem items do. The first has no single setting; the second has a threshold, and this lesson scans that threshold end to end. Three numbers are written together for every run: how many bad items were caught, how many slipped through, how many legitimate items were flagged by mistake. Detection on the network side was measured in another course, and the threshold was not modeled there; what is measured here is exactly the threshold itself.
- EP7. Twenty execution items, six indicators, and their weights belong to one field laptop
endpoint of a fictional regional measurement network. No real item is run; what is measured is the
model built with
node. - EP8. The true class of each item is known in the model. In reality this class is only known after investigation; it is assumed known here so that the missed count and the false positive count can be counted separately.
- EP9. The behavior score is the sum of indicator weights, and the weights are fixed. In real systems weights are learned and drift over time; a fixed weight simplifies the curve’s shape, it does not change the opposite direction of the two errors.
- EP10. The signature was modeled with three values: exact match, broad match, and no match. A broad match means a rule that looks at a packaging format, and it can also touch a legitimate item.
- EP11. An item either runs or is blocked; partial blocking, quarantine, and rollback were not modeled.
- EP12. The foothold point column comes from the previous lesson’s inventory: it is the number of points the item writes to. It has no relation to the score; it is carried as a separate column.
Score Derived from Indicators
The behavior score is not a number assigned by hand; it is the sum of the weights of six indicators. The weights say how distinguishing an indicator is: writing to another process’s memory and adding an item to an automatically run point carry the highest weight, running at an unusual hour the lowest. None of the indicators is a problem on its own; legitimate administration tools do the same things, and that is where the model’s tension comes from.
// events.mjs — execution events on the endpoint, indicator weights, and two detection types; model export const INDICATOR = { 'writes to a foothold point': 3, 'writes to another process': 3, 'unsigned binary': 2, 'bulk file modification': 2, 'sustained external connection': 2, 'runs outside user hours': 1, }; const B = Object.keys(INDICATOR); const [N, S, I, D, G, Z] = B; // [item, true class, signature status, indicators, foothold points written] export const EVENT = [ ['network-spreading component', 'bad', 'exact', [N, I, G, Z], 2], ['remote-command listener', 'bad', 'none', [N, S, G], 2], ['data-encrypting component', 'bad', 'exact', [I, D, G, Z], 0], ['keystroke and screen logger', 'bad', 'none', [N, S, Z], 3], ['memory-resident loader', 'bad', 'none', [I, G], 0], ['living-off-the-land step', 'bad', 'none', [G, Z], 0], ['endpoint management agent', 'legit', 'none', [N, S, G], 2], ['patch deployment agent', 'legit', 'none', [N, D, Z], 1], ['backup tool', 'legit', 'none', [D, G, Z], 0], ['inventory scan tool', 'legit', 'none', [N, G], 1], ['enterprise installer package', 'legit', 'broad', [N, I], 1], ['remote support client', 'legit', 'none', [S, G], 0], ['field measurement uploader', 'legit', 'none', [I, G, Z], 0], ['compiler output directory', 'legit', 'none', [I, D], 0], ['scheduled report script', 'legit', 'none', [N, Z], 1], ['log collector', 'legit', 'none', [G, Z], 0], ['disk encryption helper', 'legit', 'none', [D, Z], 0], ['archiving tool', 'legit', 'none', [D], 0], ['browser', 'legit', 'none', [G], 0], ['document editor', 'legit', 'none', [], 0], ]; export const score = (o) => o[3].reduce((a, b) => a + INDICATOR[b], 0); export const MAX_SCORE = Object.values(INDICATOR).reduce((a, b) => a + b, 0); export const signatureAlerts = (o) => o[2] !== 'none'; export const behaviorAlerts = (o, threshold) => score(o) >= threshold; export function count(alerts) { const bad = EVENT.filter((o) => o[1] === 'bad'); const caught = bad.filter(alerts), falsePositive = EVENT.filter((o) => o[1] === 'legit' && alerts(o)); return { caught, missed: bad.filter((o) => !alerts(o)), falsePositive, point: caught.reduce((a, o) => a + o[4], 0), legitPoint: falsePositive.reduce((a, o) => a + o[4], 0) }; }
The Signature’s Three Numbers
// run1.mjs — event set, indicator-derived behavior score, and signature-based detection's three numbers import { EVENT, INDICATOR, MAX_SCORE, score, signatureAlerts, count } from './events.mjs'; console.log(`${EVENT.length} execution items, ${EVENT.filter((o) => o[1] === 'bad').length} bad; ` + `${Object.keys(INDICATOR).length} indicators, max score ${MAX_SCORE}\n`); console.log(`${'indicator'.padEnd(30)} ${'weight'.padStart(7)} ${'bad'.padStart(12)} ${'legit'.padStart(13)}`); for (const [b, a] of Object.entries(INDICATOR)) { const g = EVENT.filter((o) => o[3].includes(b)); console.log(`${b.padEnd(30)} ${String(a).padStart(7)} ${String(g.filter((o) => o[1] === 'bad').length).padStart(12)} ` + `${String(g.filter((o) => o[1] === 'legit').length).padStart(13)}`); } console.log(`\n${'item'.padEnd(30)} ${'class'.padStart(5)} ${'sig'.padStart(5)} ${'score'.padStart(5)} ${'point'.padStart(5)}`); for (const o of [...EVENT].sort((a, b) => score(b) - score(a))) console.log(`${o[0].padEnd(30)} ${o[1].padStart(5)} ${o[2].padStart(5)} ${String(score(o)).padStart(5)} ${String(o[4]).padStart(5)}`); const r = count(signatureAlerts); console.log(`\nsignature-based detection: caught ${r.caught.length}/${r.caught.length + r.missed.length}, ` + `missed ${r.missed.length}, false positive ${r.falsePositive.length}`); console.log(` missed: ${r.missed.map((o) => o[0]).join('; ')}`); console.log(` false positive: ${r.falsePositive.map((o) => `${o[0]} (${o[2]} signature)`).join('; ')}`);
20 execution items, 6 bad; 6 indicators, max score 13 indicator weight bad legit writes to a foothold point 3 3 5 writes to another process 3 2 2 unsigned binary 2 3 3 bulk file modification 2 1 5 sustained external connection 2 5 7 runs outside user hours 1 4 6 item class sig score point network-spreading component bad exact 8 2 remote-command listener bad none 8 2 endpoint management agent legit none 8 2 data-encrypting component bad exact 7 0 keystroke and screen logger bad none 7 3 patch deployment agent legit none 6 1 backup tool legit none 5 0 inventory scan tool legit none 5 1 enterprise installer package legit broad 5 1 remote support client legit none 5 0 field measurement uploader legit none 5 0 memory-resident loader bad none 4 0 compiler output directory legit none 4 0 scheduled report script legit none 4 1 living-off-the-land step bad none 3 0 log collector legit none 3 0 disk encryption helper legit none 3 0 archiving tool legit none 2 0 browser legit none 2 0 document editor legit none 0 0 signature-based detection: caught 2/6, missed 4, false positive 1 missed: remote-command listener; keystroke and screen logger; memory-resident loader; living-off-the-land step false positive: enterprise installer package (broad signature)
The indicator table gives the first result, and it is not encouraging at all: four of the six indicators appear more often in legitimate items than in bad items, and two appear in exactly the same number; none stands out on the bad side. Sustained external connection appears in five bad and seven legitimate items; writes to a foothold point in three bad and five legitimate items. This is not a modeling flaw but a fact of endpoints: the number of legitimate programs running on an endpoint is far larger than the number of bad items, so no indicator stands out on the bad side by raw count.
The sorted list shows the result of this in a single line. One of the three highest-scoring items is legitimate and sits at the same place as two bad items, with a score of eight: the endpoint management agent writes to automatically run points, injects code into other processes, and holds a sustained outbound connection. No behavior-based threshold can separate this item from the other two, because there is no number to separate them with.
Signature-based detection’s three numbers are at the bottom of the list. It catches two of the six bad items, misses four, and flags one legitimate item. What the missed items have in common is that they were never seen before; the legitimate item it flags happens to touch a broad signature. Zero false positives is the frequently cited advantage of signature-based detection, and it is not true even in the model: a single broadly written rule ends that advantage.
Moving the Threshold
// run2.mjs — behavior threshold scan; the opposite direction of the two errors and the cost of combined detection import { EVENT, score, signatureAlerts, behaviorAlerts, count } from './events.mjs'; const BAD = EVENT.filter((o) => o[1] === 'bad').length; console.log(`${'threshold'.padStart(9)} ${'caught'.padStart(9)} ${'missed'.padStart(6)} ${'false'.padStart(6)} ` + `${'comb. caught'.padStart(13)} ${'combined false'.padStart(15)} ${'total false'.padStart(13)}`); const result = []; for (let e = 1; e <= 10; e++) { const d = count((o) => behaviorAlerts(o, e)); const b = count((o) => signatureAlerts(o) || behaviorAlerts(o, e)); result.push({ e, d, b }); console.log(`${String(e).padStart(9)} ${`${d.caught.length}/${BAD}`.padStart(9)} ${String(d.missed.length).padStart(6)} ` + `${String(d.falsePositive.length).padStart(6)} ${`${b.caught.length}/${BAD}`.padStart(13)} ${String(b.falsePositive.length).padStart(15)} ` + `${String(b.missed.length + b.falsePositive.length).padStart(13)}`); } const min = result.reduce((a, s) => (s.b.missed.length + s.b.falsePositive.length < a.b.missed.length + a.b.falsePositive.length ? s : a)); const full = result.filter((s) => s.b.missed.length === 0); console.log(`\nthreshold minimizing total false: ${min.e} (missed ${min.b.missed.length} + false ${min.b.falsePositive.length})`); console.log(`thresholds that never miss: ${full.map((s) => `${s.e} (false ${s.b.falsePositive.length})`).join(', ')}`); console.log(`zero-false-positive threshold: ${result.filter((s) => s.b.falsePositive.length === 0).map((s) => `${s.e} (missed ${s.b.missed.length})`).join(', ') || 'none'}`); console.log(`\n${'threshold'.padStart(9)} ${'closed points'.padStart(13)} ${'legit work lost'.padStart(16)} ` + `${'seen points'.padStart(13)} ${'work lost'.padStart(10)} ${'manual review alerts'.padStart(20)}`); for (const { e, b } of result.filter((s) => [3, 6, 8, 9].includes(s.e))) console.log(`${String(e).padStart(9)} ${String(b.point).padStart(13)} ${String(b.falsePositive.length).padStart(16)} ` + `${String(b.point).padStart(13)} ${'0'.padStart(10)} ${String(b.caught.length + b.falsePositive.length).padStart(20)}`); const six = count((o) => signatureAlerts(o) || behaviorAlerts(o, 6)); console.log(`\nthreshold 6 missed bad items: ${six.missed.map((o) => `${o[0]} (score ${score(o)})`).join('; ')}`); console.log(`threshold 6 legit work lost : ${six.falsePositive.map((o) => `${o[0]} (score ${score(o)})`).join('; ')}`);
threshold caught missed false comb. caught combined false total false
1 6/6 0 13 6/6 13 13
2 6/6 0 13 6/6 13 13
3 6/6 0 11 6/6 11 11
4 5/6 1 9 5/6 9 10
5 4/6 2 7 4/6 7 9
6 4/6 2 2 4/6 3 5
7 4/6 2 1 4/6 2 4
8 2/6 4 1 3/6 2 5
9 0/6 6 0 2/6 1 5
10 0/6 6 0 2/6 1 5
threshold minimizing total false: 7 (missed 2 + false 2)
thresholds that never miss: 1 (false 13), 2 (false 13), 3 (false 11)
zero-false-positive threshold: none
threshold closed points legit work lost seen points work lost manual review alerts
3 7 11 7 0 17
6 7 3 7 0 7
8 4 2 4 0 5
9 2 1 2 0 3
threshold 6 missed bad items: memory-resident loader (score 4); living-off-the-land step (score 3)
threshold 6 legit work lost : endpoint management agent (score 8); patch deployment agent (score 6); enterprise installer package (score 5)
The scan gives the opposite direction of two columns row by row. At threshold three, all six of the six bad items are caught, and eleven legitimate items are flagged along with them. At threshold nine, the false positive count drops to zero and behavior-based detection catches no bad items at all. Every row in between is a trade-off, and the trade-off ratio is not constant: going from five to six drops the false positive count from seven to two while never changing the caught count — this is the steepest point of the curve. Going from seven to eight, by contrast, drops the caught count from four to two without lowering the false positive count at all.
Combining signature with behavior is read in the difference between the third and fourth columns. There is a gain at high thresholds: at threshold nine, behavior catches nothing while combined detection catches two items. At low thresholds the signature contributes nothing, it only raises the false positive count by one. The two detection types complement each other for this reason: one works on the known, the other on the unknown, and as the known set shrinks the load the threshold carries grows.
The bottom table separates the course’s two columns. At threshold six, four bad items are caught and, in block mode, seven foothold points close; in exchange three legitimate jobs are cut, and among those cut are the patch deployment agent and the endpoint management agent — the very mechanisms that protect the endpoints. In alert mode the same seven points are only made visible, no legitimate job is cut, and in exchange seven items wait for manual review. The closed-point count and the visible-point count are numerically the same; what makes the difference is that in one the point disappears, in the other it is only written to a record.
Lowering the threshold to three shows a measurable trap in this table. The caught bad item count rises from four to six, but the closed foothold point count stays at seven, because the two newly added items have no persistence point. Cut legitimate work rises from three to eleven. A setting that decides by caught item count counts this as an improvement; a setting that decides by foothold point sees no gain at all.
The scores of the two missed items open a separate question when looked at from the bottom of the list. The memory-resident loader sits below three legitimate items with a score of four; the living-off-the-land step sits at the same place as two legitimate items with a score of three. A threshold that would catch both also catches every legitimate item with a higher score; this is not a problem that a threshold setting can solve. The item with the lower score is not doing less work — it is doing work that leaves fewer indicators, because every tool it uses is already standing on the endpoint and is signed. This distinction will be measured by number not in the next lesson but in the fifth.
Summary
- 4 of the 6 indicators appear more often in legitimate items than in bad items, and 2 appear in exactly the same number. Since the count of legitimate software on an endpoint is far larger, no indicator stands out on the bad side by raw count.
- One of the 3 highest-scoring items is legitimate and shares the same score (8) as 2 bad items: the endpoint management agent. No behavior threshold can separate these three from each other.
- Signature-based detection catches 2 of the 6 bad items, misses 4, and flags 1 legitimate item because of a single broadly written rule. “Signatures produce no false positives” is wrong even in the model.
- At threshold 3, 6/6 are caught and 11 legitimate items are cut; at threshold 9, false positives are zero and behavior catches nothing. The threshold that minimizes total error is 7 (2 missed + 2 false); no threshold zeroes both errors at once.
- At threshold 6, 7 foothold points close and 3 legitimate jobs are cut, including the patch deployment agent and the endpoint management agent. In alert mode the same 7 points are only made visible, cut work is 0, manual review alerts are 7.
- Lowering the threshold from 6 to 3 raises the caught count from 4 to 6 but leaves the closed point count at 7: the two newly caught items have no persistence point. Cut legitimate work rises from 3 to 11.
Next Step
Alert mode was this lesson’s cheapest option: no legitimate job was cut. Its cost looked at the time like a queue of seven items, but its real cost lies elsewhere. For an item to be merely visible, someone must have recorded it, and not everything on the endpoint is recorded. That was also what the two items missed at threshold six had in common: neither of them wrote anything to disk. The next lesson’s question is this: how many of the foothold points does endpoint telemetry see, how many never appear in any source at all, and how much does the volume of collected data shorten the retention window.
To keep your progress and take notes, Log in
My notes
Log in to take notes.