Skip to content
shellcodes

Null bytes in shellcode still ruin exploits in 2026

Why 0x00 breaks strcpy-style delivery, how nulls sneak into reverse TCP structs, and what to do when your encoder pass lies to you.

Published on 3 min read

Null-byte avoidance sounds like a CTF relic. It is not. Any delivery path that treats your payload as a string will truncate on 0x00. That includes naive stack overflows, some format string bugs, and half the "copy shellcode into this form field" workflows people still run in internal labs.

What a null actually breaks

strcpy, strcat, sprintf without length discipline: the first null wins. Your shellcode becomes a prefix. The CPU executes garbage or nothing.

Even when you have binary-safe memcpy, nulls can still hurt:

  • Unicode transforms that normalize bytes
  • WAF rules that dislike certain ranges
  • Debug prints that stop early and hide the tail of your buffer

Where nulls come from in real payloads

People blame the generator. Often it is the data you embedded.

Reverse TCP shellcode carries a sockaddr struct. IPv4 addresses and ports are packed into 32-bit and 16-bit fields. A listener on port 1024 is fine. A port like 0x0100 in the wrong field layout can introduce zeros depending on how the template builds the struct.

IP addresses ending in .0.0 are a classic footgun in training environments. So are addresses with trailing zero octets that you did not notice because the UI prettified them.

Encoder passes are not magic

Running an encoder to kill nulls can:

  • inflate size past your buffer
  • change register assumptions your ROP chain depends on
  • introduce new bad chars (0x0a, 0x0d) you forgot to list

I set bad-char constraints before picking an encoder order, not after a failed exploit attempt.

In shellcodes, the "Avoid null bytes" mission preset starts from ReverseTcp with a null-oriented bad-char filter. That is a starting point. Your target may also forbid 0x09, 0x20, or uppercase ASCII if the injection channel is picky.

Testing null-freedom correctly

Do not eyeball hex.

blob = open("payload.bin", "rb").read()
bad = [i for i, b in enumerate(blob) if b == 0]
assert not bad, f"null at offsets {bad}"
print("ok", len(blob))

Scan for off-by-one issues too. Some tools strip a trailing null thinking it is a string terminator. Your injector might need the raw length explicit.

Attacker perspective vs defender perspective

Attacker (authorized): null constraints shrink the feasible opcode set. You pay size and complexity.

Defender: null checks in copy paths are not sufficient protection. They reduce accidental truncation bugs in exploit chains. They do not stop a binary-safe write primitive.

Practical tradeoff

Perfect null-free shellcode that is 400 bytes longer may not fit. Sometimes the move is not better shellcode. It is a different primitive: ROP to read(), staged loader, or a smaller bootstrap that decodes the rest on the stack.

Null avoidance is a constraint, not a moral victory.

Related articles

How null and alphanumeric presets map to real injection channels, and when you must build a custom bad-char list.
When to use exec-style shellcode versus reverse TCP in authorized labs, and why the flashy option is often the wrong one.
How stacked encoders change size, decoders, and bad-char profiles, plus a sane order for lab iterations before exploit integration.