From 1b1fc8bff91cc0a363314e23dd0cc57ca8db38df Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Fri, 22 Dec 2023 12:01:33 +0100 Subject: [PATCH] app-layer: websockets protocol support Ticket: 2695 --- doc/userguide/rules/index.rst | 1 + doc/userguide/rules/intro.rst | 1 + doc/userguide/rules/websocket-keywords.rst | 57 +++ etc/schema.json | 24 ++ rules/Makefile.am | 3 +- rules/websocket-events.rules | 8 + rust/src/lib.rs | 1 + rust/src/websocket/detect.rs | 133 +++++++ rust/src/websocket/logger.rs | 45 +++ rust/src/websocket/mod.rs | 23 ++ rust/src/websocket/parser.rs | 94 +++++ rust/src/websocket/websocket.rs | 382 +++++++++++++++++++++ src/Makefile.am | 4 + src/app-layer-htp.c | 49 ++- src/app-layer-parser.c | 1 + src/app-layer-protos.c | 1 + src/app-layer-protos.h | 1 + src/detect-engine-register.c | 2 + src/detect-engine-register.h | 4 + src/detect-websocket.c | 251 ++++++++++++++ src/detect-websocket.h | 29 ++ src/output-json-websocket.c | 160 +++++++++ src/output-json-websocket.h | 29 ++ src/output.c | 4 + suricata.yaml.in | 5 + 25 files changed, 1296 insertions(+), 16 deletions(-) create mode 100644 doc/userguide/rules/websocket-keywords.rst create mode 100644 rules/websocket-events.rules create mode 100644 rust/src/websocket/detect.rs create mode 100644 rust/src/websocket/logger.rs create mode 100644 rust/src/websocket/mod.rs create mode 100644 rust/src/websocket/parser.rs create mode 100644 rust/src/websocket/websocket.rs create mode 100644 src/detect-websocket.c create mode 100644 src/detect-websocket.h create mode 100644 src/output-json-websocket.c create mode 100644 src/output-json-websocket.h diff --git a/doc/userguide/rules/index.rst b/doc/userguide/rules/index.rst index 2450f4486be9..2bd2a2d0ea48 100644 --- a/doc/userguide/rules/index.rst +++ b/doc/userguide/rules/index.rst @@ -36,6 +36,7 @@ Suricata Rules quic-keywords nfs-keywords smtp-keywords + websocket-keywords app-layer xbits thresholding diff --git a/doc/userguide/rules/intro.rst b/doc/userguide/rules/intro.rst index ab35f8a311ca..e2e9da58b610 100644 --- a/doc/userguide/rules/intro.rst +++ b/doc/userguide/rules/intro.rst @@ -110,6 +110,7 @@ you can pick from. These are: * snmp * tftp * sip +* websocket The availability of these protocols depends on whether the protocol is enabled in the configuration file, suricata.yaml. diff --git a/doc/userguide/rules/websocket-keywords.rst b/doc/userguide/rules/websocket-keywords.rst new file mode 100644 index 000000000000..e9f3104e85c3 --- /dev/null +++ b/doc/userguide/rules/websocket-keywords.rst @@ -0,0 +1,57 @@ +WebSocket Keywords +================== + +websocket.payload +----------------- + +A sticky buffer on the unmasked payload, +limited by suricata.yaml config value ``websocket.max-payload-size``. + +Examples:: + + websocket.payload; pcre:"/^123[0-9]*/"; + websocket.payload content:"swordfish"; + +``websocket.payload`` is a 'sticky buffer' and can be used as ``fast_pattern``. + +websocket.flags +--------------- + +Matches on the websocket flags. +It uses a 8-bit unsigned integer as value. +Only the four upper bits are used. + +The value can also be a list of strings (comma-separated), +where each string is the name of a specific bit like `fin` and `comp`, +and can be prefixed by `!` for negation. + +Examples:: + + websocket.flags:128; + websocket.flags:&0x40=0x40; + websocket.flags:fin,!comp; + +websocket.mask +-------------- + +Matches on the websocket mask if any. +It uses a 32-bit unsigned integer as value (big-endian). + +Examples:: + + websocket.mask:123456; + websocket.mask:>0; + +websocket.opcode +---------------- + +Matches on the websocket opcode. +It uses a 8-bit unsigned integer as value. +Only 16 values are relevant. +It can also be specified by text from the enumeration + +Examples:: + + websocket.opcode:1; + websocket.opcode:>8; + websocket.opcode:ping; diff --git a/etc/schema.json b/etc/schema.json index 0756acd00800..e78039d7b62c 100644 --- a/etc/schema.json +++ b/etc/schema.json @@ -3833,6 +3833,9 @@ }, "tls": { "$ref": "#/$defs/stats_applayer_error" + }, + "websocket": { + "$ref": "#/$defs/stats_applayer_error" } }, "additionalProperties": false @@ -3950,6 +3953,9 @@ }, "tls": { "type": "integer" + }, + "websocket": { + "type": "integer" } }, "additionalProperties": false @@ -4061,6 +4067,9 @@ }, "tls": { "type": "integer" + }, + "websocket": { + "type": "integer" } }, "additionalProperties": false @@ -5501,6 +5510,21 @@ } }, "additionalProperties": false + }, + "websocket": { + "type": "object", + "properties": { + "fin": { + "type": "boolean" + }, + "mask": { + "type": "integer" + }, + "opcode": { + "type": "string" + } + }, + "additionalProperties": false } }, "$defs": { diff --git a/rules/Makefile.am b/rules/Makefile.am index d0ea6eda622f..cba0aa370af3 100644 --- a/rules/Makefile.am +++ b/rules/Makefile.am @@ -22,4 +22,5 @@ smb-events.rules \ smtp-events.rules \ ssh-events.rules \ stream-events.rules \ -tls-events.rules +tls-events.rules \ +websocket-events.rules diff --git a/rules/websocket-events.rules b/rules/websocket-events.rules new file mode 100644 index 000000000000..3acc21132e0a --- /dev/null +++ b/rules/websocket-events.rules @@ -0,0 +1,8 @@ +# WebSocket app-layer event rules. +# +# These SIDs fall in the 2235000+ range. See: +# http://doc.emergingthreats.net/bin/view/Main/SidAllocation and +# https://redmine.openinfosecfoundation.org/projects/suricata/wiki/AppLayer + +alert websocket any any -> any any (msg:"SURICATA Websocket skipped end of payload"; app-layer-event:websocket.skip_end_of_payload; classtype:protocol-command-decode; sid:2235000; rev:1;) +alert websocket any any -> any any (msg:"SURICATA Websocket reassembly limit reached"; app-layer-event:websocket.reassembly_limit_reached; classtype:protocol-command-decode; sid:2235001; rev:1;) diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 15e21c4057d1..c49fd1894dbb 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -106,6 +106,7 @@ pub mod rfb; pub mod mqtt; pub mod pgsql; pub mod telnet; +pub mod websocket; pub mod applayertemplate; pub mod rdp; pub mod x509; diff --git a/rust/src/websocket/detect.rs b/rust/src/websocket/detect.rs new file mode 100644 index 000000000000..6088bcfa2024 --- /dev/null +++ b/rust/src/websocket/detect.rs @@ -0,0 +1,133 @@ +/* Copyright (C) 2023 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use super::websocket::WebSocketTransaction; +use crate::detect::uint::{detect_parse_uint, detect_parse_uint_enum, DetectUintData, DetectUintMode}; +use crate::websocket::parser::WebSocketOpcode; + +use nom7::branch::alt; +use nom7::bytes::complete::{is_a, tag}; +use nom7::combinator::{opt, value}; +use nom7::multi::many1; +use nom7::IResult; + +use std::ffi::CStr; + +#[no_mangle] +pub unsafe extern "C" fn SCWebSocketGetOpcode(tx: &mut WebSocketTransaction) -> u8 { + return tx.pdu.opcode; +} + +#[no_mangle] +pub unsafe extern "C" fn SCWebSocketGetFlags(tx: &mut WebSocketTransaction) -> u8 { + return tx.pdu.flags; +} + +#[no_mangle] +pub unsafe extern "C" fn SCWebSocketGetPayload( + tx: &WebSocketTransaction, buffer: *mut *const u8, buffer_len: *mut u32, +) -> bool { + *buffer = tx.pdu.payload.as_ptr(); + *buffer_len = tx.pdu.payload.len() as u32; + return true; +} + +#[no_mangle] +pub unsafe extern "C" fn SCWebSocketGetMask( + tx: &mut WebSocketTransaction, value: *mut u32, +) -> bool { + if let Some(xorkey) = tx.pdu.mask { + *value = xorkey; + return true; + } + return false; +} + +#[no_mangle] +pub unsafe extern "C" fn SCWebSocketParseOpcode( + ustr: *const std::os::raw::c_char, +) -> *mut DetectUintData { + let ft_name: &CStr = CStr::from_ptr(ustr); //unsafe + if let Ok(s) = ft_name.to_str() { + if let Some(ctx) = detect_parse_uint_enum::(s) { + let boxed = Box::new(ctx); + return Box::into_raw(boxed) as *mut _; + } + } + return std::ptr::null_mut(); +} + +struct WebSocketFlag { + neg: bool, + value: u8, +} + +fn parse_flag_list_item(s: &str) -> IResult<&str, WebSocketFlag> { + let (s, _) = opt(is_a(" "))(s)?; + let (s, neg) = opt(tag("!"))(s)?; + let neg = neg.is_some(); + let (s, value) = alt((value(0x80, tag("fin")), value(0x40, tag("comp"))))(s)?; + let (s, _) = opt(is_a(" ,"))(s)?; + Ok((s, WebSocketFlag { neg, value })) +} + +fn parse_flag_list(s: &str) -> IResult<&str, Vec> { + return many1(parse_flag_list_item)(s); +} + +fn parse_flags(s: &str) -> Option> { + // try first numerical value + if let Ok((_, ctx)) = detect_parse_uint::(s) { + return Some(ctx); + } + // otherwise, try strings for bitmask + if let Ok((_, l)) = parse_flag_list(s) { + let mut arg1 = 0; + let mut arg2 = 0; + for elem in l.iter() { + if elem.value & arg1 != 0 { + SCLogWarning!("Repeated bitflag for websocket.flags"); + return None; + } + arg1 |= elem.value; + if !elem.neg { + arg2 |= elem.value; + } + } + let ctx = DetectUintData:: { + arg1, + arg2, + mode: DetectUintMode::DetectUintModeBitmask, + }; + return Some(ctx); + } + return None; +} + +#[no_mangle] +pub unsafe extern "C" fn SCWebSocketParseFlags( + ustr: *const std::os::raw::c_char, +) -> *mut DetectUintData { + let ft_name: &CStr = CStr::from_ptr(ustr); //unsafe + if let Ok(s) = ft_name.to_str() { + if let Some(ctx) = parse_flags(s) { + let boxed = Box::new(ctx); + return Box::into_raw(boxed) as *mut _; + } + } + return std::ptr::null_mut(); +} diff --git a/rust/src/websocket/logger.rs b/rust/src/websocket/logger.rs new file mode 100644 index 000000000000..794ee1bac25d --- /dev/null +++ b/rust/src/websocket/logger.rs @@ -0,0 +1,45 @@ +/* Copyright (C) 2023 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use super::parser::WebSocketOpcode; +use super::websocket::WebSocketTransaction; +use crate::detect::Enum; +use crate::jsonbuilder::{JsonBuilder, JsonError}; +use std; + +fn log_websocket(tx: &WebSocketTransaction, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("websocket")?; + js.set_bool("fin", tx.pdu.fin)?; + if let Some(xorkey) = tx.pdu.mask { + js.set_uint("mask", xorkey.into())?; + } + if let Some(opcode) = WebSocketOpcode::from_u(tx.pdu.opcode) { + js.set_string("opcode", opcode.to_str())?; + } else { + js.set_string("opcode", &format!("unknown-{}", tx.pdu.opcode))?; + } + js.close()?; + Ok(()) +} + +#[no_mangle] +pub unsafe extern "C" fn rs_websocket_logger_log( + tx: *mut std::os::raw::c_void, js: &mut JsonBuilder, +) -> bool { + let tx = cast_pointer!(tx, WebSocketTransaction); + log_websocket(tx, js).is_ok() +} diff --git a/rust/src/websocket/mod.rs b/rust/src/websocket/mod.rs new file mode 100644 index 000000000000..c57660f2a44b --- /dev/null +++ b/rust/src/websocket/mod.rs @@ -0,0 +1,23 @@ +/* Copyright (C) 2023 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +//! Application layer websocket parser and logger module. + +pub mod detect; +pub mod logger; +mod parser; +pub mod websocket; diff --git a/rust/src/websocket/parser.rs b/rust/src/websocket/parser.rs new file mode 100644 index 000000000000..dac2adde1fca --- /dev/null +++ b/rust/src/websocket/parser.rs @@ -0,0 +1,94 @@ +/* Copyright (C) 2023 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use nom7::bytes::streaming::take; +use nom7::combinator::cond; +use nom7::number::streaming::{be_u16, be_u32, be_u64, be_u8}; +use nom7::IResult; +use suricata_derive::EnumStringU8; + +#[derive(Clone, Debug, Default, EnumStringU8)] +#[repr(u8)] +pub enum WebSocketOpcode { + #[default] + Continuation = 0, + Text = 1, + Binary = 2, + Ping = 8, + Pong = 9, +} + +#[derive(Clone, Debug, Default)] +pub struct WebSocketPdu { + pub flags: u8, + pub fin: bool, + pub compress: bool, + pub opcode: u8, + pub mask: Option, + pub payload: Vec, + pub to_skip: u64, +} + +// cf rfc6455#section-5.2 +pub fn parse_message(i: &[u8], max_pl_size: u64) -> IResult<&[u8], WebSocketPdu> { + let (i, flags_op) = be_u8(i)?; + let fin = (flags_op & 0x80) != 0; + let compress = (flags_op & 0x40) != 0; + let flags = flags_op & 0xF0; + let opcode = flags_op & 0xF; + let (i, mask_plen) = be_u8(i)?; + let mask_flag = (mask_plen & 0x80) != 0; + let (i, payload_len) = match mask_plen & 0x7F { + 126 => { + let (i, val) = be_u16(i)?; + Ok((i, val.into())) + } + 127 => be_u64(i), + _ => Ok((i, (mask_plen & 0x7F).into())), + }?; + let (i, xormask) = cond(mask_flag, take(4usize))(i)?; + let mask = if mask_flag { + let (_, m) = be_u32(xormask.unwrap())?; + Some(m) + } else { + None + }; + let (to_skip, payload_len) = if payload_len < max_pl_size { + (0, payload_len) + } else { + (payload_len - max_pl_size, max_pl_size) + }; + let (i, payload_raw) = take(payload_len)(i)?; + let mut payload = payload_raw.to_vec(); + if let Some(xorkey) = xormask { + for i in 0..payload.len() { + payload[i] ^= xorkey[i % 4]; + } + } + Ok(( + i, + WebSocketPdu { + flags, + fin, + compress, + opcode, + mask, + payload, + to_skip, + }, + )) +} diff --git a/rust/src/websocket/websocket.rs b/rust/src/websocket/websocket.rs new file mode 100644 index 000000000000..4722c953ad4b --- /dev/null +++ b/rust/src/websocket/websocket.rs @@ -0,0 +1,382 @@ +/* Copyright (C) 2023 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use super::parser; +use crate::applayer::{self, *}; +use crate::conf::conf_get; +use crate::core::{AppProto, Direction, Flow, ALPROTO_FAILED, ALPROTO_UNKNOWN, IPPROTO_TCP}; +use crate::frames::Frame; + +use nom7 as nom; +use nom7::Needed; + +use flate2::read::DeflateDecoder; + +use std; +use std::collections::VecDeque; +use std::ffi::CString; +use std::io::Read; +use std::os::raw::{c_char, c_int, c_void}; + +static mut ALPROTO_WEBSOCKET: AppProto = ALPROTO_UNKNOWN; + +static mut WEBSOCKET_MAX_PAYLOAD_SIZE: u64 = 0xFFFF; + +// app-layer-frame-documentation tag start: FrameType enum +#[derive(AppLayerFrameType)] +pub enum WebSocketFrameType { + Header, + Pdu, +} + +#[derive(AppLayerEvent)] +pub enum WebSocketEvent { + SkipEndOfPayload, + ReassemblyLimitReached, +} + +#[derive(Default)] +pub struct WebSocketTransaction { + tx_id: u64, + pub pdu: parser::WebSocketPdu, + tx_data: AppLayerTxData, +} + +impl WebSocketTransaction { + pub fn new(direction: Direction) -> WebSocketTransaction { + Self { + tx_data: AppLayerTxData::for_direction(direction), + ..Default::default() + } + } +} + +impl Transaction for WebSocketTransaction { + fn id(&self) -> u64 { + self.tx_id + } +} + +#[derive(Default)] +struct WebSocketReassemblyBuffer { + data: Vec, + compress: bool, +} + +#[derive(Default)] +pub struct WebSocketState { + state_data: AppLayerStateData, + tx_id: u64, + transactions: VecDeque, + + c2s_buf: WebSocketReassemblyBuffer, + s2c_buf: WebSocketReassemblyBuffer, + + to_skip_tc: u64, + to_skip_ts: u64, +} + +impl State for WebSocketState { + fn get_transaction_count(&self) -> usize { + self.transactions.len() + } + + fn get_transaction_by_index(&self, index: usize) -> Option<&WebSocketTransaction> { + self.transactions.get(index) + } +} + +impl WebSocketState { + pub fn new() -> Self { + Default::default() + } + + // Free a transaction by ID. + fn free_tx(&mut self, tx_id: u64) { + let len = self.transactions.len(); + let mut found = false; + let mut index = 0; + for i in 0..len { + let tx = &self.transactions[i]; + if tx.tx_id == tx_id + 1 { + found = true; + index = i; + break; + } + } + if found { + self.transactions.remove(index); + } + } + + pub fn get_tx(&mut self, tx_id: u64) -> Option<&WebSocketTransaction> { + self.transactions.iter().find(|tx| tx.tx_id == tx_id + 1) + } + + fn new_tx(&mut self, direction: Direction) -> WebSocketTransaction { + let mut tx = WebSocketTransaction::new(direction); + self.tx_id += 1; + tx.tx_id = self.tx_id; + return tx; + } + + fn parse( + &mut self, stream_slice: StreamSlice, direction: Direction, flow: *const Flow, + ) -> AppLayerResult { + let to_skip = if direction == Direction::ToClient { + &mut self.to_skip_tc + } else { + &mut self.to_skip_ts + }; + let input = stream_slice.as_slice(); + let mut start = input; + if *to_skip > 0 { + if *to_skip >= input.len() as u64 { + *to_skip -= input.len() as u64; + return AppLayerResult::ok(); + } else { + start = &input[*to_skip as usize..]; + *to_skip = 0; + } + } + + let max_pl_size = unsafe { WEBSOCKET_MAX_PAYLOAD_SIZE }; + while !start.is_empty() { + match parser::parse_message(start, max_pl_size) { + Ok((rem, pdu)) => { + let _pdu = Frame::new( + flow, + &stream_slice, + start, + (start.len() - rem.len() - pdu.payload.len()) as i64, + WebSocketFrameType::Header as u8, + ); + let _pdu = Frame::new( + flow, + &stream_slice, + start, + (start.len() - rem.len()) as i64, + WebSocketFrameType::Pdu as u8, + ); + start = rem; + let mut tx = self.new_tx(direction); + if pdu.to_skip > 0 { + if direction == Direction::ToClient { + self.to_skip_tc = pdu.to_skip; + } else { + self.to_skip_ts = pdu.to_skip; + } + tx.tx_data.set_event(WebSocketEvent::SkipEndOfPayload as u8); + } + let buf = if direction == Direction::ToClient { + &mut self.s2c_buf + } else { + &mut self.c2s_buf + }; + if !buf.data.is_empty() || !pdu.fin { + if buf.data.is_empty() { + buf.compress = pdu.compress; + } + if buf.data.len() + pdu.payload.len() < max_pl_size as usize { + buf.data.extend(&pdu.payload); + } else if buf.data.len() < max_pl_size as usize { + buf.data + .extend(&pdu.payload[..max_pl_size as usize - buf.data.len()]); + tx.tx_data + .set_event(WebSocketEvent::ReassemblyLimitReached as u8); + } + } + tx.pdu = pdu; + if tx.pdu.fin && !buf.data.is_empty() { + // the final PDU gets the full reassembled payload + std::mem::swap(&mut tx.pdu.payload, &mut buf.data); + buf.data.clear(); + } + if buf.compress && tx.pdu.fin { + buf.compress = false; + // cf RFC 7692 section-7.2.2 + tx.pdu.payload.extend_from_slice(&[0, 0, 0xFF, 0xFF]); + let mut deflater = DeflateDecoder::new(&tx.pdu.payload[..]); + let mut v = Vec::new(); + // do not check result because + // deflate with rust backend fails on good input cf https://github.com/rust-lang/flate2-rs/issues/389 + let _ = deflater.read_to_end(&mut v); + if !v.is_empty() { + std::mem::swap(&mut tx.pdu.payload, &mut v); + } + } + self.transactions.push_back(tx); + } + Err(nom::Err::Incomplete(needed)) => { + if let Needed::Size(n) = needed { + let n = usize::from(n); + // Not enough data. just ask for one more byte. + let consumed = input.len() - start.len(); + let needed = start.len() + n; + return AppLayerResult::incomplete(consumed as u32, needed as u32); + } + return AppLayerResult::err(); + } + Err(_) => { + return AppLayerResult::err(); + } + } + } + // Input was fully consumed. + return AppLayerResult::ok(); + } +} + +// C exports. + +#[no_mangle] +pub unsafe extern "C" fn rs_websocket_probing_parser( + _flow: *const Flow, _direction: u8, input: *const u8, input_len: u32, _rdir: *mut u8, +) -> AppProto { + if !input.is_null() { + let slice = build_slice!(input, input_len as usize); + if !slice.is_empty() { + // just check reserved bits are zeroed, except RSV1 + // as RSV1 is used for compression cf RFC 7692 + if slice[0] & 0x30 == 0 { + return ALPROTO_WEBSOCKET; + } + return ALPROTO_FAILED; + } + } + return ALPROTO_UNKNOWN; +} + +extern "C" fn rs_websocket_state_new( + _orig_state: *mut c_void, _orig_proto: AppProto, +) -> *mut c_void { + let state = WebSocketState::new(); + let boxed = Box::new(state); + return Box::into_raw(boxed) as *mut c_void; +} + +unsafe extern "C" fn rs_websocket_state_free(state: *mut c_void) { + std::mem::drop(Box::from_raw(state as *mut WebSocketState)); +} + +unsafe extern "C" fn rs_websocket_state_tx_free(state: *mut c_void, tx_id: u64) { + let state = cast_pointer!(state, WebSocketState); + state.free_tx(tx_id); +} + +unsafe extern "C" fn rs_websocket_parse_request( + flow: *const Flow, state: *mut c_void, _pstate: *mut c_void, stream_slice: StreamSlice, + _data: *const c_void, +) -> AppLayerResult { + let state = cast_pointer!(state, WebSocketState); + state.parse(stream_slice, Direction::ToServer, flow) +} + +unsafe extern "C" fn rs_websocket_parse_response( + flow: *const Flow, state: *mut c_void, _pstate: *mut c_void, stream_slice: StreamSlice, + _data: *const c_void, +) -> AppLayerResult { + let state = cast_pointer!(state, WebSocketState); + state.parse(stream_slice, Direction::ToClient, flow) +} + +unsafe extern "C" fn rs_websocket_state_get_tx(state: *mut c_void, tx_id: u64) -> *mut c_void { + let state = cast_pointer!(state, WebSocketState); + match state.get_tx(tx_id) { + Some(tx) => { + return tx as *const _ as *mut _; + } + None => { + return std::ptr::null_mut(); + } + } +} + +unsafe extern "C" fn rs_websocket_state_get_tx_count(state: *mut c_void) -> u64 { + let state = cast_pointer!(state, WebSocketState); + return state.tx_id; +} + +unsafe extern "C" fn rs_websocket_tx_get_alstate_progress( + _tx: *mut c_void, _direction: u8, +) -> c_int { + return 1; +} + +export_tx_data_get!(rs_websocket_get_tx_data, WebSocketTransaction); +export_state_data_get!(rs_websocket_get_state_data, WebSocketState); + +// Parser name as a C style string. +const PARSER_NAME: &[u8] = b"websocket\0"; + +#[no_mangle] +pub unsafe extern "C" fn rs_websocket_register_parser() { + let parser = RustParser { + name: PARSER_NAME.as_ptr() as *const c_char, + default_port: std::ptr::null(), + ipproto: IPPROTO_TCP, + probe_ts: Some(rs_websocket_probing_parser), + probe_tc: Some(rs_websocket_probing_parser), + min_depth: 0, + max_depth: 16, + state_new: rs_websocket_state_new, + state_free: rs_websocket_state_free, + tx_free: rs_websocket_state_tx_free, + parse_ts: rs_websocket_parse_request, + parse_tc: rs_websocket_parse_response, + get_tx_count: rs_websocket_state_get_tx_count, + get_tx: rs_websocket_state_get_tx, + tx_comp_st_ts: 1, + tx_comp_st_tc: 1, + tx_get_progress: rs_websocket_tx_get_alstate_progress, + get_eventinfo: Some(WebSocketEvent::get_event_info), + get_eventinfo_byid: Some(WebSocketEvent::get_event_info_by_id), + localstorage_new: None, + localstorage_free: None, + get_tx_files: None, + get_tx_iterator: Some( + applayer::state_get_tx_iterator::, + ), + get_tx_data: rs_websocket_get_tx_data, + get_state_data: rs_websocket_get_state_data, + apply_tx_config: None, + flags: 0, // do not accept gaps as there is no good way to resync + truncate: None, + get_frame_id_by_name: Some(WebSocketFrameType::ffi_id_from_name), + get_frame_name_by_id: Some(WebSocketFrameType::ffi_name_from_id), + }; + + let ip_proto_str = CString::new("tcp").unwrap(); + + if AppLayerProtoDetectConfProtoDetectionEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let alproto = AppLayerRegisterProtocolDetection(&parser, 1); + ALPROTO_WEBSOCKET = alproto; + if AppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let _ = AppLayerRegisterParser(&parser, alproto); + } + SCLogDebug!("Rust websocket parser registered."); + if let Some(val) = conf_get("app-layer.protocols.websocket.max-payload-size") { + if let Ok(v) = val.parse::() { + WEBSOCKET_MAX_PAYLOAD_SIZE = v; + } else { + SCLogError!("Invalid value for websocket.max-payload-size"); + } + } + } else { + SCLogDebug!("Protocol detector and parser disabled for WEBSOCKET."); + } +} diff --git a/src/Makefile.am b/src/Makefile.am index 133ed47cd1e8..58727a0f0464 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -358,6 +358,7 @@ noinst_HEADERS = \ detect-urilen.h \ detect-within.h \ detect-xbits.h \ + detect-websocket.h \ device-storage.h \ feature.h \ flow-bit.h \ @@ -431,6 +432,7 @@ noinst_HEADERS = \ output-json-snmp.h \ output-json-ssh.h \ output-json-stats.h \ + output-json-websocket.h \ output-json-template.h \ output-json-tftp.h \ output-json-tls.h \ @@ -973,6 +975,7 @@ libsuricata_c_a_SOURCES = \ detect-urilen.c \ detect-within.c \ detect-xbits.c \ + detect-websocket.c \ device-storage.c \ feature.c \ flow-bit.c \ @@ -1045,6 +1048,7 @@ libsuricata_c_a_SOURCES = \ output-json-snmp.c \ output-json-ssh.c \ output-json-stats.c \ + output-json-websocket.c \ output-json-template.c \ output-json-tftp.c \ output-json-tls.c \ diff --git a/src/app-layer-htp.c b/src/app-layer-htp.c index 5d48611812c1..5ccbac83cf1c 100644 --- a/src/app-layer-htp.c +++ b/src/app-layer-htp.c @@ -53,6 +53,7 @@ #include "app-layer-protos.h" #include "app-layer-parser.h" +#include "app-layer-expectation.h" #include "app-layer.h" #include "app-layer-detect-proto.h" @@ -975,11 +976,7 @@ static AppLayerResult HTPHandleResponseData(Flow *f, void *htp_state, AppLayerPa if (tx != NULL && tx->response_status_number == 101) { htp_header_t *h = (htp_header_t *)htp_table_get_c(tx->response_headers, "Upgrade"); - if (h == NULL || bstr_cmp_c(h->value, "h2c") != 0) { - break; - } - if (AppLayerProtoDetectGetProtoName(ALPROTO_HTTP2) == NULL) { - // if HTTP2 is disabled, keep the HTP_STREAM_TUNNEL mode + if (h == NULL) { break; } uint16_t dp = 0; @@ -987,17 +984,39 @@ static AppLayerResult HTPHandleResponseData(Flow *f, void *htp_state, AppLayerPa dp = (uint16_t)tx->request_port_number; } consumed = htp_connp_res_data_consumed(hstate->connp); - hstate->slice = NULL; - if (!AppLayerRequestProtocolChange(hstate->f, dp, ALPROTO_HTTP2)) { - HTPSetEvent(hstate, NULL, STREAM_TOCLIENT, - HTTP_DECODER_EVENT_FAILED_PROTOCOL_CHANGE); - } - // During HTTP2 upgrade, we may consume the HTTP1 part of the data - // and we need to parser the remaining part with HTTP2 - if (consumed > 0 && consumed < input_len) { - SCReturnStruct(APP_LAYER_INCOMPLETE(consumed, input_len - consumed)); + if (bstr_cmp_c(h->value, "h2c") == 0) { + if (AppLayerProtoDetectGetProtoName(ALPROTO_HTTP2) == NULL) { + // if HTTP2 is disabled, keep the HTP_STREAM_TUNNEL mode + break; + } + hstate->slice = NULL; + if (!AppLayerRequestProtocolChange(hstate->f, dp, ALPROTO_HTTP2)) { + HTPSetEvent(hstate, NULL, STREAM_TOCLIENT, + HTTP_DECODER_EVENT_FAILED_PROTOCOL_CHANGE); + } + // During HTTP2 upgrade, we may consume the HTTP1 part of the data + // and we need to parser the remaining part with HTTP2 + if (consumed > 0 && consumed < input_len) { + SCReturnStruct(APP_LAYER_INCOMPLETE(consumed, input_len - consumed)); + } + SCReturnStruct(APP_LAYER_OK); + } else if (bstr_cmp_c_nocase(h->value, "WebSocket") == 0) { + if (AppLayerProtoDetectGetProtoName(ALPROTO_WEBSOCKET) == NULL) { + // if WS is disabled, keep the HTP_STREAM_TUNNEL mode + break; + } + hstate->slice = NULL; + if (!AppLayerRequestProtocolChange(hstate->f, dp, ALPROTO_WEBSOCKET)) { + HTPSetEvent(hstate, NULL, STREAM_TOCLIENT, + HTTP_DECODER_EVENT_FAILED_PROTOCOL_CHANGE); + } + // During WS upgrade, we may consume the HTTP1 part of the data + // and we need to parser the remaining part with WS + if (consumed > 0 && consumed < input_len) { + SCReturnStruct(APP_LAYER_INCOMPLETE(consumed, input_len - consumed)); + } + SCReturnStruct(APP_LAYER_OK); } - SCReturnStruct(APP_LAYER_OK); } break; default: diff --git a/src/app-layer-parser.c b/src/app-layer-parser.c index 96fc607fd257..5cbe10978915 100644 --- a/src/app-layer-parser.c +++ b/src/app-layer-parser.c @@ -1763,6 +1763,7 @@ void AppLayerParserRegisterProtocolParsers(void) RegisterSNMPParsers(); RegisterSIPParsers(); RegisterQuicParsers(); + rs_websocket_register_parser(); rs_template_register_parser(); RegisterRFBParsers(); SCMqttRegisterParser(); diff --git a/src/app-layer-protos.c b/src/app-layer-protos.c index 368efacd88d7..b6e1b73d08d4 100644 --- a/src/app-layer-protos.c +++ b/src/app-layer-protos.c @@ -60,6 +60,7 @@ const AppProtoStringTuple AppProtoStrings[ALPROTO_MAX] = { { ALPROTO_MQTT, "mqtt" }, { ALPROTO_PGSQL, "pgsql" }, { ALPROTO_TELNET, "telnet" }, + { ALPROTO_WEBSOCKET, "websocket" }, { ALPROTO_TEMPLATE, "template" }, { ALPROTO_RDP, "rdp" }, { ALPROTO_HTTP2, "http2" }, diff --git a/src/app-layer-protos.h b/src/app-layer-protos.h index dd372550cbf5..5ecc5d88d31a 100644 --- a/src/app-layer-protos.h +++ b/src/app-layer-protos.h @@ -56,6 +56,7 @@ enum AppProtoEnum { ALPROTO_MQTT, ALPROTO_PGSQL, ALPROTO_TELNET, + ALPROTO_WEBSOCKET, ALPROTO_TEMPLATE, ALPROTO_RDP, ALPROTO_HTTP2, diff --git a/src/detect-engine-register.c b/src/detect-engine-register.c index 9f37e0945544..06465b49cfd1 100644 --- a/src/detect-engine-register.c +++ b/src/detect-engine-register.c @@ -237,6 +237,7 @@ #include "detect-quic-version.h" #include "detect-quic-cyu-hash.h" #include "detect-quic-cyu-string.h" +#include "detect-websocket.h" #include "detect-bypass.h" #include "detect-ftpdata.h" @@ -700,6 +701,7 @@ void SigTableSetup(void) DetectQuicVersionRegister(); DetectQuicCyuHashRegister(); DetectQuicCyuStringRegister(); + DetectWebsocketRegister(); DetectBypassRegister(); DetectConfigRegister(); diff --git a/src/detect-engine-register.h b/src/detect-engine-register.h index 9dd01f5fd487..7ce625cd59c6 100644 --- a/src/detect-engine-register.h +++ b/src/detect-engine-register.h @@ -316,6 +316,10 @@ enum DetectKeywordId { DETECT_AL_QUIC_UA, DETECT_AL_QUIC_CYU_HASH, DETECT_AL_QUIC_CYU_STRING, + DETECT_WEBSOCKET_MASK, + DETECT_WEBSOCKET_OPCODE, + DETECT_WEBSOCKET_FLAGS, + DETECT_WEBSOCKET_PAYLOAD, DETECT_BYPASS, diff --git a/src/detect-websocket.c b/src/detect-websocket.c new file mode 100644 index 000000000000..91f650c85c55 --- /dev/null +++ b/src/detect-websocket.c @@ -0,0 +1,251 @@ +/* Copyright (C) 2023 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * \file + * + * \author Philippe Antoine + */ + +#include "suricata-common.h" +#include "detect.h" +#include "detect-parse.h" +#include "detect-engine.h" +#include "detect-engine-content-inspection.h" +#include "detect-engine-uint.h" +#include "detect-engine-prefilter.h" +#include "detect-websocket.h" + +#include "rust.h" + +static int websocket_tx_id = 0; +static int websocket_payload_id = 0; + +/** + * \internal + * \brief this function will free memory associated with DetectWebSocketOpcodeData + * + * \param de pointer to DetectWebSocketOpcodeData + */ +static void DetectWebSocketOpcodeFree(DetectEngineCtx *de_ctx, void *de_ptr) +{ + rs_detect_u8_free(de_ptr); +} + +/** + * \internal + * \brief Function to match opcode of a websocket tx + * + * \param det_ctx Pointer to the pattern matcher thread. + * \param f Pointer to the current flow. + * \param flags Flags. + * \param state App layer state. + * \param txv Pointer to the transaction. + * \param s Pointer to the Signature. + * \param ctx Pointer to the sigmatch that we will cast into DetectWebSocketOpcodeData. + * + * \retval 0 no match. + * \retval 1 match. + */ +static int DetectWebSocketOpcodeMatch(DetectEngineThreadCtx *det_ctx, Flow *f, uint8_t flags, + void *state, void *txv, const Signature *s, const SigMatchCtx *ctx) +{ + const DetectU8Data *de = (const DetectU8Data *)ctx; + uint8_t opc = SCWebSocketGetOpcode(txv); + return DetectU8Match(opc, de); +} + +/** + * \internal + * \brief this function is used to add the parsed sigmatch into the current signature + * + * \param de_ctx pointer to the Detection Engine Context + * \param s pointer to the Current Signature + * \param rawstr pointer to the user provided options + * + * \retval 0 on Success + * \retval -1 on Failure + */ +static int DetectWebSocketOpcodeSetup(DetectEngineCtx *de_ctx, Signature *s, const char *rawstr) +{ + if (DetectSignatureSetAppProto(s, ALPROTO_WEBSOCKET) < 0) + return -1; + + DetectU8Data *de = SCWebSocketParseOpcode(rawstr); + if (de == NULL) + return -1; + + if (SigMatchAppendSMToList( + de_ctx, s, DETECT_WEBSOCKET_OPCODE, (SigMatchCtx *)de, websocket_tx_id) == NULL) { + DetectWebSocketOpcodeFree(de_ctx, de); + return -1; + } + + return 0; +} + +/** + * \internal + * \brief this function will free memory associated with DetectWebSocketMaskData + * + * \param de pointer to DetectWebSocketMaskData + */ +static void DetectWebSocketMaskFree(DetectEngineCtx *de_ctx, void *de_ptr) +{ + rs_detect_u32_free(de_ptr); +} + +static int DetectWebSocketMaskMatch(DetectEngineThreadCtx *det_ctx, Flow *f, uint8_t flags, + void *state, void *txv, const Signature *s, const SigMatchCtx *ctx) +{ + uint32_t val; + const DetectU32Data *du32 = (const DetectU32Data *)ctx; + if (SCWebSocketGetMask(txv, &val)) { + return DetectU32Match(val, du32); + } + return 0; +} + +static int DetectWebSocketMaskSetup(DetectEngineCtx *de_ctx, Signature *s, const char *rawstr) +{ + if (DetectSignatureSetAppProto(s, ALPROTO_WEBSOCKET) < 0) + return -1; + + DetectU32Data *du32 = DetectU32Parse(rawstr); + if (du32 == NULL) + return -1; + + if (SigMatchAppendSMToList( + de_ctx, s, DETECT_WEBSOCKET_MASK, (SigMatchCtx *)du32, websocket_tx_id) == NULL) { + DetectWebSocketMaskFree(de_ctx, du32); + return -1; + } + + return 0; +} + +static void DetectWebSocketFlagsFree(DetectEngineCtx *de_ctx, void *de_ptr) +{ + rs_detect_u8_free(de_ptr); +} + +static int DetectWebSocketFlagsMatch(DetectEngineThreadCtx *det_ctx, Flow *f, uint8_t flags, + void *state, void *txv, const Signature *s, const SigMatchCtx *ctx) +{ + const DetectU8Data *de = (const DetectU8Data *)ctx; + uint8_t val = SCWebSocketGetFlags(txv); + return DetectU8Match(val, de); +} + +static int DetectWebSocketFlagsSetup(DetectEngineCtx *de_ctx, Signature *s, const char *rawstr) +{ + if (DetectSignatureSetAppProto(s, ALPROTO_WEBSOCKET) < 0) + return -1; + + DetectU8Data *de = SCWebSocketParseFlags(rawstr); + if (de == NULL) + return -1; + + if (SigMatchAppendSMToList( + de_ctx, s, DETECT_WEBSOCKET_FLAGS, (SigMatchCtx *)de, websocket_tx_id) == NULL) { + DetectWebSocketOpcodeFree(de_ctx, de); + return -1; + } + + return 0; +} + +static int DetectWebSocketPayloadSetup(DetectEngineCtx *de_ctx, Signature *s, const char *rulestr) +{ + if (DetectBufferSetActiveList(de_ctx, s, websocket_payload_id) < 0) + return -1; + + if (DetectSignatureSetAppProto(s, ALPROTO_WEBSOCKET) != 0) + return -1; + + return 0; +} + +static InspectionBuffer *GetData(DetectEngineThreadCtx *det_ctx, + const DetectEngineTransforms *transforms, Flow *_f, const uint8_t _flow_flags, void *txv, + const int list_id) +{ + InspectionBuffer *buffer = InspectionBufferGet(det_ctx, list_id); + if (buffer->inspect == NULL) { + const uint8_t *b = NULL; + uint32_t b_len = 0; + + if (!SCWebSocketGetPayload(txv, &b, &b_len)) + return NULL; + if (b == NULL || b_len == 0) + return NULL; + + InspectionBufferSetup(det_ctx, list_id, buffer, b, b_len); + InspectionBufferApplyTransforms(buffer, transforms); + } + return buffer; +} + +/** + * \brief Registration function for websocket.opcode: keyword + */ +void DetectWebsocketRegister(void) +{ + sigmatch_table[DETECT_WEBSOCKET_OPCODE].name = "websocket.opcode"; + sigmatch_table[DETECT_WEBSOCKET_OPCODE].desc = "match WebSocket opcode"; + sigmatch_table[DETECT_WEBSOCKET_OPCODE].url = "/rules/websocket-keywords.html#websocket-opcode"; + sigmatch_table[DETECT_WEBSOCKET_OPCODE].AppLayerTxMatch = DetectWebSocketOpcodeMatch; + sigmatch_table[DETECT_WEBSOCKET_OPCODE].Setup = DetectWebSocketOpcodeSetup; + sigmatch_table[DETECT_WEBSOCKET_OPCODE].Free = DetectWebSocketOpcodeFree; + + DetectAppLayerInspectEngineRegister("websocket.tx", ALPROTO_WEBSOCKET, SIG_FLAG_TOSERVER, 1, + DetectEngineInspectGenericList, NULL); + DetectAppLayerInspectEngineRegister("websocket.tx", ALPROTO_WEBSOCKET, SIG_FLAG_TOCLIENT, 1, + DetectEngineInspectGenericList, NULL); + + websocket_tx_id = DetectBufferTypeGetByName("websocket.tx"); + + sigmatch_table[DETECT_WEBSOCKET_MASK].name = "websocket.mask"; + sigmatch_table[DETECT_WEBSOCKET_MASK].desc = "match WebSocket mask"; + sigmatch_table[DETECT_WEBSOCKET_MASK].url = "/rules/websocket-keywords.html#websocket-mask"; + sigmatch_table[DETECT_WEBSOCKET_MASK].AppLayerTxMatch = DetectWebSocketMaskMatch; + sigmatch_table[DETECT_WEBSOCKET_MASK].Setup = DetectWebSocketMaskSetup; + sigmatch_table[DETECT_WEBSOCKET_MASK].Free = DetectWebSocketMaskFree; + + sigmatch_table[DETECT_WEBSOCKET_FLAGS].name = "websocket.flags"; + sigmatch_table[DETECT_WEBSOCKET_FLAGS].desc = "match WebSocket flags"; + sigmatch_table[DETECT_WEBSOCKET_FLAGS].url = "/rules/websocket-keywords.html#websocket-flags"; + sigmatch_table[DETECT_WEBSOCKET_FLAGS].AppLayerTxMatch = DetectWebSocketFlagsMatch; + sigmatch_table[DETECT_WEBSOCKET_FLAGS].Setup = DetectWebSocketFlagsSetup; + sigmatch_table[DETECT_WEBSOCKET_FLAGS].Free = DetectWebSocketFlagsFree; + + sigmatch_table[DETECT_WEBSOCKET_PAYLOAD].name = "websocket.payload"; + sigmatch_table[DETECT_WEBSOCKET_PAYLOAD].desc = "match WebSocket payload"; + sigmatch_table[DETECT_WEBSOCKET_PAYLOAD].url = + "/rules/websocket-keywords.html#websocket-payload"; + sigmatch_table[DETECT_WEBSOCKET_PAYLOAD].Setup = DetectWebSocketPayloadSetup; + sigmatch_table[DETECT_WEBSOCKET_PAYLOAD].flags |= SIGMATCH_NOOPT; + DetectAppLayerInspectEngineRegister("websocket.payload", ALPROTO_WEBSOCKET, SIG_FLAG_TOSERVER, + 0, DetectEngineInspectBufferGeneric, GetData); + DetectAppLayerInspectEngineRegister("websocket.payload", ALPROTO_WEBSOCKET, SIG_FLAG_TOCLIENT, + 0, DetectEngineInspectBufferGeneric, GetData); + DetectAppLayerMpmRegister("websocket.payload", SIG_FLAG_TOSERVER, 2, + PrefilterGenericMpmRegister, GetData, ALPROTO_WEBSOCKET, 1); + DetectAppLayerMpmRegister("websocket.payload", SIG_FLAG_TOCLIENT, 2, + PrefilterGenericMpmRegister, GetData, ALPROTO_WEBSOCKET, 1); + websocket_payload_id = DetectBufferTypeGetByName("websocket.payload"); +} diff --git a/src/detect-websocket.h b/src/detect-websocket.h new file mode 100644 index 000000000000..54e8a22ae4a8 --- /dev/null +++ b/src/detect-websocket.h @@ -0,0 +1,29 @@ +/* Copyright (C) 2023 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * \file + * + * \author Philippe Antoine + */ + +#ifndef __DETECT_WEBSOCKET_H__ +#define __DETECT_WEBSOCKET_H__ + +void DetectWebsocketRegister(void); + +#endif /* __DETECT_WEBSOCKET_H__ */ diff --git a/src/output-json-websocket.c b/src/output-json-websocket.c new file mode 100644 index 000000000000..9878bcc74ba6 --- /dev/null +++ b/src/output-json-websocket.c @@ -0,0 +1,160 @@ +/* Copyright (C) 2023 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * \file + * + * \author Philippe Antoine + * + * Implement JSON/eve logging app-layer WebSocket. + */ + +#include "suricata-common.h" +#include "detect.h" +#include "pkt-var.h" +#include "conf.h" + +#include "threads.h" +#include "threadvars.h" +#include "tm-threads.h" + +#include "util-unittest.h" +#include "util-buffer.h" +#include "util-debug.h" +#include "util-byte.h" + +#include "output.h" +#include "output-json.h" + +#include "app-layer.h" +#include "app-layer-parser.h" + +#include "output-json-websocket.h" +#include "rust.h" + +typedef struct LogWebSocketFileCtx_ { + uint32_t flags; + OutputJsonCtx *eve_ctx; +} LogWebSocketFileCtx; + +typedef struct LogWebSocketLogThread_ { + LogWebSocketFileCtx *websocketlog_ctx; + OutputJsonThreadCtx *ctx; +} LogWebSocketLogThread; + +static int JsonWebSocketLogger(ThreadVars *tv, void *thread_data, const Packet *p, Flow *f, + void *state, void *tx, uint64_t tx_id) +{ + LogWebSocketLogThread *thread = thread_data; + + JsonBuilder *js = CreateEveHeader( + p, LOG_DIR_PACKET, "websocket", NULL, thread->websocketlog_ctx->eve_ctx); + if (unlikely(js == NULL)) { + return TM_ECODE_FAILED; + } + + if (!rs_websocket_logger_log(tx, js)) { + goto error; + } + + OutputJsonBuilderBuffer(js, thread->ctx); + jb_free(js); + + return TM_ECODE_OK; + +error: + jb_free(js); + return TM_ECODE_FAILED; +} + +static void OutputWebSocketLogDeInitCtxSub(OutputCtx *output_ctx) +{ + LogWebSocketFileCtx *websocketlog_ctx = (LogWebSocketFileCtx *)output_ctx->data; + SCFree(websocketlog_ctx); + SCFree(output_ctx); +} + +static OutputInitResult OutputWebSocketLogInitSub(ConfNode *conf, OutputCtx *parent_ctx) +{ + OutputInitResult result = { NULL, false }; + OutputJsonCtx *ajt = parent_ctx->data; + + LogWebSocketFileCtx *websocketlog_ctx = SCCalloc(1, sizeof(*websocketlog_ctx)); + if (unlikely(websocketlog_ctx == NULL)) { + return result; + } + websocketlog_ctx->eve_ctx = ajt; + + OutputCtx *output_ctx = SCCalloc(1, sizeof(*output_ctx)); + if (unlikely(output_ctx == NULL)) { + SCFree(websocketlog_ctx); + return result; + } + output_ctx->data = websocketlog_ctx; + output_ctx->DeInit = OutputWebSocketLogDeInitCtxSub; + + AppLayerParserRegisterLogger(IPPROTO_TCP, ALPROTO_WEBSOCKET); + + result.ctx = output_ctx; + result.ok = true; + return result; +} + +static TmEcode JsonWebSocketLogThreadInit(ThreadVars *t, const void *initdata, void **data) +{ + LogWebSocketLogThread *thread = SCCalloc(1, sizeof(*thread)); + if (unlikely(thread == NULL)) { + return TM_ECODE_FAILED; + } + + if (initdata == NULL) { + SCLogDebug("Error getting context for EveLogWebSocket. \"initdata\" is NULL."); + goto error_exit; + } + + thread->websocketlog_ctx = ((OutputCtx *)initdata)->data; + thread->ctx = CreateEveThreadCtx(t, thread->websocketlog_ctx->eve_ctx); + if (!thread->ctx) { + goto error_exit; + } + *data = (void *)thread; + + return TM_ECODE_OK; + +error_exit: + SCFree(thread); + return TM_ECODE_FAILED; +} + +static TmEcode JsonWebSocketLogThreadDeinit(ThreadVars *t, void *data) +{ + LogWebSocketLogThread *thread = (LogWebSocketLogThread *)data; + if (thread == NULL) { + return TM_ECODE_OK; + } + FreeEveThreadCtx(thread->ctx); + SCFree(thread); + return TM_ECODE_OK; +} + +void JsonWebSocketLogRegister(void) +{ + /* Register as an eve sub-module. */ + OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonWebSocketLog", "eve-log.websocket", + OutputWebSocketLogInitSub, ALPROTO_WEBSOCKET, JsonWebSocketLogger, + JsonWebSocketLogThreadInit, JsonWebSocketLogThreadDeinit, NULL); +} diff --git a/src/output-json-websocket.h b/src/output-json-websocket.h new file mode 100644 index 000000000000..481df78c0aee --- /dev/null +++ b/src/output-json-websocket.h @@ -0,0 +1,29 @@ +/* Copyright (C) 2023 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * \file + * + * \author FirstName LastName + */ + +#ifndef __OUTPUT_JSON_WEBSOCKET_H__ +#define __OUTPUT_JSON_WEBSOCKET_H__ + +void JsonWebSocketLogRegister(void); + +#endif /* __OUTPUT_JSON_WEBSOCKET_H__ */ diff --git a/src/output.c b/src/output.c index 149dda58c284..df8a18f91ffd 100644 --- a/src/output.c +++ b/src/output.c @@ -80,6 +80,7 @@ #include "output-json-rfb.h" #include "output-json-mqtt.h" #include "output-json-pgsql.h" +#include "output-json-websocket.h" #include "output-json-template.h" #include "output-json-rdp.h" #include "output-json-http2.h" @@ -1117,6 +1118,8 @@ void OutputRegisterLoggers(void) JsonMQTTLogRegister(); /* Pgsql JSON logger. */ JsonPgsqlLogRegister(); + /* WebSocket JSON logger. */ + JsonWebSocketLogRegister(); /* Template JSON logger. */ JsonTemplateLogRegister(); /* RDP JSON logger. */ @@ -1159,6 +1162,7 @@ static EveJsonSimpleAppLayerLogger simple_json_applayer_loggers[ALPROTO_MAX] = { { ALPROTO_MQTT, JsonMQTTAddMetadata }, { ALPROTO_PGSQL, NULL }, // TODO missing { ALPROTO_TELNET, NULL }, // no logging + { ALPROTO_WEBSOCKET, rs_websocket_logger_log }, { ALPROTO_TEMPLATE, rs_template_logger_log }, { ALPROTO_RDP, (EveJsonSimpleTxLogFunc)rs_rdp_to_json }, { ALPROTO_HTTP2, rs_http2_log_json }, diff --git a/suricata.yaml.in b/suricata.yaml.in index 630399126dbe..181008b7086c 100644 --- a/suricata.yaml.in +++ b/suricata.yaml.in @@ -279,6 +279,7 @@ outputs: #md5: [body, subject] #- dnp3 + - websocket - ftp - rdp - nfs @@ -923,6 +924,10 @@ app-layer: ftp: enabled: yes # memcap: 64mb + websocket: + #enabled: yes + # Maximum used payload size, the rest is skipped + # max-payload-size: 65535 rdp: #enabled: yes ssh: