diff --git a/doc/userguide/rules/ssh-keywords.rst b/doc/userguide/rules/ssh-keywords.rst index 83d2f2fe8d34..91544008663a 100644 --- a/doc/userguide/rules/ssh-keywords.rst +++ b/doc/userguide/rules/ssh-keywords.rst @@ -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, diff --git a/rust/src/enip/enip.rs b/rust/src/enip/enip.rs index 05ec13a670d9..31c9152e5cab 100644 --- a/rust/src/enip/enip.rs +++ b/rust/src/enip/enip.rs @@ -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, diff --git a/rust/src/ssh/ssh.rs b/rust/src/ssh/ssh.rs index d5341a2cfb19..1d38a1bd50e1 100644 --- a/rust/src/ssh/ssh.rs +++ b/rust/src/ssh/ssh.rs @@ -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); @@ -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, @@ -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) @@ -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() => { @@ -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; @@ -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); @@ -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 @@ -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; @@ -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; @@ -352,7 +407,7 @@ 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 { @@ -360,15 +415,15 @@ pub unsafe extern "C" fn rs_ssh_parse_request( 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 { @@ -376,9 +431,9 @@ pub unsafe extern "C" fn rs_ssh_parse_response( 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); } } @@ -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(); diff --git a/rust/src/websocket/websocket.rs b/rust/src/websocket/websocket.rs index ebbe901a53b0..f686ad471b42 100644 --- a/rust/src/websocket/websocket.rs +++ b/rust/src/websocket/websocket.rs @@ -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, diff --git a/src/detect.c b/src/detect.c index 5d6eb4df3e8a..5b4a0068f43b 100644 --- a/src/detect.c +++ b/src/detect.c @@ -150,7 +150,13 @@ static void DetectRun(ThreadVars *th_v, goto end; } const TcpSession *ssn = p->flow->protoctx; - if (ssn && (ssn->flags & STREAMTCP_FLAG_APP_LAYER_DISABLED) == 0) { + bool setting_nopayload = p->flow->alparser && + AppLayerParserStateIssetFlag( + p->flow->alparser, APP_LAYER_PARSER_NO_INSPECTION) && + !(p->flags & PKT_NOPAYLOAD_INSPECTION); + // we may be right after disabling app-layer (ssh) + if (ssn && + ((ssn->flags & STREAMTCP_FLAG_APP_LAYER_DISABLED) == 0 || setting_nopayload)) { // PACKET_PROFILING_DETECT_START(p, PROF_DETECT_TX); DetectRunFrames(th_v, de_ctx, det_ctx, p, pflow, &scratch); // PACKET_PROFILING_DETECT_END(p, PROF_DETECT_TX);