When a Quiet Run Is Not a Clean Result

A crash is the easiest result to read and the rarest one to get. These findings are drawn from seven surfaces where the defect fired, the target carried on answering, and a campaign watching for crashes would have filed the run as clean, including one parser that hands the defect to the application above it.

A crash is the easiest result in this work to read. The process is there, then it is not, and the datagram that preceded it is the evidence. Everything a test campaign reports is easier to act on when it ends that way.

It is also the rarest ending. Most of what a parser gets wrong, it gets wrong while continuing to answer.

The surfaces below are drawn from seven different audits in this series. On each of them a defect fires, the target stays up, the connection stays open, the reply looks ordinary, and a campaign whose only question is "did it fall over" records the run as clean. They are gathered here because the gap between those two outcomes is the whole argument for how a run should be measured.

The parser that hands the defect upward

Product A decodes a wireless infrastructure control protocol, and it is a library rather than a program: no entry point of its own, no socket, linked by whatever application speaks the protocol. It authenticates nothing, and the discovery exchange and the entire data channel are pre-authentication by the protocol's design regardless.

Its decoder is zero-copy. Nothing is allocated and nothing is copied; each field is checked in place and the caller is handed a pointer into the datagram buffer it already owns.

That design removes a whole class of defect, and it relocates another. One bounds check guards two possible address sizes and joins its two arms with "and" where the logic needed "or", which leaves the longer arm dead. The parser accepts a datagram two bytes short of the field it just promised, and returns a pointer to two bytes it never checked.

The parse itself touches nothing out of bounds. The read past the end happens later, in the application, on a field the library said was good. Instrumenting the library and never reading that field reports nothing at all.

Two more of its defects are pure disagreement. A declared header length is checked for a sane range and then never used to locate the payload, so a sender can declare one offset and have the bytes read from another. A declared element length is never used to bound the walk over the elements, in either direction: declare more than you send, or send more than you declare, and the walker runs to the end of the buffer either way. Both produce a receiver that parsed something other than what was sent, which is only visible against a second receiver that parsed it correctly.

There is one more detail here that belongs to anyone sizing a test case rather than reading a report. The overread in its descriptor walk lives entirely in a seven-value window of one 16-bit length field. Below the window a floor check rejects the datagram; above it the structure fits and nothing goes out of bounds. A generator that walks lengths in powers of two, or that tries zero, one, maximum and maximum minus one, passes straight over it.

The read happened and nothing came out

Product B is a desktop file transfer server, and it is the clearest case in this article of how much the observability of a defect depends on where the bytes land.

One of its defects reads the mode field past the end of a four-byte request. Those bytes go into the server's log file, and the log file can be fetched back over the same protocol by the same unauthenticated request. Thirteen bytes of adjacent memory per request, written down and then handed out.

Its option walker has the same shape and no such luck. It reads names and values past the end of the packet, and the memory it reached happened to hold zeros, so nothing came out of the connection at all. A third reads past the end and returns an empty string every time. All three are the same kind of mistake. One is a disclosure primitive and two are invisible, and the difference is what was lying next to the buffer.

The one that confirms the rest is a two-byte error message, which made the server log error code 20786. That number is two printable characters, and those two characters were part of the filename from an earlier request, still sitting in freed memory. The defect never produced a crash or a changed reply. It produced a number in a log that could only have come from memory the server had no business reading.

The processor decided it was fine

Product C implements header compression for a multiplexed web transport. One of its defects is a shift whose result the language does not define, and on the processor it runs on, the shift wraps.

The consequence is not a fault. An invalid compressed string is accepted as though its padding were valid, and it decodes to whatever was decoded before the bad padding began. The header value the application receives is quietly wrong rather than rejected.

Nothing comes out of the connection to say so. Of that product's five defects, one produces a crash with no instrumentation at all. The other four are measurements: processor time, memory growth, and this one, which needs a build instrumented for undefined behaviour before it says anything. A campaign watching only for crashes finds the first and reports the target clean.

The reply that looked ordinary

Product D is a service discovery responder. An oversized copy lands in a heap buffer and becomes the text data the service advertises, and the process keeps running and answering normally. A second defect reads one byte of adjacent memory and discards it.

No path was established from those bytes to anything an attacker can read back, and the audit says so rather than reaching for the stronger claim. Both are out-of-bounds reads, the stock build survives both, and the reply, the timing and the process are unchanged throughout.

That is a weaker result than the responder audited beside it, where a twelve-byte datagram takes the whole process down. Said plainly, because a crash oracle pointed at this one will sit quiet for as long as you leave it running.

When "no" is the normal answer

Product E is a commissioning transport for a smart home standard, carried over two different link layers, and it has the same missing window check on both.

On the second link layer the oracle itself is the problem. The natural test is to confirm one fragment and watch for an acknowledgement, and on that path the first confirmation is routed to the handshake handler, which never re-tests the window. So the oracle reports no acknowledgement at every value it tries, and that output is indistinguishable from the target being correct. The handshake has to be confirmed separately before the case under test is reachable at all.

There is a second trap behind the first. Once the window does get tested, the fragment that brings it to the threshold triggers an acknowledgement that resets the window to its maximum, so the boundary the test is hunting moves while the test is running.

The opposite mistake

Not every quiet result is a missed defect, and Product F is the counterweight this article needs.

It is an embedded management agent, and it accepts an integer twice the length the standard allows, truncates it to 32 bits and echoes the truncation back, so a manager cannot match the reply to the request it sent. That reads like a finding.

The same case sent to the reference implementation of the same protocol produces exactly the same behaviour. A deviation from the specification that the reference implementation shares is a baseline, not a defect, and an oracle armed on it fires on conforming software.

The same product supplies the other half of that lesson. One of its resource findings reproduces in a narrow band of buffer sizes and is invisible when the buffer is generously sized. The configuration the target was running is part of the result, not context around it.

A target chosen to produce nothing

Product G was not commissioned. It is a maintained, widely deployed secure datagram implementation, picked as a control precisely because nobody had selected it for its defects, so that a quiet run elsewhere would have something to be quiet against.

The finding fell out of the first run against it. One fourteen-byte datagram, no handshake, no cookie and no credentials.

The library underneath it was then measured rather than assumed, and is not affected. Both halves of that are the result: the control produced a defect, and the scope of the defect was established instead of extrapolated.

Finding these with Penzzer

Select the INET4 or INET6 module and the protocol under test. The point of the table below is that each row describes a shape to send and a property to measure, and the measurement is what makes the row worth sending at all.

| Module | The shape it provokes | | --- | --- | | Message element length abuse | A declared element length that disagrees with the elements actually carried, in both directions, so a walker that ignores the declaration is caught by what the receiver acts on | | Malformed header fields | A declared header length that disagrees with the header actually present, so a payload located by arithmetic rather than by the declaration shows up as a receiver that parsed a different message | | Truncated packets | Datagrams that end one field, and then one byte, short of what the previous field promised | | Option negotiation abuse | Option names and values whose declared extent runs past the end of the packet, including the sizes that make the walker continue rather than stop | | Short datagram header reads | A header decoded before enough bytes have arrived to hold it | | Name decompression loops | Compression pointers that refer backward, forward and to themselves | | HPACK table and header abuse | Compressed header strings with invalid padding, table sizes at the boundaries, and index values outside the table | | BER length field abuse | Length encodings at every width the format allows, including the ones longer than the value they describe |

‍

Two things matter more here than the choice of row. The first is that a run reports the shape it sent and what the target did with it, by name, measured against what the target did before the case. A reply that arrives on time and carries the wrong value is a result in that frame, and no result at all in a frame whose only question is whether the process survived.

The second is the build the cases are sent to. Several of the defects above are observable only against a target instrumented for memory errors or for undefined behaviour, and on one of them the instrumentation has to include the application above the library, not just the library. Standing that build up is the operator's side of the work, and the cases are worth sending either way, because a case is sent because the target might fail it, not because the campaign can always tell whether it did.

What this article is, and is not

The defects above are a sample of what this kind of testing surfaces, not a description of Penzzer's coverage. They are what seven specific implementations happened to get wrong, selected here for one property they share.

The attack library spans defect classes well beyond what this article shows: memory safety, information disclosure, resource exhaustion, availability, hangs and algorithmic complexity, input validation, protocol state logic, type and format handling, and error handling. Across the 107 protocols with a protocol-aware target, a run also generates its test cases from the structure of the protocol itself, walking each field through the boundaries where parsers break.

The same limit applies in reverse. What a test looks for is a property of the test, not a claim about your device.

If you run your own campaigns

Three questions, and the first one is the article.

What does your campaign record when the target answers normally? If the answer is nothing, then every defect above would have been filed as a clean run, and seven of them were real.

Where does your instrumentation stop? One of these parsers is correct at every line it executes and still hands its caller a pointer to memory nobody checked. The build that sees it has to contain the caller.

And what is your quiet result measured against? A protocol implementation nobody picked for its defects is worth running first, so that silence somewhere else means something. One of those was tried here, and it produced a defect on the first run.

Other Post
Uncover Hidden Vulnerabilities

Identify security flaws before attackers do, automatically and at scale with Penzzer's intelligent fuzzing engine.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.