diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 17:39:49 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 17:39:49 +0000 |
commit | a0aa2307322cd47bbf416810ac0292925e03be87 (patch) | |
tree | 37076262a026c4b48c8a0e84f44ff9187556ca35 /rust/src/rdp | |
parent | Initial commit. (diff) | |
download | suricata-a0aa2307322cd47bbf416810ac0292925e03be87.tar.xz suricata-a0aa2307322cd47bbf416810ac0292925e03be87.zip |
Adding upstream version 1:7.0.3.upstream/1%7.0.3
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'rust/src/rdp')
-rw-r--r-- | rust/src/rdp/error.rs | 49 | ||||
-rw-r--r-- | rust/src/rdp/log.rs | 597 | ||||
-rw-r--r-- | rust/src/rdp/mod.rs | 27 | ||||
-rw-r--r-- | rust/src/rdp/parser.rs | 1431 | ||||
-rw-r--r-- | rust/src/rdp/rdp.rs | 667 | ||||
-rw-r--r-- | rust/src/rdp/util.rs | 175 | ||||
-rw-r--r-- | rust/src/rdp/windows.rs | 660 |
7 files changed, 3606 insertions, 0 deletions
diff --git a/rust/src/rdp/error.rs b/rust/src/rdp/error.rs new file mode 100644 index 0000000..486d27a --- /dev/null +++ b/rust/src/rdp/error.rs @@ -0,0 +1,49 @@ +/* Copyright (C) 2019 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. + */ + +// Author: Zach Kelly <zach.kelly@lmco.com> +// Author: Pierre Chifflier <chifflier@wzdftpd.net> +use nom7::error::{Error, ErrorKind, ParseError}; +use nom7::ErrorConvert; + +#[derive(Debug, PartialEq, Eq)] +pub enum RdpError { + UnimplementedLengthDeterminant, + NotX224Class0Error, + NomError(ErrorKind), +} + +impl<I> ParseError<I> for RdpError { + fn from_error_kind(_input: I, kind: ErrorKind) -> Self { + RdpError::NomError(kind) + } + fn append(_input: I, kind: ErrorKind, _other: Self) -> Self { + RdpError::NomError(kind) + } +} + +impl From<Error<&[u8]>> for RdpError { + fn from(e: Error<&[u8]>) -> Self { + RdpError::NomError(e.code) + } +} + +impl ErrorConvert<RdpError> for ((&[u8], usize), ErrorKind) { + fn convert(self) -> RdpError { + RdpError::NomError(self.1) + } +} diff --git a/rust/src/rdp/log.rs b/rust/src/rdp/log.rs new file mode 100644 index 0000000..e0a71a8 --- /dev/null +++ b/rust/src/rdp/log.rs @@ -0,0 +1,597 @@ +/* Copyright (C) 2019 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. + */ + +// Author: Zach Kelly <zach.kelly@lmco.com> + +use super::rdp::{RdpTransaction, RdpTransactionItem}; +use crate::jsonbuilder::{JsonBuilder, JsonError}; +use crate::rdp::parser::*; +use crate::rdp::windows; +use x509_parser::prelude::{X509Certificate, FromDer}; + +#[no_mangle] +pub extern "C" fn rs_rdp_to_json(tx: &mut RdpTransaction, js: &mut JsonBuilder) -> bool { + log(tx, js).is_ok() +} + +/// populate a json object with transactional information, for logging +fn log(tx: &RdpTransaction, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("rdp")?; + js.set_uint("tx_id", tx.id)?; + + match &tx.item { + RdpTransactionItem::X224ConnectionRequest(ref x224) => x224_req_to_json(x224, js)?, + RdpTransactionItem::X224ConnectionConfirm(ref x224) => x224_conf_to_json(x224, js)?, + + RdpTransactionItem::McsConnectRequest(ref mcs) => { + mcs_req_to_json(mcs, js)?; + } + + RdpTransactionItem::McsConnectResponse(_) => { + // no additional JSON data beyond `event_type` + js.set_string("event_type", "connect_response")?; + } + + RdpTransactionItem::TlsCertificateChain(chain) => { + js.set_string("event_type", "tls_handshake")?; + js.open_array("x509_serials")?; + for blob in chain { + if let Ok((_, cert)) = X509Certificate::from_der(&blob.data) { + js.append_string(&cert.tbs_certificate.serial.to_str_radix(16))?; + } + } + js.close()?; + } + } + + js.close()?; + Ok(()) +} + +/// json helper for X224ConnectionRequest +fn x224_req_to_json(x224: &X224ConnectionRequest, js: &mut JsonBuilder) -> Result<(), JsonError> { + use crate::rdp::parser::NegotiationRequestFlags as Flags; + + js.set_string("event_type", "initial_request")?; + if let Some(ref cookie) = x224.cookie { + js.set_string("cookie", &cookie.mstshash)?; + } + if let Some(ref req) = x224.negotiation_request { + if !req.flags.is_empty() { + js.open_array("flags")?; + if req.flags.contains(Flags::RESTRICTED_ADMIN_MODE_REQUIRED) { + js.append_string("restricted_admin_mode_required")?; + } + if req + .flags + .contains(Flags::REDIRECTED_AUTHENTICATION_MODE_REQUIRED) + { + js.append_string("redirected_authentication_mode_required")?; + } + if req.flags.contains(Flags::CORRELATION_INFO_PRESENT) { + js.append_string("correlation_info_present")?; + } + js.close()?; + } + } + + Ok(()) +} + +/// json helper for X224ConnectionConfirm +fn x224_conf_to_json(x224: &X224ConnectionConfirm, js: &mut JsonBuilder) -> Result<(), JsonError> { + use crate::rdp::parser::NegotiationResponseFlags as Flags; + + js.set_string("event_type", "initial_response")?; + if let Some(ref from_server) = x224.negotiation_from_server { + match &from_server { + NegotiationFromServer::Response(ref resp) => { + if !resp.flags.is_empty() { + js.open_array("server_supports")?; + if resp.flags.contains(Flags::EXTENDED_CLIENT_DATA_SUPPORTED) { + js.append_string("extended_client_data")?; + } + if resp.flags.contains(Flags::DYNVC_GFX_PROTOCOL_SUPPORTED) { + js.append_string("dynvc_gfx")?; + } + + // NEGRSP_FLAG_RESERVED not logged + + if resp.flags.contains(Flags::RESTRICTED_ADMIN_MODE_SUPPORTED) { + js.append_string("restricted_admin")?; + } + if resp + .flags + .contains(Flags::REDIRECTED_AUTHENTICATION_MODE_SUPPORTED) + { + js.append_string("redirected_authentication")?; + } + js.close()?; + } + + let protocol = match resp.protocol { + Protocol::ProtocolRdp => "rdp", + Protocol::ProtocolSsl => "ssl", + Protocol::ProtocolHybrid => "hybrid", + Protocol::ProtocolRdsTls => "rds_tls", + Protocol::ProtocolHybridEx => "hybrid_ex", + }; + js.set_string("protocol", protocol)?; + } + + NegotiationFromServer::Failure(ref fail) => match fail.code { + NegotiationFailureCode::SslRequiredByServer => { + js.set_uint( + "error_code", + NegotiationFailureCode::SslRequiredByServer as u64, + )?; + js.set_string("reason", "ssl required by server")?; + } + NegotiationFailureCode::SslNotAllowedByServer => { + js.set_uint( + "error_code", + NegotiationFailureCode::SslNotAllowedByServer as u64, + )?; + js.set_string("reason", "ssl not allowed by server")?; + } + NegotiationFailureCode::SslCertNotOnServer => { + js.set_uint( + "error_code", + NegotiationFailureCode::SslCertNotOnServer as u64, + )?; + js.set_string("reason", "ssl cert not on server")?; + } + NegotiationFailureCode::InconsistentFlags => { + js.set_uint( + "error_code", + NegotiationFailureCode::InconsistentFlags as u64, + )?; + js.set_string("reason", "inconsistent flags")?; + } + NegotiationFailureCode::HybridRequiredByServer => { + js.set_uint( + "error_code", + NegotiationFailureCode::HybridRequiredByServer as u64, + )?; + js.set_string("reason", "hybrid required by server")?; + } + NegotiationFailureCode::SslWithUserAuthRequiredByServer => { + js.set_uint( + "error_code", + NegotiationFailureCode::SslWithUserAuthRequiredByServer as u64, + )?; + js.set_string("reason", "ssl with user auth required by server")?; + } + }, + } + } + + Ok(()) +} + +/// json helper for McsConnectRequest +fn mcs_req_to_json(mcs: &McsConnectRequest, js: &mut JsonBuilder) -> Result<(), JsonError> { + // placeholder string value. We do not simply omit "unknown" values so that they can + // help indicate that a given enum may be out of date (new Windows version, etc.) + let unknown = String::from("unknown"); + + js.set_string("event_type", "connect_request")?; + for child in &mcs.children { + match child { + McsConnectRequestChild::CsClientCore(ref client) => { + js.open_object("client")?; + + match client.version { + Some(ref ver) => { + js.set_string("version", &version_to_string(ver, "v"))?; + } + None => { + js.set_string("version", &unknown)?; + } + } + + js.set_uint("desktop_width", client.desktop_width as u64)?; + js.set_uint("desktop_height", client.desktop_height as u64)?; + + if let Some(depth) = get_color_depth(client) { + js.set_uint("color_depth", depth)?; + } + + // sas_sequence not logged + + js.set_string( + "keyboard_layout", + &windows::lcid_to_string(client.keyboard_layout, &unknown), + )?; + + js.set_string( + "build", + &windows::os_to_string(&client.client_build, &unknown), + )?; + + if !client.client_name.is_empty() { + js.set_string("client_name", &client.client_name)?; + } + + if let Some(ref kb) = client.keyboard_type { + js.set_string("keyboard_type", &keyboard_to_string(kb))?; + } + + if client.keyboard_subtype != 0 { + js.set_uint("keyboard_subtype", client.keyboard_subtype as u64)?; + } + + if client.keyboard_function_key != 0 { + js.set_uint("function_keys", client.keyboard_function_key as u64)?; + } + + if !client.ime_file_name.is_empty() { + js.set_string("ime", &client.ime_file_name)?; + } + + // + // optional fields + // + + if let Some(id) = client.client_product_id { + js.set_uint("product_id", id as u64)?; + } + + if let Some(serial) = client.serial_number { + if serial != 0 { + js.set_uint("serial_number", serial as u64)?; + } + } + + // supported_color_depth not logged + + if let Some(ref early_capability_flags) = client.early_capability_flags { + use crate::rdp::parser::EarlyCapabilityFlags as Flags; + + if !early_capability_flags.is_empty() { + js.open_array("capabilities")?; + if early_capability_flags.contains(Flags::RNS_UD_CS_SUPPORT_ERRINFO_PDF) { + js.append_string("support_errinfo_pdf")?; + } + if early_capability_flags.contains(Flags::RNS_UD_CS_WANT_32BPP_SESSION) { + js.append_string("want_32bpp_session")?; + } + if early_capability_flags.contains(Flags::RNS_UD_CS_SUPPORT_STATUSINFO_PDU) + { + js.append_string("support_statusinfo_pdu")?; + } + if early_capability_flags.contains(Flags::RNS_UD_CS_STRONG_ASYMMETRIC_KEYS) + { + js.append_string("strong_asymmetric_keys")?; + } + + // RNS_UD_CS_UNUSED not logged + + if early_capability_flags.contains(Flags::RNS_UD_CS_VALID_CONNECTION_TYPE) { + js.append_string("valid_connection_type")?; + } + if early_capability_flags + .contains(Flags::RNS_UD_CS_SUPPORT_MONITOR_LAYOUT_PDU) + { + js.append_string("support_monitor_layout_pdu")?; + } + if early_capability_flags + .contains(Flags::RNS_UD_CS_SUPPORT_NETCHAR_AUTODETECT) + { + js.append_string("support_netchar_autodetect")?; + } + if early_capability_flags + .contains(Flags::RNS_UD_CS_SUPPORT_DYNVC_GFX_PROTOCOL) + { + js.append_string("support_dynvc_gfx_protocol")?; + } + if early_capability_flags + .contains(Flags::RNS_UD_CS_SUPPORT_DYNAMIC_TIME_ZONE) + { + js.append_string("support_dynamic_time_zone")?; + } + if early_capability_flags.contains(Flags::RNS_UD_CS_SUPPORT_HEARTBEAT_PDU) { + js.append_string("support_heartbeat_pdu")?; + } + js.close()?; + } + } + + if let Some(ref id) = client.client_dig_product_id { + if !id.is_empty() { + js.set_string("id", id)?; + } + } + + if let Some(ref hint) = client.connection_hint { + let s = match hint { + ConnectionHint::ConnectionHintModem => "modem", + ConnectionHint::ConnectionHintBroadbandLow => "low_broadband", + ConnectionHint::ConnectionHintSatellite => "satellite", + ConnectionHint::ConnectionHintBroadbandHigh => "high_broadband", + ConnectionHint::ConnectionHintWan => "wan", + ConnectionHint::ConnectionHintLan => "lan", + ConnectionHint::ConnectionHintAutoDetect => "autodetect", + ConnectionHint::ConnectionHintNotProvided => "", + }; + if *hint != ConnectionHint::ConnectionHintNotProvided { + js.set_string("connection_hint", s)?; + } + } + + // server_selected_protocol not logged + + if let Some(width) = client.desktop_physical_width { + js.set_uint("physical_width", width as u64)?; + } + + if let Some(height) = client.desktop_physical_height { + js.set_uint("physical_height", height as u64)?; + } + + if let Some(orientation) = client.desktop_orientation { + js.set_uint("desktop_orientation", orientation as u64)?; + } + + if let Some(scale) = client.desktop_scale_factor { + js.set_uint("scale_factor", scale as u64)?; + } + + if let Some(scale) = client.device_scale_factor { + js.set_uint("device_scale_factor", scale as u64)?; + } + js.close()?; + } + + McsConnectRequestChild::CsNet(ref net) => { + if !net.channels.is_empty() { + js.open_array("channels")?; + for channel in &net.channels { + js.append_string(channel)?; + } + js.close()?; + } + } + + McsConnectRequestChild::CsUnknown(_) => {} + } + } + + Ok(()) +} + +/// converts RdpClientVersion to a string, using the provided prefix +fn version_to_string(ver: &RdpClientVersion, prefix: &str) -> String { + let mut result = String::from(prefix); + match ver { + RdpClientVersion::V4 => result.push('4'), + RdpClientVersion::V5_V8_1 => result.push('5'), + RdpClientVersion::V10_0 => result.push_str("10.0"), + RdpClientVersion::V10_1 => result.push_str("10.1"), + RdpClientVersion::V10_2 => result.push_str("10.2"), + RdpClientVersion::V10_3 => result.push_str("10.3"), + RdpClientVersion::V10_4 => result.push_str("10.4"), + RdpClientVersion::V10_5 => result.push_str("10.5"), + RdpClientVersion::V10_6 => result.push_str("10.6"), + RdpClientVersion::V10_7 => result.push_str("10.7"), + }; + result +} + +/// checks multiple client info fields to determine color depth +fn get_color_depth(client: &CsClientCoreData) -> Option<u64> { + // first check high_color_depth + match client.high_color_depth { + Some(HighColorDepth::HighColor4Bpp) => return Some(4), + Some(HighColorDepth::HighColor8Bpp) => return Some(8), + Some(HighColorDepth::HighColor15Bpp) => return Some(15), + Some(HighColorDepth::HighColor16Bpp) => return Some(16), + Some(HighColorDepth::HighColor24Bpp) => return Some(24), + _ => (), + }; + + // if not present, try post_beta2_color_depth + match client.post_beta2_color_depth { + Some(PostBeta2ColorDepth::RnsUdColor4Bpp) => return Some(4), + Some(PostBeta2ColorDepth::RnsUdColor8Bpp) => return Some(8), + Some(PostBeta2ColorDepth::RnsUdColor16Bpp555) => return Some(15), + Some(PostBeta2ColorDepth::RnsUdColor16Bpp565) => return Some(16), + Some(PostBeta2ColorDepth::RnsUdColor24Bpp) => return Some(24), + _ => (), + }; + + // if not present, try color_depth + match client.color_depth { + Some(ColorDepth::RnsUdColor4Bpp) => return Some(4), + Some(ColorDepth::RnsUdColor8Bpp) => return Some(8), + _ => return None, + } +} + +fn keyboard_to_string(kb: &KeyboardType) -> String { + let s = match kb { + KeyboardType::KbXt => "xt", + KeyboardType::KbIco => "ico", + KeyboardType::KbAt => "at", + KeyboardType::KbEnhanced => "enhanced", + KeyboardType::Kb1050 => "1050", + KeyboardType::Kb9140 => "9140", + KeyboardType::KbJapanese => "jp", + }; + String::from(s) +} + +#[cfg(test)] +mod tests { + use super::*; + + // for now, testing of JsonBuilder output is done by suricata-verify + + #[test] + fn test_version_string() { + assert_eq!("v10.7", version_to_string(&RdpClientVersion::V10_7, "v")); + } + + #[test] + fn test_color_depth_high() { + let core_data = CsClientCoreData { + version: None, + desktop_width: 1280, + desktop_height: 768, + color_depth: Some(ColorDepth::RnsUdColor4Bpp), + sas_sequence: None, + keyboard_layout: 0x409, + client_build: windows::OperatingSystem { + build: windows::Build::Win10_17763, + suffix: windows::Suffix::Rs5, + }, + client_name: String::from("SERVER-XYZ"), + keyboard_type: None, + keyboard_subtype: 0, + keyboard_function_key: 12, + ime_file_name: String::from(""), + post_beta2_color_depth: Some(PostBeta2ColorDepth::RnsUdColor8Bpp), + client_product_id: None, + serial_number: None, + high_color_depth: Some(HighColorDepth::HighColor24Bpp), + supported_color_depth: None, + early_capability_flags: None, + client_dig_product_id: None, + connection_hint: None, + server_selected_protocol: None, + desktop_physical_width: None, + desktop_physical_height: None, + desktop_orientation: None, + desktop_scale_factor: None, + device_scale_factor: None, + }; + assert_eq!(Some(24), get_color_depth(&core_data)); + } + + #[test] + fn test_color_depth_post_beta2() { + let core_data = CsClientCoreData { + version: None, + desktop_width: 1280, + desktop_height: 768, + color_depth: Some(ColorDepth::RnsUdColor4Bpp), + sas_sequence: None, + keyboard_layout: 0x409, + client_build: windows::OperatingSystem { + build: windows::Build::Win10_17763, + suffix: windows::Suffix::Rs5, + }, + client_name: String::from("SERVER-XYZ"), + keyboard_type: None, + keyboard_subtype: 0, + keyboard_function_key: 12, + ime_file_name: String::from(""), + post_beta2_color_depth: Some(PostBeta2ColorDepth::RnsUdColor8Bpp), + client_product_id: None, + serial_number: None, + high_color_depth: None, + supported_color_depth: None, + early_capability_flags: None, + client_dig_product_id: None, + connection_hint: None, + server_selected_protocol: None, + desktop_physical_width: None, + desktop_physical_height: None, + desktop_orientation: None, + desktop_scale_factor: None, + device_scale_factor: None, + }; + assert_eq!(Some(8), get_color_depth(&core_data)); + } + + #[test] + fn test_color_depth_basic() { + let core_data = CsClientCoreData { + version: None, + desktop_width: 1280, + desktop_height: 768, + color_depth: Some(ColorDepth::RnsUdColor4Bpp), + sas_sequence: None, + keyboard_layout: 0x409, + client_build: windows::OperatingSystem { + build: windows::Build::Win10_17763, + suffix: windows::Suffix::Rs5, + }, + client_name: String::from("SERVER-XYZ"), + keyboard_type: None, + keyboard_subtype: 0, + keyboard_function_key: 12, + ime_file_name: String::from(""), + post_beta2_color_depth: None, + client_product_id: None, + serial_number: None, + high_color_depth: None, + supported_color_depth: None, + early_capability_flags: None, + client_dig_product_id: None, + connection_hint: None, + server_selected_protocol: None, + desktop_physical_width: None, + desktop_physical_height: None, + desktop_orientation: None, + desktop_scale_factor: None, + device_scale_factor: None, + }; + assert_eq!(Some(4), get_color_depth(&core_data)); + } + + #[test] + fn test_color_depth_missing() { + let core_data = CsClientCoreData { + version: None, + desktop_width: 1280, + desktop_height: 768, + color_depth: None, + sas_sequence: None, + keyboard_layout: 0x409, + client_build: windows::OperatingSystem { + build: windows::Build::Win10_17763, + suffix: windows::Suffix::Rs5, + }, + client_name: String::from("SERVER-XYZ"), + keyboard_type: None, + keyboard_subtype: 0, + keyboard_function_key: 12, + ime_file_name: String::from(""), + post_beta2_color_depth: None, + client_product_id: None, + serial_number: None, + high_color_depth: None, + supported_color_depth: None, + early_capability_flags: None, + client_dig_product_id: None, + connection_hint: None, + server_selected_protocol: None, + desktop_physical_width: None, + desktop_physical_height: None, + desktop_orientation: None, + desktop_scale_factor: None, + device_scale_factor: None, + }; + assert!(get_color_depth(&core_data).is_none()); + } + + #[test] + fn test_keyboard_string() { + assert_eq!("enhanced", keyboard_to_string(&KeyboardType::KbEnhanced)); + } +} diff --git a/rust/src/rdp/mod.rs b/rust/src/rdp/mod.rs new file mode 100644 index 0000000..dc83db8 --- /dev/null +++ b/rust/src/rdp/mod.rs @@ -0,0 +1,27 @@ +/* Copyright (C) 2019 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. + */ + +//! RDP parser, logger and application layer module. +//! +//! written by Zach Kelly <zach.kelly@lmco.com> + +pub mod error; +pub mod log; +pub mod parser; +pub mod rdp; +pub mod util; +pub mod windows; diff --git a/rust/src/rdp/parser.rs b/rust/src/rdp/parser.rs new file mode 100644 index 0000000..a8004e2 --- /dev/null +++ b/rust/src/rdp/parser.rs @@ -0,0 +1,1431 @@ +/* Copyright (C) 2019 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. + */ + +// Author: Zach Kelly <zach.kelly@lmco.com> + +//! RDP parser +//! +//! References: +//! * rdp-spec: <https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/> +//! * t.123-spec: <https://www.itu.int/rec/T-REC-T.123-200701-I/en> +//! * t.125-spec: <https://www.itu.int/rec/T-REC-T.125-199802-I/en> +//! * x.224-spec: <https://www.itu.int/rec/T-REC-X.224-199511-I/en> +//! * x.691-spec: <https://www.itu.int/rec/T-REC-X.691/en> + +use crate::common::nom7::{bits, take_until_and_consume}; +use crate::rdp::error::RdpError; +use crate::rdp::util::{le_slice_to_string, parse_per_length_determinant, utf7_slice_to_string}; +use crate::rdp::windows; +use nom7::bits::streaming::take as take_bits; +use nom7::bytes::streaming::{tag, take}; +use nom7::combinator::{map, map_opt, map_res, opt, verify}; +use nom7::error::{make_error, ErrorKind}; +use nom7::multi::length_data; +use nom7::number::streaming::{be_u16, be_u8, le_u16, le_u32, le_u8}; +use nom7::sequence::tuple; +use nom7::{Err, IResult}; + +/// constrains dimension to a range, per spec +/// rdp-spec, section 2.2.1.3.2 Client Core Data +fn millimeters_to_opt(x: u32) -> Option<u32> { + if (10..=10_000).contains(&x) { + Some(x) + } else { + None + } +} + +/// constrains desktop scale to a range, per spec +/// rdp-spec, section 2.2.1.3.2 Client Core Data +fn desktop_scale_to_opt(x: u32) -> Option<u32> { + if (100..=500).contains(&x) { + Some(x) + } else { + None + } +} + +/// constrains device scale to a set of valid values, per spec +/// rdp-spec, section 2.2.1.3.2 Client Core Data +fn device_scale_to_opt(x: u32) -> Option<u32> { + if x == 100 || x == 140 || x == 180 { + Some(x) + } else { + None + } +} + +// ================ + +/// t.123-spec, section 8 +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum TpktVersion { + T123 = 0x3, +} + +/// t.123-spec, section 8 +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct T123Tpkt { + pub child: T123TpktChild, +} + +/// variants that a t.123 tpkt can hold +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum T123TpktChild { + X224ConnectionRequest(X224ConnectionRequest), + X224ConnectionConfirm(X224ConnectionConfirm), + Data(X223Data), + Raw(Vec<u8>), +} + +// ================ + +/// x.224-spec, sections 13.3.3, 13.4.3, 13.7.3 +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum X224Type { + ConnectionConfirm = 0xd, + ConnectionRequest = 0xe, + _Data = 0xf, +} + +/// x.224-spec, section 13.3 +// rdp-spec, section 2.2.1.1 +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct X224ConnectionRequest { + pub cdt: u8, + pub dst_ref: u16, + pub src_ref: u16, + pub class: u8, + pub options: u8, + pub cookie: Option<RdpCookie>, + pub negotiation_request: Option<NegotiationRequest>, + pub data: Vec<u8>, +} + +/// rdp-spec, section 2.2.1.1.1 +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RdpCookie { + pub mstshash: String, +} + +/// rdp-spec, sections 2.2.1.1.1, 2.2.1.2.1, 2.2.1.2.2 +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum X224ConnectionRequestType { + NegotiationRequest = 0x1, + NegotiationResponse = 0x2, + NegotiationFailure = 0x3, +} + +/// rdp-spec, section 2.2.1.1.1 +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct NegotiationRequest { + pub flags: NegotiationRequestFlags, + pub protocols: ProtocolFlags, +} + +// rdp-spec, section 2.2.1.1.1 +bitflags! { + #[derive(Default)] + pub struct NegotiationRequestFlags: u8 { + const RESTRICTED_ADMIN_MODE_REQUIRED = 0x1; + const REDIRECTED_AUTHENTICATION_MODE_REQUIRED = 0x2; + const CORRELATION_INFO_PRESENT = 0x8; + } +} + +/// rdp-spec, section 2.2.1.1.1 +#[derive(Clone, Debug, FromPrimitive, PartialEq, Eq)] +pub enum Protocol { + ProtocolRdp = 0x0, + ProtocolSsl = 0x1, + ProtocolHybrid = 0x2, + ProtocolRdsTls = 0x4, + ProtocolHybridEx = 0x8, +} + +// rdp-spec, section 2.2.1.1.1 +bitflags! { + pub struct ProtocolFlags: u32 { + //Protocol::ProtocolRdp is 0 as always supported + //and bitflags crate does not like zero-bit flags + const PROTOCOL_SSL = Protocol::ProtocolSsl as u32; + const PROTOCOL_HYBRID = Protocol::ProtocolHybrid as u32; + const PROTOCOL_RDSTLS = Protocol::ProtocolRdsTls as u32; + const PROTOCOL_HYBRID_EX = Protocol::ProtocolHybridEx as u32; + } +} + +/// rdp-spec, section 2.2.1.2 +/// x.224-spec, section 13.3 +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct X224ConnectionConfirm { + pub cdt: u8, + pub dst_ref: u16, + pub src_ref: u16, + pub class: u8, + pub options: u8, + pub negotiation_from_server: Option<NegotiationFromServer>, +} + +/// variants of a server negotiation +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum NegotiationFromServer { + Response(NegotiationResponse), + Failure(NegotiationFailure), +} + +/// rdp-spec, section 2.2.1.1.1 +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct NegotiationResponse { + pub flags: NegotiationResponseFlags, + pub protocol: Protocol, +} + +// rdp-spec, section 2.2.1.2.1 +bitflags! { + #[derive(Default)] + pub struct NegotiationResponseFlags: u8 { + const EXTENDED_CLIENT_DATA_SUPPORTED = 0x1; + const DYNVC_GFX_PROTOCOL_SUPPORTED = 0x2; + const NEGRSP_FLAG_RESERVED = 0x4; + const RESTRICTED_ADMIN_MODE_SUPPORTED = 0x8; + const REDIRECTED_AUTHENTICATION_MODE_SUPPORTED = 0x10; + } +} + +/// rdp-spec, section 2.2.1.1.1 +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct NegotiationFailure { + pub code: NegotiationFailureCode, +} + +/// rdp-spec, section 2.2.1.2.2 +#[derive(Clone, Debug, FromPrimitive, PartialEq, Eq)] +pub enum NegotiationFailureCode { + SslRequiredByServer = 0x1, + SslNotAllowedByServer = 0x2, + SslCertNotOnServer = 0x3, + InconsistentFlags = 0x4, + HybridRequiredByServer = 0x5, + SslWithUserAuthRequiredByServer = 0x6, +} + +// ================ + +/// x224-spec, section 13.7 +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct X223Data { + pub child: X223DataChild, +} + +/// variants that an x.223 data message can hold +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum X223DataChild { + McsConnectRequest(McsConnectRequest), + McsConnectResponse(McsConnectResponse), + Raw(Vec<u8>), +} + +/// t.125-spec, section 7, part 2 +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum T125Type { + T125TypeMcsConnectRequest = 0x65, // 101 + T125TypeMcsConnectResponse = 0x66, // 102 +} + +/// rdp-spec, section 2.2.1.3.2 +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct McsConnectRequest { + pub children: Vec<McsConnectRequestChild>, +} + +/// variants that an mcs connection message can hold +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum McsConnectRequestChild { + CsClientCore(CsClientCoreData), + CsNet(CsNet), + CsUnknown(CsUnknown), +} + +/// rdp-spec, section 2.2.1.3.1 +#[derive(Clone, Debug, FromPrimitive, PartialEq, Eq)] +pub enum CsType { + Core = 0xc001, + Net = 0xc003, +} + +/// rdp-spec, section 2.2.1.3.2 +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CsClientCoreData { + pub version: Option<RdpClientVersion>, + pub desktop_width: u16, + pub desktop_height: u16, + pub color_depth: Option<ColorDepth>, + pub sas_sequence: Option<SasSequence>, + pub keyboard_layout: u32, // see windows::lcid_to_string + pub client_build: windows::OperatingSystem, + pub client_name: String, + pub keyboard_type: Option<KeyboardType>, + pub keyboard_subtype: u32, + pub keyboard_function_key: u32, + pub ime_file_name: String, + // optional fields + pub post_beta2_color_depth: Option<PostBeta2ColorDepth>, + pub client_product_id: Option<u16>, + pub serial_number: Option<u32>, + pub high_color_depth: Option<HighColorDepth>, + pub supported_color_depth: Option<SupportedColorDepth>, + pub early_capability_flags: Option<EarlyCapabilityFlags>, + pub client_dig_product_id: Option<String>, + pub connection_hint: Option<ConnectionHint>, + pub server_selected_protocol: Option<ProtocolFlags>, + pub desktop_physical_width: Option<u32>, + pub desktop_physical_height: Option<u32>, + pub desktop_orientation: Option<DesktopOrientation>, + pub desktop_scale_factor: Option<u32>, + pub device_scale_factor: Option<u32>, +} + +/// rdp-spec, section 2.2.1.3.2 Client Core Data +#[derive(Clone, Debug, FromPrimitive, PartialEq, Eq)] +#[allow(non_camel_case_types)] +pub enum RdpClientVersion { + V4 = 0x80001, + V5_V8_1 = 0x80004, + V10_0 = 0x80005, + V10_1 = 0x80006, + V10_2 = 0x80007, + V10_3 = 0x80008, + V10_4 = 0x80009, + V10_5 = 0x8000a, + V10_6 = 0x8000b, + V10_7 = 0x8000c, +} + +/// rdp-spec, section 2.2.1.3.2 Client Core Data +#[derive(Clone, Debug, FromPrimitive, PartialEq, Eq)] +pub enum ColorDepth { + RnsUdColor4Bpp = 0xca00, + RnsUdColor8Bpp = 0xca01, +} + +/// rdp-spec, section 2.2.1.3.2 Client Core Data +#[derive(Clone, Debug, FromPrimitive, PartialEq, Eq)] +pub enum SasSequence { + RnsUdSasDel = 0xaa03, +} + +// for keyboard layout, see windows::lcid_to_string + +/// rdp-spec, section 2.2.1.3.2 Client Core Data +#[derive(Clone, Debug, FromPrimitive, PartialEq, Eq)] +pub enum KeyboardType { + KbXt = 0x1, + KbIco = 0x2, + KbAt = 0x3, + KbEnhanced = 0x4, + Kb1050 = 0x5, + Kb9140 = 0x6, + KbJapanese = 0x7, +} + +/// rdp-spec, section 2.2.1.3.2 Client Core Data +#[derive(Clone, Debug, FromPrimitive, PartialEq, Eq)] +pub enum PostBeta2ColorDepth { + RnsUdColorNotProvided = 0x0, + RnsUdColor4Bpp = 0xca00, + RnsUdColor8Bpp = 0xca01, + RnsUdColor16Bpp555 = 0xca02, + RnsUdColor16Bpp565 = 0xca03, + RnsUdColor24Bpp = 0xca04, +} + +/// rdp-spec, section 2.2.1.3.2 Client Core Data +#[derive(Clone, Debug, FromPrimitive, PartialEq, Eq)] +pub enum HighColorDepth { + HighColorNotProvided = 0x0, + HighColor4Bpp = 0x4, + HighColor8Bpp = 0x8, + HighColor15Bpp = 0xf, + HighColor16Bpp = 0x10, + HighColor24Bpp = 0x18, +} + +// rdp-spec, section 2.2.1.3.2 Client Core Data +bitflags! { + #[derive(Default)] + pub struct SupportedColorDepth: u16 { + const RNS_UD_24_BPP_SUPPORT = 0x1; + const RNS_UD_16_BPP_SUPPORT = 0x2; + const RNS_UD_15_BPP_SUPPORT = 0x4; + const RNS_UD_32_BPP_SUPPORT = 0x8; + } +} + +// rdp-spec, section 2.2.1.3.2 Client Core Data +bitflags! { + #[derive(Default)] + pub struct EarlyCapabilityFlags: u16 { + const RNS_UD_CS_SUPPORT_ERRINFO_PDF = 0x1; + const RNS_UD_CS_WANT_32BPP_SESSION = 0x2; + const RNS_UD_CS_SUPPORT_STATUSINFO_PDU = 0x4; + const RNS_UD_CS_STRONG_ASYMMETRIC_KEYS = 0x8; + const RNS_UD_CS_UNUSED = 0x10; + const RNS_UD_CS_VALID_CONNECTION_TYPE = 0x20; + const RNS_UD_CS_SUPPORT_MONITOR_LAYOUT_PDU = 0x40; + const RNS_UD_CS_SUPPORT_NETCHAR_AUTODETECT = 0x80; + const RNS_UD_CS_SUPPORT_DYNVC_GFX_PROTOCOL = 0x100; + const RNS_UD_CS_SUPPORT_DYNAMIC_TIME_ZONE = 0x200; + const RNS_UD_CS_SUPPORT_HEARTBEAT_PDU = 0x400; + } +} + +/// rdp-spec, section 2.2.1.3.2 Client Core Data, `connectionType` +#[derive(Clone, Debug, FromPrimitive, PartialEq, Eq)] +pub enum ConnectionHint { + ConnectionHintNotProvided = 0x0, + ConnectionHintModem = 0x1, + ConnectionHintBroadbandLow = 0x2, + ConnectionHintSatellite = 0x3, + ConnectionHintBroadbandHigh = 0x4, + ConnectionHintWan = 0x5, + ConnectionHintLan = 0x6, + ConnectionHintAutoDetect = 0x7, +} + +/// rdp-spec, section 2.2.1.3.2 Client Core Data +#[derive(Clone, Copy, Debug, FromPrimitive, PartialEq, Eq)] +pub enum DesktopOrientation { + OrientationLandscape = 0, + OrientationPortrait = 90, // 0x5a + OrientationLandscapeFlipped = 180, // 0xb4 + OrientationPortraitFlipped = 270, // 0x10e +} + +/// rdp-spec, section 2.2.1.3.4 +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CsNet { + pub channels: Vec<String>, +} + +/// generic structure +/// cf. rdp-spec, section 2.2.1.3.4 +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CsUnknown { + pub typ: u16, + pub data: Vec<u8>, +} + +/// rdp-spec, section 2.2.1.4 +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct McsConnectResponse {} + +// ================== + +/// parser for t.123 and children +/// t.123-spec, section 8 +pub fn parse_t123_tpkt(input: &[u8]) -> IResult<&[u8], T123Tpkt, RdpError> { + let (i1, _version) = verify(be_u8, |&x| x == TpktVersion::T123 as u8)(input)?; + let (i2, _reserved) = be_u8(i1)?; + // less u8, u8, u16 + let (i3, sz) = map_opt(be_u16, |x: u16| x.checked_sub(4))(i2)?; + let (i4, data) = take(sz)(i3)?; + + let opt1: Option<T123TpktChild> = { + match opt(parse_x224_connection_request_class_0)(data) { + Ok((_remainder, opt)) => opt.map(T123TpktChild::X224ConnectionRequest), + Err(e) => return Err(e), + } + }; + + let opt2: Option<T123TpktChild> = match opt1 { + Some(x) => Some(x), + None => match opt(parse_x224_connection_confirm_class_0)(data) { + Ok((_remainder, opt)) => opt.map(T123TpktChild::X224ConnectionConfirm), + Err(e) => return Err(e), + }, + }; + + let opt3: Option<T123TpktChild> = match opt2 { + Some(x) => Some(x), + None => match opt(parse_x223_data_class_0)(data) { + Ok((_remainder, opt)) => opt.map(T123TpktChild::Data), + Err(e) => return Err(e), + }, + }; + let child: T123TpktChild = match opt3 { + Some(x) => x, + None => T123TpktChild::Raw(data.to_vec()), + }; + + return Ok((i4, T123Tpkt { child })); +} + +fn take_4_4_bits(input: &[u8]) -> IResult<&[u8], (u8, u8), RdpError> { + map(be_u8, |b| (b >> 4, b & 0xf))(input) +} + +fn parse_class_options(i: &[u8]) -> IResult<&[u8], (u8, u8)> { + bits( + tuple(( + verify(take_bits(4u8), |&x| x <= 4), + verify(take_bits(4u8), |&x| x <= 3) + )) + )(i) +} + +/// rdp-spec, section 2.2.1.1 +fn parse_x224_connection_request(input: &[u8]) -> IResult<&[u8], X224ConnectionRequest, RdpError> { + let (i1, length) = verify(be_u8, |&x| x != 0xff)(input)?; // 0xff is reserved + let (i2, cr_cdt) = take_4_4_bits(i1)?; + if cr_cdt.0 != X224Type::ConnectionRequest as u8 { + return Err(Err::Error(make_error(i1, ErrorKind::Verify))); + } + if !(cr_cdt.1 == 0 || cr_cdt.1 == 1) { + return Err(Err::Error(make_error(i1, ErrorKind::Verify))); + } + let (i3, dst_ref) = verify(be_u16, |&x| x == 0)(i2)?; + let (i4, src_ref) = be_u16(i3)?; + let (i5, class_options) = parse_class_options(i4).map_err(Err::convert)?; + // less cr_cdt (u8), dst_ref (u16), src_ref (u16), class_options (u8) + if length < 6 { + return Err(Err::Error(make_error(i1, ErrorKind::Verify))); + } + let i6 = i5; + let sz = length - 6; + + // + // optionally find cookie and/or negotiation request + // + + let (i7, data) = { + if sz > 0 { + take(sz)(i6)? + } else { + (i6, &[][..]) + } + }; + + let (j1, cookie) = { + if !data.is_empty() { + match opt(parse_rdp_cookie)(data) { + Ok((remainder, opt)) => (remainder, opt), + Err(e) => return Err(e), + } + } else { + (&[][..], None) + } + }; + + let (j2, negotiation_request) = { + if !j1.is_empty() { + match opt(parse_negotiation_request)(j1) { + Ok((remainder, opt)) => (remainder, opt), + Err(e) => return Err(e), + } + } else { + (&[][..], None) + } + }; + + return Ok(( + i7, + X224ConnectionRequest { + cdt: cr_cdt.1, + dst_ref, + src_ref, + class: class_options.0, + options: class_options.1, + cookie, + negotiation_request, + data: j2.to_vec(), + }, + )); +} + +/// rdp-spec, section 2.2.1.1 +/// "An X.224 Class 0 Connection Request TPDU, as specified in [X224] section 13.3." +fn parse_x224_connection_request_class_0( + input: &[u8], +) -> IResult<&[u8], X224ConnectionRequest, RdpError> { + let (i1, x224) = parse_x224_connection_request(input)?; + if x224.class == 0 && x224.options == 0 { + Ok((i1, x224)) + } else { + Err(Err::Error(RdpError::NotX224Class0Error)) + } +} + +// rdp-spec, section 2.2.1.1.1 +fn parse_rdp_cookie(i: &[u8]) -> IResult<&[u8], RdpCookie, RdpError> { + let (i, _key) = tag(b"Cookie: ")(i)?; + let (i, _name) = tag(b"mstshash=")(i)?; + let (i, bytes) = take_until_and_consume(b"\r\n")(i)?; + // let (i, s) = map_res(value!(bytes), std::str::from_utf8)(i)?; + let s = std::str::from_utf8(bytes).map_err(|_| Err::Error(make_error(bytes, ErrorKind::MapRes)))?; + let cookie = RdpCookie{ mstshash: String::from(s) }; + Ok((i, cookie)) +} + +// rdp-spec, section 2.2.1.1.1 +fn parse_negotiation_request(i: &[u8]) -> IResult<&[u8], NegotiationRequest, RdpError> { + let (i, _typ) = verify(le_u8, |&x| x == X224ConnectionRequestType::NegotiationRequest as u8)(i)?; + let (i, flags) = map_opt(le_u8, NegotiationRequestFlags::from_bits)(i)?; + // u8, u8, u16, and u32 give _length of 8 + let (i, _length) = verify(le_u16, |&x| x == 8)(i)?; + let (i, protocols) = map_opt(le_u32, ProtocolFlags::from_bits)(i)?; + Ok((i, NegotiationRequest { flags, protocols })) +} + +/// rdp-spec, section 2.2.1.2 +/// x.224-spec, section 13.3 +fn parse_x224_connection_confirm(input: &[u8]) -> IResult<&[u8], X224ConnectionConfirm, RdpError> { + let (i1, length) = verify(be_u8, |&x| x != 0xff)(input)?; // 0xff is reserved + let (i2, cr_cdt) = take_4_4_bits(i1)?; + if cr_cdt.0 != X224Type::ConnectionConfirm as u8 { + return Err(Err::Error(make_error(i1, ErrorKind::Verify))); + } + if !(cr_cdt.1 == 0 || cr_cdt.1 == 1) { + return Err(Err::Error(make_error(i1, ErrorKind::Verify))); + } + let (i3, dst_ref) = verify(be_u16, |&x| x == 0)(i2)?; + let (i4, src_ref) = be_u16(i3)?; + let (i5, class_options) = parse_class_options(i4).map_err(Err::convert)?; + + // less cr_cdt (u8), dst_ref (u16), src_ref (u16), class_options (u8) + if length < 6 { + return Err(Err::Error(make_error(i1, ErrorKind::Verify))); + } + let i6 = i5; + let sz = length - 6; + + // a negotiation message from the server might be absent (sz == 0) + let (i7, negotiation_from_server) = { + if sz > 0 { + let (i7, data) = take(sz)(i6)?; + + // it will be one of a response message or a failure message + let opt1: Option<NegotiationFromServer> = match opt(parse_negotiation_response)(data) { + Ok((_remainder, opt)) => opt.map(NegotiationFromServer::Response), + Err(e) => return Err(e), + }; + let opt2: Option<NegotiationFromServer> = match opt1 { + Some(x) => Some(x), + None => match opt(parse_negotiation_failure)(data) { + Ok((_remainder, opt)) => opt.map(NegotiationFromServer::Failure), + Err(e) => return Err(e), + }, + }; + (i7, opt2) + } else { + (i6, None) + } + }; + + return Ok(( + i7, + X224ConnectionConfirm { + cdt: cr_cdt.1, + dst_ref, + src_ref, + class: class_options.0, + options: class_options.1, + negotiation_from_server, + }, + )); +} + +/// rdp-spec, section 2.2.1.2 +/// "An X.224 Class 0 Connection Confirm TPDU, as specified in [X224] section 13.4." +fn parse_x224_connection_confirm_class_0( + input: &[u8], +) -> IResult<&[u8], X224ConnectionConfirm, RdpError> { + let (i1, x224) = parse_x224_connection_confirm(input)?; + if x224.class == 0 && x224.options == 0 { + Ok((i1, x224)) + } else { + // x.224, but not a class 0 x.224 message + Err(Err::Error(RdpError::NotX224Class0Error)) + } +} + +// rdp-spec, section 2.2.1.1.1 +fn parse_negotiation_response(i: &[u8]) -> IResult<&[u8], NegotiationResponse, RdpError> { + let (i, _typ) = verify(le_u8, |&x| x == X224ConnectionRequestType::NegotiationResponse as u8)(i)?; + let (i, flags) = map_opt(le_u8, NegotiationResponseFlags::from_bits)(i)?; + // u8, u8, u16, and u32 give _length of 8 + let (i, _length) = verify(le_u16, |&x| x == 8)(i)?; + let (i, protocol) = map_opt(le_u32, num::FromPrimitive::from_u32)(i)?; + Ok((i, NegotiationResponse { flags, protocol })) +} + +// rdp-spec, section 2.2.1.1.1 +fn parse_negotiation_failure(i: &[u8]) -> IResult<&[u8], NegotiationFailure, RdpError> { + let (i, _typ) = verify(le_u8, |&x| x == X224ConnectionRequestType::NegotiationFailure as u8)(i)?; + let (i, _flags) = le_u8(i)?; + // u8, u8, u16, and u32 give _length of 8 + let (i, _length) = verify(le_u16, |&x| x == 8)(i)?; + let (i, code) = map_opt(le_u32, num::FromPrimitive::from_u32)(i)?; + Ok((i, NegotiationFailure { code })) +} + +/// x224-spec, section 13.7 +fn parse_x223_data_class_0(input: &[u8]) -> IResult<&[u8], X223Data, RdpError> { + fn parser(i: &[u8]) -> IResult<&[u8], (u8, u8, u8)> { + bits( + tuple(( + verify(take_bits(4u8), |&x| x == 0xf), + verify(take_bits(3u8), |&x| x == 0), + verify(take_bits(1u8), |&x| x == 0) + )) + )(i) + } + let (i1, _length) = verify(be_u8, |&x| x == 2)(input)?; + let (i2, _dt_x_roa) = parser(i1).map_err(Err::convert)?; + let (i3, _eot) = verify(be_u8, |&x| x == 0x80)(i2)?; + + // + // optionally find exactly one of the child messages + // + + let opt1: Option<X223DataChild> = match opt(parse_mcs_connect)(i3) { + Ok((_remainder, opt)) => opt.map(X223DataChild::McsConnectRequest), + Err(e) => return Err(e), + }; + + let opt2: Option<X223DataChild> = match opt1 { + Some(x) => Some(x), + None => match opt(parse_mcs_connect_response)(i3) { + Ok((_remainder, opt)) => opt.map(X223DataChild::McsConnectResponse), + Err(e) => return Err(e), + }, + }; + + let child: X223DataChild = match opt2 { + Some(x) => x, + None => X223DataChild::Raw(i3.to_vec()), + }; + + return Ok((&[], X223Data { child })); +} + +/// rdp-spec, section 2.2.1.3.2 +fn parse_mcs_connect(input: &[u8]) -> IResult<&[u8], McsConnectRequest, RdpError> { + let (i1, _ber_type) = verify( + le_u8, + // BER: 0b01=application, 0b1=non-primitive, 0b11111 + |&x| x == 0x7f + )(input)?; + let (i2, _t125_type) = verify(le_u8, |&x| x + == T125Type::T125TypeMcsConnectRequest as u8)(i1)?; + + // skip to, and consume, H.221 client-to-server key + let (i3, _skipped) = take_until_and_consume(b"Duca")(i2)?; + + let (i4, data) = length_data(parse_per_length_determinant)(i3)?; + let mut remainder: &[u8] = data; + let mut children = Vec::new(); + + // repeatedly attempt to parse optional CsClientCoreData, CsNet, and CsUnknown + // until data buffer is exhausted + loop { + remainder = match opt(parse_cs_client_core_data)(remainder) { + Ok((rem, o)) => match o { + // found CsClientCoreData + Some(core_data) => { + children.push(McsConnectRequestChild::CsClientCore(core_data)); + rem + } + None => match opt(parse_cs_net)(remainder) { + // found CsNet + Ok((rem, o)) => match o { + Some(net) => { + children.push(McsConnectRequestChild::CsNet(net)); + rem + } + None => { + match opt(parse_cs_unknown)(remainder) { + // was able to parse CsUnknown + Ok((rem, o)) => match o { + Some(unknown) => { + children.push(McsConnectRequestChild::CsUnknown(unknown)); + rem + } + None => { + break; + } + }, + Err(Err::Incomplete(i)) => { + return Err(Err::Incomplete(i)) + } + Err(Err::Failure(_)) | Err(Err::Error(_)) => break, + } + } + }, + Err(Err::Incomplete(i)) => return Err(Err::Incomplete(i)), + Err(Err::Failure(_)) | Err(Err::Error(_)) => break, + }, + }, + Err(Err::Incomplete(i)) => return Err(Err::Incomplete(i)), + Err(Err::Failure(_)) | Err(Err::Error(_)) => break, + }; + if remainder.is_empty() { + break; + } + } + + return Ok((i4, McsConnectRequest { children })); +} + +/// rdp-spec, section 2.2.1.3.2 +fn parse_cs_client_core_data(input: &[u8]) -> IResult<&[u8], CsClientCoreData> { + let (i1, _typ) = verify(le_u16, |&x| x == CsType::Core as u16)(input)?; + // less u16, u16 + let (i2, sz) = map_opt(le_u16, |x: u16| x.checked_sub(4))(i1)?; + let (i3, data) = take(sz)(i2)?; + let (j1, version) = map(le_u32, num::FromPrimitive::from_u32)(data)?; + let (j2, desktop_width) = le_u16(j1)?; + let (j3, desktop_height) = le_u16(j2)?; + let (j4, color_depth) = map(le_u16, num::FromPrimitive::from_u16)(j3)?; + let (j5, sas_sequence) = map(le_u16, num::FromPrimitive::from_u16)(j4)?; + let (j6, keyboard_layout) = le_u32(j5)?; + let (j7, client_build) = map(le_u32, windows::build_number_to_os)(j6)?; + let (j8, client_name) = map_res(take(32_usize), le_slice_to_string)(j7)?; + let (j9, keyboard_type) = map(le_u32, num::FromPrimitive::from_u32)(j8)?; + let (j10, keyboard_subtype) = le_u32(j9)?; + let (j11, keyboard_function_key) = le_u32(j10)?; + let (j12, ime_file_name) = map_res(take(64_usize), le_slice_to_string)(j11)?; + + // + // optional fields below (but each requires the previous) + // + + let (j13, post_beta2_color_depth) = + match opt(map_opt(le_u16, num::FromPrimitive::from_u16))(j12) as IResult<&[u8], _> { + Ok((rem, obj)) => (rem, obj), + _ => (j12, None), + }; + + let (j14, client_product_id) = match post_beta2_color_depth { + None => (j13, None), + Some(_) => match opt(le_u16)(j13) as IResult<&[u8], _> { + Ok((rem, obj)) => (rem, obj), + _ => (j13, None), + }, + }; + + let (j15, serial_number) = match client_product_id { + None => (j14, None), + Some(_) => match opt(le_u32)(j14) as IResult<&[u8], _> { + Ok((rem, obj)) => (rem, obj), + _ => (j14, None), + }, + }; + + let (j16, high_color_depth) = match serial_number { + None => (j15, None), + Some(_) => { + match opt(map_opt(le_u16, num::FromPrimitive::from_u16))(j15) as IResult<&[u8], _> { + Ok((rem, obj)) => (rem, obj), + _ => (j15, None), + } + } + }; + + let (j17, supported_color_depth) = match high_color_depth { + None => (j16, None), + Some(_) => { + match opt(map_opt(le_u16, SupportedColorDepth::from_bits))(j16) as IResult<&[u8], _> { + Ok((rem, obj)) => (rem, obj), + _ => (j16, None), + } + } + }; + + let (j18, early_capability_flags) = match supported_color_depth { + None => (j17, None), + Some(_) => { + match opt(map_opt(le_u16, EarlyCapabilityFlags::from_bits))(j17) as IResult<&[u8], _> + { + Ok((rem, obj)) => (rem, obj), + _ => (j17, None), + } + } + }; + + let (j19, client_dig_product_id) = match early_capability_flags { + None => (j18, None), + Some(_) => { + match opt(map_res(take(64usize), le_slice_to_string))(j18) as IResult<&[u8], _> { + Ok((rem, obj)) => (rem, obj), + _ => (j18, None), + } + } + }; + + let (j20, connection_hint) = match client_dig_product_id { + None => (j19, None), + Some(_) => { + match opt(map_opt(le_u8, num::FromPrimitive::from_u8))(j19) as IResult<&[u8], _> { + Ok((rem, obj)) => (rem, obj), + _ => (j19, None), + } + } + }; + + let (j21, pad) = match connection_hint { + None => (j20, None), + Some(_) => match opt(take(1usize))(j20) as IResult<&[u8], _> { + Ok((rem, obj)) => (rem, obj), + _ => (j20, None), + }, + }; + + let (j22, server_selected_protocol) = match pad { + None => (j21, None), + Some(_) => { + match opt(map_opt(le_u32, ProtocolFlags::from_bits))(j21) as IResult<&[u8], _> { + Ok((rem, obj)) => (rem, obj), + _ => (j21, None), + } + } + }; + + let (j23, desktop_physical_width) = match server_selected_protocol { + None => (j22, None), + Some(_) => match opt(map_opt(le_u32, millimeters_to_opt))(j22) as IResult<&[u8], _> { + Ok((rem, obj)) => (rem, obj), + _ => (j22, None), + }, + }; + + let (j24, desktop_physical_height) = match desktop_physical_width { + None => (j23, None), + Some(_) => match opt(map_opt(le_u32, millimeters_to_opt))(j23) as IResult<&[u8], _> { + Ok((rem, obj)) => (rem, obj), + _ => (j23, None), + }, + }; + + let (j25, desktop_orientation) = match desktop_physical_height { + None => (j24, None), + Some(_) => { + match opt(map_opt(le_u16, num::FromPrimitive::from_u16))(j24) as IResult<&[u8], _> { + Ok((rem, obj)) => (rem, obj), + _ => (j24, None), + } + } + }; + + let (j26, desktop_scale_factor) = match desktop_orientation { + None => (j25, None), + Some(_) => match opt(map_opt(le_u32, desktop_scale_to_opt))(j25) as IResult<&[u8], _> { + Ok((rem, obj)) => (rem, obj), + _ => (j25, None), + }, + }; + + let (_j27, device_scale_factor) = match desktop_scale_factor { + None => (j26, None), + Some(_) => match opt(map_opt(le_u32, device_scale_to_opt))(j26) as IResult<&[u8], _> { + Ok((rem, obj)) => (rem, obj), + _ => (j26, None), + }, + }; + + return Ok(( + i3, + CsClientCoreData { + version, + desktop_width, + desktop_height, + color_depth, + sas_sequence, + keyboard_layout, + client_build, + client_name, + keyboard_type, + keyboard_subtype, + keyboard_function_key, + ime_file_name, + post_beta2_color_depth, + client_product_id, + serial_number, + high_color_depth, + supported_color_depth, + early_capability_flags, + client_dig_product_id, + connection_hint, + server_selected_protocol, + desktop_physical_width, + desktop_physical_height, + desktop_orientation, + desktop_scale_factor, + device_scale_factor, + }, + )); +} + +/// rdp-spec, section 2.2.1.3.4 +fn parse_cs_net(input: &[u8]) -> IResult<&[u8], CsNet> { + let (i1, _typ) = verify(le_u16, |&x| x == CsType::Net as u16)(input)?; + // less _typ (u16), this length indicator (u16), count (u32) + let (i2, sz) = map_opt(le_u16, |x: u16| x.checked_sub(8))(i1)?; + let (i3, count) = le_u32(i2)?; + let (i4, data) = take(sz)(i3)?; + + let mut remainder: &[u8] = data; + let mut channels = Vec::new(); + + for _index in 0..count { + // a channel name is 8 bytes, section 2.2.1.3.4.1 + let (j1, name) = map_res(take(8_usize), utf7_slice_to_string)(remainder)?; + channels.push(name); + // options (u32) are discarded for now + let (j2, _options) = le_u32(j1)?; + remainder = j2; + } + + return Ok((i4, CsNet { channels })); +} + +// generic CS structure parse +// cf. rdp-spec, section 2.2.1.3.4 +fn parse_cs_unknown(i: &[u8]) -> IResult<&[u8], CsUnknown> { + let (i, typ) = map_opt(le_u16, |x| { + let opt: Option<CsType> = num::FromPrimitive::from_u16(x); + match opt { + // an unknown type must not be present in CsType + Some(_) => None, + None => Some(x), + } + })(i)?; + // less u16, u16 + let (i, sz) = map_opt(le_u16, |x: u16| x.checked_sub(4))(i)?; + let (i, data) = take(sz)(i)?; + Ok((i, CsUnknown { typ, data: data.to_vec() })) +} + +// rdp-spec, section 2.2.1.4 +fn parse_mcs_connect_response(i: &[u8]) -> IResult<&[u8], McsConnectResponse, RdpError> { + let (i, _ber_type) = verify( + le_u8, + // BER: 0b01=application, 0b1=non-primitive, 0b11111 + |&x| x == 0x7f)(i)?; + let (i, _t125_type) = verify(le_u8, |&x| x == T125Type::T125TypeMcsConnectResponse as u8)(i)?; + Ok((i, McsConnectResponse {})) +} + +#[cfg(test)] +mod tests_cookie_21182 { + use crate::rdp::parser::*; + + static BYTES: [u8; 37] = [ + 0x03, 0x00, 0x00, 0x25, 0x20, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x43, 0x6f, 0x6f, 0x6b, + 0x69, 0x65, 0x3a, 0x20, 0x6d, 0x73, 0x74, 0x73, 0x68, 0x61, 0x73, 0x68, 0x3d, 0x75, 0x73, + 0x65, 0x72, 0x31, 0x32, 0x33, 0x0d, 0x0a, + ]; + + #[test] + fn test_t123_x224_cookie() { + let t123_bytes = &BYTES[..]; + let t123_tpkt: T123Tpkt = T123Tpkt { + child: T123TpktChild::X224ConnectionRequest(X224ConnectionRequest { + cdt: 0, + dst_ref: 0, + src_ref: 0, + class: 0, + options: 0, + cookie: Some(RdpCookie { + mstshash: String::from("user123"), + }), + negotiation_request: None, + data: Vec::new(), + }), + }; + assert_eq!(Ok((&[][..], t123_tpkt)), parse_t123_tpkt(t123_bytes)); + } +} + +#[cfg(test)] +mod tests_negotiate_49350 { + use crate::rdp::parser::*; + + static BYTES: [u8; 20] = [ + 0x03, 0x00, 0x00, 0x13, 0x0e, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x08, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xff, + ]; + static TPKT_BEGIN: usize = 0; + static X224_BEGIN: usize = TPKT_BEGIN + 4; + static NEG_REQ_BEGIN: usize = X224_BEGIN + 7; + static NEG_REQ_END: usize = NEG_REQ_BEGIN + 8; + static X224_END: usize = NEG_REQ_END; + static TPKT_END: usize = X224_END; + static PADDING_BEGIN: usize = TPKT_END; + + #[test] + fn test_t123_x224_negotiate() { + let t123_bytes = &BYTES[TPKT_BEGIN..]; + let t123_tpkt: T123Tpkt = T123Tpkt { + child: T123TpktChild::X224ConnectionRequest(X224ConnectionRequest { + cdt: 0, + dst_ref: 0, + src_ref: 0, + class: 0, + options: 0, + cookie: None, + negotiation_request: Some(NegotiationRequest { + flags: NegotiationRequestFlags::empty(), + protocols: ProtocolFlags { bits: Protocol::ProtocolRdp as u32 }, + }), + data: Vec::new(), + }), + }; + assert_eq!( + Ok((&BYTES[PADDING_BEGIN..][..], t123_tpkt)), + parse_t123_tpkt(t123_bytes) + ) + } +} + +#[cfg(test)] +mod tests_core_49350 { + use crate::rdp::parser::*; + + static BYTES: [u8; 429] = [ + 0x03, 0x00, 0x01, 0xac, 0x02, 0xf0, 0x80, 0x7f, 0x65, 0x82, 0x01, 0xa0, 0x04, 0x01, 0x01, + 0x04, 0x01, 0x01, 0x01, 0x01, 0xff, 0x30, 0x19, 0x02, 0x01, 0x22, 0x02, 0x01, 0x02, 0x02, + 0x01, 0x00, 0x02, 0x01, 0x01, 0x02, 0x01, 0x00, 0x02, 0x01, 0x01, 0x02, 0x02, 0xff, 0xff, + 0x02, 0x01, 0x02, 0x30, 0x19, 0x02, 0x01, 0x01, 0x02, 0x01, 0x01, 0x02, 0x01, 0x01, 0x02, + 0x01, 0x01, 0x02, 0x01, 0x00, 0x02, 0x01, 0x01, 0x02, 0x02, 0x04, 0x20, 0x02, 0x01, 0x02, + 0x30, 0x1c, 0x02, 0x02, 0xff, 0xff, 0x02, 0x02, 0xfc, 0x17, 0x02, 0x02, 0xff, 0xff, 0x02, + 0x01, 0x01, 0x02, 0x01, 0x00, 0x02, 0x01, 0x01, 0x02, 0x02, 0xff, 0xff, 0x02, 0x01, 0x02, + 0x04, 0x82, 0x01, 0x3f, 0x00, 0x05, 0x00, 0x14, 0x7c, 0x00, 0x01, 0x81, 0x36, 0x00, 0x08, + 0x00, 0x10, 0x00, 0x01, 0xc0, 0x00, 0x44, 0x75, 0x63, 0x61, 0x81, 0x28, 0x01, 0xc0, 0xd8, + 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x05, 0x00, 0x03, 0x01, 0xca, 0x03, 0xaa, 0x09, 0x04, + 0x00, 0x00, 0x71, 0x17, 0x00, 0x00, 0x53, 0x00, 0x45, 0x00, 0x52, 0x00, 0x56, 0x00, 0x45, + 0x00, 0x52, 0x00, 0x2d, 0x00, 0x58, 0x00, 0x59, 0x00, 0x5a, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xca, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x0f, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x04, 0xc0, 0x0c, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x02, 0xc0, 0x0c, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xc0, 0x38, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x72, 0x64, 0x70, 0x64, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x80, 0x80, 0x72, 0x64, 0x70, 0x73, 0x6e, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x64, + 0x72, 0x64, 0x79, 0x6e, 0x76, 0x63, 0x00, 0x00, 0x00, 0x80, 0xc0, 0x63, 0x6c, 0x69, 0x70, + 0x72, 0x64, 0x72, 0x00, 0x00, 0x00, 0xa0, 0xc0, 0xff, + ]; + static TPKT_BEGIN: usize = 0; + static X223_BEGIN: usize = TPKT_BEGIN + 4; + static MCS_CONNECT_BEGIN: usize = X223_BEGIN + 3; + static MCS_CONNECT_END: usize = MCS_CONNECT_BEGIN + 421; + static X223_END: usize = MCS_CONNECT_END; + static TPKT_END: usize = X223_END; + static PADDING_BEGIN: usize = TPKT_END; + + #[test] + fn test_t123_x223_connect_core() { + let t123_bytes = &BYTES[TPKT_BEGIN..]; + let core_data = CsClientCoreData { + version: Some(RdpClientVersion::V5_V8_1), + desktop_width: 1280, + desktop_height: 768, + color_depth: Some(ColorDepth::RnsUdColor8Bpp), + sas_sequence: Some(SasSequence::RnsUdSasDel), + keyboard_layout: 0x409, + client_build: windows::OperatingSystem { + build: windows::Build::Vista_6001, + suffix: windows::Suffix::Sp1, + }, + client_name: String::from("SERVER-XYZ"), + keyboard_type: Some(KeyboardType::KbEnhanced), + keyboard_subtype: 0, + keyboard_function_key: 12, + ime_file_name: String::from(""), + post_beta2_color_depth: Some(PostBeta2ColorDepth::RnsUdColor8Bpp), + client_product_id: Some(1), + serial_number: Some(0), + high_color_depth: Some(HighColorDepth::HighColor8Bpp), + supported_color_depth: Some( + SupportedColorDepth::RNS_UD_15_BPP_SUPPORT + | SupportedColorDepth::RNS_UD_16_BPP_SUPPORT + | SupportedColorDepth::RNS_UD_24_BPP_SUPPORT + | SupportedColorDepth::RNS_UD_32_BPP_SUPPORT, + ), + early_capability_flags: Some( + EarlyCapabilityFlags::RNS_UD_CS_SUPPORT_ERRINFO_PDF + | EarlyCapabilityFlags::RNS_UD_CS_STRONG_ASYMMETRIC_KEYS, + ), + client_dig_product_id: Some(String::from("")), + connection_hint: Some(ConnectionHint::ConnectionHintNotProvided), + server_selected_protocol: Some(ProtocolFlags { bits: Protocol::ProtocolRdp as u32 }), + desktop_physical_width: None, + desktop_physical_height: None, + desktop_orientation: None, + desktop_scale_factor: None, + device_scale_factor: None, + }; + let mut children = Vec::new(); + children.push(McsConnectRequestChild::CsClientCore(core_data)); + children.push(McsConnectRequestChild::CsUnknown(CsUnknown { + typ: 0xc004, + data: BYTES[0x160..0x160 + 0x8].to_vec(), + })); + children.push(McsConnectRequestChild::CsUnknown(CsUnknown { + typ: 0xc002, + data: BYTES[0x16c..0x16c + 0x8].to_vec(), + })); + let mut channels = Vec::new(); + channels.push(String::from("rdpdr")); + channels.push(String::from("rdpsnd")); + channels.push(String::from("drdynvc")); + channels.push(String::from("cliprdr")); + children.push(McsConnectRequestChild::CsNet(CsNet { channels })); + let t123_tpkt: T123Tpkt = T123Tpkt { + child: T123TpktChild::Data(X223Data { + child: X223DataChild::McsConnectRequest(McsConnectRequest { children }), + }), + }; + assert_eq!( + Ok((&BYTES[PADDING_BEGIN..][..], t123_tpkt)), + parse_t123_tpkt(t123_bytes) + ); + } +} + +#[cfg(test)] +mod tests_x223_response_49350 { + use crate::rdp::parser::*; + + // changed offset 9 from 0x65 to 0x66 so it is no longer an mcs connect + static BYTES: [u8; 9] = [0x03, 0x00, 0x00, 0x09, 0x02, 0xf0, 0x80, 0x7f, 0x66]; + + #[test] + fn test_x223_response() { + let t123_bytes = &BYTES[..]; + assert_eq!( + Ok(( + &[][..], + T123Tpkt { + child: T123TpktChild::Data(X223Data { + child: X223DataChild::McsConnectResponse(McsConnectResponse {}), + }) + } + )), + parse_t123_tpkt(t123_bytes) + ) + } +} + +#[cfg(test)] +mod tests_t123_raw_49350 { + use crate::rdp::parser::*; + + // changed offset 4 from 0x02 to 0x03 so it is no longer an X223 data object + static BYTES: [u8; 9] = [0x03, 0x00, 0x00, 0x09, 0x03, 0xf0, 0x80, 0x7f, 0x65]; + + #[test] + fn test_t123_raw() { + let t123_bytes = &BYTES[..]; + assert_eq!( + Ok(( + &[][..], + T123Tpkt { + child: T123TpktChild::Raw(BYTES[4..].to_vec()) + } + )), + parse_t123_tpkt(t123_bytes) + ) + } +} + +#[cfg(test)] +mod tests_x224_raw_49350 { + use crate::rdp::parser::*; + + // changed offset 11 from 0x01 to 0x02 so it is not a known X224 payload type + static BYTES: [u8; 19] = [ + 0x03, 0x00, 0x00, 0x13, 0x0e, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x08, 0x00, + 0x00, 0x00, 0x00, 0x00, + ]; + + #[test] + fn test_x224_raw() { + let t123_bytes = &BYTES[..]; + assert_eq!( + Ok(( + &[][..], + T123Tpkt { + child: T123TpktChild::X224ConnectionRequest(X224ConnectionRequest { + cdt: 0, + dst_ref: 0, + src_ref: 0, + class: 0, + options: 0, + cookie: None, + negotiation_request: None, + data: BYTES[11..].to_vec(), + }) + } + )), + parse_t123_tpkt(t123_bytes) + ) + } +} + +#[cfg(test)] +mod tests_x223_raw_49350 { + use crate::rdp::parser::*; + + // changed offset 9 from 0x65 to 0xff so it is no longer an mcs connect + static BYTES: [u8; 9] = [0x03, 0x00, 0x00, 0x09, 0x02, 0xf0, 0x80, 0x7f, 0xff]; + + #[test] + fn test_x223_raw() { + let t123_bytes = &BYTES[..]; + assert_eq!( + Ok(( + &[][..], + T123Tpkt { + child: T123TpktChild::Data(X223Data { + child: X223DataChild::Raw(BYTES[7..].to_vec()), + }) + } + )), + parse_t123_tpkt(t123_bytes) + ) + } +} + +#[cfg(test)] +mod tests_negotiate_incomplete_49350 { + use crate::rdp::parser::*; + use nom7::Needed; + + static BYTES: [u8; 19] = [ + 0x03, 0x00, 0x00, 0x13, 0x0e, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x08, 0x00, + 0x00, 0x00, 0x00, 0x00, + ]; + static TPKT_BEGIN: usize = 0; + static X224_BEGIN: usize = TPKT_BEGIN + 4; + static NEG_REQ_BEGIN: usize = X224_BEGIN + 7; + static NEG_REQ_END: usize = NEG_REQ_BEGIN + 8; + static X224_END: usize = NEG_REQ_END; + static TPKT_END: usize = X224_END; + + #[test] + fn test_t123_incomplete() { + let t123_bytes = &BYTES[TPKT_BEGIN..TPKT_END - 1]; + assert_eq!( + // fails: map_opt!(i2, be_u16, |x: u16| x.checked_sub(4))? + Err(Err::Incomplete(Needed::new(1))), + parse_t123_tpkt(t123_bytes) + ) + } + + #[test] + fn test_x224_incomplete() { + let x224_bytes = &BYTES[X224_BEGIN..X224_END - 1]; + assert_eq!( + // fails: expr_opt!(i5, length.checked_sub(6))? + // not counting a u8 length read, which was also successful + Err(Err::Incomplete(Needed::new( 1))), + parse_x224_connection_request_class_0(x224_bytes) + ) + } + + #[test] + fn test_negotiate_incomplete() { + let neg_req_bytes = &BYTES[NEG_REQ_BEGIN..NEG_REQ_END - 1]; + assert_eq!( + // fails: map_opt!(le_u32, num::FromPrimitive::from_u32)? + Err(Err::Incomplete(Needed::new(1))), + parse_negotiation_request(neg_req_bytes) + ) + } +} + +#[cfg(test)] +mod tests_core_incomplete_49350 { + use crate::rdp::parser::*; + use nom7::Needed; + + static BYTES: [u8; 428] = [ + 0x03, 0x00, 0x01, 0xac, 0x02, 0xf0, 0x80, 0x7f, 0x65, 0x82, 0x01, 0xa0, 0x04, 0x01, 0x01, + 0x04, 0x01, 0x01, 0x01, 0x01, 0xff, 0x30, 0x19, 0x02, 0x01, 0x22, 0x02, 0x01, 0x02, 0x02, + 0x01, 0x00, 0x02, 0x01, 0x01, 0x02, 0x01, 0x00, 0x02, 0x01, 0x01, 0x02, 0x02, 0xff, 0xff, + 0x02, 0x01, 0x02, 0x30, 0x19, 0x02, 0x01, 0x01, 0x02, 0x01, 0x01, 0x02, 0x01, 0x01, 0x02, + 0x01, 0x01, 0x02, 0x01, 0x00, 0x02, 0x01, 0x01, 0x02, 0x02, 0x04, 0x20, 0x02, 0x01, 0x02, + 0x30, 0x1c, 0x02, 0x02, 0xff, 0xff, 0x02, 0x02, 0xfc, 0x17, 0x02, 0x02, 0xff, 0xff, 0x02, + 0x01, 0x01, 0x02, 0x01, 0x00, 0x02, 0x01, 0x01, 0x02, 0x02, 0xff, 0xff, 0x02, 0x01, 0x02, + 0x04, 0x82, 0x01, 0x3f, 0x00, 0x05, 0x00, 0x14, 0x7c, 0x00, 0x01, 0x81, 0x36, 0x00, 0x08, + 0x00, 0x10, 0x00, 0x01, 0xc0, 0x00, 0x44, 0x75, 0x63, 0x61, 0x81, 0x28, 0x01, 0xc0, 0xd8, + 0x00, 0x04, 0x00, 0x08, 0x00, 0x00, 0x05, 0x00, 0x03, 0x01, 0xca, 0x03, 0xaa, 0x09, 0x04, + 0x00, 0x00, 0x71, 0x17, 0x00, 0x00, 0x53, 0x00, 0x45, 0x00, 0x52, 0x00, 0x56, 0x00, 0x45, + 0x00, 0x52, 0x00, 0x2d, 0x00, 0x58, 0x00, 0x59, 0x00, 0x5a, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xca, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x0f, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x04, 0xc0, 0x0c, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x02, 0xc0, 0x0c, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xc0, 0x38, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x72, 0x64, 0x70, 0x64, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x80, 0x80, 0x72, 0x64, 0x70, 0x73, 0x6e, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x64, + 0x72, 0x64, 0x79, 0x6e, 0x76, 0x63, 0x00, 0x00, 0x00, 0x80, 0xc0, 0x63, 0x6c, 0x69, 0x70, + 0x72, 0x64, 0x72, 0x00, 0x00, 0x00, 0xa0, 0xc0, + ]; + static X223_BEGIN: usize = 4; + static MCS_CONNECT_BEGIN: usize = X223_BEGIN + 3; + static MCS_CONNECT_END: usize = MCS_CONNECT_BEGIN + 421; + static _X223_END: usize = MCS_CONNECT_END; + + #[test] + fn test_x223_incomplete() { + let x223_bytes = &BYTES[X223_BEGIN..X223_BEGIN + 2]; + assert_eq!( + // fails: verify!(i2, be_u8, |x| x == 0x80)? + Err(Err::Incomplete(Needed::new(1))), + parse_x223_data_class_0(x223_bytes) + ) + } + + #[test] + fn test_connect_incomplete() { + let connect_bytes = &BYTES[MCS_CONNECT_BEGIN..MCS_CONNECT_END - 1]; + assert_eq!( + // fails: length_data!(i3, parse_per_length_determinant)? + // which reads the length (2) but not the full data (0x128) + Err(Err::Incomplete(Needed::new(1))), + parse_mcs_connect(connect_bytes) + ) + } +} diff --git a/rust/src/rdp/rdp.rs b/rust/src/rdp/rdp.rs new file mode 100644 index 0000000..f08026a --- /dev/null +++ b/rust/src/rdp/rdp.rs @@ -0,0 +1,667 @@ +/* Copyright (C) 2019-2022 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. + */ + +// Author: Zach Kelly <zach.kelly@lmco.com> + +//! RDP application layer + +use crate::applayer::{self, *}; +use crate::core::{AppProto, Flow, ALPROTO_UNKNOWN, IPPROTO_TCP}; +use crate::rdp::parser::*; +use nom7::Err; +use std; +use std::collections::VecDeque; +use tls_parser::{parse_tls_plaintext, TlsMessage, TlsMessageHandshake, TlsRecordType}; + +static mut ALPROTO_RDP: AppProto = ALPROTO_UNKNOWN; + +// +// transactions +// + +#[derive(Debug, PartialEq, Eq)] +pub struct CertificateBlob { + pub data: Vec<u8>, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum RdpTransactionItem { + X224ConnectionRequest(X224ConnectionRequest), + X224ConnectionConfirm(X224ConnectionConfirm), + McsConnectRequest(McsConnectRequest), + McsConnectResponse(McsConnectResponse), + TlsCertificateChain(Vec<CertificateBlob>), +} + +#[derive(Debug, PartialEq, Eq)] +pub struct RdpTransaction { + pub id: u64, + pub item: RdpTransactionItem, + // managed by macros `export_tx_get_detect_state!` and `export_tx_set_detect_state!` + tx_data: AppLayerTxData, +} + +impl Transaction for RdpTransaction { + fn id(&self) -> u64 { + self.id + } +} + +impl RdpTransaction { + fn new(id: u64, item: RdpTransactionItem) -> Self { + Self { + id, + item, + tx_data: AppLayerTxData::new(), + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn rs_rdp_state_get_tx( + state: *mut std::os::raw::c_void, tx_id: u64, +) -> *mut std::os::raw::c_void { + let state = cast_pointer!(state, RdpState); + match state.get_tx(tx_id) { + Some(tx) => { + return tx as *const _ as *mut _; + } + None => { + return std::ptr::null_mut(); + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn rs_rdp_state_get_tx_count(state: *mut std::os::raw::c_void) -> u64 { + let state = cast_pointer!(state, RdpState); + return state.next_id; +} + +#[no_mangle] +pub extern "C" fn rs_rdp_tx_get_progress( + _tx: *mut std::os::raw::c_void, _direction: u8, +) -> std::os::raw::c_int { + // tx complete when `rs_rdp_tx_get_progress(...) == rs_rdp_tx_get_progress_complete(...)` + // here, all transactions are immediately complete on insert + return 1; +} + +// +// state +// + +#[derive(Debug, PartialEq, Eq)] +pub struct RdpState { + state_data: AppLayerStateData, + next_id: u64, + transactions: VecDeque<RdpTransaction>, + tls_parsing: bool, + bypass_parsing: bool, +} + +impl State<RdpTransaction> for RdpState { + fn get_transaction_count(&self) -> usize { + self.transactions.len() + } + + fn get_transaction_by_index(&self, index: usize) -> Option<&RdpTransaction> { + self.transactions.get(index) + } +} + +impl RdpState { + fn new() -> Self { + Self { + state_data: AppLayerStateData::new(), + next_id: 0, + transactions: VecDeque::new(), + tls_parsing: false, + bypass_parsing: false, + } + } + + fn free_tx(&mut self, tx_id: u64) { + let len = self.transactions.len(); + let mut found = false; + let mut index = 0; + for ii in 0..len { + let tx = &self.transactions[ii]; + if tx.id == tx_id { + found = true; + index = ii; + break; + } + } + if found { + self.transactions.remove(index); + } + } + + fn get_tx(&self, tx_id: u64) -> Option<&RdpTransaction> { + self.transactions.iter().find(|&tx| tx.id == tx_id) + } + + fn new_tx(&mut self, item: RdpTransactionItem) -> RdpTransaction { + self.next_id += 1; + let tx = RdpTransaction::new(self.next_id, item); + return tx; + } + + /// parse buffer captures from client to server + fn parse_ts(&mut self, input: &[u8]) -> AppLayerResult { + // no need to process input buffer + if self.bypass_parsing { + return AppLayerResult::ok(); + } + let mut available = input; + + loop { + if available.is_empty() { + return AppLayerResult::ok(); + } + if self.tls_parsing { + match parse_tls_plaintext(available) { + Ok((remainder, _tls)) => { + // bytes available for further parsing are what remain + available = remainder; + } + + Err(Err::Incomplete(_)) => { + // nom need not compatible with applayer need, request one more byte + return AppLayerResult::incomplete( + (input.len() - available.len()) as u32, + (available.len() + 1) as u32, + ); + } + + Err(Err::Failure(_)) | Err(Err::Error(_)) => { + return AppLayerResult::err(); + } + } + } else { + // every message should be encapsulated within a T.123 tpkt + match parse_t123_tpkt(available) { + // success + Ok((remainder, t123)) => { + // bytes available for further parsing are what remain + available = remainder; + // evaluate message within the tpkt + match t123.child { + // X.224 connection request + T123TpktChild::X224ConnectionRequest(x224) => { + let tx = + self.new_tx(RdpTransactionItem::X224ConnectionRequest(x224)); + self.transactions.push_back(tx); + } + + // X.223 data packet, evaluate what it encapsulates + T123TpktChild::Data(x223) => { + #[allow(clippy::single_match)] + match x223.child { + X223DataChild::McsConnectRequest(mcs) => { + let tx = + self.new_tx(RdpTransactionItem::McsConnectRequest(mcs)); + self.transactions.push_back(tx); + } + // unknown message in X.223, skip + _ => (), + } + } + + // unknown message in T.123, skip + _ => (), + } + } + + Err(Err::Incomplete(_)) => { + // nom need not compatible with applayer need, request one more byte + return AppLayerResult::incomplete( + (input.len() - available.len()) as u32, + (available.len() + 1) as u32, + ); + } + + Err(Err::Failure(_)) | Err(Err::Error(_)) => { + if probe_tls_handshake(available) { + self.tls_parsing = true; + let r = self.parse_ts(available); + if r.status == 1 { + //adds bytes already consumed to incomplete result + let consumed = (input.len() - available.len()) as u32; + return AppLayerResult::incomplete(r.consumed + consumed, r.needed); + } else { + return r; + } + } else { + return AppLayerResult::err(); + } + } + } + } + } + } + + /// parse buffer captures from server to client + fn parse_tc(&mut self, input: &[u8]) -> AppLayerResult { + // no need to process input buffer + if self.bypass_parsing { + return AppLayerResult::ok(); + } + let mut available = input; + + loop { + if available.is_empty() { + return AppLayerResult::ok(); + } + if self.tls_parsing { + match parse_tls_plaintext(available) { + Ok((remainder, tls)) => { + // bytes available for further parsing are what remain + available = remainder; + for message in &tls.msg { + #[allow(clippy::single_match)] + match message { + TlsMessage::Handshake(TlsMessageHandshake::Certificate( + contents, + )) => { + let mut chain = Vec::new(); + for cert in &contents.cert_chain { + chain.push(CertificateBlob { + data: cert.data.to_vec(), + }); + } + let tx = + self.new_tx(RdpTransactionItem::TlsCertificateChain(chain)); + self.transactions.push_back(tx); + self.bypass_parsing = true; + } + _ => {} + } + } + } + + Err(Err::Incomplete(_)) => { + // nom need not compatible with applayer need, request one more byte + return AppLayerResult::incomplete( + (input.len() - available.len()) as u32, + (available.len() + 1) as u32, + ); + } + + Err(Err::Failure(_)) | Err(Err::Error(_)) => { + return AppLayerResult::err(); + } + } + } else { + // every message should be encapsulated within a T.123 tpkt + match parse_t123_tpkt(available) { + // success + Ok((remainder, t123)) => { + // bytes available for further parsing are what remain + available = remainder; + // evaluate message within the tpkt + match t123.child { + // X.224 connection confirm + T123TpktChild::X224ConnectionConfirm(x224) => { + let tx = + self.new_tx(RdpTransactionItem::X224ConnectionConfirm(x224)); + self.transactions.push_back(tx); + } + + // X.223 data packet, evaluate what it encapsulates + T123TpktChild::Data(x223) => { + #[allow(clippy::single_match)] + match x223.child { + X223DataChild::McsConnectResponse(mcs) => { + let tx = self + .new_tx(RdpTransactionItem::McsConnectResponse(mcs)); + self.transactions.push_back(tx); + self.bypass_parsing = true; + return AppLayerResult::ok(); + } + + // unknown message in X.223, skip + _ => (), + } + } + + // unknown message in T.123, skip + _ => (), + } + } + + Err(Err::Incomplete(_)) => { + // nom need not compatible with applayer need, request one more byte + return AppLayerResult::incomplete( + (input.len() - available.len()) as u32, + (available.len() + 1) as u32, + ); + } + + Err(Err::Failure(_)) | Err(Err::Error(_)) => { + if probe_tls_handshake(available) { + self.tls_parsing = true; + let r = self.parse_tc(available); + if r.status == 1 { + //adds bytes already consumed to incomplete result + let consumed = (input.len() - available.len()) as u32; + return AppLayerResult::incomplete(r.consumed + consumed, r.needed); + } else { + return r; + } + } else { + return AppLayerResult::err(); + } + } + } + } + } + } +} + +#[no_mangle] +pub extern "C" fn rs_rdp_state_new(_orig_state: *mut std::os::raw::c_void, _orig_proto: AppProto) -> *mut std::os::raw::c_void { + let state = RdpState::new(); + let boxed = Box::new(state); + return Box::into_raw(boxed) as *mut _; +} + +#[no_mangle] +pub extern "C" fn rs_rdp_state_free(state: *mut std::os::raw::c_void) { + std::mem::drop(unsafe { Box::from_raw(state as *mut RdpState) }); +} + +#[no_mangle] +pub unsafe extern "C" fn rs_rdp_state_tx_free(state: *mut std::os::raw::c_void, tx_id: u64) { + let state = cast_pointer!(state, RdpState); + state.free_tx(tx_id); +} + +// +// probe +// + +/// probe for T.123 type identifier, as each message is encapsulated in T.123 +fn probe_rdp(input: &[u8]) -> bool { + !input.is_empty() && input[0] == TpktVersion::T123 as u8 +} + +/// probe for T.123 message, whether to client or to server +#[no_mangle] +pub unsafe extern "C" fn rs_rdp_probe_ts_tc( + _flow: *const Flow, _direction: u8, input: *const u8, input_len: u32, _rdir: *mut u8, +) -> AppProto { + if !input.is_null() { + // probe bytes for `rdp` protocol pattern + let slice = build_slice!(input, input_len as usize); + + // Some sessions immediately (first byte) switch to TLS/SSL, e.g. + // https://wiki.wireshark.org/SampleCaptures?action=AttachFile&do=view&target=rdp-ssl.pcap.gz + // but this callback will not be exercised, so `probe_tls_handshake` not needed here. + if probe_rdp(slice) { + return ALPROTO_RDP; + } + } + return ALPROTO_UNKNOWN; +} + +/// probe for TLS +fn probe_tls_handshake(input: &[u8]) -> bool { + !input.is_empty() && input[0] == u8::from(TlsRecordType::Handshake) +} + +// +// parse +// + +#[no_mangle] +pub unsafe extern "C" fn rs_rdp_parse_ts( + _flow: *const Flow, state: *mut std::os::raw::c_void, _pstate: *mut std::os::raw::c_void, + stream_slice: StreamSlice, + _data: *const std::os::raw::c_void +) -> AppLayerResult { + let state = cast_pointer!(state, RdpState); + let buf = stream_slice.as_slice(); + // attempt to parse bytes as `rdp` protocol + return state.parse_ts(buf); +} + +#[no_mangle] +pub unsafe extern "C" fn rs_rdp_parse_tc( + _flow: *const Flow, state: *mut std::os::raw::c_void, _pstate: *mut std::os::raw::c_void, + stream_slice: StreamSlice, + _data: *const std::os::raw::c_void +) -> AppLayerResult { + let state = cast_pointer!(state, RdpState); + let buf = stream_slice.as_slice(); + // attempt to parse bytes as `rdp` protocol + return state.parse_tc(buf); +} + +export_tx_data_get!(rs_rdp_get_tx_data, RdpTransaction); +export_state_data_get!(rs_rdp_get_state_data, RdpState); + +// +// registration +// + +const PARSER_NAME: &[u8] = b"rdp\0"; + +#[no_mangle] +pub unsafe extern "C" fn rs_rdp_register_parser() { + let default_port = std::ffi::CString::new("[3389]").unwrap(); + let parser = RustParser { + name: PARSER_NAME.as_ptr() as *const std::os::raw::c_char, + default_port: default_port.as_ptr(), + ipproto: IPPROTO_TCP, + probe_ts: Some(rs_rdp_probe_ts_tc), + probe_tc: Some(rs_rdp_probe_ts_tc), + min_depth: 0, + max_depth: 16, + state_new: rs_rdp_state_new, + state_free: rs_rdp_state_free, + tx_free: rs_rdp_state_tx_free, + parse_ts: rs_rdp_parse_ts, + parse_tc: rs_rdp_parse_tc, + get_tx_count: rs_rdp_state_get_tx_count, + get_tx: rs_rdp_state_get_tx, + tx_comp_st_ts: 1, + tx_comp_st_tc: 1, + tx_get_progress: rs_rdp_tx_get_progress, + get_eventinfo: None, + get_eventinfo_byid: None, + localstorage_new: None, + localstorage_free: None, + get_tx_files: None, + get_tx_iterator: Some(applayer::state_get_tx_iterator::<RdpState, RdpTransaction>), + get_tx_data: rs_rdp_get_tx_data, + get_state_data: rs_rdp_get_state_data, + apply_tx_config: None, + flags: 0, + truncate: None, + get_frame_id_by_name: None, + get_frame_name_by_id: None, + }; + + let ip_proto_str = std::ffi::CString::new("tcp").unwrap(); + + if AppLayerProtoDetectConfProtoDetectionEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let alproto = AppLayerRegisterProtocolDetection(&parser, 1); + ALPROTO_RDP = alproto; + if AppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let _ = AppLayerRegisterParser(&parser, alproto); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::rdp::parser::{RdpCookie, X224ConnectionRequest}; + + #[test] + fn test_probe_rdp() { + let buf: &[u8] = &[0x03, 0x00]; + assert!(probe_rdp(buf)); + } + + #[test] + fn test_probe_rdp_other() { + let buf: &[u8] = &[0x04, 0x00]; + assert!(!probe_rdp(buf)); + } + + #[test] + fn test_probe_tls_handshake() { + let buf: &[u8] = &[0x16, 0x00]; + assert!(probe_tls_handshake(buf)); + } + + #[test] + fn test_probe_tls_handshake_other() { + let buf: &[u8] = &[0x17, 0x00]; + assert!(!probe_tls_handshake(buf)); + } + + #[test] + fn test_parse_ts_rdp() { + let buf_1: &[u8] = &[0x03, 0x00, 0x00, 0x25, 0x20, 0xe0, 0x00, 0x00]; + let buf_2: &[u8] = &[ + 0x03, 0x00, 0x00, 0x25, 0x20, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x43, 0x6f, 0x6f, + 0x6b, 0x69, 0x65, 0x3a, 0x20, 0x6d, 0x73, 0x74, 0x73, 0x68, 0x61, 0x73, 0x68, 0x3d, + 0x75, 0x73, 0x65, 0x72, 0x31, 0x32, 0x33, 0x0d, 0x0a, + ]; + let mut state = RdpState::new(); + // will consume 0, request length + 1 + assert_eq!(AppLayerResult::incomplete(0, 9), state.parse_ts(buf_1)); + assert_eq!(0, state.transactions.len()); + // exactly aligns with transaction + assert_eq!(AppLayerResult::ok(), state.parse_ts(buf_2)); + assert_eq!(1, state.transactions.len()); + let item = RdpTransactionItem::X224ConnectionRequest(X224ConnectionRequest { + cdt: 0, + dst_ref: 0, + src_ref: 0, + class: 0, + options: 0, + cookie: Some(RdpCookie { + mstshash: String::from("user123"), + }), + negotiation_request: None, + data: Vec::new(), + }); + assert_eq!(item, state.transactions[0].item); + } + + #[test] + fn test_parse_ts_other() { + let buf: &[u8] = &[0x03, 0x00, 0x00, 0x01, 0x00]; + let mut state = RdpState::new(); + assert_eq!(AppLayerResult::err(), state.parse_ts(buf)); + } + + #[test] + fn test_parse_tc_rdp() { + let buf_1: &[u8] = &[0x03, 0x00, 0x00, 0x09, 0x02]; + let buf_2: &[u8] = &[0x03, 0x00, 0x00, 0x09, 0x02, 0xf0, 0x80, 0x7f, 0x66]; + let mut state = RdpState::new(); + // will consume 0, request length + 1 + assert_eq!(AppLayerResult::incomplete(0, 6), state.parse_tc(buf_1)); + assert_eq!(0, state.transactions.len()); + // exactly aligns with transaction + assert_eq!(AppLayerResult::ok(), state.parse_tc(buf_2)); + assert_eq!(1, state.transactions.len()); + let item = RdpTransactionItem::McsConnectResponse(McsConnectResponse {}); + assert_eq!(item, state.transactions[0].item); + } + + #[test] + fn test_parse_tc_other() { + let buf: &[u8] = &[0x03, 0x00, 0x00, 0x01, 0x00]; + let mut state = RdpState::new(); + assert_eq!(AppLayerResult::err(), state.parse_tc(buf)); + } + + #[test] + fn test_state_new_tx() { + let mut state = RdpState::new(); + let item0 = RdpTransactionItem::McsConnectRequest(McsConnectRequest { + children: Vec::new(), + }); + let item1 = RdpTransactionItem::McsConnectRequest(McsConnectRequest { + children: Vec::new(), + }); + let tx0 = state.new_tx(item0); + let tx1 = state.new_tx(item1); + assert_eq!(2, state.next_id); + state.transactions.push_back(tx0); + state.transactions.push_back(tx1); + assert_eq!(2, state.transactions.len()); + assert_eq!(1, state.transactions[0].id); + assert_eq!(2, state.transactions[1].id); + assert!(!state.tls_parsing); + assert!(!state.bypass_parsing); + } + + #[test] + fn test_state_get_tx() { + let mut state = RdpState::new(); + let item0 = RdpTransactionItem::McsConnectRequest(McsConnectRequest { + children: Vec::new(), + }); + let item1 = RdpTransactionItem::McsConnectRequest(McsConnectRequest { + children: Vec::new(), + }); + let item2 = RdpTransactionItem::McsConnectRequest(McsConnectRequest { + children: Vec::new(), + }); + let tx0 = state.new_tx(item0); + let tx1 = state.new_tx(item1); + let tx2 = state.new_tx(item2); + state.transactions.push_back(tx0); + state.transactions.push_back(tx1); + state.transactions.push_back(tx2); + assert_eq!(Some(&state.transactions[1]), state.get_tx(2)); + } + + #[test] + fn test_state_free_tx() { + let mut state = RdpState::new(); + let item0 = RdpTransactionItem::McsConnectRequest(McsConnectRequest { + children: Vec::new(), + }); + let item1 = RdpTransactionItem::McsConnectRequest(McsConnectRequest { + children: Vec::new(), + }); + let item2 = RdpTransactionItem::McsConnectRequest(McsConnectRequest { + children: Vec::new(), + }); + let tx0 = state.new_tx(item0); + let tx1 = state.new_tx(item1); + let tx2 = state.new_tx(item2); + state.transactions.push_back(tx0); + state.transactions.push_back(tx1); + state.transactions.push_back(tx2); + state.free_tx(1); + assert_eq!(3, state.next_id); + assert_eq!(2, state.transactions.len()); + assert_eq!(2, state.transactions[0].id); + assert_eq!(3, state.transactions[1].id); + assert_eq!(None, state.get_tx(1)); + } +} diff --git a/rust/src/rdp/util.rs b/rust/src/rdp/util.rs new file mode 100644 index 0000000..a4228f2 --- /dev/null +++ b/rust/src/rdp/util.rs @@ -0,0 +1,175 @@ +/* Copyright (C) 2019 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. + */ + +// Author: Zach Kelly <zach.kelly@lmco.com> + +use crate::rdp::error::RdpError; +use byteorder::ReadBytesExt; +use memchr::memchr; +use nom7::{Err, IResult, Needed}; +use std::io::Cursor; +use widestring::U16CString; + +/// converts a raw u8 slice of little-endian wide chars into a String +pub fn le_slice_to_string(input: &[u8]) -> Result<String, Box<dyn std::error::Error>> { + let mut vec = Vec::new(); + let mut cursor = Cursor::new(input); + while let Ok(x) = cursor.read_u16::<byteorder::LittleEndian>() { + if x == 0 { + break; + } + vec.push(x); + } + match U16CString::new(vec) { + Ok(x) => match x.to_string() { + Ok(x) => Ok(x), + Err(e) => Err(e.into()), + }, + Err(e) => Err(e.into()), + } +} + +/// converts a raw u8 slice of null-padded utf7 chars into a String, dropping the nulls +pub fn utf7_slice_to_string(input: &[u8]) -> Result<String, Box<dyn std::error::Error>> { + let s = match memchr(b'\0', input) { + Some(end) => &input[..end], + None => input, + }; + match std::str::from_utf8(s) { + Ok(s) => Ok(String::from(s)), + Err(e) => Err(e.into()), + } +} + +/// parses a PER length determinant, to determine the length of the data following +/// x.691-spec: section 10.9 +pub fn parse_per_length_determinant(input: &[u8]) -> IResult<&[u8], u32, RdpError> { + if input.is_empty() { + // need a single byte to begin length determination + Err(Err::Incomplete(Needed::new(1))) + } else { + let bit7 = input[0] >> 7; + match bit7 { + 0b0 => { + // byte starts with 0b0. Length stored in the lower 7 bits of the current byte + let length = input[0] as u32 & 0x7f; + Ok((&input[1..], length)) + } + _ => { + let bit6 = input[0] >> 6 & 0x1; + match bit6 { + 0b0 => { + // byte starts with 0b10. Length stored in the remaining 6 bits and the next byte + if input.len() < 2 { + Err(Err::Incomplete(Needed::new(2))) + } else { + let length = ((input[0] as u32 & 0x3f) << 8) | input[1] as u32; + Ok((&input[2..], length)) + } + } + _ => { + // byte starts with 0b11. Without an example to confirm 16K+ lengths are properly + // handled, leaving this branch unimplemented + Err(Err::Error(RdpError::UnimplementedLengthDeterminant)) + } + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::rdp::error::RdpError; + use nom7::Needed; + + #[test] + fn test_le_string_abc() { + let abc = &[0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00]; + assert_eq!(String::from("ABC"), le_slice_to_string(abc).unwrap()); + } + + #[test] + fn test_le_string_empty() { + let empty = &[]; + assert_eq!(String::from(""), le_slice_to_string(empty).unwrap()); + } + + #[test] + fn test_le_string_invalid() { + let not_utf16le = &[0x00, 0xd8, 0x01, 0x00]; + assert!(le_slice_to_string(not_utf16le).is_err()); + } + + #[test] + fn test_utf7_string_abc() { + let abc = &[0x41, 0x42, 0x43, 0x00, 0x00]; + assert_eq!(String::from("ABC"), utf7_slice_to_string(abc).unwrap()); + } + + #[test] + fn test_utf7_string_empty() { + let empty = &[]; + assert_eq!(String::from(""), utf7_slice_to_string(empty).unwrap()); + } + + #[test] + fn test_utf7_string_invalid() { + let not_utf7 = &[0x80]; + assert!(utf7_slice_to_string(not_utf7).is_err()); + } + + #[test] + fn test_length_single_length() { + let bytes = &[0x28]; + assert_eq!(Ok((&[][..], 0x28)), parse_per_length_determinant(bytes)); + } + + #[test] + fn test_length_double_length() { + let bytes = &[0x81, 0x28]; + assert_eq!(Ok((&[][..], 0x128)), parse_per_length_determinant(bytes)); + } + + #[test] + fn test_length_single_length_incomplete() { + let bytes = &[]; + assert_eq!( + Err(Err::Incomplete(Needed::new(1))), + parse_per_length_determinant(bytes) + ) + } + + #[test] + fn test_length_16k_unimplemented() { + let bytes = &[0xc0]; + assert_eq!( + Err(Err::Error(RdpError::UnimplementedLengthDeterminant)), + parse_per_length_determinant(bytes) + ) + } + + #[test] + fn test_length_double_length_incomplete() { + let bytes = &[0x81]; + assert_eq!( + Err(Err::Incomplete(Needed::new(2))), + parse_per_length_determinant(bytes) + ) + } +} diff --git a/rust/src/rdp/windows.rs b/rust/src/rdp/windows.rs new file mode 100644 index 0000000..29ee4b2 --- /dev/null +++ b/rust/src/rdp/windows.rs @@ -0,0 +1,660 @@ +/* Copyright (C) 2019 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. + */ + +// Author: Zach Kelly <zach.kelly@lmco.com> + +/// converts a locale identifier into a locale name +/// <https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f> +pub fn lcid_to_string(lcid: u32, default: &str) -> String { + let s = match lcid { + 0x0001 => "ar", + 0x0002 => "bg", + 0x0003 => "ca", + 0x0004 => "zh-Hans", + 0x0005 => "cs", + 0x0006 => "da", + 0x0007 => "de", + 0x0008 => "el", + 0x0009 => "en", + 0x000A => "es", + 0x000B => "fi", + 0x000C => "fr", + 0x000D => "he", + 0x000E => "hu", + 0x000F => "is", + 0x0010 => "it", + 0x0011 => "ja", + 0x0012 => "ko", + 0x0013 => "nl", + 0x0014 => "no", + 0x0015 => "pl", + 0x0016 => "pt", + 0x0017 => "rm", + 0x0018 => "ro", + 0x0019 => "ru", + 0x001A => "hr", + 0x001B => "sk", + 0x001C => "sq", + 0x001D => "sv", + 0x001E => "th", + 0x001F => "tr", + 0x0020 => "ur", + 0x0021 => "id", + 0x0022 => "uk", + 0x0023 => "be", + 0x0024 => "sl", + 0x0025 => "et", + 0x0026 => "lv", + 0x0027 => "lt", + 0x0028 => "tg", + 0x0029 => "fa", + 0x002A => "vi", + 0x002B => "hy", + 0x002C => "az", + 0x002D => "eu", + 0x002E => "hsb", + 0x002F => "mk", + 0x0030 => "st", + 0x0031 => "ts", + 0x0032 => "tn", + 0x0033 => "ve", + 0x0034 => "xh", + 0x0035 => "zu", + 0x0036 => "af", + 0x0037 => "ka", + 0x0038 => "fo", + 0x0039 => "hi", + 0x003A => "mt", + 0x003B => "se", + 0x003C => "ga", + 0x003D => "yi", + 0x003E => "ms", + 0x003F => "kk", + 0x0040 => "ky", + 0x0041 => "sw", + 0x0042 => "tk", + 0x0043 => "uz", + 0x0044 => "tt", + 0x0045 => "bn", + 0x0046 => "pa", + 0x0047 => "gu", + 0x0048 => "or", + 0x0049 => "ta", + 0x004A => "te", + 0x004B => "kn", + 0x004C => "ml", + 0x004D => "as", + 0x004E => "mr", + 0x004F => "sa", + 0x0050 => "mn", + 0x0051 => "bo", + 0x0052 => "cy", + 0x0053 => "km", + 0x0054 => "lo", + 0x0055 => "my", + 0x0056 => "gl", + 0x0057 => "kok", + 0x0058 => "mni", + 0x0059 => "sd", + 0x005A => "syr", + 0x005B => "si", + 0x005C => "chr", + 0x005D => "iu", + 0x005E => "am", + 0x005F => "tzm", + 0x0060 => "ks", + 0x0061 => "ne", + 0x0062 => "fy", + 0x0063 => "ps", + 0x0064 => "fil", + 0x0065 => "dv", + 0x0066 => "bin", + 0x0067 => "ff", + 0x0068 => "ha", + 0x0069 => "ibb", + 0x006A => "yo", + 0x006B => "quz", + 0x006C => "nso", + 0x006D => "ba", + 0x006E => "lb", + 0x006F => "kl", + 0x0070 => "ig", + 0x0071 => "kr", + 0x0072 => "om", + 0x0073 => "ti", + 0x0074 => "gn", + 0x0075 => "haw", + 0x0076 => "la", + 0x0077 => "so", + 0x0078 => "ii", + 0x0079 => "pap", + 0x007A => "arn", + 0x007C => "moh", + 0x007E => "br", + 0x0080 => "ug", + 0x0081 => "mi", + 0x0082 => "oc", + 0x0083 => "co", + 0x0084 => "gsw", + 0x0085 => "sah", + 0x0086 => "qut", + 0x0087 => "rw", + 0x0088 => "wo", + 0x008C => "prs", + 0x0091 => "gd", + 0x0092 => "ku", + 0x0093 => "quc", + 0x0401 => "ar-SA", + 0x0402 => "bg-BG", + 0x0403 => "ca-ES", + 0x0404 => "zh-TW", + 0x0405 => "cs-CZ", + 0x0406 => "da-DK", + 0x0407 => "de-DE", + 0x0408 => "el-GR", + 0x0409 => "en-US", + 0x040A => "es-ES_tradnl", + 0x040B => "fi-FI", + 0x040C => "fr-FR", + 0x040D => "he-IL", + 0x040E => "hu-HU", + 0x040F => "is-IS", + 0x0410 => "it-IT", + 0x0411 => "ja-JP", + 0x0412 => "ko-KR", + 0x0413 => "nl-NL", + 0x0414 => "nb-NO", + 0x0415 => "pl-PL", + 0x0416 => "pt-BR", + 0x0417 => "rm-CH", + 0x0418 => "ro-RO", + 0x0419 => "ru-RU", + 0x041A => "hr-HR", + 0x041B => "sk-SK", + 0x041C => "sq-AL", + 0x041D => "sv-SE", + 0x041E => "th-TH", + 0x041F => "tr-TR", + 0x0420 => "ur-PK", + 0x0421 => "id-ID", + 0x0422 => "uk-UA", + 0x0423 => "be-BY", + 0x0424 => "sl-SI", + 0x0425 => "et-EE", + 0x0426 => "lv-LV", + 0x0427 => "lt-LT", + 0x0428 => "tg-Cyrl-TJ", + 0x0429 => "fa-IR", + 0x042A => "vi-VN", + 0x042B => "hy-AM", + 0x042C => "az-Latn-AZ", + 0x042D => "eu-ES", + 0x042E => "hsb-DE", + 0x042F => "mk-MK", + 0x0430 => "st-ZA", + 0x0431 => "ts-ZA", + 0x0432 => "tn-ZA", + 0x0433 => "ve-ZA", + 0x0434 => "xh-ZA", + 0x0435 => "zu-ZA", + 0x0436 => "af-ZA", + 0x0437 => "ka-GE", + 0x0438 => "fo-FO", + 0x0439 => "hi-IN", + 0x043A => "mt-MT", + 0x043B => "se-NO", + 0x043D => "yi-Hebr", + 0x043E => "ms-MY", + 0x043F => "kk-KZ", + 0x0440 => "ky-KG", + 0x0441 => "sw-KE", + 0x0442 => "tk-TM", + 0x0443 => "uz-Latn-UZ", + 0x0444 => "tt-RU", + 0x0445 => "bn-IN", + 0x0446 => "pa-IN", + 0x0447 => "gu-IN", + 0x0448 => "or-IN", + 0x0449 => "ta-IN", + 0x044A => "te-IN", + 0x044B => "kn-IN", + 0x044C => "ml-IN", + 0x044D => "as-IN", + 0x044E => "mr-IN", + 0x044F => "sa-IN", + 0x0450 => "mn-MN", + 0x0451 => "bo-CN", + 0x0452 => "cy-GB", + 0x0453 => "km-KH", + 0x0454 => "lo-LA", + 0x0455 => "my-MM", + 0x0456 => "gl-ES", + 0x0457 => "kok-IN", + 0x0458 => "mni-IN", + 0x0459 => "sd-Deva-IN", + 0x045A => "syr-SY", + 0x045B => "si-LK", + 0x045C => "chr-Cher-US", + 0x045D => "iu-Cans-CA", + 0x045E => "am-ET", + 0x045F => "tzm-Arab-MA", + 0x0460 => "ks-Arab", + 0x0461 => "ne-NP", + 0x0462 => "fy-NL", + 0x0463 => "ps-AF", + 0x0464 => "fil-PH", + 0x0465 => "dv-MV", + 0x0466 => "bin-NG", + 0x0467 => "fuv-NG", + 0x0468 => "ha-Latn-NG", + 0x0469 => "ibb-NG", + 0x046A => "yo-NG", + 0x046B => "quz-BO", + 0x046C => "nso-ZA", + 0x046D => "ba-RU", + 0x046E => "lb-LU", + 0x046F => "kl-GL", + 0x0470 => "ig-NG", + 0x0471 => "kr-NG", + 0x0472 => "om-ET", + 0x0473 => "ti-ET", + 0x0474 => "gn-PY", + 0x0475 => "haw-US", + 0x0476 => "la-Latn", + 0x0477 => "so-SO", + 0x0478 => "ii-CN", + 0x0479 => "pap-029", + 0x047A => "arn-CL", + 0x047C => "moh-CA", + 0x047E => "br-FR", + 0x0480 => "ug-CN", + 0x0481 => "mi-NZ", + 0x0482 => "oc-FR", + 0x0483 => "co-FR", + 0x0484 => "gsw-FR", + 0x0485 => "sah-RU", + 0x0486 => "qut-GT", + 0x0487 => "rw-RW", + 0x0488 => "wo-SN", + 0x048C => "prs-AF", + 0x048D => "plt-MG", + 0x048E => "zh-yue-HK", + 0x048F => "tdd-Tale-CN", + 0x0490 => "khb-Talu-CN", + 0x0491 => "gd-GB", + 0x0492 => "ku-Arab-IQ", + 0x0493 => "quc-CO", + 0x0501 => "qps-ploc", + 0x05FE => "qps-ploca", + 0x0801 => "ar-IQ", + 0x0803 => "ca-ES-valencia", + 0x0804 => "zh-CN", + 0x0807 => "de-CH", + 0x0809 => "en-GB", + 0x080A => "es-MX", + 0x080C => "fr-BE", + 0x0810 => "it-CH", + 0x0811 => "ja-Ploc-JP", + 0x0813 => "nl-BE", + 0x0814 => "nn-NO", + 0x0816 => "pt-PT", + 0x0818 => "ro-MD", + 0x0819 => "ru-MD", + 0x081A => "sr-Latn-CS", + 0x081D => "sv-FI", + 0x0820 => "ur-IN", + 0x082C => "az-Cyrl-AZ", + 0x082E => "dsb-DE", + 0x0832 => "tn-BW", + 0x083B => "se-SE", + 0x083C => "ga-IE", + 0x083E => "ms-BN", + 0x0843 => "uz-Cyrl-UZ", + 0x0845 => "bn-BD", + 0x0846 => "pa-Arab-PK", + 0x0849 => "ta-LK", + 0x0850 => "mn-Mong-CN", + 0x0851 => "bo-BT", + 0x0859 => "sd-Arab-PK", + 0x085D => "iu-Latn-CA", + 0x085F => "tzm-Latn-DZ", + 0x0860 => "ks-Deva", + 0x0861 => "ne-IN", + 0x0867 => "ff-Latn-SN", + 0x086B => "quz-EC", + 0x0873 => "ti-ER", + 0x09FF => "qps-plocm", + 0x0C01 => "ar-EG", + 0x0C04 => "zh-HK", + 0x0C07 => "de-AT", + 0x0C09 => "en-AU", + 0x0C0A => "es-ES", + 0x0C0C => "fr-CA", + 0x0C1A => "sr-Cyrl-CS", + 0x0C3B => "se-FI", + 0x0C50 => "mn-Mong-MN", + 0x0C51 => "dz-BT", + 0x0C5F => "tmz-MA", + 0x0C6B => "quz-PE", + 0x1001 => "ar-LY", + 0x1004 => "zh-SG", + 0x1007 => "de-LU", + 0x1009 => "en-CA", + 0x100A => "es-GT", + 0x100C => "fr-CH", + 0x101A => "hr-BA", + 0x103B => "smj-NO", + 0x105F => "tzm-Tfng-MA", + 0x1401 => "ar-DZ", + 0x1404 => "zh-MO", + 0x1407 => "de-LI", + 0x1409 => "en-NZ", + 0x140A => "es-CR", + 0x140C => "fr-LU", + 0x141A => "bs-Latn-BA", + 0x143B => "smj-SE", + 0x1801 => "ar-MA", + 0x1809 => "en-IE", + 0x180A => "es-PA", + 0x180C => "fr-MC", + 0x181A => "sr-Latn-BA", + 0x183B => "sma-NO", + 0x1C01 => "ar-TN", + 0x1C09 => "en-ZA", + 0x1C0A => "es-DO", + 0x1C1A => "sr-Cyrl-BA", + 0x1C3B => "sma-SE", + 0x2001 => "ar-OM", + 0x2009 => "en-JM", + 0x200A => "es-VE", + 0x200C => "fr-RE", + 0x201A => "bs-Cyrl-BA", + 0x203B => "sms-FI", + 0x2401 => "ar-YE", + 0x2409 => "en-029", + 0x240A => "es-CO", + 0x240C => "fr-CD", + 0x241A => "sr-Latn-RS", + 0x243B => "smn-FI", + 0x2801 => "ar-SY", + 0x2809 => "en-BZ", + 0x280A => "es-PE", + 0x280C => "fr-SN", + 0x281A => "sr-Cyrl-RS", + 0x2C01 => "ar-JO", + 0x2C09 => "en-TT", + 0x2C0A => "es-AR", + 0x2C0C => "fr-CM", + 0x2C1A => "sr-Latn-ME", + 0x3001 => "ar-LB", + 0x3009 => "en-ZW", + 0x300A => "es-EC", + 0x300C => "fr-CI", + 0x301A => "sr-Cyrl-ME", + 0x3401 => "ar-KW", + 0x3409 => "en-PH", + 0x340A => "es-CL", + 0x340C => "fr-ML", + 0x3801 => "ar-AE", + 0x3809 => "en-ID", + 0x380A => "es-UY", + 0x380C => "fr-MA", + 0x3c01 => "ar-BH", + 0x3c09 => "en-HK", + 0x3C0A => "es-PY", + 0x3C0C => "fr-HT", + 0x4001 => "ar-QA", + 0x4009 => "en-IN", + 0x400A => "es-BO", + 0x4401 => "ar-Ploc-SA", + 0x4409 => "en-MY", + 0x440A => "es-SV", + 0x4801 => "ar-145", + 0x4809 => "en-SG", + 0x480A => "es-HN", + 0x4C09 => "en-AE", + 0x4C0A => "es-NI", + 0x5009 => "en-BH", + 0x500A => "es-PR", + 0x5409 => "en-EG", + 0x540A => "es-US", + 0x5809 => "en-JO", + 0x580A => "es-419", + 0x5C09 => "en-KW", + 0x5C0A => "es-CU", + 0x6009 => "en-TR", + 0x6409 => "en-YE", + 0x641A => "bs-Cyrl", + 0x681A => "bs-Latn", + 0x6C1A => "sr-Cyrl", + 0x701A => "sr-Latn", + 0x703B => "smn", + 0x742C => "az-Cyrl", + 0x743B => "sms", + 0x7804 => "zh", + 0x7814 => "nn", + 0x781A => "bs", + 0x782C => "az-Latn", + 0x783B => "sma", + 0x7843 => "uz-Cyrl", + 0x7850 => "mn-Cyrl", + 0x785D => "iu-Cans", + 0x785F => "tzm-Tfng", + 0x7C04 => "zh-Hant", + 0x7C14 => "nb", + 0x7C1A => "sr", + 0x7C28 => "tg-Cyrl", + 0x7C2E => "dsb", + 0x7C3B => "smj", + 0x7C43 => "uz-Latn", + 0x7C46 => "pa-Arab", + 0x7C50 => "mn-Mong", + 0x7C59 => "sd-Arab", + 0x7C5C => "chr-Cher", + 0x7C5D => "iu-Latn", + 0x7C5F => "tzm-Latn", + 0x7C67 => "ff-Latn", + 0x7C68 => "ha-Latn", + 0x7C92 => "ku-Arab", + _ => default, + }; + String::from(s) +} + +/// Windows operating system type (build and suffix/pack) +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct OperatingSystem { + pub build: Build, + pub suffix: Suffix, +} + +// <https://en.wikipedia.org/wiki/Windows_NT#Releases> +#[derive(Clone, Debug, FromPrimitive, PartialEq, Eq)] +#[allow(non_camel_case_types)] +pub enum Build { + Other, + Win31 = 528, + Win35 = 807, + Win351 = 1057, + Win40 = 1381, + Win2000 = 2195, + WinXP = 2600, + Vista_6000 = 6000, + Vista_6001 = 6001, + Vista_6002 = 6002, + Win7_7600 = 7600, + Win7_7601 = 7601, + Win8 = 9200, + Win81 = 9600, + Win10_10240 = 10240, + Win10_10586 = 10586, + Win10_14393 = 14393, + Win10_15063 = 15063, + Win10_16299 = 16299, + Win10_17134 = 17134, + Win10_17763 = 17763, + Server2003 = 3790, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Suffix { + Empty, + Rtm, + Sp1, + Sp2, + Th1, + Th2, + Rs1, + Rs2, + Rs3, + Rs4, + Rs5, +} + +/// convert a build number into an OperatingSystem type +pub fn build_number_to_os(number: u32) -> OperatingSystem { + let build = match num::FromPrimitive::from_u32(number) { + Some(x) => x, + None => Build::Other, + }; + let suffix = match number { + 6000 => Suffix::Rtm, + 7600 => Suffix::Rtm, + 6001 => Suffix::Sp1, + 6002 => Suffix::Sp2, + 7601 => Suffix::Sp1, + 10240 => Suffix::Th1, + 10586 => Suffix::Th2, + 14393 => Suffix::Rs1, + 15063 => Suffix::Rs2, + 16299 => Suffix::Rs3, + 17134 => Suffix::Rs4, + 17763 => Suffix::Rs5, + _ => Suffix::Empty, + }; + OperatingSystem { build, suffix } +} + +/// convert an OperatingSystem into a string description +pub fn os_to_string(os: &OperatingSystem, default: &str) -> String { + let s = match os.build { + Build::Win31 => "Windows NT 3.1", + Build::Win35 => "Windows NT 3.5", + Build::Win351 => "Windows NT 3.51", + Build::Win40 => "Windows NT 4.0", + Build::Win2000 => "Windows 2000", + Build::WinXP => "Windows XP", + Build::Vista_6000 => "Windows Vista", + Build::Vista_6001 => "Windows Vista", + Build::Vista_6002 => "Windows Vista", + Build::Win7_7600 => "Windows 7", + Build::Win7_7601 => "Windows 7", + Build::Win8 => "Windows 8", + Build::Win81 => "Windows 8.1", + Build::Win10_10240 => "Windows 10", + Build::Win10_10586 => "Windows 10", + Build::Win10_14393 => "Windows 10", + Build::Win10_15063 => "Windows 10", + Build::Win10_16299 => "Windows 10", + Build::Win10_17134 => "Windows 10", + Build::Win10_17763 => "Windows 10", + Build::Server2003 => "Windows Server 2003", + Build::Other => default, + }; + let mut result = String::from(s); + match os.suffix { + Suffix::Rtm => result.push_str(" RTM"), + Suffix::Sp1 => result.push_str(" SP1"), + Suffix::Sp2 => result.push_str(" SP2"), + Suffix::Th1 => result.push_str(" TH1"), + Suffix::Th2 => result.push_str(" TH2"), + Suffix::Rs1 => result.push_str(" RS1"), + Suffix::Rs2 => result.push_str(" RS2"), + Suffix::Rs3 => result.push_str(" RS3"), + Suffix::Rs4 => result.push_str(" RS4"), + Suffix::Rs5 => result.push_str(" RS5"), + Suffix::Empty => (), + }; + result +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_lcid_string_en() { + let default = "default-lcid-name"; + assert_eq!("en-US", lcid_to_string(0x409, default)); + } + + #[test] + fn test_lcid_string_default() { + let default = "default-lcid-name"; + assert_eq!(default, lcid_to_string(0xffff, default)); + } + + #[test] + fn test_build_os_win10() { + let w10_rs5 = OperatingSystem { + build: Build::Win10_17763, + suffix: Suffix::Rs5, + }; + assert_eq!(w10_rs5, build_number_to_os(17763)); + } + + #[test] + fn test_build_os_other() { + let other = OperatingSystem { + build: Build::Other, + suffix: Suffix::Empty, + }; + assert_eq!(other, build_number_to_os(1)); + } + + #[test] + fn test_os_string_win7_sp1() { + let w7_sp1 = "Windows 7 SP1"; + let default = "default-os-name"; + let w7_os = OperatingSystem { + build: Build::Win7_7601, + suffix: Suffix::Sp1, + }; + assert_eq!(w7_sp1, os_to_string(&w7_os, default)); + } + + #[test] + fn test_os_string_win81() { + let w81 = "Windows 8.1"; + let default = "default-os-name"; + let w81_os = OperatingSystem { + build: Build::Win81, + suffix: Suffix::Empty, + }; + assert_eq!(w81, os_to_string(&w81_os, default)); + } + + #[test] + fn test_os_string_default() { + let default = "default-os-name"; + let other_os = OperatingSystem { + build: Build::Other, + suffix: Suffix::Empty, + }; + assert_eq!(default, os_to_string(&other_os, default)); + } +} |