Lesson 11 / 16
Living Off the Land
The overlap of fourteen built-in, signed tools across seven capability classes is counted: an average of 3.4 tools carrying the same capability, three signature rules flagging zero of twenty-four runs, and three context dimensions — parent process, command pattern, timing — evaluated singly, combined, and intersected; five of six suspicious runs are separated out, one is identical in four dimensions to a legitimate run.
Published Updated
In this lesson
- Counts the number of built-in tools carrying the same capability class and shows the option left behind by blocking one tool
- Measures that the signature-based rule set flags zero items in built-in, signed tool runs
- Evaluates three context dimensions separately and together: counts the separated suspicious run and the false positive together
Contents
The previous lesson’s allowlist closed the paths it could close and named the paths it could not: the remote support flow could not be removed. The tool that carries out that flow sits on the endpoint, signed and recorded in the inventory’s baseline. This lesson counts what happens on the detection side when that tool — and thirteen others like it — is run with malicious intent.
Three things are measured. The first is overlap: how many separate built-in tools can do the same job. The second is how many items the signature-based rule flags among these runs. The third is how many suspicious runs the behavioral context — parent process, command pattern, and timing — can separate, and how many legitimate runs it flags in return. No command, parameter, or step sequence is written in this lesson; the runs are modeled at the level of type and count. Trying these techniques on a system without written authorization and a defined scope is both illegal and unprofessional.
- EP25. Fourteen tools, seven capability classes, and twenty-four runs belong to one endpoint of
the fictional regional measurement network. What is measured is the model built with
node. - EP26. All of the tools are built-in, validly signed, and recorded in the baseline. This is the lesson’s definition, not its assumption: a tool not in the inventory was already measured in another lesson.
- EP27. The true class of each run is known in the model. In reality it is known only after investigation; it is assumed known here so that the separated and the false positive can be counted separately.
- EP28. Context was modeled with three discrete dimensions. In reality each dimension has a continuous value and needs a threshold; the discrete value shrinks the rule count, it does not change the direction of the combinations.
- EP29. The command pattern classes are a definition of form, not content; no example command was written.
- EP30. A run either matches a rule or it does not; frequency, recurrence, and correlation with other runs on the same endpoint were not modeled. All three increase separating power, and all three need more data.
How Many Tools Can Do the Same Job
// tools.mjs — built-in and signed tool inventory on the endpoint, capabilities, and run record; model // tool type -> capability classes it carries (all assumed built-in and signed) export const TOOL = { 'script interpreter': ['code interpretation', 'process launch', 'network data fetch'], 'shell interpreter': ['code interpretation', 'process launch'], 'file transfer tool': ['network data fetch'], 'package manager': ['network data fetch', 'process launch'], 'installer engine': ['network data fetch', 'process launch'], 'certificate helper': ['network data fetch'], 'remote management client': ['remote connection', 'process launch'], 'management script interface': ['remote connection', 'process launch', 'code interpretation'], 'archiver': ['archiving'], 'backup helper': ['archiving', 'scheduling'], 'scheduler interface': ['scheduling', 'process launch'], 'system information tool': ['inventory read'], 'configuration query tool': ['inventory read'], 'log query tool': ['inventory read'], }; export const NAME = Object.keys(TOOL); export const CAPABILITY = [...new Set(Object.values(TOOL).flat())]; export const carries = (y) => NAME.filter((a) => TOOL[a].includes(y)); // signature-based rule set: each rule looks at one field of a run export const SIGNATURE_RULE = [ ['unknown file identity', (k) => k.identity === 'unknown'], ['unsigned binary', (k) => k.signature === 'none'], ['item not in baseline', (k) => k.baseline === false], ]; // run: [tool, parent process class, command pattern, time slot, true class] export const RUN = [ ['script interpreter', 'scheduler', 'ordinary', 'maintenance window', 'legitimate'], ['script interpreter', 'management agent', 'ordinary', 'business hours', 'legitimate'], ['shell interpreter', 'scheduler', 'ordinary', 'off hours', 'legitimate'], ['archiver', 'scheduler', 'ordinary', 'maintenance window', 'legitimate'], ['file transfer tool', 'management agent', 'ordinary', 'business hours', 'legitimate'], ['file transfer tool', 'scheduler', 'externally sourced parameter', 'maintenance window', 'legitimate'], ['remote management client', 'user session', 'ordinary', 'business hours', 'legitimate'], ['remote management client', 'remote session', 'ordinary', 'business hours', 'legitimate'], ['scheduler interface', 'management agent', 'ordinary', 'business hours', 'legitimate'], ['certificate helper', 'service', 'ordinary', 'business hours', 'legitimate'], ['system information tool', 'management agent', 'ordinary', 'business hours', 'legitimate'], ['configuration query tool', 'user session', 'ordinary', 'business hours', 'legitimate'], ['log query tool', 'management agent', 'long single line', 'off hours', 'legitimate'], ['backup helper', 'scheduler', 'ordinary', 'off hours', 'legitimate'], ['installer engine', 'user session', 'ordinary', 'business hours', 'legitimate'], ['package manager', 'management agent', 'ordinary', 'maintenance window', 'legitimate'], ['management script interface', 'service', 'ordinary', 'business hours', 'legitimate'], ['shell interpreter', 'document application', 'ordinary', 'business hours', 'legitimate'], ['script interpreter', 'document application', 'encoded input', 'business hours', 'suspicious'], ['shell interpreter', 'remote session', 'long single line', 'off hours', 'suspicious'], ['file transfer tool', 'user session', 'externally sourced parameter', 'off hours', 'suspicious'], ['archiver', 'service', 'ordinary', 'off hours', 'suspicious'], ['remote management client', 'user session', 'ordinary', 'business hours', 'suspicious'], ['scheduler interface', 'remote session', 'ordinary', 'business hours', 'suspicious'], ]; export const record = (k) => ({ tool: k[0], identity: 'known', signature: 'valid', baseline: true }); export const CONTEXT = [ ['parent process', (k) => ['document application', 'remote session'].includes(k[1])], ['command pattern', (k) => ['encoded input', 'externally sourced parameter'].includes(k[2])], ['timing', (k) => k[3] === 'off hours' && k[1] !== 'scheduler'], ];
// run1.mjs — tool count carrying the same capability, and items the signature-based rule set flags import { TOOL, NAME, CAPABILITY, RUN, SIGNATURE_RULE, carries, record } from './tools.mjs'; console.log(`${NAME.length} built-in tools, ${CAPABILITY.length} capability classes, ${RUN.length} run records\n`); console.log(`${'capability class'.padEnd(24)} ${'tools'.padStart(12)} ${'left if one blocked'.padStart(23)}`); for (const y of CAPABILITY) console.log(`${y.padEnd(24)} ${String(carries(y).length).padStart(12)} ${String(carries(y).length - 1).padStart(23)}`); const many = CAPABILITY.filter((y) => carries(y).length > 1); console.log(`\ncapabilities tied to one tool: ${CAPABILITY.length - many.length}/${CAPABILITY.length}`); console.log(`capabilities with multiple tools: ${many.length}/${CAPABILITY.length}, average ` + `${(many.reduce((a, y) => a + carries(y).length, 0) / many.length).toFixed(1)} tools`); const suspicious = RUN.filter((k) => k[4] === 'suspicious'); console.log(`\n${'signature rule'.padEnd(26)} ${'flagged runs'.padStart(18)} ${'of suspicious'.padStart(16)}`); for (const [name, f] of SIGNATURE_RULE) { const flagged = RUN.filter((k) => f(record(k))); console.log(`${name.padEnd(26)} ${String(flagged.length).padStart(18)} ` + `${`${flagged.filter((k) => k[4] === 'suspicious').length}/${suspicious.length}`.padStart(16)}`); } console.log(`\ntool count used by suspicious runs: ${new Set(suspicious.map((k) => k[0])).size}`); console.log(`how many of these tools have a valid signature and a baseline record: ` + `${[...new Set(suspicious.map((k) => k[0]))].filter((a) => NAME.includes(a)).length}/${new Set(suspicious.map((k) => k[0])).size}`);
14 built-in tools, 7 capability classes, 24 run records capability class tools left if one blocked code interpretation 3 2 process launch 7 6 network data fetch 5 4 remote connection 2 1 archiving 2 1 scheduling 2 1 inventory read 3 2 capabilities tied to one tool: 0/7 capabilities with multiple tools: 7/7, average 3.4 tools signature rule flagged runs of suspicious unknown file identity 0 0/6 unsigned binary 0 0/6 item not in baseline 0 0/6 tool count used by suspicious runs: 6 how many of these tools have a valid signature and a baseline record: 6/6
The first table gives, in numbers, why one defensive move does not work. None of the seven capability classes is tied to a single tool; an average of 3.4 tools carry the same capability. Seven tools can launch a process; five can fetch data from the network. Removing a tool from the application allowlist does not remove the capability, it only reduces the option count by one — and if the removed tool is used for legitimate work, it stops that work too. This is the measure of the overlap: blocking has a gain of one and a cost of one.
The second table gives the result the lesson is named for. Three signature-based rules flag zero of the twenty-four runs and catch zero of the six suspicious runs. This is not because the rules are badly written, but because the field they look at carries the same value in every one of these runs: file identity known, signature valid, item recorded in the baseline. All six of the tools used by the suspicious runs pass all three criteria. Signature-based detection does not give a wrong answer; it gives no answer at all, because the question it asks is not discriminating here.
Three Context Dimensions
A question that could be discriminating looks not at the tool but at the run: what launched this tool, how was it invoked, when did it run. All three dimensions can already be recorded on the endpoint; the separating power of all three is measured separately and together.
// run2.mjs — three context dimensions singly, combined, and intersected; the run that cannot be separated import { RUN, CONTEXT } from './tools.mjs'; const S = RUN.filter((k) => k[4] === 'suspicious'), M = RUN.filter((k) => k[4] === 'legitimate'); const matches = (k) => CONTEXT.filter(([, f]) => f(k)).map(([a]) => a); const measure = (rule) => [S.filter(rule).length, M.filter(rule).length]; console.log(`${'context set'.padEnd(44)} ${'separated suspicious'.padStart(21)} ${'false positive'.padStart(14)}`); for (let m = 1; m < 8; m++) { const chosen = CONTEXT.filter((_, i) => m & (1 << i)); const [a, y] = measure((k) => chosen.some(([, f]) => f(k))); console.log(`${chosen.map(([name]) => name).join(' or ').padEnd(44)} ${`${a}/${S.length}`.padStart(21)} ${`${y}/${M.length}`.padStart(14)}`); } for (const n of [2, 3]) { const [a, y] = measure((k) => matches(k).length >= n); console.log(`${`at least ${n} dimensions at once`.padEnd(44)} ${`${a}/${S.length}`.padStart(21)} ${`${y}/${M.length}`.padStart(14)}`); } console.log(`\n${'run'.padEnd(30)} ${'parent process'.padStart(22)} ${'pattern'.padStart(29)} ${'time'.padStart(15)} matching dimensions`); for (const k of S) console.log(`${k[0].padEnd(30)} ${k[1].padStart(22)} ${k[2].padStart(29)} ${k[3].padStart(15)} ${matches(k).join(', ') || '-'}`); const key = (k) => k.slice(0, 4).join('|'); const twin = S.filter((s) => M.some((m) => key(m) === key(s))); console.log(`\nsuspicious runs identical to a legitimate run in four dimensions: ${twin.length}/${S.length}`); for (const s of twin) console.log(` ${s[0]} / ${s[1]} / ${s[2]} / ${s[3]}`); console.log(`suspicious runs matching no context dimension: ${S.filter((k) => matches(k).length === 0).length}/${S.length}`);
context set separated suspicious false positive parent process 3/6 2/18 command pattern 2/6 1/18 parent process or command pattern 4/6 3/18 timing 3/6 1/18 parent process or timing 5/6 3/18 command pattern or timing 4/6 2/18 parent process or command pattern or timing 5/6 4/18 at least 2 dimensions at once 3/6 0/18 at least 3 dimensions at once 0/6 0/18 run parent process pattern time matching dimensions script interpreter document application encoded input business hours parent process, command pattern shell interpreter remote session long single line off hours parent process, timing file transfer tool user session externally sourced parameter off hours command pattern, timing archiver service ordinary off hours timing remote management client user session ordinary business hours - scheduler interface remote session ordinary business hours parent process suspicious runs identical to a legitimate run in four dimensions: 1/6 remote management client / user session / ordinary / business hours suspicious runs matching no context dimension: 1/6
Single dimensions are weak: the best one separates three of the six suspicious runs. Combinations do better, but they do not add up cleanly. Parent process together with timing separates five of them and flags three legitimate runs. Adding the third dimension — command pattern — does not change the separated count at all and raises the false positives from three to four. Command pattern alone separated two runs; both were runs the other two dimensions already caught. If adding a layer closes zero additional paths, that layer counts as unmeasured, and this rule holds on the detection side here too.
The intersection rows show the gain in the opposite direction. When at least two dimensions are required to match at once, the separated count drops to three, but the false positives go to zero: none of the eighteen legitimate runs match two dimensions at once. This cuts the number of alerts to review to three, and all three of them are real. The choice here has the same shape as in earlier lessons — five separated and four wrong, or three separated and zero wrong.
The list below names the run that cannot be separated, and the reason is measurable: a remote management client running from a user session with an ordinary pattern during business hours is identical, in four dimensions, to a legitimate run in the list. There is no field left to separate it by. The only way to separate this run in the model is to add a fourth dimension — the endpoint it connects to, the volume of data carried, the other work the same user did that day — and each fourth dimension means a new telemetry source, which means a new cut into the window measured in the third lesson.
Blocking’s Two Columns
Blocking instead of separating is also an option: removing the tools that appear in suspicious runs from the application allowlist. The gain and the cost of this move can be counted on the same model.
// run3.mjs — the gain and cost of blocking a tool: stopped runs and the remaining alternative tool import { TOOL, RUN, carries } from './tools.mjs'; const used = [...new Set(RUN.map((k) => k[0]))]; console.log(`${'blocked tool'.padEnd(30)} ${'stopped legit'.padStart(13)} ${'stopped suspicious'.padStart(19)} ` + `${'other tool, same capability'.padStart(27)}`); let totalLegit = 0, totalSuspicious = 0; for (const a of used) { const k = RUN.filter((x) => x[0] === a); const [m, s] = [k.filter((x) => x[4] === 'legitimate').length, k.filter((x) => x[4] === 'suspicious').length]; const alternative = new Set(TOOL[a].flatMap((y) => carries(y)).filter((x) => x !== a)); if (s > 0) { totalLegit += m; totalSuspicious += s; } console.log(`${a.padEnd(30)} ${String(m).padStart(13)} ${String(s).padStart(19)} ${String(alternative.size).padStart(27)}`); } const suspiciousTools = used.filter((a) => RUN.some((x) => x[0] === a && x[4] === 'suspicious')); const transferable = suspiciousTools.filter((a) => TOOL[a].some((y) => carries(y).length > 1)); console.log(`\ntools used by suspicious runs: ${suspiciousTools.length}`); console.log(`cost of blocking all of them: ${totalLegit} legitimate runs stop, ${totalSuspicious} suspicious runs stop`); console.log(`of these tools, how many can hand their capability to another tool: ${transferable.length}/${suspiciousTools.length}`);
blocked tool stopped legit stopped suspicious other tool, same capability script interpreter 2 1 8 shell interpreter 2 1 6 archiver 1 1 1 file transfer tool 2 1 4 remote management client 2 1 6 scheduler interface 1 1 7 certificate helper 1 0 4 system information tool 1 0 2 configuration query tool 1 0 2 log query tool 1 0 2 backup helper 1 0 2 installer engine 1 0 8 package manager 1 0 8 management script interface 1 0 6 tools used by suspicious runs: 6 cost of blocking all of them: 10 legitimate runs stop, 6 suspicious runs stop of these tools, how many can hand their capability to another tool: 6/6
Blocking all six tools stops six suspicious runs and stops ten legitimate runs along with them: the ratio is greater than one, meaning more than one legitimate job is cut for every run gained. The column on the right, however, shows that the gain is not lasting. Other tools carrying the capabilities of every blocked tool still sit on the endpoint — eight for the script interpreter, seven for the scheduler interface, one in the narrowest case, the archiver. All six of the six tools can hand their capability to another tool.
Reading this table separates the course’s two columns once more. Blocking really does close something here, but what it closes is the tool, not the capability; as long as the capability stays in the inventory, no path is closed. Separating, on the other hand, closes nothing — it only makes visible — and for the five suspicious runs it makes visible, it also flags four legitimate runs.
Summary
- None of the 7 capability classes is tied to a single tool; an average of 3.4 built-in tools carry the same capability. 7 tools can launch a process. Blocking a tool does not remove the capability, it reduces the option by one and stops a legitimate job.
- 3 signature-based rules flag 0 of the 24 runs. All 6 of the tools used by suspicious runs have a known identity, a valid signature, and a baseline record; the rule does not give a wrong answer, it gives no answer.
- The best single context dimension separates 3 of 6 suspicious runs. Parent process together with timing separates 5 of them and produces 3 false positives.
- Adding a third dimension raises false positives from 3 to 4 without changing the separated count: a layer that closes zero paths counts as unmeasured here too.
- When at least 2 dimensions are required to match at once, the separated count drops to 3 and the false positives go to 0. None of the 18 legitimate runs match two dimensions at once.
- 1 suspicious run is identical to a legitimate run in four dimensions and matches no context dimension. Separating it requires a fourth dimension — a new telemetry source.
Next Step
This lesson gave, in numbers, the limit of separating the misuse of a tool, but what it measured were individual runs. A run is transient: it ends and leaves only a record behind. What persists is the item that run writes on the endpoint — the foothold points counted in the first lesson. The topic therefore returns to where it began. The next lesson’s question is this: how many items are the persistence point types on the endpoint as an inventory, how many of them do five separate audits close, how many do they only log, and how many points remain that no audit either closes or sees.
To keep your progress and take notes, Log in
My notes
Log in to take notes.