Skip to content

ImageFileName is truncated by Windows (14+NUL): surface the full image path instead #13

Description

@h4x0r

A 15-character process name loses its last byte in every view. ps and
netstat both report coreupdater.ex where the real name is coreupdater.exe.

Evidence

Independent oracle — the DFIR Madness Case-001 memory writeup, Volatility on
this same dump:

The other interesting, but now suspect process, 3644 coreupdater.exe had an
active network connection to 203.78.103.109

The answer-key writeups say coreupdater.exe 66 times and never the 14-character
form.

issen (current main, release build) on
tests/data/dfirmadness-szechuan-sauce/DC01-memory.zip:

$ issen memory DC01-memory.zip --command ps
3644  2244  coreupdater.ex  Exited

$ issen memory DC01-memory.zip --command netstat --format json
{"Proto":"TCPv4","Local":"10.42.85.10:62613","Remote":"203.78.103.109:443",
 "State":"ESTABLISHED","PID":"3644","Process":"coreupdater.ex", ...}

coreupdater.exe is 15 bytes; coreupdater.ex is 14.

Why 15 characters is the sharp edge

_EPROCESS.ImageFileName is UCHAR[15]. A 15-character name fills the array
exactly, leaving no room for a NUL terminator. Anything treating the field as
NUL-terminated, or reserving a byte for one, silently drops the final character —
but only for names of exactly 15 characters. Shorter names are unaffected, which
is why this has gone unnoticed.

memf-windows/src/network.rs::owner_info reads the correct length:

let name_bytes = reader.read_bytes(owner + name_off, 15)?;
let process_name = String::from_utf8_lossy(&name_bytes)
    .trim_end_matches('\0')
    .to_string();

Since ps and netstat are both affected, the loss is in shared decode rather
than in either command.

Why it matters more than a display nit

It corrupts an IDENTIFIER, silently, with nothing to signal it happened — no
ellipsis, no lossy flag. An examiner grepping for coreupdater.exe gets no
hit; a report cites a filename that does not exist on the volume.

It is also present in the JSON view, which the fleet rule reserves for exact
values: machine views carry the exact serialization token, never humanized or
truncated
.

Why the existing test does not catch it

tests/szechuan_netstat.rs:

.find(|r| name_of(r).starts_with("coreupdater"))

A prefix match passes identically for coreupdater.exe and coreupdater.ex. The
assertion is weaker than the property it protects, so the test sits directly
beside the defect and cannot see it.

Suggested fix

  1. Trace the shared _EPROCESS.ImageFileName decode and stop reserving/assuming
    a terminator; take all 15 bytes and trim only actual NULs.
  2. Change the test to assert EQUALITY against coreupdater.exe, not a prefix.
    Confirm it FAILS against current main first, or it proves nothing.
  3. Sweep for the same shape elsewhere — any fixed-width, non-NUL-terminated
    field decoded as a C string has this bug for maximum-length values.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions