Skip to main content

iris_core/protocols/stream/ssh/
parser.rs

1//! SSH parser.
2//!
3//! Uses parsing functions from the [Rusticata SSH parser](https://github.com/rusticata/ssh-parser/blob/master/src/ssh.rs).
4
5use super::handshake::*;
6use super::Ssh;
7use crate::conntrack::pdu::L4Pdu;
8use crate::protocols::stream::{
9    ConnParsable, ParseResult, ParsingState, ProbeResult, Session, SessionData,
10};
11
12use ssh_parser::*;
13
14#[derive(Debug)]
15pub struct SshParser {
16    sessions: Vec<Ssh>,
17}
18
19impl Default for SshParser {
20    fn default() -> Self {
21        SshParser {
22            sessions: vec![Ssh::new()],
23        }
24    }
25}
26
27impl ConnParsable for SshParser {
28    fn parse(&mut self, pdu: &L4Pdu) -> ParseResult {
29        log::debug!("Updating parser ssh");
30        let offset = pdu.offset();
31        let length = pdu.length();
32        if length == 0 {
33            return ParseResult::Skipped;
34        }
35
36        if let Ok(data) = (pdu.mbuf_ref()).get_data_slice(offset, length) {
37            if !self.sessions.is_empty() {
38                return self.sessions[0].process(data, pdu.dir);
39            }
40            ParseResult::Skipped
41        } else {
42            log::warn!("Malformed packet on parse");
43            ParseResult::Skipped
44        }
45    }
46
47    fn probe(&self, pdu: &L4Pdu) -> ProbeResult {
48        let offset = pdu.offset();
49        let length = pdu.length();
50
51        if length < 4 {
52            return ProbeResult::Unsure;
53        }
54
55        if let Ok(data) = (pdu.mbuf).get_data_slice(offset, length) {
56            // check if first 4 bytes match the beginning of a SSH identification string ("SSH-")
57            match &data[..4] {
58                b"SSH-" => ProbeResult::Certain,
59                _ => ProbeResult::NotForUs,
60            }
61        } else {
62            log::warn!("Malformed packet");
63            ProbeResult::Error
64        }
65    }
66
67    fn remove_session(&mut self, _session_id: usize) -> Option<Session> {
68        self.sessions.pop().map(|ssh| Session {
69            data: SessionData::Ssh(Box::new(ssh)),
70            id: 0,
71        })
72    }
73
74    fn drain_sessions(&mut self) -> Vec<Session> {
75        self.sessions
76            .drain(..)
77            .map(|ssh| Session {
78                data: SessionData::Ssh(Box::new(ssh)),
79                id: 0,
80            })
81            .collect()
82    }
83
84    fn session_parsed_state(&self) -> ParsingState {
85        ParsingState::Stop
86    }
87
88    fn body_offset(&mut self) -> Option<usize> {
89        match self.sessions.last_mut() {
90            Some(session) => std::mem::take(&mut session.last_body_offset),
91            None => None,
92        }
93    }
94}
95
96impl Ssh {
97    /// Allocate a new SSH handshake instance.
98    pub(crate) fn new() -> Ssh {
99        Ssh {
100            client_version_exchange: None,
101            server_version_exchange: None,
102            key_exchange: None,
103            client_dh_key_exchange: None,
104            server_dh_key_exchange: None,
105            client_new_keys: None,
106            server_new_keys: None,
107            last_body_offset: None,
108        }
109    }
110
111    fn byte_to_string(&mut self, b: &[u8]) -> String {
112        String::from_utf8(b.to_vec()).unwrap()
113    }
114
115    pub(crate) fn parse_version_exchange(&mut self, data: &[u8], dir: bool) {
116        let ssh_identifier = b"SSH-";
117        if let Some(contains_ssh_identifier) = data
118            .windows(ssh_identifier.len())
119            .position(|window| window == ssh_identifier)
120            .map(|p| &data[p..])
121        {
122            match ssh_parser::parse_ssh_identification(contains_ssh_identifier) {
123                Ok((_, (_, ssh_id_string))) => {
124                    let version_exchange = SshVersionExchange {
125                        protoversion: Some(self.byte_to_string(ssh_id_string.proto)),
126                        softwareversion: Some(self.byte_to_string(ssh_id_string.software)),
127                        comments: ssh_id_string.comments.map(|c| self.byte_to_string(c)),
128                    };
129
130                    if dir {
131                        self.client_version_exchange = Some(version_exchange);
132                    } else {
133                        self.server_version_exchange = Some(version_exchange);
134                    }
135                }
136                e => log::debug!("Not a valid SSH version exchange message: {:?}", e),
137            }
138        }
139    }
140
141    fn bytes_to_string_vec(&mut self, data: &[u8]) -> Vec<String> {
142        data.split(|&b| b == b',')
143            .map(|chunk| String::from_utf8(chunk.to_vec()).unwrap())
144            .collect()
145    }
146
147    pub(crate) fn parse_key_exchange(&mut self, data: &[u8]) {
148        match ssh_parser::parse_ssh_packet(data) {
149            Ok((_, (pkt, _))) => match pkt {
150                SshPacket::KeyExchange(pkt) => {
151                    let key_exchange = SshKeyExchange {
152                        cookie: pkt.cookie.to_vec(),
153                        kex_algs: self.bytes_to_string_vec(pkt.kex_algs),
154                        server_host_key_algs: self.bytes_to_string_vec(pkt.server_host_key_algs),
155                        encryption_algs_client_to_server: self
156                            .bytes_to_string_vec(pkt.encr_algs_client_to_server),
157                        encryption_algs_server_to_client: self
158                            .bytes_to_string_vec(pkt.encr_algs_server_to_client),
159                        mac_algs_client_to_server: self
160                            .bytes_to_string_vec(pkt.mac_algs_client_to_server),
161                        mac_algs_server_to_client: self
162                            .bytes_to_string_vec(pkt.mac_algs_server_to_client),
163                        compression_algs_client_to_server: self
164                            .bytes_to_string_vec(pkt.comp_algs_client_to_server),
165                        compression_algs_server_to_client: self
166                            .bytes_to_string_vec(pkt.comp_algs_server_to_client),
167                        languages_client_to_server: self
168                            .bytes_to_string_vec(pkt.langs_client_to_server),
169                        languages_server_to_client: self
170                            .bytes_to_string_vec(pkt.langs_server_to_client),
171                        first_kex_packet_follows: pkt.first_kex_packet_follows,
172                    };
173
174                    self.key_exchange = Some(key_exchange);
175                }
176                e => log::debug!("Could not parse data as a SSH KeyExchange packet: {:?}", e),
177            },
178            e => log::debug!("Could not parse data as a SSH packet: {:?}", e),
179        }
180    }
181
182    pub(crate) fn parse_dh_client_init(&mut self, data: &[u8]) {
183        match ssh_parser::parse_ssh_packet(data) {
184            Ok((_, (pkt, _))) => match pkt {
185                SshPacket::DiffieHellmanInit(pkt) => {
186                    let dh_init = SshDhInit { e: pkt.e.to_vec() };
187
188                    self.client_dh_key_exchange = Some(dh_init);
189                }
190                e => log::debug!(
191                    "Could not parse data as a SSH DiffieHellmanInit packet: {:?}",
192                    e
193                ),
194            },
195            e => log::debug!("Could not parse data as a SSH packet: {:?}", e),
196        }
197    }
198
199    pub(crate) fn parse_dh_server_response(&mut self, data: &[u8]) {
200        match ssh_parser::parse_ssh_packet(data) {
201            Ok((_, (pkt, _))) => match pkt {
202                SshPacket::DiffieHellmanReply(pkt) => {
203                    let dh_response = SshDhResponse {
204                        pubkey_and_certs: pkt.pubkey_and_cert.to_vec(),
205                        f: pkt.f.to_vec(),
206                        signature: pkt.signature.to_vec(),
207                    };
208
209                    self.server_dh_key_exchange = Some(dh_response);
210                }
211                e => log::debug!(
212                    "Could not parse data as a SSH DiffieHellmanReply packet: {:?}",
213                    e
214                ),
215            },
216            e => log::debug!("Could not parse data as a SSH packet: {:?}", e),
217        }
218    }
219
220    /// Parse a new keys packet. Return length of remaining data.
221    pub(crate) fn parse_new_keys(&mut self, data: &[u8], dir: bool) -> usize {
222        let mut remaining = 0;
223        match ssh_parser::parse_ssh_packet(data) {
224            Ok((rem, (pkt, _))) => match pkt {
225                SshPacket::NewKeys => {
226                    let new_keys = SshNewKeys;
227                    remaining = rem.len();
228                    if dir {
229                        self.client_new_keys = Some(new_keys);
230                    } else {
231                        self.server_new_keys = Some(new_keys);
232                    }
233                }
234                e => log::debug!("Could not parse data as a SSH NewKeys packet: {:?}", e),
235            },
236            e => log::debug!("Could not parse data as a SSH packet: {:?}", e),
237        }
238        remaining
239    }
240
241    pub(crate) fn process(&mut self, data: &[u8], dir: bool) -> ParseResult {
242        let mut status = ParseResult::Continue(0);
243        log::trace!("process ({} bytes)", data.len());
244
245        let ssh_identifier = b"SSH-";
246        if data
247            .windows(ssh_identifier.len())
248            .position(|window| window == ssh_identifier)
249            .map(|p| &data[p..])
250            .is_some()
251        {
252            self.parse_version_exchange(data, dir);
253            status = ParseResult::Continue(0);
254        } else {
255            match ssh_parser::parse_ssh_packet(data) {
256                Ok((_, (pkt, _))) => {
257                    match pkt {
258                        SshPacket::KeyExchange(_) => {
259                            self.parse_key_exchange(data);
260                            status = ParseResult::Continue(0);
261                        }
262                        SshPacket::DiffieHellmanInit(_) => {
263                            self.parse_dh_client_init(data);
264                            status = ParseResult::Continue(0);
265                        }
266                        SshPacket::DiffieHellmanReply(_) => {
267                            self.parse_dh_server_response(data);
268                            status = ParseResult::Continue(0);
269                        }
270                        SshPacket::NewKeys => {
271                            let remaining = self.parse_new_keys(data, dir);
272
273                            // finish parsing when client and server have both sent a NewKeys packet
274                            if self.client_new_keys.is_some() && self.server_new_keys.is_some() {
275                                if remaining > 0 && remaining < data.len() {
276                                    self.last_body_offset = Some(data.len() - remaining - 1);
277                                }
278                                return ParseResult::HeadersDone(0);
279                            }
280                            status = ParseResult::Continue(0);
281                        }
282                        _ => (),
283                    }
284                }
285                e => {
286                    log::debug!("parse error: {:?}", e);
287                    status = ParseResult::Skipped;
288                }
289            }
290        }
291        status
292    }
293}