Coverage Report

Created: 2026-08-14 07:34

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/suricata7/rust/src/ssh/ssh.rs
Line
Count
Source
1
/* Copyright (C) 2020 Open Information Security Foundation
2
 *
3
 * You can copy, redistribute or modify this Program under the terms of
4
 * the GNU General Public License version 2 as published by the Free
5
 * Software Foundation.
6
 *
7
 * This program is distributed in the hope that it will be useful,
8
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
9
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10
 * GNU General Public License for more details.
11
 *
12
 * You should have received a copy of the GNU General Public License
13
 * version 2 along with this program; if not, write to the Free Software
14
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
15
 * 02110-1301, USA.
16
 */
17
18
use super::parser;
19
use crate::applayer::*;
20
use crate::core::*;
21
use nom7::Err;
22
use std::ffi::CString;
23
use std::sync::atomic::{AtomicBool, Ordering};
24
25
static mut ALPROTO_SSH: AppProto = ALPROTO_UNKNOWN;
26
static HASSH_ENABLED: AtomicBool = AtomicBool::new(false);
27
28
64.5k
fn hassh_is_enabled() -> bool {
29
64.5k
    HASSH_ENABLED.load(Ordering::Relaxed)
30
64.5k
}
31
32
#[derive(AppLayerEvent)]
33
pub enum SSHEvent {
34
    InvalidBanner,
35
    LongBanner,
36
    InvalidRecord,
37
    LongKexRecord,
38
}
39
40
#[repr(u8)]
41
#[derive(Copy, Clone, PartialOrd, PartialEq, Eq)]
42
pub enum SSHConnectionState {
43
    SshStateInProgress = 0,
44
    SshStateBannerWaitEol = 1,
45
    SshStateBannerDone = 2,
46
    SshStateFinished = 3,
47
}
48
49
pub const SSH_MAX_BANNER_LEN: usize = 256;
50
const SSH_RECORD_HEADER_LEN: usize = 6;
51
const SSH_MAX_REASSEMBLED_RECORD_LEN: usize = 65535;
52
53
pub struct SshHeader {
54
    record_left: u32,
55
    record_left_msg: parser::MessageCode,
56
57
    flags: SSHConnectionState,
58
    pub protover: Vec<u8>,
59
    pub swver: Vec<u8>,
60
61
    pub hassh: Vec<u8>,
62
    pub hassh_string: Vec<u8>,
63
}
64
65
impl Default for SshHeader {
66
6.92k
    fn default() -> Self {
67
6.92k
        Self::new()
68
6.92k
    }
69
}
70
71
impl SshHeader {
72
6.92k
    pub fn new() -> SshHeader {
73
6.92k
        Self {
74
6.92k
            record_left: 0,
75
6.92k
            record_left_msg: parser::MessageCode::Undefined(0),
76
6.92k
77
6.92k
            flags: SSHConnectionState::SshStateInProgress,
78
6.92k
            protover: Vec::new(),
79
6.92k
            swver: Vec::new(),
80
6.92k
81
6.92k
            hassh: Vec::new(),
82
6.92k
            hassh_string: Vec::new(),
83
6.92k
        }
84
6.92k
    }
85
}
86
87
#[derive(Default)]
88
pub struct SSHTransaction {
89
    pub srv_hdr: SshHeader,
90
    pub cli_hdr: SshHeader,
91
92
    tx_data: AppLayerTxData,
93
}
94
95
#[derive(Default)]
96
pub struct SSHState {
97
    state_data: AppLayerStateData,
98
    transaction: SSHTransaction,
99
}
100
101
impl SSHState {
102
3.46k
    pub fn new() -> Self {
103
3.46k
        Default::default()
104
3.46k
    }
105
106
1.30k
    fn set_event(&mut self, event: SSHEvent) {
107
1.30k
        self.transaction.tx_data.set_event(event as u8);
108
1.30k
    }
109
110
102k
    fn parse_record(
111
102k
        &mut self, mut input: &[u8], resp: bool, pstate: *mut std::os::raw::c_void,
112
102k
    ) -> AppLayerResult {
113
102k
        let (hdr, ohdr) = if !resp {
114
84.8k
            (&mut self.transaction.cli_hdr, &self.transaction.srv_hdr)
115
        } else {
116
17.9k
            (&mut self.transaction.srv_hdr, &self.transaction.cli_hdr)
117
        };
118
102k
        let il = input.len();
119
        //first skip record left bytes
120
102k
        if hdr.record_left > 0 {
121
            //should we check for overflow ?
122
84.4k
            let ilen = input.len() as u32;
123
84.4k
            if hdr.record_left > ilen {
124
71.4k
                hdr.record_left -= ilen;
125
71.4k
                return AppLayerResult::ok();
126
            } else {
127
13.0k
                let start = hdr.record_left as usize;
128
5.28k
                match hdr.record_left_msg {
129
                    // parse reassembled tcp segments
130
5.28k
                    parser::MessageCode::Kexinit if hassh_is_enabled() => {
131
4.09k
                        if let Ok((_rem, key_exchange)) =
132
5.28k
                            parser::ssh_parse_key_exchange(&input[..start])
133
4.09k
                        {
134
4.09k
                            key_exchange.generate_hassh(
135
4.09k
                                &mut hdr.hassh_string,
136
4.09k
                                &mut hdr.hassh,
137
4.09k
                                &resp,
138
4.09k
                            );
139
4.09k
                        }
140
5.28k
                        hdr.record_left_msg = parser::MessageCode::Undefined(0);
141
                    }
142
7.79k
                    _ => {}
143
                }
144
13.0k
                input = &input[start..];
145
13.0k
                hdr.record_left = 0;
146
            }
147
18.3k
        }
148
        //parse records out of input
149
368k
        while !input.is_empty() {
150
362k
            match parser::ssh_parse_record(input) {
151
337k
                Ok((rem, head)) => {
152
                    SCLogDebug!("SSH valid record {}", head);
153
33.4k
                    match head.msg_code {
154
33.4k
                        parser::MessageCode::Kexinit if hassh_is_enabled() => {
155
                            //let endkex = SSH_RECORD_HEADER_LEN + head.pkt_len - 2;
156
33.4k
                            let endkex = input.len() - rem.len();
157
33.4k
                            if let Ok((_, key_exchange)) = parser::ssh_parse_key_exchange(&input[SSH_RECORD_HEADER_LEN..endkex]) {
158
9.24k
                                key_exchange.generate_hassh(&mut hdr.hassh_string, &mut hdr.hassh, &resp);
159
24.2k
                            }
160
                        }
161
                        parser::MessageCode::NewKeys => {
162
881
                            hdr.flags = SSHConnectionState::SshStateFinished;
163
881
                            if ohdr.flags >= SSHConnectionState::SshStateFinished {
164
464
                                unsafe {
165
464
                                    AppLayerParserStateSetFlag(
166
464
                                        pstate,
167
464
                                        APP_LAYER_PARSER_NO_INSPECTION
168
464
                                        | APP_LAYER_PARSER_NO_REASSEMBLY
169
464
                                        | APP_LAYER_PARSER_BYPASS_READY,
170
464
                                    );
171
464
                                }
172
417
                            }
173
                        }
174
303k
                        _ => {}
175
                    }
176
                    
177
337k
                    input = rem;
178
                    //header and complete data (not returned)
179
                }
180
                Err(Err::Incomplete(_)) => {
181
24.5k
                    match parser::ssh_parse_record_header(input) {
182
14.4k
                        Ok((rem, head)) => {
183
                            SCLogDebug!("SSH valid record header {}", head);
184
14.4k
                            let remlen = rem.len() as u32;
185
14.4k
                            hdr.record_left = head.pkt_len - 2 - remlen;
186
                            //header with rem as incomplete data
187
5.67k
                            match head.msg_code { 
188
298
                                parser::MessageCode::NewKeys => {
189
298
                                    hdr.flags = SSHConnectionState::SshStateFinished;
190
298
                                }
191
5.67k
                                parser::MessageCode::Kexinit if hassh_is_enabled() => {
192
                                    // check if buffer is bigger than maximum reassembled packet size
193
5.67k
                                    hdr.record_left = head.pkt_len - 2;
194
5.67k
                                    if hdr.record_left < SSH_MAX_REASSEMBLED_RECORD_LEN as u32 {
195
                                        // saving type of incomplete kex message
196
5.50k
                                        hdr.record_left_msg = parser::MessageCode::Kexinit;
197
5.50k
                                        return AppLayerResult::incomplete(
198
5.50k
                                            (il - rem.len()) as u32,
199
5.50k
                                            head.pkt_len - 2
200
                                        );
201
                                    }
202
177
                                    else {
203
177
                                        SCLogDebug!("SSH buffer is bigger than maximum reassembled packet size");
204
177
                                        self.set_event(SSHEvent::LongKexRecord);
205
177
                                    }
206
                                }
207
8.45k
                                _ => {}
208
                            }
209
8.92k
                            return AppLayerResult::ok();
210
                        }
211
                        Err(Err::Incomplete(_)) => {
212
                            //we may have consumed data from previous records
213
10.1k
                            if input.len() < SSH_RECORD_HEADER_LEN {
214
                                //do not trust nom incomplete value
215
10.1k
                                return AppLayerResult::incomplete(
216
10.1k
                                    (il - input.len()) as u32,
217
10.1k
                                    SSH_RECORD_HEADER_LEN as u32,
218
                                );
219
                            } else {
220
0
                                panic!("SSH invalid length record header");
221
                            }
222
                        }
223
0
                        Err(_e) => {
224
                            SCLogDebug!("SSH invalid record header {}", _e);
225
0
                            self.set_event(SSHEvent::InvalidRecord);
226
0
                            return AppLayerResult::err();
227
                        }
228
                    }
229
                }
230
164
                Err(_e) => {
231
                    SCLogDebug!("SSH invalid record {}", _e);
232
164
                    self.set_event(SSHEvent::InvalidRecord);
233
164
                    return AppLayerResult::err();
234
                }
235
            }
236
        }
237
6.68k
        return AppLayerResult::ok();
238
102k
    }
239
240
97.4k
    fn parse_banner(
241
97.4k
        &mut self, input: &[u8], resp: bool, pstate: *mut std::os::raw::c_void,
242
97.4k
    ) -> AppLayerResult {
243
97.4k
        let hdr = if !resp {
244
20.5k
            &mut self.transaction.cli_hdr
245
        } else {
246
76.9k
            &mut self.transaction.srv_hdr
247
        };
248
97.4k
        if hdr.flags == SSHConnectionState::SshStateBannerWaitEol {
249
20.7k
            match parser::ssh_parse_line(input) {
250
10.7k
                Ok((rem, _)) => {
251
10.7k
                    let mut r = self.parse_record(rem, resp, pstate);
252
10.7k
                    if r.is_incomplete() {
253
1.47k
                        //adds bytes consumed by banner to incomplete result
254
1.47k
                        r.consumed += (input.len() - rem.len()) as u32;
255
9.25k
                    }
256
10.7k
                    return r;
257
                }
258
                Err(Err::Incomplete(_)) => {
259
                    // we do not need to retain these bytes
260
                    // we parsed them, we skip them
261
10.0k
                    return AppLayerResult::ok();
262
                }
263
0
                Err(_e) => {
264
                    SCLogDebug!("SSH invalid banner {}", _e);
265
0
                    self.set_event(SSHEvent::InvalidBanner);
266
0
                    return AppLayerResult::err();
267
                }
268
            }
269
76.7k
        }
270
76.7k
        match parser::ssh_parse_line(input) {
271
3.62k
            Ok((rem, line)) => {
272
3.62k
                if let Ok((_, banner)) = parser::ssh_parse_banner(line) {
273
3.26k
                    hdr.protover.extend(banner.protover);
274
3.26k
                    if !banner.swver.is_empty() {
275
1.55k
                        hdr.swver.extend(banner.swver);
276
1.70k
                    }
277
3.26k
                    hdr.flags = SSHConnectionState::SshStateBannerDone;
278
                } else {
279
                    SCLogDebug!("SSH invalid banner");
280
359
                    self.set_event(SSHEvent::InvalidBanner);
281
359
                    return AppLayerResult::err();
282
                }
283
3.26k
                if line.len() >= SSH_MAX_BANNER_LEN {
284
121
                    SCLogDebug!(
285
121
                        "SSH banner too long {} vs {}",
286
121
                        line.len(),
287
121
                        SSH_MAX_BANNER_LEN
288
121
                    );
289
121
                    self.set_event(SSHEvent::LongBanner);
290
3.14k
                }
291
3.26k
                let mut r = self.parse_record(rem, resp, pstate);
292
3.26k
                if r.is_incomplete() {
293
945
                    //adds bytes consumed by banner to incomplete result
294
945
                    r.consumed += (input.len() - rem.len()) as u32;
295
2.31k
                }
296
3.26k
                return r;
297
            }
298
            Err(Err::Incomplete(_)) => {
299
                // see https://github.com/rust-lang/rust-clippy/issues/15158
300
                #[allow(clippy::collapsible_else_if)]
301
73.1k
                if input.len() < SSH_MAX_BANNER_LEN {
302
                    //0 consumed, needs at least one more byte
303
72.6k
                    return AppLayerResult::incomplete(0_u32, (input.len() + 1) as u32);
304
                } else {
305
                    SCLogDebug!(
306
                        "SSH banner too long {} vs {} and waiting for eol",
307
                        input.len(),
308
                        SSH_MAX_BANNER_LEN
309
                    );
310
481
                    if let Ok((_, banner)) = parser::ssh_parse_banner(input) {
311
458
                        hdr.protover.extend(banner.protover);
312
458
                        if !banner.swver.is_empty() {
313
434
                            hdr.swver.extend(banner.swver);
314
434
                        }
315
458
                        hdr.flags = SSHConnectionState::SshStateBannerWaitEol;
316
458
                        self.set_event(SSHEvent::LongBanner);
317
458
                        return AppLayerResult::ok();
318
                    } else {
319
23
                        self.set_event(SSHEvent::InvalidBanner);
320
23
                        return AppLayerResult::err();
321
                    }
322
                }
323
            }
324
0
            Err(_e) => {
325
                SCLogDebug!("SSH invalid banner {}", _e);
326
0
                self.set_event(SSHEvent::InvalidBanner);
327
0
                return AppLayerResult::err();
328
            }
329
        }
330
97.4k
    }
331
}
332
333
// C exports.
334
335
export_tx_data_get!(rs_ssh_get_tx_data, SSHTransaction);
336
export_state_data_get!(rs_ssh_get_state_data, SSHState);
337
338
#[no_mangle]
339
3.46k
pub extern "C" fn rs_ssh_state_new(_orig_state: *mut std::os::raw::c_void, _orig_proto: AppProto) -> *mut std::os::raw::c_void {
340
3.46k
    let state = SSHState::new();
341
3.46k
    let boxed = Box::new(state);
342
3.46k
    return Box::into_raw(boxed) as *mut _;
343
3.46k
}
344
345
#[no_mangle]
346
3.46k
pub unsafe extern "C" fn rs_ssh_state_free(state: *mut std::os::raw::c_void) {
347
3.46k
    std::mem::drop(Box::from_raw(state as *mut SSHState));
348
3.46k
}
349
350
#[no_mangle]
351
34
pub extern "C" fn rs_ssh_state_tx_free(_state: *mut std::os::raw::c_void, _tx_id: u64) {
352
    //do nothing
353
34
}
354
355
#[no_mangle]
356
96.2k
pub unsafe extern "C" fn rs_ssh_parse_request(
357
96.2k
    _flow: *const Flow, state: *mut std::os::raw::c_void, pstate: *mut std::os::raw::c_void,
358
96.2k
    stream_slice: StreamSlice,
359
96.2k
    _data: *const std::os::raw::c_void
360
96.2k
) -> AppLayerResult {
361
96.2k
    let state = &mut cast_pointer!(state, SSHState);
362
96.2k
    let buf = stream_slice.as_slice();
363
96.2k
    let hdr = &mut state.transaction.cli_hdr;
364
96.2k
    state.transaction.tx_data.updated_ts = true;
365
96.2k
    if hdr.flags < SSHConnectionState::SshStateBannerDone {
366
20.5k
        return state.parse_banner(buf, false, pstate);
367
    } else {
368
75.6k
        return state.parse_record(buf, false, pstate);
369
    }
370
96.2k
}
371
372
#[no_mangle]
373
90.0k
pub unsafe extern "C" fn rs_ssh_parse_response(
374
90.0k
    _flow: *const Flow, state: *mut std::os::raw::c_void, pstate: *mut std::os::raw::c_void,
375
90.0k
    stream_slice: StreamSlice,
376
90.0k
    _data: *const std::os::raw::c_void
377
90.0k
) -> AppLayerResult {
378
90.0k
    let state = &mut cast_pointer!(state, SSHState);
379
90.0k
    let buf = stream_slice.as_slice();
380
90.0k
    let hdr = &mut state.transaction.srv_hdr;
381
90.0k
    state.transaction.tx_data.updated_tc = true;
382
90.0k
    if hdr.flags < SSHConnectionState::SshStateBannerDone {
383
76.9k
        return state.parse_banner(buf, true, pstate);
384
    } else {
385
13.1k
        return state.parse_record(buf, true, pstate);
386
    }
387
90.0k
}
388
389
#[no_mangle]
390
201k
pub unsafe extern "C" fn rs_ssh_state_get_tx(
391
201k
    state: *mut std::os::raw::c_void, _tx_id: u64,
392
201k
) -> *mut std::os::raw::c_void {
393
201k
    let state = cast_pointer!(state, SSHState);
394
201k
    return &state.transaction as *const _ as *mut _;
395
201k
}
396
397
#[no_mangle]
398
573k
pub extern "C" fn rs_ssh_state_get_tx_count(_state: *mut std::os::raw::c_void) -> u64 {
399
573k
    return 1;
400
573k
}
401
402
#[no_mangle]
403
0
pub unsafe extern "C" fn rs_ssh_tx_get_flags(
404
0
    tx: *mut std::os::raw::c_void, direction: u8,
405
0
) -> SSHConnectionState {
406
0
    let tx = cast_pointer!(tx, SSHTransaction);
407
0
    if direction == Direction::ToServer.into() {
408
0
        return tx.cli_hdr.flags;
409
    } else {
410
0
        return tx.srv_hdr.flags;
411
    }
412
0
}
413
414
#[no_mangle]
415
210k
pub unsafe extern "C" fn rs_ssh_tx_get_alstate_progress(
416
210k
    tx: *mut std::os::raw::c_void, direction: u8,
417
210k
) -> std::os::raw::c_int {
418
210k
    let tx = cast_pointer!(tx, SSHTransaction);
419
420
210k
    if tx.cli_hdr.flags >= SSHConnectionState::SshStateFinished
421
123k
        && tx.srv_hdr.flags >= SSHConnectionState::SshStateFinished
422
    {
423
127
        return SSHConnectionState::SshStateFinished as i32;
424
210k
    }
425
426
210k
    if direction == Direction::ToServer.into() {
427
14.4k
        if tx.cli_hdr.flags >= SSHConnectionState::SshStateBannerDone {
428
4.77k
            return SSHConnectionState::SshStateBannerDone as i32;
429
9.67k
        }
430
196k
    } else if tx.srv_hdr.flags >= SSHConnectionState::SshStateBannerDone {
431
27.6k
        return SSHConnectionState::SshStateBannerDone as i32;
432
168k
    }
433
434
178k
    return SSHConnectionState::SshStateInProgress as i32;
435
210k
}
436
437
// Parser name as a C style string.
438
const PARSER_NAME: &[u8] = b"ssh\0";
439
440
#[no_mangle]
441
34
pub unsafe extern "C" fn rs_ssh_register_parser() {
442
34
    let parser = RustParser {
443
34
        name: PARSER_NAME.as_ptr() as *const std::os::raw::c_char,
444
34
        default_port: std::ptr::null(),
445
34
        ipproto: IPPROTO_TCP,
446
34
        //simple patterns, no probing
447
34
        probe_ts: None,
448
34
        probe_tc: None,
449
34
        min_depth: 0,
450
34
        max_depth: 0,
451
34
        state_new: rs_ssh_state_new,
452
34
        state_free: rs_ssh_state_free,
453
34
        tx_free: rs_ssh_state_tx_free,
454
34
        parse_ts: rs_ssh_parse_request,
455
34
        parse_tc: rs_ssh_parse_response,
456
34
        get_tx_count: rs_ssh_state_get_tx_count,
457
34
        get_tx: rs_ssh_state_get_tx,
458
34
        tx_comp_st_ts: SSHConnectionState::SshStateFinished as i32,
459
34
        tx_comp_st_tc: SSHConnectionState::SshStateFinished as i32,
460
34
        tx_get_progress: rs_ssh_tx_get_alstate_progress,
461
34
        get_eventinfo: Some(SSHEvent::get_event_info),
462
34
        get_eventinfo_byid: Some(SSHEvent::get_event_info_by_id),
463
34
        localstorage_new: None,
464
34
        localstorage_free: None,
465
34
        get_tx_files: None,
466
34
        get_tx_iterator: None,
467
34
        get_tx_data: rs_ssh_get_tx_data,
468
34
        get_state_data: rs_ssh_get_state_data,
469
34
        apply_tx_config: None,
470
34
        flags: 0,
471
34
        truncate: None,
472
34
        get_frame_id_by_name: None,
473
34
        get_frame_name_by_id: None,
474
34
    };
475
476
34
    let ip_proto_str = CString::new("tcp").unwrap();
477
478
34
    if AppLayerProtoDetectConfProtoDetectionEnabled(ip_proto_str.as_ptr(), parser.name) != 0 {
479
34
        let alproto = AppLayerRegisterProtocolDetection(&parser, 1);
480
34
        ALPROTO_SSH = alproto;
481
34
        if AppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 {
482
34
            let _ = AppLayerRegisterParser(&parser, alproto);
483
34
        }
484
        SCLogDebug!("Rust ssh parser registered.");
485
    } else {
486
0
        SCLogNotice!("Protocol detector and parser disabled for SSH.");
487
    }
488
34
}
489
490
#[no_mangle]
491
10.0k
pub extern "C" fn rs_ssh_enable_hassh() {
492
10.0k
    HASSH_ENABLED.store(true, Ordering::Relaxed)
493
10.0k
}
494
495
#[no_mangle]
496
20.1k
pub extern "C" fn rs_ssh_hassh_is_enabled() -> bool {
497
20.1k
    hassh_is_enabled()
498
20.1k
}
499
500
#[no_mangle]
501
10.0k
pub unsafe extern "C" fn rs_ssh_tx_get_log_condition( tx: *mut std::os::raw::c_void) -> bool {
502
10.0k
    let tx = cast_pointer!(tx, SSHTransaction);
503
    
504
10.0k
    if rs_ssh_hassh_is_enabled() {
505
10.0k
        if  tx.cli_hdr.flags == SSHConnectionState::SshStateFinished &&
506
380
            tx.srv_hdr.flags == SSHConnectionState::SshStateFinished {
507
1
            return true; 
508
10.0k
        }
509
    }
510
0
    else if  tx.cli_hdr.flags == SSHConnectionState::SshStateBannerDone && 
511
0
        tx.srv_hdr.flags == SSHConnectionState::SshStateBannerDone {
512
0
        return true;
513
0
    }
514
10.0k
    return false;
515
10.0k
}