Bad character handling

Bad character handling

TL;DR: Before any shellcode or ROP chain lands, identify which bytes the input pipeline truncates, transforms, or escapes — then either avoid them or encode around them.

What it is

“Bad characters” are bytes the target’s parsing, copying, or encoding logic refuses to pass through unchanged. The canonical case is 0x00 terminating a strcpy, but in practice many bytes are corrupted by URL/ANSI/UTF-8 decoders, charset conversions, control-character filtering, or DBCS lead-byte handling. If your payload contains a bad byte at offset N, everything past N is garbage.

Preconditions / where it applies

  • Any exploitation primitive where the attacker’s bytes are passed through some processing before reaching memory: stack/heap overflow, file-format parsers, network protocols, registry strings.
  • Especially relevant for strcpy/strncpy (null terminator), lstrcpyA (ANSI translation), URL decoders, MultiByteToWideChar/WideCharToMultiByte, telnet/IAC, SMTP CRLF normalisation.

Technique

1. Generate a complete byte-range probe. Send all bytes \x01..\xff (drop \x00 for the moment) inline at the location of interest. Many people put them right after the EIP overwrite in a stack overflow so the bytes land at a known offset.

1
2
bad_chars = bytes(range(1, 256))
payload = b'A'*offset + p32(eip) + bad_chars

2. Compare in the debugger. Inspect the bytes as they sit in memory; any byte missing or replaced is bad. Mona’s !mona compare -f orig.bin -a <addr> automates the diff:

1
2
!mona bytearray -cpb "\x00\x0a\x0d"
!mona compare -f bytearray.bin -a 0x0012f100

3. Iterate. Add each newly discovered bad byte to the exclude list and re-run until the compare is clean. Common offenders to expect early:

  • \x00 — C string terminator
  • \x0a \x0d — line feed / carriage return in line-based protocols
  • \x20 — token separator in many parsers
  • \x1a — Ctrl-Z (DOS EOF)
  • \xff — telnet IAC; sometimes doubled, sometimes eaten

4. Encode around the survivors. Use an encoder (msfvenom -b "\x00\x0a\x0d" -e x86/shikata_ga_nai), pick an alternate ROP gadget that avoids the byte, or split the chain across two writes. For SEH or RET addresses, search for an alternative module whose entry address doesn’t contain the bad byte.

5. Bigger context: charset conversions. When the path crosses MultiByteToWideChar, every byte > 0x7f may map differently per code page; payloads need to be ASCII-safe or constructed inside the target after the conversion.

Detection and defence

  • Strong input validation and length-limited copies (StrSafe StringCchCopy) close the original bug class.
  • WAFs / IDS can fingerprint encoder output (e.g. shikata_ga_nai decoder stub); change encoders or write custom ones.
  • Build with /GS, /SAFESEH, and CFG to make even successful byte delivery harder to convert into control-flow hijack.

References

See also: stack-buffer-overflow, seh-overwrite, mona-py.