|
|
|
|
/* Copyright (C) 2007-2014 Open Information Security Foundation
|
|
|
|
|
*
|
|
|
|
|
* You can copy, redistribute or modify this Program under the terms of
|
|
|
|
|
* the GNU General Public License version 2 as published by the Free
|
|
|
|
|
* Software Foundation.
|
|
|
|
|
*
|
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
|
*
|
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
|
* version 2 along with this program; if not, write to the Free Software
|
|
|
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
|
|
|
|
* 02110-1301, USA.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* \file
|
|
|
|
|
*
|
|
|
|
|
* \author Pablo Rincon <pablo.rincon.crespo@gmail.com>
|
|
|
|
|
* \author Victor Julien <victor@inliniac.net>
|
|
|
|
|
*
|
|
|
|
|
* App-layer parser for SSH protocol
|
|
|
|
|
*
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include "suricata-common.h"
|
ssh: expose the parse failure reason and state in the eve ssh log
A flow that hit an unrecoverable ssh parse error (invalid banner
or invalid record) was never visible in the eve ssh output: the
log condition only admitted a successful handshake (both
directions at kex or session), and the tx logger is one-shot, so
the successful-handshake log fired before a later failure could
be reported and the failure was then silent - the reason was
only visible through the app-layer events.
The log condition is now failure-only: the failure latches, so
the object is emitted exactly once - at the failure, carrying
the error field (invalid_banner / invalid_record) and the state
field (the state the failure occurred in - banner, kex, or
session if a later record fails after NewKeys) - and successful
flows are logged at the flow-end flush, where the engine logs
unconditionally. Logging on the success condition mid-flow would
consume the one-shot tx log and drop a later failure, so it is
not done. The state field is emitted only alongside the error.
The object may be error + state only, when no banner was
parsed. A unit test pins the condition for a failed, a
banner-only and a both-at-kex flow (the failure admits the log
on the failing delivery); the user guide and eve reference doc
commits that follow carry the field docs.
That flow-end flush is skipped for flows that end without a
further app-layer update, though: in IPS/firewall mode a drop
disables the app layer (StreamTcpCheckFlowDrops ->
StreamTcpDisableAppLayer), after which no app-layer update
reaches the tx loggers, and a bypassed flow gets no flow-end
packet either. A successful ssh record - e.g. one that only saw
the banners - was therefore never emitted for those flows. The
hook is the app-layer end: on the transition to app-layer EOF,
StreamTcpDisableAppLayer raises FLOW_APP_LAYER_FLUSH_PENDING,
and FlowUpdateState raises it on the transition to a bypassed
state; OutputTxLog runs the tx loggers once for the next packet,
also when that packet itself has no app-layer update and is not
marked established (the disabling packet returns from the
stream engine before that), and flow-worker clears the flag when
it frees the app layer of a bypassed flow. The flush is
deliberately not a flow-end flush: when the flush is the only
reason to run the loggers, eof is not forced, so each logger's
condition decides and no other parser starts logging records it
suppressed; a disabling packet that does carry an app-layer
update keeps the existing EOF behaviour. SSHTxLogCondition
admits an IPS drop or a bypass, so the pending record is emitted
there; an IDS drop does not end the app layer, so the condition
stays failure-only - a later failure still wins, and the success
record is emitted by the flow-end flush, which the flow timeout
path now also runs for IDS-dropped flows. The unrelated
schema-sort ordering of the new state keys is fixed up as well.
Ticket: #9046.
4 weeks ago
|
|
|
#include "suricata.h"
|
|
|
|
|
#include "decode.h"
|
|
|
|
|
|
|
|
|
|
#include "app-layer-detect-proto.h"
|
|
|
|
|
#include "app-layer-protos.h"
|
|
|
|
|
#include "app-layer-parser.h"
|
|
|
|
|
#include "app-layer-ssh.h"
|
|
|
|
|
#include "rust.h"
|
|
|
|
|
|
|
|
|
|
#include "conf.h"
|
|
|
|
|
|
|
|
|
|
#include "util-debug.h"
|
|
|
|
|
|
|
|
|
|
#include "util-memcmp.h"
|
|
|
|
|
#include "rust-bindings.h"
|
|
|
|
|
#include "suricata.h"
|
|
|
|
|
|
|
|
|
|
/* HASSH fingerprints are disabled by default */
|
|
|
|
|
#define SSH_CONFIG_DEFAULT_HASSH false
|
|
|
|
|
/* Bypassing the encrypted part of the connections */
|
|
|
|
|
#define SSH_CONFIG_DEFAULT_ENCRYPTION_BYPASS ENCRYPTION_HANDLING_TRACK_ONLY
|
|
|
|
|
|
|
|
|
|
static int SSHRegisterPatternsForProtocolDetection(void)
|
|
|
|
|
{
|
|
|
|
|
if (SCAppLayerProtoDetectPMRegisterPatternCI(
|
|
|
|
|
IPPROTO_TCP, ALPROTO_SSH, "SSH-", 4, 0, STREAM_TOSERVER) < 0) {
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
if (SCAppLayerProtoDetectPMRegisterPatternCI(
|
|
|
|
|
IPPROTO_TCP, ALPROTO_SSH, "SSH-", 4, 0, STREAM_TOCLIENT) < 0) {
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool SSHTxLogCondition(ThreadVars *tv, const Packet *p, void *state, void *tx, uint64_t tx_id)
|
|
|
|
|
{
|
ssh: expose the parse failure reason and state in the eve ssh log
A flow that hit an unrecoverable ssh parse error (invalid banner
or invalid record) was never visible in the eve ssh output: the
log condition only admitted a successful handshake (both
directions at kex or session), and the tx logger is one-shot, so
the successful-handshake log fired before a later failure could
be reported and the failure was then silent - the reason was
only visible through the app-layer events.
The log condition is now failure-only: the failure latches, so
the object is emitted exactly once - at the failure, carrying
the error field (invalid_banner / invalid_record) and the state
field (the state the failure occurred in - banner, kex, or
session if a later record fails after NewKeys) - and successful
flows are logged at the flow-end flush, where the engine logs
unconditionally. Logging on the success condition mid-flow would
consume the one-shot tx log and drop a later failure, so it is
not done. The state field is emitted only alongside the error.
The object may be error + state only, when no banner was
parsed. A unit test pins the condition for a failed, a
banner-only and a both-at-kex flow (the failure admits the log
on the failing delivery); the user guide and eve reference doc
commits that follow carry the field docs.
That flow-end flush is skipped for flows that end without a
further app-layer update, though: in IPS/firewall mode a drop
disables the app layer (StreamTcpCheckFlowDrops ->
StreamTcpDisableAppLayer), after which no app-layer update
reaches the tx loggers, and a bypassed flow gets no flow-end
packet either. A successful ssh record - e.g. one that only saw
the banners - was therefore never emitted for those flows. The
hook is the app-layer end: on the transition to app-layer EOF,
StreamTcpDisableAppLayer raises FLOW_APP_LAYER_FLUSH_PENDING,
and FlowUpdateState raises it on the transition to a bypassed
state; OutputTxLog runs the tx loggers once for the next packet,
also when that packet itself has no app-layer update and is not
marked established (the disabling packet returns from the
stream engine before that), and flow-worker clears the flag when
it frees the app layer of a bypassed flow. The flush is
deliberately not a flow-end flush: when the flush is the only
reason to run the loggers, eof is not forced, so each logger's
condition decides and no other parser starts logging records it
suppressed; a disabling packet that does carry an app-layer
update keeps the existing EOF behaviour. SSHTxLogCondition
admits an IPS drop or a bypass, so the pending record is emitted
there; an IDS drop does not end the app layer, so the condition
stays failure-only - a later failure still wins, and the success
record is emitted by the flow-end flush, which the flow timeout
path now also runs for IDS-dropped flows. The unrelated
schema-sort ordering of the new state keys is fixed up as well.
Ticket: #9046.
4 weeks ago
|
|
|
/* A pending success record must be emitted when an IPS drop or a
|
|
|
|
|
* bypass ends the flow: the flow-end flush that would log it is
|
|
|
|
|
* skipped for those flows, and the app layer is disabled or freed
|
|
|
|
|
* right after, so no further update reaches this condition. An IDS
|
|
|
|
|
* drop keeps parsing, so the condition stays failure-only there: a
|
|
|
|
|
* later failure still wins, and the success record is emitted by
|
|
|
|
|
* the flow-end flush. */
|
|
|
|
|
if (p->flow != NULL) {
|
|
|
|
|
if ((p->flow->flags & FLOW_ACTION_DROP) != 0 && EngineModeIsIPS()) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (FlowIsBypassed(p->flow)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return SCSshTxGetLogCondition(tx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \brief Function to register the SSH protocol parsers and other functions
|
|
|
|
|
*/
|
|
|
|
|
void RegisterSSHParsers(void)
|
|
|
|
|
{
|
|
|
|
|
const char *proto_name = "ssh";
|
|
|
|
|
|
|
|
|
|
if (SCAppLayerProtoDetectConfProtoDetectionEnabled("tcp", proto_name)) {
|
|
|
|
|
AppLayerProtoDetectRegisterProtocol(ALPROTO_SSH, proto_name);
|
|
|
|
|
if (SSHRegisterPatternsForProtocolDetection() < 0)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
/* Check if we should generate Hassh fingerprints */
|
|
|
|
|
int enable_hassh = SSH_CONFIG_DEFAULT_HASSH;
|
|
|
|
|
const char *strval = NULL;
|
|
|
|
|
if (SCConfGetNonNull("app-layer.protocols.ssh.hassh", &strval) != 1) {
|
|
|
|
|
enable_hassh = SSH_CONFIG_DEFAULT_HASSH;
|
|
|
|
|
} else if (strcmp(strval, "auto") == 0) {
|
|
|
|
|
enable_hassh = SSH_CONFIG_DEFAULT_HASSH;
|
|
|
|
|
} else if (SCConfValIsFalse(strval)) {
|
|
|
|
|
enable_hassh = SSH_CONFIG_DEFAULT_HASSH;
|
|
|
|
|
SCSshDisableHassh();
|
|
|
|
|
} else if (SCConfValIsTrue(strval)) {
|
|
|
|
|
enable_hassh = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (RunmodeIsUnittests() || enable_hassh) {
|
|
|
|
|
SCSshEnableHassh();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
EncryptionHandling encryption_bypass = SSH_CONFIG_DEFAULT_ENCRYPTION_BYPASS;
|
|
|
|
|
SCConfNode *encryption_node = SCConfGetNode("app-layer.protocols.ssh.encryption-handling");
|
|
|
|
|
if (encryption_node != NULL && encryption_node->val != NULL) {
|
|
|
|
|
if (strcmp(encryption_node->val, "full") == 0) {
|
|
|
|
|
encryption_bypass = ENCRYPTION_HANDLING_FULL;
|
|
|
|
|
} else if (strcmp(encryption_node->val, "track-only") == 0) {
|
|
|
|
|
encryption_bypass = ENCRYPTION_HANDLING_TRACK_ONLY;
|
|
|
|
|
} else if (strcmp(encryption_node->val, "bypass") == 0) {
|
|
|
|
|
encryption_bypass = ENCRYPTION_HANDLING_BYPASS;
|
|
|
|
|
} else {
|
|
|
|
|
encryption_bypass = SSH_CONFIG_DEFAULT_ENCRYPTION_BYPASS;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (encryption_bypass) {
|
|
|
|
|
SCLogConfig("ssh: bypass on the start of encryption enabled");
|
|
|
|
|
SCSshEnableBypass(encryption_bypass);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SCLogDebug("Registering Rust SSH parser.");
|
|
|
|
|
SCRegisterSshParser();
|
|
|
|
|
|
|
|
|
|
#ifdef UNITTESTS
|
|
|
|
|
AppLayerParserRegisterProtocolUnittests(IPPROTO_TCP, ALPROTO_SSH, SSHParserRegisterTests);
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* UNITTESTS */
|
|
|
|
|
#ifdef UNITTESTS
|
|
|
|
|
#include "flow-util.h"
|
|
|
|
|
#include "stream-tcp-util.h"
|
|
|
|
|
#include "util-unittest-helper.h"
|
|
|
|
|
#include "stream-tcp-private.h"
|
|
|
|
|
#include "stream-tcp-reassemble.h"
|
|
|
|
|
#include "stream-tcp.h"
|
|
|
|
|
|
|
|
|
|
static int SSHParserTestUtilCheck(const char *protoexp, const char *softexp, void *tx, uint8_t flags) {
|
|
|
|
|
const uint8_t *protocol = NULL;
|
|
|
|
|
uint32_t p_len = 0;
|
|
|
|
|
const uint8_t *software = NULL;
|
|
|
|
|
uint32_t s_len = 0;
|
|
|
|
|
|
|
|
|
|
if (SCSshTxGetProtocol(tx, flags, &protocol, &p_len) != 1) {
|
|
|
|
|
printf("Version string not parsed correctly return: ");
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
if (protocol == NULL) {
|
|
|
|
|
printf("Version string not parsed correctly NULL: ");
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (p_len != strlen(protoexp)) {
|
|
|
|
|
printf("Version string not parsed correctly length: ");
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
if (memcmp(protocol, protoexp, strlen(protoexp)) != 0) {
|
|
|
|
|
printf("Version string not parsed correctly: ");
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (softexp != NULL) {
|
|
|
|
|
if (SCSshTxGetSoftware(tx, flags, &software, &s_len) != 1)
|
|
|
|
|
return 1;
|
|
|
|
|
if (software == NULL)
|
|
|
|
|
return 1;
|
|
|
|
|
if (s_len != strlen(softexp)) {
|
|
|
|
|
printf("Software string not parsed correctly length: ");
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
if (memcmp(software, softexp, strlen(softexp)) != 0) {
|
|
|
|
|
printf("Software string not parsed correctly: ");
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test Send a version string in one chunk (client version str). */
|
|
|
|
|
static int SSHParserTest01(void)
|
|
|
|
|
{
|
|
|
|
|
Flow f;
|
|
|
|
|
uint8_t sshbuf[] = "SSH-2.0-MySSHClient-0.5.1\n";
|
|
|
|
|
uint32_t sshlen = sizeof(sshbuf) - 1;
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
AppLayerParserThreadCtx *alp_tctx = AppLayerParserThreadCtxAlloc();
|
|
|
|
|
|
|
|
|
|
memset(&f, 0, sizeof(f));
|
|
|
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
|
FLOW_INITIALIZE(&f);
|
|
|
|
|
f.protoctx = (void *)&ssn;
|
|
|
|
|
f.proto = IPPROTO_TCP;
|
|
|
|
|
f.alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
StreamTcpInitConfig(true);
|
|
|
|
|
|
|
|
|
|
int r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH,
|
|
|
|
|
STREAM_TOSERVER | STREAM_EOF, sshbuf, sshlen);
|
|
|
|
|
FAIL_IF_NOT(r == 0);
|
|
|
|
|
|
|
|
|
|
void *ssh_state = f.alstate;
|
|
|
|
|
FAIL_IF_NULL(ssh_state);
|
|
|
|
|
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
|
|
|
|
FAIL_IF_NULL(tx);
|
ssh: rebuild the state machine as per-direction states
The old ssh state scale was coupled across the two directions: the
terminal finished state was only reached after both sides sent NewKeys,
and the progress accessor clamped every per-direction phase to a
shared value. A rule hooking the old top state therefore meant "the
flow completed", not "this direction completed the handshake", and
there was no room above the terminal state for a true completion
marker.
Replace the scale with per-direction phases:
banner(0) -> banner_wait_eol(1) -> kex(2) -> session(3)
NewKeys now sets session on the packet's own direction only, banner
completion sets kex, and the progress accessor is identity: each
direction reports its own raw phase. Add the registered completion
state done(4), never assigned during stream parsing: it stays out
of both name tables, so a rule referencing it fails to load, the
hook listing omits it and the firewall policy loader does not
consult it; the progress accessor reports it only on flows
disrupted by stream depth or async reassembly.
The legacy state names (in_progress, banner_done, finished) no
longer resolve through the name table (pre-production - breaking
changes allowed); the name-table unit test pins the rejection.
The old firewall policy state keys are added to the rename hint
table (like the TLS migration), so a config using them fails
loudly with the replacement named instead of degrading to a bare
warning. The non-prefilter engine-name registration now guards
the null id-to-name result (a hook-only rule at the completion
progress previously formatted a null name into the engine
name).
Add unit tests for the state name table (new names, rejected legacy
names, the unhookable completion state, id-to-name) and for the
per-direction session state.
The eve log condition compares the per-direction states with
>= rather than ==: a direction at session has necessarily passed
kex, and a single delivery can take a direction
banner -> kex -> session, so a final {session, kex}
pair must still admit the log (done is never assigned, so >=
changes only the kex branch).
Ticket: #9046.
1 month ago
|
|
|
FAIL_IF(SCSshTxGetAlStateProgress(tx, STREAM_TOSERVER) != SshStateKex);
|
|
|
|
|
FAIL_IF(SSHParserTestUtilCheck("2.0", "MySSHClient-0.5.1", tx, STREAM_TOSERVER));
|
|
|
|
|
|
|
|
|
|
FLOW_DESTROY(&f);
|
|
|
|
|
AppLayerParserThreadCtxFree(alp_tctx);
|
|
|
|
|
StreamTcpFreeConfig(true);
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test Send a version string in one chunk but multiple lines and comments.
|
|
|
|
|
* (client version str)
|
|
|
|
|
*/
|
|
|
|
|
static int SSHParserTest02(void)
|
|
|
|
|
{
|
|
|
|
|
int result = 0;
|
|
|
|
|
Flow f;
|
|
|
|
|
uint8_t sshbuf[] = "SSH-2.0-MySSHClient-0.5.1 some comments...\n";
|
|
|
|
|
uint32_t sshlen = sizeof(sshbuf) - 1;
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
AppLayerParserThreadCtx *alp_tctx = AppLayerParserThreadCtxAlloc();
|
|
|
|
|
|
|
|
|
|
memset(&f, 0, sizeof(f));
|
|
|
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
|
FLOW_INITIALIZE(&f);
|
|
|
|
|
f.protoctx = (void *)&ssn;
|
|
|
|
|
f.proto = IPPROTO_TCP;
|
|
|
|
|
f.alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
StreamTcpInitConfig(true);
|
|
|
|
|
|
|
|
|
|
int r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH,
|
|
|
|
|
STREAM_TOSERVER | STREAM_EOF, sshbuf, sshlen);
|
|
|
|
|
if (r != 0) {
|
|
|
|
|
printf("toclient chunk 1 returned %" PRId32 ", expected 0: ", r);
|
|
|
|
|
goto end;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void *ssh_state = f.alstate;
|
|
|
|
|
if (ssh_state == NULL) {
|
|
|
|
|
printf("no ssh state: ");
|
|
|
|
|
goto end;
|
|
|
|
|
}
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
|
|
|
|
|
ssh: rebuild the state machine as per-direction states
The old ssh state scale was coupled across the two directions: the
terminal finished state was only reached after both sides sent NewKeys,
and the progress accessor clamped every per-direction phase to a
shared value. A rule hooking the old top state therefore meant "the
flow completed", not "this direction completed the handshake", and
there was no room above the terminal state for a true completion
marker.
Replace the scale with per-direction phases:
banner(0) -> banner_wait_eol(1) -> kex(2) -> session(3)
NewKeys now sets session on the packet's own direction only, banner
completion sets kex, and the progress accessor is identity: each
direction reports its own raw phase. Add the registered completion
state done(4), never assigned during stream parsing: it stays out
of both name tables, so a rule referencing it fails to load, the
hook listing omits it and the firewall policy loader does not
consult it; the progress accessor reports it only on flows
disrupted by stream depth or async reassembly.
The legacy state names (in_progress, banner_done, finished) no
longer resolve through the name table (pre-production - breaking
changes allowed); the name-table unit test pins the rejection.
The old firewall policy state keys are added to the rename hint
table (like the TLS migration), so a config using them fails
loudly with the replacement named instead of degrading to a bare
warning. The non-prefilter engine-name registration now guards
the null id-to-name result (a hook-only rule at the completion
progress previously formatted a null name into the engine
name).
Add unit tests for the state name table (new names, rejected legacy
names, the unhookable completion state, id-to-name) and for the
per-direction session state.
The eve log condition compares the per-direction states with
>= rather than ==: a direction at session has necessarily passed
kex, and a single delivery can take a direction
banner -> kex -> session, so a final {session, kex}
pair must still admit the log (done is never assigned, so >=
changes only the kex branch).
Ticket: #9046.
1 month ago
|
|
|
if (SCSshTxGetAlStateProgress(tx, STREAM_TOSERVER) != SshStateKex) {
|
|
|
|
|
printf("Client version string not parsed: ");
|
|
|
|
|
goto end;
|
|
|
|
|
}
|
|
|
|
|
if (SSHParserTestUtilCheck("2.0", "MySSHClient-0.5.1", tx, STREAM_TOSERVER))
|
|
|
|
|
goto end;
|
|
|
|
|
|
|
|
|
|
result = 1;
|
|
|
|
|
end:
|
|
|
|
|
FLOW_DESTROY(&f);
|
|
|
|
|
if (alp_tctx != NULL)
|
|
|
|
|
AppLayerParserThreadCtxFree(alp_tctx);
|
|
|
|
|
StreamTcpFreeConfig(true);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test Send a invalid version string in one chunk but multiple lines and comments.
|
|
|
|
|
* (client version str)
|
|
|
|
|
*/
|
|
|
|
|
static int SSHParserTest03(void)
|
|
|
|
|
{
|
|
|
|
|
int result = 0;
|
|
|
|
|
Flow f;
|
|
|
|
|
uint8_t sshbuf[] = "SSH-2.0 some comments...\n";
|
|
|
|
|
uint32_t sshlen = sizeof(sshbuf) - 1;
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
AppLayerParserThreadCtx *alp_tctx = AppLayerParserThreadCtxAlloc();
|
|
|
|
|
|
|
|
|
|
memset(&f, 0, sizeof(f));
|
|
|
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
|
FLOW_INITIALIZE(&f);
|
|
|
|
|
f.protoctx = (void *)&ssn;
|
|
|
|
|
f.proto = IPPROTO_TCP;
|
|
|
|
|
f.alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
StreamTcpInitConfig(true);
|
|
|
|
|
|
|
|
|
|
int r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH,
|
|
|
|
|
STREAM_TOSERVER | STREAM_EOF, sshbuf, sshlen);
|
|
|
|
|
if (r == 0) {
|
|
|
|
|
printf("toclient chunk 1 returned %" PRId32 ", expected != 0: ", r);
|
|
|
|
|
goto end;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void *ssh_state = f.alstate;
|
|
|
|
|
if (ssh_state == NULL) {
|
|
|
|
|
printf("no ssh state: ");
|
|
|
|
|
goto end;
|
|
|
|
|
}
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
|
|
|
|
|
ssh: rebuild the state machine as per-direction states
The old ssh state scale was coupled across the two directions: the
terminal finished state was only reached after both sides sent NewKeys,
and the progress accessor clamped every per-direction phase to a
shared value. A rule hooking the old top state therefore meant "the
flow completed", not "this direction completed the handshake", and
there was no room above the terminal state for a true completion
marker.
Replace the scale with per-direction phases:
banner(0) -> banner_wait_eol(1) -> kex(2) -> session(3)
NewKeys now sets session on the packet's own direction only, banner
completion sets kex, and the progress accessor is identity: each
direction reports its own raw phase. Add the registered completion
state done(4), never assigned during stream parsing: it stays out
of both name tables, so a rule referencing it fails to load, the
hook listing omits it and the firewall policy loader does not
consult it; the progress accessor reports it only on flows
disrupted by stream depth or async reassembly.
The legacy state names (in_progress, banner_done, finished) no
longer resolve through the name table (pre-production - breaking
changes allowed); the name-table unit test pins the rejection.
The old firewall policy state keys are added to the rename hint
table (like the TLS migration), so a config using them fails
loudly with the replacement named instead of degrading to a bare
warning. The non-prefilter engine-name registration now guards
the null id-to-name result (a hook-only rule at the completion
progress previously formatted a null name into the engine
name).
Add unit tests for the state name table (new names, rejected legacy
names, the unhookable completion state, id-to-name) and for the
per-direction session state.
The eve log condition compares the per-direction states with
>= rather than ==: a direction at session has necessarily passed
kex, and a single delivery can take a direction
banner -> kex -> session, so a final {session, kex}
pair must still admit the log (done is never assigned, so >=
changes only the kex branch).
Ticket: #9046.
1 month ago
|
|
|
if (SCSshTxGetAlStateProgress(tx, STREAM_TOSERVER) == SshStateKex) {
|
|
|
|
|
printf("Client version string parsed? It's not a valid string: ");
|
|
|
|
|
goto end;
|
|
|
|
|
}
|
|
|
|
|
const uint8_t *dummy = NULL;
|
|
|
|
|
uint32_t dummy_len = 0;
|
|
|
|
|
if (SCSshTxGetProtocol(tx, STREAM_TOSERVER, &dummy, &dummy_len) != 0)
|
|
|
|
|
goto end;
|
|
|
|
|
if (SCSshTxGetSoftware(tx, STREAM_TOSERVER, &dummy, &dummy_len) != 0)
|
|
|
|
|
goto end;
|
|
|
|
|
|
|
|
|
|
result = 1;
|
|
|
|
|
end:
|
|
|
|
|
FLOW_DESTROY(&f);
|
|
|
|
|
if (alp_tctx != NULL)
|
|
|
|
|
AppLayerParserThreadCtxFree(alp_tctx);
|
|
|
|
|
StreamTcpFreeConfig(true);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test Send a version string in one chunk (server version str). */
|
|
|
|
|
static int SSHParserTest04(void)
|
|
|
|
|
{
|
|
|
|
|
int result = 0;
|
|
|
|
|
Flow f;
|
|
|
|
|
uint8_t sshbuf[] = "SSH-2.0-MySSHClient-0.5.1\n";
|
|
|
|
|
uint32_t sshlen = sizeof(sshbuf) - 1;
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
AppLayerParserThreadCtx *alp_tctx = AppLayerParserThreadCtxAlloc();
|
|
|
|
|
|
|
|
|
|
memset(&f, 0, sizeof(f));
|
|
|
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
|
FLOW_INITIALIZE(&f);
|
|
|
|
|
f.protoctx = (void *)&ssn;
|
|
|
|
|
f.proto = IPPROTO_TCP;
|
|
|
|
|
f.alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
StreamTcpInitConfig(true);
|
|
|
|
|
|
|
|
|
|
int r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH,
|
|
|
|
|
STREAM_TOCLIENT | STREAM_EOF, sshbuf, sshlen);
|
|
|
|
|
if (r != 0) {
|
|
|
|
|
printf("toserver chunk 1 returned %" PRId32 ", expected 0: ", r);
|
|
|
|
|
goto end;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void *ssh_state = f.alstate;
|
|
|
|
|
if (ssh_state == NULL) {
|
|
|
|
|
printf("no ssh state: ");
|
|
|
|
|
goto end;
|
|
|
|
|
}
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
|
|
|
|
|
ssh: rebuild the state machine as per-direction states
The old ssh state scale was coupled across the two directions: the
terminal finished state was only reached after both sides sent NewKeys,
and the progress accessor clamped every per-direction phase to a
shared value. A rule hooking the old top state therefore meant "the
flow completed", not "this direction completed the handshake", and
there was no room above the terminal state for a true completion
marker.
Replace the scale with per-direction phases:
banner(0) -> banner_wait_eol(1) -> kex(2) -> session(3)
NewKeys now sets session on the packet's own direction only, banner
completion sets kex, and the progress accessor is identity: each
direction reports its own raw phase. Add the registered completion
state done(4), never assigned during stream parsing: it stays out
of both name tables, so a rule referencing it fails to load, the
hook listing omits it and the firewall policy loader does not
consult it; the progress accessor reports it only on flows
disrupted by stream depth or async reassembly.
The legacy state names (in_progress, banner_done, finished) no
longer resolve through the name table (pre-production - breaking
changes allowed); the name-table unit test pins the rejection.
The old firewall policy state keys are added to the rename hint
table (like the TLS migration), so a config using them fails
loudly with the replacement named instead of degrading to a bare
warning. The non-prefilter engine-name registration now guards
the null id-to-name result (a hook-only rule at the completion
progress previously formatted a null name into the engine
name).
Add unit tests for the state name table (new names, rejected legacy
names, the unhookable completion state, id-to-name) and for the
per-direction session state.
The eve log condition compares the per-direction states with
>= rather than ==: a direction at session has necessarily passed
kex, and a single delivery can take a direction
banner -> kex -> session, so a final {session, kex}
pair must still admit the log (done is never assigned, so >=
changes only the kex branch).
Ticket: #9046.
1 month ago
|
|
|
if (SCSshTxGetAlStateProgress(tx, STREAM_TOCLIENT) != SshStateKex) {
|
|
|
|
|
printf("Client version string not parsed: ");
|
|
|
|
|
goto end;
|
|
|
|
|
}
|
|
|
|
|
if (SSHParserTestUtilCheck("2.0", "MySSHClient-0.5.1", tx, STREAM_TOCLIENT))
|
|
|
|
|
goto end;
|
|
|
|
|
|
|
|
|
|
result = 1;
|
|
|
|
|
|
|
|
|
|
end:
|
|
|
|
|
FLOW_DESTROY(&f);
|
|
|
|
|
if (alp_tctx != NULL)
|
|
|
|
|
AppLayerParserThreadCtxFree(alp_tctx);
|
|
|
|
|
StreamTcpFreeConfig(true);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test Send a version string in one chunk (server version str)
|
|
|
|
|
*/
|
|
|
|
|
static int SSHParserTest05(void)
|
|
|
|
|
{
|
|
|
|
|
int result = 0;
|
|
|
|
|
Flow f;
|
|
|
|
|
uint8_t sshbuf[] = "SSH-2.0-MySSHClient-0.5.1 some comments...\n";
|
|
|
|
|
uint32_t sshlen = sizeof(sshbuf) - 1;
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
AppLayerParserThreadCtx *alp_tctx = AppLayerParserThreadCtxAlloc();
|
|
|
|
|
|
|
|
|
|
memset(&f, 0, sizeof(f));
|
|
|
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
|
FLOW_INITIALIZE(&f);
|
|
|
|
|
f.protoctx = (void *)&ssn;
|
|
|
|
|
f.proto = IPPROTO_TCP;
|
|
|
|
|
f.alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
StreamTcpInitConfig(true);
|
|
|
|
|
|
|
|
|
|
int r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH,
|
|
|
|
|
STREAM_TOCLIENT | STREAM_EOF, sshbuf, sshlen);
|
|
|
|
|
if (r != 0) {
|
|
|
|
|
printf("toserver chunk 1 returned %" PRId32 ", expected 0: ", r);
|
|
|
|
|
goto end;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void *ssh_state = f.alstate;
|
|
|
|
|
if (ssh_state == NULL) {
|
|
|
|
|
printf("no ssh state: ");
|
|
|
|
|
goto end;
|
|
|
|
|
}
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
|
|
|
|
|
ssh: rebuild the state machine as per-direction states
The old ssh state scale was coupled across the two directions: the
terminal finished state was only reached after both sides sent NewKeys,
and the progress accessor clamped every per-direction phase to a
shared value. A rule hooking the old top state therefore meant "the
flow completed", not "this direction completed the handshake", and
there was no room above the terminal state for a true completion
marker.
Replace the scale with per-direction phases:
banner(0) -> banner_wait_eol(1) -> kex(2) -> session(3)
NewKeys now sets session on the packet's own direction only, banner
completion sets kex, and the progress accessor is identity: each
direction reports its own raw phase. Add the registered completion
state done(4), never assigned during stream parsing: it stays out
of both name tables, so a rule referencing it fails to load, the
hook listing omits it and the firewall policy loader does not
consult it; the progress accessor reports it only on flows
disrupted by stream depth or async reassembly.
The legacy state names (in_progress, banner_done, finished) no
longer resolve through the name table (pre-production - breaking
changes allowed); the name-table unit test pins the rejection.
The old firewall policy state keys are added to the rename hint
table (like the TLS migration), so a config using them fails
loudly with the replacement named instead of degrading to a bare
warning. The non-prefilter engine-name registration now guards
the null id-to-name result (a hook-only rule at the completion
progress previously formatted a null name into the engine
name).
Add unit tests for the state name table (new names, rejected legacy
names, the unhookable completion state, id-to-name) and for the
per-direction session state.
The eve log condition compares the per-direction states with
>= rather than ==: a direction at session has necessarily passed
kex, and a single delivery can take a direction
banner -> kex -> session, so a final {session, kex}
pair must still admit the log (done is never assigned, so >=
changes only the kex branch).
Ticket: #9046.
1 month ago
|
|
|
if (SCSshTxGetAlStateProgress(tx, STREAM_TOCLIENT) != SshStateKex) {
|
|
|
|
|
printf("Client version string not parsed: ");
|
|
|
|
|
goto end;
|
|
|
|
|
}
|
|
|
|
|
if (SSHParserTestUtilCheck("2.0", "MySSHClient-0.5.1", tx, STREAM_TOCLIENT))
|
|
|
|
|
goto end;
|
|
|
|
|
|
|
|
|
|
result = 1;
|
|
|
|
|
end:
|
|
|
|
|
FLOW_DESTROY(&f);
|
|
|
|
|
if (alp_tctx != NULL)
|
|
|
|
|
AppLayerParserThreadCtxFree(alp_tctx);
|
|
|
|
|
StreamTcpFreeConfig(true);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test Send a invalid version string in one chunk (server version str)
|
|
|
|
|
*/
|
|
|
|
|
static int SSHParserTest06(void)
|
|
|
|
|
{
|
|
|
|
|
int result = 0;
|
|
|
|
|
Flow f;
|
|
|
|
|
uint8_t sshbuf[] = "SSH-2.0 some comments...\n";
|
|
|
|
|
uint32_t sshlen = sizeof(sshbuf) - 1;
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
AppLayerParserThreadCtx *alp_tctx = AppLayerParserThreadCtxAlloc();
|
|
|
|
|
|
|
|
|
|
memset(&f, 0, sizeof(f));
|
|
|
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
|
FLOW_INITIALIZE(&f);
|
|
|
|
|
f.protoctx = (void *)&ssn;
|
|
|
|
|
f.proto = IPPROTO_TCP;
|
|
|
|
|
f.alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
StreamTcpInitConfig(true);
|
|
|
|
|
|
|
|
|
|
int r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH,
|
|
|
|
|
STREAM_TOCLIENT | STREAM_EOF, sshbuf, sshlen);
|
|
|
|
|
if (r == 0) {
|
|
|
|
|
printf("toserver chunk 1 returned %" PRId32 ", expected != 0: ", r);
|
|
|
|
|
goto end;
|
|
|
|
|
}
|
|
|
|
|
/* Ok, it returned an error. Let's make sure we didn't parse the string at all */
|
|
|
|
|
|
|
|
|
|
void *ssh_state = f.alstate;
|
|
|
|
|
if (ssh_state == NULL) {
|
|
|
|
|
printf("no ssh state: ");
|
|
|
|
|
goto end;
|
|
|
|
|
}
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
|
|
|
|
|
ssh: rebuild the state machine as per-direction states
The old ssh state scale was coupled across the two directions: the
terminal finished state was only reached after both sides sent NewKeys,
and the progress accessor clamped every per-direction phase to a
shared value. A rule hooking the old top state therefore meant "the
flow completed", not "this direction completed the handshake", and
there was no room above the terminal state for a true completion
marker.
Replace the scale with per-direction phases:
banner(0) -> banner_wait_eol(1) -> kex(2) -> session(3)
NewKeys now sets session on the packet's own direction only, banner
completion sets kex, and the progress accessor is identity: each
direction reports its own raw phase. Add the registered completion
state done(4), never assigned during stream parsing: it stays out
of both name tables, so a rule referencing it fails to load, the
hook listing omits it and the firewall policy loader does not
consult it; the progress accessor reports it only on flows
disrupted by stream depth or async reassembly.
The legacy state names (in_progress, banner_done, finished) no
longer resolve through the name table (pre-production - breaking
changes allowed); the name-table unit test pins the rejection.
The old firewall policy state keys are added to the rename hint
table (like the TLS migration), so a config using them fails
loudly with the replacement named instead of degrading to a bare
warning. The non-prefilter engine-name registration now guards
the null id-to-name result (a hook-only rule at the completion
progress previously formatted a null name into the engine
name).
Add unit tests for the state name table (new names, rejected legacy
names, the unhookable completion state, id-to-name) and for the
per-direction session state.
The eve log condition compares the per-direction states with
>= rather than ==: a direction at session has necessarily passed
kex, and a single delivery can take a direction
banner -> kex -> session, so a final {session, kex}
pair must still admit the log (done is never assigned, so >=
changes only the kex branch).
Ticket: #9046.
1 month ago
|
|
|
if (SCSshTxGetAlStateProgress(tx, STREAM_TOCLIENT) == SshStateKex) {
|
|
|
|
|
printf("Client version string parsed? It's not a valid string: ");
|
|
|
|
|
goto end;
|
|
|
|
|
}
|
|
|
|
|
const uint8_t *dummy = NULL;
|
|
|
|
|
uint32_t dummy_len = 0;
|
|
|
|
|
if (SCSshTxGetProtocol(tx, STREAM_TOCLIENT, &dummy, &dummy_len) != 0)
|
|
|
|
|
goto end;
|
|
|
|
|
if (SCSshTxGetSoftware(tx, STREAM_TOCLIENT, &dummy, &dummy_len) != 0)
|
|
|
|
|
goto end;
|
|
|
|
|
|
|
|
|
|
result = 1;
|
|
|
|
|
end:
|
|
|
|
|
FLOW_DESTROY(&f);
|
|
|
|
|
if (alp_tctx != NULL)
|
|
|
|
|
AppLayerParserThreadCtxFree(alp_tctx);
|
|
|
|
|
StreamTcpFreeConfig(true);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#define MAX_SSH_TEST_SIZE 512
|
|
|
|
|
|
|
|
|
|
static int SSHParserTest07(void)
|
|
|
|
|
{
|
|
|
|
|
TcpReassemblyThreadCtx *ra_ctx = NULL;
|
|
|
|
|
ThreadVars tv;
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
Flow *f = NULL;
|
|
|
|
|
Packet *p = NULL;
|
|
|
|
|
|
|
|
|
|
char sshbufs[2][MAX_SSH_TEST_SIZE] = {"SSH-2.", "0-MySSHClient-0.5.1\r\n"};
|
|
|
|
|
|
|
|
|
|
memset(&tv, 0x00, sizeof(tv));
|
|
|
|
|
|
|
|
|
|
StreamTcpUTInit(&ra_ctx);
|
|
|
|
|
StreamTcpUTInitInline();
|
|
|
|
|
StreamTcpUTSetupSession(&ssn);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.server, 1);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.client, 1);
|
|
|
|
|
|
|
|
|
|
f = UTHBuildFlow(AF_INET, "1.1.1.1", "2.2.2.2", 1234, 2222);
|
|
|
|
|
FAIL_IF_NULL(f);
|
|
|
|
|
f->protoctx = &ssn;
|
|
|
|
|
f->proto = IPPROTO_TCP;
|
|
|
|
|
f->alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
p = PacketGetFromAlloc();
|
|
|
|
|
FAIL_IF(unlikely(p == NULL));
|
|
|
|
|
p->proto = IPPROTO_TCP;
|
|
|
|
|
p->flow = f;
|
|
|
|
|
|
|
|
|
|
uint32_t seq = 2;
|
|
|
|
|
for (int i=0; i<2; i++) {
|
|
|
|
|
FAIL_IF(StreamTcpUTAddSegmentWithPayload(&tv, ra_ctx, &ssn.client, seq, (uint8_t *) sshbufs[i], strlen(sshbufs[i])) == -1);
|
|
|
|
|
seq += strlen(sshbufs[i]);
|
|
|
|
|
FAIL_IF(StreamTcpReassembleAppLayer(&tv, ra_ctx, &ssn, &ssn.client, p, UPDATE_DIR_PACKET) < 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void *ssh_state = f->alstate;
|
|
|
|
|
FAIL_IF_NULL(ssh_state);
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
ssh: rebuild the state machine as per-direction states
The old ssh state scale was coupled across the two directions: the
terminal finished state was only reached after both sides sent NewKeys,
and the progress accessor clamped every per-direction phase to a
shared value. A rule hooking the old top state therefore meant "the
flow completed", not "this direction completed the handshake", and
there was no room above the terminal state for a true completion
marker.
Replace the scale with per-direction phases:
banner(0) -> banner_wait_eol(1) -> kex(2) -> session(3)
NewKeys now sets session on the packet's own direction only, banner
completion sets kex, and the progress accessor is identity: each
direction reports its own raw phase. Add the registered completion
state done(4), never assigned during stream parsing: it stays out
of both name tables, so a rule referencing it fails to load, the
hook listing omits it and the firewall policy loader does not
consult it; the progress accessor reports it only on flows
disrupted by stream depth or async reassembly.
The legacy state names (in_progress, banner_done, finished) no
longer resolve through the name table (pre-production - breaking
changes allowed); the name-table unit test pins the rejection.
The old firewall policy state keys are added to the rename hint
table (like the TLS migration), so a config using them fails
loudly with the replacement named instead of degrading to a bare
warning. The non-prefilter engine-name registration now guards
the null id-to-name result (a hook-only rule at the completion
progress previously formatted a null name into the engine
name).
Add unit tests for the state name table (new names, rejected legacy
names, the unhookable completion state, id-to-name) and for the
per-direction session state.
The eve log condition compares the per-direction states with
>= rather than ==: a direction at session has necessarily passed
kex, and a single delivery can take a direction
banner -> kex -> session, so a final {session, kex}
pair must still admit the log (done is never assigned, so >=
changes only the kex branch).
Ticket: #9046.
1 month ago
|
|
|
FAIL_IF(SCSshTxGetAlStateProgress(tx, STREAM_TOSERVER) != SshStateKex);
|
|
|
|
|
|
|
|
|
|
FAIL_IF(SSHParserTestUtilCheck("2.0", "MySSHClient-0.5.1", tx, STREAM_TOSERVER));
|
|
|
|
|
|
|
|
|
|
UTHFreePacket(p);
|
|
|
|
|
UTHFreeFlow(f);
|
|
|
|
|
StreamTcpUTClearSession(&ssn);
|
|
|
|
|
StreamTcpUTDeinit(ra_ctx);
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test Send a version banner in three chunks. */
|
|
|
|
|
static int SSHParserTest08(void)
|
|
|
|
|
{
|
|
|
|
|
TcpReassemblyThreadCtx *ra_ctx = NULL;
|
|
|
|
|
ThreadVars tv;
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
Flow *f = NULL;
|
|
|
|
|
Packet *p = NULL;
|
|
|
|
|
|
|
|
|
|
char sshbufs[3][MAX_SSH_TEST_SIZE] = {"SSH-", "2.", "0-MySSHClient-0.5.1\r\n"};
|
|
|
|
|
|
|
|
|
|
memset(&tv, 0x00, sizeof(tv));
|
|
|
|
|
|
|
|
|
|
StreamTcpUTInit(&ra_ctx);
|
|
|
|
|
StreamTcpUTInitInline();
|
|
|
|
|
StreamTcpUTSetupSession(&ssn);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.server, 1);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.client, 1);
|
|
|
|
|
|
|
|
|
|
f = UTHBuildFlow(AF_INET, "1.1.1.1", "2.2.2.2", 1234, 2222);
|
|
|
|
|
FAIL_IF_NULL(f);
|
|
|
|
|
f->protoctx = &ssn;
|
|
|
|
|
f->proto = IPPROTO_TCP;
|
|
|
|
|
f->alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
p = PacketGetFromAlloc();
|
|
|
|
|
FAIL_IF(unlikely(p == NULL));
|
|
|
|
|
p->proto = IPPROTO_TCP;
|
|
|
|
|
p->flow = f;
|
|
|
|
|
|
|
|
|
|
uint32_t seq = 2;
|
|
|
|
|
for (int i=0; i<3; i++) {
|
|
|
|
|
FAIL_IF(StreamTcpUTAddSegmentWithPayload(&tv, ra_ctx, &ssn.client, seq, (uint8_t *) sshbufs[i], strlen(sshbufs[i])) == -1);
|
|
|
|
|
seq += strlen(sshbufs[i]);
|
|
|
|
|
FAIL_IF(StreamTcpReassembleAppLayer(&tv, ra_ctx, &ssn, &ssn.client, p, UPDATE_DIR_PACKET) < 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void *ssh_state = f->alstate;
|
|
|
|
|
FAIL_IF_NULL(ssh_state);
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
ssh: rebuild the state machine as per-direction states
The old ssh state scale was coupled across the two directions: the
terminal finished state was only reached after both sides sent NewKeys,
and the progress accessor clamped every per-direction phase to a
shared value. A rule hooking the old top state therefore meant "the
flow completed", not "this direction completed the handshake", and
there was no room above the terminal state for a true completion
marker.
Replace the scale with per-direction phases:
banner(0) -> banner_wait_eol(1) -> kex(2) -> session(3)
NewKeys now sets session on the packet's own direction only, banner
completion sets kex, and the progress accessor is identity: each
direction reports its own raw phase. Add the registered completion
state done(4), never assigned during stream parsing: it stays out
of both name tables, so a rule referencing it fails to load, the
hook listing omits it and the firewall policy loader does not
consult it; the progress accessor reports it only on flows
disrupted by stream depth or async reassembly.
The legacy state names (in_progress, banner_done, finished) no
longer resolve through the name table (pre-production - breaking
changes allowed); the name-table unit test pins the rejection.
The old firewall policy state keys are added to the rename hint
table (like the TLS migration), so a config using them fails
loudly with the replacement named instead of degrading to a bare
warning. The non-prefilter engine-name registration now guards
the null id-to-name result (a hook-only rule at the completion
progress previously formatted a null name into the engine
name).
Add unit tests for the state name table (new names, rejected legacy
names, the unhookable completion state, id-to-name) and for the
per-direction session state.
The eve log condition compares the per-direction states with
>= rather than ==: a direction at session has necessarily passed
kex, and a single delivery can take a direction
banner -> kex -> session, so a final {session, kex}
pair must still admit the log (done is never assigned, so >=
changes only the kex branch).
Ticket: #9046.
1 month ago
|
|
|
FAIL_IF(SCSshTxGetAlStateProgress(tx, STREAM_TOSERVER) != SshStateKex);
|
|
|
|
|
|
|
|
|
|
FAIL_IF(SSHParserTestUtilCheck("2.0", "MySSHClient-0.5.1", tx, STREAM_TOSERVER));
|
|
|
|
|
|
|
|
|
|
UTHFreePacket(p);
|
|
|
|
|
UTHFreeFlow(f);
|
|
|
|
|
StreamTcpUTClearSession(&ssn);
|
|
|
|
|
StreamTcpUTDeinit(ra_ctx);
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int SSHParserTest09(void)
|
|
|
|
|
{
|
|
|
|
|
TcpReassemblyThreadCtx *ra_ctx = NULL;
|
|
|
|
|
ThreadVars tv;
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
Flow *f = NULL;
|
|
|
|
|
Packet *p = NULL;
|
|
|
|
|
|
|
|
|
|
char sshbufs[2][MAX_SSH_TEST_SIZE] = {"SSH-2.", "0-MySSHClient-0.5.1\r\n"};
|
|
|
|
|
|
|
|
|
|
memset(&tv, 0x00, sizeof(tv));
|
|
|
|
|
|
|
|
|
|
StreamTcpUTInit(&ra_ctx);
|
|
|
|
|
StreamTcpUTInitInline();
|
|
|
|
|
StreamTcpUTSetupSession(&ssn);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.server, 1);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.client, 1);
|
|
|
|
|
|
|
|
|
|
f = UTHBuildFlow(AF_INET, "1.1.1.1", "2.2.2.2", 1234, 2222);
|
|
|
|
|
FAIL_IF_NULL(f);
|
|
|
|
|
f->protoctx = &ssn;
|
|
|
|
|
f->proto = IPPROTO_TCP;
|
|
|
|
|
f->alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
p = PacketGetFromAlloc();
|
|
|
|
|
FAIL_IF(unlikely(p == NULL));
|
|
|
|
|
p->proto = IPPROTO_TCP;
|
|
|
|
|
p->flow = f;
|
|
|
|
|
|
|
|
|
|
uint32_t seq = 2;
|
|
|
|
|
for (int i=0; i<2; i++) {
|
|
|
|
|
FAIL_IF(StreamTcpUTAddSegmentWithPayload(&tv, ra_ctx, &ssn.server, seq, (uint8_t *) sshbufs[i], strlen(sshbufs[i])) == -1);
|
|
|
|
|
seq += strlen(sshbufs[i]);
|
|
|
|
|
FAIL_IF(StreamTcpReassembleAppLayer(&tv, ra_ctx, &ssn, &ssn.server, p, UPDATE_DIR_PACKET) < 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void *ssh_state = f->alstate;
|
|
|
|
|
FAIL_IF_NULL(ssh_state);
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
ssh: rebuild the state machine as per-direction states
The old ssh state scale was coupled across the two directions: the
terminal finished state was only reached after both sides sent NewKeys,
and the progress accessor clamped every per-direction phase to a
shared value. A rule hooking the old top state therefore meant "the
flow completed", not "this direction completed the handshake", and
there was no room above the terminal state for a true completion
marker.
Replace the scale with per-direction phases:
banner(0) -> banner_wait_eol(1) -> kex(2) -> session(3)
NewKeys now sets session on the packet's own direction only, banner
completion sets kex, and the progress accessor is identity: each
direction reports its own raw phase. Add the registered completion
state done(4), never assigned during stream parsing: it stays out
of both name tables, so a rule referencing it fails to load, the
hook listing omits it and the firewall policy loader does not
consult it; the progress accessor reports it only on flows
disrupted by stream depth or async reassembly.
The legacy state names (in_progress, banner_done, finished) no
longer resolve through the name table (pre-production - breaking
changes allowed); the name-table unit test pins the rejection.
The old firewall policy state keys are added to the rename hint
table (like the TLS migration), so a config using them fails
loudly with the replacement named instead of degrading to a bare
warning. The non-prefilter engine-name registration now guards
the null id-to-name result (a hook-only rule at the completion
progress previously formatted a null name into the engine
name).
Add unit tests for the state name table (new names, rejected legacy
names, the unhookable completion state, id-to-name) and for the
per-direction session state.
The eve log condition compares the per-direction states with
>= rather than ==: a direction at session has necessarily passed
kex, and a single delivery can take a direction
banner -> kex -> session, so a final {session, kex}
pair must still admit the log (done is never assigned, so >=
changes only the kex branch).
Ticket: #9046.
1 month ago
|
|
|
FAIL_IF(SCSshTxGetAlStateProgress(tx, STREAM_TOCLIENT) != SshStateKex);
|
|
|
|
|
|
|
|
|
|
FAIL_IF(SSHParserTestUtilCheck("2.0", "MySSHClient-0.5.1", tx, STREAM_TOCLIENT));
|
|
|
|
|
|
|
|
|
|
UTHFreePacket(p);
|
|
|
|
|
UTHFreeFlow(f);
|
|
|
|
|
StreamTcpUTClearSession(&ssn);
|
|
|
|
|
StreamTcpUTDeinit(ra_ctx);
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test Send a version banner in three chunks. */
|
|
|
|
|
static int SSHParserTest10(void)
|
|
|
|
|
{
|
|
|
|
|
TcpReassemblyThreadCtx *ra_ctx = NULL;
|
|
|
|
|
ThreadVars tv;
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
Flow *f = NULL;
|
|
|
|
|
Packet *p = NULL;
|
|
|
|
|
|
|
|
|
|
char sshbufs[3][MAX_SSH_TEST_SIZE] = {"SSH-", "2.", "0-MySSHClient-0.5.1\r\n"};
|
|
|
|
|
|
|
|
|
|
memset(&tv, 0x00, sizeof(tv));
|
|
|
|
|
|
|
|
|
|
StreamTcpUTInit(&ra_ctx);
|
|
|
|
|
StreamTcpUTInitInline();
|
|
|
|
|
StreamTcpUTSetupSession(&ssn);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.server, 1);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.client, 1);
|
|
|
|
|
|
|
|
|
|
f = UTHBuildFlow(AF_INET, "1.1.1.1", "2.2.2.2", 1234, 2222);
|
|
|
|
|
FAIL_IF_NULL(f);
|
|
|
|
|
f->protoctx = &ssn;
|
|
|
|
|
f->proto = IPPROTO_TCP;
|
|
|
|
|
f->alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
p = PacketGetFromAlloc();
|
|
|
|
|
FAIL_IF(unlikely(p == NULL));
|
|
|
|
|
p->proto = IPPROTO_TCP;
|
|
|
|
|
p->flow = f;
|
|
|
|
|
|
|
|
|
|
uint32_t seq = 2;
|
|
|
|
|
for (int i=0; i<3; i++) {
|
|
|
|
|
FAIL_IF(StreamTcpUTAddSegmentWithPayload(&tv, ra_ctx, &ssn.server, seq, (uint8_t *) sshbufs[i], strlen(sshbufs[i])) == -1);
|
|
|
|
|
seq += strlen(sshbufs[i]);
|
|
|
|
|
FAIL_IF(StreamTcpReassembleAppLayer(&tv, ra_ctx, &ssn, &ssn.server, p, UPDATE_DIR_PACKET) < 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void *ssh_state = f->alstate;
|
|
|
|
|
FAIL_IF_NULL(ssh_state);
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
ssh: rebuild the state machine as per-direction states
The old ssh state scale was coupled across the two directions: the
terminal finished state was only reached after both sides sent NewKeys,
and the progress accessor clamped every per-direction phase to a
shared value. A rule hooking the old top state therefore meant "the
flow completed", not "this direction completed the handshake", and
there was no room above the terminal state for a true completion
marker.
Replace the scale with per-direction phases:
banner(0) -> banner_wait_eol(1) -> kex(2) -> session(3)
NewKeys now sets session on the packet's own direction only, banner
completion sets kex, and the progress accessor is identity: each
direction reports its own raw phase. Add the registered completion
state done(4), never assigned during stream parsing: it stays out
of both name tables, so a rule referencing it fails to load, the
hook listing omits it and the firewall policy loader does not
consult it; the progress accessor reports it only on flows
disrupted by stream depth or async reassembly.
The legacy state names (in_progress, banner_done, finished) no
longer resolve through the name table (pre-production - breaking
changes allowed); the name-table unit test pins the rejection.
The old firewall policy state keys are added to the rename hint
table (like the TLS migration), so a config using them fails
loudly with the replacement named instead of degrading to a bare
warning. The non-prefilter engine-name registration now guards
the null id-to-name result (a hook-only rule at the completion
progress previously formatted a null name into the engine
name).
Add unit tests for the state name table (new names, rejected legacy
names, the unhookable completion state, id-to-name) and for the
per-direction session state.
The eve log condition compares the per-direction states with
>= rather than ==: a direction at session has necessarily passed
kex, and a single delivery can take a direction
banner -> kex -> session, so a final {session, kex}
pair must still admit the log (done is never assigned, so >=
changes only the kex branch).
Ticket: #9046.
1 month ago
|
|
|
FAIL_IF(SCSshTxGetAlStateProgress(tx, STREAM_TOCLIENT) != SshStateKex);
|
|
|
|
|
|
|
|
|
|
FAIL_IF(SSHParserTestUtilCheck("2.0", "MySSHClient-0.5.1", tx, STREAM_TOCLIENT));
|
|
|
|
|
|
|
|
|
|
UTHFreePacket(p);
|
|
|
|
|
UTHFreeFlow(f);
|
|
|
|
|
StreamTcpUTClearSession(&ssn);
|
|
|
|
|
StreamTcpUTDeinit(ra_ctx);
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test Send a banner and record in three chunks. */
|
|
|
|
|
static int SSHParserTest11(void)
|
|
|
|
|
{
|
|
|
|
|
int result = 0;
|
|
|
|
|
Flow f;
|
|
|
|
|
uint8_t sshbuf1[] = "SSH-2.0-MySSHClient-0.5.1\r\n";
|
|
|
|
|
uint32_t sshlen1 = sizeof(sshbuf1) - 1;
|
|
|
|
|
uint8_t sshbuf2[] = { 0x00, 0x00, 0x00, 0x03, 0x01, 21, 0x00};
|
|
|
|
|
uint32_t sshlen2 = sizeof(sshbuf2);
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
AppLayerParserThreadCtx *alp_tctx = AppLayerParserThreadCtxAlloc();
|
|
|
|
|
|
|
|
|
|
memset(&f, 0, sizeof(f));
|
|
|
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
|
FLOW_INITIALIZE(&f);
|
|
|
|
|
f.protoctx = (void *)&ssn;
|
|
|
|
|
f.proto = IPPROTO_TCP;
|
|
|
|
|
f.alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
StreamTcpInitConfig(true);
|
|
|
|
|
|
|
|
|
|
int r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH,
|
|
|
|
|
STREAM_TOSERVER, sshbuf1, sshlen1);
|
|
|
|
|
if (r != 0) {
|
|
|
|
|
printf("toserver chunk 1 returned %" PRId32 ", expected 0: ", r);
|
|
|
|
|
goto end;
|
|
|
|
|
}
|
|
|
|
|
r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOSERVER,
|
|
|
|
|
sshbuf2, sshlen2);
|
|
|
|
|
if (r != 0) {
|
|
|
|
|
printf("toserver chunk 2 returned %" PRId32 ", expected 0: ", r);
|
|
|
|
|
goto end;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void *ssh_state = f.alstate;
|
|
|
|
|
if (ssh_state == NULL) {
|
|
|
|
|
printf("no ssh state: ");
|
|
|
|
|
goto end;
|
|
|
|
|
}
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
ssh: rebuild the state machine as per-direction states
The old ssh state scale was coupled across the two directions: the
terminal finished state was only reached after both sides sent NewKeys,
and the progress accessor clamped every per-direction phase to a
shared value. A rule hooking the old top state therefore meant "the
flow completed", not "this direction completed the handshake", and
there was no room above the terminal state for a true completion
marker.
Replace the scale with per-direction phases:
banner(0) -> banner_wait_eol(1) -> kex(2) -> session(3)
NewKeys now sets session on the packet's own direction only, banner
completion sets kex, and the progress accessor is identity: each
direction reports its own raw phase. Add the registered completion
state done(4), never assigned during stream parsing: it stays out
of both name tables, so a rule referencing it fails to load, the
hook listing omits it and the firewall policy loader does not
consult it; the progress accessor reports it only on flows
disrupted by stream depth or async reassembly.
The legacy state names (in_progress, banner_done, finished) no
longer resolve through the name table (pre-production - breaking
changes allowed); the name-table unit test pins the rejection.
The old firewall policy state keys are added to the rename hint
table (like the TLS migration), so a config using them fails
loudly with the replacement named instead of degrading to a bare
warning. The non-prefilter engine-name registration now guards
the null id-to-name result (a hook-only rule at the completion
progress previously formatted a null name into the engine
name).
Add unit tests for the state name table (new names, rejected legacy
names, the unhookable completion state, id-to-name) and for the
per-direction session state.
The eve log condition compares the per-direction states with
>= rather than ==: a direction at session has necessarily passed
kex, and a single delivery can take a direction
banner -> kex -> session, so a final {session, kex}
pair must still admit the log (done is never assigned, so >=
changes only the kex branch).
Ticket: #9046.
1 month ago
|
|
|
if (SCSshTxGetFlags(tx, STREAM_TOSERVER) != SshStateSession) {
|
|
|
|
|
printf("Didn't detect the msg code of new keys (ciphered data starts): ");
|
|
|
|
|
goto end;
|
|
|
|
|
}
|
|
|
|
|
if (SSHParserTestUtilCheck("2.0", "MySSHClient-0.5.1", tx, STREAM_TOSERVER))
|
|
|
|
|
goto end;
|
|
|
|
|
|
|
|
|
|
result = 1;
|
|
|
|
|
end:
|
|
|
|
|
FLOW_DESTROY(&f);
|
|
|
|
|
if (alp_tctx != NULL)
|
|
|
|
|
AppLayerParserThreadCtxFree(alp_tctx);
|
|
|
|
|
StreamTcpFreeConfig(true);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test Send a banner and 2 records record in four chunks. */
|
|
|
|
|
static int SSHParserTest12(void)
|
|
|
|
|
{
|
|
|
|
|
int result = 0;
|
|
|
|
|
Flow f;
|
|
|
|
|
uint8_t sshbuf1[] = "SSH-2.0-MySSHClient-0.5.1\r\n";
|
|
|
|
|
uint32_t sshlen1 = sizeof(sshbuf1) - 1;
|
|
|
|
|
uint8_t sshbuf2[] = { 0x00, 0x00, 0x00, 0x03,0x01, 17, 0x00};
|
|
|
|
|
uint32_t sshlen2 = sizeof(sshbuf2);
|
|
|
|
|
uint8_t sshbuf3[] = { 0x00, 0x00, 0x00, 0x03,0x01, 21, 0x00};
|
|
|
|
|
uint32_t sshlen3 = sizeof(sshbuf3);
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
AppLayerParserThreadCtx *alp_tctx = AppLayerParserThreadCtxAlloc();
|
|
|
|
|
|
|
|
|
|
memset(&f, 0, sizeof(f));
|
|
|
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
|
FLOW_INITIALIZE(&f);
|
|
|
|
|
f.protoctx = (void *)&ssn;
|
|
|
|
|
f.proto = IPPROTO_TCP;
|
|
|
|
|
f.alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
StreamTcpInitConfig(true);
|
|
|
|
|
|
|
|
|
|
int r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH,
|
|
|
|
|
STREAM_TOSERVER, sshbuf1, sshlen1);
|
|
|
|
|
if (r != 0) {
|
|
|
|
|
printf("toserver chunk 1 returned %" PRId32 ", expected 0: ", r);
|
|
|
|
|
goto end;
|
|
|
|
|
}
|
|
|
|
|
r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOSERVER,
|
|
|
|
|
sshbuf2, sshlen2);
|
|
|
|
|
if (r != 0) {
|
|
|
|
|
printf("toserver chunk 2 returned %" PRId32 ", expected 0: ", r);
|
|
|
|
|
goto end;
|
|
|
|
|
}
|
|
|
|
|
r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOSERVER,
|
|
|
|
|
sshbuf3, sshlen3);
|
|
|
|
|
if (r != 0) {
|
|
|
|
|
printf("toserver chunk 3 returned %" PRId32 ", expected 0: ", r);
|
|
|
|
|
goto end;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void *ssh_state = f.alstate;
|
|
|
|
|
if (ssh_state == NULL) {
|
|
|
|
|
printf("no ssh state: ");
|
|
|
|
|
goto end;
|
|
|
|
|
}
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
ssh: rebuild the state machine as per-direction states
The old ssh state scale was coupled across the two directions: the
terminal finished state was only reached after both sides sent NewKeys,
and the progress accessor clamped every per-direction phase to a
shared value. A rule hooking the old top state therefore meant "the
flow completed", not "this direction completed the handshake", and
there was no room above the terminal state for a true completion
marker.
Replace the scale with per-direction phases:
banner(0) -> banner_wait_eol(1) -> kex(2) -> session(3)
NewKeys now sets session on the packet's own direction only, banner
completion sets kex, and the progress accessor is identity: each
direction reports its own raw phase. Add the registered completion
state done(4), never assigned during stream parsing: it stays out
of both name tables, so a rule referencing it fails to load, the
hook listing omits it and the firewall policy loader does not
consult it; the progress accessor reports it only on flows
disrupted by stream depth or async reassembly.
The legacy state names (in_progress, banner_done, finished) no
longer resolve through the name table (pre-production - breaking
changes allowed); the name-table unit test pins the rejection.
The old firewall policy state keys are added to the rename hint
table (like the TLS migration), so a config using them fails
loudly with the replacement named instead of degrading to a bare
warning. The non-prefilter engine-name registration now guards
the null id-to-name result (a hook-only rule at the completion
progress previously formatted a null name into the engine
name).
Add unit tests for the state name table (new names, rejected legacy
names, the unhookable completion state, id-to-name) and for the
per-direction session state.
The eve log condition compares the per-direction states with
>= rather than ==: a direction at session has necessarily passed
kex, and a single delivery can take a direction
banner -> kex -> session, so a final {session, kex}
pair must still admit the log (done is never assigned, so >=
changes only the kex branch).
Ticket: #9046.
1 month ago
|
|
|
if (SCSshTxGetFlags(tx, STREAM_TOSERVER) != SshStateSession) {
|
|
|
|
|
printf("Didn't detect the msg code of new keys (ciphered data starts): ");
|
|
|
|
|
goto end;
|
|
|
|
|
}
|
|
|
|
|
if (SSHParserTestUtilCheck("2.0", "MySSHClient-0.5.1", tx, STREAM_TOSERVER))
|
|
|
|
|
goto end;
|
|
|
|
|
|
|
|
|
|
result = 1;
|
|
|
|
|
end:
|
|
|
|
|
FLOW_DESTROY(&f);
|
|
|
|
|
if (alp_tctx != NULL)
|
|
|
|
|
AppLayerParserThreadCtxFree(alp_tctx);
|
|
|
|
|
StreamTcpFreeConfig(true);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test Send a banner and 2 records record in four chunks. */
|
|
|
|
|
static int SSHParserTest13(void)
|
|
|
|
|
{
|
|
|
|
|
TcpReassemblyThreadCtx *ra_ctx = NULL;
|
|
|
|
|
ThreadVars tv;
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
Flow *f = NULL;
|
|
|
|
|
Packet *p = NULL;
|
|
|
|
|
|
|
|
|
|
uint8_t sshbuf1[] = "SSH-2.0-MySSHClient-0.5.1\r\n";
|
|
|
|
|
uint8_t sshbuf2[] = { 0x00, 0x00, 0x00, 0x02, 0x01, 17};
|
|
|
|
|
uint8_t sshbuf3[] = { 0x00, 0x00, 0x00, 0x02, 0x01, 21};
|
|
|
|
|
|
|
|
|
|
uint8_t* sshbufs[3] = {sshbuf1, sshbuf2, sshbuf3};
|
|
|
|
|
uint32_t sshlens[3] = {sizeof(sshbuf1) - 1, sizeof(sshbuf2), sizeof(sshbuf3)};
|
|
|
|
|
|
|
|
|
|
memset(&tv, 0x00, sizeof(tv));
|
|
|
|
|
|
|
|
|
|
StreamTcpUTInit(&ra_ctx);
|
|
|
|
|
StreamTcpUTInitInline();
|
|
|
|
|
StreamTcpUTSetupSession(&ssn);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.server, 1);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.client, 1);
|
|
|
|
|
|
|
|
|
|
f = UTHBuildFlow(AF_INET, "1.1.1.1", "2.2.2.2", 1234, 2222);
|
|
|
|
|
FAIL_IF_NULL(f);
|
|
|
|
|
f->protoctx = &ssn;
|
|
|
|
|
f->proto = IPPROTO_TCP;
|
|
|
|
|
f->alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
p = PacketGetFromAlloc();
|
|
|
|
|
FAIL_IF(unlikely(p == NULL));
|
|
|
|
|
p->proto = IPPROTO_TCP;
|
|
|
|
|
p->flow = f;
|
|
|
|
|
|
|
|
|
|
uint32_t seq = 2;
|
|
|
|
|
for (int i=0; i<3; i++) {
|
|
|
|
|
FAIL_IF(StreamTcpUTAddSegmentWithPayload(&tv, ra_ctx, &ssn.client, seq, sshbufs[i], sshlens[i]) == -1);
|
|
|
|
|
seq += sshlens[i];
|
|
|
|
|
FAIL_IF(StreamTcpReassembleAppLayer(&tv, ra_ctx, &ssn, &ssn.client, p, UPDATE_DIR_PACKET) < 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void *ssh_state = f->alstate;
|
|
|
|
|
FAIL_IF_NULL(ssh_state);
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
ssh: rebuild the state machine as per-direction states
The old ssh state scale was coupled across the two directions: the
terminal finished state was only reached after both sides sent NewKeys,
and the progress accessor clamped every per-direction phase to a
shared value. A rule hooking the old top state therefore meant "the
flow completed", not "this direction completed the handshake", and
there was no room above the terminal state for a true completion
marker.
Replace the scale with per-direction phases:
banner(0) -> banner_wait_eol(1) -> kex(2) -> session(3)
NewKeys now sets session on the packet's own direction only, banner
completion sets kex, and the progress accessor is identity: each
direction reports its own raw phase. Add the registered completion
state done(4), never assigned during stream parsing: it stays out
of both name tables, so a rule referencing it fails to load, the
hook listing omits it and the firewall policy loader does not
consult it; the progress accessor reports it only on flows
disrupted by stream depth or async reassembly.
The legacy state names (in_progress, banner_done, finished) no
longer resolve through the name table (pre-production - breaking
changes allowed); the name-table unit test pins the rejection.
The old firewall policy state keys are added to the rename hint
table (like the TLS migration), so a config using them fails
loudly with the replacement named instead of degrading to a bare
warning. The non-prefilter engine-name registration now guards
the null id-to-name result (a hook-only rule at the completion
progress previously formatted a null name into the engine
name).
Add unit tests for the state name table (new names, rejected legacy
names, the unhookable completion state, id-to-name) and for the
per-direction session state.
The eve log condition compares the per-direction states with
>= rather than ==: a direction at session has necessarily passed
kex, and a single delivery can take a direction
banner -> kex -> session, so a final {session, kex}
pair must still admit the log (done is never assigned, so >=
changes only the kex branch).
Ticket: #9046.
1 month ago
|
|
|
FAIL_IF(SCSshTxGetFlags(tx, STREAM_TOSERVER) != SshStateSession);
|
|
|
|
|
|
|
|
|
|
FAIL_IF(SSHParserTestUtilCheck("2.0", "MySSHClient-0.5.1", tx, STREAM_TOSERVER));
|
|
|
|
|
|
|
|
|
|
UTHFreePacket(p);
|
|
|
|
|
UTHFreeFlow(f);
|
|
|
|
|
StreamTcpUTClearSession(&ssn);
|
|
|
|
|
StreamTcpUTDeinit(ra_ctx);
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test Send a banner and 2 records record in four chunks. */
|
|
|
|
|
static int SSHParserTest14(void)
|
|
|
|
|
{
|
|
|
|
|
TcpReassemblyThreadCtx *ra_ctx = NULL;
|
|
|
|
|
ThreadVars tv;
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
Flow *f = NULL;
|
|
|
|
|
Packet *p = NULL;
|
|
|
|
|
|
|
|
|
|
uint8_t sshbuf1[] = "SSH-2.0-MySSHClient-0.5.1\r\n";
|
|
|
|
|
uint8_t sshbuf2[] = { 0x00, 0x00, 0x00, 0x10, 0x01, 17, 0x00};
|
|
|
|
|
uint8_t sshbuf3[] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
|
|
|
|
|
uint8_t sshbuf4[] = { 0x09, 0x10, 0x11, 0x12, 0x13, 0x00};
|
|
|
|
|
/* first byte of this record in sshbuf4 */
|
|
|
|
|
uint8_t sshbuf5[] = { 0x00, 0x00, 0x02, 0x01, 21};
|
|
|
|
|
|
|
|
|
|
uint8_t* sshbufs[5] = {sshbuf1, sshbuf2, sshbuf3, sshbuf4, sshbuf5};
|
|
|
|
|
uint32_t sshlens[5] = {sizeof(sshbuf1) - 1, sizeof(sshbuf2), sizeof(sshbuf3), sizeof(sshbuf4), sizeof(sshbuf5)};
|
|
|
|
|
|
|
|
|
|
memset(&tv, 0x00, sizeof(tv));
|
|
|
|
|
|
|
|
|
|
StreamTcpUTInit(&ra_ctx);
|
|
|
|
|
StreamTcpUTInitInline();
|
|
|
|
|
StreamTcpUTSetupSession(&ssn);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.server, 1);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.client, 1);
|
|
|
|
|
|
|
|
|
|
f = UTHBuildFlow(AF_INET, "1.1.1.1", "2.2.2.2", 1234, 2222);
|
|
|
|
|
FAIL_IF_NULL(f);
|
|
|
|
|
f->protoctx = &ssn;
|
|
|
|
|
f->proto = IPPROTO_TCP;
|
|
|
|
|
f->alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
p = PacketGetFromAlloc();
|
|
|
|
|
FAIL_IF(unlikely(p == NULL));
|
|
|
|
|
p->proto = IPPROTO_TCP;
|
|
|
|
|
p->flow = f;
|
|
|
|
|
|
|
|
|
|
uint32_t seq = 2;
|
|
|
|
|
for (int i=0; i<5; i++) {
|
|
|
|
|
FAIL_IF(StreamTcpUTAddSegmentWithPayload(&tv, ra_ctx, &ssn.client, seq, sshbufs[i], sshlens[i]) == -1);
|
|
|
|
|
seq += sshlens[i];
|
|
|
|
|
FAIL_IF(StreamTcpReassembleAppLayer(&tv, ra_ctx, &ssn, &ssn.client, p, UPDATE_DIR_PACKET) < 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void *ssh_state = f->alstate;
|
|
|
|
|
FAIL_IF_NULL(ssh_state);
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
ssh: rebuild the state machine as per-direction states
The old ssh state scale was coupled across the two directions: the
terminal finished state was only reached after both sides sent NewKeys,
and the progress accessor clamped every per-direction phase to a
shared value. A rule hooking the old top state therefore meant "the
flow completed", not "this direction completed the handshake", and
there was no room above the terminal state for a true completion
marker.
Replace the scale with per-direction phases:
banner(0) -> banner_wait_eol(1) -> kex(2) -> session(3)
NewKeys now sets session on the packet's own direction only, banner
completion sets kex, and the progress accessor is identity: each
direction reports its own raw phase. Add the registered completion
state done(4), never assigned during stream parsing: it stays out
of both name tables, so a rule referencing it fails to load, the
hook listing omits it and the firewall policy loader does not
consult it; the progress accessor reports it only on flows
disrupted by stream depth or async reassembly.
The legacy state names (in_progress, banner_done, finished) no
longer resolve through the name table (pre-production - breaking
changes allowed); the name-table unit test pins the rejection.
The old firewall policy state keys are added to the rename hint
table (like the TLS migration), so a config using them fails
loudly with the replacement named instead of degrading to a bare
warning. The non-prefilter engine-name registration now guards
the null id-to-name result (a hook-only rule at the completion
progress previously formatted a null name into the engine
name).
Add unit tests for the state name table (new names, rejected legacy
names, the unhookable completion state, id-to-name) and for the
per-direction session state.
The eve log condition compares the per-direction states with
>= rather than ==: a direction at session has necessarily passed
kex, and a single delivery can take a direction
banner -> kex -> session, so a final {session, kex}
pair must still admit the log (done is never assigned, so >=
changes only the kex branch).
Ticket: #9046.
1 month ago
|
|
|
FAIL_IF(SCSshTxGetFlags(tx, STREAM_TOSERVER) != SshStateSession);
|
|
|
|
|
|
|
|
|
|
FAIL_IF(SSHParserTestUtilCheck("2.0", "MySSHClient-0.5.1", tx, STREAM_TOSERVER));
|
|
|
|
|
|
|
|
|
|
UTHFreePacket(p);
|
|
|
|
|
UTHFreeFlow(f);
|
|
|
|
|
StreamTcpUTClearSession(&ssn);
|
|
|
|
|
StreamTcpUTDeinit(ra_ctx);
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test Send a banner and 2 records record in four chunks. */
|
|
|
|
|
static int SSHParserTest15(void)
|
|
|
|
|
{
|
|
|
|
|
TcpReassemblyThreadCtx *ra_ctx = NULL;
|
|
|
|
|
ThreadVars tv;
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
Flow *f = NULL;
|
|
|
|
|
Packet *p = NULL;
|
|
|
|
|
|
|
|
|
|
uint8_t sshbuf1[] = "SSH-2.0-MySSHClient-0.5.1\r\n";
|
|
|
|
|
uint8_t sshbuf2[] = { 0x00, 0x00, 0x00, 0x10, 0x01, 17, 0x00};
|
|
|
|
|
uint8_t sshbuf3[] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
|
|
|
|
|
uint8_t sshbuf4[] = { 0x09, 0x10, 0x11, 0x12, 0x13, 0x00};
|
|
|
|
|
uint8_t sshbuf5[] = { 0x00, 0x00, 0x02, 0x01, 20, 0x00, 0x00, 0x00, 0x02, 0x01, 21};
|
|
|
|
|
|
|
|
|
|
uint8_t* sshbufs[5] = {sshbuf1, sshbuf2, sshbuf3, sshbuf4, sshbuf5};
|
|
|
|
|
uint32_t sshlens[5] = {sizeof(sshbuf1) - 1, sizeof(sshbuf2), sizeof(sshbuf3), sizeof(sshbuf4), sizeof(sshbuf5)};
|
|
|
|
|
|
|
|
|
|
memset(&tv, 0x00, sizeof(tv));
|
|
|
|
|
|
|
|
|
|
StreamTcpUTInit(&ra_ctx);
|
|
|
|
|
StreamTcpUTInitInline();
|
|
|
|
|
StreamTcpUTSetupSession(&ssn);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.server, 1);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.client, 1);
|
|
|
|
|
|
|
|
|
|
f = UTHBuildFlow(AF_INET, "1.1.1.1", "2.2.2.2", 1234, 2222);
|
|
|
|
|
FAIL_IF_NULL(f);
|
|
|
|
|
f->protoctx = &ssn;
|
|
|
|
|
f->proto = IPPROTO_TCP;
|
|
|
|
|
f->alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
p = PacketGetFromAlloc();
|
|
|
|
|
FAIL_IF(unlikely(p == NULL));
|
|
|
|
|
p->proto = IPPROTO_TCP;
|
|
|
|
|
p->flow = f;
|
|
|
|
|
|
|
|
|
|
uint32_t seq = 2;
|
|
|
|
|
for (int i=0; i<5; i++) {
|
|
|
|
|
FAIL_IF(StreamTcpUTAddSegmentWithPayload(&tv, ra_ctx, &ssn.client, seq, sshbufs[i], sshlens[i]) == -1);
|
|
|
|
|
seq += sshlens[i];
|
|
|
|
|
FAIL_IF(StreamTcpReassembleAppLayer(&tv, ra_ctx, &ssn, &ssn.client, p, UPDATE_DIR_PACKET) < 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void *ssh_state = f->alstate;
|
|
|
|
|
FAIL_IF_NULL(ssh_state);
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
ssh: rebuild the state machine as per-direction states
The old ssh state scale was coupled across the two directions: the
terminal finished state was only reached after both sides sent NewKeys,
and the progress accessor clamped every per-direction phase to a
shared value. A rule hooking the old top state therefore meant "the
flow completed", not "this direction completed the handshake", and
there was no room above the terminal state for a true completion
marker.
Replace the scale with per-direction phases:
banner(0) -> banner_wait_eol(1) -> kex(2) -> session(3)
NewKeys now sets session on the packet's own direction only, banner
completion sets kex, and the progress accessor is identity: each
direction reports its own raw phase. Add the registered completion
state done(4), never assigned during stream parsing: it stays out
of both name tables, so a rule referencing it fails to load, the
hook listing omits it and the firewall policy loader does not
consult it; the progress accessor reports it only on flows
disrupted by stream depth or async reassembly.
The legacy state names (in_progress, banner_done, finished) no
longer resolve through the name table (pre-production - breaking
changes allowed); the name-table unit test pins the rejection.
The old firewall policy state keys are added to the rename hint
table (like the TLS migration), so a config using them fails
loudly with the replacement named instead of degrading to a bare
warning. The non-prefilter engine-name registration now guards
the null id-to-name result (a hook-only rule at the completion
progress previously formatted a null name into the engine
name).
Add unit tests for the state name table (new names, rejected legacy
names, the unhookable completion state, id-to-name) and for the
per-direction session state.
The eve log condition compares the per-direction states with
>= rather than ==: a direction at session has necessarily passed
kex, and a single delivery can take a direction
banner -> kex -> session, so a final {session, kex}
pair must still admit the log (done is never assigned, so >=
changes only the kex branch).
Ticket: #9046.
1 month ago
|
|
|
FAIL_IF(SCSshTxGetFlags(tx, STREAM_TOSERVER) != SshStateSession);
|
|
|
|
|
|
|
|
|
|
FAIL_IF(SSHParserTestUtilCheck("2.0", "MySSHClient-0.5.1", tx, STREAM_TOSERVER));
|
|
|
|
|
|
|
|
|
|
UTHFreePacket(p);
|
|
|
|
|
UTHFreeFlow(f);
|
|
|
|
|
StreamTcpUTClearSession(&ssn);
|
|
|
|
|
StreamTcpUTDeinit(ra_ctx);
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test Send toserver a banner and record in three chunks. */
|
|
|
|
|
static int SSHParserTest16(void)
|
|
|
|
|
{
|
|
|
|
|
TcpReassemblyThreadCtx *ra_ctx = NULL;
|
|
|
|
|
ThreadVars tv;
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
Flow *f = NULL;
|
|
|
|
|
Packet *p = NULL;
|
|
|
|
|
|
|
|
|
|
uint8_t sshbuf1[] = "SSH-";
|
|
|
|
|
uint8_t sshbuf2[] = "2.0-MySSHClient-0.5.1\r\n";
|
|
|
|
|
uint8_t sshbuf3[] = { 0x00, 0x00, 0x00, 0x03,0x01, 21, 0x00};
|
|
|
|
|
|
|
|
|
|
uint8_t* sshbufs[3] = {sshbuf1, sshbuf2, sshbuf3};
|
|
|
|
|
uint32_t sshlens[3] = {sizeof(sshbuf1) - 1, sizeof(sshbuf2) - 1, sizeof(sshbuf3)};
|
|
|
|
|
|
|
|
|
|
memset(&tv, 0x00, sizeof(tv));
|
|
|
|
|
|
|
|
|
|
StreamTcpUTInit(&ra_ctx);
|
|
|
|
|
StreamTcpUTInitInline();
|
|
|
|
|
StreamTcpUTSetupSession(&ssn);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.server, 1);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.client, 1);
|
|
|
|
|
|
|
|
|
|
f = UTHBuildFlow(AF_INET, "1.1.1.1", "2.2.2.2", 1234, 2222);
|
|
|
|
|
FAIL_IF_NULL(f);
|
|
|
|
|
f->protoctx = &ssn;
|
|
|
|
|
f->proto = IPPROTO_TCP;
|
|
|
|
|
f->alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
p = PacketGetFromAlloc();
|
|
|
|
|
FAIL_IF(unlikely(p == NULL));
|
|
|
|
|
p->proto = IPPROTO_TCP;
|
|
|
|
|
p->flow = f;
|
|
|
|
|
|
|
|
|
|
uint32_t seq = 2;
|
|
|
|
|
for (int i=0; i<3; i++) {
|
|
|
|
|
FAIL_IF(StreamTcpUTAddSegmentWithPayload(&tv, ra_ctx, &ssn.server, seq, sshbufs[i], sshlens[i]) == -1);
|
|
|
|
|
seq += sshlens[i];
|
|
|
|
|
FAIL_IF(StreamTcpReassembleAppLayer(&tv, ra_ctx, &ssn, &ssn.server, p, UPDATE_DIR_PACKET) < 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void *ssh_state = f->alstate;
|
|
|
|
|
FAIL_IF_NULL(ssh_state);
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
ssh: rebuild the state machine as per-direction states
The old ssh state scale was coupled across the two directions: the
terminal finished state was only reached after both sides sent NewKeys,
and the progress accessor clamped every per-direction phase to a
shared value. A rule hooking the old top state therefore meant "the
flow completed", not "this direction completed the handshake", and
there was no room above the terminal state for a true completion
marker.
Replace the scale with per-direction phases:
banner(0) -> banner_wait_eol(1) -> kex(2) -> session(3)
NewKeys now sets session on the packet's own direction only, banner
completion sets kex, and the progress accessor is identity: each
direction reports its own raw phase. Add the registered completion
state done(4), never assigned during stream parsing: it stays out
of both name tables, so a rule referencing it fails to load, the
hook listing omits it and the firewall policy loader does not
consult it; the progress accessor reports it only on flows
disrupted by stream depth or async reassembly.
The legacy state names (in_progress, banner_done, finished) no
longer resolve through the name table (pre-production - breaking
changes allowed); the name-table unit test pins the rejection.
The old firewall policy state keys are added to the rename hint
table (like the TLS migration), so a config using them fails
loudly with the replacement named instead of degrading to a bare
warning. The non-prefilter engine-name registration now guards
the null id-to-name result (a hook-only rule at the completion
progress previously formatted a null name into the engine
name).
Add unit tests for the state name table (new names, rejected legacy
names, the unhookable completion state, id-to-name) and for the
per-direction session state.
The eve log condition compares the per-direction states with
>= rather than ==: a direction at session has necessarily passed
kex, and a single delivery can take a direction
banner -> kex -> session, so a final {session, kex}
pair must still admit the log (done is never assigned, so >=
changes only the kex branch).
Ticket: #9046.
1 month ago
|
|
|
FAIL_IF(SCSshTxGetFlags(tx, STREAM_TOCLIENT) != SshStateSession);
|
|
|
|
|
|
|
|
|
|
FAIL_IF(SSHParserTestUtilCheck("2.0", "MySSHClient-0.5.1", tx, STREAM_TOCLIENT));
|
|
|
|
|
|
|
|
|
|
UTHFreePacket(p);
|
|
|
|
|
UTHFreeFlow(f);
|
|
|
|
|
StreamTcpUTClearSession(&ssn);
|
|
|
|
|
StreamTcpUTDeinit(ra_ctx);
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test Send toserver a banner and 2 records record in four chunks. */
|
|
|
|
|
static int SSHParserTest17(void)
|
|
|
|
|
{
|
|
|
|
|
TcpReassemblyThreadCtx *ra_ctx = NULL;
|
|
|
|
|
ThreadVars tv;
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
Flow *f = NULL;
|
|
|
|
|
Packet *p = NULL;
|
|
|
|
|
|
|
|
|
|
uint8_t sshbuf1[] = "SSH-";
|
|
|
|
|
uint8_t sshbuf2[] = "2.0-MySSHClient-0.5.1\r\n";
|
|
|
|
|
uint8_t sshbuf3[] = { 0x00, 0x00, 0x00, 0x03, 0x01, 17, 0x00};
|
|
|
|
|
uint8_t sshbuf4[] = { 0x00, 0x00, 0x00, 0x03, 0x01, 21, 0x00};
|
|
|
|
|
|
|
|
|
|
uint8_t* sshbufs[4] = {sshbuf1, sshbuf2, sshbuf3, sshbuf4};
|
|
|
|
|
uint32_t sshlens[4] = {sizeof(sshbuf1) - 1, sizeof(sshbuf2) - 1, sizeof(sshbuf3), sizeof(sshbuf4)};
|
|
|
|
|
|
|
|
|
|
memset(&tv, 0x00, sizeof(tv));
|
|
|
|
|
|
|
|
|
|
StreamTcpUTInit(&ra_ctx);
|
|
|
|
|
StreamTcpUTInitInline();
|
|
|
|
|
StreamTcpUTSetupSession(&ssn);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.server, 1);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.client, 1);
|
|
|
|
|
|
|
|
|
|
f = UTHBuildFlow(AF_INET, "1.1.1.1", "2.2.2.2", 1234, 2222);
|
|
|
|
|
FAIL_IF_NULL(f);
|
|
|
|
|
f->protoctx = &ssn;
|
|
|
|
|
f->proto = IPPROTO_TCP;
|
|
|
|
|
f->alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
p = PacketGetFromAlloc();
|
|
|
|
|
FAIL_IF(unlikely(p == NULL));
|
|
|
|
|
p->proto = IPPROTO_TCP;
|
|
|
|
|
p->flow = f;
|
|
|
|
|
|
|
|
|
|
uint32_t seq = 2;
|
|
|
|
|
for (int i=0; i<4; i++) {
|
|
|
|
|
FAIL_IF(StreamTcpUTAddSegmentWithPayload(&tv, ra_ctx, &ssn.server, seq, sshbufs[i], sshlens[i]) == -1);
|
|
|
|
|
seq += sshlens[i];
|
|
|
|
|
FAIL_IF(StreamTcpReassembleAppLayer(&tv, ra_ctx, &ssn, &ssn.server, p, UPDATE_DIR_PACKET) < 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void *ssh_state = f->alstate;
|
|
|
|
|
FAIL_IF_NULL(ssh_state);
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
ssh: rebuild the state machine as per-direction states
The old ssh state scale was coupled across the two directions: the
terminal finished state was only reached after both sides sent NewKeys,
and the progress accessor clamped every per-direction phase to a
shared value. A rule hooking the old top state therefore meant "the
flow completed", not "this direction completed the handshake", and
there was no room above the terminal state for a true completion
marker.
Replace the scale with per-direction phases:
banner(0) -> banner_wait_eol(1) -> kex(2) -> session(3)
NewKeys now sets session on the packet's own direction only, banner
completion sets kex, and the progress accessor is identity: each
direction reports its own raw phase. Add the registered completion
state done(4), never assigned during stream parsing: it stays out
of both name tables, so a rule referencing it fails to load, the
hook listing omits it and the firewall policy loader does not
consult it; the progress accessor reports it only on flows
disrupted by stream depth or async reassembly.
The legacy state names (in_progress, banner_done, finished) no
longer resolve through the name table (pre-production - breaking
changes allowed); the name-table unit test pins the rejection.
The old firewall policy state keys are added to the rename hint
table (like the TLS migration), so a config using them fails
loudly with the replacement named instead of degrading to a bare
warning. The non-prefilter engine-name registration now guards
the null id-to-name result (a hook-only rule at the completion
progress previously formatted a null name into the engine
name).
Add unit tests for the state name table (new names, rejected legacy
names, the unhookable completion state, id-to-name) and for the
per-direction session state.
The eve log condition compares the per-direction states with
>= rather than ==: a direction at session has necessarily passed
kex, and a single delivery can take a direction
banner -> kex -> session, so a final {session, kex}
pair must still admit the log (done is never assigned, so >=
changes only the kex branch).
Ticket: #9046.
1 month ago
|
|
|
FAIL_IF(SCSshTxGetFlags(tx, STREAM_TOCLIENT) != SshStateSession);
|
|
|
|
|
|
|
|
|
|
FAIL_IF(SSHParserTestUtilCheck("2.0", "MySSHClient-0.5.1", tx, STREAM_TOCLIENT));
|
|
|
|
|
|
|
|
|
|
UTHFreePacket(p);
|
|
|
|
|
UTHFreeFlow(f);
|
|
|
|
|
StreamTcpUTClearSession(&ssn);
|
|
|
|
|
StreamTcpUTDeinit(ra_ctx);
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test 2 directional test */
|
|
|
|
|
static int SSHParserTest18(void)
|
|
|
|
|
{
|
|
|
|
|
TcpReassemblyThreadCtx *ra_ctx = NULL;
|
|
|
|
|
ThreadVars tv;
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
Flow *f = NULL;
|
|
|
|
|
Packet *p = NULL;
|
|
|
|
|
|
|
|
|
|
uint8_t server1[] = "SSH-2.0-OpenSSH_4.7p1 Debian-8ubuntu3\r\n";
|
|
|
|
|
uint8_t sshbuf1[] = "SSH-";
|
|
|
|
|
uint8_t sshbuf2[] = "2.0-MySSHClient-0.5.1\r\n";
|
|
|
|
|
uint8_t server2[] = { 0x00, 0x00, 0x00, 0x03, 0x01, 21, 0x00 };
|
|
|
|
|
uint8_t sshbuf3[] = { 0x00, 0x00, 0x00, 0x03, 0x01, 21, 0x00 };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
memset(&tv, 0x00, sizeof(tv));
|
|
|
|
|
|
|
|
|
|
StreamTcpUTInit(&ra_ctx);
|
|
|
|
|
StreamTcpUTInitInline();
|
|
|
|
|
StreamTcpUTSetupSession(&ssn);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.server, 1);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.client, 1);
|
|
|
|
|
|
|
|
|
|
uint8_t* sshbufs[5] = {server1, sshbuf1, sshbuf2, server2, sshbuf3};
|
|
|
|
|
uint32_t sshlens[5] = {sizeof(server1) - 1, sizeof(sshbuf1) - 1, sizeof(sshbuf2) -1, sizeof(server2) - 1, sizeof(sshbuf3)};
|
|
|
|
|
bool sshdirs[5] = {true, false, false, true, false};
|
|
|
|
|
|
|
|
|
|
f = UTHBuildFlow(AF_INET, "1.1.1.1", "2.2.2.2", 1234, 2222);
|
|
|
|
|
FAIL_IF_NULL(f);
|
|
|
|
|
f->protoctx = &ssn;
|
|
|
|
|
f->proto = IPPROTO_TCP;
|
|
|
|
|
f->alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
p = PacketGetFromAlloc();
|
|
|
|
|
FAIL_IF(unlikely(p == NULL));
|
|
|
|
|
p->proto = IPPROTO_TCP;
|
|
|
|
|
p->flow = f;
|
|
|
|
|
|
|
|
|
|
uint32_t seqcli = 2;
|
|
|
|
|
uint32_t seqsrv = 2;
|
|
|
|
|
for (int i=0; i<5; i++) {
|
|
|
|
|
if (sshdirs[i]) {
|
|
|
|
|
FAIL_IF(StreamTcpUTAddSegmentWithPayload(&tv, ra_ctx, &ssn.server, seqsrv, sshbufs[i], sshlens[i]) == -1);
|
|
|
|
|
seqsrv += sshlens[i];
|
|
|
|
|
FAIL_IF(StreamTcpReassembleAppLayer(&tv, ra_ctx, &ssn, &ssn.server, p, UPDATE_DIR_PACKET) < 0);
|
|
|
|
|
} else {
|
|
|
|
|
FAIL_IF(StreamTcpUTAddSegmentWithPayload(&tv, ra_ctx, &ssn.client, seqcli, sshbufs[i], sshlens[i]) == -1);
|
|
|
|
|
seqcli += sshlens[i];
|
|
|
|
|
FAIL_IF(StreamTcpReassembleAppLayer(&tv, ra_ctx, &ssn, &ssn.client, p, UPDATE_DIR_PACKET) < 0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void *ssh_state = f->alstate;
|
|
|
|
|
FAIL_IF_NULL(ssh_state);
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
ssh: rebuild the state machine as per-direction states
The old ssh state scale was coupled across the two directions: the
terminal finished state was only reached after both sides sent NewKeys,
and the progress accessor clamped every per-direction phase to a
shared value. A rule hooking the old top state therefore meant "the
flow completed", not "this direction completed the handshake", and
there was no room above the terminal state for a true completion
marker.
Replace the scale with per-direction phases:
banner(0) -> banner_wait_eol(1) -> kex(2) -> session(3)
NewKeys now sets session on the packet's own direction only, banner
completion sets kex, and the progress accessor is identity: each
direction reports its own raw phase. Add the registered completion
state done(4), never assigned during stream parsing: it stays out
of both name tables, so a rule referencing it fails to load, the
hook listing omits it and the firewall policy loader does not
consult it; the progress accessor reports it only on flows
disrupted by stream depth or async reassembly.
The legacy state names (in_progress, banner_done, finished) no
longer resolve through the name table (pre-production - breaking
changes allowed); the name-table unit test pins the rejection.
The old firewall policy state keys are added to the rename hint
table (like the TLS migration), so a config using them fails
loudly with the replacement named instead of degrading to a bare
warning. The non-prefilter engine-name registration now guards
the null id-to-name result (a hook-only rule at the completion
progress previously formatted a null name into the engine
name).
Add unit tests for the state name table (new names, rejected legacy
names, the unhookable completion state, id-to-name) and for the
per-direction session state.
The eve log condition compares the per-direction states with
>= rather than ==: a direction at session has necessarily passed
kex, and a single delivery can take a direction
banner -> kex -> session, so a final {session, kex}
pair must still admit the log (done is never assigned, so >=
changes only the kex branch).
Ticket: #9046.
1 month ago
|
|
|
FAIL_IF(SCSshTxGetFlags(tx, STREAM_TOCLIENT) != SshStateSession);
|
|
|
|
|
|
|
|
|
|
FAIL_IF(!(SCAppLayerParserStateIssetFlag(f->alparser, APP_LAYER_PARSER_NO_INSPECTION)));
|
|
|
|
|
|
|
|
|
|
UTHFreePacket(p);
|
|
|
|
|
UTHFreeFlow(f);
|
|
|
|
|
StreamTcpUTClearSession(&ssn);
|
|
|
|
|
StreamTcpUTDeinit(ra_ctx);
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test Really long banner handling: bannel exactly 255 */
|
|
|
|
|
static int SSHParserTest19(void)
|
|
|
|
|
{
|
|
|
|
|
TcpReassemblyThreadCtx *ra_ctx = NULL;
|
|
|
|
|
ThreadVars tv;
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
Flow *f = NULL;
|
|
|
|
|
Packet *p = NULL;
|
|
|
|
|
|
|
|
|
|
uint8_t sshbuf1[] = "SSH-";
|
|
|
|
|
uint8_t sshbuf2[] = "2.0-";
|
|
|
|
|
uint8_t sshbuf3[] = "abcdefghijklmnopqrstuvwxyz"
|
|
|
|
|
"abcdefghijklmnopqrstuvwxyz"//60
|
|
|
|
|
"abcdefghijklmnopqrstuvwxyz"
|
|
|
|
|
"abcdefghijklmnopqrstuvwxyz"//112
|
|
|
|
|
"abcdefghijklmnopqrstuvwxyz"
|
|
|
|
|
"abcdefghijklmnopqrstuvwxyz"//164
|
|
|
|
|
"abcdefghijklmnopqrstuvwxyz"
|
|
|
|
|
"abcdefghijklmnopqrstuvwxyz"//216
|
|
|
|
|
"abcdefghijklmnopqrstuvwxyz"//242
|
|
|
|
|
"abcdefghijkl\r";//255
|
|
|
|
|
uint8_t sshbuf4[] = { 0x00, 0x00, 0x00, 0x03, 0x01, 21, 0x00};
|
|
|
|
|
|
|
|
|
|
uint8_t* sshbufs[4] = {sshbuf1, sshbuf2, sshbuf3, sshbuf4};
|
|
|
|
|
uint32_t sshlens[4] = {sizeof(sshbuf1) - 1, sizeof(sshbuf2) - 1, sizeof(sshbuf3) - 1, sizeof(sshbuf4)};
|
|
|
|
|
|
|
|
|
|
memset(&tv, 0x00, sizeof(tv));
|
|
|
|
|
|
|
|
|
|
StreamTcpUTInit(&ra_ctx);
|
|
|
|
|
StreamTcpUTInitInline();
|
|
|
|
|
StreamTcpUTSetupSession(&ssn);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.server, 1);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.client, 1);
|
|
|
|
|
|
|
|
|
|
f = UTHBuildFlow(AF_INET, "1.1.1.1", "2.2.2.2", 1234, 2222);
|
|
|
|
|
FAIL_IF_NULL(f);
|
|
|
|
|
f->protoctx = &ssn;
|
|
|
|
|
f->proto = IPPROTO_TCP;
|
|
|
|
|
f->alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
p = PacketGetFromAlloc();
|
|
|
|
|
FAIL_IF(unlikely(p == NULL));
|
|
|
|
|
p->proto = IPPROTO_TCP;
|
|
|
|
|
p->flow = f;
|
|
|
|
|
|
|
|
|
|
uint32_t seq = 2;
|
|
|
|
|
for (int i=0; i<4; i++) {
|
|
|
|
|
FAIL_IF(StreamTcpUTAddSegmentWithPayload(&tv, ra_ctx, &ssn.server, seq, sshbufs[i], sshlens[i]) == -1);
|
|
|
|
|
seq += sshlens[i];
|
|
|
|
|
FAIL_IF(StreamTcpReassembleAppLayer(&tv, ra_ctx, &ssn, &ssn.server, p, UPDATE_DIR_PACKET) < 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void *ssh_state = f->alstate;
|
|
|
|
|
FAIL_IF_NULL(ssh_state);
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
ssh: rebuild the state machine as per-direction states
The old ssh state scale was coupled across the two directions: the
terminal finished state was only reached after both sides sent NewKeys,
and the progress accessor clamped every per-direction phase to a
shared value. A rule hooking the old top state therefore meant "the
flow completed", not "this direction completed the handshake", and
there was no room above the terminal state for a true completion
marker.
Replace the scale with per-direction phases:
banner(0) -> banner_wait_eol(1) -> kex(2) -> session(3)
NewKeys now sets session on the packet's own direction only, banner
completion sets kex, and the progress accessor is identity: each
direction reports its own raw phase. Add the registered completion
state done(4), never assigned during stream parsing: it stays out
of both name tables, so a rule referencing it fails to load, the
hook listing omits it and the firewall policy loader does not
consult it; the progress accessor reports it only on flows
disrupted by stream depth or async reassembly.
The legacy state names (in_progress, banner_done, finished) no
longer resolve through the name table (pre-production - breaking
changes allowed); the name-table unit test pins the rejection.
The old firewall policy state keys are added to the rename hint
table (like the TLS migration), so a config using them fails
loudly with the replacement named instead of degrading to a bare
warning. The non-prefilter engine-name registration now guards
the null id-to-name result (a hook-only rule at the completion
progress previously formatted a null name into the engine
name).
Add unit tests for the state name table (new names, rejected legacy
names, the unhookable completion state, id-to-name) and for the
per-direction session state.
The eve log condition compares the per-direction states with
>= rather than ==: a direction at session has necessarily passed
kex, and a single delivery can take a direction
banner -> kex -> session, so a final {session, kex}
pair must still admit the log (done is never assigned, so >=
changes only the kex branch).
Ticket: #9046.
1 month ago
|
|
|
FAIL_IF(SCSshTxGetFlags(tx, STREAM_TOCLIENT) != SshStateSession);
|
|
|
|
|
|
|
|
|
|
sshbuf3[sizeof(sshbuf3) - 2] = 0;
|
|
|
|
|
FAIL_IF(SSHParserTestUtilCheck("2.0", (char *)sshbuf3, tx, STREAM_TOCLIENT));
|
|
|
|
|
|
|
|
|
|
UTHFreePacket(p);
|
|
|
|
|
UTHFreeFlow(f);
|
|
|
|
|
StreamTcpUTClearSession(&ssn);
|
|
|
|
|
StreamTcpUTDeinit(ra_ctx);
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test Really long banner handling: banner exactly 255,
|
|
|
|
|
* followed by malformed record */
|
|
|
|
|
static int SSHParserTest20(void)
|
|
|
|
|
{
|
|
|
|
|
TcpReassemblyThreadCtx *ra_ctx = NULL;
|
|
|
|
|
ThreadVars tv;
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
Flow *f = NULL;
|
|
|
|
|
Packet *p = NULL;
|
|
|
|
|
|
|
|
|
|
uint8_t sshbuf1[] = "SSH-";
|
|
|
|
|
uint8_t sshbuf2[] = "2.0-";
|
|
|
|
|
uint8_t sshbuf3[] = "abcdefghijklmnopqrstuvwxyz"
|
|
|
|
|
"abcdefghijklmnopqrstuvwxyz"//60
|
|
|
|
|
"abcdefghijklmnopqrstuvwxyz"
|
|
|
|
|
"abcdefghijklmnopqrstuvwxyz"//112
|
|
|
|
|
"abcdefghijklmnopqrstuvwxyz"
|
|
|
|
|
"abcdefghijklmnopqrstuvwxyz"//164
|
|
|
|
|
"abcdefghijklmnopqrstuvwxyz"
|
|
|
|
|
"abcdefghijklmnopqrstuvwxyz"//216
|
|
|
|
|
"abcdefghijklmnopqrstuvwxyz"//242
|
|
|
|
|
"abcdefghijklm\r";//256
|
|
|
|
|
uint8_t sshbuf4[] = {'a','b','c','d','e','f', '\r',
|
|
|
|
|
0x00, 0x00, 0x00, 0x06, 0x01, 21, 0x00, 0x00, 0x00};
|
|
|
|
|
|
|
|
|
|
uint8_t* sshbufs[4] = {sshbuf1, sshbuf2, sshbuf3, sshbuf4};
|
|
|
|
|
uint32_t sshlens[4] = {sizeof(sshbuf1) - 1, sizeof(sshbuf2) - 1, sizeof(sshbuf3) - 1, sizeof(sshbuf4) - 1};
|
|
|
|
|
|
|
|
|
|
memset(&tv, 0x00, sizeof(tv));
|
|
|
|
|
|
|
|
|
|
StreamTcpUTInit(&ra_ctx);
|
|
|
|
|
StreamTcpUTInitInline();
|
|
|
|
|
StreamTcpUTSetupSession(&ssn);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.server, 1);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.client, 1);
|
|
|
|
|
|
|
|
|
|
f = UTHBuildFlow(AF_INET, "1.1.1.1", "2.2.2.2", 1234, 2222);
|
|
|
|
|
FAIL_IF_NULL(f);
|
|
|
|
|
f->protoctx = &ssn;
|
|
|
|
|
f->proto = IPPROTO_TCP;
|
|
|
|
|
f->alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
p = PacketGetFromAlloc();
|
|
|
|
|
FAIL_IF(unlikely(p == NULL));
|
|
|
|
|
p->proto = IPPROTO_TCP;
|
|
|
|
|
p->flow = f;
|
|
|
|
|
|
|
|
|
|
uint32_t seq = 2;
|
|
|
|
|
for (int i=0; i<4; i++) {
|
|
|
|
|
FAIL_IF(StreamTcpUTAddSegmentWithPayload(&tv, ra_ctx, &ssn.server, seq, sshbufs[i], sshlens[i]) == -1);
|
|
|
|
|
seq += sshlens[i];
|
|
|
|
|
FAIL_IF(StreamTcpReassembleAppLayer(&tv, ra_ctx, &ssn, &ssn.server, p, UPDATE_DIR_PACKET) < 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void *ssh_state = f->alstate;
|
|
|
|
|
FAIL_IF_NULL(ssh_state);
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
ssh: rebuild the state machine as per-direction states
The old ssh state scale was coupled across the two directions: the
terminal finished state was only reached after both sides sent NewKeys,
and the progress accessor clamped every per-direction phase to a
shared value. A rule hooking the old top state therefore meant "the
flow completed", not "this direction completed the handshake", and
there was no room above the terminal state for a true completion
marker.
Replace the scale with per-direction phases:
banner(0) -> banner_wait_eol(1) -> kex(2) -> session(3)
NewKeys now sets session on the packet's own direction only, banner
completion sets kex, and the progress accessor is identity: each
direction reports its own raw phase. Add the registered completion
state done(4), never assigned during stream parsing: it stays out
of both name tables, so a rule referencing it fails to load, the
hook listing omits it and the firewall policy loader does not
consult it; the progress accessor reports it only on flows
disrupted by stream depth or async reassembly.
The legacy state names (in_progress, banner_done, finished) no
longer resolve through the name table (pre-production - breaking
changes allowed); the name-table unit test pins the rejection.
The old firewall policy state keys are added to the rename hint
table (like the TLS migration), so a config using them fails
loudly with the replacement named instead of degrading to a bare
warning. The non-prefilter engine-name registration now guards
the null id-to-name result (a hook-only rule at the completion
progress previously formatted a null name into the engine
name).
Add unit tests for the state name table (new names, rejected legacy
names, the unhookable completion state, id-to-name) and for the
per-direction session state.
The eve log condition compares the per-direction states with
>= rather than ==: a direction at session has necessarily passed
kex, and a single delivery can take a direction
banner -> kex -> session, so a final {session, kex}
pair must still admit the log (done is never assigned, so >=
changes only the kex branch).
Ticket: #9046.
1 month ago
|
|
|
FAIL_IF(SCSshTxGetFlags(tx, STREAM_TOCLIENT) != SshStateSession);
|
|
|
|
|
|
|
|
|
|
FAIL_IF(SSHParserTestUtilCheck("2.0", NULL, tx, STREAM_TOCLIENT));
|
|
|
|
|
|
|
|
|
|
UTHFreePacket(p);
|
|
|
|
|
UTHFreeFlow(f);
|
|
|
|
|
StreamTcpUTClearSession(&ssn);
|
|
|
|
|
StreamTcpUTDeinit(ra_ctx);
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test Fragmented banner handling: chunk has final part of bannel plus
|
|
|
|
|
* a record. */
|
|
|
|
|
static int SSHParserTest21(void)
|
|
|
|
|
{
|
|
|
|
|
TcpReassemblyThreadCtx *ra_ctx = NULL;
|
|
|
|
|
ThreadVars tv;
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
Flow *f = NULL;
|
|
|
|
|
Packet *p = NULL;
|
|
|
|
|
|
|
|
|
|
uint8_t sshbuf1[] = "SSH-";
|
|
|
|
|
uint8_t sshbuf2[] = "2.0-";
|
|
|
|
|
uint8_t sshbuf3[] = "abcdefghijklmnopqrstuvwxyz"
|
|
|
|
|
"abcdefghijklmnopqrstuvwxyz"//60
|
|
|
|
|
"abcdefghijklmnopqrstuvwxyz"
|
|
|
|
|
"abcdefghijklmnopqrstuvwxyz"//112
|
|
|
|
|
"abcdefghijklmnopqrstuvwxyz"
|
|
|
|
|
"abcdefghijklmnopqrstuvwxyz"//164
|
|
|
|
|
"abcdefghijklmnopqrstuvwxyz"
|
|
|
|
|
"abcdefghijklmnopqrstuvwxyz"//216
|
|
|
|
|
"abcdefghijklmnopqrstuvwxy";//241
|
|
|
|
|
uint8_t sshbuf4[] = {'l','i','b','s','s','h', '\r',
|
|
|
|
|
0x00, 0x00, 0x00, 0x06, 0x01, 21, 0x00, 0x00, 0x00};
|
|
|
|
|
|
|
|
|
|
uint8_t* sshbufs[4] = {sshbuf1, sshbuf2, sshbuf3, sshbuf4};
|
|
|
|
|
uint32_t sshlens[4] = {sizeof(sshbuf1) - 1, sizeof(sshbuf2) - 1, sizeof(sshbuf3) - 1, sizeof(sshbuf4)};
|
|
|
|
|
|
|
|
|
|
memset(&tv, 0x00, sizeof(tv));
|
|
|
|
|
|
|
|
|
|
StreamTcpUTInit(&ra_ctx);
|
|
|
|
|
StreamTcpUTInitInline();
|
|
|
|
|
StreamTcpUTSetupSession(&ssn);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.server, 1);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.client, 1);
|
|
|
|
|
|
|
|
|
|
f = UTHBuildFlow(AF_INET, "1.1.1.1", "2.2.2.2", 1234, 2222);
|
|
|
|
|
FAIL_IF_NULL(f);
|
|
|
|
|
f->protoctx = &ssn;
|
|
|
|
|
f->proto = IPPROTO_TCP;
|
|
|
|
|
f->alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
p = PacketGetFromAlloc();
|
|
|
|
|
FAIL_IF(unlikely(p == NULL));
|
|
|
|
|
p->proto = IPPROTO_TCP;
|
|
|
|
|
p->flow = f;
|
|
|
|
|
|
|
|
|
|
uint32_t seq = 2;
|
|
|
|
|
for (int i=0; i<4; i++) {
|
|
|
|
|
FAIL_IF(StreamTcpUTAddSegmentWithPayload(&tv, ra_ctx, &ssn.server, seq, sshbufs[i], sshlens[i]) == -1);
|
|
|
|
|
seq += sshlens[i];
|
|
|
|
|
FAIL_IF(StreamTcpReassembleAppLayer(&tv, ra_ctx, &ssn, &ssn.server, p, UPDATE_DIR_PACKET) < 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void *ssh_state = f->alstate;
|
|
|
|
|
FAIL_IF_NULL(ssh_state);
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
ssh: rebuild the state machine as per-direction states
The old ssh state scale was coupled across the two directions: the
terminal finished state was only reached after both sides sent NewKeys,
and the progress accessor clamped every per-direction phase to a
shared value. A rule hooking the old top state therefore meant "the
flow completed", not "this direction completed the handshake", and
there was no room above the terminal state for a true completion
marker.
Replace the scale with per-direction phases:
banner(0) -> banner_wait_eol(1) -> kex(2) -> session(3)
NewKeys now sets session on the packet's own direction only, banner
completion sets kex, and the progress accessor is identity: each
direction reports its own raw phase. Add the registered completion
state done(4), never assigned during stream parsing: it stays out
of both name tables, so a rule referencing it fails to load, the
hook listing omits it and the firewall policy loader does not
consult it; the progress accessor reports it only on flows
disrupted by stream depth or async reassembly.
The legacy state names (in_progress, banner_done, finished) no
longer resolve through the name table (pre-production - breaking
changes allowed); the name-table unit test pins the rejection.
The old firewall policy state keys are added to the rename hint
table (like the TLS migration), so a config using them fails
loudly with the replacement named instead of degrading to a bare
warning. The non-prefilter engine-name registration now guards
the null id-to-name result (a hook-only rule at the completion
progress previously formatted a null name into the engine
name).
Add unit tests for the state name table (new names, rejected legacy
names, the unhookable completion state, id-to-name) and for the
per-direction session state.
The eve log condition compares the per-direction states with
>= rather than ==: a direction at session has necessarily passed
kex, and a single delivery can take a direction
banner -> kex -> session, so a final {session, kex}
pair must still admit the log (done is never assigned, so >=
changes only the kex branch).
Ticket: #9046.
1 month ago
|
|
|
FAIL_IF(SCSshTxGetFlags(tx, STREAM_TOCLIENT) != SshStateSession);
|
|
|
|
|
|
|
|
|
|
FAIL_IF(SSHParserTestUtilCheck("2.0", NULL, tx, STREAM_TOCLIENT));
|
|
|
|
|
|
|
|
|
|
UTHFreePacket(p);
|
|
|
|
|
UTHFreeFlow(f);
|
|
|
|
|
StreamTcpUTClearSession(&ssn);
|
|
|
|
|
StreamTcpUTDeinit(ra_ctx);
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test Fragmented banner handling: chunk has final part of bannel plus
|
|
|
|
|
* a record. */
|
|
|
|
|
static int SSHParserTest22(void)
|
|
|
|
|
{
|
|
|
|
|
TcpReassemblyThreadCtx *ra_ctx = NULL;
|
|
|
|
|
ThreadVars tv;
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
Flow *f = NULL;
|
|
|
|
|
Packet *p = NULL;
|
|
|
|
|
|
|
|
|
|
uint8_t sshbuf1[] = "SSH-";
|
|
|
|
|
uint8_t sshbuf2[] = "2.0-";
|
|
|
|
|
uint8_t sshbuf3[] = {
|
|
|
|
|
'l', 'i', 'b', 's', 's', 'h', '\r', // 7
|
|
|
|
|
|
|
|
|
|
0x00, 0x00, 0x00, 0x06, 0x01, 17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x01, 17,
|
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x01, 17, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
|
0x00, 0x00, 0x06, 0x01, 17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x01, 17, 0x00,
|
|
|
|
|
0x00, 0x00, 0x00, // 50
|
|
|
|
|
|
|
|
|
|
0x00, 0x00, 0x00, 0x06, 0x01, 17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x01, 17,
|
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x01, 17, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
|
0x00, 0x00, 0x06, 0x01, 17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x01, 17, 0x00,
|
|
|
|
|
0x00, 0x00, 0x00, // 100
|
|
|
|
|
|
|
|
|
|
0x00, 0x00, 0x00, 0x06, 0x01, 17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x01, 17,
|
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x01, 17, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
|
0x00, 0x00, 0x06, 0x01, 17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x01, 17, 0x00,
|
|
|
|
|
0x00, 0x00, 0x00, // 150
|
|
|
|
|
|
|
|
|
|
0x00, 0x00, 0x00, 0x06, 0x01, 17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x01, 17,
|
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x01, 17, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
|
0x00, 0x00, 0x06, 0x01, 17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x01, 17, 0x00,
|
|
|
|
|
0x00, 0x00, 0x00, // 200
|
|
|
|
|
|
|
|
|
|
0x00, 0x00, 0x00, 0x06, 0x01, 17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x01, 17,
|
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x01, 17, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
|
0x00, 0x00, 0x06, 0x01, 17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x01, 17, 0x00,
|
|
|
|
|
0x00, 0x00, 0x00, // 250
|
|
|
|
|
|
|
|
|
|
0x00, 0x00, 0x00, 0x06, 0x01, 17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x01, 17,
|
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x01, 17, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
|
0x00, 0x00, 0x06, 0x01, 17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x01, 21, 0x00,
|
|
|
|
|
0x00, 0x00, 0x00, // 300
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
uint8_t *sshbufs[3] = { sshbuf1, sshbuf2, sshbuf3 };
|
|
|
|
|
uint32_t sshlens[3] = { sizeof(sshbuf1) - 1, sizeof(sshbuf2) - 1, sizeof(sshbuf3) - 1 };
|
|
|
|
|
|
|
|
|
|
memset(&tv, 0x00, sizeof(tv));
|
|
|
|
|
|
|
|
|
|
StreamTcpUTInit(&ra_ctx);
|
|
|
|
|
StreamTcpUTInitInline();
|
|
|
|
|
StreamTcpUTSetupSession(&ssn);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.server, 1);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.client, 1);
|
|
|
|
|
|
|
|
|
|
f = UTHBuildFlow(AF_INET, "1.1.1.1", "2.2.2.2", 1234, 2222);
|
|
|
|
|
FAIL_IF_NULL(f);
|
|
|
|
|
f->protoctx = &ssn;
|
|
|
|
|
f->proto = IPPROTO_TCP;
|
|
|
|
|
f->alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
p = PacketGetFromAlloc();
|
|
|
|
|
FAIL_IF(unlikely(p == NULL));
|
|
|
|
|
p->proto = IPPROTO_TCP;
|
|
|
|
|
p->flow = f;
|
|
|
|
|
|
|
|
|
|
uint32_t seq = 2;
|
|
|
|
|
for (int i = 0; i < 3; i++) {
|
|
|
|
|
FAIL_IF(StreamTcpUTAddSegmentWithPayload(
|
|
|
|
|
&tv, ra_ctx, &ssn.server, seq, sshbufs[i], sshlens[i]) == -1);
|
|
|
|
|
seq += sshlens[i];
|
|
|
|
|
FAIL_IF(StreamTcpReassembleAppLayer(&tv, ra_ctx, &ssn, &ssn.server, p, UPDATE_DIR_PACKET) <
|
|
|
|
|
0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void *ssh_state = f->alstate;
|
|
|
|
|
FAIL_IF_NULL(ssh_state);
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
ssh: rebuild the state machine as per-direction states
The old ssh state scale was coupled across the two directions: the
terminal finished state was only reached after both sides sent NewKeys,
and the progress accessor clamped every per-direction phase to a
shared value. A rule hooking the old top state therefore meant "the
flow completed", not "this direction completed the handshake", and
there was no room above the terminal state for a true completion
marker.
Replace the scale with per-direction phases:
banner(0) -> banner_wait_eol(1) -> kex(2) -> session(3)
NewKeys now sets session on the packet's own direction only, banner
completion sets kex, and the progress accessor is identity: each
direction reports its own raw phase. Add the registered completion
state done(4), never assigned during stream parsing: it stays out
of both name tables, so a rule referencing it fails to load, the
hook listing omits it and the firewall policy loader does not
consult it; the progress accessor reports it only on flows
disrupted by stream depth or async reassembly.
The legacy state names (in_progress, banner_done, finished) no
longer resolve through the name table (pre-production - breaking
changes allowed); the name-table unit test pins the rejection.
The old firewall policy state keys are added to the rename hint
table (like the TLS migration), so a config using them fails
loudly with the replacement named instead of degrading to a bare
warning. The non-prefilter engine-name registration now guards
the null id-to-name result (a hook-only rule at the completion
progress previously formatted a null name into the engine
name).
Add unit tests for the state name table (new names, rejected legacy
names, the unhookable completion state, id-to-name) and for the
per-direction session state.
The eve log condition compares the per-direction states with
>= rather than ==: a direction at session has necessarily passed
kex, and a single delivery can take a direction
banner -> kex -> session, so a final {session, kex}
pair must still admit the log (done is never assigned, so >=
changes only the kex branch).
Ticket: #9046.
1 month ago
|
|
|
FAIL_IF(SCSshTxGetFlags(tx, STREAM_TOCLIENT) != SshStateSession);
|
|
|
|
|
|
|
|
|
|
FAIL_IF(SSHParserTestUtilCheck("2.0", "libssh", tx, STREAM_TOCLIENT));
|
|
|
|
|
|
|
|
|
|
UTHFreePacket(p);
|
|
|
|
|
UTHFreeFlow(f);
|
|
|
|
|
StreamTcpUTClearSession(&ssn);
|
|
|
|
|
StreamTcpUTDeinit(ra_ctx);
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test Send a version string in one chunk (client version str). */
|
|
|
|
|
static int SSHParserTest23(void)
|
|
|
|
|
{
|
|
|
|
|
int result = 0;
|
|
|
|
|
Flow f;
|
|
|
|
|
uint8_t sshbuf[] = "SSH-2.0\r-MySSHClient-0.5.1\n";
|
|
|
|
|
uint32_t sshlen = sizeof(sshbuf) - 1;
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
AppLayerParserThreadCtx *alp_tctx = AppLayerParserThreadCtxAlloc();
|
|
|
|
|
|
|
|
|
|
memset(&f, 0, sizeof(f));
|
|
|
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
|
FLOW_INITIALIZE(&f);
|
|
|
|
|
f.protoctx = (void *)&ssn;
|
|
|
|
|
f.proto = IPPROTO_TCP;
|
|
|
|
|
f.alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
StreamTcpInitConfig(true);
|
|
|
|
|
|
|
|
|
|
int r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH,
|
|
|
|
|
STREAM_TOSERVER | STREAM_EOF, sshbuf, sshlen);
|
|
|
|
|
if (r == 0) {
|
|
|
|
|
printf("toclient chunk 1 returned 0 expected non null: ");
|
|
|
|
|
goto end;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result = 1;
|
|
|
|
|
end:
|
|
|
|
|
if (alp_tctx != NULL)
|
|
|
|
|
AppLayerParserThreadCtxFree(alp_tctx);
|
|
|
|
|
StreamTcpFreeConfig(true);
|
|
|
|
|
FLOW_DESTROY(&f);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test Send a version string in one chunk (client version str). */
|
|
|
|
|
static int SSHParserTest24(void)
|
|
|
|
|
{
|
|
|
|
|
int result = 0;
|
|
|
|
|
Flow f;
|
|
|
|
|
uint8_t sshbuf[] = "SSH-2.0-\rMySSHClient-0.5.1\n";
|
|
|
|
|
uint32_t sshlen = sizeof(sshbuf) - 1;
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
AppLayerParserThreadCtx *alp_tctx = AppLayerParserThreadCtxAlloc();
|
|
|
|
|
|
|
|
|
|
memset(&f, 0, sizeof(f));
|
|
|
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
|
FLOW_INITIALIZE(&f);
|
|
|
|
|
f.protoctx = (void *)&ssn;
|
|
|
|
|
f.proto = IPPROTO_TCP;
|
|
|
|
|
f.alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
StreamTcpInitConfig(true);
|
|
|
|
|
|
|
|
|
|
int r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH,
|
|
|
|
|
STREAM_TOSERVER | STREAM_EOF, sshbuf, sshlen);
|
|
|
|
|
if (r != 0) {
|
|
|
|
|
printf("toclient chunk 1 returned %" PRId32 ", expected 0: ", r);
|
|
|
|
|
goto end;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void *ssh_state = f.alstate;
|
|
|
|
|
if (ssh_state == NULL) {
|
|
|
|
|
printf("no ssh state: ");
|
|
|
|
|
goto end;
|
|
|
|
|
}
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
ssh: rebuild the state machine as per-direction states
The old ssh state scale was coupled across the two directions: the
terminal finished state was only reached after both sides sent NewKeys,
and the progress accessor clamped every per-direction phase to a
shared value. A rule hooking the old top state therefore meant "the
flow completed", not "this direction completed the handshake", and
there was no room above the terminal state for a true completion
marker.
Replace the scale with per-direction phases:
banner(0) -> banner_wait_eol(1) -> kex(2) -> session(3)
NewKeys now sets session on the packet's own direction only, banner
completion sets kex, and the progress accessor is identity: each
direction reports its own raw phase. Add the registered completion
state done(4), never assigned during stream parsing: it stays out
of both name tables, so a rule referencing it fails to load, the
hook listing omits it and the firewall policy loader does not
consult it; the progress accessor reports it only on flows
disrupted by stream depth or async reassembly.
The legacy state names (in_progress, banner_done, finished) no
longer resolve through the name table (pre-production - breaking
changes allowed); the name-table unit test pins the rejection.
The old firewall policy state keys are added to the rename hint
table (like the TLS migration), so a config using them fails
loudly with the replacement named instead of degrading to a bare
warning. The non-prefilter engine-name registration now guards
the null id-to-name result (a hook-only rule at the completion
progress previously formatted a null name into the engine
name).
Add unit tests for the state name table (new names, rejected legacy
names, the unhookable completion state, id-to-name) and for the
per-direction session state.
The eve log condition compares the per-direction states with
>= rather than ==: a direction at session has necessarily passed
kex, and a single delivery can take a direction
banner -> kex -> session, so a final {session, kex}
pair must still admit the log (done is never assigned, so >=
changes only the kex branch).
Ticket: #9046.
1 month ago
|
|
|
if (SCSshTxGetFlags(tx, STREAM_TOSERVER) != SshStateKex) {
|
|
|
|
|
printf("Didn't detect the msg code of new keys (ciphered data starts): ");
|
|
|
|
|
goto end;
|
|
|
|
|
}
|
|
|
|
|
if (SSHParserTestUtilCheck("2.0", NULL, tx, STREAM_TOSERVER))
|
|
|
|
|
goto end;
|
|
|
|
|
|
|
|
|
|
result = 1;
|
|
|
|
|
end:
|
|
|
|
|
FLOW_DESTROY(&f);
|
|
|
|
|
if (alp_tctx != NULL)
|
|
|
|
|
AppLayerParserThreadCtxFree(alp_tctx);
|
|
|
|
|
StreamTcpFreeConfig(true);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test Send a malformed banner */
|
|
|
|
|
static int SSHParserTest25(void)
|
|
|
|
|
{
|
|
|
|
|
Flow f;
|
|
|
|
|
uint8_t sshbuf[] = "\n";
|
|
|
|
|
uint32_t sshlen = sizeof(sshbuf) - 1;
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
AppLayerParserThreadCtx *alp_tctx = AppLayerParserThreadCtxAlloc();
|
|
|
|
|
FAIL_IF_NULL(alp_tctx);
|
|
|
|
|
|
|
|
|
|
memset(&f, 0, sizeof(f));
|
|
|
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
|
FLOW_INITIALIZE(&f);
|
|
|
|
|
f.protoctx = (void *)&ssn;
|
|
|
|
|
f.proto = IPPROTO_TCP;
|
|
|
|
|
f.alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
StreamTcpInitConfig(true);
|
|
|
|
|
|
|
|
|
|
int r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH,
|
|
|
|
|
STREAM_TOSERVER | STREAM_EOF, sshbuf, sshlen);
|
|
|
|
|
FAIL_IF(r != -1);
|
|
|
|
|
|
|
|
|
|
void *ssh_state = f.alstate;
|
|
|
|
|
FAIL_IF_NULL(ssh_state);
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
ssh: rebuild the state machine as per-direction states
The old ssh state scale was coupled across the two directions: the
terminal finished state was only reached after both sides sent NewKeys,
and the progress accessor clamped every per-direction phase to a
shared value. A rule hooking the old top state therefore meant "the
flow completed", not "this direction completed the handshake", and
there was no room above the terminal state for a true completion
marker.
Replace the scale with per-direction phases:
banner(0) -> banner_wait_eol(1) -> kex(2) -> session(3)
NewKeys now sets session on the packet's own direction only, banner
completion sets kex, and the progress accessor is identity: each
direction reports its own raw phase. Add the registered completion
state done(4), never assigned during stream parsing: it stays out
of both name tables, so a rule referencing it fails to load, the
hook listing omits it and the firewall policy loader does not
consult it; the progress accessor reports it only on flows
disrupted by stream depth or async reassembly.
The legacy state names (in_progress, banner_done, finished) no
longer resolve through the name table (pre-production - breaking
changes allowed); the name-table unit test pins the rejection.
The old firewall policy state keys are added to the rename hint
table (like the TLS migration), so a config using them fails
loudly with the replacement named instead of degrading to a bare
warning. The non-prefilter engine-name registration now guards
the null id-to-name result (a hook-only rule at the completion
progress previously formatted a null name into the engine
name).
Add unit tests for the state name table (new names, rejected legacy
names, the unhookable completion state, id-to-name) and for the
per-direction session state.
The eve log condition compares the per-direction states with
>= rather than ==: a direction at session has necessarily passed
kex, and a single delivery can take a direction
banner -> kex -> session, so a final {session, kex}
pair must still admit the log (done is never assigned, so >=
changes only the kex branch).
Ticket: #9046.
1 month ago
|
|
|
FAIL_IF(SCSshTxGetFlags(tx, STREAM_TOSERVER) == SshStateKex);
|
|
|
|
|
const uint8_t *dummy = NULL;
|
|
|
|
|
uint32_t dummy_len = 0;
|
|
|
|
|
FAIL_IF(SCSshTxGetSoftware(tx, STREAM_TOCLIENT, &dummy, &dummy_len) != 0);
|
|
|
|
|
|
|
|
|
|
FLOW_DESTROY(&f);
|
|
|
|
|
AppLayerParserThreadCtxFree(alp_tctx);
|
|
|
|
|
StreamTcpFreeConfig(true);
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test State name table: the four phase names, the rejected legacy
|
|
|
|
|
* names (no backward-compat aliases), the unhookable completion
|
|
|
|
|
* state, unknown names and the id-to-name mapping. */
|
ssh: rebuild the state machine as per-direction states
The old ssh state scale was coupled across the two directions: the
terminal finished state was only reached after both sides sent NewKeys,
and the progress accessor clamped every per-direction phase to a
shared value. A rule hooking the old top state therefore meant "the
flow completed", not "this direction completed the handshake", and
there was no room above the terminal state for a true completion
marker.
Replace the scale with per-direction phases:
banner(0) -> banner_wait_eol(1) -> kex(2) -> session(3)
NewKeys now sets session on the packet's own direction only, banner
completion sets kex, and the progress accessor is identity: each
direction reports its own raw phase. Add the registered completion
state done(4), never assigned during stream parsing: it stays out
of both name tables, so a rule referencing it fails to load, the
hook listing omits it and the firewall policy loader does not
consult it; the progress accessor reports it only on flows
disrupted by stream depth or async reassembly.
The legacy state names (in_progress, banner_done, finished) no
longer resolve through the name table (pre-production - breaking
changes allowed); the name-table unit test pins the rejection.
The old firewall policy state keys are added to the rename hint
table (like the TLS migration), so a config using them fails
loudly with the replacement named instead of degrading to a bare
warning. The non-prefilter engine-name registration now guards
the null id-to-name result (a hook-only rule at the completion
progress previously formatted a null name into the engine
name).
Add unit tests for the state name table (new names, rejected legacy
names, the unhookable completion state, id-to-name) and for the
per-direction session state.
The eve log condition compares the per-direction states with
>= rather than ==: a direction at session has necessarily passed
kex, and a single delivery can take a direction
banner -> kex -> session, so a final {session, kex}
pair must still admit the log (done is never assigned, so >=
changes only the kex branch).
Ticket: #9046.
1 month ago
|
|
|
static int SSHParserTest26(void)
|
|
|
|
|
{
|
|
|
|
|
/* new names */
|
|
|
|
|
FAIL_IF(AppLayerParserGetStateIdByName(
|
|
|
|
|
IPPROTO_TCP, ALPROTO_SSH, "request_banner", STREAM_TOSERVER) != SshStateBanner);
|
|
|
|
|
FAIL_IF(AppLayerParserGetStateIdByName(
|
|
|
|
|
IPPROTO_TCP, ALPROTO_SSH, "request_kex", STREAM_TOSERVER) != SshStateKex);
|
|
|
|
|
FAIL_IF(AppLayerParserGetStateIdByName(IPPROTO_TCP, ALPROTO_SSH, "request_session",
|
|
|
|
|
STREAM_TOSERVER) != SshStateSession);
|
|
|
|
|
FAIL_IF(AppLayerParserGetStateIdByName(IPPROTO_TCP, ALPROTO_SSH, "response_banner",
|
|
|
|
|
STREAM_TOCLIENT) != SshStateBanner);
|
|
|
|
|
FAIL_IF(AppLayerParserGetStateIdByName(IPPROTO_TCP, ALPROTO_SSH, "response_session",
|
|
|
|
|
STREAM_TOCLIENT) != SshStateSession);
|
|
|
|
|
|
|
|
|
|
/* legacy names are rejected: no backward-compat aliases
|
|
|
|
|
* (pre-production — breaking changes are allowed) */
|
|
|
|
|
FAIL_IF(AppLayerParserGetStateIdByName(
|
|
|
|
|
IPPROTO_TCP, ALPROTO_SSH, "request_in_progress", STREAM_TOSERVER) != -1);
|
|
|
|
|
FAIL_IF(AppLayerParserGetStateIdByName(
|
|
|
|
|
IPPROTO_TCP, ALPROTO_SSH, "request_banner_done", STREAM_TOSERVER) != -1);
|
|
|
|
|
FAIL_IF(AppLayerParserGetStateIdByName(
|
|
|
|
|
IPPROTO_TCP, ALPROTO_SSH, "request_finished", STREAM_TOSERVER) != -1);
|
|
|
|
|
FAIL_IF(AppLayerParserGetStateIdByName(
|
|
|
|
|
IPPROTO_TCP, ALPROTO_SSH, "response_finished", STREAM_TOCLIENT) != -1);
|
|
|
|
|
|
|
|
|
|
/* the completion state is registered but not hookable */
|
|
|
|
|
FAIL_IF(AppLayerParserGetStateIdByName(
|
|
|
|
|
IPPROTO_TCP, ALPROTO_SSH, "request_done", STREAM_TOSERVER) != -1);
|
|
|
|
|
FAIL_IF(AppLayerParserGetStateIdByName(
|
|
|
|
|
IPPROTO_TCP, ALPROTO_SSH, "response_done", STREAM_TOCLIENT) != -1);
|
|
|
|
|
|
|
|
|
|
/* unknown names (including banner_wait_eol) and wrong direction
|
|
|
|
|
* prefix */
|
ssh: rebuild the state machine as per-direction states
The old ssh state scale was coupled across the two directions: the
terminal finished state was only reached after both sides sent NewKeys,
and the progress accessor clamped every per-direction phase to a
shared value. A rule hooking the old top state therefore meant "the
flow completed", not "this direction completed the handshake", and
there was no room above the terminal state for a true completion
marker.
Replace the scale with per-direction phases:
banner(0) -> banner_wait_eol(1) -> kex(2) -> session(3)
NewKeys now sets session on the packet's own direction only, banner
completion sets kex, and the progress accessor is identity: each
direction reports its own raw phase. Add the registered completion
state done(4), never assigned during stream parsing: it stays out
of both name tables, so a rule referencing it fails to load, the
hook listing omits it and the firewall policy loader does not
consult it; the progress accessor reports it only on flows
disrupted by stream depth or async reassembly.
The legacy state names (in_progress, banner_done, finished) no
longer resolve through the name table (pre-production - breaking
changes allowed); the name-table unit test pins the rejection.
The old firewall policy state keys are added to the rename hint
table (like the TLS migration), so a config using them fails
loudly with the replacement named instead of degrading to a bare
warning. The non-prefilter engine-name registration now guards
the null id-to-name result (a hook-only rule at the completion
progress previously formatted a null name into the engine
name).
Add unit tests for the state name table (new names, rejected legacy
names, the unhookable completion state, id-to-name) and for the
per-direction session state.
The eve log condition compares the per-direction states with
>= rather than ==: a direction at session has necessarily passed
kex, and a single delivery can take a direction
banner -> kex -> session, so a final {session, kex}
pair must still admit the log (done is never assigned, so >=
changes only the kex branch).
Ticket: #9046.
1 month ago
|
|
|
FAIL_IF(AppLayerParserGetStateIdByName(
|
|
|
|
|
IPPROTO_TCP, ALPROTO_SSH, "request_nosuchstate", STREAM_TOSERVER) != -1);
|
|
|
|
|
FAIL_IF(AppLayerParserGetStateIdByName(
|
|
|
|
|
IPPROTO_TCP, ALPROTO_SSH, "request_banner_wait_eol", STREAM_TOSERVER) != -1);
|
ssh: rebuild the state machine as per-direction states
The old ssh state scale was coupled across the two directions: the
terminal finished state was only reached after both sides sent NewKeys,
and the progress accessor clamped every per-direction phase to a
shared value. A rule hooking the old top state therefore meant "the
flow completed", not "this direction completed the handshake", and
there was no room above the terminal state for a true completion
marker.
Replace the scale with per-direction phases:
banner(0) -> banner_wait_eol(1) -> kex(2) -> session(3)
NewKeys now sets session on the packet's own direction only, banner
completion sets kex, and the progress accessor is identity: each
direction reports its own raw phase. Add the registered completion
state done(4), never assigned during stream parsing: it stays out
of both name tables, so a rule referencing it fails to load, the
hook listing omits it and the firewall policy loader does not
consult it; the progress accessor reports it only on flows
disrupted by stream depth or async reassembly.
The legacy state names (in_progress, banner_done, finished) no
longer resolve through the name table (pre-production - breaking
changes allowed); the name-table unit test pins the rejection.
The old firewall policy state keys are added to the rename hint
table (like the TLS migration), so a config using them fails
loudly with the replacement named instead of degrading to a bare
warning. The non-prefilter engine-name registration now guards
the null id-to-name result (a hook-only rule at the completion
progress previously formatted a null name into the engine
name).
Add unit tests for the state name table (new names, rejected legacy
names, the unhookable completion state, id-to-name) and for the
per-direction session state.
The eve log condition compares the per-direction states with
>= rather than ==: a direction at session has necessarily passed
kex, and a single delivery can take a direction
banner -> kex -> session, so a final {session, kex}
pair must still admit the log (done is never assigned, so >=
changes only the kex branch).
Ticket: #9046.
1 month ago
|
|
|
FAIL_IF(AppLayerParserGetStateIdByName(
|
|
|
|
|
IPPROTO_TCP, ALPROTO_SSH, "response_banner", STREAM_TOSERVER) != -1);
|
|
|
|
|
|
|
|
|
|
/* id to name */
|
|
|
|
|
FAIL_IF(strcmp(AppLayerParserGetStateNameById(
|
|
|
|
|
IPPROTO_TCP, ALPROTO_SSH, SshStateBanner, STREAM_TOSERVER),
|
|
|
|
|
"request_banner") != 0);
|
|
|
|
|
FAIL_IF(strcmp(AppLayerParserGetStateNameById(
|
|
|
|
|
IPPROTO_TCP, ALPROTO_SSH, SshStateKex, STREAM_TOSERVER),
|
|
|
|
|
"request_kex") != 0);
|
|
|
|
|
FAIL_IF(strcmp(AppLayerParserGetStateNameById(
|
|
|
|
|
IPPROTO_TCP, ALPROTO_SSH, SshStateSession, STREAM_TOSERVER),
|
|
|
|
|
"request_session") != 0);
|
|
|
|
|
/* the completion state is out of the id-to-name table: the hook
|
|
|
|
|
* listing and the policy key walk must not offer it */
|
|
|
|
|
FAIL_IF(AppLayerParserGetStateNameById(
|
|
|
|
|
IPPROTO_TCP, ALPROTO_SSH, SshStateDone, STREAM_TOSERVER) != NULL);
|
|
|
|
|
FAIL_IF(AppLayerParserGetStateNameById(
|
|
|
|
|
IPPROTO_TCP, ALPROTO_SSH, SshStateDone, STREAM_TOCLIENT) != NULL);
|
|
|
|
|
FAIL_IF(AppLayerParserGetStateNameById(IPPROTO_TCP, ALPROTO_SSH, 9, STREAM_TOSERVER) != NULL);
|
|
|
|
|
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test Per-direction session state: the toserver direction reports
|
|
|
|
|
* session after its own NewKeys while the toclient direction has not
|
|
|
|
|
* seen anything yet. */
|
|
|
|
|
static int SSHParserTest27(void)
|
|
|
|
|
{
|
|
|
|
|
Flow f;
|
|
|
|
|
uint8_t sshbuf1[] = "SSH-2.0-MySSHClient-0.5.1\r\n";
|
|
|
|
|
uint32_t sshlen1 = sizeof(sshbuf1) - 1;
|
|
|
|
|
uint8_t sshbuf2[] = { 0x00, 0x00, 0x00, 0x03, 0x01, 21, 0x00 };
|
|
|
|
|
uint32_t sshlen2 = sizeof(sshbuf2);
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
AppLayerParserThreadCtx *alp_tctx = AppLayerParserThreadCtxAlloc();
|
|
|
|
|
FAIL_IF_NULL(alp_tctx);
|
|
|
|
|
|
|
|
|
|
memset(&f, 0, sizeof(f));
|
|
|
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
|
FLOW_INITIALIZE(&f);
|
|
|
|
|
f.protoctx = (void *)&ssn;
|
|
|
|
|
f.proto = IPPROTO_TCP;
|
|
|
|
|
f.alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
StreamTcpInitConfig(true);
|
|
|
|
|
|
|
|
|
|
int r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOSERVER, sshbuf1, sshlen1);
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOSERVER, sshbuf2, sshlen2);
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
|
|
|
|
|
void *ssh_state = f.alstate;
|
|
|
|
|
FAIL_IF_NULL(ssh_state);
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
|
|
|
|
FAIL_IF(SCSshTxGetAlStateProgress(tx, STREAM_TOSERVER) != SshStateSession);
|
|
|
|
|
FAIL_IF(SCSshTxGetAlStateProgress(tx, STREAM_TOCLIENT) != SshStateBanner);
|
|
|
|
|
|
|
|
|
|
FLOW_DESTROY(&f);
|
|
|
|
|
AppLayerParserThreadCtxFree(alp_tctx);
|
|
|
|
|
StreamTcpFreeConfig(true);
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test Unrecoverable parse failure freezes the failing direction
|
|
|
|
|
* in the state it failed in: an invalid banner leaves it at
|
|
|
|
|
* banner, an invalid record at kex. The other direction is
|
|
|
|
|
* unaffected, and post-failure data does not advance the failed
|
|
|
|
|
* direction (the parse entry short-circuits on the error flag).
|
|
|
|
|
* The parse call reports -1. */
|
|
|
|
|
static int SSHParserTest28(void)
|
|
|
|
|
{
|
|
|
|
|
Flow f;
|
|
|
|
|
uint8_t badbanner[] = "SSH-bogus\r\n";
|
|
|
|
|
uint32_t badbannerlen = sizeof(badbanner) - 1;
|
|
|
|
|
uint8_t banner[] = "SSH-2.0-TestClient-1.0\r\n";
|
|
|
|
|
uint32_t bannerlen = sizeof(banner) - 1;
|
|
|
|
|
uint8_t badrecord[] = { 0x00, 0x00, 0x00, 0x00, 0x08, 0x21, 0x00, 0x00 };
|
|
|
|
|
uint32_t badrecordlen = sizeof(badrecord);
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
AppLayerParserThreadCtx *alp_tctx = AppLayerParserThreadCtxAlloc();
|
|
|
|
|
FAIL_IF_NULL(alp_tctx);
|
|
|
|
|
|
|
|
|
|
/* invalid banner */
|
|
|
|
|
memset(&f, 0, sizeof(f));
|
|
|
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
|
FLOW_INITIALIZE(&f);
|
|
|
|
|
f.protoctx = (void *)&ssn;
|
|
|
|
|
f.proto = IPPROTO_TCP;
|
|
|
|
|
f.alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
StreamTcpInitConfig(true);
|
|
|
|
|
|
|
|
|
|
int r = AppLayerParserParse(
|
|
|
|
|
NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOSERVER, badbanner, badbannerlen);
|
|
|
|
|
FAIL_IF(r != -1);
|
|
|
|
|
void *ssh_state = f.alstate;
|
|
|
|
|
FAIL_IF_NULL(ssh_state);
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
|
|
|
|
FAIL_IF(SCSshTxGetAlStateProgress(tx, STREAM_TOSERVER) != SshStateBanner);
|
|
|
|
|
FAIL_IF(SCSshTxGetAlStateProgress(tx, STREAM_TOCLIENT) != SshStateBanner);
|
|
|
|
|
|
|
|
|
|
/* the failure event is published data: the tx is marked updated */
|
|
|
|
|
struct AppLayerTxData *txdata = AppLayerParserGetTxData(IPPROTO_TCP, ALPROTO_SSH, tx);
|
|
|
|
|
FAIL_IF_NULL(txdata);
|
|
|
|
|
FAIL_IF(!txdata->updated_ts);
|
|
|
|
|
|
|
|
|
|
/* the failed direction is frozen: a valid banner does not advance it */
|
|
|
|
|
r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOSERVER, banner, bannerlen);
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
tx = SCSshStateGetTx(ssh_state, 0);
|
|
|
|
|
FAIL_IF(SCSshTxGetAlStateProgress(tx, STREAM_TOSERVER) != SshStateBanner);
|
|
|
|
|
FAIL_IF(SCSshTxGetAlStateProgress(tx, STREAM_TOCLIENT) != SshStateBanner);
|
|
|
|
|
|
|
|
|
|
FLOW_DESTROY(&f);
|
|
|
|
|
|
|
|
|
|
/* valid banner then invalid record (pkt_len=0) */
|
|
|
|
|
memset(&f, 0, sizeof(f));
|
|
|
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
|
FLOW_INITIALIZE(&f);
|
|
|
|
|
f.protoctx = (void *)&ssn;
|
|
|
|
|
f.proto = IPPROTO_TCP;
|
|
|
|
|
f.alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOSERVER, banner, bannerlen);
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
r = AppLayerParserParse(
|
|
|
|
|
NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOSERVER, badrecord, badrecordlen);
|
|
|
|
|
FAIL_IF(r != -1);
|
|
|
|
|
ssh_state = f.alstate;
|
|
|
|
|
FAIL_IF_NULL(ssh_state);
|
|
|
|
|
tx = SCSshStateGetTx(ssh_state, 0);
|
|
|
|
|
FAIL_IF(SCSshTxGetAlStateProgress(tx, STREAM_TOSERVER) != SshStateKex);
|
|
|
|
|
|
|
|
|
|
/* the failure event is published data: the tx is marked updated */
|
|
|
|
|
txdata = AppLayerParserGetTxData(IPPROTO_TCP, ALPROTO_SSH, tx);
|
|
|
|
|
FAIL_IF_NULL(txdata);
|
|
|
|
|
FAIL_IF(!txdata->updated_ts);
|
|
|
|
|
|
|
|
|
|
/* the failed direction is frozen: a valid newkeys record does not
|
|
|
|
|
* advance it to session */
|
|
|
|
|
uint8_t newkeys[] = { 0x00, 0x00, 0x00, 0x03, 0x01, 21, 0x00 };
|
|
|
|
|
r = AppLayerParserParse(
|
|
|
|
|
NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOSERVER, newkeys, sizeof(newkeys));
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
tx = SCSshStateGetTx(ssh_state, 0);
|
|
|
|
|
FAIL_IF(SCSshTxGetAlStateProgress(tx, STREAM_TOSERVER) != SshStateKex);
|
|
|
|
|
|
|
|
|
|
FLOW_DESTROY(&f);
|
|
|
|
|
AppLayerParserThreadCtxFree(alp_tctx);
|
|
|
|
|
StreamTcpFreeConfig(true);
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test A record reassembly stash and a header fragment mark the tx
|
|
|
|
|
* \test updated at parse entry, like a record completion: the flag
|
|
|
|
|
* \test is set at the parse entry, as the base parser did, so the
|
|
|
|
|
* \test deliveries are swept by the firewall default policy. A
|
|
|
|
|
* \test pkt_len=29 record (27 payload bytes after the 6B header,
|
|
|
|
|
* \test msg 50) is fed in 3 calls: 16B (header + 10), 4B (pure
|
|
|
|
|
* \test stash), 13B (completion); then a 3B header fragment.
|
|
|
|
|
*/
|
|
|
|
|
static int SSHParserTest29(void)
|
|
|
|
|
{
|
|
|
|
|
Flow f;
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
AppLayerParserThreadCtx *alp_tctx = NULL;
|
|
|
|
|
|
|
|
|
|
uint8_t banner[] = "SSH-2.0-Client-1.0\r\n";
|
|
|
|
|
uint8_t seg2[] = { 0x00, 0x00, 0x00, 29, 0x00, 50, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67,
|
|
|
|
|
0x68, 0x69, 0x6a };
|
|
|
|
|
uint8_t seg3[4] = { 0x6b, 0x6c, 0x6d, 0x6e };
|
|
|
|
|
uint8_t seg4[13] = { 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a,
|
|
|
|
|
0x7b };
|
|
|
|
|
uint8_t seg5[3] = { 0x7c, 0x7d, 0x7e };
|
|
|
|
|
|
|
|
|
|
void *tx = NULL;
|
|
|
|
|
struct AppLayerTxData *txdata = NULL;
|
|
|
|
|
int r = 0;
|
|
|
|
|
|
|
|
|
|
memset(&f, 0, sizeof(f));
|
|
|
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
|
FLOW_INITIALIZE(&f);
|
|
|
|
|
f.protoctx = (void *)&ssn;
|
|
|
|
|
f.proto = IPPROTO_TCP;
|
|
|
|
|
f.alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
StreamTcpInitConfig(true);
|
|
|
|
|
alp_tctx = AppLayerParserThreadCtxAlloc();
|
|
|
|
|
FAIL_IF_NULL(alp_tctx);
|
|
|
|
|
|
|
|
|
|
r = AppLayerParserParse(
|
|
|
|
|
NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOSERVER, banner, sizeof(banner) - 1);
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
FAIL_IF_NULL(f.alstate);
|
|
|
|
|
tx = SCSshStateGetTx(f.alstate, 0);
|
|
|
|
|
FAIL_IF(SCSshTxGetAlStateProgress(tx, STREAM_TOSERVER) != SshStateKex);
|
|
|
|
|
txdata = AppLayerParserGetTxData(IPPROTO_TCP, ALPROTO_SSH, tx);
|
|
|
|
|
FAIL_IF_NULL(txdata);
|
|
|
|
|
FAIL_IF(!txdata->updated_ts);
|
|
|
|
|
|
|
|
|
|
// record header + 10 payload bytes: frames are published
|
|
|
|
|
r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOSERVER, seg2, sizeof(seg2));
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
FAIL_IF(!txdata->updated_ts);
|
|
|
|
|
txdata->updated_ts = false;
|
|
|
|
|
|
|
|
|
|
// the 4B chunk is fully held by the reassembly stash: nothing is
|
|
|
|
|
// published, but the tx is marked updated at parse entry
|
|
|
|
|
r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOSERVER, seg3, sizeof(seg3));
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
FAIL_IF(!txdata->updated_ts);
|
|
|
|
|
|
|
|
|
|
// the final 13B chunk completes the record
|
|
|
|
|
r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOSERVER, seg4, sizeof(seg4));
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
FAIL_IF(!txdata->updated_ts);
|
|
|
|
|
txdata->updated_ts = false;
|
|
|
|
|
|
|
|
|
|
// 3B header fragment: held for the next delivery, the tx is
|
|
|
|
|
// marked updated at parse entry
|
|
|
|
|
r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOSERVER, seg5, sizeof(seg5));
|
|
|
|
|
FAIL_IF(r != 1);
|
|
|
|
|
FAIL_IF(!txdata->updated_ts);
|
|
|
|
|
FAIL_IF(SCSshTxGetAlStateProgress(tx, STREAM_TOSERVER) != SshStateKex);
|
|
|
|
|
|
|
|
|
|
FLOW_DESTROY(&f);
|
|
|
|
|
AppLayerParserThreadCtxFree(alp_tctx);
|
|
|
|
|
StreamTcpFreeConfig(true);
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test A long banner (>=256B without EOL) keeps the direction in
|
|
|
|
|
* \test the banner phase with the line consumed greedily; the line
|
|
|
|
|
* \test completion moves it to kex, and a record that then fails
|
|
|
|
|
* \test freezes the direction at kex. A >=256B line without EOL that
|
|
|
|
|
* \test does not parse as a banner freezes the direction at banner
|
|
|
|
|
* \test at once.
|
|
|
|
|
*/
|
|
|
|
|
static int SSHParserTest30(void)
|
|
|
|
|
{
|
|
|
|
|
Flow f;
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
AppLayerParserThreadCtx *alp_tctx = NULL;
|
|
|
|
|
uint8_t longbanner[300];
|
|
|
|
|
uint8_t junk[300];
|
|
|
|
|
uint8_t badrecord[] = { 0x00, 0x00, 0x00, 0x00, 0x08, 0x21, 0x00, 0x00 };
|
|
|
|
|
void *tx;
|
|
|
|
|
|
|
|
|
|
memset(&f, 0, sizeof(f));
|
|
|
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
|
FLOW_INITIALIZE(&f);
|
|
|
|
|
f.protoctx = (void *)&ssn;
|
|
|
|
|
f.proto = IPPROTO_TCP;
|
|
|
|
|
f.alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
StreamTcpInitConfig(true);
|
|
|
|
|
alp_tctx = AppLayerParserThreadCtxAlloc();
|
|
|
|
|
FAIL_IF_NULL(alp_tctx);
|
|
|
|
|
|
|
|
|
|
// phase 1: 300B banner without EOL -> the direction stays in the
|
|
|
|
|
// banner phase (the line is still open); the line completion
|
|
|
|
|
// moves it to kex; the following record is invalid -> the
|
|
|
|
|
// direction freezes at kex
|
|
|
|
|
memset(longbanner, 'A', sizeof(longbanner));
|
|
|
|
|
memcpy(longbanner, "SSH-2.0-", 8);
|
|
|
|
|
int r = AppLayerParserParse(
|
|
|
|
|
NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOSERVER, longbanner, sizeof(longbanner));
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
FAIL_IF_NULL(f.alstate);
|
|
|
|
|
tx = SCSshStateGetTx(f.alstate, 0);
|
|
|
|
|
FAIL_IF(SCSshTxGetAlStateProgress(tx, STREAM_TOSERVER) != SshStateBanner);
|
|
|
|
|
|
|
|
|
|
// line completion -> kex
|
|
|
|
|
r = AppLayerParserParse(
|
|
|
|
|
NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOSERVER, (const uint8_t *)"\r\n", 2);
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
FAIL_IF(SCSshTxGetAlStateProgress(tx, STREAM_TOSERVER) != SshStateKex);
|
|
|
|
|
|
|
|
|
|
r = AppLayerParserParse(
|
|
|
|
|
NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOSERVER, badrecord, sizeof(badrecord));
|
|
|
|
|
FAIL_IF(r != -1);
|
|
|
|
|
FAIL_IF(SCSshTxGetAlStateProgress(tx, STREAM_TOSERVER) != SshStateKex);
|
|
|
|
|
FAIL_IF(SCSshTxGetAlStateProgress(tx, STREAM_TOCLIENT) != SshStateBanner);
|
|
|
|
|
|
|
|
|
|
// phase 2: 300B of junk without EOL (>= 256, not a valid banner)
|
|
|
|
|
// -> frozen at banner at once
|
|
|
|
|
FLOW_DESTROY(&f);
|
|
|
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
|
memset(&f, 0, sizeof(f));
|
|
|
|
|
FLOW_INITIALIZE(&f);
|
|
|
|
|
f.protoctx = (void *)&ssn;
|
|
|
|
|
f.proto = IPPROTO_TCP;
|
|
|
|
|
f.alproto = ALPROTO_SSH;
|
|
|
|
|
memset(junk, 'X', sizeof(junk));
|
|
|
|
|
r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOSERVER, junk, sizeof(junk));
|
|
|
|
|
FAIL_IF(r != -1);
|
|
|
|
|
FAIL_IF_NULL(f.alstate);
|
|
|
|
|
tx = SCSshStateGetTx(f.alstate, 0);
|
|
|
|
|
FAIL_IF(SCSshTxGetAlStateProgress(tx, STREAM_TOSERVER) != SshStateBanner);
|
|
|
|
|
FAIL_IF(SCSshTxGetAlStateProgress(tx, STREAM_TOCLIENT) != SshStateBanner);
|
|
|
|
|
|
|
|
|
|
FLOW_DESTROY(&f);
|
|
|
|
|
AppLayerParserThreadCtxFree(alp_tctx);
|
|
|
|
|
StreamTcpFreeConfig(true);
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
|
|
|
|
|
ssh: expose the parse failure reason and state in the eve ssh log
A flow that hit an unrecoverable ssh parse error (invalid banner
or invalid record) was never visible in the eve ssh output: the
log condition only admitted a successful handshake (both
directions at kex or session), and the tx logger is one-shot, so
the successful-handshake log fired before a later failure could
be reported and the failure was then silent - the reason was
only visible through the app-layer events.
The log condition is now failure-only: the failure latches, so
the object is emitted exactly once - at the failure, carrying
the error field (invalid_banner / invalid_record) and the state
field (the state the failure occurred in - banner, kex, or
session if a later record fails after NewKeys) - and successful
flows are logged at the flow-end flush, where the engine logs
unconditionally. Logging on the success condition mid-flow would
consume the one-shot tx log and drop a later failure, so it is
not done. The state field is emitted only alongside the error.
The object may be error + state only, when no banner was
parsed. A unit test pins the condition for a failed, a
banner-only and a both-at-kex flow (the failure admits the log
on the failing delivery); the user guide and eve reference doc
commits that follow carry the field docs.
That flow-end flush is skipped for flows that end without a
further app-layer update, though: in IPS/firewall mode a drop
disables the app layer (StreamTcpCheckFlowDrops ->
StreamTcpDisableAppLayer), after which no app-layer update
reaches the tx loggers, and a bypassed flow gets no flow-end
packet either. A successful ssh record - e.g. one that only saw
the banners - was therefore never emitted for those flows. The
hook is the app-layer end: on the transition to app-layer EOF,
StreamTcpDisableAppLayer raises FLOW_APP_LAYER_FLUSH_PENDING,
and FlowUpdateState raises it on the transition to a bypassed
state; OutputTxLog runs the tx loggers once for the next packet,
also when that packet itself has no app-layer update and is not
marked established (the disabling packet returns from the
stream engine before that), and flow-worker clears the flag when
it frees the app layer of a bypassed flow. The flush is
deliberately not a flow-end flush: when the flush is the only
reason to run the loggers, eof is not forced, so each logger's
condition decides and no other parser starts logging records it
suppressed; a disabling packet that does carry an app-layer
update keeps the existing EOF behaviour. SSHTxLogCondition
admits an IPS drop or a bypass, so the pending record is emitted
there; an IDS drop does not end the app layer, so the condition
stays failure-only - a later failure still wins, and the success
record is emitted by the flow-end flush, which the flow timeout
path now also runs for IDS-dropped flows. The unrelated
schema-sort ordering of the new state keys is fixed up as well.
Ticket: #9046.
4 weeks ago
|
|
|
/** \test The eve ssh log condition is failure-only: a direction
|
|
|
|
|
* \test that fails at banner (invalid banner) satisfies the
|
|
|
|
|
* \test condition (the error field makes the failure observable
|
|
|
|
|
* \test in eve), while a flow without failure does not - neither
|
|
|
|
|
* \test a banner-only flow nor one with both banners parsed
|
|
|
|
|
* \test (both directions at kex): the one-shot tx log must not be
|
|
|
|
|
* \test consumed mid-flow before a later failure could be
|
|
|
|
|
* \test reported, so successful flows are logged at the flow-end
|
|
|
|
|
* \test flush. The failure admits the log on the failing
|
|
|
|
|
* \test delivery.
|
|
|
|
|
*/
|
|
|
|
|
static int SSHParserTest31(void)
|
|
|
|
|
{
|
|
|
|
|
Flow f;
|
|
|
|
|
uint8_t badbanner[] = "SSH-bogus\r\n";
|
|
|
|
|
uint32_t badbannerlen = sizeof(badbanner) - 1;
|
|
|
|
|
uint8_t banner[] = "SSH-2.0-TestClient-1.0\r\n";
|
|
|
|
|
uint32_t bannerlen = sizeof(banner) - 1;
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
AppLayerParserThreadCtx *alp_tctx = AppLayerParserThreadCtxAlloc();
|
|
|
|
|
FAIL_IF_NULL(alp_tctx);
|
|
|
|
|
|
|
|
|
|
/* invalid banner: the failed direction admits the log condition */
|
|
|
|
|
memset(&f, 0, sizeof(f));
|
|
|
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
|
FLOW_INITIALIZE(&f);
|
|
|
|
|
f.protoctx = (void *)&ssn;
|
|
|
|
|
f.proto = IPPROTO_TCP;
|
|
|
|
|
f.alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
StreamTcpInitConfig(true);
|
|
|
|
|
|
|
|
|
|
int r = AppLayerParserParse(
|
|
|
|
|
NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOSERVER, badbanner, badbannerlen);
|
|
|
|
|
FAIL_IF(r != -1);
|
|
|
|
|
void *ssh_state = f.alstate;
|
|
|
|
|
FAIL_IF_NULL(ssh_state);
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
|
|
|
|
FAIL_IF(SCSshTxGetAlStateProgress(tx, STREAM_TOSERVER) != SshStateBanner);
|
|
|
|
|
FAIL_IF(SCSshTxGetLogCondition(tx) != true);
|
|
|
|
|
|
|
|
|
|
FLOW_DESTROY(&f);
|
|
|
|
|
|
|
|
|
|
/* no failure yet (client banner only): the condition stays
|
|
|
|
|
* false - the one-shot tx log is reserved for the failure
|
|
|
|
|
* or the next EOF-flush delivery */
|
|
|
|
|
memset(&f, 0, sizeof(f));
|
|
|
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
|
FLOW_INITIALIZE(&f);
|
|
|
|
|
f.protoctx = (void *)&ssn;
|
|
|
|
|
f.proto = IPPROTO_TCP;
|
|
|
|
|
f.alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOSERVER, banner, bannerlen);
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
ssh_state = f.alstate;
|
|
|
|
|
FAIL_IF_NULL(ssh_state);
|
|
|
|
|
tx = SCSshStateGetTx(ssh_state, 0);
|
|
|
|
|
FAIL_IF(SCSshTxGetLogCondition(tx) != false);
|
|
|
|
|
|
|
|
|
|
FLOW_DESTROY(&f);
|
|
|
|
|
|
|
|
|
|
/* both banners parsed (both directions at kex), still no
|
|
|
|
|
* failure: the condition must stay false - a mid-flow success
|
|
|
|
|
* log would consume the one-shot tx log before a later failure
|
|
|
|
|
* could be reported */
|
|
|
|
|
memset(&f, 0, sizeof(f));
|
|
|
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
|
FLOW_INITIALIZE(&f);
|
|
|
|
|
f.protoctx = (void *)&ssn;
|
|
|
|
|
f.proto = IPPROTO_TCP;
|
|
|
|
|
f.alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOSERVER, banner, bannerlen);
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOCLIENT, banner, bannerlen);
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
ssh_state = f.alstate;
|
|
|
|
|
FAIL_IF_NULL(ssh_state);
|
|
|
|
|
tx = SCSshStateGetTx(ssh_state, 0);
|
|
|
|
|
FAIL_IF(SCSshTxGetLogCondition(tx) != false);
|
|
|
|
|
|
|
|
|
|
/* the failure admits the log on the failing delivery */
|
|
|
|
|
uint8_t badrecord[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
|
|
|
|
|
r = AppLayerParserParse(
|
|
|
|
|
NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOCLIENT, badrecord, sizeof(badrecord));
|
|
|
|
|
FAIL_IF(r != -1);
|
|
|
|
|
FAIL_IF(SCSshTxGetLogCondition(tx) != true);
|
|
|
|
|
|
|
|
|
|
FLOW_DESTROY(&f);
|
|
|
|
|
AppLayerParserThreadCtxFree(alp_tctx);
|
|
|
|
|
StreamTcpFreeConfig(true);
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test A banner line split over two segments: the open-line segment
|
|
|
|
|
* \test publishes nothing but still marks the tx updated at parse
|
|
|
|
|
* \test entry; the line completion publishes the banner and takes
|
|
|
|
|
* \test the direction to kex. */
|
|
|
|
|
static int SSHParserTest32(void)
|
|
|
|
|
{
|
|
|
|
|
TcpReassemblyThreadCtx *ra_ctx = NULL;
|
|
|
|
|
ThreadVars tv;
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
Flow *f = NULL;
|
|
|
|
|
Packet *p = NULL;
|
|
|
|
|
|
|
|
|
|
uint8_t part1[] = "SSH-2.0-TestClient-1.0";
|
|
|
|
|
uint8_t part2[] = "\r\n";
|
|
|
|
|
uint32_t part1len = sizeof(part1) - 1;
|
|
|
|
|
uint32_t part2len = sizeof(part2) - 1;
|
|
|
|
|
|
|
|
|
|
memset(&tv, 0x00, sizeof(tv));
|
|
|
|
|
|
|
|
|
|
StreamTcpUTInit(&ra_ctx);
|
|
|
|
|
StreamTcpUTInitInline();
|
|
|
|
|
StreamTcpUTSetupSession(&ssn);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.server, 1);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.client, 1);
|
|
|
|
|
|
|
|
|
|
f = UTHBuildFlow(AF_INET, "1.1.1.1", "2.2.2.2", 1234, 2222);
|
|
|
|
|
FAIL_IF_NULL(f);
|
|
|
|
|
f->protoctx = &ssn;
|
|
|
|
|
f->proto = IPPROTO_TCP;
|
|
|
|
|
f->alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
p = PacketGetFromAlloc();
|
|
|
|
|
FAIL_IF_NULL(p);
|
|
|
|
|
p->proto = IPPROTO_TCP;
|
|
|
|
|
p->flow = f;
|
|
|
|
|
|
|
|
|
|
uint32_t seqcli = 2;
|
|
|
|
|
// segment 1: the open banner line; the parse consumes nothing and
|
|
|
|
|
// publishes nothing, but the tx is marked updated at parse entry
|
|
|
|
|
FAIL_IF(StreamTcpUTAddSegmentWithPayload(&tv, ra_ctx, &ssn.client, seqcli, part1, part1len) ==
|
|
|
|
|
-1);
|
|
|
|
|
seqcli += part1len;
|
|
|
|
|
FAIL_IF(StreamTcpReassembleAppLayer(&tv, ra_ctx, &ssn, &ssn.client, p, UPDATE_DIR_PACKET) < 0);
|
|
|
|
|
|
|
|
|
|
void *ssh_state = f->alstate;
|
|
|
|
|
FAIL_IF_NULL(ssh_state);
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
|
|
|
|
struct AppLayerTxData *txdata = AppLayerParserGetTxData(IPPROTO_TCP, ALPROTO_SSH, tx);
|
|
|
|
|
FAIL_IF_NULL(txdata);
|
|
|
|
|
FAIL_IF(!txdata->updated_ts);
|
|
|
|
|
|
|
|
|
|
// segment 2: the end-of-line completes the line; the banner
|
|
|
|
|
// publishes and the direction advances to kex
|
|
|
|
|
FAIL_IF(StreamTcpUTAddSegmentWithPayload(&tv, ra_ctx, &ssn.client, seqcli, part2, part2len) ==
|
|
|
|
|
-1);
|
|
|
|
|
seqcli += part2len;
|
|
|
|
|
FAIL_IF(StreamTcpReassembleAppLayer(&tv, ra_ctx, &ssn, &ssn.client, p, UPDATE_DIR_PACKET) < 0);
|
|
|
|
|
|
|
|
|
|
FAIL_IF(SCSshTxGetAlStateProgress(tx, STREAM_TOSERVER) != SshStateKex);
|
|
|
|
|
FAIL_IF(!txdata->updated_ts);
|
|
|
|
|
|
|
|
|
|
UTHFreePacket(p);
|
|
|
|
|
UTHFreeFlow(f);
|
|
|
|
|
StreamTcpUTClearSession(&ssn);
|
|
|
|
|
StreamTcpUTDeinit(ra_ctx);
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test NewKeys while the peer direction failed: the peer never
|
|
|
|
|
* \test actually switched keys, so it is not terminal for the
|
|
|
|
|
* \test no-inspection decision (encryption bypass stays armed-able
|
|
|
|
|
* \test only when the peer's own NewKeys was observed). */
|
|
|
|
|
static int SSHParserTest33(void)
|
|
|
|
|
{
|
|
|
|
|
Flow f;
|
|
|
|
|
uint8_t badbanner[] = "SSH-bogus\r\n";
|
|
|
|
|
uint32_t badbannerlen = sizeof(badbanner) - 1;
|
|
|
|
|
uint8_t banner[] = "SSH-2.0-TestClient-1.0\r\n";
|
|
|
|
|
uint32_t bannerlen = sizeof(banner) - 1;
|
|
|
|
|
uint8_t newkeys[] = { 0x00, 0x00, 0x00, 0x03, 0x01, 21, 0x00 };
|
|
|
|
|
uint32_t newkeyslen = sizeof(newkeys);
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
AppLayerParserThreadCtx *alp_tctx = AppLayerParserThreadCtxAlloc();
|
|
|
|
|
FAIL_IF_NULL(alp_tctx);
|
|
|
|
|
|
|
|
|
|
memset(&f, 0, sizeof(f));
|
|
|
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
|
FLOW_INITIALIZE(&f);
|
|
|
|
|
f.protoctx = (void *)&ssn;
|
|
|
|
|
f.proto = IPPROTO_TCP;
|
|
|
|
|
f.alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
StreamTcpInitConfig(true);
|
|
|
|
|
|
|
|
|
|
int r = AppLayerParserParse(
|
|
|
|
|
NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOSERVER, badbanner, badbannerlen);
|
|
|
|
|
FAIL_IF(r != -1);
|
|
|
|
|
r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOCLIENT, banner, bannerlen);
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOCLIENT, newkeys, newkeyslen);
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
|
|
|
|
|
void *ssh_state = f.alstate;
|
|
|
|
|
FAIL_IF_NULL(ssh_state);
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
|
|
|
|
FAIL_IF(SCSshTxGetFlags(tx, STREAM_TOSERVER) != SshStateBanner);
|
|
|
|
|
FAIL_IF(SCSshTxGetFlags(tx, STREAM_TOCLIENT) != SshStateSession);
|
|
|
|
|
FAIL_IF(SCAppLayerParserStateIssetFlag(f.alparser, APP_LAYER_PARSER_NO_INSPECTION));
|
|
|
|
|
|
|
|
|
|
FLOW_DESTROY(&f);
|
|
|
|
|
AppLayerParserThreadCtxFree(alp_tctx);
|
|
|
|
|
StreamTcpFreeConfig(true);
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test a parse failure ends app-layer parsing for the whole flow
|
|
|
|
|
* \test (engine path): the failure site returns a parse error, so
|
|
|
|
|
* \test StreamTcpDisableAppLayer runs and no further data in either
|
|
|
|
|
* \test direction reaches the parser - mid-flow or at the flow-end
|
|
|
|
|
* \test flush (the disable flag is checked at the reassembly
|
|
|
|
|
* \test entry, so even pre-failure buffered bytes are not
|
|
|
|
|
* \test delivered at EOF); the failing direction's state stays
|
|
|
|
|
* \test where the error occurred, readable for the progress
|
|
|
|
|
* \test accessor, and the failure event marks the tx updated. */
|
|
|
|
|
static int SSHParserTest34(void)
|
|
|
|
|
{
|
|
|
|
|
TcpReassemblyThreadCtx *ra_ctx = NULL;
|
|
|
|
|
ThreadVars tv;
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
Flow *f = NULL;
|
|
|
|
|
Packet *p = NULL;
|
|
|
|
|
|
|
|
|
|
uint8_t cli_banner[] = "SSH-2.0-TestClient-1.0\r\n";
|
|
|
|
|
uint32_t cli_bannerlen = sizeof(cli_banner) - 1;
|
|
|
|
|
uint8_t srv_banner[] = "SSH-2.0-TestServer-1.0\r\n";
|
|
|
|
|
uint32_t srv_bannerlen = sizeof(srv_banner) - 1;
|
|
|
|
|
uint8_t badrecord[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
|
|
|
|
|
uint32_t badrecordlen = sizeof(badrecord);
|
|
|
|
|
uint8_t newkeys[] = { 0x00, 0x00, 0x00, 0x03, 0x01, 21, 0x00 };
|
|
|
|
|
uint32_t newkeyslen = sizeof(newkeys);
|
|
|
|
|
|
|
|
|
|
memset(&tv, 0x00, sizeof(tv));
|
|
|
|
|
|
|
|
|
|
StreamTcpUTInit(&ra_ctx);
|
|
|
|
|
StreamTcpUTInitInline();
|
|
|
|
|
StreamTcpUTSetupSession(&ssn);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.server, 1);
|
|
|
|
|
StreamTcpUTSetupStream(&ssn.client, 1);
|
|
|
|
|
|
|
|
|
|
f = UTHBuildFlow(AF_INET, "1.1.1.1", "2.2.2.2", 1234, 2222);
|
|
|
|
|
FAIL_IF_NULL(f);
|
|
|
|
|
f->protoctx = &ssn;
|
|
|
|
|
f->proto = IPPROTO_TCP;
|
|
|
|
|
f->alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
p = PacketGetFromAlloc();
|
|
|
|
|
FAIL_IF_NULL(p);
|
|
|
|
|
p->proto = IPPROTO_TCP;
|
|
|
|
|
p->flow = f;
|
|
|
|
|
|
|
|
|
|
// the server banner is buffered, not yet delivered to the parser
|
|
|
|
|
uint32_t seqsrv = 2;
|
|
|
|
|
FAIL_IF(StreamTcpUTAddSegmentWithPayload(
|
|
|
|
|
&tv, ra_ctx, &ssn.server, seqsrv, srv_banner, srv_bannerlen) == -1);
|
|
|
|
|
seqsrv += srv_bannerlen;
|
|
|
|
|
|
|
|
|
|
uint32_t seqcli = 2;
|
|
|
|
|
// the client banner is delivered and parsed: the direction
|
|
|
|
|
// advances to kex
|
|
|
|
|
FAIL_IF(StreamTcpUTAddSegmentWithPayload(
|
|
|
|
|
&tv, ra_ctx, &ssn.client, seqcli, cli_banner, cli_bannerlen) == -1);
|
|
|
|
|
seqcli += cli_bannerlen;
|
|
|
|
|
FAIL_IF(StreamTcpReassembleAppLayer(&tv, ra_ctx, &ssn, &ssn.client, p, UPDATE_DIR_PACKET) < 0);
|
|
|
|
|
|
|
|
|
|
void *ssh_state = f->alstate;
|
|
|
|
|
FAIL_IF_NULL(ssh_state);
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
|
|
|
|
FAIL_IF(SCSshTxGetAlStateProgress(tx, STREAM_TOSERVER) != SshStateKex);
|
|
|
|
|
|
|
|
|
|
// an invalid record in the client direction: the engine ends
|
|
|
|
|
// app-layer parsing for the whole flow
|
|
|
|
|
FAIL_IF(StreamTcpUTAddSegmentWithPayload(
|
|
|
|
|
&tv, ra_ctx, &ssn.client, seqcli, badrecord, badrecordlen) == -1);
|
|
|
|
|
seqcli += badrecordlen;
|
|
|
|
|
FAIL_IF(StreamTcpReassembleAppLayer(&tv, ra_ctx, &ssn, &ssn.client, p, UPDATE_DIR_PACKET) < 0);
|
|
|
|
|
|
|
|
|
|
// the engine ended app-layer parsing for the whole flow
|
|
|
|
|
FAIL_IF(!StreamTcpAppLayerIsDisabled(f));
|
|
|
|
|
// the failing direction is frozen at kex, still readable
|
|
|
|
|
FAIL_IF(SCSshTxGetAlStateProgress(tx, STREAM_TOSERVER) != SshStateKex);
|
|
|
|
|
// the failure event published: the tx is marked updated, so the
|
ssh: expose the parse failure reason and state in the eve ssh log
A flow that hit an unrecoverable ssh parse error (invalid banner
or invalid record) was never visible in the eve ssh output: the
log condition only admitted a successful handshake (both
directions at kex or session), and the tx logger is one-shot, so
the successful-handshake log fired before a later failure could
be reported and the failure was then silent - the reason was
only visible through the app-layer events.
The log condition is now failure-only: the failure latches, so
the object is emitted exactly once - at the failure, carrying
the error field (invalid_banner / invalid_record) and the state
field (the state the failure occurred in - banner, kex, or
session if a later record fails after NewKeys) - and successful
flows are logged at the flow-end flush, where the engine logs
unconditionally. Logging on the success condition mid-flow would
consume the one-shot tx log and drop a later failure, so it is
not done. The state field is emitted only alongside the error.
The object may be error + state only, when no banner was
parsed. A unit test pins the condition for a failed, a
banner-only and a both-at-kex flow (the failure admits the log
on the failing delivery); the user guide and eve reference doc
commits that follow carry the field docs.
That flow-end flush is skipped for flows that end without a
further app-layer update, though: in IPS/firewall mode a drop
disables the app layer (StreamTcpCheckFlowDrops ->
StreamTcpDisableAppLayer), after which no app-layer update
reaches the tx loggers, and a bypassed flow gets no flow-end
packet either. A successful ssh record - e.g. one that only saw
the banners - was therefore never emitted for those flows. The
hook is the app-layer end: on the transition to app-layer EOF,
StreamTcpDisableAppLayer raises FLOW_APP_LAYER_FLUSH_PENDING,
and FlowUpdateState raises it on the transition to a bypassed
state; OutputTxLog runs the tx loggers once for the next packet,
also when that packet itself has no app-layer update and is not
marked established (the disabling packet returns from the
stream engine before that), and flow-worker clears the flag when
it frees the app layer of a bypassed flow. The flush is
deliberately not a flow-end flush: when the flush is the only
reason to run the loggers, eof is not forced, so each logger's
condition decides and no other parser starts logging records it
suppressed; a disabling packet that does carry an app-layer
update keeps the existing EOF behaviour. SSHTxLogCondition
admits an IPS drop or a bypass, so the pending record is emitted
there; an IDS drop does not end the app layer, so the condition
stays failure-only - a later failure still wins, and the success
record is emitted by the flow-end flush, which the flow timeout
path now also runs for IDS-dropped flows. The unrelated
schema-sort ordering of the new state keys is fixed up as well.
Ticket: #9046.
4 weeks ago
|
|
|
// engine evaluates it (not only at the next EOF-flush delivery)
|
|
|
|
|
struct AppLayerTxData *txdata = AppLayerParserGetTxData(IPPROTO_TCP, ALPROTO_SSH, tx);
|
|
|
|
|
FAIL_IF_NULL(txdata);
|
|
|
|
|
FAIL_IF(!txdata->updated_ts);
|
|
|
|
|
|
|
|
|
|
// further data in either direction does not reach the parser -
|
ssh: expose the parse failure reason and state in the eve ssh log
A flow that hit an unrecoverable ssh parse error (invalid banner
or invalid record) was never visible in the eve ssh output: the
log condition only admitted a successful handshake (both
directions at kex or session), and the tx logger is one-shot, so
the successful-handshake log fired before a later failure could
be reported and the failure was then silent - the reason was
only visible through the app-layer events.
The log condition is now failure-only: the failure latches, so
the object is emitted exactly once - at the failure, carrying
the error field (invalid_banner / invalid_record) and the state
field (the state the failure occurred in - banner, kex, or
session if a later record fails after NewKeys) - and successful
flows are logged at the flow-end flush, where the engine logs
unconditionally. Logging on the success condition mid-flow would
consume the one-shot tx log and drop a later failure, so it is
not done. The state field is emitted only alongside the error.
The object may be error + state only, when no banner was
parsed. A unit test pins the condition for a failed, a
banner-only and a both-at-kex flow (the failure admits the log
on the failing delivery); the user guide and eve reference doc
commits that follow carry the field docs.
That flow-end flush is skipped for flows that end without a
further app-layer update, though: in IPS/firewall mode a drop
disables the app layer (StreamTcpCheckFlowDrops ->
StreamTcpDisableAppLayer), after which no app-layer update
reaches the tx loggers, and a bypassed flow gets no flow-end
packet either. A successful ssh record - e.g. one that only saw
the banners - was therefore never emitted for those flows. The
hook is the app-layer end: on the transition to app-layer EOF,
StreamTcpDisableAppLayer raises FLOW_APP_LAYER_FLUSH_PENDING,
and FlowUpdateState raises it on the transition to a bypassed
state; OutputTxLog runs the tx loggers once for the next packet,
also when that packet itself has no app-layer update and is not
marked established (the disabling packet returns from the
stream engine before that), and flow-worker clears the flag when
it frees the app layer of a bypassed flow. The flush is
deliberately not a flow-end flush: when the flush is the only
reason to run the loggers, eof is not forced, so each logger's
condition decides and no other parser starts logging records it
suppressed; a disabling packet that does carry an app-layer
update keeps the existing EOF behaviour. SSHTxLogCondition
admits an IPS drop or a bypass, so the pending record is emitted
there; an IDS drop does not end the app layer, so the condition
stays failure-only - a later failure still wins, and the success
record is emitted by the flow-end flush, which the flow timeout
path now also runs for IDS-dropped flows. The unrelated
schema-sort ordering of the new state keys is fixed up as well.
Ticket: #9046.
4 weeks ago
|
|
|
// neither directly nor via the next EOF-flush delivery: the disable flag
|
|
|
|
|
// is checked at the reassembly entry, so the buffered
|
|
|
|
|
// pre-failure server banner is not delivered at EOF
|
|
|
|
|
p->flags |= PKT_PSEUDO_STREAM_END;
|
|
|
|
|
FAIL_IF(StreamTcpUTAddSegmentWithPayload(
|
|
|
|
|
&tv, ra_ctx, &ssn.client, seqcli, newkeys, newkeyslen) == -1);
|
|
|
|
|
seqcli += newkeyslen;
|
|
|
|
|
FAIL_IF(StreamTcpReassembleAppLayer(&tv, ra_ctx, &ssn, &ssn.client, p, UPDATE_DIR_PACKET) < 0);
|
|
|
|
|
FAIL_IF(StreamTcpReassembleAppLayer(&tv, ra_ctx, &ssn, &ssn.server, p, UPDATE_DIR_PACKET) < 0);
|
|
|
|
|
|
|
|
|
|
FAIL_IF(SCSshTxGetAlStateProgress(tx, STREAM_TOSERVER) != SshStateKex);
|
|
|
|
|
FAIL_IF(SCSshTxGetAlStateProgress(tx, STREAM_TOCLIENT) != SshStateBanner);
|
|
|
|
|
|
|
|
|
|
UTHFreePacket(p);
|
|
|
|
|
UTHFreeFlow(f);
|
|
|
|
|
StreamTcpUTClearSession(&ssn);
|
|
|
|
|
StreamTcpUTDeinit(ra_ctx);
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test a NewKeys record split across two parser calls (segment
|
|
|
|
|
* \test boundary after the 6-byte record header) arms the
|
|
|
|
|
* \test no-inspection/bypass flags like a complete record: the
|
|
|
|
|
* \test arming runs on the session transition itself, shared by
|
|
|
|
|
* \test the complete-record and incomplete-header arms. The peer
|
|
|
|
|
* \test direction completes its NewKeys record in one call first;
|
|
|
|
|
* \test the split direction then delivers the record header (the
|
|
|
|
|
* \test body byte is stashed) and the arming must happen on that
|
|
|
|
|
* \test delivery. Also pins BYPASS_READY in bypass mode.
|
|
|
|
|
*/
|
|
|
|
|
static int SSHParserTest35(void)
|
|
|
|
|
{
|
|
|
|
|
Flow f;
|
|
|
|
|
uint8_t banner[] = "SSH-2.0-TestClient-1.0\r\n";
|
|
|
|
|
uint32_t bannerlen = sizeof(banner) - 1;
|
|
|
|
|
uint8_t newkeys[] = { 0x00, 0x00, 0x00, 0x03, 0x01, 21, 0x00 };
|
|
|
|
|
uint8_t newkeys_hdr[6] = { 0x00, 0x00, 0x00, 0x03, 0x01, 21 };
|
|
|
|
|
uint8_t newkeys_body[1] = { 0x00 };
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
AppLayerParserThreadCtx *alp_tctx = AppLayerParserThreadCtxAlloc();
|
|
|
|
|
FAIL_IF_NULL(alp_tctx);
|
|
|
|
|
|
|
|
|
|
memset(&f, 0, sizeof(f));
|
|
|
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
|
FLOW_INITIALIZE(&f);
|
|
|
|
|
f.protoctx = (void *)&ssn;
|
|
|
|
|
f.proto = IPPROTO_TCP;
|
|
|
|
|
f.alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
StreamTcpInitConfig(true);
|
|
|
|
|
|
|
|
|
|
int r = AppLayerParserParse(
|
|
|
|
|
NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOSERVER, banner, bannerlen);
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOCLIENT, banner, bannerlen);
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
|
|
|
|
|
/* peer direction: complete NewKeys record in one call -> session */
|
|
|
|
|
r = AppLayerParserParse(
|
|
|
|
|
NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOCLIENT, newkeys, sizeof(newkeys));
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
void *ssh_state = f.alstate;
|
|
|
|
|
FAIL_IF_NULL(ssh_state);
|
|
|
|
|
void *tx = SCSshStateGetTx(ssh_state, 0);
|
|
|
|
|
FAIL_IF(SCSshTxGetFlags(tx, STREAM_TOCLIENT) != SshStateSession);
|
|
|
|
|
|
|
|
|
|
/* split direction: the record header only (the body byte is
|
|
|
|
|
* stashed and arrives in the next call) */
|
|
|
|
|
r = AppLayerParserParse(
|
|
|
|
|
NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOSERVER, newkeys_hdr, sizeof(newkeys_hdr));
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
tx = SCSshStateGetTx(ssh_state, 0);
|
|
|
|
|
FAIL_IF(SCSshTxGetFlags(tx, STREAM_TOSERVER) != SshStateSession);
|
|
|
|
|
/* the session transition armed the flags despite the split */
|
|
|
|
|
FAIL_IF(!SCAppLayerParserStateIssetFlag(f.alparser, APP_LAYER_PARSER_NO_INSPECTION));
|
|
|
|
|
|
|
|
|
|
/* the stashed body byte completes the record */
|
|
|
|
|
r = AppLayerParserParse(
|
|
|
|
|
NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOSERVER, newkeys_body, sizeof(newkeys_body));
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
FAIL_IF(!SCAppLayerParserStateIssetFlag(f.alparser, APP_LAYER_PARSER_NO_INSPECTION));
|
|
|
|
|
|
|
|
|
|
FLOW_DESTROY(&f);
|
|
|
|
|
|
|
|
|
|
/* bypass mode: the transition also arms BYPASS_READY */
|
|
|
|
|
SCSshEnableBypass(ENCRYPTION_HANDLING_BYPASS);
|
|
|
|
|
memset(&f, 0, sizeof(f));
|
|
|
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
|
FLOW_INITIALIZE(&f);
|
|
|
|
|
f.protoctx = (void *)&ssn;
|
|
|
|
|
f.proto = IPPROTO_TCP;
|
|
|
|
|
f.alproto = ALPROTO_SSH;
|
|
|
|
|
|
|
|
|
|
r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOSERVER, banner, bannerlen);
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOCLIENT, banner, bannerlen);
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
r = AppLayerParserParse(
|
|
|
|
|
NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOCLIENT, newkeys, sizeof(newkeys));
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
r = AppLayerParserParse(
|
|
|
|
|
NULL, alp_tctx, &f, ALPROTO_SSH, STREAM_TOSERVER, newkeys_hdr, sizeof(newkeys_hdr));
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
FAIL_IF(!SCAppLayerParserStateIssetFlag(f.alparser, APP_LAYER_PARSER_BYPASS_READY));
|
|
|
|
|
SCSshEnableBypass(ENCRYPTION_HANDLING_TRACK_ONLY);
|
|
|
|
|
|
|
|
|
|
FLOW_DESTROY(&f);
|
|
|
|
|
AppLayerParserThreadCtxFree(alp_tctx);
|
|
|
|
|
StreamTcpFreeConfig(true);
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endif /* UNITTESTS */
|
|
|
|
|
|
|
|
|
|
void SSHParserRegisterTests(void)
|
|
|
|
|
{
|
|
|
|
|
#ifdef UNITTESTS
|
|
|
|
|
UtRegisterTest("SSHParserTest01 - ToServer", SSHParserTest01);
|
|
|
|
|
UtRegisterTest("SSHParserTest02 - ToServer", SSHParserTest02);
|
|
|
|
|
UtRegisterTest("SSHParserTest03 - ToServer", SSHParserTest03);
|
|
|
|
|
UtRegisterTest("SSHParserTest04 - ToClient", SSHParserTest04);
|
|
|
|
|
UtRegisterTest("SSHParserTest05 - ToClient", SSHParserTest05);
|
|
|
|
|
UtRegisterTest("SSHParserTest06 - ToClient", SSHParserTest06);
|
|
|
|
|
UtRegisterTest("SSHParserTest07 - ToServer 2 chunks", SSHParserTest07);
|
|
|
|
|
UtRegisterTest("SSHParserTest08 - ToServer 3 chunks", SSHParserTest08);
|
|
|
|
|
UtRegisterTest("SSHParserTest09 - ToClient 2 chunks", SSHParserTest09);
|
|
|
|
|
UtRegisterTest("SSHParserTest10 - ToClient 3 chunks", SSHParserTest10);
|
|
|
|
|
UtRegisterTest("SSHParserTest11 - ToClient 4 chunks", SSHParserTest11);
|
|
|
|
|
UtRegisterTest("SSHParserTest12 - ToClient 4 chunks", SSHParserTest12);
|
|
|
|
|
UtRegisterTest("SSHParserTest13 - ToClient 4 chunks", SSHParserTest13);
|
|
|
|
|
UtRegisterTest("SSHParserTest14 - ToClient 4 chunks", SSHParserTest14);
|
|
|
|
|
UtRegisterTest("SSHParserTest15", SSHParserTest15);
|
|
|
|
|
UtRegisterTest("SSHParserTest16", SSHParserTest16);
|
|
|
|
|
UtRegisterTest("SSHParserTest17", SSHParserTest17);
|
|
|
|
|
UtRegisterTest("SSHParserTest18", SSHParserTest18);
|
|
|
|
|
UtRegisterTest("SSHParserTest19", SSHParserTest19);
|
|
|
|
|
UtRegisterTest("SSHParserTest20", SSHParserTest20);
|
|
|
|
|
UtRegisterTest("SSHParserTest21", SSHParserTest21);
|
|
|
|
|
UtRegisterTest("SSHParserTest22", SSHParserTest22);
|
|
|
|
|
UtRegisterTest("SSHParserTest23", SSHParserTest23);
|
|
|
|
|
UtRegisterTest("SSHParserTest24", SSHParserTest24);
|
|
|
|
|
UtRegisterTest("SSHParserTest25", SSHParserTest25);
|
ssh: rebuild the state machine as per-direction states
The old ssh state scale was coupled across the two directions: the
terminal finished state was only reached after both sides sent NewKeys,
and the progress accessor clamped every per-direction phase to a
shared value. A rule hooking the old top state therefore meant "the
flow completed", not "this direction completed the handshake", and
there was no room above the terminal state for a true completion
marker.
Replace the scale with per-direction phases:
banner(0) -> banner_wait_eol(1) -> kex(2) -> session(3)
NewKeys now sets session on the packet's own direction only, banner
completion sets kex, and the progress accessor is identity: each
direction reports its own raw phase. Add the registered completion
state done(4), never assigned during stream parsing: it stays out
of both name tables, so a rule referencing it fails to load, the
hook listing omits it and the firewall policy loader does not
consult it; the progress accessor reports it only on flows
disrupted by stream depth or async reassembly.
The legacy state names (in_progress, banner_done, finished) no
longer resolve through the name table (pre-production - breaking
changes allowed); the name-table unit test pins the rejection.
The old firewall policy state keys are added to the rename hint
table (like the TLS migration), so a config using them fails
loudly with the replacement named instead of degrading to a bare
warning. The non-prefilter engine-name registration now guards
the null id-to-name result (a hook-only rule at the completion
progress previously formatted a null name into the engine
name).
Add unit tests for the state name table (new names, rejected legacy
names, the unhookable completion state, id-to-name) and for the
per-direction session state.
The eve log condition compares the per-direction states with
>= rather than ==: a direction at session has necessarily passed
kex, and a single delivery can take a direction
banner -> kex -> session, so a final {session, kex}
pair must still admit the log (done is never assigned, so >=
changes only the kex branch).
Ticket: #9046.
1 month ago
|
|
|
UtRegisterTest("SSHParserTest26 - State name table", SSHParserTest26);
|
|
|
|
|
UtRegisterTest("SSHParserTest27 - Per-direction session state", SSHParserTest27);
|
|
|
|
|
UtRegisterTest("SSHParserTest28 - failure freezes the failing direction at its state",
|
|
|
|
|
SSHParserTest28);
|
|
|
|
|
UtRegisterTest(
|
|
|
|
|
"SSHParserTest29 - stash and fragment deliveries mark the tx updated", SSHParserTest29);
|
|
|
|
|
UtRegisterTest(
|
|
|
|
|
"SSHParserTest30 - long-banner continuation failure freezes at kex", SSHParserTest30);
|
ssh: expose the parse failure reason and state in the eve ssh log
A flow that hit an unrecoverable ssh parse error (invalid banner
or invalid record) was never visible in the eve ssh output: the
log condition only admitted a successful handshake (both
directions at kex or session), and the tx logger is one-shot, so
the successful-handshake log fired before a later failure could
be reported and the failure was then silent - the reason was
only visible through the app-layer events.
The log condition is now failure-only: the failure latches, so
the object is emitted exactly once - at the failure, carrying
the error field (invalid_banner / invalid_record) and the state
field (the state the failure occurred in - banner, kex, or
session if a later record fails after NewKeys) - and successful
flows are logged at the flow-end flush, where the engine logs
unconditionally. Logging on the success condition mid-flow would
consume the one-shot tx log and drop a later failure, so it is
not done. The state field is emitted only alongside the error.
The object may be error + state only, when no banner was
parsed. A unit test pins the condition for a failed, a
banner-only and a both-at-kex flow (the failure admits the log
on the failing delivery); the user guide and eve reference doc
commits that follow carry the field docs.
That flow-end flush is skipped for flows that end without a
further app-layer update, though: in IPS/firewall mode a drop
disables the app layer (StreamTcpCheckFlowDrops ->
StreamTcpDisableAppLayer), after which no app-layer update
reaches the tx loggers, and a bypassed flow gets no flow-end
packet either. A successful ssh record - e.g. one that only saw
the banners - was therefore never emitted for those flows. The
hook is the app-layer end: on the transition to app-layer EOF,
StreamTcpDisableAppLayer raises FLOW_APP_LAYER_FLUSH_PENDING,
and FlowUpdateState raises it on the transition to a bypassed
state; OutputTxLog runs the tx loggers once for the next packet,
also when that packet itself has no app-layer update and is not
marked established (the disabling packet returns from the
stream engine before that), and flow-worker clears the flag when
it frees the app layer of a bypassed flow. The flush is
deliberately not a flow-end flush: when the flush is the only
reason to run the loggers, eof is not forced, so each logger's
condition decides and no other parser starts logging records it
suppressed; a disabling packet that does carry an app-layer
update keeps the existing EOF behaviour. SSHTxLogCondition
admits an IPS drop or a bypass, so the pending record is emitted
there; an IDS drop does not end the app layer, so the condition
stays failure-only - a later failure still wins, and the success
record is emitted by the flow-end flush, which the flow timeout
path now also runs for IDS-dropped flows. The unrelated
schema-sort ordering of the new state keys is fixed up as well.
Ticket: #9046.
4 weeks ago
|
|
|
UtRegisterTest("SSHParserTest31 - failed flow admits the eve log condition", SSHParserTest31);
|
|
|
|
|
UtRegisterTest("SSHParserTest32 - banner continuation marks the tx updated at parse entry",
|
|
|
|
|
SSHParserTest32);
|
|
|
|
|
UtRegisterTest("SSHParserTest33 - failed peer does not arm no-inspection", SSHParserTest33);
|
|
|
|
|
UtRegisterTest("SSHParserTest34 - failure ends the flow's app-layer parsing (engine path)",
|
|
|
|
|
SSHParserTest34);
|
|
|
|
|
UtRegisterTest(
|
|
|
|
|
"SSHParserTest35 - split NewKeys arms the no-inspection/bypass flags", SSHParserTest35);
|
|
|
|
|
#endif /* UNITTESTS */
|
|
|
|
|
}
|
|
|
|
|
|