From: Philippe Antoine Date: Thu, 20 Jun 2024 15:10:24 +0000 (+0200) Subject: ssh: frames support X-Git-Tag: suricata-8.0.0-beta1~974 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=0b2ed97f3678f996a890cd67b4184b8dd8ab1088;p=thirdparty%2Fsuricata.git ssh: frames support Ticket: 5734 Adds frames for SSH records, that come after banner, and before the data is encrypted. These records may contain cipher lists for instance. --- diff --git a/doc/userguide/rules/ssh-keywords.rst b/doc/userguide/rules/ssh-keywords.rst index fe61d14646..8e967e3d53 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/ssh/ssh.rs b/rust/src/ssh/ssh.rs index d5341a2cfb..ce651de60d 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; @@ -239,6 +296,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 +306,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 +346,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 +410,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 +418,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 +434,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 +522,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();