summaryrefslogtreecommitdiffstats
path: root/rust/src/rdp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 17:39:49 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 17:39:49 +0000
commita0aa2307322cd47bbf416810ac0292925e03be87 (patch)
tree37076262a026c4b48c8a0e84f44ff9187556ca35 /rust/src/rdp
parentInitial commit. (diff)
downloadsuricata-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.rs49
-rw-r--r--rust/src/rdp/log.rs597
-rw-r--r--rust/src/rdp/mod.rs27
-rw-r--r--rust/src/rdp/parser.rs1431
-rw-r--r--rust/src/rdp/rdp.rs667
-rw-r--r--rust/src/rdp/util.rs175
-rw-r--r--rust/src/rdp/windows.rs660
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));
+ }
+}