Skip to the content.

Slack-Space Hiding

What it is

Filesystems allocate storage in fixed-size units: a cluster on NTFS, a block on ext4 and APFS, 4 KiB by default. A file almost never ends exactly on a unit boundary, so the last unit is only partly used. The filesystem still counts the whole unit as allocated to that file, and the leftover bytes at the tail belong to nobody and are read by nothing. Those leftover bytes are slack, and you can write a payload there.

Walk the arithmetic. Take a 10,000-byte file on a filesystem with 4096-byte blocks:

ceil(10000 / 4096) = 3 blocks allocated  = 12288 bytes reserved
file content                             = 10000 bytes used
slack                                    =  2288 bytes  <-- hiding spot

The file’s reported size stays 10,000. Reading the file returns 10,000 bytes. du reports 12 KiB, which it always did, because that is just the allocation. Nothing about the file’s visible behaviour changes when those 2288 bytes get written.

Kinds of slack

one 4096-byte block:
+-----------------------------+---------------------+
|   file's last 10000-9*4096  |       slack         |
|          bytes              |   payload goes here  |
+-----------------------------+---------------------+
                              ^ end of file content

On-disk mechanics

nemo’s slack write is filesystem-agnostic at the technique layer. A filesystem’s Entry implements filesystem.SlackSpaceCapable and returns []filesystem.SlackRegion{ {Offset, Length} }, giving absolute byte offsets into the image. The technique picks the first region big enough, reads the bytes it is about to overwrite so it can back them up, and writes the frame. What differs per filesystem is how the regions get computed.

NTFS

ext4

APFS

Why it is fragile

Contributors get this wrong more than anything else about the technique. A slack payload is destroyed by any of:

Treat slack hiding as volatile storage. If the payload has to persist, this is the wrong technique.

Privilege

Reading and writing slack needs raw device access, because no normal file API on any OS exposes bytes past end-of-file. In nemo’s image mode the image is the raw bytes, so nothing beyond read access to the file is required. In live mode, slack-space means opening \\.\PhysicalDriveN, /dev/diskN, or /dev/sdX and running the same parser, which needs admin or root. Per docs/architecture.md, live hide, detect, and clear with --technique slack-space must detect the missing-privilege condition and fail with a clear error rather than silently degrade. named-stream and timestomp do not have this requirement; they go through ordinary syscalls.

nemo’s payload frame

Raw slack normally holds whatever residual bytes the last file to own that block left behind, so an arbitrary payload cannot be told apart from that noise. detect would have nothing to key on, and clear would not know how many bytes it wrote. nemo therefore wraps every slack payload in a 12-byte frame (internal/technique/slackframe.go):

offset  size  field
  0      4    magic    ASCII "NEMO"
  4      4    length   payload length, uint32 little-endian
  8      4    crc32    CRC-32 (IEEE polynomial) of the payload
 12    length payload  the hidden bytes

detect reports a slack finding only when the magic matches and the CRC validates (decodeFrame and readFrame in technique.go). This is a deliberate trade-off. It makes nemo’s own payloads easy to spot, since the literal string NEMO at a block boundary followed by a valid CRC is not subtle, and in exchange hide, detect, and clear stay reliable and reversible. nemo is a forensic and CTF tool, not a stealth implant. Hiding from a determined examiner is a non-goal.

The payload length is capped at math.MaxUint32, about 4 GiB, by the frame’s length field. In practice a region is one block’s slack, a few KiB.

Reversibility: the manifest

Before overwriting slack bytes, slackSpaceTechnique.Hide calls Request.Backup with the original residual bytes. The command layer persists that as one JSON Lines record per line in nemo-manifest.jsonl (--manifest to relocate), with Original base64-encoded. See internal/technique/manifest.go (AppendManifest, LoadManifest, LatestBackup). A hide aborts before touching disk if the manifest write fails.

clear replays it. With a matching manifest record it writes the original residual bytes back over the frame and reports Result.Restored = true. Without one it zero-fills the frame and reports Restored = false. A Request.Restore slice whose length does not match the frame is rejected, not zero-filled (slackSpaceTechnique.Clear). Later manifest records win, so re-hiding then clearing a target restores the most recent hide’s bytes.

Detection by a third party

Traces left behind

How nemo implements it

filesystem.SlackSpaceCapable:

SlackRegions() ([]filesystem.SlackRegion, error)  // {Offset, Length} into the image

slackSpaceTechnique (internal/technique/technique.go) requires Request.Image (image-backed storage), frames the payload, and writes into the first region that fits.