Suricata keyword highlight: subslice

New in Suricata 9 and later

Precision hunting with subslice

Most Suricata rules express the presence of content in a buffer, and the existing offset and depth modifiers already pin that content to a position anchored from the start. What’s been awkward is the other direction — anchoring to the *end* of a buffer, like the final bytes of a URI — and feeding a precisely-bounded slice of a buffer into a transform. Both have traditionally meant reaching for a regular expression. The subslice transform keyword addresses that gap directly. It was requested in Redmine issue 7672 and merged to the main branch on April 29, 2026 with this PR.

What does subslice do?

subslice is a transform that creates a window from a sticky buffer before content matching runs.

Syntax:

subslice: offset [, nbytes] [, truncate];

The start of the window is specified by offset; the window size is controlled by nbytes. Both parameters accept negative values, which count backward from the end of the buffer. Everything outside the window is discarded before subsequent matches run.

truncate is a flag with no associated value. It controls behavior when the window parameters extend past the buffer boundary. Without truncate, an out-of-bounds slice produces an empty buffer — detectable with bsize:0. With truncate, the slice is capped at whatever data is available.

Examples

1. Match file extension at the end of a URI

Webshells commonly use .php extensions. The following rule creates a 4-byte window at the end of the URI and matches against it:

alert http any any -> any any (
    msg:"PHP file accessed";
    http.uri; subslice: -4; content:".php"; bsize:4;
    sid:1001; rev:1;)

subslice: -4 starts 4 bytes from the end of the URI. bsize:4 confirms the buffer is exactly 4 bytes, enforcing that .php is the final four characters rather than appearing anywhere in the path. This is a case where the existing depth and offset modifiers can’t help — they count from the start of the buffer, not the end. Because the match is anchored to the end of http.uri, it fires only on URIs that literally end in .php; a request for /shell.php?id=1 ends in the query string, so the last four bytes are id=1 and the rule won’t match — worth keeping in mind, since webshell requests often carry parameters.

2. Match file magic bytes — subslice vs. depth

PE files begin with the two-byte magic value MZ. Here is the same detection written two ways:

Using depth:

alert http any any -> any any (
    msg:"PE file in HTTP response";
    file.data; content:"MZ"; depth:2;
    sid:1003; rev:1;)

Using subslice:

alert http any any -> any any (
    msg:"PE file in HTTP response";
    file.data; subslice: 0, 2; content:"MZ"; bsize:2;
    sid:1003; rev:1;)

For this specific case, depth:2 is simpler and familiar to any Suricata rule writer — use it. subslice is the better choice when the inspection window needs to feed into a transform chain. The difference shows up the moment a transform enters the picture — say you want to lowercase those first two bytes before matching:

alert http any any -> any any (
    msg:"PE file in HTTP response, case-insensitive";
    file.data; subslice: 0, 2; to_lowercase;
    content:"mz"; bsize:2; sid:1003; rev:2;)

depth:2 can constrain *where* content looks, but it can’t hand a 2-byte window to to_lowercase (or to_sha256, or from_base64) — only a transform can feed another transform. That composability is what Part 2 of this series covers in detail.

3. Skip a fixed-length header

Some C2 frameworks prepend a fixed-length header or nonce before the payload. PowerShell Empire, for instance, frames its agent traffic with a fixed 20-byte routing header (a 4-byte RC4 IV followed by a 16-byte encrypted routing block) ahead of the agent data. When a protocol consistently uses a fixed-size header and the bytes that follow are inspectable, subslice positions the inspection window past the header:

alert http $HOME_NET any -> $EXTERNAL_NET any (
    msg:"C2 callback pattern"; flow:to_server;
    file.data; subslice: 20; content:"heartbeat";
    sid:1004;)

subslice: 20 skips the 20-byte header so content inspects only what follows it. Two details matter here. The rule is outbound — a callback is an internal host beaconing to an external server, hence $HOME_NET -> $EXTERNAL_NET with flow:to_server so file_data resolves to the request body the agent sends. And the buffer is file_data, the HTTP body, not the raw TCP payload. Transforms attach only to sticky buffers, so there is no pkt_data; subslice form; the “When subslice isn’t the right tool” section below returns to this.

4. Detect missing or too-short User-Agents

subslice can detect the absence of sufficient data. To flag User-Agents shorter than 8 bytes:

alert http any any -> any any (
    msg:"Suspiciously short User-Agent";
    http.user_agent; subslice: 0, 8;
    bsize:0;
    sid:1006; rev:1;)

Without truncate, a buffer shorter than 8 bytes produces an empty result. bsize:0 matches that empty result. It’s a quick way to hunt for minimal or skeleton HTTP stacks by expressing a minimum buffer length requirement directly in the rule.

A note on truncate

With truncate, the slice is capped at the available data in the buffer. Use truncate when matching a prefix or suffix regardless of overall field length. Omit it when buffer length is itself a signal worth detecting.

When subslice isn’t the right tool

subslice requires fixed offsets. If the target pattern doesn’t appear at a predictable position — a value embedded anywhere in a variable-length field, for example — a plain content match is simpler and correct. Using subslice in that case would require a separate rule for each possible offset.

For positional matching without transforms, the existing content modifiers (offset, depth, within, distance) are appropriate and familiar. subslice is most useful when negative offsets are needed or when the inspection window feeds into a transform chain — the upcoming Part 2 covers that in depth.

For length-prefixed binary protocols, subslice alone is insufficient. If the inspection position depends on a length value embedded in the packet, byte_extract and byte_math are needed to compute that offset — subslice parameters must be constants. When detection requires matching at two different positions in the same buffer, two separate rules are necessary; subslice creates one window per transform chain.

Try it out!

The examples from this post are in the following rule file along with the pcap and the script used to create the pcap. Remember, this transform is only in Suricata’s main branch

  1. Subslice rule file (add -S /path/to/these.rules): subslice rules
  2. Pcap file to use with the subslice rule file (add -r /path/to/this.pcap): subslice pcap file
  3. Python program that created the pcap file: pcap generator script

Wrapping up

subslice extends Suricata’s detection language with positional matching. The existing content modifiers handle many positional cases, but negative offsets — and feeding a precisely-bounded buffer into a transform chain — required different solutions before this keyword. With subslice, a rule can express “this content appears at this position” as a single, composable step.

Part 2 of this series gets into how transforms interact with Suricata’s prefilter and rule evaluation pipeline, how to chain subslice with other transforms, and where the keyword is headed with planned support for dynamic offsets via byte_extract.

The post Suricata keyword highlight: subslice appeared first on Suricata.

Image

Pensée du jour :

Ce que l'homme a fait ,

l'homme peut le défaire.

 

"No secure path in the world"