//! `ComObjRef` — DCOM OBJREF parser. //! //! Direct port of `src/MxNativeClient/ComObjRef.cs`. Parses the marshalled //! interface byte stream produced by `CoMarshalInterface` (per `[MS-DCOM]` //! §2.2.18) into a structured form the RPC layer can inspect for OXID/OID/IPID //! and the dual-string-array bindings (string + security towers). //! //! The .NET reference parses 68 fixed bytes followed by the dual-string array, //! which is decoded character-by-character and used purely for diagnostics. //! The Rust port mirrors that parser shape exactly — every field offset, //! the `Math.Min(entries, data.Length / 2)` bound, and the printable-ASCII //! escaping of each UTF-16 code unit are 1:1 with `ComObjRef.cs:18-117`. //! //! `ComObjRefProvider.cs` is **not** ported here — it is a thin wrapper around //! Win32 `CoMarshalInterface` / `IStream` / `GlobalLock` and produces OBJREF //! bytes by calling into ole32. That belongs behind `windows-rs` in a later //! M2/M3 wave; the pure-Rust parser stands alone and is what M2 wave 1 needs //! for OBJREF inspection on inbound activation responses. See followup F1 //! in this module's report. // Direct byte indexing — every access is guarded by an explicit length check // and the result reads as a 1:1 mirror of the .NET `BinaryPrimitives` calls. // `.get(n)?` would obscure the byte map. Mirrors the rationale documented in // `crates/mxaccess-codec/src/reference_handle.rs:7-11`. #![allow(clippy::indexing_slicing)] use std::fmt::Write as _; // `Guid` and `RpcError` are crate-shared since M2 wave 2 — see // `design/followups.md` F7+F8. pub use crate::error::RpcError; pub use crate::guid::Guid; /// Encoded layout per `ComObjRef.cs:25-39`: /// /// ```text /// offset size field /// 0 4 signature u32 LE = 0x574F454D ("MEOW") /// 4 4 flags u32 LE (1 = OBJREF_STANDARD; only standard parsed) /// 8 16 iid GUID /// 24 4 std_flags u32 LE (STDOBJREF flags) /// 28 4 public_refs u32 LE /// 32 8 oxid u64 LE /// 40 8 oid u64 LE /// 48 16 ipid GUID /// 64 2 dual_string_entries u16 LE (count of u16 code units in the array) /// 66 2 dual_string_security_offset u16 LE (boundary index between string and security bindings) /// 68 .. dual-string array (variable; UTF-16LE code units terminated by 0x0000 per entry) /// ``` /// /// **Header length** is 68 bytes; the dual-string array follows starting at /// offset 68. The `dual_string_entries` count is bounded by the actual byte /// length of the trailing bytes via `min(entries, data.len() / 2)` /// (`ComObjRef.cs:59`). pub const OBJREF_HEADER_LEN: usize = 68; /// "MEOW" — the OBJREF signature (`ComObjRef.cs:29`, also `[MS-DCOM]` §2.2.18.1). pub const OBJREF_SIGNATURE: u32 = 0x574F_454D; const FLAGS_OFFSET: usize = 4; const IID_OFFSET: usize = 8; const STD_FLAGS_OFFSET: usize = 24; const PUBLIC_REFS_OFFSET: usize = 28; const OXID_OFFSET: usize = 32; const OID_OFFSET: usize = 40; const IPID_OFFSET: usize = 48; const DUAL_STRING_ENTRIES_OFFSET: usize = 64; const DUAL_STRING_SECURITY_OFFSET_OFFSET: usize = 66; /// One decoded entry of the OBJREF dual-string array. `value` is the /// printable-ASCII escaping of the UTF-16 string per `ComObjRef.cs:82-91` — /// non-printable code units appear as `` lowercase hex. `is_security_binding` /// is set when the entry's start offset (in u16 units) is at or past /// `DualStringSecurityOffset`. /// /// Mirrors `ComDualStringEntry` (`ComObjRef.cs:138-145`). /// /// `protocol` is `Cow<'static, str>` because the OBJREF parser uses the /// 7-entry static table (`Cow::Borrowed`) while the M2 wave 2 OXID-resolve /// parser uses `format!("protseq_0x{:04x}", tower_id)` (`Cow::Owned`) for /// unknown tower ids (`ObjectExporterMessages.cs:120`). The two parsers /// share the entry type but emit different protocol labels for the same /// tower id — this is intentional and matches the .NET reference. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ComDualStringEntry { pub tower_id: u16, pub protocol: std::borrow::Cow<'static, str>, pub value: String, pub is_security_binding: bool, } impl ComDualStringEntry { /// Mirrors `ComDualStringEntry.ToDiagnosticString` (`ComObjRef.cs:140-144`): /// `":0x::"`. pub fn to_diagnostic_string(&self) -> String { let kind = if self.is_security_binding { "security" } else { "string" }; format!( "{}:0x{:04x}:{}:{}", kind, self.tower_id, self.protocol, self.value ) } } /// Parsed DCOM OBJREF (standard form). /// /// Mirrors `ComObjRef` record (`ComObjRef.cs:5-16`). All eleven fields of the /// .NET record are preserved including `signature`, `flags`, `std_flags`, /// `dual_string_entries`, and `dual_string_security_offset` — even though the /// signature is a known constant, the parser does not validate it (the .NET /// reference doesn't either; bytes are surfaced verbatim per CLAUDE.md /// preserve-unknown-bytes rule). #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ComObjRef { pub signature: u32, pub flags: u32, pub iid: Guid, pub standard_flags: u32, pub public_refs: u32, pub oxid: u64, pub oid: u64, pub ipid: Guid, /// Raw entry-count field — measured in **u16 code units**, not entries. /// Preserved verbatim from the wire even when it overruns the buffer; the /// parse loop bounds itself by `min(entries, data.len() / 2)` /// (`ComObjRef.cs:59`). pub dual_string_entries: u16, /// Boundary (in u16 code-unit indices) between string bindings and /// security bindings within the dual-string array (`ComObjRef.cs:98`). pub dual_string_security_offset: u16, pub dual_string_entries_decoded: Vec, } impl ComObjRef { /// Header length (68 bytes) before the dual-string array. pub const HEADER_LEN: usize = OBJREF_HEADER_LEN; /// Parse an OBJREF buffer. Mirrors `ComObjRef.Parse` (`ComObjRef.cs:18-40`) /// byte-for-byte: 68-byte fixed header followed by a UTF-16LE /// dual-string array bounded by `min(entries, tail.len() / 2)`. /// /// The signature field is read but not validated — the .NET reference /// surfaces it verbatim so callers can diff against captures. /// /// # Errors /// /// - [`RpcError::ShortRead`] if `buffer.len() < 68` /// (matches `ComObjRef.cs:20-23`). pub fn parse(buffer: &[u8]) -> Result { if buffer.len() < Self::HEADER_LEN { return Err(RpcError::ShortRead { expected: Self::HEADER_LEN, actual: buffer.len(), }); } let dual_string_entries = read_u16_le(buffer, DUAL_STRING_ENTRIES_OFFSET); let security_offset = read_u16_le(buffer, DUAL_STRING_SECURITY_OFFSET_OFFSET); let mut iid_bytes = [0u8; 16]; iid_bytes.copy_from_slice(&buffer[IID_OFFSET..IID_OFFSET + 16]); let mut ipid_bytes = [0u8; 16]; ipid_bytes.copy_from_slice(&buffer[IPID_OFFSET..IPID_OFFSET + 16]); let tail = &buffer[Self::HEADER_LEN..]; let decoded = decode_dual_string_array(tail, dual_string_entries, security_offset); Ok(Self { signature: read_u32_le(buffer, 0), flags: read_u32_le(buffer, FLAGS_OFFSET), iid: Guid(iid_bytes), standard_flags: read_u32_le(buffer, STD_FLAGS_OFFSET), public_refs: read_u32_le(buffer, PUBLIC_REFS_OFFSET), oxid: read_u64_le(buffer, OXID_OFFSET), oid: read_u64_le(buffer, OID_OFFSET), ipid: Guid(ipid_bytes), dual_string_entries, dual_string_security_offset: security_offset, dual_string_entries_decoded: decoded, }) } /// Diagnostic line emitter — byte-identical to `ToDiagnosticLines` /// (`ComObjRef.cs:42-55`). The output is intended for matching against /// Frida-captured probe output (`captures/057-managed-callback-route-service-trace-saved/probe.stdout.txt`). pub fn to_diagnostic_lines(&self) -> Vec { let dual_strings = self .dual_string_entries_decoded .iter() .map(ComDualStringEntry::to_diagnostic_string) .collect::>() .join("|"); vec![ format!("objref_signature=0x{:08X}", self.signature), format!("objref_flags=0x{:08X}", self.flags), format!("objref_iid={}", self.iid), format!("std_flags=0x{:08X}", self.standard_flags), format!("std_public_refs={}", self.public_refs), format!("std_oxid=0x{:016X}", self.oxid), format!("std_oid=0x{:016X}", self.oid), format!("std_ipid={}", self.ipid), format!("dual_string_entries={}", self.dual_string_entries), format!( "dual_string_security_offset={}", self.dual_string_security_offset ), format!("dual_strings={}", dual_strings), ] } } /// Decode the trailing dual-string array. Mirrors /// `DecodeDualStringArray` (`ComObjRef.cs:57-102`). /// /// The loop walks `i` in u16 code-unit indices, capped by /// `min(entries, data.len() / 2)`. Each entry begins with a 16-bit /// `tower_id`; if zero, it terminates the string-binding region (the /// `continue` skips to the next index without producing an entry — same as /// the .NET source). Otherwise the following u16 code units up to (but not /// including) the next 0x0000 terminator form the entry's value, escaped /// printable-ASCII per the `0x20..=0x7e` rule. fn decode_dual_string_array( data: &[u8], entries: u16, security_offset: u16, ) -> Vec { let entries = entries as usize; let count = entries.min(data.len() / 2); let mut strings = Vec::new(); let mut i: usize = 0; while i < count { let entry_start = i; let tower_id = read_u16_le(data, i * 2); i += 1; if tower_id == 0 { continue; } let mut text = String::new(); while i < count { let value = read_u16_le(data, i * 2); i += 1; if value == 0 { break; } if (0x20..=0x7e).contains(&value) { // Safe: 0x20..=0x7e is printable ASCII, valid UTF-8. text.push(value as u8 as char); } else { // Non-printable: emit "" lowercase hex (mirrors .NET // `value.ToString("x4", InvariantCulture)`). // write! to a String never fails; ignore the Result. let _ = write!(&mut text, "<{:04x}>", value); } } strings.push(ComDualStringEntry { tower_id, protocol: std::borrow::Cow::Borrowed(protocol_tower_name(tower_id)), value: text, is_security_binding: entry_start >= security_offset as usize, }); } strings } /// Protocol-tower name table per `ComObjRef.cs:104-117`. Returns `"unknown"` /// for unrecognised tower ids — mirrors the `_ =>` fall-through. pub const fn protocol_tower_name(tower_id: u16) -> &'static str { match tower_id { 0x0007 => "ncacn_ip_tcp", 0x0008 => "ncadg_ip_udp", 0x0009 => "ncacn_np", 0x000f => "ncacn_spx", 0x0010 => "ncacn_nb_nb", 0x0016 => "ncadg_ip_udp_or_netbios", 0x001f => "ncalrpc", _ => "unknown", } } #[inline] fn read_u16_le(bytes: &[u8], offset: usize) -> u16 { u16::from_le_bytes([bytes[offset], bytes[offset + 1]]) } #[inline] fn read_u32_le(bytes: &[u8], offset: usize) -> u32 { u32::from_le_bytes([ bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3], ]) } #[inline] fn read_u64_le(bytes: &[u8], offset: usize) -> u64 { u64::from_le_bytes([ bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3], bytes[offset + 4], bytes[offset + 5], bytes[offset + 6], bytes[offset + 7], ]) } // Compile-time invariant: header length matches the documented byte layout. const _: () = assert!(OBJREF_HEADER_LEN == 68); const _: () = assert!(OBJREF_SIGNATURE == 0x574F_454D); // --------------------------------------------------------------------------- // ComObjRefBuilder — pure-Rust OBJREF emitter. // Direct port of the second class in // `src/MxNativeClient/ManagedCallbackExporter.cs:337-393`. // --------------------------------------------------------------------------- /// Auth-service tower IDs the .NET reference advertises in every callback /// OBJREF. Mirrors the hard-coded array at /// `ManagedCallbackExporter.cs:362`. Each id appears in the security-binding /// portion of the dual-string array followed by `0xFFFF` and a terminator. /// /// IDs in order: NTLM SSP (0x0009), GSS Negotiate (0x001E), Kerberos (0x0010), /// SSL/TLS (0x000A), Schannel (0x0016), DPA (0x001F), Kerberos extension /// (0x000E). The Rust port carries the same set verbatim — no synthesis. pub const CALLBACK_OBJREF_AUTH_SERVICES: [u16; 7] = [0x0009, 0x001E, 0x0010, 0x000A, 0x0016, 0x001F, 0x000E]; /// Builds standard OBJREF byte buffers for the callback exporter to publish. /// /// Mirrors the static `ComObjRefBuilder` class /// (`src/MxNativeClient/ManagedCallbackExporter.cs:337-393`). The .NET reference /// only ever emits *standard* OBJREFs (`flags = 1`); the Rust port matches. /// /// This is the higher-level emitter that builds OBJREF bytes from primitives. /// It is **not** the Win32 `CoMarshalInterface`-based emitter from /// `ComObjRefProvider.cs` — that wrapper around `ole32` is still tracked as /// open follow-up F6 (it requires `windows-rs` and the M2 wave 3 callback /// exporter to register the emitted OBJREF with COM). pub struct ComObjRefBuilder; impl ComObjRefBuilder { /// Build a standard-OBJREF buffer for a given IID, OXID/OID/IPID, and one /// or more `ncacn_ip_tcp` string bindings (e.g. `"hostname[5985]"`). /// Mirrors `ComObjRefBuilder.CreateStandardObjRef` /// (`ManagedCallbackExporter.cs:339-392`). /// /// # Layout (`cs:348-389`) /// /// ```text /// offset size field /// 0 4 signature u32 LE = 0x574F454D ("MEOW") /// 4 4 flags u32 LE = 1 /// 8 16 iid GUID /// 24 4 std_flags u32 LE /// 28 4 public_refs u32 LE /// 32 8 oxid u64 LE /// 40 8 oid u64 LE /// 48 16 ipid GUID /// 64 2 entries u16 LE (count of u16 code units below) /// 66 2 security_offset u16 LE (in u16 code units) /// 68 .. dual-string array (variable-length u16 LE words) /// ``` /// /// # `entries` and `security_offset` /// /// `entries` is the **total u16-code-unit count** of the dual-string /// array (string bindings + 0 separator + 7 security entries + final 0). /// `security_offset` is the index (in u16 units) where security bindings /// begin — `cs:348` computes this as /// `sum(1 + binding.len() + 1 for binding in stringBindings) + 1`, i.e. /// per-binding `tower_id` (1 word) + `binding.len()` ASCII chars (one /// word each) + null terminator (1 word), plus the trailing 0 separator /// that ends the string section. /// /// # Panics /// /// Never panics. All length math saturates: bindings longer than /// `u16::MAX - HEADER_LEN/2 - SECURITY_TAIL_LEN` are not representable /// in the 16-bit `entries` field, and the .NET reference does not guard /// against this either; callers are expected to keep bindings short /// (typical `hostname[port]` is < 100 chars). #[must_use] pub fn create_standard_objref( iid: Guid, std_flags: u32, public_refs: u32, oxid: u64, oid: u64, ipid: Guid, string_bindings: &[&str], ) -> Vec { // security_offset = sum_{b in string_bindings}(1 + b.len() + 1) + 1 // (cs:348). u16-truncating cast mirrors `(ushort)`. let security_offset: u16 = string_bindings .iter() .map(|b| 1 + b.len() + 1) .sum::() .saturating_add(1) .min(u16::MAX as usize) as u16; // Build the u16 word array. let mut words: Vec = Vec::new(); // String-bindings section: per binding, [0x0007 (ncacn_ip_tcp), each // ASCII char as u16, terminator 0] (cs:350-359). for binding in string_bindings { words.push(0x0007); for ch in binding.chars() { words.push(ch as u16); } words.push(0); } // 0 separator that ends the string section (cs:361). words.push(0); // Security-bindings section: 7 hard-coded tower entries, each // [tower_id, 0xFFFF, 0] (cs:362-367). for &auth in &CALLBACK_OBJREF_AUTH_SERVICES { words.push(auth); words.push(0xFFFF); words.push(0); } // Final terminator (cs:369). words.push(0); // u16-truncating cast mirrors `(ushort)words.Count` (cs:371). let entries: u16 = words.len().min(u16::MAX as usize) as u16; let mut buffer = vec![0u8; OBJREF_HEADER_LEN + words.len() * 2]; // Fixed 68-byte header (cs:373-382). buffer[0..4].copy_from_slice(&OBJREF_SIGNATURE.to_le_bytes()); buffer[4..8].copy_from_slice(&1u32.to_le_bytes()); // flags = 1 (OBJREF_STANDARD) buffer[8..24].copy_from_slice(iid.as_bytes()); buffer[24..28].copy_from_slice(&std_flags.to_le_bytes()); buffer[28..32].copy_from_slice(&public_refs.to_le_bytes()); buffer[32..40].copy_from_slice(&oxid.to_le_bytes()); buffer[40..48].copy_from_slice(&oid.to_le_bytes()); buffer[48..64].copy_from_slice(ipid.as_bytes()); buffer[64..66].copy_from_slice(&entries.to_le_bytes()); buffer[66..68].copy_from_slice(&security_offset.to_le_bytes()); // Dual-string array body (cs:384-389). let mut offset = OBJREF_HEADER_LEN; for word in &words { buffer[offset..offset + 2].copy_from_slice(&word.to_le_bytes()); offset += 2; } buffer } } #[cfg(test)] #[allow( clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing, clippy::panic )] mod tests { use super::*; /// Hand-built OBJREF: signature + flags=1 + sample IID + std_flags + /// public_refs=5 + fake OXID/OID/IPID + dual_string array containing one /// `ncacn_ip_tcp` entry then a `0x0000` terminator. Returns the bytes. fn build_minimal_objref() -> Vec { let mut buf = Vec::new(); // signature "MEOW" 0x574F454D LE buf.extend_from_slice(&0x574F_454Du32.to_le_bytes()); // flags = 1 (OBJREF_STANDARD) buf.extend_from_slice(&1u32.to_le_bytes()); // iid (16 bytes; arbitrary) buf.extend_from_slice(&[ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, ]); // std_flags buf.extend_from_slice(&0u32.to_le_bytes()); // public_refs = 5 buf.extend_from_slice(&5u32.to_le_bytes()); // oxid buf.extend_from_slice(&0x1122_3344_5566_7788u64.to_le_bytes()); // oid buf.extend_from_slice(&0xAABB_CCDD_EEFF_0011u64.to_le_bytes()); // ipid buf.extend_from_slice(&[ 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, ]); // Build the dual-string array: // tower_id 0x0007 (ncacn_ip_tcp) // "AB" as UTF-16LE // 0x0000 terminator // That's 4 u16 code units = 8 bytes. let array_units: [u16; 4] = [0x0007, b'A' as u16, b'B' as u16, 0x0000]; let dual_entries: u16 = array_units.len() as u16; let security_offset: u16 = dual_entries; // no security bindings // dual_string_entries (count of u16 code units) buf.extend_from_slice(&dual_entries.to_le_bytes()); // dual_string_security_offset buf.extend_from_slice(&security_offset.to_le_bytes()); // header now exactly 68 bytes assert_eq!(buf.len(), 68); for unit in array_units { buf.extend_from_slice(&unit.to_le_bytes()); } buf } #[test] fn parse_minimal_objref() { let bytes = build_minimal_objref(); let parsed = ComObjRef::parse(&bytes).unwrap(); assert_eq!(parsed.signature, 0x574F_454D); assert_eq!(parsed.flags, 1); assert_eq!(parsed.standard_flags, 0); assert_eq!(parsed.public_refs, 5); assert_eq!(parsed.oxid, 0x1122_3344_5566_7788); assert_eq!(parsed.oid, 0xAABB_CCDD_EEFF_0011); assert_eq!(parsed.dual_string_entries, 4); assert_eq!(parsed.dual_string_security_offset, 4); assert_eq!(parsed.dual_string_entries_decoded.len(), 1); let entry = &parsed.dual_string_entries_decoded[0]; assert_eq!(entry.tower_id, 0x0007); assert_eq!(entry.protocol, "ncacn_ip_tcp"); assert_eq!(entry.value, "AB"); // entry_start (0) < security_offset (4) → string binding. assert!(!entry.is_security_binding); } #[test] fn diagnostic_lines_format_minimal() { let bytes = build_minimal_objref(); let parsed = ComObjRef::parse(&bytes).unwrap(); let lines = parsed.to_diagnostic_lines(); // Per ComObjRef.cs:42-55 there are exactly 11 lines. assert_eq!(lines.len(), 11); assert_eq!(lines[0], "objref_signature=0x574F454D"); assert_eq!(lines[1], "objref_flags=0x00000001"); assert_eq!(lines[3], "std_flags=0x00000000"); assert_eq!(lines[4], "std_public_refs=5"); assert_eq!(lines[5], "std_oxid=0x1122334455667788"); assert_eq!(lines[6], "std_oid=0xAABBCCDDEEFF0011"); assert_eq!(lines[8], "dual_string_entries=4"); assert_eq!(lines[9], "dual_string_security_offset=4"); assert_eq!(lines[10], "dual_strings=string:0x0007:ncacn_ip_tcp:AB"); } #[test] fn parse_rejects_short_buffer() { // 67-byte buffer (one shy of header) must error, not panic. let err = ComObjRef::parse(&[0u8; 67]).unwrap_err(); match err { RpcError::ShortRead { expected, actual } => { assert_eq!(expected, 68); assert_eq!(actual, 67); } other => panic!("expected ShortRead, got {other:?}"), } } #[test] fn parse_accepts_exact_header_no_array() { // 68 bytes with dual_string_entries=0 → no decoded entries. let mut buf = vec![0u8; 68]; // signature buf[0..4].copy_from_slice(&0x574F_454Du32.to_le_bytes()); let parsed = ComObjRef::parse(&buf).unwrap(); assert_eq!(parsed.dual_string_entries, 0); assert_eq!(parsed.dual_string_security_offset, 0); assert!(parsed.dual_string_entries_decoded.is_empty()); } #[test] fn protocol_tower_name_table() { // All 7 documented tower ids per ComObjRef.cs:106-117. assert_eq!(protocol_tower_name(0x0007), "ncacn_ip_tcp"); assert_eq!(protocol_tower_name(0x0008), "ncadg_ip_udp"); assert_eq!(protocol_tower_name(0x0009), "ncacn_np"); assert_eq!(protocol_tower_name(0x000f), "ncacn_spx"); assert_eq!(protocol_tower_name(0x0010), "ncacn_nb_nb"); assert_eq!(protocol_tower_name(0x0016), "ncadg_ip_udp_or_netbios"); assert_eq!(protocol_tower_name(0x001f), "ncalrpc"); // Fall-through. assert_eq!(protocol_tower_name(0x0000), "unknown"); assert_eq!(protocol_tower_name(0xFFFF), "unknown"); } #[test] fn dual_string_array_overrun_bounded() { // Build a header that claims 1000 dual-string code units but only // includes 4 bytes (= 2 code units) of trailing data. The parser // must bound itself via min(entries, data.len()/2) per // ComObjRef.cs:59 and not read past the end. let mut buf = build_minimal_objref(); // Truncate the trailing dual-string bytes back to 0 and lie about // entries=1000. buf.truncate(68); buf[DUAL_STRING_ENTRIES_OFFSET..DUAL_STRING_ENTRIES_OFFSET + 2] .copy_from_slice(&1000u16.to_le_bytes()); let parsed = ComObjRef::parse(&buf).unwrap(); // The wire-declared entry count is preserved verbatim per // CLAUDE.md unknown-bytes rule. assert_eq!(parsed.dual_string_entries, 1000); // But the loop bound prevents any decoding. assert!(parsed.dual_string_entries_decoded.is_empty()); } #[test] fn security_binding_flag_split() { // Build a dual-string array with one string binding then one security // binding. Layout (u16 code units): // [0] tower=0x0007 (string binding starts at index 0) // [1] 'A' // [2] 0x0000 terminator // [3] tower=0x0007 (security binding starts at index 3) // [4] 'B' // [5] 0x0000 terminator // dual_string_entries = 6, security_offset = 3 (entries with start // index >= 3 are security bindings). let mut buf = vec![0u8; 68]; buf[0..4].copy_from_slice(&0x574F_454Du32.to_le_bytes()); let entries: u16 = 6; let sec_off: u16 = 3; buf[DUAL_STRING_ENTRIES_OFFSET..DUAL_STRING_ENTRIES_OFFSET + 2] .copy_from_slice(&entries.to_le_bytes()); buf[DUAL_STRING_SECURITY_OFFSET_OFFSET..DUAL_STRING_SECURITY_OFFSET_OFFSET + 2] .copy_from_slice(&sec_off.to_le_bytes()); for unit in [0x0007u16, b'A' as u16, 0x0000, 0x0007, b'B' as u16, 0x0000] { buf.extend_from_slice(&unit.to_le_bytes()); } let parsed = ComObjRef::parse(&buf).unwrap(); assert_eq!(parsed.dual_string_entries_decoded.len(), 2); assert_eq!(parsed.dual_string_entries_decoded[0].value, "A"); assert!(!parsed.dual_string_entries_decoded[0].is_security_binding); assert_eq!(parsed.dual_string_entries_decoded[1].value, "B"); assert!(parsed.dual_string_entries_decoded[1].is_security_binding); } #[test] fn non_printable_codeunit_escaped_as_hex() { // tower=0x0007, then a non-printable u16 (0x0100), then 'a', then 0x0000. let mut buf = vec![0u8; 68]; buf[0..4].copy_from_slice(&0x574F_454Du32.to_le_bytes()); let entries: u16 = 4; buf[DUAL_STRING_ENTRIES_OFFSET..DUAL_STRING_ENTRIES_OFFSET + 2] .copy_from_slice(&entries.to_le_bytes()); buf[DUAL_STRING_SECURITY_OFFSET_OFFSET..DUAL_STRING_SECURITY_OFFSET_OFFSET + 2] .copy_from_slice(&entries.to_le_bytes()); for unit in [0x0007u16, 0x0100, b'a' as u16, 0x0000] { buf.extend_from_slice(&unit.to_le_bytes()); } let parsed = ComObjRef::parse(&buf).unwrap(); assert_eq!(parsed.dual_string_entries_decoded.len(), 1); // Expect "<0100>a" per the printable-ASCII escape rule // (ComObjRef.cs:82-91). assert_eq!(parsed.dual_string_entries_decoded[0].value, "<0100>a"); } /// Captured OBJREF from `captures/057-managed-callback-route-service-trace-saved/probe.stdout.txt:6` /// (`managed_callback_objref_hex`). 366 bytes; produced by the .NET /// managed-callback exporter via `MarshalIUnknownObjRef` in the live /// probe. Used to verify that the Rust parser interprets a real-world /// OBJREF identically to the .NET reference. const CAPTURED_OBJREF_HEX: &str = "4D454F5701000000F7929FB448C769418ECAA0670B012746800A000005000000750CC6C8BA9B1EFD3BF71E12FEE5B5C1022C0000DC32FFFF8AC67645E6ED23FF95007F0007004400450053004B0054004F0050002D0036004A004C0033004B004B004F0000000700310030002E003100300030002E0030002E0034003800000007003100370032002E00320039002E003200320034002E0031000000070066006400650031003A0061006500340031003A0038006100300030003A0034003500320061003A0062006200340031003A0065006500370065003A0035006600640034003A0064006300310038000000070066006400650031003A0061006500340031003A0038006100300030003A0034003500320061003A0035003000620031003A0038003400360066003A0037006200350031003A006500610034003000000000000900FFFF00001E00FFFF00001000FFFF00000A00FFFF00001600FFFF00001F00FFFF00000E00FFFF00000000"; fn hex_decode(hex: &str) -> Vec { let bytes = hex.as_bytes(); assert!(bytes.len() % 2 == 0); let mut out = Vec::with_capacity(bytes.len() / 2); for chunk in bytes.chunks(2) { let hi = (chunk[0] as char).to_digit(16).unwrap() as u8; let lo = (chunk[1] as char).to_digit(16).unwrap() as u8; out.push((hi << 4) | lo); } out } #[test] fn captured_objref_parses() { let bytes = hex_decode(CAPTURED_OBJREF_HEX); // probe.stdout.txt:5 reports managed_callback_objref_size=366. assert_eq!(bytes.len(), 366); let parsed = ComObjRef::parse(&bytes).unwrap(); // Signature is the canonical "MEOW". assert_eq!(parsed.signature, OBJREF_SIGNATURE); // OBJREF_STANDARD. assert_eq!(parsed.flags, 1); // public_refs = 5 (per the captured bytes 28..32 = 05 00 00 00). assert_eq!(parsed.public_refs, 5); // Captured bytes at offset 64..68 are `95 00 7F 00`: // dual_string_entries (u16 LE) = 0x0095 = 149 // dual_string_security_offset (u16 LE) = 0x007F = 127 // 149 u16 units from offset 68 onwards exactly fills the remaining // 366 - 68 = 298 bytes (149 * 2), so the entries count saturates the // buffer — confirming the parser's `min(entries, data.len()/2)` // bound at `ComObjRef.cs:59` produces the same effective walk length. assert_eq!(parsed.dual_string_entries, 0x0095); assert_eq!(parsed.dual_string_security_offset, 0x007F); // First decoded string-binding is the hostname over ncacn_ip_tcp. // The probe was run on host DESKTOP-6JL3KKO per the captured UTF-16 // bytes immediately following the dual-string header. let first = &parsed.dual_string_entries_decoded[0]; assert_eq!(first.tower_id, 0x0007); assert_eq!(first.protocol, "ncacn_ip_tcp"); assert_eq!(first.value, "DESKTOP-6JL3KKO"); assert!(!first.is_security_binding); // Subsequent string-bindings are the IPv4 + IPv6 endpoint addresses. // Confirm we got at least 4 string-bindings (host + v4 + 2x v6) plus // the security-binding entries. assert!(parsed.dual_string_entries_decoded.len() >= 5); // At least one entry must be a security binding (entries past // security_offset). The "0900FFFF" sequence in the captured bytes // decodes as tower=0x0009 (ncacn_np) with a single non-printable // u16 0xFFFF — appears in the security-binding tail. assert!( parsed .dual_string_entries_decoded .iter() .any(|e| e.is_security_binding), "expected at least one security binding in captured OBJREF" ); // The diagnostic emitter must produce exactly 11 lines. let lines = parsed.to_diagnostic_lines(); assert_eq!(lines.len(), 11); assert_eq!(lines[0], "objref_signature=0x574F454D"); } #[test] fn guid_display_matches_dotnet_d_format() { // .NET Guid("F7929FB4-48C7-6941-8ECA-A0670B012746".replace order): // The 16-byte sequence F7 92 9F B4 48 C7 69 41 8E CA A0 67 0B 01 27 46 // displays as "b49f92f7-c748-4169-8eca-a0670b012746" — first three // groups are byte-swapped (LE on wire, BE in display). let g = Guid([ 0xF7, 0x92, 0x9F, 0xB4, 0x48, 0xC7, 0x69, 0x41, 0x8E, 0xCA, 0xA0, 0x67, 0x0B, 0x01, 0x27, 0x46, ]); assert_eq!(format!("{}", g), "b49f92f7-c748-4169-8eca-a0670b012746"); } #[test] fn header_length_constant() { assert_eq!(ComObjRef::HEADER_LEN, 68); assert_eq!(OBJREF_HEADER_LEN, 68); assert_eq!(OBJREF_SIGNATURE, 0x574F_454D); } // ---- ComObjRefBuilder tests -------------------------------------- #[test] fn builder_emits_meow_header_and_flags() { let iid = Guid::new([0xAA; 16]); let ipid = Guid::new([0xBB; 16]); let buf = ComObjRefBuilder::create_standard_objref( iid, 0x280, 5, 0x0123_4567_89AB_CDEF, 0xFEDC_BA98_7654_3210, ipid, &["host[5985]"], ); assert!(buf.len() >= OBJREF_HEADER_LEN); // Signature assert_eq!(&buf[0..4], &OBJREF_SIGNATURE.to_le_bytes()); // flags = 1 (OBJREF_STANDARD) assert_eq!(&buf[4..8], &1u32.to_le_bytes()); // IID assert_eq!(&buf[8..24], iid.as_bytes()); // std_flags assert_eq!(&buf[24..28], &0x280u32.to_le_bytes()); // public_refs assert_eq!(&buf[28..32], &5u32.to_le_bytes()); // OXID/OID assert_eq!(&buf[32..40], &0x0123_4567_89AB_CDEFu64.to_le_bytes()); assert_eq!(&buf[40..48], &0xFEDC_BA98_7654_3210u64.to_le_bytes()); // IPID assert_eq!(&buf[48..64], ipid.as_bytes()); } #[test] fn builder_round_trips_through_parser() { // The emitted OBJREF must parse back through ComObjRef::parse with // the same key fields. let iid = Guid::new([0x11; 16]); let ipid = Guid::new([0x22; 16]); let buf = ComObjRefBuilder::create_standard_objref( iid, 0x280, 5, 0x1111_2222_3333_4444, 0x5555_6666_7777_8888, ipid, &["DESKTOP[12345]"], ); let parsed = ComObjRef::parse(&buf).unwrap(); assert_eq!(parsed.signature, OBJREF_SIGNATURE); assert_eq!(parsed.flags, 1); assert_eq!(parsed.iid, iid); assert_eq!(parsed.standard_flags, 0x280); assert_eq!(parsed.public_refs, 5); assert_eq!(parsed.oxid, 0x1111_2222_3333_4444); assert_eq!(parsed.oid, 0x5555_6666_7777_8888); assert_eq!(parsed.ipid, ipid); // First decoded entry should be the ncacn_ip_tcp string binding, // and at least one security binding (auth-service tail) follows. let first = &parsed.dual_string_entries_decoded[0]; assert_eq!(first.tower_id, 0x0007); assert_eq!(first.protocol, "ncacn_ip_tcp"); assert_eq!(first.value, "DESKTOP[12345]"); assert!(!first.is_security_binding); let security_count = parsed .dual_string_entries_decoded .iter() .filter(|e| e.is_security_binding) .count(); assert!( security_count >= 1, "expected at least one security binding, got {security_count}" ); } #[test] fn builder_security_offset_matches_dotnet_formula() { // security_offset = sum(1 + binding.len() + 1) + 1 (cs:348). // For one binding "host[12]" (8 chars): 1 + 8 + 1 + 1 = 11. let buf = ComObjRefBuilder::create_standard_objref( Guid::ZERO, 0, 5, 0, 0, Guid::ZERO, &["host[12]"], ); let security_offset = u16::from_le_bytes([buf[66], buf[67]]); assert_eq!(security_offset, 11); } #[test] fn builder_two_bindings_security_offset() { // Two bindings: "a[1]" (4) + "b[2]" (4): // (1+4+1) + (1+4+1) + 1 = 13. let buf = ComObjRefBuilder::create_standard_objref( Guid::ZERO, 0, 5, 0, 0, Guid::ZERO, &["a[1]", "b[2]"], ); let security_offset = u16::from_le_bytes([buf[66], buf[67]]); assert_eq!(security_offset, 13); } #[test] fn builder_emits_seven_security_entries() { // Each security entry contributes 3 u16 words [tower_id, 0xFFFF, 0]. // Total security words = 7 * 3 = 21, plus a trailing 0 = 22. // String section for one binding "h[1]" (4 chars): 1+4+1+1 = 7 words. // Total entries = 7 + 22 = 29. let buf = ComObjRefBuilder::create_standard_objref(Guid::ZERO, 0, 5, 0, 0, Guid::ZERO, &["h[1]"]); let entries = u16::from_le_bytes([buf[64], buf[65]]); assert_eq!(entries, 29); } #[test] fn builder_auth_services_table_matches_dotnet_order() { // The auth-service tower ids in the security tail must appear in the // order the .NET reference writes them (cs:362). assert_eq!( CALLBACK_OBJREF_AUTH_SERVICES, [0x0009, 0x001E, 0x0010, 0x000A, 0x0016, 0x001F, 0x000E] ); } #[test] fn builder_total_buffer_length_matches_words_count() { // entries * 2 + HEADER_LEN let buf = ComObjRefBuilder::create_standard_objref(Guid::ZERO, 0, 5, 0, 0, Guid::ZERO, &["x[1]"]); let entries = u16::from_le_bytes([buf[64], buf[65]]) as usize; assert_eq!(buf.len(), OBJREF_HEADER_LEN + entries * 2); } }