diff --git a/etc/schema.json b/etc/schema.json index f7f6c9588499..7dc4845afa4d 100644 --- a/etc/schema.json +++ b/etc/schema.json @@ -4495,6 +4495,11 @@ }, "additionalProperties": false }, + "socks": { + "type": "object", + "optional": true, + "additionalProperties": true + }, "ssh": { "type": "object", "optional": true, @@ -4732,6 +4737,10 @@ "description": "Errors encountered parsing SNMP", "$ref": "#/$defs/stats_applayer_error" }, + "socks": { + "description": "Errors encountered parsing SOCKS", + "$ref": "#/$defs/stats_applayer_error" + }, "ssh": { "description": "Errors encountered parsing SSH protocol", "$ref": "#/$defs/stats_applayer_error" @@ -4907,6 +4916,10 @@ "description": "Number of flows for SNMP", "type": "integer" }, + "socks": { + "description": "Number of flows for SOCKS", + "type": "integer" + }, "ssh": { "description": "Number of flows for SSH protocol", "type": "integer" @@ -5077,6 +5090,10 @@ "description": "Number of transactions for SNMP", "type": "integer" }, + "socks": { + "description": "Number of transactions for SOCKS", + "type": "integer" + }, "ssh": { "description": "Number of transactions for SSH protocol", "type": "integer" diff --git a/rust/src/lib.rs b/rust/src/lib.rs index bea7854f107e..243fa22cd908 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -123,6 +123,7 @@ pub mod pgsql; pub mod telnet; pub mod websocket; pub mod enip; +pub mod socks; pub mod applayertemplate; pub mod rdp; pub mod x509; diff --git a/rust/src/socks/logger.rs b/rust/src/socks/logger.rs new file mode 100644 index 000000000000..71f61a703afa --- /dev/null +++ b/rust/src/socks/logger.rs @@ -0,0 +1,98 @@ +/* Copyright (C) 2024 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::socks::SocksTransaction; +use crate::dns::log::dns_print_addr; +use crate::jsonbuilder::{JsonBuilder, JsonError}; +use std; + +fn auth_method_string(m: u8) -> String { + match m { + 0 => "No authentication", + 1 => "GSSAPI", + 2 => "Username/Password", + _ => { + return m.to_string(); + } + } + .to_string() +} + +fn status_string(m: u8) -> String { + match m { + 0 => "Success", + _ => { + return m.to_string(); + } + } + .to_string() +} + +fn log_socks(tx: &SocksTransaction, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("socks")?; + if let Some(ref connect) = tx.connect { + js.open_object("connect")?; + if let Some(ref domain) = &connect.domain { + let domain = String::from_utf8_lossy(domain); + js.set_string("domain", &domain)?; + } + if let Some(ref ipv4) = &connect.ipv4 { + js.set_string("ipv4", &dns_print_addr(ipv4))?; + } + js.set_uint("port", connect.port as u64)?; + if let Some(status) = connect.response { + js.set_string("response", &status_string(status))?; + } + js.close()?; + } + if let Some(ref auth) = tx.auth_userpass { + js.open_object("auth_userpass")?; + js.set_uint("subnegotiation_version", auth.subver as u64)?; + let user = String::from_utf8_lossy(&auth.user); + js.set_string("user", &user)?; + // TODO needs to be optional and disabled by default + let pass = String::from_utf8_lossy(&auth.pass); + js.set_string("pass", &pass)?; + if let Some(status) = auth.response { + js.set_string("response", &status_string(status))?; + } + js.close()?; + } + if let Some(ref auth_methods) = tx.auth_methods { + js.open_object("auth_methods")?; + js.open_array("request")?; + for m in &auth_methods.request_methods { + js.append_string(&auth_method_string(*m))?; + } + js.close()?; + js.set_string( + "response", + &auth_method_string(auth_methods.response_method), + )?; + js.close()?; + } + js.close()?; + Ok(()) +} + +#[no_mangle] +pub unsafe extern "C" fn rs_socks_logger_log( + tx: *mut std::os::raw::c_void, js: &mut JsonBuilder, +) -> bool { + let tx = cast_pointer!(tx, SocksTransaction); + log_socks(tx, js).is_ok() +} diff --git a/rust/src/socks/mod.rs b/rust/src/socks/mod.rs new file mode 100644 index 000000000000..785a9808a9f1 --- /dev/null +++ b/rust/src/socks/mod.rs @@ -0,0 +1,22 @@ +/* Copyright (C) 2024 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 socks parser and logger module. + +pub mod logger; +mod parser; +pub mod socks; diff --git a/rust/src/socks/parser.rs b/rust/src/socks/parser.rs new file mode 100644 index 000000000000..40ba8364faca --- /dev/null +++ b/rust/src/socks/parser.rs @@ -0,0 +1,161 @@ +/* Copyright (C) 2024 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, + combinator::verify, + error::{make_error, ErrorKind}, + multi::count, + number::streaming::{be_u16, be_u8}, + Err, IResult, +}; + +pub struct SocksConnectRequest { + pub _ver: u8, + pub auth_methods: Vec, +} + +#[derive(Debug)] +pub struct SocksAuthRequest<'a> { + pub subver: u8, + pub user: &'a [u8], + pub pass: &'a [u8], +} + +pub fn parse_connect_request(i: &[u8]) -> IResult<&[u8], SocksConnectRequest> { + let (i, ver) = verify(be_u8, |&v| v == 5)(i)?; + let (i, n) = be_u8(i)?; + let (i, auth_methods_vec) = count(be_u8, n as usize)(i)?; + let record = SocksConnectRequest { + _ver: ver, + auth_methods: auth_methods_vec, + }; + Ok((i, record)) +} + +pub fn parse_connect_response(i: &[u8]) -> IResult<&[u8], u8> { + let (i, _ver) = verify(be_u8, |&v| v == 5)(i)?; + let (i, method) = be_u8(i)?; + Ok((i, method)) +} + +pub fn parse_auth_request(i: &[u8]) -> IResult<&[u8], SocksAuthRequest> { + let (i, subver) = verify(be_u8, |&v| v == 1)(i)?; + let (i, len) = be_u8(i)?; + let (i, user) = take(len)(i)?; + let (i, len) = be_u8(i)?; + let (i, pass) = take(len)(i)?; + let record = SocksAuthRequest { subver, user, pass }; + Ok((i, record)) +} + +pub fn parse_auth_response(i: &[u8]) -> IResult<&[u8], u8> { + let (i, _subver) = be_u8(i)?; + let (i, status) = be_u8(i)?; + Ok((i, status)) +} + +pub struct SocksConnectCommandRequest { + pub domain: Option>, + pub ipv4: Option>, + /// TODO + pub _ipv6: Option>, + pub port: u16, +} + +fn parse_connect_command_request_ipv4(i: &[u8]) -> IResult<&[u8], &[u8]> { + let (i, dst) = take(4_usize)(i)?; + Ok((i, dst)) +} + +fn parse_connect_command_request_ipv6(i: &[u8]) -> IResult<&[u8], &[u8]> { + let (i, dst) = take(16_usize)(i)?; + Ok((i, dst)) +} + +fn parse_connect_command_request_domain(i: &[u8]) -> IResult<&[u8], &[u8]> { + let (i, dlen) = be_u8(i)?; // domain + let (i, domain) = take(dlen)(i)?; + Ok((i, domain)) +} + +pub fn parse_connect_command_request(i: &[u8]) -> IResult<&[u8], SocksConnectCommandRequest> { + let (i, _ver) = verify(be_u8, |&v| v == 5)(i)?; + let (i, _cmd) = verify(be_u8, |&v| v == 1)(i)?; + let (i, _res) = verify(be_u8, |&v| v == 0)(i)?; + // RFC 1928 defines: 1: ipv4, 3: domain, 4: ipv6. Consider all else invalid. + let (i, t) = verify(be_u8, |&v| v == 1 || v == 3 || v == 4)(i)?; + let (i, dst) = if t == 1 { + parse_connect_command_request_ipv4(i)? + } else if t == 3 { + parse_connect_command_request_domain(i)? + } else if t == 4 { + parse_connect_command_request_ipv6(i)? + } else { + return Err(Err::Error(make_error(i, ErrorKind::Verify))); + }; + let (i, port) = be_u16(i)?; + + let record = if t == 1 { + SocksConnectCommandRequest { + domain: None, + ipv4: Some(dst.to_vec()), + _ipv6: None, + port, + } + } else if t == 3 { + SocksConnectCommandRequest { + domain: Some(dst.to_vec()), + ipv4: None, + _ipv6: None, + port, + } + } else if t == 4 { + SocksConnectCommandRequest { + domain: None, + ipv4: None, + _ipv6: Some(dst.to_vec()), + port, + } + } else { + return Err(Err::Error(make_error(i, ErrorKind::Verify))); + }; + Ok((i, record)) +} + +pub struct SocksConnectCommandResponse<'a> { + pub results: u8, + pub _address_type: u8, + pub _address: &'a [u8], + pub _port: u16, +} + +pub fn parse_connect_command_response(i: &[u8]) -> IResult<&[u8], SocksConnectCommandResponse> { + let (i, _ver) = verify(be_u8, |&v| v == 5)(i)?; + let (i, results) = be_u8(i)?; + let (i, _res) = verify(be_u8, |&v| v == 0)(i)?; + let (i, at) = verify(be_u8, |&v| v == 1)(i)?; // domain + let (i, address) = take(4usize)(i)?; + let (i, port) = be_u16(i)?; + let record = SocksConnectCommandResponse { + results, + _address_type: at, + _address: address, + _port: port, + }; + Ok((i, record)) +} diff --git a/rust/src/socks/socks.rs b/rust/src/socks/socks.rs new file mode 100644 index 000000000000..3bfcb551ee83 --- /dev/null +++ b/rust/src/socks/socks.rs @@ -0,0 +1,678 @@ +/* Copyright (C) 2024 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, Flow, ALPROTO_UNKNOWN, IPPROTO_TCP}; +use nom7 as nom; +use std::collections::VecDeque; +use std::ffi::CString; +use std::os::raw::{c_char, c_int, c_void}; +use std::{self, fmt}; + +static mut SOCKS_MAX_TX: usize = 256; + +pub(super) static mut ALPROTO_SOCKS: AppProto = ALPROTO_UNKNOWN; + +#[derive(AppLayerEvent)] +enum SocksEvent { + TooManyTransactions, +} + +pub struct SocksTransactionAuthMethods { + pub request_methods: Vec, + pub response_method: u8, +} + +pub struct SocksTransactionAuth { + pub subver: u8, + pub user: Vec, + pub pass: Vec, + pub response: Option, +} + +pub struct SocksTransactionConnect { + pub domain: Option>, + pub ipv4: Option>, + pub port: u16, + pub response: Option, +} + +pub struct SocksTransaction { + tx_id: u64, + tx_data: AppLayerTxData, + complete: bool, + pub connect: Option, + pub auth_userpass: Option, + pub auth_methods: Option, +} + +impl Default for SocksTransaction { + fn default() -> Self { + SCLogDebug!("new tx! default"); + Self::new() + } +} + +impl SocksTransaction { + pub fn new() -> SocksTransaction { + SCLogDebug!("new tx!"); + Self { + tx_id: 0, + tx_data: AppLayerTxData::new(), + complete: false, + connect: None, + auth_userpass: None, + auth_methods: None, + } + } +} + +impl Transaction for SocksTransaction { + fn id(&self) -> u64 { + self.tx_id + } +} + +#[derive(PartialEq, Eq, Debug)] +enum SocksConnectionState { + SocksStateNew = 0, + SocksStateAuthMethodSent = 1, + SocksStateAuthMethodResponded = 2, + SocksStateAuthDataSent = 3, + SocksStateAuthDataResponded = 4, + SocksStateConnectSent = 5, + SocksStateConnectResponded = 6, +} +impl Default for SocksConnectionState { + fn default() -> Self { + SocksConnectionState::SocksStateNew + } +} +impl fmt::Display for SocksConnectionState { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +#[derive(Default)] +pub struct SocksState { + state_data: AppLayerStateData, + tx_id: u64, + transactions: VecDeque, + request_gap: bool, + response_gap: bool, + state: SocksConnectionState, +} + +impl State for SocksState { + fn get_transaction_count(&self) -> usize { + self.transactions.len() + } + + fn get_transaction_by_index(&self, index: usize) -> Option<&SocksTransaction> { + self.transactions.get(index) + } +} + +impl SocksState { + 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<&SocksTransaction> { + self.transactions.iter().find(|tx| tx.tx_id == tx_id + 1) + } + + fn new_tx(&mut self) -> SocksTransaction { + SCLogDebug!("new tx!"); + let mut tx = SocksTransaction::new(); + self.tx_id += 1; + tx.tx_id = self.tx_id; + return tx; + } + + fn find_request(&mut self) -> Option<&mut SocksTransaction> { + self.transactions.iter_mut().find(|tx| !tx.complete) + } + + fn parse_request_data<'a>( + &mut self, tinput: &'a [u8], rinput: &'a [u8], + ) -> (AppLayerResult, &'a [u8]) { + match self.state { + SocksConnectionState::SocksStateNew => { + let r = parser::parse_connect_request(rinput); + match r { + Ok((rem, request)) => { + let mut tx = self.new_tx(); + tx.auth_methods = Some(SocksTransactionAuthMethods { + request_methods: request.auth_methods, + response_method: 0, + }); + if self.transactions.len() >= unsafe { SOCKS_MAX_TX } { + tx.tx_data.set_event(SocksEvent::TooManyTransactions as u8); + } + self.transactions.push_back(tx); + if self.transactions.len() >= unsafe { SOCKS_MAX_TX } { + return (AppLayerResult::err(), &[]); + } + self.state = SocksConnectionState::SocksStateAuthMethodSent; + SCLogDebug!("> state now {}", self.state); + return (AppLayerResult::ok(), rem); + } + Err(nom::Err::Incomplete(_)) => { + // Not enough data. This parser doesn't give us a good indication + // of how much data is missing so just ask for one more byte so the + // parse is called as soon as more data is received. + let consumed = tinput.len() - rinput.len(); + let needed = rinput.len() + 1; + return ( + AppLayerResult::incomplete(consumed as u32, needed as u32), + &[], + ); + } + Err(_) => { + return (AppLayerResult::err(), &[]); + } + } + } + SocksConnectionState::SocksStateAuthMethodSent => {} + SocksConnectionState::SocksStateAuthMethodResponded => { + let r = parser::parse_auth_request(rinput); + match r { + Ok((rem, request)) => { + let mut tx = self.new_tx(); + tx.auth_userpass = Some(SocksTransactionAuth { + subver: request.subver, + user: request.user.to_vec(), + pass: request.pass.to_vec(), + response: None, + }); + if self.transactions.len() >= unsafe { SOCKS_MAX_TX } { + tx.tx_data.set_event(SocksEvent::TooManyTransactions as u8); + } + self.transactions.push_back(tx); + if self.transactions.len() >= unsafe { SOCKS_MAX_TX } { + return (AppLayerResult::err(), &[]); + } + self.state = SocksConnectionState::SocksStateAuthDataSent; + SCLogDebug!("> state now {}", self.state); + return (AppLayerResult::ok(), rem); + } + Err(nom::Err::Incomplete(_)) => { + // Not enough data. This parser doesn't give us a good indication + // of how much data is missing so just ask for one more byte so the + // parse is called as soon as more data is received. + let consumed = tinput.len() - rinput.len(); + let needed = rinput.len() + 1; + return ( + AppLayerResult::incomplete(consumed as u32, needed as u32), + &[], + ); + } + Err(_) => { + return (AppLayerResult::err(), &[]); + } + } + } + SocksConnectionState::SocksStateAuthDataSent => {} + SocksConnectionState::SocksStateAuthDataResponded => { + SCLogDebug!("connect request!"); + let r = parser::parse_connect_command_request(rinput); + match r { + Ok((rem, request)) => { + let mut tx = self.new_tx(); + tx.connect = Some(SocksTransactionConnect { + domain: request.domain, + ipv4: request.ipv4, + port: request.port, + response: None, + }); + if self.transactions.len() >= unsafe { SOCKS_MAX_TX } { + tx.tx_data.set_event(SocksEvent::TooManyTransactions as u8); + } + self.transactions.push_back(tx); + if self.transactions.len() >= unsafe { SOCKS_MAX_TX } { + return (AppLayerResult::err(), &[]); + } + self.state = SocksConnectionState::SocksStateConnectSent; + SCLogDebug!("> state now {}", self.state); + return (AppLayerResult::ok(), rem); + } + Err(nom::Err::Incomplete(_)) => { + // Not enough data. This parser doesn't give us a good indication + // of how much data is missing so just ask for one more byte so the + // parse is called as soon as more data is received. + let consumed = tinput.len() - rinput.len(); + let needed = rinput.len() + 1; + return ( + AppLayerResult::incomplete(consumed as u32, needed as u32), + &[], + ); + } + Err(_) => { + return (AppLayerResult::err(), &[]); + } + } + } + SocksConnectionState::SocksStateConnectSent => {} + SocksConnectionState::SocksStateConnectResponded => {} + } + return (AppLayerResult::err(), &[]); + } + + fn parse_response_data<'a>( + &mut self, tinput: &'a [u8], rinput: &'a [u8], + ) -> (AppLayerResult, &'a [u8]) { + SCLogDebug!("< state {}", self.state); + match self.state { + SocksConnectionState::SocksStateNew => {} + SocksConnectionState::SocksStateAuthMethodSent => { + let r = parser::parse_connect_response(rinput); + match r { + Ok((rem, response)) => { + if let Some(tx) = self.find_request() { + SCLogDebug!("< tx {} found", tx.tx_id); + tx.tx_data.updated_tc = true; + tx.complete = true; + + if let Some(ref mut am) = tx.auth_methods { + am.response_method = response; + } + } else { + SCLogDebug!("< no tx found"); + } + + if response == 0 { + self.state = SocksConnectionState::SocksStateAuthDataResponded; + } else { + self.state = SocksConnectionState::SocksStateAuthMethodResponded; + } + + SCLogDebug!("< state now {}", self.state); + return (AppLayerResult::ok(), rem); + } + Err(nom::Err::Incomplete(_)) => { + // Not enough data. This parser doesn't give us a good indication + // of how much data is missing so just ask for one more byte so the + // parse is called as soon as more data is received. + let consumed = tinput.len() - rinput.len(); + let needed = rinput.len() + 1; + SCLogDebug!("error incomplete"); + return ( + AppLayerResult::incomplete(consumed as u32, needed as u32), + &[], + ); + } + Err(_) => { + SCLogDebug!("error"); + return (AppLayerResult::err(), &[]); + } + } + } + SocksConnectionState::SocksStateAuthMethodResponded => {} + SocksConnectionState::SocksStateAuthDataSent => { + SCLogDebug!("auth response!"); + let r = parser::parse_auth_response(rinput); + match r { + Ok((rem, response)) => { + if let Some(tx) = self.find_request() { + SCLogDebug!("< tx {} found", tx.tx_id); + tx.tx_data.updated_tc = true; + tx.complete = true; + if let Some(auth) = &mut tx.auth_userpass { + auth.response = Some(response); + } + } else { + SCLogDebug!("< no tx found"); + } + self.state = SocksConnectionState::SocksStateAuthDataResponded; + + SCLogDebug!("< state now {}", self.state); + return (AppLayerResult::ok(), rem); + } + Err(nom::Err::Incomplete(_)) => { + // Not enough data. This parser doesn't give us a good indication + // of how much data is missing so just ask for one more byte so the + // parse is called as soon as more data is received. + let consumed = tinput.len() - rinput.len(); + let needed = rinput.len() + 1; + return ( + AppLayerResult::incomplete(consumed as u32, needed as u32), + &[], + ); + } + Err(_) => { + return (AppLayerResult::err(), &[]); + } + } + } + SocksConnectionState::SocksStateAuthDataResponded => {} + SocksConnectionState::SocksStateConnectSent => { + SCLogDebug!("connect response!"); + let r = parser::parse_connect_command_response(rinput); + match r { + Ok((rem, response)) => { + if let Some(tx) = self.find_request() { + SCLogDebug!("< tx {} found", tx.tx_id); + tx.tx_data.updated_tc = true; + tx.complete = true; + if let Some(connect) = &mut tx.connect { + connect.response = Some(response.results); + } + } else { + SCLogDebug!("< no tx found"); + } + self.state = SocksConnectionState::SocksStateConnectResponded; + + SCLogDebug!("< state now {}", self.state); + return (AppLayerResult::ok(), rem); + } + Err(nom::Err::Incomplete(_)) => { + // Not enough data. This parser doesn't give us a good indication + // of how much data is missing so just ask for one more byte so the + // parse is called as soon as more data is received. + let consumed = tinput.len() - rinput.len(); + let needed = rinput.len() + 1; + return ( + AppLayerResult::incomplete(consumed as u32, needed as u32), + &[], + ); + } + Err(_) => { + return (AppLayerResult::err(), &[]); + } + } + } + SocksConnectionState::SocksStateConnectResponded => {} + } + return (AppLayerResult::err(), &[]); + } + + fn parse_request(&mut self, input: &[u8]) -> AppLayerResult { + // We're not interested in empty requests. + if input.is_empty() { + return AppLayerResult::ok(); + } + + SCLogDebug!("> got {} bytes of SOCKS data", input.len()); + + // If there was gap, check we can sync up again. + if self.request_gap { + if probe(input).is_err() { + // The parser now needs to decide what to do as we are not in sync. + // For this socks, we'll just try again next time. + return AppLayerResult::ok(); + } + + // It looks like we're in sync with a message header, clear gap + // state and keep parsing. + self.request_gap = false; + } + + let mut record = input; + while !record.is_empty() { + let (r, remaining) = self.parse_request_data(input, record); + if r != AppLayerResult::ok() { + SCLogDebug!("issue"); + return r; + } + record = remaining; + } + + // Input was fully consumed. + return AppLayerResult::ok(); + } + + fn parse_response(&mut self, flow: *const Flow, input: &[u8]) -> AppLayerResult { + // We're not interested in empty responses. + if input.is_empty() { + return AppLayerResult::ok(); + } + + SCLogDebug!("< got {} bytes of SOCKS data", input.len()); + + if self.response_gap { + if probe(input).is_err() { + // The parser now needs to decide what to do as we are not in sync. + // For this socks, we'll just try again next time. + return AppLayerResult::ok(); + } + + // It looks like we're in sync with a message header, clear gap + // state and keep parsing. + self.response_gap = false; + } + let mut record = input; + while !record.is_empty() { + let (r, remaining) = self.parse_response_data(input, record); + if r != AppLayerResult::ok() { + SCLogDebug!("issue {:?}", r); + return r; + } + if self.state == SocksConnectionState::SocksStateConnectResponded { + // TODO how does it work if we got more data in `input` here? + break; + } + record = remaining; + } + if self.state == SocksConnectionState::SocksStateConnectResponded { + SCLogDebug!("requesting upgrade"); + unsafe { + // TODO TLS is wrong, it can by any protocol. + AppLayerRequestProtocolTLSUpgrade(flow); + } + } + // All input was fully consumed. + return AppLayerResult::ok(); + } + + fn on_request_gap(&mut self, _size: u32) { + self.request_gap = true; + } + + fn on_response_gap(&mut self, _size: u32) { + self.response_gap = true; + } +} + +/// Probe for a valid header. +/// +/// As this socks protocol uses messages prefixed with the size +/// as a string followed by a ':', we look at up to the first 10 +/// characters for that pattern. +fn probe(input: &[u8]) -> nom::IResult<&[u8], ()> { + let (input, _) = nom7::combinator::verify(nom7::number::complete::be_u8, |&v| v == 5)(input)?; + Ok((input, ())) +} + +// C exports. + +/// C entry point for a probing parser. +unsafe extern "C" fn rs_socks_probing_parser( + _flow: *const Flow, _direction: u8, input: *const u8, input_len: u32, _rdir: *mut u8, +) -> AppProto { + // Need at least 2 bytes. + if input_len > 1 && !input.is_null() { + let slice = build_slice!(input, input_len as usize); + if probe(slice).is_ok() { + return ALPROTO_SOCKS; + } + } + return ALPROTO_UNKNOWN; +} + +extern "C" fn rs_socks_state_new(_orig_state: *mut c_void, _orig_proto: AppProto) -> *mut c_void { + let state = SocksState::new(); + let boxed = Box::new(state); + return Box::into_raw(boxed) as *mut c_void; +} + +unsafe extern "C" fn rs_socks_state_free(state: *mut c_void) { + std::mem::drop(Box::from_raw(state as *mut SocksState)); +} + +unsafe extern "C" fn rs_socks_state_tx_free(state: *mut c_void, tx_id: u64) { + let state = cast_pointer!(state, SocksState); + state.free_tx(tx_id); +} + +unsafe extern "C" fn rs_socks_parse_request( + _flow: *const Flow, state: *mut c_void, pstate: *mut c_void, stream_slice: StreamSlice, + _data: *const c_void, +) -> AppLayerResult { + let eof = AppLayerParserStateIssetFlag(pstate, APP_LAYER_PARSER_EOF_TS) > 0; + + if eof { + // If needed, handle EOF, or pass it into the parser. + return AppLayerResult::ok(); + } + + let state = cast_pointer!(state, SocksState); + + if stream_slice.is_gap() { + // Here we have a gap signaled by the input being null, but a greater + // than 0 input_len which provides the size of the gap. + state.on_request_gap(stream_slice.gap_size()); + AppLayerResult::ok() + } else { + let buf = stream_slice.as_slice(); + state.parse_request(buf) + } +} + +unsafe extern "C" fn rs_socks_parse_response( + flow: *const Flow, state: *mut c_void, pstate: *mut c_void, stream_slice: StreamSlice, + _data: *const c_void, +) -> AppLayerResult { + let _eof = AppLayerParserStateIssetFlag(pstate, APP_LAYER_PARSER_EOF_TC) > 0; + let state = cast_pointer!(state, SocksState); + + if stream_slice.is_gap() { + // Here we have a gap signaled by the input being null, but a greater + // than 0 input_len which provides the size of the gap. + state.on_response_gap(stream_slice.gap_size()); + AppLayerResult::ok() + } else { + let buf = stream_slice.as_slice(); + state.parse_response(flow, buf) + } +} + +unsafe extern "C" fn rs_socks_state_get_tx(state: *mut c_void, tx_id: u64) -> *mut c_void { + let state = cast_pointer!(state, SocksState); + 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_socks_state_get_tx_count(state: *mut c_void) -> u64 { + let state = cast_pointer!(state, SocksState); + return state.tx_id; +} + +unsafe extern "C" fn rs_socks_tx_get_alstate_progress(tx: *mut c_void, _direction: u8) -> c_int { + let tx = cast_pointer!(tx, SocksTransaction); + return tx.complete as c_int; +} + +export_tx_data_get!(rs_socks_get_tx_data, SocksTransaction); +export_state_data_get!(rs_socks_get_state_data, SocksState); + +// Parser name as a C style string. +const PARSER_NAME: &[u8] = b"socks\0"; + +#[no_mangle] +pub unsafe extern "C" fn rs_socks_register_parser() { + let default_port = CString::new("[1080]").unwrap(); + let parser = RustParser { + name: PARSER_NAME.as_ptr() as *const c_char, + default_port: default_port.as_ptr(), + ipproto: IPPROTO_TCP, + probe_ts: Some(rs_socks_probing_parser), + probe_tc: Some(rs_socks_probing_parser), + min_depth: 0, + max_depth: 16, + state_new: rs_socks_state_new, + state_free: rs_socks_state_free, + tx_free: rs_socks_state_tx_free, + parse_ts: rs_socks_parse_request, + parse_tc: rs_socks_parse_response, + get_tx_count: rs_socks_state_get_tx_count, + get_tx: rs_socks_state_get_tx, + tx_comp_st_ts: 1, + tx_comp_st_tc: 1, + tx_get_progress: rs_socks_tx_get_alstate_progress, + get_eventinfo: Some(SocksEvent::get_event_info), + get_eventinfo_byid: Some(SocksEvent::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_socks_get_tx_data, + get_state_data: rs_socks_get_state_data, + apply_tx_config: None, + flags: APP_LAYER_PARSER_OPT_ACCEPT_GAPS, + get_frame_id_by_name: None, + get_frame_name_by_id: None, + }; + + let ip_proto_str = CString::new("tcp").unwrap(); + + if AppLayerProtoDetectConfProtoDetectionEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let alproto = AppLayerRegisterProtocolDetection(&parser, 1); + ALPROTO_SOCKS = alproto; + if AppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let _ = AppLayerRegisterParser(&parser, alproto); + } + if let Some(val) = conf_get("app-layer.protocols.socks.max-tx") { + if let Ok(v) = val.parse::() { + SOCKS_MAX_TX = v; + } else { + SCLogError!("Invalid value for socks.max-tx"); + } + } + AppLayerParserRegisterLogger(IPPROTO_TCP, ALPROTO_SOCKS); + SCLogDebug!("Rust socks parser registered."); + } else { + SCLogDebug!("Protocol detector and parser disabled for SOCKS."); + } +} diff --git a/src/app-layer-parser.c b/src/app-layer-parser.c index 045fcb086e1f..40df9b6beff5 100644 --- a/src/app-layer-parser.c +++ b/src/app-layer-parser.c @@ -1731,6 +1731,7 @@ void AppLayerParserRegisterProtocolParsers(void) rs_websocket_register_parser(); SCRegisterLdapTcpParser(); SCRegisterLdapUdpParser(); + rs_socks_register_parser(); rs_template_register_parser(); SCRfbRegisterParser(); SCMqttRegisterParser(); diff --git a/src/app-layer-protos.c b/src/app-layer-protos.c index 03736554c7b6..e35f4c3babc3 100644 --- a/src/app-layer-protos.c +++ b/src/app-layer-protos.c @@ -63,6 +63,7 @@ const AppProtoStringTuple AppProtoStrings[ALPROTO_MAX] = { { ALPROTO_WEBSOCKET, "websocket" }, { ALPROTO_LDAP, "ldap" }, { ALPROTO_DOH2, "doh2" }, + { ALPROTO_SOCKS, "socks" }, { ALPROTO_TEMPLATE, "template" }, { ALPROTO_RDP, "rdp" }, { ALPROTO_HTTP2, "http2" }, diff --git a/src/app-layer-protos.h b/src/app-layer-protos.h index 10b8959772c4..a87b5141f346 100644 --- a/src/app-layer-protos.h +++ b/src/app-layer-protos.h @@ -59,6 +59,7 @@ enum AppProtoEnum { ALPROTO_WEBSOCKET, ALPROTO_LDAP, ALPROTO_DOH2, + ALPROTO_SOCKS, ALPROTO_TEMPLATE, ALPROTO_RDP, ALPROTO_HTTP2, diff --git a/src/output.c b/src/output.c index b99897509c0f..53ebba2afb99 100644 --- a/src/output.c +++ b/src/output.c @@ -894,6 +894,7 @@ void OutputRegisterRootLoggers(void) RegisterSimpleJsonApplayerLogger(ALPROTO_WEBSOCKET, rs_websocket_logger_log, NULL); RegisterSimpleJsonApplayerLogger(ALPROTO_LDAP, rs_ldap_logger_log, NULL); RegisterSimpleJsonApplayerLogger(ALPROTO_DOH2, AlertJsonDoh2, NULL); + RegisterSimpleJsonApplayerLogger(ALPROTO_SOCKS, rs_socks_logger_log, NULL); RegisterSimpleJsonApplayerLogger(ALPROTO_TEMPLATE, rs_template_logger_log, NULL); RegisterSimpleJsonApplayerLogger(ALPROTO_RDP, (EveJsonSimpleTxLogFunc)rs_rdp_to_json, NULL); // special case : http2 is logged in http object @@ -1083,6 +1084,10 @@ void OutputRegisterLoggers(void) JsonLogThreadDeinit); /* DoH2 JSON logger. */ JsonDoh2LogRegister(); + /* Socks JSON logger. */ + OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonSocksLog", "eve-log.socks", + OutputJsonLogInitSub, ALPROTO_SOCKS, JsonGenericDirPacketLogger, JsonLogThreadInit, + JsonLogThreadDeinit); /* Template JSON logger. */ OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonTemplateLog", "eve-log.template", OutputJsonLogInitSub, ALPROTO_TEMPLATE, JsonGenericDirPacketLogger, JsonLogThreadInit, diff --git a/suricata.yaml.in b/suricata.yaml.in index 0c71090cb3bd..319d226856b2 100644 --- a/suricata.yaml.in +++ b/suricata.yaml.in @@ -324,6 +324,7 @@ outputs: - sip - quic - ldap + - socks - arp: enabled: no # Many events can be logged. Disabled by default - dhcp: @@ -1195,6 +1196,8 @@ app-layer: dp: 389, 3268 # Maximum number of live LDAP transactions per flow # max-tx: 1024 + socks: + enabled: yes # Limit for the maximum number of asn1 frames to decode (default 256) asn1-max-frames: 256