Bug #8880
Updated by Jeff Lucovsky 14 days ago
FTP tracks two globals with similar names: ftp_config_memcap, the configured
limit that FTPCheckMemcap enforces, and ftp_memcap, a counter of how often
that limit was hit. Three separate problems follow from allocations and
commands consulting the wrong one of these, or neither.
The unix socket commands operate on the hit counter, not the limit.
src/runmode-unix-socket.c registers FTPSetMemcap and FTPMemcapGlobalCounter
as the set/get pair for "ftp", and both of those touch the hit counter. So
"memcap-set ftp <value>" reports success while changing nothing that is
enforced -- there is no way to adjust the FTP memcap at runtime -- and as a
side effect it overwrites the ftp.memcap statistic with the requested byte
size. "memcap-show ftp" and "memcap-list" have the matching problem in
reverse, printing the hit counter formatted as a size:
$ suricata --unix-socket --set app-layer.protocols.ftp.memcap=10mb
$ suricatasc -c "memcap-show ftp"
{"message": {"value": "unlimited"}, "return": "OK"}
A process that has not yet hit its memcap reports the FTP limit as
"unlimited" no matter what the configuration says. Setting 0 for unlimited
is also rejected, unlike the other memcaps.
ftp.memuse leaks on command lines with trailing whitespace.
CopyCommandLine allocates line->len + 1 bytes, then trims trailing
whitespace by decrementing line->len, and returns the post-trim length.
That value is stored as tx->request_length and is what FTPTransactionFree
credits back, so the difference stays in ftp.memuse permanently. The
whitespace has to be inside the line rather than the delimiter --
FTPGetLineForDirection already strips CR/LF -- so this needs commands such
as "USER user \r\n". A five-command session with 11 trailing whitespace
characters ends with ftp.memuse at 11 after the flow is gone. Since memuse
only drifts upwards, the effective memcap tightens over the life of the
process.
Two allocations are charged to ftp.memuse without consulting the memcap.
The transfer command allocated by SCFTPTransferCmdNew, and the response
line payload whose total_size is added to memuse when the wrapper is built,
are both added to ftp.memuse with no memcap check. Running a session whose
server response is ~3000 bytes under app-layer.protocols.ftp.memcap=2048
stores the response and pushes memuse past the configured limit; the next
allocation that does check is then denied. The excess is bounded, but the
memcap is not a ceiling.
ftp.command_data reads past a null pointer once a request copy is denied.
DetectFTPCommandDataGetData decides whether a transaction carries command
data with (tx->request_length - b_len - 1) > 0. request_length is a uint32_t
and b_len a uint8_t, so the subtraction is unsigned: a transaction whose
request allocation was denied has request_length 0, the expression evaluates
to 0xfffffffb, and the buffer reaches the matcher as tx->request + b_len + 1,
an offset from a NULL pointer, with a length of 4294967291. FTPParseRequest
fills in tx->command_descriptor before calling CopyCommandLine, so the denied
copy leaves a valid command code behind and the transaction is inspected like
any other. With a rule using ftp.command_data loaded, a session that pipelines
commands until ftp.memuse reaches a 1024 byte memcap ends the process on
SIGSEGV.
Expected: memcap-set/memcap-show operate on the enforced limit and accept 0
for unlimited; ftp.memuse returns to 0 once transactions are freed; no
allocation charged to ftp.memuse bypasses the memcap check.
Affected versions: 8.0.x and 9.0.0-dev have all three.