]> git.ipfire.org Git - thirdparty/suricata.git/commitdiff
ssh: frames support
authorPhilippe Antoine <pantoine@oisf.net>
Thu, 20 Jun 2024 15:10:24 +0000 (17:10 +0200)
committerVictor Julien <victor@inliniac.net>
Thu, 1 Aug 2024 05:05:13 +0000 (07:05 +0200)
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.

doc/userguide/rules/ssh-keywords.rst
rust/src/ssh/ssh.rs

index fe61d1464643638cbac0b74d1e978928b39bb547..8e967e3d536ba56863b0ad51abdb3496b8760210 100644 (file)
@@ -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,
index d5341a2cfb19b34a6d0826697c366eb50d087c54..ce651de60db1aea8aae3828c657ec74acdc491cd 100644 (file)
@@ -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();