]> git.ipfire.org Git - thirdparty/suricata.git/commitdiff
protocol parser: rdp
authorZach Kelly <zach.kelly@lmco.com>
Thu, 5 Sep 2019 17:46:14 +0000 (13:46 -0400)
committerVictor Julien <victor@inliniac.net>
Mon, 9 Sep 2019 20:00:29 +0000 (22:00 +0200)
Initial implementation of feature 2314:
1. Add protocol parser for RDP
2. Add transactions for RDP negotiation
3. Add eve logging of transactions

22 files changed:
doc/userguide/output/eve/eve-json-format.rst
rust/Cargo.toml.in
rust/src/lib.rs
rust/src/rdp/error.rs [new file with mode: 0644]
rust/src/rdp/log.rs [new file with mode: 0644]
rust/src/rdp/mod.rs [new file with mode: 0644]
rust/src/rdp/parser.rs [new file with mode: 0644]
rust/src/rdp/rdp.rs [new file with mode: 0644]
rust/src/rdp/util.rs [new file with mode: 0644]
rust/src/rdp/windows.rs [new file with mode: 0644]
src/Makefile.am
src/app-layer-parser.c
src/app-layer-protos.c
src/app-layer-protos.h
src/app-layer-rdp.c [new file with mode: 0644]
src/app-layer-rdp.h [new file with mode: 0644]
src/output-json-rdp.c [new file with mode: 0644]
src/output-json-rdp.h [new file with mode: 0644]
src/output.c
src/suricata-common.h
src/util-profiling.c
suricata.yaml.in

index 239c573843e80b7101f4b1c1700fd39e6131802d..4bd7b8c81d688890591de1b72b64b40a140ed6c9 100644 (file)
@@ -1032,3 +1032,190 @@ Example ::
     "reason": "timeout",
     "alerted": false
   }
+
+Event type: RDP
+---------------
+
+Initial negotiations between RDP client and server are stored as transactions and logged.
+
+Each RDP record contains a per-flow incrementing "tx_id" field.
+
+The "event_type" field indicates an RDP event subtype. Possible values:
+
+* "initial_request"
+* "initial_response"
+* "connect_request"
+* "connect_response"
+* "tls_handshake"
+
+RDP type: Initial Request
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The optional "cookie" field is a string identifier the RDP client has chosen to provide.
+
+The optional "flags" field is a list of client directives. Possible values:
+
+* "restricted_admin_mode_required"
+* "redirected_authentication_mode_required"
+* "correlation_info_present"
+
+RDP type: Initial Response
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In the event of a standard initial response:
+
+The "protocol" field is the selected protocol. Possible values:
+
+* "rdp"
+* "ssl"
+* "hybrid"
+* "rds_tls"
+* "hybrid_ex"
+
+The optional "flags" field is a list of support server modes. Possible values:
+
+* "extended_client_data"
+* "dynvc_gfx"
+* "restricted_admin"
+* "redirected_authentication"
+
+Alternatively, in the event of an error-indicating initial response:
+
+There will be no "protocol" or "flags" fields.
+
+The "error_code" field will contain the numeric code provided by the RDP server.
+
+The "reason" field will contain a text summary of this code. Possible values:
+
+* "ssl required by server" (error code 0x1)
+* "ssl not allowed by server" (error code 0x2)
+* "ssl cert not on server" (error code 0x3)
+* "inconsistent flags" (error code 0x4)
+* "hybrid required by server" (error code 0x5)
+* "ssl with user auth required by server" (error code 0x6)
+
+RDP type: Connect Request
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The optional "channel" field is a list of requested data channel names.
+
+Common channels:
+
+* "rdpdr" (device redirection)
+* "cliprdr" (shared clipboard)
+* "rdpsnd" (sound)
+
+The optional "client" field is a sub-object that may contain the following:
+
+* "version": RDP protocol version. Possible values are "v4", "v5", "v10.0", "v10.1", "v10.2", "v10.3", "v10.4", "v10.5", "v10.6", "v10.7", "unknown".
+* "desktop_width": Numeric desktop width value.
+* "desktop_height": Numeric desktop height value.
+* "color_depth": Numeric color depth. Possible values are 4, 8, 15, 16, 24.
+* "keyboard_layout": Locale identifier name, e.g., "en-US".
+* "build": OS and SP level, e.g., "Windows XP", "Windows 7 SP1".
+* "client_name": Client computer name.
+* "keyboard_type": Possible values are "xt", "ico", "at", "enhanced", "1050", "9140", "jp".
+* "keyboard_subtype": Numeric code for keyboard.
+* "function_keys": Number of function keys on client keyboard.
+* "ime": Input method editor (IME) file name.
+* "product_id": Product id string.
+* "serial_number": Numeric value.
+* "capabilities": List of any of the following: "support_errinfo_pdf", "want_32bpp_session", "support_statusinfo_pdu", "strong_asymmetric_keys", "valid_connection_type", "support_monitor_layout_pdu", "support_netchar_autodetect", "support_dynvc_gfx_protocol", "support_dynamic_time_zone", "support_heartbeat_pdu".
+* "id": Client product id string.
+* "connection_hint": Possible values are "modem", "low_broadband", "satellite", "high_broadband", "wan", "lan", "autodetect".
+* "physical_width": Numeric phyical width of display.
+* "physical_height": Numeric physical height of display.
+* "desktop_orientation": Numeric angle of orientation.
+* "scale_factor": Numeric scale factor of desktop.
+* "device_scale_factor": Numeric scale factor of display.
+
+RDP type: Connect Response
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+With this event, the initial RDP negotiation is complete in terms of tracking and logging.
+
+RDP type: TLS Handshake
+~~~~~~~~~~~~~~~~~~~~~~~
+
+With this event, the initial RDP negotiation is complete in terms of tracking and logging.
+
+The session will use TLS encryption.
+
+The "x509_serials" field is a list of observed certificate serial numbers, e.g., "16ed2aa0495f259d4f5d99edada570d1".
+
+Examples
+~~~~~~~~
+
+RDP logging:
+
+::
+
+  "rdp": {
+    "tx_id": 0,
+    "event_type": "initial_request",
+    "cookie": "A70067"
+  }
+
+  "rdp": {
+    "tx_id": 1,
+    "event_type": "initial_response"
+  }
+
+  "rdp": {
+    "tx_id": 2,
+    "event_type": "connect_request",
+    "client": {
+      "version": "v5",
+      "desktop_width": 1152,
+      "desktop_height": 864,
+      "color_depth": 15,
+      "keyboard_layout": "en-US",
+      "build": "Windows XP",
+      "client_name": "ISD2-KM84178",
+      "keyboard_type": "enhanced",
+      "function_keys": 12,
+      "product_id": 1,
+      "capabilities": [
+        "support_errinfo_pdf"
+      ],
+      "id": "55274-OEM-0011903-00107"
+    },
+    "channels": [
+      "rdpdr",
+      "cliprdr",
+      "rdpsnd"
+    ]
+  }
+
+  "rdp": {
+    "tx_id": 3,
+    "event_type": "connect_response"
+  }
+
+
+RDP logging, with transition to TLS:
+
+::
+
+  "rdp": {
+    "tx_id": 0,
+    "event_type": "initial_request",
+    "cookie": "AWAKECODI"
+  }
+
+  "rdp": {
+    "tx_id": 1,
+    "event_type": "initial_response",
+    "server_supports": [
+      "extended_client_data"
+    ],
+    "protocol": "hybrid"
+  }
+
+  "rdp": {
+    "tx_id": 2,
+    "event_type": "tls_handshake",
+    "x509_serials": [
+      "16ed2aa0495f259d4f5d99edada570d1"
+    ]
+  }
index 859c7a96554f2877402ece7df24f07bf49cdf62b..75a77bf71ba7602764bc02cb96b5fb3fda1cdc10 100644 (file)
@@ -16,10 +16,20 @@ debug = []
 
 [dependencies]
 nom = "4.2"
+bitflags = "1.0"
+byteorder = "1.3"
 crc = "1.8"
+memchr = "2.2"
+num = "0.2"
+num-derive = "0.2"
+num-traits = "0.2"
+widestring = "0.4"
+
 der-parser = "1.1"
 kerberos-parser = "0.2"
 
 ntp-parser = "0.3"
 ipsec-parser = "0.4"
 snmp-parser = "0.3.0"
+tls-parser = "0.8"
+x509-parser = "0.4"
index 4673e72665f4f5e7dfff020bb3fefe7518e1e586..3a08d605c3dcbf6732193b3b415d696069cb3bb5 100644 (file)
 #[macro_use]
 extern crate nom;
 
+#[macro_use]
+extern crate bitflags;
+extern crate byteorder;
 extern crate crc;
+extern crate memchr;
+#[macro_use]
+extern crate num_derive;
+extern crate widestring;
 
 extern crate der_parser;
 extern crate kerberos_parser;
+extern crate tls_parser;
+extern crate x509_parser;
 
 #[macro_use]
 pub mod log;
@@ -57,3 +66,4 @@ pub mod ntp;
 pub mod tftp;
 pub mod dhcp;
 pub mod applayertemplate;
+pub mod rdp;
diff --git a/rust/src/rdp/error.rs b/rust/src/rdp/error.rs
new file mode 100644 (file)
index 0000000..3306682
--- /dev/null
@@ -0,0 +1,22 @@
+/* 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>
+
+// custom errors the parser can emit
+pub const RDP_UNIMPLEMENTED_LENGTH_DETERMINANT: u32 = 128;
+pub const RDP_NOT_X224_CLASS_0_ERROR: u32 = 129;
diff --git a/rust/src/rdp/log.rs b/rust/src/rdp/log.rs
new file mode 100644 (file)
index 0000000..40d7a15
--- /dev/null
@@ -0,0 +1,652 @@
+/* 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 json::{Json, JsonT};
+use rdp::parser::*;
+use rdp::windows;
+use std;
+use x509_parser::parse_x509_der;
+
+#[no_mangle]
+pub extern "C" fn rs_rdp_to_json(tx: *mut std::os::raw::c_void) -> *mut JsonT {
+    let tx = cast_pointer!(tx, RdpTransaction);
+    match to_json(tx) {
+        Some(js) => js.unwrap(),
+        None => std::ptr::null_mut(),
+    }
+}
+
+/// populate a json object with transactional information, for logging
+fn to_json(tx: &RdpTransaction) -> Option<Json> {
+    let js = Json::object();
+
+    js.set_integer("tx_id", tx.id);
+
+    match &tx.item {
+        RdpTransactionItem::X224ConnectionRequest(ref x224) => {
+            x224_req_to_json(&js, x224)
+        }
+        RdpTransactionItem::X224ConnectionConfirm(x224) => {
+            x224_conf_to_json(&js, x224)
+        }
+
+        RdpTransactionItem::McsConnectRequest(ref mcs) => {
+            mcs_req_to_json(&js, mcs);
+        }
+
+        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");
+            let js_chain = Json::array();
+            for blob in chain {
+                match parse_x509_der(&blob.data) {
+                    Ok((_, cert)) => {
+                        js_chain.array_append_string(
+                            &cert.tbs_certificate.serial.to_str_radix(16),
+                        );
+                    }
+                    _ => {}
+                }
+            }
+            js.set("x509_serials", js_chain);
+        }
+    }
+
+    return Some(js);
+}
+
+/// json helper for X224ConnectionRequest
+fn x224_req_to_json(js: &Json, x224: &X224ConnectionRequest) {
+    use 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() {
+            let flags = Json::array();
+            if req.flags.contains(Flags::RESTRICTED_ADMIN_MODE_REQUIRED) {
+                flags.array_append_string("restricted_admin_mode_required");
+            }
+            if req
+                .flags
+                .contains(Flags::REDIRECTED_AUTHENTICATION_MODE_REQUIRED)
+            {
+                flags.array_append_string(
+                    "redirected_authentication_mode_required",
+                );
+            }
+            if req.flags.contains(Flags::CORRELATION_INFO_PRESENT) {
+                flags.array_append_string("correlation_info_present");
+            }
+            js.set("flags", flags);
+        }
+    }
+}
+
+/// json helper for X224ConnectionConfirm
+fn x224_conf_to_json(js: &Json, x224: &X224ConnectionConfirm) {
+    use 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() {
+                    let flags = Json::array();
+                    if resp
+                        .flags
+                        .contains(Flags::EXTENDED_CLIENT_DATA_SUPPORTED)
+                    {
+                        flags.array_append_string("extended_client_data");
+                    }
+                    if resp.flags.contains(Flags::DYNVC_GFX_PROTOCOL_SUPPORTED)
+                    {
+                        flags.array_append_string("dynvc_gfx");
+                    }
+
+                    // NEGRSP_FLAG_RESERVED not logged
+
+                    if resp
+                        .flags
+                        .contains(Flags::RESTRICTED_ADMIN_MODE_SUPPORTED)
+                    {
+                        flags.array_append_string("restricted_admin");
+                    }
+                    if resp.flags.contains(
+                        Flags::REDIRECTED_AUTHENTICATION_MODE_SUPPORTED,
+                    ) {
+                        flags.array_append_string("redirected_authentication");
+                    }
+                    js.set("server_supports", flags);
+                }
+
+                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_integer(
+                        "error_code",
+                        NegotiationFailureCode::SslRequiredByServer as u64,
+                    );
+                    js.set_string("reason", "ssl required by server")
+                }
+                NegotiationFailureCode::SslNotAllowedByServer => {
+                    js.set_integer(
+                        "error_code",
+                        NegotiationFailureCode::SslNotAllowedByServer as u64,
+                    );
+                    js.set_string("reason", "ssl not allowed by server")
+                }
+                NegotiationFailureCode::SslCertNotOnServer => {
+                    js.set_integer(
+                        "error_code",
+                        NegotiationFailureCode::SslCertNotOnServer as u64,
+                    );
+                    js.set_string("reason", "ssl cert not on server")
+                }
+                NegotiationFailureCode::InconsistentFlags => {
+                    js.set_integer(
+                        "error_code",
+                        NegotiationFailureCode::InconsistentFlags as u64,
+                    );
+                    js.set_string("reason", "inconsistent flags")
+                }
+                NegotiationFailureCode::HybridRequiredByServer => {
+                    js.set_integer(
+                        "error_code",
+                        NegotiationFailureCode::HybridRequiredByServer as u64,
+                    );
+                    js.set_string("reason", "hybrid required by server")
+                }
+                NegotiationFailureCode::SslWithUserAuthRequiredByServer => {
+                    js.set_integer(
+                        "error_code",
+                        NegotiationFailureCode::SslWithUserAuthRequiredByServer
+                            as u64,
+                    );
+                    js.set_string(
+                        "reason",
+                        "ssl with user auth required by server",
+                    )
+                }
+            },
+        }
+    }
+}
+
+/// json helper for McsConnectRequest
+fn mcs_req_to_json(js: &Json, mcs: &McsConnectRequest) {
+    // 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) => {
+                let js_client = Json::object();
+
+                match client.version {
+                    Some(ref ver) => js_client
+                        .set_string("version", &version_to_string(ver, "v")),
+                    None => js_client.set_string("version", &unknown),
+                }
+
+                js_client
+                    .set_integer("desktop_width", client.desktop_width as u64);
+                js_client.set_integer(
+                    "desktop_height",
+                    client.desktop_height as u64,
+                );
+
+                if let Some(depth) = get_color_depth(client) {
+                    js_client.set_integer("color_depth", depth);
+                }
+
+                // sas_sequence not logged
+
+                js_client.set_string(
+                    "keyboard_layout",
+                    &windows::lcid_to_string(client.keyboard_layout, &unknown),
+                );
+
+                js_client.set_string(
+                    "build",
+                    &windows::os_to_string(&client.client_build, &unknown),
+                );
+
+                if client.client_name.len() > 0 {
+                    js_client.set_string("client_name", &client.client_name);
+                }
+
+                if let Some(ref kb) = client.keyboard_type {
+                    js_client
+                        .set_string("keyboard_type", &keyboard_to_string(kb));
+                }
+
+                if client.keyboard_subtype != 0 {
+                    js_client.set_integer(
+                        "keyboard_subtype",
+                        client.keyboard_subtype as u64,
+                    );
+                }
+
+                if client.keyboard_function_key != 0 {
+                    js_client.set_integer(
+                        "function_keys",
+                        client.keyboard_function_key as u64,
+                    );
+                }
+
+                if client.ime_file_name.len() > 0 {
+                    js_client.set_string("ime", &client.ime_file_name);
+                }
+
+                //
+                // optional fields
+                //
+
+                if let Some(id) = client.client_product_id {
+                    js_client.set_integer("product_id", id as u64);
+                }
+
+                if let Some(serial) = client.serial_number {
+                    if serial != 0 {
+                        js_client.set_integer("serial_number", serial as u64);
+                    }
+                }
+
+                // supported_color_depth not logged
+
+                if let Some(ref early_capability_flags) =
+                    client.early_capability_flags
+                {
+                    use rdp::parser::EarlyCapabilityFlags as Flags;
+
+                    if !early_capability_flags.is_empty() {
+                        let flags = Json::array();
+                        if early_capability_flags
+                            .contains(Flags::RNS_UD_CS_SUPPORT_ERRINFO_PDF)
+                        {
+                            flags.array_append_string("support_errinfo_pdf");
+                        }
+                        if early_capability_flags
+                            .contains(Flags::RNS_UD_CS_WANT_32BPP_SESSION)
+                        {
+                            flags.array_append_string("want_32bpp_session");
+                        }
+                        if early_capability_flags
+                            .contains(Flags::RNS_UD_CS_SUPPORT_STATUSINFO_PDU)
+                        {
+                            flags.array_append_string("support_statusinfo_pdu");
+                        }
+                        if early_capability_flags
+                            .contains(Flags::RNS_UD_CS_STRONG_ASYMMETRIC_KEYS)
+                        {
+                            flags.array_append_string("strong_asymmetric_keys");
+                        }
+
+                        // RNS_UD_CS_UNUSED not logged
+
+                        if early_capability_flags
+                            .contains(Flags::RNS_UD_CS_VALID_CONNECTION_TYPE)
+                        {
+                            flags.array_append_string("valid_connection_type");
+                        }
+                        if early_capability_flags.contains(
+                            Flags::RNS_UD_CS_SUPPORT_MONITOR_LAYOUT_PDU,
+                        ) {
+                            flags.array_append_string(
+                                "support_monitor_layout_pdu",
+                            );
+                        }
+                        if early_capability_flags.contains(
+                            Flags::RNS_UD_CS_SUPPORT_NETCHAR_AUTODETECT,
+                        ) {
+                            flags.array_append_string(
+                                "support_netchar_autodetect",
+                            );
+                        }
+                        if early_capability_flags.contains(
+                            Flags::RNS_UD_CS_SUPPORT_DYNVC_GFX_PROTOCOL,
+                        ) {
+                            flags.array_append_string(
+                                "support_dynvc_gfx_protocol",
+                            );
+                        }
+                        if early_capability_flags.contains(
+                            Flags::RNS_UD_CS_SUPPORT_DYNAMIC_TIME_ZONE,
+                        ) {
+                            flags.array_append_string(
+                                "support_dynamic_time_zone",
+                            );
+                        }
+                        if early_capability_flags
+                            .contains(Flags::RNS_UD_CS_SUPPORT_HEARTBEAT_PDU)
+                        {
+                            flags.array_append_string("support_heartbeat_pdu");
+                        }
+                        js_client.set("capabilities", flags);
+                    }
+                }
+
+                if let Some(ref id) = client.client_dig_product_id {
+                    if id.len() > 0 {
+                        js_client.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_client.set_string("connection_hint", s);
+                    }
+                }
+
+                // server_selected_procotol not logged
+
+                if let Some(width) = client.desktop_physical_width {
+                    js_client.set_integer("physical_width", width as u64);
+                }
+
+                if let Some(height) = client.desktop_physical_height {
+                    js_client.set_integer("physical_height", height as u64);
+                }
+
+                if let Some(orientation) = client.desktop_orientation {
+                    js_client
+                        .set_integer("desktop_orientation", orientation as u64);
+                }
+
+                if let Some(scale) = client.desktop_scale_factor {
+                    js_client.set_integer("scale_factor", scale as u64);
+                }
+
+                if let Some(scale) = client.device_scale_factor {
+                    js_client.set_integer("device_scale_factor", scale as u64);
+                }
+                js.set("client", js_client);
+            }
+
+            McsConnectRequestChild::CsNet(ref net) => {
+                if net.channels.len() > 0 {
+                    let channels = Json::array();
+                    for channel in &net.channels {
+                        channels.array_append_string(&channel);
+                    }
+                    js.set("channels", channels);
+                }
+            }
+
+            McsConnectRequestChild::CsUnknown(_) => {}
+        }
+    }
+}
+
+/// converts RdpClientVersion to a string, using the provided prefix
+fn version_to_string<'a>(ver: &RdpClientVersion, prefix: &'a str) -> String {
+    let mut result = String::from(prefix);
+    match ver {
+        RdpClientVersion::V4 => result.push_str("4"),
+        RdpClientVersion::V5_V8_1 => result.push_str("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, unsure how to effectively test Json/JsonT
+
+    #[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 (file)
index 0000000..ddfc511
--- /dev/null
@@ -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 and application layer
+//!
+//! 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 (file)
index 0000000..ac98deb
--- /dev/null
@@ -0,0 +1,1544 @@
+/* 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 nom::{be_u16, be_u8, le_u16, le_u32, le_u8, ErrorKind, IResult};
+use rdp::error::RDP_NOT_X224_CLASS_0_ERROR;
+use rdp::util::{
+    le_slice_to_string, parse_per_length_determinant, utf7_slice_to_string,
+};
+use rdp::windows;
+
+/// 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 x >= 10 && x <= 10_000 {
+        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 x >= 100 && x <= 500 {
+        Some(x)
+    } else {
+        None
+    }
+}
+
+/// constrains device scale to a range, 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)]
+pub enum TpktVersion {
+    T123 = 0x3,
+}
+
+/// t.123-spec, section 8
+#[derive(Clone, Debug, PartialEq)]
+pub struct T123Tpkt {
+    pub child: T123TpktChild,
+}
+
+/// variants that a t.123 tpkt can hold
+#[derive(Clone, Debug, PartialEq)]
+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)]
+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)]
+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)]
+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)]
+pub enum X224ConnectionRequestType {
+    NegotiationRequest = 0x1,
+    NegotiationResponse = 0x2,
+    NegotiationFailure = 0x3,
+}
+
+/// rdp-spec, section 2.2.1.1.1
+#[derive(Clone, Debug, PartialEq)]
+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)]
+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 {
+        const PROTOCOL_RDP = Protocol::ProtocolRdp as u32;
+        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)]
+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)]
+pub enum NegotiationFromServer {
+    Response(NegotiationResponse),
+    Failure(NegotiationFailure),
+}
+
+/// rdp-spec, section 2.2.1.1.1
+#[derive(Clone, Debug, PartialEq)]
+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)]
+pub struct NegotiationFailure {
+    pub code: NegotiationFailureCode,
+}
+
+/// rdp-spec, section 2.2.1.2.2
+#[derive(Clone, Debug, FromPrimitive, PartialEq)]
+pub enum NegotiationFailureCode {
+    SslRequiredByServer = 0x1,
+    SslNotAllowedByServer = 0x2,
+    SslCertNotOnServer = 0x3,
+    InconsistentFlags = 0x4,
+    HybridRequiredByServer = 0x5,
+    SslWithUserAuthRequiredByServer = 0x6,
+}
+
+// ================
+
+/// x224-spec, section 13.7
+#[derive(Clone, Debug, PartialEq)]
+pub struct X223Data {
+    pub child: X223DataChild,
+}
+
+/// variants that an x.223 data message can hold
+#[derive(Clone, Debug, PartialEq)]
+pub enum X223DataChild {
+    McsConnectRequest(McsConnectRequest),
+    McsConnectResponse(McsConnectResponse),
+    Raw(Vec<u8>),
+}
+
+/// t.125-spec, section 7, part 2
+#[derive(Clone, Debug, PartialEq)]
+pub enum T125Type {
+    T125TypeMcsConnectRequest = 0x65,  // 101
+    T125TypeMcsConnectResponse = 0x66, // 102
+}
+
+/// rdp-spec, section 2.2.1.3.2
+#[derive(Clone, Debug, PartialEq)]
+pub struct McsConnectRequest {
+    pub children: Vec<McsConnectRequestChild>,
+}
+
+/// variants that an mcs connection message can hold
+#[derive(Clone, Debug, PartialEq)]
+pub enum McsConnectRequestChild {
+    CsClientCore(CsClientCoreData),
+    CsNet(CsNet),
+    CsUnknown(CsUnknown),
+}
+
+/// rdp-spec, section 2.2.1.3.1
+#[derive(Clone, Debug, FromPrimitive, PartialEq)]
+pub enum CsType {
+    Core = 0xc001,
+    Net = 0xc003,
+}
+
+/// rdp-spec, section 2.2.1.3.2
+#[derive(Clone, Debug, PartialEq)]
+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)]
+#[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)]
+pub enum ColorDepth {
+    RnsUdColor4Bpp = 0xca00,
+    RnsUdColor8Bpp = 0xca01,
+}
+
+/// rdp-spec, section 2.2.1.3.2 Client Core Data
+#[derive(Clone, Debug, FromPrimitive, PartialEq)]
+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)]
+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)]
+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)]
+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)]
+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)]
+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)]
+pub struct CsNet {
+    pub channels: Vec<String>,
+}
+
+/// generic structure
+/// cf. rdp-spec, section 2.2.1.3.4
+#[derive(Clone, Debug, PartialEq)]
+pub struct CsUnknown {
+    pub typ: u16,
+    pub data: Vec<u8>,
+}
+
+/// rdp-spec, section 2.2.1.4
+#[derive(Clone, Debug, PartialEq)]
+pub struct McsConnectResponse {}
+
+// ==================
+
+/// parser for t.123 and children
+/// t.123-spec, section 8
+pub fn parse_t123_tpkt(input: &[u8]) -> IResult<&[u8], T123Tpkt> {
+    let (i1, _version) =
+        verify!(input, be_u8, |x| x == TpktVersion::T123 as u8)?;
+    let (i2, _reserved) = try_parse!(i1, be_u8);
+    // less u8, u8, u16
+    let (i3, sz) = map_opt!(i2, be_u16, |x: u16| x.checked_sub(4))?;
+    let (i4, data) = take!(i3, sz)?;
+
+    let opt1: Option<T123TpktChild> = {
+        match opt!(data, parse_x224_connection_request_class_0) {
+            Ok((_remainder, opt)) => match opt {
+                Some(x) => Some(T123TpktChild::X224ConnectionRequest(x)),
+                None => None,
+            },
+            Err(e) => return Err(e),
+        }
+    };
+
+    let opt2: Option<T123TpktChild> = match opt1 {
+        Some(x) => Some(x),
+        None => match opt!(data, parse_x224_connection_confirm_class_0) {
+            Ok((_remainder, opt)) => match opt {
+                Some(x) => Some(T123TpktChild::X224ConnectionConfirm(x)),
+                None => None,
+            },
+            Err(e) => return Err(e),
+        },
+    };
+
+    let opt3: Option<T123TpktChild> = match opt2 {
+        Some(x) => Some(x),
+        None => match opt!(data, parse_x223_data_class_0) {
+            Ok((_remainder, opt)) => match opt {
+                Some(x) => Some(T123TpktChild::Data(x)),
+                None => None,
+            },
+            Err(e) => return Err(e),
+        },
+    };
+    let child: T123TpktChild = match opt3 {
+        Some(x) => x,
+        None => T123TpktChild::Raw(data.to_vec()),
+    };
+
+    return Ok((i4, T123Tpkt { child }));
+}
+
+/// rdp-spec, section 2.2.1.1
+fn parse_x224_connection_request(
+    input: &[u8],
+) -> IResult<&[u8], X224ConnectionRequest> {
+    let (i1, length) = verify!(input, be_u8, |x| x != 0xff)?; // 0xff is reserved
+    let (i2, cr_cdt) = bits!(
+        i1,
+        tuple!(
+            verify!(take_bits!(u8, 4), |x| x
+                == X224Type::ConnectionRequest as u8),
+            verify!(take_bits!(u8, 4), |x| x == 0 || x == 1)
+        )
+    )?;
+    let (i3, dst_ref) = verify!(i2, be_u16, |x| x == 0)?;
+    let (i4, src_ref) = try_parse!(i3, be_u16);
+    let (i5, class_options) = bits!(
+        i4,
+        tuple!(
+            verify!(take_bits!(u8, 4), |x| x <= 4),
+            verify!(take_bits!(u8, 4), |x| x <= 3)
+        )
+    )?;
+    // less cr_cdt (u8), dst_ref (u16), src_ref (u16), class_options (u8)
+    let (i6, sz) = expr_opt!(i5, length.checked_sub(6))?;
+
+    //
+    // optionally find cookie and/or negotiation request
+    //
+
+    let (i7, data) = {
+        if sz > 0 {
+            take!(i6, sz)?
+        } else {
+            (i6, &[][..])
+        }
+    };
+
+    let (j1, cookie) = {
+        if data.len() > 0 {
+            match opt!(data, parse_rdp_cookie) {
+                Ok((remainder, opt)) => (remainder, opt),
+                Err(e) => return Err(e),
+            }
+        } else {
+            (&[][..], None)
+        }
+    };
+
+    let (j2, negotiation_request) = {
+        if j1.len() > 0 {
+            match opt!(j1, parse_negotiation_request) {
+                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> {
+    let (i1, x224) = try_parse!(input, parse_x224_connection_request);
+    if x224.class == 0 && x224.options == 0 {
+        Ok((i1, x224))
+    } else {
+        Err(nom::Err::Error(error_position!(
+            input,
+            ErrorKind::Custom(RDP_NOT_X224_CLASS_0_ERROR)
+        )))
+    }
+}
+
+// rdp-spec, section 2.2.1.1.1
+fn parse_rdp_cookie(input: &[u8]) -> IResult<&[u8], RdpCookie> {
+    do_parse! {
+        input,
+        _key: verify!(
+            take!(8),
+            |x| x == b"Cookie: ")
+        >> _name: verify!(
+            take!(9),
+            |x| x == b"mstshash=")
+        >> bytes: take_until_and_consume!("\r\n")
+        >> s: map_res!(value!(bytes), std::str::from_utf8)
+        >> (RdpCookie{ mstshash: String::from(s) })
+    }
+}
+
+// rdp-spec, section 2.2.1.1.1
+fn parse_negotiation_request(
+    input: &[u8],
+) -> IResult<&[u8], NegotiationRequest> {
+    do_parse! {
+        input,
+        _typ: verify!(
+            le_u8,
+            |x| x == X224ConnectionRequestType::NegotiationRequest as u8)
+        >> flags: map_opt!(
+            le_u8,
+            NegotiationRequestFlags::from_bits)
+        // u8, u8, u16, and u32 give _length of 8
+        >> _length: verify!(
+            le_u16,
+            |x| x == 8)
+        >> protocols: map_opt!(
+            le_u32,
+            ProtocolFlags::from_bits)
+        >> (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> {
+    let (i1, length) = verify!(input, be_u8, |x| x != 0xff)?; // 0xff is reserved
+    let (i2, cr_cdt) = bits!(
+        i1,
+        tuple!(
+            verify!(take_bits!(u8, 4), |x| x
+                == X224Type::ConnectionConfirm as u8),
+            verify!(take_bits!(u8, 4), |x| x == 0 || x == 1)
+        )
+    )?;
+    let (i3, dst_ref) = verify!(i2, be_u16, |x| x == 0)?;
+    let (i4, src_ref) = try_parse!(i3, be_u16);
+    let (i5, class_options) = bits!(
+        i4,
+        tuple!(
+            verify!(take_bits!(u8, 4), |x| x <= 4),
+            verify!(take_bits!(u8, 4), |x| x <= 3)
+        )
+    )?;
+
+    // less cr_cdt (u8), dst_ref (u16), src_ref (u16), class_options (u8)
+    let (i6, sz) = expr_opt!(i5, length.checked_sub(6))?;
+
+    // a negotiation message from the server might be absent (sz == 0)
+    let (i7, negotiation_from_server) = {
+        if sz > 0 {
+            let (i7, data) = take!(i6, sz)?;
+
+            // it will be one of a response message or a failure message
+            let opt1: Option<NegotiationFromServer> =
+                match opt!(data, parse_negotiation_response) {
+                    Ok((_remainder, opt)) => match opt {
+                        Some(x) => Some(NegotiationFromServer::Response(x)),
+                        None => None,
+                    },
+                    Err(e) => return Err(e),
+                };
+            let opt2: Option<NegotiationFromServer> = match opt1 {
+                Some(x) => Some(x),
+                None => match opt!(data, parse_negotiation_failure) {
+                    Ok((_remainder, opt)) => match opt {
+                        Some(x) => Some(NegotiationFromServer::Failure(x)),
+                        None => None,
+                    },
+                    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> {
+    let (i1, x224) = try_parse!(input, parse_x224_connection_confirm);
+    if x224.class == 0 && x224.options == 0 {
+        Ok((i1, x224))
+    } else {
+        // x.224, but not a class 0 x.224 message
+        Err(nom::Err::Error(error_position!(
+            input,
+            ErrorKind::Custom(RDP_NOT_X224_CLASS_0_ERROR)
+        )))
+    }
+}
+
+// rdp-spec, section 2.2.1.1.1
+fn parse_negotiation_response(
+    input: &[u8],
+) -> IResult<&[u8], NegotiationResponse> {
+    do_parse! {
+        input,
+        _typ: verify!(
+            le_u8,
+            |x| x == X224ConnectionRequestType::NegotiationResponse as u8)
+        >> flags: map_opt!(
+            le_u8,
+            NegotiationResponseFlags::from_bits)
+        // u8, u8, u16, and u32 give _length of 8
+        >> _length: verify!(
+            le_u16,
+            |x| x == 8)
+        >> protocol: map_opt!(
+            le_u32,
+            num::FromPrimitive::from_u32)
+        >> (NegotiationResponse { flags, protocol })
+    }
+}
+
+// rdp-spec, section 2.2.1.1.1
+fn parse_negotiation_failure(
+    input: &[u8],
+) -> IResult<&[u8], NegotiationFailure> {
+    do_parse! {
+        input,
+        _typ: verify!(
+            le_u8,
+            |x| x == X224ConnectionRequestType::NegotiationFailure as u8)
+        >> _flags: le_u8
+        // u8, u8, u16, and u32 give _length of 8
+        >> _length: verify!(
+            le_u16,
+            |x| x == 8)
+        >> code: map_opt!(
+            le_u32,
+            num::FromPrimitive::from_u32)
+        >> (NegotiationFailure { code })
+    }
+}
+
+/// x224-spec, section 13.7
+fn parse_x223_data_class_0(input: &[u8]) -> IResult<&[u8], X223Data> {
+    let (i1, _length) = verify!(input, be_u8, |x| x == 2)?;
+    let (i2, _dt_x_roa) = bits!(
+        i1,
+        tuple!(
+            verify!(take_bits!(u8, 4), |x| x == 0xf),
+            verify!(take_bits!(u8, 3), |x| x == 0),
+            verify!(take_bits!(u8, 1), |x| x == 0)
+        )
+    )?;
+    let (i3, _eot) = verify!(i2, be_u8, |x| x == 0x80)?;
+
+    //
+    // optionally find exactly one of the child messages
+    //
+
+    let opt1: Option<X223DataChild> = match opt!(i3, parse_mcs_connect) {
+        Ok((_remainder, opt)) => match opt {
+            Some(x) => Some(X223DataChild::McsConnectRequest(x)),
+            None => None,
+        },
+        Err(e) => return Err(e),
+    };
+
+    let opt2: Option<X223DataChild> = match opt1 {
+        Some(x) => Some(x),
+        None => match opt!(i3, parse_mcs_connect_response) {
+            Ok((_remainder, opt)) => match opt {
+                Some(x) => Some(X223DataChild::McsConnectResponse(x)),
+                None => None,
+            },
+            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> {
+    let (i1, _ber_type) = verify!(
+        input,
+        le_u8,
+        // BER: 0b01=application, 0b1=non-primitive, 0b11111
+        |x| x == 0x7f
+    )?;
+    let (i2, _t125_type) = verify!(i1, le_u8, |x| x
+        == T125Type::T125TypeMcsConnectRequest as u8)?;
+
+    // skip to, and consume, H.221 client-to-server key
+    let (i3, _skipped) = take_until_and_consume!(i2, "Duca")?;
+
+    let (i4, data) = length_data!(i3, parse_per_length_determinant)?;
+    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!(remainder, parse_cs_client_core_data) {
+            Ok((rem, opt)) => match opt {
+                // found CsClientCoreData
+                Some(core_data) => {
+                    children
+                        .push(McsConnectRequestChild::CsClientCore(core_data));
+                    rem
+                }
+                None => match opt!(remainder, parse_cs_net) {
+                    // found CsNet
+                    Ok((rem, opt)) => match opt {
+                        Some(net) => {
+                            children.push(McsConnectRequestChild::CsNet(net));
+                            rem
+                        }
+                        None => {
+                            match opt!(remainder, parse_cs_unknown) {
+                                // was able to parse CsUnknown
+                                Ok((rem, opt)) => {
+                                    match opt {
+                                        Some(unknown) => {
+                                            children.push(McsConnectRequestChild::CsUnknown(unknown));
+                                            rem
+                                        }
+                                        None => {
+                                            break;
+                                        }
+                                    }
+                                }
+                                Err(nom::Err::Incomplete(i)) => {
+                                    return Err(nom::Err::Incomplete(i))
+                                }
+                                Err(nom::Err::Failure(_))
+                                | Err(nom::Err::Error(_)) => break,
+                            }
+                        }
+                    },
+                    Err(nom::Err::Incomplete(i)) => {
+                        return Err(nom::Err::Incomplete(i))
+                    }
+                    Err(nom::Err::Failure(_)) | Err(nom::Err::Error(_)) => {
+                        break
+                    }
+                },
+            },
+            Err(nom::Err::Incomplete(i)) => {
+                return Err(nom::Err::Incomplete(i))
+            }
+            Err(nom::Err::Failure(_)) | Err(nom::Err::Error(_)) => break,
+        };
+        if remainder.len() == 0 {
+            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!(input, le_u16, |x| x == CsType::Core as u16)?;
+    // less u16, u16
+    let (i2, sz) = map_opt!(i1, le_u16, |x: u16| x.checked_sub(4))?;
+    let (i3, data) = take!(i2, sz)?;
+    let (j1, version) = map!(data, le_u32, num::FromPrimitive::from_u32)?;
+    let (j2, desktop_width) = try_parse!(j1, le_u16);
+    let (j3, desktop_height) = try_parse!(j2, le_u16);
+    let (j4, color_depth) = map!(j3, le_u16, num::FromPrimitive::from_u16)?;
+    let (j5, sas_sequence) = map!(j4, le_u16, num::FromPrimitive::from_u16)?;
+    let (j6, keyboard_layout) = try_parse!(j5, le_u32);
+    let (j7, client_build) = map!(j6, le_u32, windows::build_number_to_os)?;
+    let (j8, client_name) = map_res!(j7, take!(32), le_slice_to_string)?;
+    let (j9, keyboard_type) = map!(j8, le_u32, num::FromPrimitive::from_u32)?;
+    let (j10, keyboard_subtype) = try_parse!(j9, le_u32);
+    let (j11, keyboard_function_key) = try_parse!(j10, le_u32);
+    let (j12, ime_file_name) = map_res!(j11, take!(64), le_slice_to_string)?;
+
+    //
+    // optional fields below (but each requires the previous)
+    //
+
+    let (j13, post_beta2_color_depth) =
+        match opt!(j12, map_opt!(le_u16, num::FromPrimitive::from_u16)) {
+            Ok((rem, obj)) => (rem, obj),
+            _ => (j12, None),
+        };
+
+    let (j14, client_product_id) = match post_beta2_color_depth {
+        None => (j13, None),
+        Some(_) => match opt!(j13, le_u16) {
+            Ok((rem, obj)) => (rem, obj),
+            _ => (j13, None),
+        },
+    };
+
+    let (j15, serial_number) = match client_product_id {
+        None => (j14, None),
+        Some(_) => match opt!(j14, le_u32) {
+            Ok((rem, obj)) => (rem, obj),
+            _ => (j14, None),
+        },
+    };
+
+    let (j16, high_color_depth) = match serial_number {
+        None => (j15, None),
+        Some(_) => {
+            match opt!(j15, map_opt!(le_u16, num::FromPrimitive::from_u16)) {
+                Ok((rem, obj)) => (rem, obj),
+                _ => (j15, None),
+            }
+        }
+    };
+
+    let (j17, supported_color_depth) = match high_color_depth {
+        None => (j16, None),
+        Some(_) => {
+            match opt!(j16, map_opt!(le_u16, SupportedColorDepth::from_bits)) {
+                Ok((rem, obj)) => (rem, obj),
+                _ => (j16, None),
+            }
+        }
+    };
+
+    let (j18, early_capability_flags) = match supported_color_depth {
+        None => (j17, None),
+        Some(_) => {
+            match opt!(j17, map_opt!(le_u16, EarlyCapabilityFlags::from_bits)) {
+                Ok((rem, obj)) => (rem, obj),
+                _ => (j17, None),
+            }
+        }
+    };
+
+    let (j19, client_dig_product_id) = match early_capability_flags {
+        None => (j18, None),
+        Some(_) => match opt!(j18, map_res!(take!(64), le_slice_to_string)) {
+            Ok((rem, obj)) => (rem, obj),
+            _ => (j18, None),
+        },
+    };
+
+    let (j20, connection_hint) = match client_dig_product_id {
+        None => (j19, None),
+        Some(_) => {
+            match opt!(j19, map_opt!(le_u8, num::FromPrimitive::from_u8)) {
+                Ok((rem, obj)) => (rem, obj),
+                _ => (j19, None),
+            }
+        }
+    };
+
+    let (j21, pad) = match connection_hint {
+        None => (j20, None),
+        Some(_) => match opt!(j20, take!(1)) {
+            Ok((rem, obj)) => (rem, obj),
+            _ => (j20, None),
+        },
+    };
+
+    let (j22, server_selected_protocol) = match pad {
+        None => (j21, None),
+        Some(_) => {
+            match opt!(j21, map_opt!(le_u32, ProtocolFlags::from_bits)) {
+                Ok((rem, obj)) => (rem, obj),
+                _ => (j21, None),
+            }
+        }
+    };
+
+    let (j23, desktop_physical_width) = match server_selected_protocol {
+        None => (j22, None),
+        Some(_) => match opt!(j22, map_opt!(le_u32, millimeters_to_opt)) {
+            Ok((rem, obj)) => (rem, obj),
+            _ => (j22, None),
+        },
+    };
+
+    let (j24, desktop_physical_height) = match desktop_physical_width {
+        None => (j23, None),
+        Some(_) => match opt!(j23, map_opt!(le_u32, millimeters_to_opt)) {
+            Ok((rem, obj)) => (rem, obj),
+            _ => (j23, None),
+        },
+    };
+
+    let (j25, desktop_orientation) = match desktop_physical_height {
+        None => (j24, None),
+        Some(_) => {
+            match opt!(j24, map_opt!(le_u16, num::FromPrimitive::from_u16)) {
+                Ok((rem, obj)) => (rem, obj),
+                _ => (j24, None),
+            }
+        }
+    };
+
+    let (j26, desktop_scale_factor) = match desktop_orientation {
+        None => (j25, None),
+        Some(_) => match opt!(j25, map_opt!(le_u32, desktop_scale_to_opt)) {
+            Ok((rem, obj)) => (rem, obj),
+            _ => (j25, None),
+        },
+    };
+
+    let (_j27, device_scale_factor) = match desktop_scale_factor {
+        None => (j26, None),
+        Some(_) => match opt!(j26, map_opt!(le_u32, device_scale_to_opt)) {
+            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!(input, le_u16, |x| x == CsType::Net as u16)?;
+    // less _typ (u16), this length indicator (u16), count (u32)
+    let (i2, sz) = map_opt!(i1, le_u16, |x: u16| x.checked_sub(8))?;
+    let (i3, count) = try_parse!(i2, le_u32);
+    let (i4, data) = take!(i3, sz)?;
+
+    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!(remainder, take!(8), utf7_slice_to_string)?;
+        channels.push(name);
+        // options (u32) are discarded for now
+        let (j2, _options) = try_parse!(j1, le_u32);
+        remainder = j2;
+    }
+
+    return Ok((i4, CsNet { channels }));
+}
+
+// generic CS structure parse
+// cf. rdp-spec, section 2.2.1.3.4
+fn parse_cs_unknown(input: &[u8]) -> IResult<&[u8], CsUnknown> {
+    do_parse! {
+        input,
+        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),
+                }
+            })
+        // less u16, u16
+        >> sz: map_opt!(le_u16, |x: u16| x.checked_sub(4))
+        >> data: take!(sz)
+        >> (CsUnknown { typ, data: data.to_vec() })
+    }
+}
+
+// rdp-spec, section 2.2.1.4
+fn parse_mcs_connect_response(
+    input: &[u8],
+) -> IResult<&[u8], McsConnectResponse> {
+    do_parse! {
+        input,
+        _ber_type: verify!(
+            le_u8,
+            // BER: 0b01=application, 0b1=non-primitive, 0b11111
+            |x| x == 0x7f)
+        >> _t125_type: verify!(
+            le_u8,
+            |x| x == T125Type::T125TypeMcsConnectResponse as u8)
+        >> (McsConnectResponse {})
+    }
+}
+
+#[cfg(test)]
+mod tests_cookie_21182 {
+    use 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 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::PROTOCOL_RDP,
+                    }),
+                    data: Vec::new(),
+                },
+            ),
+        };
+        assert_eq!(
+            Ok((&BYTES[PADDING_BEGIN..][..], t123_tpkt)),
+            parse_t123_tpkt(t123_bytes)
+        )
+    }
+}
+
+#[cfg(test)]
+mod tests_core_49350 {
+    use 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::PROTOCOL_RDP),
+            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 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 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 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 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 nom;
+    use rdp::parser::*;
+
+    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(nom::Err::Incomplete(nom::Needed::Size(
+                TPKT_END - TPKT_BEGIN - 4
+            ))),
+            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(nom::Err::Incomplete(nom::Needed::Size(
+                X224_END - X224_BEGIN - (6 + 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(nom::Err::Incomplete(nom::Needed::Size(
+                NEG_REQ_END - NEG_REQ_BEGIN - (1 + 1 + 2)
+            ))),
+            parse_negotiation_request(neg_req_bytes)
+        )
+    }
+}
+
+#[cfg(test)]
+mod tests_core_incomplete_49350 {
+    use nom;
+    use rdp::parser::*;
+
+    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(nom::Err::Incomplete(nom::Needed::Size(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(nom::Err::Incomplete(nom::Needed::Size(0x128))),
+            parse_mcs_connect(connect_bytes)
+        )
+    }
+}
diff --git a/rust/src/rdp/rdp.rs b/rust/src/rdp/rdp.rs
new file mode 100644 (file)
index 0000000..f00aa2e
--- /dev/null
@@ -0,0 +1,705 @@
+/* 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 application layer
+
+use core::{
+    self, AppProto, DetectEngineState, Flow, ALPROTO_UNKNOWN, IPPROTO_TCP,
+};
+use nom;
+use parser::*;
+use rdp::parser::*;
+use std;
+use std::mem::transmute;
+use tls_parser::{
+    parse_tls_plaintext, TlsMessage, TlsMessageHandshake, TlsRecordType,
+};
+
+static mut ALPROTO_RDP: AppProto = ALPROTO_UNKNOWN;
+
+//
+// transactions
+//
+
+#[derive(Debug, PartialEq)]
+pub struct CertificateBlob {
+    pub data: Vec<u8>,
+}
+
+#[derive(Debug, PartialEq)]
+pub enum RdpTransactionItem {
+    X224ConnectionRequest(X224ConnectionRequest),
+    X224ConnectionConfirm(X224ConnectionConfirm),
+    McsConnectRequest(McsConnectRequest),
+    McsConnectResponse(McsConnectResponse),
+    TlsCertificateChain(Vec<CertificateBlob>),
+}
+
+#[derive(Debug, PartialEq)]
+pub struct RdpTransaction {
+    pub id: u64,
+    pub item: RdpTransactionItem,
+    // managed by macros `export_tx_get_detect_state!` and `export_tx_set_detect_state!`
+    de_state: Option<*mut DetectEngineState>,
+}
+
+impl RdpTransaction {
+    fn new(id: u64, item: RdpTransactionItem) -> Self {
+        Self {
+            id,
+            item,
+            de_state: None,
+        }
+    }
+
+    fn free(&mut self) {
+        if let Some(de_state) = self.de_state {
+            core::sc_detect_engine_state_free(de_state);
+        }
+    }
+}
+
+impl Drop for RdpTransaction {
+    fn drop(&mut self) {
+        self.free();
+    }
+}
+
+#[no_mangle]
+pub 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 unsafe { transmute(tx) };
+        }
+        None => {
+            return std::ptr::null_mut();
+        }
+    }
+}
+
+#[no_mangle]
+pub 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_complete(
+    _direction: u8,
+) -> std::os::raw::c_int {
+    // a parser can implement a multi-step tx completion by using an arbitrary `n`
+    return 1;
+}
+
+#[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)]
+pub struct RdpState {
+    next_id: u64,
+    to_client: Vec<u8>,
+    to_server: Vec<u8>,
+    transactions: Vec<RdpTransaction>,
+    tls_parsing: bool,
+    bypass_parsing: bool,
+}
+
+impl RdpState {
+    fn new() -> Self {
+        Self {
+            next_id: 0,
+            to_client: Vec::new(),
+            to_server: Vec::new(),
+            transactions: Vec::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> {
+        for tx in &self.transactions {
+            if tx.id == tx_id {
+                return Some(tx);
+            }
+        }
+        return None;
+    }
+
+    fn new_tx(&mut self, item: RdpTransactionItem) -> RdpTransaction {
+        let tx = RdpTransaction::new(self.next_id, item);
+        self.next_id += 1;
+        return tx;
+    }
+
+    /// parse buffer captures from client to server
+    fn parse_ts(&mut self, input: &[u8]) -> bool {
+        // no need to process input buffer
+        if self.bypass_parsing {
+            return true;
+        }
+        // combine residual buffer with provided buffer
+        self.to_server.extend(input);
+        let temp: Vec<u8> = self.to_server.split_off(0);
+        let mut available = temp.as_slice();
+
+        loop {
+            if available.len() == 0 {
+                return true;
+            }
+            if self.tls_parsing {
+                match parse_tls_plaintext(&available) {
+                    Ok((remainder, _tls)) => {
+                        // update awaiting-parsing buffer
+                        available = remainder;
+                    }
+
+                    Err(nom::Err::Incomplete(_)) => {
+                        // save unparsed residual buffer for next parse
+                        self.to_server.extend(available);
+                        return true;
+                    }
+
+                    Err(nom::Err::Failure(_)) | Err(nom::Err::Error(_)) => {
+                        return false;
+                    }
+                }
+            } else {
+                // every message should be encapsulated within a T.123 tpkt
+                match parse_t123_tpkt(&available) {
+                    // success
+                    Ok((remainder, t123)) => {
+                        // update awaiting-parsing buffer
+                        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(tx);
+                            }
+
+                            // X.223 data packet, evaluate what it encapsulates
+                            T123TpktChild::Data(x223) => {
+                                match x223.child {
+                                    X223DataChild::McsConnectRequest(mcs) => {
+                                        let tx =
+                                            self.new_tx(RdpTransactionItem::McsConnectRequest(mcs));
+                                        self.transactions.push(tx);
+                                    }
+                                    // unknown message in X.223, skip
+                                    _ => (),
+                                }
+                            }
+
+                            // unknown message in T.123, skip
+                            _ => (),
+                        }
+                    }
+
+                    Err(nom::Err::Incomplete(_)) => {
+                        // save unparsed residual buffer for next parse
+                        self.to_server.extend(available);
+                        return true;
+                    }
+
+                    Err(nom::Err::Failure(_)) | Err(nom::Err::Error(_)) => {
+                        if probe_tls_handshake(available) {
+                            self.tls_parsing = true;
+                            return self.parse_ts(available);
+                        } else {
+                            return false;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /// parse buffer captures from server to client
+    fn parse_tc(&mut self, input: &[u8]) -> bool {
+        // no need to process input buffer
+        if self.bypass_parsing {
+            return true;
+        }
+        // combine residual buffer with provided buffer
+        self.to_client.extend(input);
+        let temp: Vec<u8> = self.to_client.split_off(0);
+        let mut available = temp.as_slice();
+
+        loop {
+            if available.len() == 0 {
+                return true;
+            }
+            if self.tls_parsing {
+                match parse_tls_plaintext(&available) {
+                    Ok((remainder, tls)) => {
+                        // update awaiting-parsing buffer
+                        available = remainder;
+                        for message in &tls.msg {
+                            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(tx);
+                                    self.bypass_parsing = true;
+                                }
+                                _ => {}
+                            }
+                        }
+                    }
+
+                    Err(nom::Err::Incomplete(_)) => {
+                        // save unparsed residual buffer for next parse
+                        self.to_client.extend(available);
+                        return true;
+                    }
+
+                    Err(nom::Err::Failure(_)) | Err(nom::Err::Error(_)) => {
+                        return false;
+                    }
+                }
+            } else {
+                // every message should be encapsulated within a T.123 tpkt
+                match parse_t123_tpkt(&available) {
+                    // success
+                    Ok((remainder, t123)) => {
+                        // update awaiting-parsing buffer
+                        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(tx);
+                            }
+
+                            // X.223 data packet, evaluate what it encapsulates
+                            T123TpktChild::Data(x223) => {
+                                match x223.child {
+                                    X223DataChild::McsConnectResponse(mcs) => {
+                                        let tx = self
+                                            .new_tx(RdpTransactionItem::McsConnectResponse(mcs));
+                                        self.transactions.push(tx);
+                                        self.bypass_parsing = true;
+                                        return true;
+                                    }
+
+                                    // unknown message in X.223, skip
+                                    _ => (),
+                                }
+                            }
+
+                            // unknown message in T.123, skip
+                            _ => (),
+                        }
+                    }
+
+                    Err(nom::Err::Incomplete(_)) => {
+                        self.to_client.extend(available);
+                        return true;
+                    }
+
+                    Err(nom::Err::Failure(_)) | Err(nom::Err::Error(_)) => {
+                        if probe_tls_handshake(available) {
+                            self.tls_parsing = true;
+                            return self.parse_tc(available);
+                        } else {
+                            return false;
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+#[no_mangle]
+pub extern "C" fn rs_rdp_state_new() -> *mut std::os::raw::c_void {
+    let state = RdpState::new();
+    let boxed = Box::new(state);
+    return unsafe { std::mem::transmute(boxed) };
+}
+
+#[no_mangle]
+pub extern "C" fn rs_rdp_state_free(state: *mut std::os::raw::c_void) {
+    let _drop: Box<RdpState> = unsafe { std::mem::transmute(state) };
+}
+
+#[no_mangle]
+pub 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);
+}
+
+//
+// detection state
+//
+
+export_tx_get_detect_state!(rs_rdp_tx_get_detect_state, RdpTransaction);
+export_tx_set_detect_state!(rs_rdp_tx_set_detect_state, RdpTransaction);
+
+//
+// probe
+//
+
+/// probe for T.123 type identifier, as each message is encapsulated in T.123
+fn probe_rdp(input: &[u8]) -> bool {
+    input.len() > 0 && input[0] == TpktVersion::T123 as u8
+}
+
+/// probe for T.123 message, whether to client or to server
+#[no_mangle]
+pub 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 != std::ptr::null_mut() {
+        // 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 unsafe { ALPROTO_RDP };
+        }
+    }
+    return ALPROTO_UNKNOWN;
+}
+
+/// probe for TLS
+fn probe_tls_handshake(input: &[u8]) -> bool {
+    input.len() > 0 && input[0] == u8::from(TlsRecordType::Handshake)
+}
+
+//
+// parse
+//
+
+#[no_mangle]
+pub extern "C" fn rs_rdp_parse_ts(
+    _flow: *const Flow,
+    state: *mut std::os::raw::c_void,
+    _pstate: *mut std::os::raw::c_void,
+    input: *const u8,
+    input_len: u32,
+    _data: *const std::os::raw::c_void,
+    _flags: u8,
+) -> i32 {
+    let state = cast_pointer!(state, RdpState);
+    let buf = build_slice!(input, input_len as usize);
+    // attempt to parse bytes as `rdp` protocol
+    if state.parse_ts(buf) {
+        return 1;
+    }
+    // no need for further parsing
+    return -1;
+}
+
+#[no_mangle]
+pub extern "C" fn rs_rdp_parse_tc(
+    _flow: *const Flow,
+    state: *mut std::os::raw::c_void,
+    _pstate: *mut std::os::raw::c_void,
+    input: *const u8,
+    input_len: u32,
+    _data: *const std::os::raw::c_void,
+    _flags: u8,
+) -> i32 {
+    let state = cast_pointer!(state, RdpState);
+    let buf = build_slice!(input, input_len as usize);
+    // attempt to parse bytes as `rdp` protocol
+    if state.parse_tc(buf) {
+        return 1;
+    }
+    // no need for further parsing
+    return -1;
+}
+
+//
+// registration
+//
+
+const PARSER_NAME: &'static [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: rs_rdp_probe_ts_tc,
+        probe_tc: 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_get_comp_st: rs_rdp_tx_get_progress_complete,
+        tx_get_progress: rs_rdp_tx_get_progress,
+        get_tx_logged: None,
+        set_tx_logged: None,
+        get_de_state: rs_rdp_tx_get_detect_state,
+        set_de_state: rs_rdp_tx_set_detect_state,
+        get_events: None,
+        get_eventinfo: None,
+        get_eventinfo_byid: None,
+        localstorage_new: None,
+        localstorage_free: None,
+        get_tx_mpm_id: None,
+        set_tx_mpm_id: None,
+        get_files: None,
+        get_tx_iterator: 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 rdp::parser::{RdpCookie, X224ConnectionRequest};
+
+    #[test]
+    fn test_probe_rdp() {
+        let buf: &[u8] = &[0x03, 0x00];
+        assert_eq!(true, probe_rdp(&buf));
+    }
+
+    #[test]
+    fn test_probe_rdp_other() {
+        let buf: &[u8] = &[0x04, 0x00];
+        assert_eq!(false, probe_rdp(&buf));
+    }
+
+    #[test]
+    fn test_probe_tls_handshake() {
+        let buf: &[u8] = &[0x16, 0x00];
+        assert_eq!(true, probe_tls_handshake(&buf));
+    }
+
+    #[test]
+    fn test_probe_tls_handshake_other() {
+        let buf: &[u8] = &[0x17, 0x00];
+        assert_eq!(false, 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] = &[
+            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();
+        assert_eq!(true, state.parse_ts(&buf_1));
+        assert_eq!(0, state.transactions.len());
+        assert_eq!(true, 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!(false, state.parse_ts(&buf));
+    }
+
+    #[test]
+    fn test_parse_tc_rdp() {
+        let buf_1: &[u8] = &[0x03, 0x00, 0x00, 0x09, 0x02];
+        let buf_2: &[u8] = &[0xf0, 0x80, 0x7f, 0x66];
+        let mut state = RdpState::new();
+        assert_eq!(true, state.parse_tc(&buf_1));
+        assert_eq!(0, state.transactions.len());
+        assert_eq!(true, 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!(false, 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(tx0);
+        state.transactions.push(tx1);
+        assert_eq!(2, state.transactions.len());
+        assert_eq!(0, state.transactions[0].id);
+        assert_eq!(1, state.transactions[1].id);
+        assert_eq!(false, state.tls_parsing);
+        assert_eq!(false, 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(tx0);
+        state.transactions.push(tx1);
+        state.transactions.push(tx2);
+        assert_eq!(Some(&state.transactions[1]), state.get_tx(1));
+    }
+
+    #[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(tx0);
+        state.transactions.push(tx1);
+        state.transactions.push(tx2);
+        state.free_tx(1);
+        assert_eq!(3, state.next_id);
+        assert_eq!(2, state.transactions.len());
+        assert_eq!(0, state.transactions[0].id);
+        assert_eq!(2, 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 (file)
index 0000000..dc44ff1
--- /dev/null
@@ -0,0 +1,187 @@
+/* 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 byteorder::ReadBytesExt;
+use memchr::memchr;
+use nom;
+use nom::{ErrorKind, IResult, Needed};
+use rdp::error::RDP_UNIMPLEMENTED_LENGTH_DETERMINANT;
+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);
+    loop {
+        match cursor.read_u16::<byteorder::LittleEndian>() {
+            Ok(x) => {
+                if x == 0 {
+                    break;
+                };
+                vec.push(x)
+            }
+            Err(_) => break,
+        }
+    }
+    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> {
+    if input.is_empty() {
+        // need a single byte to begin length determination
+        Err(nom::Err::Incomplete(Needed::Size(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(nom::Err::Incomplete(Needed::Size(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(nom::Err::Error(error_position!(
+                            input,
+                            ErrorKind::Custom(RDP_UNIMPLEMENTED_LENGTH_DETERMINANT)
+                        )))
+                    }
+                }
+            }
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use nom;
+    use rdp::error::RDP_UNIMPLEMENTED_LENGTH_DETERMINANT;
+
+    #[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(nom::Err::Incomplete(nom::Needed::Size(1))),
+            parse_per_length_determinant(bytes)
+        )
+    }
+
+    #[test]
+    fn test_length_16k_unimplemented() {
+        let bytes = &[0xc0];
+        assert_eq!(
+            Err(nom::Err::Error(error_position!(
+                &bytes[..],
+                ErrorKind::Custom(RDP_UNIMPLEMENTED_LENGTH_DETERMINANT)
+            ))),
+            parse_per_length_determinant(bytes)
+        )
+    }
+
+    #[test]
+    fn test_length_double_length_incomplete() {
+        let bytes = &[0x81];
+        assert_eq!(
+            Err(nom::Err::Incomplete(nom::Needed::Size(2))),
+            parse_per_length_determinant(bytes)
+        )
+    }
+}
diff --git a/rust/src/rdp/windows.rs b/rust/src/rdp/windows.rs
new file mode 100644 (file)
index 0000000..2282d30
--- /dev/null
@@ -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<'a>(lcid: u32, default: &'a 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)]
+pub struct OperatingSystem {
+    pub build: Build,
+    pub suffix: Suffix,
+}
+
+// <https://en.wikipedia.org/wiki/Windows_NT#Releases>
+#[derive(Clone, Debug, FromPrimitive, PartialEq)]
+#[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)]
+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<'a>(os: &OperatingSystem, default: &'a 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));
+    }
+}
index d9779218c9ff79998203c8acbaf6c2c8d93fb7ee..b6b6c173ebc8bf1dc59dee7debb91aae31ebe7f9 100644 (file)
@@ -51,6 +51,7 @@ app-layer-krb5.c app-layer-krb5.h \
 app-layer-dhcp.c app-layer-dhcp.h \
 app-layer-template.c app-layer-template.h \
 app-layer-template-rust.c app-layer-template-rust.h \
+app-layer-rdp.c app-layer-rdp.h \
 app-layer-ssh.c app-layer-ssh.h \
 app-layer-ssl.c app-layer-ssl.h \
 conf.c conf.h \
@@ -342,6 +343,7 @@ output-json-dhcp.c output-json-dhcp.h \
 output-json-snmp.c output-json-snmp.h \
 output-json-template.c output-json-template.h \
 output-json-template-rust.c output-json-template-rust.h \
+output-json-rdp.c output-json-rdp.h \
 output-json-metadata.c output-json-metadata.h \
 output-lua.c output-lua.h \
 output-packet.c output-packet.h \
index 7d0b3f0e10a46350ea8b9ab8fd8eb80221e707cc..78b5fa0549a983b0f56674f57a185ef479999840 100644 (file)
@@ -69,6 +69,7 @@
 #include "app-layer-snmp.h"
 #include "app-layer-template.h"
 #include "app-layer-template-rust.h"
+#include "app-layer-rdp.h"
 
 #include "conf.h"
 #include "util-spm.h"
@@ -1508,6 +1509,7 @@ void AppLayerParserRegisterProtocolParsers(void)
     RegisterSNMPParsers();
     RegisterTemplateRustParsers();
     RegisterTemplateParsers();
+    RegisterRdpParsers();
 
     /** IMAP */
     AppLayerProtoDetectRegisterProtocol(ALPROTO_IMAP, "imap");
index 04120d34540a14e148fbddaf9ae5aab128491503..c937fc9ce5e0b79b1415748b9eb5756ae06363e9 100644 (file)
@@ -108,6 +108,9 @@ const char *AppProtoToString(AppProto alproto)
         case ALPROTO_TEMPLATE_RUST:
             proto_name = "template-rust";
             break;
+        case ALPROTO_RDP:
+            proto_name = "rdp";
+            break;
         case ALPROTO_FAILED:
             proto_name = "failed";
             break;
@@ -149,6 +152,7 @@ AppProto StringToAppProto(const char *proto_name)
     if (strcmp(proto_name,"snmp")==0) return ALPROTO_SNMP;
     if (strcmp(proto_name,"template")==0) return ALPROTO_TEMPLATE;
     if (strcmp(proto_name,"template-rust")==0) return ALPROTO_TEMPLATE_RUST;
+    if (strcmp(proto_name,"rdp")==0) return ALPROTO_RDP;
     if (strcmp(proto_name,"failed")==0) return ALPROTO_FAILED;
 
     return ALPROTO_UNKNOWN;
index ffd0bd9251f5f0379fdbae6b153b41d9dd0f31a7..da9548f97a1c7a139b93c4077d30d7971a238e53 100644 (file)
@@ -53,6 +53,7 @@ enum AppProtoEnum {
     ALPROTO_SNMP,
     ALPROTO_TEMPLATE,
     ALPROTO_TEMPLATE_RUST,
+    ALPROTO_RDP,
 
     /* used by the probing parser when alproto detection fails
      * permanently for that particular stream */
diff --git a/src/app-layer-rdp.c b/src/app-layer-rdp.c
new file mode 100644 (file)
index 0000000..f02dae1
--- /dev/null
@@ -0,0 +1,42 @@
+/* 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.
+ */
+
+/**
+ * \file
+ *
+ * \author Zach Kelly <zach.kelly@lmco.com>
+ *
+ * Application layer parser for RDP
+ */
+
+#include "suricata-common.h"
+#include "stream.h"
+#include "conf.h"
+#include "util-unittest.h"
+#include "app-layer-detect-proto.h"
+#include "app-layer-parser.h"
+#include "app-layer-rdp.h"
+#include "rust-rdp-rdp-gen.h"
+
+void RegisterRdpParsers(void) {
+    /* only register if enabled in config */
+    if (ConfGetNode("app-layer.protocols.rdp") == NULL) {
+        return;
+    }
+    SCLogDebug("Registering rdp parser");
+    rs_rdp_register_parser();
+}
diff --git a/src/app-layer-rdp.h b/src/app-layer-rdp.h
new file mode 100644 (file)
index 0000000..9af2b14
--- /dev/null
@@ -0,0 +1,30 @@
+/* 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.
+ */
+
+/**
+ * \file
+ *
+ * \author Zach Kelly <zach.kelly@lmco.com>
+ */
+
+#ifndef __APP_LAYER_RDP_H__
+#define __APP_LAYER_RDP_H__
+
+void RegisterRdpParsers(void);
+void RdpParserRegisterTests(void);
+
+#endif /* __APP_LAYER_RDP_H__ */
diff --git a/src/output-json-rdp.c b/src/output-json-rdp.c
new file mode 100644 (file)
index 0000000..050009d
--- /dev/null
@@ -0,0 +1,180 @@
+/* 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.
+ */
+
+/**
+ * \file
+ *
+ * \author Zach Kelly <zach.kelly@lmco.com>
+ *
+ * Application layer logger for RDP
+ */
+
+#include "suricata-common.h"
+#include "debug.h"
+#include "detect.h"
+#include "pkt-var.h"
+#include "conf.h"
+#include "threads.h"
+#include "threadvars.h"
+#include "tm-threads.h"
+#include "util-unittest.h"
+#include "util-buffer.h"
+#include "util-debug.h"
+#include "util-byte.h"
+#include "output.h"
+#include "output-json.h"
+#include "app-layer.h"
+#include "app-layer-parser.h"
+#include "app-layer-rdp.h"
+#include "output-json-rdp.h"
+#include "rust-rdp-log-gen.h"
+
+typedef struct LogRdpFileCtx_ {
+    LogFileCtx *file_ctx;
+    uint32_t    flags;
+} LogRdpFileCtx;
+
+typedef struct LogRdpLogThread_ {
+    LogRdpFileCtx *rdplog_ctx;
+    uint32_t         count;
+    MemBuffer       *buffer;
+} LogRdpLogThread;
+
+static int JsonRdpLogger(ThreadVars *tv, void *thread_data,
+    const Packet *p, Flow *f, void *state, void *tx, uint64_t tx_id)
+{
+    LogRdpLogThread *thread = thread_data;
+
+    json_t *js = CreateJSONHeader(p, LOG_DIR_PACKET, "rdp");
+    if (unlikely(js == NULL)) {
+        return TM_ECODE_FAILED;
+    }
+
+    json_t *rdp_js = rs_rdp_to_json(tx);
+    if (unlikely(rdp_js == NULL)) {
+        goto error;
+    }
+    json_object_set_new(js, "rdp", rdp_js);
+
+    MemBufferReset(thread->buffer);
+    OutputJSONBuffer(js, thread->rdplog_ctx->file_ctx, &thread->buffer);
+    json_decref(js);
+
+    return TM_ECODE_OK;
+
+error:
+    json_decref(js);
+    return TM_ECODE_FAILED;
+}
+
+static void OutputRdpLogDeInitCtxSub(OutputCtx *output_ctx)
+{
+    LogRdpFileCtx *rdplog_ctx = (LogRdpFileCtx *)output_ctx->data;
+    SCFree(rdplog_ctx);
+    SCFree(output_ctx);
+}
+
+static OutputInitResult OutputRdpLogInitSub(ConfNode *conf,
+    OutputCtx *parent_ctx)
+{
+    OutputInitResult result = { NULL, false };
+    OutputJsonCtx *ajt = parent_ctx->data;
+
+    LogRdpFileCtx *rdplog_ctx = SCCalloc(1, sizeof(*rdplog_ctx));
+    if (unlikely(rdplog_ctx == NULL)) {
+        return result;
+    }
+    rdplog_ctx->file_ctx = ajt->file_ctx;
+
+    OutputCtx *output_ctx = SCCalloc(1, sizeof(*output_ctx));
+    if (unlikely(output_ctx == NULL)) {
+        SCFree(rdplog_ctx);
+        return result;
+    }
+    output_ctx->data = rdplog_ctx;
+    output_ctx->DeInit = OutputRdpLogDeInitCtxSub;
+
+    AppLayerParserRegisterLogger(IPPROTO_TCP, ALPROTO_RDP);
+
+    SCLogDebug("rdp log sub-module initialized.");
+
+    result.ctx = output_ctx;
+    result.ok = true;
+    return result;
+}
+
+#define OUTPUT_BUFFER_SIZE 65535
+
+static TmEcode JsonRdpLogThreadInit(ThreadVars *t, const void *initdata, void **data)
+{
+    LogRdpLogThread *thread = SCCalloc(1, sizeof(*thread));
+    if (unlikely(thread == NULL)) {
+        return TM_ECODE_FAILED;
+    }
+
+    if (initdata == NULL) {
+        SCLogDebug("Error getting context for EveLogRdp.  \"initdata\" is NULL.");
+        SCFree(thread);
+        return TM_ECODE_FAILED;
+    }
+
+    thread->buffer = MemBufferCreateNew(OUTPUT_BUFFER_SIZE);
+    if (unlikely(thread->buffer == NULL)) {
+        SCFree(thread);
+        return TM_ECODE_FAILED;
+    }
+
+    thread->rdplog_ctx = ((OutputCtx *)initdata)->data;
+    *data = (void *)thread;
+
+    return TM_ECODE_OK;
+}
+
+static TmEcode JsonRdpLogThreadDeinit(ThreadVars *t, void *data)
+{
+    LogRdpLogThread *thread = (LogRdpLogThread *)data;
+    if (thread == NULL) {
+        return TM_ECODE_OK;
+    }
+    if (thread->buffer != NULL) {
+        MemBufferFree(thread->buffer);
+    }
+    SCFree(thread);
+    return TM_ECODE_OK;
+}
+
+void JsonRdpLogRegister(void)
+{
+    if (ConfGetNode("app-layer.protocols.rdp") == NULL) {
+        return;
+    }
+    /* Register as an eve sub-module. */
+    OutputRegisterTxSubModule(
+        LOGGER_JSON_RDP,
+        "eve-log",
+        "JsonRdpLog",
+        "eve-log.rdp",
+        OutputRdpLogInitSub,
+        ALPROTO_RDP,
+        JsonRdpLogger,
+        JsonRdpLogThreadInit,
+        JsonRdpLogThreadDeinit,
+        NULL
+    );
+
+    SCLogDebug("rdp json logger registered.");
+}
diff --git a/src/output-json-rdp.h b/src/output-json-rdp.h
new file mode 100644 (file)
index 0000000..5dc9237
--- /dev/null
@@ -0,0 +1,29 @@
+/* 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.
+ */
+
+/**
+ * \file
+ *
+ * \author Zach Kelly <zach.kelly@lmco.com>
+ */
+
+#ifndef __OUTPUT_JSON_RDP_H__
+#define __OUTPUT_JSON_RDP_H__
+
+void JsonRdpLogRegister(void);
+
+#endif /* __OUTPUT_JSON_RDP_H__ */
index 1c27a758cfc29971c837a40a02ffdd6c834bf184..3e109c5c19673ce5a58c4402292f1bfdb53212b2 100644 (file)
@@ -77,6 +77,7 @@
 #include "output-json-snmp.h"
 #include "output-json-template.h"
 #include "output-json-template-rust.h"
+#include "output-json-rdp.h"
 #include "output-lua.h"
 #include "output-json-dnp3.h"
 #include "output-json-metadata.h"
@@ -1113,4 +1114,6 @@ void OutputRegisterLoggers(void)
     JsonTemplateLogRegister();
     /* Template Rust JSON logger. */
     JsonTemplateRustLogRegister();
+    /* RDP JSON logger. */
+    JsonRdpLogRegister();
 }
index 31764c5ae060eb55249a32ae050bade7a8c96b32..b35a49ad621c9ada2818cbb9d8a41d0fd4a4eba7 100644 (file)
@@ -442,6 +442,7 @@ typedef enum {
     LOGGER_JSON_SNMP,
     LOGGER_JSON_TEMPLATE_RUST,
     LOGGER_JSON_TEMPLATE,
+    LOGGER_JSON_RDP,
 
     LOGGER_ALERT_DEBUG,
     LOGGER_ALERT_FAST,
index 2a11d77805eeed54de4a5411707bf17cf6587ea9..4b618ab6cb294b6c384a99577d6ec0ba2a204fe3 100644 (file)
@@ -1320,6 +1320,7 @@ const char * PacketProfileLoggertIdToString(LoggerId id)
         CASE_CODE (LOGGER_JSON_TLS);
         CASE_CODE (LOGGER_JSON_TEMPLATE_RUST);
         CASE_CODE (LOGGER_JSON_TEMPLATE);
+        CASE_CODE (LOGGER_JSON_RDP);
         CASE_CODE (LOGGER_TLS_STORE);
         CASE_CODE (LOGGER_TLS);
         CASE_CODE (LOGGER_FILE_STORE);
index 4180af2e7a0c769102aa79df618213ac9afce18f..422e3a0c51c665532da3704b04856473fc58850f 100644 (file)
@@ -233,6 +233,7 @@ outputs:
 
         #- dnp3
         - ftp
+        - rdp
         - nfs
         - smb
         - tftp
@@ -785,6 +786,8 @@ app-layer:
     ftp:
       enabled: yes
       # memcap: 64mb
+    rdp:
+      enabled: yes
     ssh:
       enabled: yes
     smtp: