Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions doc/userguide/rules/ssh-keywords.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,26 @@ Suricata has several rule keywords to match on different elements of SSH
connections.


Frames
------

The SSH parser supports the following frames:

* ssh.record_hdr
* ssh.record_data
* ssh.record_pdu

These are header + data = pdu for SSH records, after the banner and before encryption.
The SSH record header is 6 bytes long : 4 bytes length, 1 byte passing, 1 byte message code.

Example:

.. container:: example-rule

alert ssh any any -> any any (msg:"hdr frame new keys"; :example-rule-emphasis:`frame:ssh.record.hdr; content: "|15|"; endswith;` bsize: 6; sid:2;)

This rule matches like Wireshark ``ssh.message_code == 0x15``.

ssh.proto
---------
Match on the version of the SSH protocol used. ``ssh.proto`` is a sticky buffer,
Expand Down
1 change: 0 additions & 1 deletion rust/src/enip/enip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,6 @@ unsafe extern "C" fn rs_enip_tx_get_alstate_progress(tx: *mut c_void, direction:
return 0;
}

// app-layer-frame-documentation tag start: FrameType enum
#[derive(AppLayerFrameType)]
pub enum EnipFrameType {
Hdr,
Expand Down
22 changes: 11 additions & 11 deletions rust/src/http2/detect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -434,11 +434,11 @@ pub fn http2_frames_get_header_value_vec(
vec.extend_from_slice(&block.value);
found = 1;
} else if found == 1 && Rc::strong_count(&block.name) <= 2 {
vec.extend_from_slice(&[b',', b' ']);
vec.extend_from_slice(b", ");
vec.extend_from_slice(&block.value);
found = 2;
} else if Rc::strong_count(&block.name) <= 2 {
vec.extend_from_slice(&[b',', b' ']);
vec.extend_from_slice(b", ");
vec.extend_from_slice(&block.value);
}
}
Expand Down Expand Up @@ -474,11 +474,11 @@ fn http2_frames_get_header_value<'a>(
if let Ok(s) = single {
vec.extend_from_slice(s);
}
vec.extend_from_slice(&[b',', b' ']);
vec.extend_from_slice(b", ");
vec.extend_from_slice(&block.value);
found = 2;
} else if Rc::strong_count(&block.name) <= 2 {
vec.extend_from_slice(&[b',', b' ']);
vec.extend_from_slice(b", ");
vec.extend_from_slice(&block.value);
}
}
Expand Down Expand Up @@ -730,7 +730,7 @@ fn http2_escape_header(blocks: &[parser::HTTP2FrameHeaderBlock], i: u32) -> Vec<
let normalsize = blocks[i as usize].value.len() + 2 + blocks[i as usize].name.len();
let mut vec = Vec::with_capacity(normalsize);
vec.extend_from_slice(&blocks[i as usize].name);
vec.extend_from_slice(&[b':', b' ']);
vec.extend_from_slice(b": ");
vec.extend_from_slice(&blocks[i as usize].value);
return vec;
}
Expand All @@ -750,12 +750,12 @@ pub unsafe extern "C" fn rs_http2_tx_get_header_names(
for block in blocks.iter() {
// we do not escape linefeeds in headers names
vec.extend_from_slice(&block.name);
vec.extend_from_slice(&[b'\r', b'\n']);
vec.extend_from_slice(b"\r\n");
}
}
}
if vec.len() > 2 {
vec.extend_from_slice(&[b'\r', b'\n']);
vec.extend_from_slice(b"\r\n");
tx.escaped.push(vec);
let idx = tx.escaped.len() - 1;
let value = &tx.escaped[idx];
Expand Down Expand Up @@ -815,9 +815,9 @@ pub unsafe extern "C" fn rs_http2_tx_get_headers(
if !http2_header_iscookie(direction.into(), &block.name) {
// we do not escape linefeeds nor : in headers names
vec.extend_from_slice(&block.name);
vec.extend_from_slice(&[b':', b' ']);
vec.extend_from_slice(b": ");
vec.extend_from_slice(http2_header_trimspaces(&block.value));
vec.extend_from_slice(&[b'\r', b'\n']);
vec.extend_from_slice(b"\r\n");
}
}
}
Expand Down Expand Up @@ -848,9 +848,9 @@ pub unsafe extern "C" fn rs_http2_tx_get_headers_raw(
for block in blocks.iter() {
// we do not escape linefeeds nor : in headers names
vec.extend_from_slice(&block.name);
vec.extend_from_slice(&[b':', b' ']);
vec.extend_from_slice(b": ");
vec.extend_from_slice(&block.value);
vec.extend_from_slice(&[b'\r', b'\n']);
vec.extend_from_slice(b"\r\n");
}
}
}
Expand Down
38 changes: 17 additions & 21 deletions rust/src/ike/ikev2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,24 +186,20 @@ fn add_proposals(
// Rule 1: warn on weak or unknown transforms
for xform in &transforms {
match *xform {
IkeV2Transform::Encryption(ref enc) => {
match *enc {
IkeTransformEncType::ENCR_DES_IV64
| IkeTransformEncType::ENCR_DES
| IkeTransformEncType::ENCR_3DES
| IkeTransformEncType::ENCR_RC5
| IkeTransformEncType::ENCR_IDEA
| IkeTransformEncType::ENCR_CAST
| IkeTransformEncType::ENCR_BLOWFISH
| IkeTransformEncType::ENCR_3IDEA
| IkeTransformEncType::ENCR_DES_IV32
| IkeTransformEncType::ENCR_NULL => {
SCLogDebug!("Weak Encryption: {:?}", enc);
// XXX send event only if direction == Direction::ToClient ?
tx.set_event(IkeEvent::WeakCryptoEnc);
}
_ => (),
}
IkeV2Transform::Encryption(
IkeTransformEncType::ENCR_DES_IV64
| IkeTransformEncType::ENCR_DES
| IkeTransformEncType::ENCR_3DES
| IkeTransformEncType::ENCR_RC5
| IkeTransformEncType::ENCR_IDEA
| IkeTransformEncType::ENCR_CAST
| IkeTransformEncType::ENCR_BLOWFISH
| IkeTransformEncType::ENCR_3IDEA
| IkeTransformEncType::ENCR_DES_IV32
| IkeTransformEncType::ENCR_NULL,
) => {
// XXX send event only if direction == Direction::ToClient ?
tx.set_event(IkeEvent::WeakCryptoEnc);
}
IkeV2Transform::PRF(ref prf) => match *prf {
IkeTransformPRFType::PRF_NULL => {
Expand Down Expand Up @@ -276,9 +272,9 @@ fn add_proposals(
IkeV2Transform::Auth(_) => true,
_ => false,
}) && !transforms.iter().any(|x| match *x {
IkeV2Transform::Encryption(ref enc) => enc.is_aead(),
_ => false,
}) {
IkeV2Transform::Encryption(ref enc) => enc.is_aead(),
_ => false,
}) {
SCLogDebug!("No integrity transform found");
tx.set_event(IkeEvent::WeakCryptoNoAuth);
}
Expand Down
2 changes: 1 addition & 1 deletion rust/src/mime/smtp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,7 @@ fn mime_smtp_complete(ctx: &mut MimeStateSMTP) {
ctx.md5_result = ctx.md5.finalize_reset();
}
// look for url in the last unfinished line
mime_smtp_find_url_strings(ctx, &[b'\n']);
mime_smtp_find_url_strings(ctx, b"\n");
}

#[no_mangle]
Expand Down
93 changes: 74 additions & 19 deletions rust/src/ssh/ssh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use crate::core::*;
use nom7::Err;
use std::ffi::CString;
use std::sync::atomic::{AtomicBool, Ordering};
use crate::frames::Frame;

static mut ALPROTO_SSH: AppProto = ALPROTO_UNKNOWN;
static HASSH_ENABLED: AtomicBool = AtomicBool::new(false);
Expand All @@ -29,6 +30,13 @@ fn hassh_is_enabled() -> bool {
HASSH_ENABLED.load(Ordering::Relaxed)
}

#[derive(AppLayerFrameType)]
pub enum SshFrameType {
RecordHdr,
RecordData,
RecordPdu,
}

#[derive(AppLayerEvent)]
pub enum SSHEvent {
InvalidBanner,
Expand Down Expand Up @@ -109,6 +117,7 @@ impl SSHState {

fn parse_record(
&mut self, mut input: &[u8], resp: bool, pstate: *mut std::os::raw::c_void,
flow: *const Flow, stream_slice: &StreamSlice,
) -> AppLayerResult {
let (hdr, ohdr) = if !resp {
(&mut self.transaction.cli_hdr, &self.transaction.srv_hdr)
Expand Down Expand Up @@ -149,6 +158,30 @@ impl SSHState {
while !input.is_empty() {
match parser::ssh_parse_record(input) {
Ok((rem, head)) => {
let _pdu = Frame::new(
flow,
stream_slice,
input,
SSH_RECORD_HEADER_LEN as i64,
SshFrameType::RecordHdr as u8,
Some(0),
);
let _pdu = Frame::new(
flow,
stream_slice,
&input[SSH_RECORD_HEADER_LEN..],
(head.pkt_len - 2) as i64,
SshFrameType::RecordData as u8,
Some(0),
);
let _pdu = Frame::new(
flow,
stream_slice,
input,
(head.pkt_len + 4) as i64,
SshFrameType::RecordPdu as u8,
Some(0),
);
SCLogDebug!("SSH valid record {}", head);
match head.msg_code {
parser::MessageCode::Kexinit if hassh_is_enabled() => {
Expand Down Expand Up @@ -180,6 +213,30 @@ impl SSHState {
Err(Err::Incomplete(_)) => {
match parser::ssh_parse_record_header(input) {
Ok((rem, head)) => {
let _pdu = Frame::new(
flow,
stream_slice,
input,
SSH_RECORD_HEADER_LEN as i64,
SshFrameType::RecordHdr as u8,
Some(0),
);
let _pdu = Frame::new(
flow,
stream_slice,
&input[SSH_RECORD_HEADER_LEN..],
(head.pkt_len - 2) as i64,
SshFrameType::RecordData as u8,
Some(0),
);
let _pdu = Frame::new(
flow,
stream_slice,
input,
(head.pkt_len + 4) as i64,
SshFrameType::RecordPdu as u8,
Some(0),
);
SCLogDebug!("SSH valid record header {}", head);
let remlen = rem.len() as u32;
hdr.record_left = head.pkt_len - 2 - remlen;
Expand Down Expand Up @@ -210,15 +267,12 @@ impl SSHState {
}
Err(Err::Incomplete(_)) => {
//we may have consumed data from previous records
if input.len() < SSH_RECORD_HEADER_LEN {
//do not trust nom incomplete value
return AppLayerResult::incomplete(
(il - input.len()) as u32,
SSH_RECORD_HEADER_LEN as u32,
);
} else {
panic!("SSH invalid length record header");
}
debug_validate_bug_on!(input.len() >= SSH_RECORD_HEADER_LEN);
//do not trust nom incomplete value
return AppLayerResult::incomplete(
(il - input.len()) as u32,
SSH_RECORD_HEADER_LEN as u32,
);
}
Err(_e) => {
SCLogDebug!("SSH invalid record header {}", _e);
Expand All @@ -239,6 +293,7 @@ impl SSHState {

fn parse_banner(
&mut self, input: &[u8], resp: bool, pstate: *mut std::os::raw::c_void,
flow: *const Flow, stream_slice: &StreamSlice,
) -> AppLayerResult {
let hdr = if !resp {
&mut self.transaction.cli_hdr
Expand All @@ -248,7 +303,7 @@ impl SSHState {
if hdr.flags == SSHConnectionState::SshStateBannerWaitEol {
match parser::ssh_parse_line(input) {
Ok((rem, _)) => {
let mut r = self.parse_record(rem, resp, pstate);
let mut r = self.parse_record(rem, resp, pstate, flow, stream_slice);
if r.is_incomplete() {
//adds bytes consumed by banner to incomplete result
r.consumed += (input.len() - rem.len()) as u32;
Expand Down Expand Up @@ -288,7 +343,7 @@ impl SSHState {
);
self.set_event(SSHEvent::LongBanner);
}
let mut r = self.parse_record(rem, resp, pstate);
let mut r = self.parse_record(rem, resp, pstate, flow, stream_slice);
if r.is_incomplete() {
//adds bytes consumed by banner to incomplete result
r.consumed += (input.len() - rem.len()) as u32;
Expand Down Expand Up @@ -352,33 +407,33 @@ pub extern "C" fn rs_ssh_state_tx_free(_state: *mut std::os::raw::c_void, _tx_id

#[no_mangle]
pub unsafe extern "C" fn rs_ssh_parse_request(
_flow: *const Flow, state: *mut std::os::raw::c_void, pstate: *mut std::os::raw::c_void,
flow: *const Flow, state: *mut std::os::raw::c_void, pstate: *mut std::os::raw::c_void,
stream_slice: StreamSlice,
_data: *const std::os::raw::c_void
) -> AppLayerResult {
let state = &mut cast_pointer!(state, SSHState);
let buf = stream_slice.as_slice();
let hdr = &mut state.transaction.cli_hdr;
if hdr.flags < SSHConnectionState::SshStateBannerDone {
return state.parse_banner(buf, false, pstate);
return state.parse_banner(buf, false, pstate, flow, &stream_slice);
} else {
return state.parse_record(buf, false, pstate);
return state.parse_record(buf, false, pstate, flow, &stream_slice);
}
}

#[no_mangle]
pub unsafe extern "C" fn rs_ssh_parse_response(
_flow: *const Flow, state: *mut std::os::raw::c_void, pstate: *mut std::os::raw::c_void,
flow: *const Flow, state: *mut std::os::raw::c_void, pstate: *mut std::os::raw::c_void,
stream_slice: StreamSlice,
_data: *const std::os::raw::c_void
) -> AppLayerResult {
let state = &mut cast_pointer!(state, SSHState);
let buf = stream_slice.as_slice();
let hdr = &mut state.transaction.srv_hdr;
if hdr.flags < SSHConnectionState::SshStateBannerDone {
return state.parse_banner(buf, true, pstate);
return state.parse_banner(buf, true, pstate, flow, &stream_slice);
} else {
return state.parse_record(buf, true, pstate);
return state.parse_record(buf, true, pstate, flow, &stream_slice);
}
}

Expand Down Expand Up @@ -464,8 +519,8 @@ pub unsafe extern "C" fn rs_ssh_register_parser() {
get_state_data: rs_ssh_get_state_data,
apply_tx_config: None,
flags: 0,
get_frame_id_by_name: None,
get_frame_name_by_id: None,
get_frame_id_by_name: Some(SshFrameType::ffi_id_from_name),
get_frame_name_by_id: Some(SshFrameType::ffi_name_from_id),
};

let ip_proto_str = CString::new("tcp").unwrap();
Expand Down
1 change: 0 additions & 1 deletion rust/src/websocket/websocket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ pub(super) static mut ALPROTO_WEBSOCKET: AppProto = ALPROTO_UNKNOWN;

static mut WEBSOCKET_MAX_PAYLOAD_SIZE: u32 = 0xFFFF;

// app-layer-frame-documentation tag start: FrameType enum
#[derive(AppLayerFrameType)]
pub enum WebSocketFrameType {
Header,
Expand Down
Loading