mirror of
https://github.com/zeek/zeek.git
synced 2025-10-12 19:48:20 +00:00
Merge remote-tracking branch 'origin/topic/vladg/ssh'
I replaced a few strcmps with either calls to std::str.compare or with the == operator of BroString. Also changed two of the input framework tests that did not pass anymore after the merge. The new SSH analyzer no longer loads the scripts that let network time run, hence those tests failed because updates were not propagated from the threads (that took a while to find.) * origin/topic/vladg/ssh: (25 commits) SSH: Register analyzer for 22/tcp. SSH: Add 22/tcp to likely_server_ports SSH: Ignore encrypted packets by default. SSH: Fix some edge-cases which created BinPAC exceptions SSH: Add memleak btest SSH: Update baselines SSH: Added some more events for SSH2 SSH: Intel framework integration (PUBKEY_HASH) Update baselines for new SSH analyzer. Update SSH policy scripts with new events. SSH: Add documentation Refactoring ssh-protocol.pac: SSH: Use the compression_algorithms const in another place. Some cleanup and refactoring on SSH main.bro. SSH: A bit of code cleanup. Move SSH constants to consts.pac SSH: Cleanup code style. SSH: Fix some memleaks. Refactored the SSH analyzer. Added supported for algorithm detection and more key exchange message types. Add host key support for SSH1. Add support for SSH1 Move SSH analyzer to new plugin architecture. ... Conflicts: scripts/base/protocols/ssh/main.bro testing/btest/Baseline/core.print-bpf-filters/output2 testing/btest/Baseline/plugins.hooks/output BIT-1344: #merged
This commit is contained in:
commit
f79b5adc08
38 changed files with 1494 additions and 324 deletions
|
@ -4,6 +4,8 @@ include(BroPlugin)
|
|||
include_directories(BEFORE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR})
|
||||
|
||||
bro_plugin_begin(Bro SSH)
|
||||
bro_plugin_cc(SSH.cc Plugin.cc)
|
||||
bro_plugin_bif(events.bif)
|
||||
bro_plugin_cc(SSH.cc Plugin.cc)
|
||||
bro_plugin_bif(types.bif)
|
||||
bro_plugin_bif(events.bif)
|
||||
bro_plugin_pac(ssh.pac ssh-analyzer.pac ssh-protocol.pac)
|
||||
bro_plugin_end()
|
||||
|
|
|
@ -1,25 +1,24 @@
|
|||
// See the file in the main distribution directory for copyright.
|
||||
|
||||
|
||||
#include "plugin/Plugin.h"
|
||||
|
||||
#include "SSH.h"
|
||||
|
||||
namespace plugin {
|
||||
namespace Bro_SSH {
|
||||
namespace Bro_SSH {
|
||||
|
||||
class Plugin : public plugin::Plugin {
|
||||
public:
|
||||
plugin::Configuration Configure()
|
||||
{
|
||||
AddComponent(new ::analyzer::Component("SSH", ::analyzer::ssh::SSH_Analyzer::Instantiate));
|
||||
class Plugin : public plugin::Plugin {
|
||||
public:
|
||||
plugin::Configuration Configure()
|
||||
{
|
||||
AddComponent(new ::analyzer::Component("SSH", ::analyzer::SSH::SSH_Analyzer::Instantiate));
|
||||
|
||||
plugin::Configuration config;
|
||||
config.name = "Bro::SSH";
|
||||
config.description = "Secure Shell analyzer";
|
||||
return config;
|
||||
}
|
||||
} plugin;
|
||||
|
||||
plugin::Configuration config;
|
||||
config.name = "Bro::SSH";
|
||||
config.description = "SSH analyzer";
|
||||
return config;
|
||||
}
|
||||
} plugin;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,105 +1,148 @@
|
|||
// See the file "COPYING" in the main distribution directory for copyright.
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <ctype.h>
|
||||
|
||||
#include "NetVar.h"
|
||||
#include "SSH.h"
|
||||
#include "Event.h"
|
||||
#include "analyzer/protocol/tcp/ContentLine.h"
|
||||
|
||||
#include "analyzer/protocol/tcp/TCP_Reassembler.h"
|
||||
|
||||
#include "Reporter.h"
|
||||
|
||||
#include "types.bif.h"
|
||||
#include "events.bif.h"
|
||||
|
||||
using namespace analyzer::ssh;
|
||||
using namespace analyzer::SSH;
|
||||
|
||||
SSH_Analyzer::SSH_Analyzer(Connection* c)
|
||||
: tcp::TCP_ApplicationAnalyzer("SSH", c)
|
||||
: tcp::TCP_ApplicationAnalyzer("SSH", c)
|
||||
{
|
||||
orig = new tcp::ContentLine_Analyzer(c, true);
|
||||
orig->SetSkipPartial(true);
|
||||
orig->SetCRLFAsEOL(LF_as_EOL);
|
||||
AddSupportAnalyzer(orig);
|
||||
|
||||
resp = new tcp::ContentLine_Analyzer(c, false);
|
||||
resp->SetSkipPartial(true);
|
||||
resp->SetCRLFAsEOL(LF_as_EOL);
|
||||
AddSupportAnalyzer(resp);
|
||||
interp = new binpac::SSH::SSH_Conn(this);
|
||||
had_gap = false;
|
||||
auth_decision_made = false;
|
||||
skipped_banner = false;
|
||||
service_accept_size = 0;
|
||||
userauth_failure_size = 0;
|
||||
}
|
||||
|
||||
void SSH_Analyzer::DeliverStream(int length, const u_char* data, bool is_orig)
|
||||
SSH_Analyzer::~SSH_Analyzer()
|
||||
{
|
||||
tcp::TCP_ApplicationAnalyzer::DeliverStream(length, data, is_orig);
|
||||
delete interp;
|
||||
}
|
||||
|
||||
// We're all done processing this endpoint - flag it as such,
|
||||
// before we even determine whether we have any event generation
|
||||
// work to do, to make sure we don't do any further work on it.
|
||||
if ( is_orig )
|
||||
orig->SetSkipDeliveries(true);
|
||||
else
|
||||
resp->SetSkipDeliveries(true);
|
||||
void SSH_Analyzer::Done()
|
||||
{
|
||||
tcp::TCP_ApplicationAnalyzer::Done();
|
||||
|
||||
if ( TCP() )
|
||||
interp->FlowEOF(true);
|
||||
interp->FlowEOF(false);
|
||||
}
|
||||
|
||||
void SSH_Analyzer::EndpointEOF(bool is_orig)
|
||||
{
|
||||
tcp::TCP_ApplicationAnalyzer::EndpointEOF(is_orig);
|
||||
interp->FlowEOF(is_orig);
|
||||
}
|
||||
|
||||
void SSH_Analyzer::DeliverStream(int len, const u_char* data, bool orig)
|
||||
{
|
||||
tcp::TCP_ApplicationAnalyzer::DeliverStream(len, data, orig);
|
||||
|
||||
assert(TCP());
|
||||
if ( TCP()->IsPartial() )
|
||||
return;
|
||||
|
||||
if ( had_gap )
|
||||
// If only one side had a content gap, we could still try to
|
||||
// deliver data to the other side if the script layer can handle this.
|
||||
return;
|
||||
|
||||
if ( interp->get_state(orig) == binpac::SSH::ENCRYPTED )
|
||||
{
|
||||
// Don't try to parse version if there has already been a gap.
|
||||
tcp::TCP_Endpoint* endp = is_orig ? TCP()->Orig() : TCP()->Resp();
|
||||
if ( endp->HadGap() )
|
||||
if ( ssh_encrypted_packet )
|
||||
BifEvent::generate_ssh_encrypted_packet(interp->bro_analyzer(), interp->bro_analyzer()->Conn(),
|
||||
orig, len);
|
||||
|
||||
if ( ! auth_decision_made )
|
||||
ProcessEncrypted(len, orig);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
interp->NewData(orig, data, data + len);
|
||||
}
|
||||
catch ( const binpac::Exception& e )
|
||||
{
|
||||
ProtocolViolation(fmt("Binpac exception: %s", e.c_msg()));
|
||||
}
|
||||
}
|
||||
|
||||
void SSH_Analyzer::Undelivered(uint64 seq, int len, bool orig)
|
||||
{
|
||||
tcp::TCP_ApplicationAnalyzer::Undelivered(seq, len, orig);
|
||||
had_gap = true;
|
||||
interp->NewGap(orig, len);
|
||||
}
|
||||
|
||||
void SSH_Analyzer::ProcessEncrypted(int len, bool orig)
|
||||
{
|
||||
// We're interested in messages from the server for SSH2
|
||||
if ( ! orig && (interp->get_version() == binpac::SSH::SSH2) )
|
||||
{
|
||||
// The first thing we see and want to know is the length of
|
||||
// SSH_MSG_SERVICE_REQUEST, which has a fixed (decrypted) size
|
||||
// of 24 bytes (17 for content pad-aligned to 8-byte
|
||||
// boundaries)
|
||||
if ( ! service_accept_size )
|
||||
{
|
||||
service_accept_size = len;
|
||||
return;
|
||||
}
|
||||
|
||||
const char* line = (const char*) data;
|
||||
|
||||
// The SSH identification looks like this:
|
||||
//
|
||||
// SSH-<protocolmajor>.<protocolminor>-<version>\n
|
||||
//
|
||||
// We're interested in the "version" part here.
|
||||
|
||||
if ( length < 4 || memcmp(line, "SSH-", 4) != 0 )
|
||||
{
|
||||
Weird("malformed_ssh_identification");
|
||||
ProtocolViolation("malformed ssh identification", line, length);
|
||||
return;
|
||||
}
|
||||
|
||||
int i;
|
||||
for ( i = 4; i < length && line[i] != '-'; ++i )
|
||||
;
|
||||
|
||||
if ( TCP() )
|
||||
{
|
||||
if ( length >= i )
|
||||
{
|
||||
IPAddr dst;
|
||||
|
||||
if ( is_orig )
|
||||
dst = TCP()->Orig()->dst_addr;
|
||||
else
|
||||
dst = TCP()->Resp()->dst_addr;
|
||||
|
||||
if ( Conn()->VersionFoundEvent(dst, line + i,
|
||||
length - i) )
|
||||
ProtocolConfirmation();
|
||||
else
|
||||
ProtocolViolation("malformed ssh version",
|
||||
line, length);
|
||||
}
|
||||
else
|
||||
|
||||
// If our user can authenticate via the "none" method, this
|
||||
// packet will be a SSH_MSG_USERAUTH_SUCCESS, which has a
|
||||
// fixed (decrypted) size of 8 bytes (1 for content
|
||||
// pad-aligned to 8-byte boundaries). relative_len would be
|
||||
// -16.
|
||||
if ( ! userauth_failure_size && (len + 16 == service_accept_size) )
|
||||
{
|
||||
Weird("malformed_ssh_version");
|
||||
ProtocolViolation("malformed ssh version", line, length);
|
||||
auth_decision_made = true;
|
||||
if ( ssh_auth_successful )
|
||||
BifEvent::generate_ssh_auth_successful(interp->bro_analyzer(), interp->bro_analyzer()->Conn(), true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Normally, this packet would be a SSH_MSG_USERAUTH_FAILURE
|
||||
// message, with a variable length, depending on the
|
||||
// authentication methods the server supports. If it's too
|
||||
// big, it might contain a pre-auth MOTD/banner, so we'll just
|
||||
// skip it.
|
||||
if ( ! userauth_failure_size )
|
||||
{
|
||||
if ( ! skipped_banner && (len - service_accept_size) > 256 )
|
||||
{
|
||||
skipped_banner = true;
|
||||
return;
|
||||
}
|
||||
userauth_failure_size = len;
|
||||
return;
|
||||
}
|
||||
|
||||
// If we've already seen a failure, let's see if this is
|
||||
// another packet of the same size.
|
||||
if ( len == userauth_failure_size )
|
||||
{
|
||||
if ( ssh_auth_failed )
|
||||
BifEvent::generate_ssh_auth_failed(interp->bro_analyzer(), interp->bro_analyzer()->Conn());
|
||||
return;
|
||||
}
|
||||
|
||||
// ...or a success packet.
|
||||
if ( len - service_accept_size == -16 )
|
||||
{
|
||||
auth_decision_made = true;
|
||||
if ( ssh_auth_successful )
|
||||
BifEvent::generate_ssh_auth_successful(interp->bro_analyzer(), interp->bro_analyzer()->Conn(), false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate SSH events.
|
||||
EventHandlerPtr event = is_orig ?
|
||||
ssh_client_version : ssh_server_version;
|
||||
if ( ! event )
|
||||
return;
|
||||
|
||||
val_list* vl = new val_list;
|
||||
vl->append(BuildConnVal());
|
||||
vl->append(new StringVal(length, line));
|
||||
|
||||
ConnectionEvent(event, vl);
|
||||
}
|
||||
|
|
|
@ -3,25 +3,46 @@
|
|||
#ifndef ANALYZER_PROTOCOL_SSH_SSH_H
|
||||
#define ANALYZER_PROTOCOL_SSH_SSH_H
|
||||
|
||||
#include "events.bif.h"
|
||||
|
||||
#include "analyzer/protocol/tcp/TCP.h"
|
||||
#include "analyzer/protocol/tcp/ContentLine.h"
|
||||
#include "ssh_pac.h"
|
||||
|
||||
namespace analyzer { namespace ssh {
|
||||
namespace analyzer {
|
||||
namespace SSH {
|
||||
class SSH_Analyzer : public tcp::TCP_ApplicationAnalyzer {
|
||||
|
||||
class SSH_Analyzer : public tcp::TCP_ApplicationAnalyzer {
|
||||
public:
|
||||
SSH_Analyzer(Connection* conn);
|
||||
public:
|
||||
SSH_Analyzer(Connection* conn);
|
||||
virtual ~SSH_Analyzer();
|
||||
|
||||
virtual void DeliverStream(int len, const u_char* data, bool orig);
|
||||
// Overriden from Analyzer.
|
||||
virtual void Done();
|
||||
virtual void DeliverStream(int len, const u_char* data, bool orig);
|
||||
virtual void Undelivered(uint64 seq, int len, bool orig);
|
||||
|
||||
static analyzer::Analyzer* Instantiate(Connection* conn)
|
||||
{ return new SSH_Analyzer(conn); }
|
||||
// Overriden from tcp::TCP_ApplicationAnalyzer.
|
||||
virtual void EndpointEOF(bool is_orig);
|
||||
|
||||
private:
|
||||
tcp::ContentLine_Analyzer* orig;
|
||||
tcp::ContentLine_Analyzer* resp;
|
||||
};
|
||||
static analyzer::Analyzer* Instantiate(Connection* conn)
|
||||
{ return new SSH_Analyzer(conn); }
|
||||
|
||||
} } // namespace analyzer::*
|
||||
protected:
|
||||
binpac::SSH::SSH_Conn* interp;
|
||||
|
||||
void ProcessEncrypted(int len, bool orig);
|
||||
|
||||
bool had_gap;
|
||||
|
||||
// Packet analysis stuff
|
||||
bool auth_decision_made;
|
||||
bool skipped_banner;
|
||||
|
||||
int service_accept_size;
|
||||
int userauth_failure_size;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
126
src/analyzer/protocol/ssh/consts.pac
Normal file
126
src/analyzer/protocol/ssh/consts.pac
Normal file
|
@ -0,0 +1,126 @@
|
|||
enum version {
|
||||
SSH1 = 1,
|
||||
SSH2 = 2,
|
||||
UNK = 3,
|
||||
};
|
||||
|
||||
enum state {
|
||||
VERSION_EXCHANGE = 0,
|
||||
KEX_INIT = 1,
|
||||
KEX_DH_GEX = 2,
|
||||
KEX_DH = 3,
|
||||
KEX_ECC = 4,
|
||||
KEX_GSS = 5,
|
||||
KEX_RSA = 6,
|
||||
ENCRYPTED = 7,
|
||||
};
|
||||
|
||||
# diffie-hellman-group1-sha1 [RFC4253] Section 8.1
|
||||
# diffie-hellman-group14-sha1 [RFC4253] Section 8.2
|
||||
enum KEX_DH_message_id {
|
||||
SSH_MSG_KEXDH_INIT = 30,
|
||||
SSH_MSG_KEXDH_REPLY = 31,
|
||||
};
|
||||
|
||||
# diffie-hellman-group-exchange-sha1 [RFC4419] Section 4.1
|
||||
# diffie-hellman-group-exchange-sha256 [RFC4419] Section 4.2
|
||||
enum KEX_DH_GEX_message_id {
|
||||
SSH_MSG_KEX_DH_GEX_REQUEST_OLD = 30,
|
||||
SSH_MSG_KEX_DH_GEX_GROUP = 31,
|
||||
SSH_MSG_KEX_DH_GEX_INIT = 32,
|
||||
SSH_MSG_KEX_DH_GEX_REPLY = 33,
|
||||
SSH_MSG_KEX_DH_GEX_REQUEST = 34,
|
||||
};
|
||||
|
||||
# rsa1024-sha1 [RFC4432]
|
||||
# rsa2048-sha256 [RFC4432]
|
||||
enum KEX_RSA_message_id {
|
||||
SSH_MSG_KEXRSA_PUBKEY = 30,
|
||||
SSH_MSG_KEXRSA_SECRET = 31,
|
||||
SSH_MSG_KEXRSA_DONE = 32,
|
||||
};
|
||||
|
||||
# gss-group1-sha1-* [RFC4462] Section 2.3
|
||||
# gss-group14-sha1-* [RFC4462] Section 2.4
|
||||
# gss-gex-sha1-* [RFC4462] Section 2.5
|
||||
# gss-* [RFC4462] Section 2.6
|
||||
enum KEX_GSS_message_id {
|
||||
SSH_MSG_KEXGSS_INIT = 30,
|
||||
SSH_MSG_KEXGSS_CONTINUE = 31,
|
||||
SSH_MSG_KEXGSS_COMPLETE = 32,
|
||||
SSH_MSG_KEXGSS_HOSTKEY = 33,
|
||||
SSH_MSG_KEXGSS_ERROR = 34,
|
||||
SSH_MSG_KEXGSS_GROUPREQ = 40,
|
||||
SSH_MSG_KEXGSS_GROUP = 41,
|
||||
};
|
||||
|
||||
# ecdh-sha2-* [RFC5656]
|
||||
enum KEX_ECDH_message_id {
|
||||
SSH_MSG_KEX_ECDH_INIT = 30,
|
||||
SSH_MSG_KEX_ECDH_REPLY = 31,
|
||||
};
|
||||
|
||||
# ecmqv-sha2 [RFC5656]
|
||||
enum KEX_ECMQV_message_id {
|
||||
SSH_MSG_ECMQV_INIT = 30,
|
||||
SSH_MSG_ECMQV_REPLY = 31,
|
||||
};
|
||||
|
||||
enum ssh1_message_id {
|
||||
SSH_MSG_NONE = 0,
|
||||
SSH_MSG_DISCONNECT = 1,
|
||||
SSH_SMSG_PUBLIC_KEY = 2,
|
||||
SSH_CMSG_SESSION_KEY = 3,
|
||||
SSH_CMSG_USER = 4,
|
||||
SSH_CMSG_AUTH_RHOSTS = 5,
|
||||
SSH_CMSG_AUTH_RSA = 6,
|
||||
SSH_SMSG_AUTH_RSA_CHALLENGE = 7,
|
||||
SSH_CMSG_AUTH_RSA_RESPONSE = 8,
|
||||
SSH_CMSG_AUTH_PASSWORD = 9,
|
||||
SSH_CMSG_REQUEST_PTY = 10,
|
||||
SSH_CMSG_WINDOW_SIZE = 11,
|
||||
SSH_CMSG_EXEC_SHELL = 12,
|
||||
SSH_CMSG_EXEC_CMD = 13,
|
||||
SSH_SMSG_SUCCESS = 14,
|
||||
SSH_SMSG_FAILURE = 15,
|
||||
SSH_CMSG_STDIN_DATA = 16,
|
||||
SSH_SMSG_STDOUT_DATA = 17,
|
||||
SSH_SMSG_STDERR_DATA = 18,
|
||||
SSH_CMSG_EOF = 19,
|
||||
SSH_SMSG_EXITSTATUS = 20,
|
||||
SSH_MSG_CHANNEL_OPEN_CONFIRMATION = 21,
|
||||
SSH_MSG_CHANNEL_OPEN_FAILURE = 22,
|
||||
SSH_MSG_CHANNEL_DATA = 23,
|
||||
SSH_MSG_CHANNEL_CLOSE = 24,
|
||||
SSH_MSG_CHANNEL_CLOSE_CONFIRMATION = 25,
|
||||
SSH_CMSG_X11_REQUEST_FORWARDING_OLD = 26,
|
||||
SSH_SMSG_X11_OPEN = 27,
|
||||
SSH_CMSG_PORT_FORWARD_REQUEST = 28,
|
||||
SSH_MSG_PORT_OPEN = 29,
|
||||
SSH_CMSG_AGENT_REQUEST_FORWARDING = 30,
|
||||
SSH_SMSG_AGENT_OPEN = 31,
|
||||
SSH_MSG_IGNORE = 32,
|
||||
SSH_CMSG_EXIT_CONFIRMATION = 33,
|
||||
SSH_CMSG_X11_REQUEST_FORWARDING = 34,
|
||||
SSH_CMSG_AUTH_RHOSTS_RSA = 35,
|
||||
SSH_MSG_DEBUG = 36,
|
||||
SSH_CMSG_REQUEST_COMPRESSION = 37,
|
||||
SSH_CMSG_MAX_PACKET_SIZE = 38,
|
||||
SSH_CMSG_AUTH_TIS = 39,
|
||||
SSH_SMSG_AUTH_TIS_CHALLENGE = 40,
|
||||
SSH_CMSG_AUTH_TIS_RESPONSE = 41,
|
||||
SSH_CMSG_AUTH_KERBEROS = 42,
|
||||
SSH_SMSG_AUTH_KERBEROS_RESPONSE = 43,
|
||||
SSH_CMSG_HAVE_KERBEROS_TGT = 44,
|
||||
};
|
||||
|
||||
enum ssh2_message_id {
|
||||
MSG_DISCONNECT = 1,
|
||||
MSG_IGNORE = 2,
|
||||
MSG_UNIMPLEMENTED = 3,
|
||||
MSG_DEBUG = 4,
|
||||
MSG_SERVICE_REQUEST = 5,
|
||||
MSG_SERVICE_ACCEPT = 6,
|
||||
MSG_KEXINIT = 20,
|
||||
MSG_NEWKEYS = 21,
|
||||
};
|
|
@ -1,38 +1,194 @@
|
|||
## Generated when seeing an SSH client's version identification. The SSH
|
||||
## protocol starts with a clear-text handshake message that reports client and
|
||||
## server protocol/software versions. This event provides access to what the
|
||||
## client sent.
|
||||
## An :abbr:`SSH (Secure Shell)` Protocol Version Exchange message
|
||||
## from the server. This contains an identification string that's used
|
||||
## for version identification. See :rfc:`4253#section-4.2` for
|
||||
## details.
|
||||
##
|
||||
## c: The connection over which the message was sent.
|
||||
##
|
||||
## See `Wikipedia <http://en.wikipedia.org/wiki/Secure_Shell>`__ for more
|
||||
## information about the SSH protocol.
|
||||
## version: The identification string
|
||||
##
|
||||
## c: The connection.
|
||||
##
|
||||
## version: The version string the client sent (e.g., `SSH-2.0-libssh-0.11`).
|
||||
##
|
||||
## .. bro:see:: ssh_server_version
|
||||
##
|
||||
## .. note:: As everything after the initial version handshake proceeds
|
||||
## encrypted, Bro cannot further analyze SSH sessions.
|
||||
event ssh_client_version%(c: connection, version: string%);
|
||||
|
||||
## Generated when seeing an SSH server's version identification. The SSH
|
||||
## protocol starts with a clear-text handshake message that reports client and
|
||||
## server protocol/software versions. This event provides access to what the
|
||||
## server sent.
|
||||
##
|
||||
## See `Wikipedia <http://en.wikipedia.org/wiki/Secure_Shell>`__ for more
|
||||
## information about the SSH protocol.
|
||||
##
|
||||
## c: The connection.
|
||||
##
|
||||
## version: The version string the server sent (e.g.,
|
||||
## ``SSH-1.99-OpenSSH_3.9p1``).
|
||||
##
|
||||
## .. bro:see:: ssh_client_version
|
||||
##
|
||||
## .. note:: As everything coming after the initial version handshake proceeds
|
||||
## encrypted, Bro cannot further analyze SSH sessions.
|
||||
## .. bro:see:: ssh_client_version ssh_auth_successful ssh_auth_failed
|
||||
## ssh_capabilities ssh2_server_host_key ssh1_server_host_key
|
||||
## ssh_encrypted_packet ssh2_dh_server_params
|
||||
## ssh2_gss_error ssh2_ecc_key
|
||||
event ssh_server_version%(c: connection, version: string%);
|
||||
|
||||
## An :abbr:`SSH (Secure Shell)` Protocol Version Exchange message
|
||||
## from the client. This contains an identification string that's used
|
||||
## for version identification. See :rfc:`4253#section-4.2` for
|
||||
## details.
|
||||
##
|
||||
## c: The connection over which the message was sent.
|
||||
##
|
||||
## version: The identification string
|
||||
##
|
||||
## .. bro:see:: ssh_server_version ssh_auth_successful ssh_auth_failed
|
||||
## ssh_capabilities ssh2_server_host_key ssh1_server_host_key
|
||||
## ssh_encrypted_packet ssh2_dh_server_params
|
||||
## ssh2_gss_error ssh2_ecc_key
|
||||
event ssh_client_version%(c: connection, version: string%);
|
||||
|
||||
## This event is generated when an :abbr:`SSH (Secure Shell)`
|
||||
## connection was determined to have had a successful
|
||||
## authentication. This determination is based on packet size
|
||||
## analysis, and errs on the side of caution - that is, if there's any
|
||||
## doubt about the authentication success, this event is *not* raised.
|
||||
##
|
||||
## c: The connection over which the :abbr:`SSH (Secure Shell)`
|
||||
## connection took place.
|
||||
##
|
||||
## auth_method_none: This is true if the analyzer detected a
|
||||
## successful connection before any authentication challenge. The
|
||||
## :abbr:`SSH (Secure Shell)` protocol provides a mechanism for
|
||||
## unauthenticated access, which some servers support.
|
||||
##
|
||||
## .. bro:see:: ssh_server_version ssh_client_version ssh_auth_failed
|
||||
## ssh_capabilities ssh2_server_host_key ssh1_server_host_key
|
||||
## ssh_encrypted_packet ssh2_dh_server_params
|
||||
## ssh2_gss_error ssh2_ecc_key
|
||||
event ssh_auth_successful%(c: connection, auth_method_none: bool%);
|
||||
|
||||
## This event is generated when an :abbr:`SSH (Secure Shell)`
|
||||
## connection was determined to have had a failed authentication. This
|
||||
## determination is based on packet size analysis, and errs on the
|
||||
## side of caution - that is, if there's any doubt about the
|
||||
## authentication failure, this event is *not* raised.
|
||||
##
|
||||
## c: The connection over which the :abbr:`SSH (Secure Shell)`
|
||||
## connection took place.
|
||||
##
|
||||
## .. bro:see:: ssh_server_version ssh_client_version
|
||||
## ssh_auth_successful ssh_capabilities ssh2_server_host_key
|
||||
## ssh1_server_host_key ssh_encrypted_packet ssh2_dh_server_params
|
||||
## ssh2_gss_error ssh2_ecc_key
|
||||
event ssh_auth_failed%(c: connection%);
|
||||
|
||||
## During the initial :abbr:`SSH (Secure Shell)` key exchange, each
|
||||
## endpoint lists the algorithms that it supports, in order of
|
||||
## preference. This event is generated for each endpoint, when the
|
||||
## SSH_MSG_KEXINIT message is seen. See :rfc:`4253#section-7.1` for
|
||||
## details.
|
||||
##
|
||||
## c: The connection over which the :abbr:`SSH (Secure Shell)`
|
||||
## connection took place.
|
||||
##
|
||||
## cookie: The SSH_MSG_KEXINIT cookie - a random value generated by
|
||||
## the sender.
|
||||
##
|
||||
## capabilities: The list of algorithms and languages that the sender
|
||||
## advertises support for, in order of preference.
|
||||
##
|
||||
## .. bro:see:: ssh_server_version ssh_client_version
|
||||
## ssh_auth_successful ssh_auth_failed ssh2_server_host_key
|
||||
## ssh1_server_host_key ssh_encrypted_packet ssh2_dh_server_params
|
||||
## ssh2_gss_error ssh2_ecc_key
|
||||
event ssh_capabilities%(c: connection, cookie: string, capabilities: SSH::Capabilities%);
|
||||
|
||||
## During the :abbr:`SSH (Secure Shell)` key exchange, the server
|
||||
## supplies its public host key. This event is generated when the
|
||||
## appropriate key exchange message is seen for SSH2.
|
||||
##
|
||||
## c: The connection over which the :abbr:`SSH (Secure Shell)`
|
||||
## connection took place.
|
||||
##
|
||||
## key: The server's public host key. Note that this is the public key
|
||||
## itself, and not just the fingerprint or hash.
|
||||
##
|
||||
## .. bro:see:: ssh_server_version ssh_client_version
|
||||
## ssh_auth_successful ssh_auth_failed ssh_capabilities
|
||||
## ssh1_server_host_key ssh_encrypted_packet ssh2_dh_server_params
|
||||
## ssh2_gss_error ssh2_ecc_key
|
||||
event ssh2_server_host_key%(c: connection, key: string%);
|
||||
|
||||
## During the :abbr:`SSH (Secure Shell)` key exchange, the server
|
||||
## supplies its public host key. This event is generated when the
|
||||
## appropriate key exchange message is seen for SSH1.
|
||||
##
|
||||
## c: The connection over which the :abbr:`SSH (Secure Shell)`
|
||||
## connection took place.
|
||||
##
|
||||
## p: The prime for the server's public host key.
|
||||
##
|
||||
## e: The exponent for the serer's public host key.
|
||||
##
|
||||
## .. bro:see:: ssh_server_version ssh_client_version
|
||||
## ssh_auth_successful ssh_auth_failed ssh_capabilities
|
||||
## ssh2_server_host_key ssh_encrypted_packet ssh2_dh_server_params
|
||||
## ssh2_gss_error ssh2_ecc_key
|
||||
event ssh1_server_host_key%(c: connection, p: string, e: string%);
|
||||
|
||||
## This event is generated when an :abbr:`SSH (Secure Shell)`
|
||||
## encrypted packet is seen. This event is not handled by default, but
|
||||
## is provided for heuristic analysis scripts. Note that you have to set
|
||||
## :bro:id:`SSH::skip_processing_after_detection` to false to use this
|
||||
## event. This carries a performance penalty.
|
||||
##
|
||||
## c: The connection over which the :abbr:`SSH (Secure Shell)`
|
||||
## connection took place.
|
||||
##
|
||||
## orig: Whether the packet was sent by the originator of the TCP
|
||||
## connection.
|
||||
##
|
||||
## len: The length of the :abbr:`SSH (Secure Shell)` payload, in
|
||||
## bytes. Note that this ignores reassembly, as this is unknown.
|
||||
##
|
||||
## .. bro:see:: ssh_server_version ssh_client_version
|
||||
## ssh_auth_successful ssh_auth_failed ssh_capabilities
|
||||
## ssh2_server_host_key ssh1_server_host_key ssh2_dh_server_params
|
||||
## ssh2_gss_error ssh2_ecc_key
|
||||
event ssh_encrypted_packet%(c: connection, orig: bool, len: count%);
|
||||
|
||||
## Generated if the connection uses a Diffie-Hellman Group Exchange
|
||||
## key exchange method. This event contains the server DH parameters,
|
||||
## which are sent in the SSH_MSG_KEY_DH_GEX_GROUP message as defined in
|
||||
## :rfc:`4419#section-3`.
|
||||
##
|
||||
## c: The connection.
|
||||
##
|
||||
## p: The DH prime modulus.
|
||||
##
|
||||
## q: The DH generator.
|
||||
##
|
||||
## .. bro:see:: ssl_dh_server_params ssh_server_version
|
||||
## ssh_client_version ssh_auth_successful ssh_auth_failed
|
||||
## ssh_capabilities ssh2_server_host_key ssh1_server_host_key
|
||||
## ssh_encrypted_packet ssh2_gss_error ssh2_ecc_key
|
||||
event ssh2_dh_server_params%(c: connection, p: string, q: string%);
|
||||
|
||||
## In the event of a GSS-API error on the server, the server MAY send
|
||||
## send an error message with some additional details. This event is
|
||||
## generated when such an error message is seen. For more information,
|
||||
## see :rfc:`4462#section-2.1`.
|
||||
##
|
||||
## c: The connection.
|
||||
##
|
||||
## major_status: GSS-API major status code.
|
||||
##
|
||||
## minor_status: GSS-API minor status code.
|
||||
##
|
||||
## err_msg: Detailed human-readable error message
|
||||
##
|
||||
## .. bro:see:: ssh_server_version ssh_client_version
|
||||
## ssh_auth_successful ssh_auth_failed ssh_capabilities
|
||||
## ssh2_server_host_key ssh1_server_host_key ssh_encrypted_packet
|
||||
## ssh2_dh_server_params ssh2_ecc_key
|
||||
event ssh2_gss_error%(c: connection, major_status: count, minor_status: count, err_msg: string%);
|
||||
|
||||
## The :abbr:`ECDH (Elliptic Curve Diffie-Hellman)` and
|
||||
## :abbr:`ECMQV (Elliptic Curve Menezes-Qu-Vanstone)` key exchange
|
||||
## algorithms use two ephemeral key pairs to generate a shared
|
||||
## secret. This event is generated when either the client's or
|
||||
## server's ephemeral public key is seen. For more information, see:
|
||||
## :rfc:`5656#section-4`.
|
||||
##
|
||||
## c: The connection
|
||||
##
|
||||
## is_orig: Did this message come from the originator?
|
||||
##
|
||||
## q: The ephemeral public key
|
||||
##
|
||||
## .. bro:see:: ssh_server_version ssh_client_version
|
||||
## ssh_auth_successful ssh_auth_failed ssh_capabilities
|
||||
## ssh2_server_host_key ssh1_server_host_key ssh_encrypted_packet
|
||||
## ssh2_dh_server_params ssh2_gss_error
|
||||
event ssh2_ecc_key%(c: connection, is_orig: bool, q: string%);
|
||||
|
|
221
src/analyzer/protocol/ssh/ssh-analyzer.pac
Normal file
221
src/analyzer/protocol/ssh/ssh-analyzer.pac
Normal file
|
@ -0,0 +1,221 @@
|
|||
%extern{
|
||||
#include <cstdlib>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
%}
|
||||
|
||||
%header{
|
||||
VectorVal* name_list_to_vector(const bytestring nl);
|
||||
%}
|
||||
|
||||
%code{
|
||||
// Copied from IRC_Analyzer::SplitWords
|
||||
VectorVal* name_list_to_vector(const bytestring nl)
|
||||
{
|
||||
VectorVal* vv = new VectorVal(internal_type("string_vec")->AsVectorType());
|
||||
|
||||
string name_list = std_str(nl);
|
||||
if ( name_list.size() < 1 )
|
||||
return vv;
|
||||
|
||||
unsigned int start = 0;
|
||||
unsigned int split_pos = 0;
|
||||
|
||||
while ( name_list[start] == ',' )
|
||||
{
|
||||
++start;
|
||||
++split_pos;
|
||||
}
|
||||
|
||||
string word;
|
||||
while ( (split_pos = name_list.find(',', start)) < name_list.size() )
|
||||
{
|
||||
word = name_list.substr(start, split_pos - start);
|
||||
if ( word.size() > 0 && word[0] != ',' )
|
||||
vv->Assign(vv->Size(), new StringVal(word));
|
||||
|
||||
start = split_pos + 1;
|
||||
}
|
||||
|
||||
// Add line end if needed.
|
||||
if ( start < name_list.size() )
|
||||
{
|
||||
word = name_list.substr(start, name_list.size() - start);
|
||||
vv->Assign(vv->Size(), new StringVal(word));
|
||||
}
|
||||
return vv;
|
||||
}
|
||||
%}
|
||||
|
||||
refine flow SSH_Flow += {
|
||||
function proc_ssh_version(msg: SSH_Version): bool
|
||||
%{
|
||||
if ( ssh_client_version && ${msg.is_orig } )
|
||||
{
|
||||
BifEvent::generate_ssh_client_version(connection()->bro_analyzer(),
|
||||
connection()->bro_analyzer()->Conn(),
|
||||
bytestring_to_val(${msg.version}));
|
||||
}
|
||||
else if ( ssh_server_version )
|
||||
{
|
||||
BifEvent::generate_ssh_server_version(connection()->bro_analyzer(),
|
||||
connection()->bro_analyzer()->Conn(),
|
||||
bytestring_to_val(${msg.version}));
|
||||
}
|
||||
return true;
|
||||
%}
|
||||
|
||||
function proc_ssh2_kexinit(msg: SSH2_KEXINIT): bool
|
||||
%{
|
||||
if ( ! ssh_capabilities )
|
||||
return false;
|
||||
|
||||
RecordVal* result = new RecordVal(BifType::Record::SSH::Capabilities);
|
||||
result->Assign(0, name_list_to_vector(${msg.kex_algorithms.val}));
|
||||
result->Assign(1, name_list_to_vector(${msg.server_host_key_algorithms.val}));
|
||||
|
||||
RecordVal* encryption_algs = new RecordVal(BifType::Record::SSH::Algorithm_Prefs);
|
||||
encryption_algs->Assign(0, name_list_to_vector(${msg.encryption_algorithms_client_to_server.val}));
|
||||
encryption_algs->Assign(1, name_list_to_vector(${msg.encryption_algorithms_server_to_client.val}));
|
||||
result->Assign(2, encryption_algs);
|
||||
|
||||
RecordVal* mac_algs = new RecordVal(BifType::Record::SSH::Algorithm_Prefs);
|
||||
mac_algs->Assign(0, name_list_to_vector(${msg.mac_algorithms_client_to_server.val}));
|
||||
mac_algs->Assign(1, name_list_to_vector(${msg.mac_algorithms_server_to_client.val}));
|
||||
result->Assign(3, mac_algs);
|
||||
|
||||
RecordVal* compression_algs = new RecordVal(BifType::Record::SSH::Algorithm_Prefs);
|
||||
compression_algs->Assign(0, name_list_to_vector(${msg.compression_algorithms_client_to_server.val}));
|
||||
compression_algs->Assign(1, name_list_to_vector(${msg.compression_algorithms_server_to_client.val}));
|
||||
result->Assign(4, compression_algs);
|
||||
|
||||
if ( ${msg.languages_client_to_server.len} || ${msg.languages_server_to_client.len} )
|
||||
{
|
||||
RecordVal* languages = new RecordVal(BifType::Record::SSH::Algorithm_Prefs);
|
||||
if ( ${msg.languages_client_to_server.len} )
|
||||
languages->Assign(0, name_list_to_vector(${msg.languages_client_to_server.val}));
|
||||
if ( ${msg.languages_server_to_client.len} )
|
||||
languages->Assign(1, name_list_to_vector(${msg.languages_server_to_client.val}));
|
||||
|
||||
result->Assign(5, languages);
|
||||
}
|
||||
|
||||
|
||||
result->Assign(6, new Val(${msg.is_orig}, TYPE_BOOL));
|
||||
|
||||
BifEvent::generate_ssh_capabilities(connection()->bro_analyzer(),
|
||||
connection()->bro_analyzer()->Conn(), bytestring_to_val(${msg.cookie}),
|
||||
result);
|
||||
|
||||
return true;
|
||||
%}
|
||||
|
||||
|
||||
function proc_ssh2_dh_gex_group(msg: SSH2_DH_GEX_GROUP): bool
|
||||
%{
|
||||
if ( ssh2_dh_server_params )
|
||||
{
|
||||
BifEvent::generate_ssh2_dh_server_params(connection()->bro_analyzer(),
|
||||
connection()->bro_analyzer()->Conn(),
|
||||
bytestring_to_val(${msg.p.val}), bytestring_to_val(${msg.g.val}));
|
||||
}
|
||||
return true;
|
||||
%}
|
||||
|
||||
function proc_ssh2_ecc_key(q: bytestring, is_orig: bool): bool
|
||||
%{
|
||||
if ( ssh2_ecc_key )
|
||||
{
|
||||
BifEvent::generate_ssh2_ecc_key(connection()->bro_analyzer(),
|
||||
connection()->bro_analyzer()->Conn(),
|
||||
is_orig, bytestring_to_val(q));
|
||||
}
|
||||
return true;
|
||||
%}
|
||||
|
||||
function proc_ssh2_gss_error(msg: SSH2_GSS_ERROR): bool
|
||||
%{
|
||||
if ( ssh2_gss_error )
|
||||
{
|
||||
BifEvent::generate_ssh2_gss_error(connection()->bro_analyzer(),
|
||||
connection()->bro_analyzer()->Conn(),
|
||||
${msg.major_status}, ${msg.minor_status},
|
||||
bytestring_to_val(${msg.message.val}));
|
||||
}
|
||||
return true;
|
||||
%}
|
||||
|
||||
function proc_ssh2_server_host_key(key: bytestring): bool
|
||||
%{
|
||||
if ( ssh2_server_host_key )
|
||||
{
|
||||
BifEvent::generate_ssh2_server_host_key(connection()->bro_analyzer(),
|
||||
connection()->bro_analyzer()->Conn(),
|
||||
bytestring_to_val(${key}));
|
||||
}
|
||||
return true;
|
||||
%}
|
||||
|
||||
function proc_ssh1_server_host_key(p: bytestring, e: bytestring): bool
|
||||
%{
|
||||
if ( ssh1_server_host_key )
|
||||
{
|
||||
BifEvent::generate_ssh1_server_host_key(connection()->bro_analyzer(),
|
||||
connection()->bro_analyzer()->Conn(),
|
||||
bytestring_to_val(${p}),
|
||||
bytestring_to_val(${e}));
|
||||
}
|
||||
return true;
|
||||
%}
|
||||
|
||||
function proc_newkeys(): bool
|
||||
%{
|
||||
connection()->bro_analyzer()->ProtocolConfirmation();
|
||||
return true;
|
||||
%}
|
||||
};
|
||||
|
||||
refine typeattr SSH_Version += &let {
|
||||
proc: bool = $context.flow.proc_ssh_version(this);
|
||||
};
|
||||
|
||||
refine typeattr SSH2_KEXINIT += &let {
|
||||
proc: bool = $context.flow.proc_ssh2_kexinit(this);
|
||||
};
|
||||
|
||||
refine typeattr SSH1_Message += &let {
|
||||
proc_newkeys: bool = $context.flow.proc_newkeys() &if(msg_type == SSH_CMSG_SESSION_KEY);
|
||||
};
|
||||
|
||||
refine typeattr SSH2_Message += &let {
|
||||
proc_newkeys: bool = $context.flow.proc_newkeys() &if(msg_type == MSG_NEWKEYS);
|
||||
};
|
||||
|
||||
refine typeattr SSH2_DH_GEX_REPLY += &let {
|
||||
proc: bool = $context.flow.proc_ssh2_server_host_key(k_s.val);
|
||||
};
|
||||
|
||||
refine typeattr SSH2_GSS_HOSTKEY += &let {
|
||||
proc: bool = $context.flow.proc_ssh2_server_host_key(k_s.val);
|
||||
};
|
||||
|
||||
refine typeattr SSH2_GSS_ERROR += &let {
|
||||
proc: bool = $context.flow.proc_ssh2_gss_error(this);
|
||||
};
|
||||
|
||||
refine typeattr SSH2_DH_GEX_GROUP += &let {
|
||||
proc: bool = $context.flow.proc_ssh2_dh_gex_group(this);
|
||||
};
|
||||
|
||||
refine typeattr SSH2_ECC_REPLY += &let {
|
||||
proc_k: bool = $context.flow.proc_ssh2_server_host_key(k_s.val);
|
||||
proc_q: bool = $context.flow.proc_ssh2_ecc_key(q_s.val, false);
|
||||
};
|
||||
|
||||
refine typeattr SSH2_ECC_INIT += &let {
|
||||
proc: bool = $context.flow.proc_ssh2_ecc_key(q_c.val, true);
|
||||
};
|
||||
|
||||
refine typeattr SSH1_PUBLIC_KEY += &let {
|
||||
proc: bool = $context.flow.proc_ssh1_server_host_key(host_key_p.val, host_key_e.val);
|
||||
};
|
430
src/analyzer/protocol/ssh/ssh-protocol.pac
Normal file
430
src/analyzer/protocol/ssh/ssh-protocol.pac
Normal file
|
@ -0,0 +1,430 @@
|
|||
%include consts.pac
|
||||
|
||||
# Common constructs across SSH1 and SSH2
|
||||
########################################
|
||||
|
||||
# We have 3 basic types of messages:
|
||||
#
|
||||
# - SSH_Version messages just have a string with the banner string of the client or server
|
||||
# - Encrypted messages have no usable data, so we'll just ignore them as best we can.
|
||||
# - Finally, key exchange messages have a common format.
|
||||
|
||||
type SSH_PDU(is_orig: bool) = case $context.connection.get_state(is_orig) of {
|
||||
VERSION_EXCHANGE -> version : SSH_Version(is_orig);
|
||||
ENCRYPTED -> encrypted : bytestring &length=1 &transient;
|
||||
default -> kex : SSH_Key_Exchange(is_orig);
|
||||
} &byteorder=bigendian;
|
||||
|
||||
type SSH_Version(is_orig: bool) = record {
|
||||
version : bytestring &oneline;
|
||||
} &let {
|
||||
update_state : bool = $context.connection.update_state(KEX_INIT, is_orig);
|
||||
update_version : bool = $context.connection.update_version(version, is_orig);
|
||||
};
|
||||
|
||||
type SSH_Key_Exchange(is_orig: bool) = case $context.connection.get_version() of {
|
||||
SSH1 -> ssh1_msg : SSH1_Key_Exchange(is_orig);
|
||||
SSH2 -> ssh2_msg : SSH2_Key_Exchange(is_orig);
|
||||
};
|
||||
|
||||
# SSH1 constructs
|
||||
#################
|
||||
|
||||
type SSH1_Key_Exchange(is_orig: bool) = record {
|
||||
packet_length : uint32;
|
||||
pad_fill : bytestring &length = 8 - (packet_length % 8);
|
||||
msg_type : uint8;
|
||||
message : SSH1_Message(is_orig, msg_type, packet_length - 5);
|
||||
crc : uint32;
|
||||
} &length = packet_length + 4 + 8 - (packet_length % 8);
|
||||
|
||||
type SSH1_Message(is_orig: bool, msg_type: uint8, length: uint32) = case msg_type of {
|
||||
SSH_SMSG_PUBLIC_KEY -> public_key : SSH1_PUBLIC_KEY(length);
|
||||
SSH_CMSG_SESSION_KEY -> session_key : SSH1_SESSION_KEY(length);
|
||||
} &let {
|
||||
detach : bool=$context.connection.update_state(ENCRYPTED, is_orig);
|
||||
};
|
||||
|
||||
type SSH1_PUBLIC_KEY(length: uint32) = record {
|
||||
cookie : bytestring &length=8;
|
||||
server_key : uint32;
|
||||
server_key_p : ssh1_mp_int;
|
||||
server_key_e : ssh1_mp_int;
|
||||
host_key : uint32;
|
||||
host_key_p : ssh1_mp_int;
|
||||
host_key_e : ssh1_mp_int;
|
||||
flags : uint32;
|
||||
supported_ciphers : uint32;
|
||||
supported_auths : uint32;
|
||||
} &length=length;
|
||||
|
||||
type SSH1_SESSION_KEY(length: uint32) = record {
|
||||
cipher : uint8;
|
||||
cookie : bytestring &length=8;
|
||||
session_key : ssh1_mp_int;
|
||||
flags : uint32;
|
||||
} &length=length;
|
||||
|
||||
type ssh1_mp_int = record {
|
||||
len : uint16;
|
||||
val : bytestring &length=(len+7)/8;
|
||||
};
|
||||
|
||||
|
||||
## SSH2
|
||||
|
||||
type SSH2_Header(is_orig: bool) = record {
|
||||
packet_length : uint32;
|
||||
padding_length : uint8;
|
||||
msg_type : uint8;
|
||||
} &let {
|
||||
payload_length : uint32 = packet_length - padding_length - 2;
|
||||
detach : bool = $context.connection.update_state(ENCRYPTED, is_orig) &if(msg_type == MSG_NEWKEYS);
|
||||
};
|
||||
|
||||
type SSH2_Key_Exchange(is_orig: bool) = record {
|
||||
header : SSH2_Header(is_orig);
|
||||
payload : SSH2_Message(is_orig, header.msg_type, header.payload_length);
|
||||
pad : bytestring &length=header.padding_length;
|
||||
} &length=header.packet_length + 4;
|
||||
|
||||
type SSH2_Message(is_orig: bool, msg_type: uint8, length: uint32) = case $context.connection.get_state(is_orig) of {
|
||||
KEX_INIT -> kex : SSH2_KEXINIT(length, is_orig);
|
||||
KEX_DH_GEX -> kex_dh_gex : SSH2_Key_Exchange_DH_GEX_Message(is_orig, msg_type, length);
|
||||
KEX_DH -> kex_dh : SSH2_Key_Exchange_DH_Message(is_orig, msg_type, length);
|
||||
KEX_ECC -> kex_ecc : SSH2_Key_Exchange_ECC_Message(is_orig, msg_type, length);
|
||||
KEX_GSS -> kex_gss : SSH2_Key_Exchange_GSS_Message(is_orig, msg_type, length);
|
||||
KEX_RSA -> kex_rsa : SSH2_Key_Exchange_RSA_Message(is_orig, msg_type, length);
|
||||
default -> unknown : bytestring &length=length;
|
||||
};
|
||||
|
||||
type SSH2_KEXINIT(length: uint32, is_orig: bool) = record {
|
||||
cookie : bytestring &length=16;
|
||||
kex_algorithms : ssh_string;
|
||||
server_host_key_algorithms : ssh_string;
|
||||
encryption_algorithms_client_to_server : ssh_string;
|
||||
encryption_algorithms_server_to_client : ssh_string;
|
||||
mac_algorithms_client_to_server : ssh_string;
|
||||
mac_algorithms_server_to_client : ssh_string;
|
||||
compression_algorithms_client_to_server : ssh_string;
|
||||
compression_algorithms_server_to_client : ssh_string;
|
||||
languages_client_to_server : ssh_string;
|
||||
languages_server_to_client : ssh_string;
|
||||
first_kex_packet_follows : uint8;
|
||||
reserved : uint32;
|
||||
} &let {
|
||||
proc_kex : bool= $context.connection.update_kex(kex_algorithms.val, is_orig);
|
||||
} &length=length;
|
||||
|
||||
# KEX_DH exchanges
|
||||
|
||||
type SSH2_Key_Exchange_DH_Message(is_orig: bool, msg_type: uint8, length: uint32) = case msg_type of {
|
||||
SSH_MSG_KEXDH_INIT -> init : SSH2_DH_GEX_INIT(length);
|
||||
SSH_MSG_KEXDH_REPLY -> reply : SSH2_DH_GEX_REPLY(length);
|
||||
default -> unknown: bytestring &length=length &transient;
|
||||
};
|
||||
|
||||
# KEX_DH_GEX exchanges
|
||||
|
||||
type SSH2_Key_Exchange_DH_GEX_Message(is_orig: bool, msg_type: uint8, length: uint32) = case msg_type of {
|
||||
SSH_MSG_KEX_DH_GEX_REQUEST_OLD -> request_old : SSH2_DH_GEX_REQUEST_OLD;
|
||||
SSH_MSG_KEX_DH_GEX_REQUEST -> request : SSH2_DH_GEX_REQUEST;
|
||||
SSH_MSG_KEX_DH_GEX_GROUP -> group : SSH2_DH_GEX_GROUP(length);
|
||||
SSH_MSG_KEX_DH_GEX_INIT -> init : SSH2_DH_GEX_INIT(length);
|
||||
SSH_MSG_KEX_DH_GEX_REPLY -> reply : SSH2_DH_GEX_REPLY(length);
|
||||
default -> unknown : bytestring &length=length &transient;
|
||||
};
|
||||
|
||||
type SSH2_DH_GEX_REQUEST = record {
|
||||
min : uint32;
|
||||
n : uint32;
|
||||
max : uint32;
|
||||
} &length=12;
|
||||
|
||||
type SSH2_DH_GEX_REQUEST_OLD = record {
|
||||
n : uint32;
|
||||
} &length=4;
|
||||
|
||||
type SSH2_DH_GEX_GROUP(length: uint32) = record {
|
||||
p : ssh_string;
|
||||
g : ssh_string;
|
||||
} &length=length;
|
||||
|
||||
type SSH2_DH_GEX_INIT(length: uint32) = record {
|
||||
e : ssh_string;
|
||||
} &length=length;
|
||||
|
||||
type SSH2_DH_GEX_REPLY(length: uint32) = record {
|
||||
k_s : ssh_string;
|
||||
f : ssh_string;
|
||||
signature : ssh_string;
|
||||
} &length=length;
|
||||
|
||||
# KEX_RSA exchanges
|
||||
|
||||
type SSH2_Key_Exchange_RSA_Message(is_orig: bool, msg_type: uint8, length: uint32) = case msg_type of {
|
||||
SSH_MSG_KEXRSA_PUBKEY -> pubkey : SSH2_RSA_PUBKEY(length);
|
||||
SSH_MSG_KEXRSA_SECRET -> secret : SSH2_RSA_SECRET(length);
|
||||
SSH_MSG_KEXRSA_DONE -> done : SSH2_RSA_DONE(length);
|
||||
};
|
||||
|
||||
type SSH2_RSA_PUBKEY(length: uint32) = record {
|
||||
k_s : ssh_string;
|
||||
k_t : ssh_string;
|
||||
} &length=length;
|
||||
|
||||
type SSH2_RSA_SECRET(length: uint32) = record {
|
||||
encrypted_payload : ssh_string;
|
||||
} &length=length;
|
||||
|
||||
type SSH2_RSA_DONE(length: uint32) = record {
|
||||
signature : ssh_string;
|
||||
} &length=length;
|
||||
|
||||
# KEX_GSS exchanges
|
||||
|
||||
type SSH2_Key_Exchange_GSS_Message(is_orig: bool, msg_type: uint8, length: uint32) = case msg_type of {
|
||||
SSH_MSG_KEXGSS_INIT -> init : SSH2_GSS_INIT(length);
|
||||
SSH_MSG_KEXGSS_CONTINUE -> cont : SSH2_GSS_CONTINUE(length);
|
||||
SSH_MSG_KEXGSS_COMPLETE -> complete : SSH2_GSS_COMPLETE(length);
|
||||
SSH_MSG_KEXGSS_HOSTKEY -> hostkey : SSH2_GSS_HOSTKEY(length);
|
||||
SSH_MSG_KEXGSS_ERROR -> error : SSH2_GSS_ERROR(length);
|
||||
SSH_MSG_KEXGSS_GROUPREQ -> groupreq : SSH2_DH_GEX_REQUEST;
|
||||
SSH_MSG_KEXGSS_GROUP -> group : SSH2_DH_GEX_GROUP(length);
|
||||
};
|
||||
|
||||
type SSH2_GSS_INIT(length: uint32) = record {
|
||||
output_token : ssh_string;
|
||||
e : ssh_string;
|
||||
} &length=length;
|
||||
|
||||
type SSH2_GSS_CONTINUE(length: uint32) = record {
|
||||
output_token : ssh_string;
|
||||
} &length=length;
|
||||
|
||||
type SSH2_GSS_COMPLETE(length: uint32) = record {
|
||||
f : ssh_string;
|
||||
per_msg_token : ssh_string;
|
||||
have_token : uint8;
|
||||
parse_token : case have_token of {
|
||||
0 -> no_token : empty;
|
||||
default -> token : ssh_string;
|
||||
};
|
||||
} &length=length;
|
||||
|
||||
type SSH2_GSS_HOSTKEY(length: uint32) = record {
|
||||
k_s : ssh_string;
|
||||
} &length=length;
|
||||
|
||||
type SSH2_GSS_ERROR(length: uint32) = record {
|
||||
major_status : uint32;
|
||||
minor_status : uint32;
|
||||
message : ssh_string;
|
||||
language : ssh_string;
|
||||
} &length=length;
|
||||
|
||||
# KEX_ECDH and KEX_ECMQV exchanges
|
||||
|
||||
type SSH2_Key_Exchange_ECC_Message(is_orig: bool, msg_type: uint8, length: uint32) = case msg_type of {
|
||||
SSH_MSG_KEX_ECDH_INIT -> init : SSH2_ECC_INIT(length);
|
||||
SSH_MSG_KEX_ECDH_REPLY -> reply : SSH2_ECC_REPLY(length);
|
||||
};
|
||||
|
||||
# This deviates from the RFC. SSH_MSG_KEX_ECDH_INIT and
|
||||
# SSH_MSG_KEX_ECMQV_INIT can be parsed the same way.
|
||||
type SSH2_ECC_INIT(length: uint32) = record {
|
||||
q_c : ssh_string;
|
||||
};
|
||||
|
||||
# This deviates from the RFC. SSH_MSG_KEX_ECDH_REPLY and
|
||||
# SSH_MSG_KEX_ECMQV_REPLY can be parsed the same way.
|
||||
type SSH2_ECC_REPLY(length: uint32) = record {
|
||||
k_s : ssh_string;
|
||||
q_s : ssh_string;
|
||||
signature : ssh_string;
|
||||
};
|
||||
|
||||
# Helper types
|
||||
|
||||
type ssh_string = record {
|
||||
len : uint32;
|
||||
val : bytestring &length=len;
|
||||
};
|
||||
|
||||
type ssh_host_key = record {
|
||||
len : uint32;
|
||||
key_type : ssh_string;
|
||||
key : ssh_string;
|
||||
} &length=(len + 4);
|
||||
|
||||
## Done with types
|
||||
|
||||
refine connection SSH_Conn += {
|
||||
%member{
|
||||
int state_up_;
|
||||
int state_down_;
|
||||
int version_;
|
||||
|
||||
bool kex_orig_;
|
||||
bool kex_seen_;
|
||||
bytestring kex_algs_cache_;
|
||||
bytestring kex_algorithm_;
|
||||
%}
|
||||
|
||||
%init{
|
||||
state_up_ = VERSION_EXCHANGE;
|
||||
state_down_ = VERSION_EXCHANGE;
|
||||
version_ = UNK;
|
||||
|
||||
kex_seen_ = false;
|
||||
kex_orig_ = false;
|
||||
kex_algs_cache_ = bytestring();
|
||||
kex_algorithm_ = bytestring();
|
||||
%}
|
||||
|
||||
%cleanup{
|
||||
kex_algorithm_.free();
|
||||
kex_algs_cache_.free();
|
||||
%}
|
||||
|
||||
function get_state(is_orig: bool) : int
|
||||
%{
|
||||
if ( is_orig )
|
||||
{
|
||||
return state_up_;
|
||||
}
|
||||
else
|
||||
{
|
||||
return state_down_;
|
||||
}
|
||||
%}
|
||||
|
||||
function update_state(s: state, is_orig: bool) : bool
|
||||
%{
|
||||
if ( is_orig )
|
||||
state_up_ = s;
|
||||
else
|
||||
state_down_ = s;
|
||||
return true;
|
||||
%}
|
||||
|
||||
function get_version() : int
|
||||
%{
|
||||
return version_;
|
||||
%}
|
||||
|
||||
function update_version(v: bytestring, is_orig: bool) : bool
|
||||
%{
|
||||
if ( is_orig && ( v.length() >= 4 ) )
|
||||
{
|
||||
if ( v[4] == '2' )
|
||||
version_ = SSH2;
|
||||
if ( v[4] == '1' )
|
||||
version_ = SSH1;
|
||||
}
|
||||
return true;
|
||||
%}
|
||||
|
||||
function update_kex_state_if_equal(s: string, to_state: state) : bool
|
||||
%{
|
||||
if ( std_str(kex_algorithm_).compare(s) == 0 )
|
||||
{
|
||||
update_state(to_state, true);
|
||||
update_state(to_state, false);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
%}
|
||||
|
||||
function update_kex_state_if_startswith(s: string, to_state: state) : bool
|
||||
%{
|
||||
if ( (uint) kex_algorithm_.length() < s.length() )
|
||||
return false;
|
||||
|
||||
if ( std_str(kex_algorithm_).substr(0, s.length()).compare(s) == 0 )
|
||||
{
|
||||
update_state(to_state, true);
|
||||
update_state(to_state, false);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
%}
|
||||
|
||||
function update_kex(algs: bytestring, orig: bool) : bool
|
||||
%{
|
||||
if ( !kex_seen_ )
|
||||
{
|
||||
kex_seen_ = true;
|
||||
kex_orig_ = orig;
|
||||
kex_algs_cache_.init(${algs}.data(), ${algs}.length());
|
||||
|
||||
return false;
|
||||
}
|
||||
else if ( kex_orig_ == orig )
|
||||
return false;
|
||||
|
||||
VectorVal* client_list = name_list_to_vector(orig ? algs : kex_algs_cache_);
|
||||
VectorVal* server_list = name_list_to_vector(orig ? kex_algs_cache_ : algs);
|
||||
|
||||
for ( unsigned int i = 0; i < client_list->Size(); ++i )
|
||||
{
|
||||
for ( unsigned int j = 0; j < server_list->Size(); ++j )
|
||||
{
|
||||
if ( *(client_list->Lookup(i)->AsStringVal()->AsString()) == *(server_list->Lookup(j)->AsStringVal()->AsString()) )
|
||||
{
|
||||
kex_algorithm_.init((const uint8 *) client_list->Lookup(i)->AsStringVal()->Bytes(),
|
||||
client_list->Lookup(i)->AsStringVal()->Len());
|
||||
|
||||
Unref(client_list);
|
||||
Unref(server_list);
|
||||
|
||||
// UNTESTED
|
||||
if ( update_kex_state_if_equal("rsa1024-sha1", KEX_RSA) )
|
||||
return true;
|
||||
// UNTESTED
|
||||
if ( update_kex_state_if_equal("rsa2048-sha256", KEX_RSA) )
|
||||
return true;
|
||||
|
||||
// UNTESTED
|
||||
if ( update_kex_state_if_equal("diffie-hellman-group1-sha1", KEX_DH) )
|
||||
return true;
|
||||
// UNTESTED
|
||||
if ( update_kex_state_if_equal("diffie-hellman-group14-sha1", KEX_DH) )
|
||||
return true;
|
||||
|
||||
if ( update_kex_state_if_equal("diffie-hellman-group-exchange-sha1", KEX_DH_GEX) )
|
||||
return true;
|
||||
if ( update_kex_state_if_equal("diffie-hellman-group-exchange-sha256", KEX_DH_GEX) )
|
||||
return true;
|
||||
|
||||
if ( update_kex_state_if_startswith("gss-group1-sha1-", KEX_GSS) )
|
||||
return true;
|
||||
if ( update_kex_state_if_startswith("gss-group14-sha1-", KEX_GSS) )
|
||||
return true;
|
||||
if ( update_kex_state_if_startswith("gss-gex-sha1-", KEX_GSS) )
|
||||
return true;
|
||||
if ( update_kex_state_if_startswith("gss-", KEX_GSS) )
|
||||
return true;
|
||||
|
||||
if ( update_kex_state_if_startswith("ecdh-sha2-", KEX_ECC) )
|
||||
return true;
|
||||
if ( update_kex_state_if_equal("ecmqv-sha2", KEX_ECC) )
|
||||
return true;
|
||||
if ( update_kex_state_if_equal("curve25519-sha256@libssh.org", KEX_ECC) )
|
||||
return true;
|
||||
|
||||
|
||||
bro_analyzer()->Weird(fmt("ssh_unknown_kex_algorithm=%s", c_str(kex_algorithm_)));
|
||||
return true;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Unref(client_list);
|
||||
Unref(server_list);
|
||||
|
||||
return true;
|
||||
%}
|
||||
|
||||
};
|
33
src/analyzer/protocol/ssh/ssh.pac
Normal file
33
src/analyzer/protocol/ssh/ssh.pac
Normal file
|
@ -0,0 +1,33 @@
|
|||
# Generated by binpac_quickstart
|
||||
|
||||
# Analyzer for Secure Shell
|
||||
# - ssh-protocol.pac: describes the SSH protocol messages
|
||||
# - ssh-analyzer.pac: describes the SSH analyzer code
|
||||
|
||||
%include binpac.pac
|
||||
%include bro.pac
|
||||
|
||||
%extern{
|
||||
#include "types.bif.h"
|
||||
#include "events.bif.h"
|
||||
%}
|
||||
|
||||
analyzer SSH withcontext {
|
||||
connection: SSH_Conn;
|
||||
flow: SSH_Flow;
|
||||
};
|
||||
|
||||
# Our connection consists of two flows, one in each direction.
|
||||
connection SSH_Conn(bro_analyzer: BroAnalyzer) {
|
||||
upflow = SSH_Flow(true);
|
||||
downflow = SSH_Flow(false);
|
||||
};
|
||||
|
||||
%include ssh-protocol.pac
|
||||
|
||||
# Now we define the flow:
|
||||
flow SSH_Flow(is_orig: bool) {
|
||||
flowunit = SSH_PDU(is_orig) withcontext(connection, this);
|
||||
};
|
||||
|
||||
%include ssh-analyzer.pac
|
6
src/analyzer/protocol/ssh/types.bif
Normal file
6
src/analyzer/protocol/ssh/types.bif
Normal file
|
@ -0,0 +1,6 @@
|
|||
module SSH;
|
||||
|
||||
type Algorithm_Prefs: record;
|
||||
type Capabilities: record;
|
||||
|
||||
module GLOBAL;
|
Loading…
Add table
Add a link
Reference in a new issue