With the Brine Signet shattered, every house hunts whatever might make its story law. Lady Seralyne, the Velvet Spider of Suncourt, sells what she claims is the dragon's true note: not the lost thing itself, only a counterfeit cadence arranged to be believed, and a wavering house is ready to buy it as proof its claim rings true. We cut one of her sendings from the wire first. Read the pleasant words; then attend to the silences between them, and expose the forgery she is truly selling.
A logic analyser capture of a UART line. The transmitted text is a decoy - the actual message is encoded in the gaps between the characters.
$ ls -la
-rw-rw-r--@ 1 feyrer staff 112808 Jul 6 01:53 capture.sr
$ file capture.sr
capture.sr: Zip archive data, at least v2.0 to extract, compression method=store
A .sr file is a sigrok capture - a zip with
raw logic samples and a metadata file naming the channels and sample rate.
Neither. A pure offline capture, opened in PulseView.
The capture in PulseView, with the UART decoder attached.
A UART decoder needs the baud rate. It is measurable straight off the trace: find the narrowest pulse, which is one bit time - or here, a two-bit run whose edges are easy to place.
>>> 3788685 - 3788480
205 # microseconds for 2 bits
>>> 1 / ((3788685 - 3788480) * 1e-6) * 2
9756.09756097561 # bits per second
Conclusion: 9756 is 9600 within measurement error - the standard rate closest to it. With 8N1 the decoder produces clean bytes immediately.
Export the decoded bytes and reassemble the text.
$ head export-rx.txt
4001195-4001404 UART: RX: Start bit
4001403-4003071 UART: RX: 54
4003070-4003279 UART: RX: Stop bit
4007289-4007498 UART: RX: Start bit
4007497-4009165 UART: RX: 6F
...
$ cat export-rx.txt | grep -v bit | sed 's,.*: ,,' | xxd -r -p | fold -s -w70
To the buyer who paid in secrets: what follows is the pleasant tone,
the goods I sell in daylight and never miss. Lord Varo's debt, due at
the second thaw, yours for a marriage you already own. The Harlow
inheritance, contested by a cousin whose witnesses I arranged. Take
them and thank me. But what is written is worth nothing. The dragon's
true note does not live in the words; it lives in the rests between
them. A long rest raises the mark to one, a short rest lets it fall
to nothing; count eight rests to every letter before the note will
speak. Read the silence, not the song, and pay.
Conclusion: No flag in the text - and the text says so itself. It also gives the complete specification of the real encoding: long gap = 1, short gap = 0, eight gaps per character. The payload is in the timing, not the data.
Zooming out in PulseView makes the structure visible: the characters do not arrive at a steady rate.
Zoomed out, the idle stretches between characters differ visibly in length.
The export carries absolute sample positions, so the gaps can be measured exactly: from the end of one stop bit to the start of the next start bit.
...
4003070-4003279 UART: RX: Stop bit <- gap starts at 4003279
4007289-4007498 UART: RX: Start bit <- gap ends at 4007289
gap = 4010 us
short gaps ~ 4010 us -> bit 0
long gaps ~ 20013 us -> bit 1
Conclusion: The two populations are a factor of five apart, so a threshold anywhere around 10000 us separates them cleanly - no calibration needed. Eight gaps per character, MSB first.
2hf.pl walks the export in triples (stop bit, start bit, data), measures each
gap, thresholds it and reassembles bytes.
#!perl
open(IN, "export-rx.txt") or die;
$skip = <IN>; # skip first
$skip = <IN>; # skip first
$bit_str = "";
while (!eof(IN)) {
$stop = <IN>; chomp($stop);
$start = <IN>; chomp($start);
$data = <IN>; chomp($data);
#
print("stop: $stop\n");
print("start: $start\n");
$pause_start = (split(/[ -]/, $stop))[1];
$pause_end = (split(/[ -]/, $start))[0];
#
print("pause_start: $pause_start\n");
print("pause_end: $pause_end\n");
$diff = $pause_end - $pause_start;
print("diff: $diff\n");
if ($diff >0) {
if ($diff < 10000) { # values are ~4010 / ~20013
$bit = 0;
} else {
$bit = 1;
}
print("bit: $bit - bit_str=$bit_str\n");
$bit_str .= $bit;
if (length($bit_str) == 8) {
$bit_num = oct("0b". $bit_str);
printf("bit_num: 0x%x\n", $bit_num);
$bit_str = "";
}
}
print("\n");
}
$ perl 2hf.pl | grep bit_num | sed 's,.*0x,,' | xxd -r -p
you read the silence well HTB{th3_f1rst_m4rk_r1ngs_tru3_b3n34th_th3_w0rds}
The hidden channel even acknowledges being read before handing over the flag.
HTB{th3_f1rst_m4rk_r1ngs_tru3_b3n34th_th3_w0rds}
| # | Stage | Mechanism |
|---|---|---|
| 1 | Decode the obvious layer | Measure the shortest pulse to get 9600 baud, attach the UART decoder, export the byte stream. Result: a letter with no flag in it. |
| 2 | The text is the manual | "A long rest raises the mark to one, a short rest lets it fall to nothing; count eight rests to every letter." The decoy explains the real encoding. |
| 3 | Measure the silence | The gap between one stop bit and the next start bit is either ~4010 us or ~20013 us. Threshold at 10000, eight gaps per byte. |
This is a covert timing channel, and it survives everything a normal analysis does to it: the UART decode is correct, the bytes are correct, the text is readable - and none of that touches the payload. The information is in when the characters arrive, a dimension the decoder discards by design. Worth remembering that a sigrok export keeps the absolute sample positions, which is exactly what makes the timing recoverable after the fact.