~/tutorials/042-the-other-chair.md
042: The other chair
Every technique in this tier produces an event. The last lesson ended looking at your own callback traffic. Sit there a while. This lesson does.
Purple is not a compromise between attack and defense. It is the statement that they are the same work with the log facing the other way.
The rule, one more time
The authorization line has run through every lesson of this site, and it was said first and plainest in the tier you finished:
cat ~/tutorials/004-first-scan.md | head -14
Everything in this lesson is aimed at machines you own. Not because scanning is dangerous, but
because the habit of asking permission is the difference between a researcher and a liability.
The chair you sit in changes nothing about that line. It is the same rule read from both sides.
The whole loop, run once, with receipts
The lesson's own four step loop, executed end to end on the callback lab, because a loop described is a loop not run.
Step one, the attack. The metronome client from lesson 041, two second interval, against the same listener, with its arrival log doing the work of the defender's telemetry.
Step two, the detection. Read the interval column and write the rule for the thing, not the class. The thing is a metronome, so the rule is a metronome's signature: the standard deviation of the last eight intervals. Below half a second of spread is machine regularity, because humans and printers and sleep schedules do not repeat to the millisecond. The rule is one Python file, eleven lines, and it reads the same log the defender would.
Step three, the verdict on the attack itself:
window=8 intervals n=8 stdev=0.000s -> METRONOME
Eight for eight, no spread at all, the rule fires. Note what did not have to be true: the rule does not know the destination, the port, the user agent, or the protocol. It keys on time alone, which is the cheapest telemetry there is, and it caught the implant anyway.
Step four, break it on purpose. One line changed about the attack, the jitter line from last lesson, base interval plus uniform random. Same listener, same rule, same window:
window=8 intervals n=8 stdev=0.670s -> quiet
The rule goes silent. One line of random.uniform defeated eleven lines of detection, and the honest writeup is that the rule was keying on regularity, regularity was the attack's only sin, and the attack stopped sinning. That is the table with two columns: what I did, what fired. The column titles matter less than the discipline of filling both.
What survives the break is the lesson from 041 coming back: the jittered client still checks in three times a minute, forever, to a destination nothing else visits. A volume rule, arrivals per minute above a threshold for a fresh destination, holds where the interval rule fell. Writing that rule and breaking it is the exercise below, and its break is harder than a sleep call.
What the last lessons look like from the chair
The sweep is a spike of icmp on one subnet, hundreds of hosts asked one question in seconds. The enumeration is a version banner served to an address that asked and never came back. The credential hunt is a process reading another user's home directory. The tunnel is one internal host opening an ssh session outward. The callback is the metronome from last lesson.
Not one of these requires exotic telemetry. Ping logs, service logs, file audit, egress rules, proxy logs. The events were always there. Detection is deciding to look, and knowing which looking is worth the storage.
Signals
A defender's day is ruled by false positives. A rule that fires daily on the backup agent gets muted within a week, and the day the real thing walks past it, nobody reads the page. Precision is not a virtue in detection. It is the whole budget.
The craft: write the rule for the thing, not the class. Not outbound ssh exists. An internal host that has never done ssh before opened outbound ssh to a fresh domain. The second sentence costs more to build and survives contact with the network, because the first sentence was muted before you were hired.
The metronome rule above is a small instance of exactly that craft. Outbound http exists is a class rule and fires on half the building. Intervals with zero spread is a thing rule, and in the receipt it fired on precisely the implant and nothing else in the log.
Try it
- Rerun the callback lab from 041 with the listener's log as your telemetry, and write the interval rule yourself before reading the numbers. Then run it: the metronome's
stdev 0.000and the jitter'sstdev 0.670are your two-column table, one row each. - Write the volume rule that survives the jitter: arrivals per minute, per destination, above a threshold. Test it against both clients from 041. It should read the same on both, and that sameness is the point.
- Break the volume rule. You will need to change the attack's volume, not its rhythm, and the honest breaks are slower intervals, or fewer hours, or both. Note what the attack lost in exchange. Detection that cannot be broken quietly is detection that gets broken loudly, and you have now written both halves of that sentence.
The attack half of this tier taught you what the events are. This is the half that decides whether anybody sees them.
Reveal the answer
The interval rule, complete, as run for the receipts above:
import sys, statistics
window = int(sys.argv[1]) if len(sys.argv) > 1 else 8
stamps = [float(l.split()[0]) for l in open("arrivals.log")]
ivals = [b - a for a, b in zip(stamps[-(window+1):], stamps[-window:])]
if len(ivals) < window:
print(f"not enough samples ({len(ivals)}/{window})")
else:
sd = statistics.pstdev(ivals)
print(f"window={window} intervals n={len(ivals)} stdev={sd:.3f}s "
f"-> {'METRONOME' if sd < 0.05 else 'quiet'}")
Run it while each client is talking: python3 detect.py 8. The threshold of 0.05 seconds is honest laziness, not tuning: the metronome measured 0.000 and the jitter 0.670, so any line between them works, and picking a round number in the middle of a gap that wide is the whole art of a first threshold. Tune when measurements crowd the line, not before.
The volume rule skeleton for the second exercise, deliberately unfinished:
from collections import Counter
import sys
minute = sys.argv[1] if len(sys.argv) > 1 else 60
# arrivals.log rows: "<epoch> GET <user-agent>"
# your work: bucket stamps into windows of `minute` seconds,
# count per window, flag any window over a threshold you pick
The skeleton ends where it does because the threshold is the exercise. Count what the metronome produces, count what honest loopback traffic produces, and put the line where those two numbers are not.