Skip to main content

iris_core/protocols/stream/ssh/
parser.rs

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