diff --git a/doc/userguide/rules/ssh-keywords.rst b/doc/userguide/rules/ssh-keywords.rst index 83d2f2fe8d34..60dd53d3334e 100644 --- a/doc/userguide/rules/ssh-keywords.rst +++ b/doc/userguide/rules/ssh-keywords.rst @@ -6,6 +6,15 @@ 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 + ssh.proto --------- Match on the version of the SSH protocol used. ``ssh.proto`` is a sticky buffer, diff --git a/rust/src/ssh/ssh.rs b/rust/src/ssh/ssh.rs index c1f08a904c4d..f5de038a585b 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,14 @@ fn hassh_is_enabled() -> bool { HASSH_ENABLED.load(Ordering::Relaxed) } +// app-layer-frame-documentation tag start: FrameType enum +#[derive(AppLayerFrameType)] +pub enum SshFrameType { + RecordHdr, + RecordData, + RecordPdu, +} + #[derive(AppLayerEvent)] pub enum SSHEvent { InvalidBanner, @@ -109,6 +118,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 +159,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 +214,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 +268,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 +294,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 +304,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 +344,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 +408,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 +416,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 +432,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); } } @@ -465,8 +521,8 @@ pub unsafe extern "C" fn rs_ssh_register_parser() { apply_tx_config: None, flags: 0, truncate: None, - 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/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);