Skip to main content

ms_ndr/
lib.rs

1//! NDR (Network Data Representation) marshaling — the DCE/RPC Transfer Syntax, little-endian.
2//!
3//! The foundation every MS-RPCE interface rides on. Implements the pieces the
4//! SAMR/LSAT/EPM/DRSUAPI/WMI calls need: aligned primitives, conformant & varying
5//! arrays, unique (referent) pointers, and conformant-varying UTF-16 strings
6//! (the `RPC_UNICODE_STRING` payload shape).
7//!
8//! Alignment is relative to the start of the stub buffer, matching the NDR rule that an
9//! aggregate aligns to its largest member. This crate is transport- and interface-agnostic:
10//! it only turns Rust values into NDR bytes and back.
11
12/// The only way marshaling fails: the decoder ran off the end of the stub buffer.
13#[derive(Debug, thiserror::Error)]
14pub enum NdrError {
15    #[error("NDR buffer underrun: need {need} bytes at offset {pos}")]
16    Underrun { need: usize, pos: usize },
17}
18
19pub type Result<T> = std::result::Result<T, NdrError>;
20
21/// Serializer. Alignment is relative to the start of the stub buffer.
22#[derive(Default)]
23pub struct NdrEncoder {
24    buf: Vec<u8>,
25    next_referent: u32,
26}
27
28impl NdrEncoder {
29    pub fn new() -> Self {
30        NdrEncoder {
31            buf: Vec::new(),
32            next_referent: 0x0002_0000,
33        }
34    }
35
36    /// Pad with zero bytes up to an `a`-byte boundary. Public so callers can align a union
37    /// arm or struct to its natural alignment (NDR aligns an aggregate to its largest member).
38    pub fn align(&mut self, a: usize) {
39        if a <= 1 {
40            return;
41        }
42        while self.buf.len() % a != 0 {
43            self.buf.push(0);
44        }
45    }
46
47    pub fn u8(&mut self, v: u8) {
48        self.buf.push(v);
49    }
50    pub fn u16(&mut self, v: u16) {
51        self.align(2);
52        self.buf.extend_from_slice(&v.to_le_bytes());
53    }
54    pub fn u32(&mut self, v: u32) {
55        self.align(4);
56        self.buf.extend_from_slice(&v.to_le_bytes());
57    }
58    pub fn u64(&mut self, v: u64) {
59        self.align(8);
60        self.buf.extend_from_slice(&v.to_le_bytes());
61    }
62    pub fn bytes(&mut self, b: &[u8]) {
63        self.buf.extend_from_slice(b);
64    }
65    /// A UUID / GUID field (DCE wire layout, 16 bytes, aligned to 4).
66    pub fn uuid(&mut self, b: &[u8; 16]) {
67        self.align(4);
68        self.buf.extend_from_slice(b);
69    }
70
71    /// A non-null unique pointer: emit a fresh referent id. Caller emits the pointee next.
72    pub fn referent(&mut self) -> u32 {
73        let id = self.next_referent;
74        self.next_referent += 4;
75        self.u32(id);
76        id
77    }
78    pub fn null_ptr(&mut self) {
79        self.u32(0);
80    }
81
82    /// A conformant+varying wide string: max_count, offset(0), actual_count, then UTF-16LE
83    /// including the terminating NUL. Matches the `[size_is][length_is]` wchar payload.
84    pub fn conformant_varying_wstr(&mut self, s: &str) {
85        let mut units: Vec<u16> = s.encode_utf16().collect();
86        units.push(0);
87        let n = units.len() as u32;
88        self.u32(n); // max_count
89        self.u32(0); // offset
90        self.u32(n); // actual_count
91        for u in units {
92            self.u16(u);
93        }
94    }
95
96    pub fn into_bytes(self) -> Vec<u8> {
97        self.buf
98    }
99    pub fn len(&self) -> usize {
100        self.buf.len()
101    }
102    pub fn is_empty(&self) -> bool {
103        self.buf.is_empty()
104    }
105}
106
107/// Deserializer over a stub buffer.
108pub struct NdrDecoder<'a> {
109    buf: &'a [u8],
110    pos: usize,
111}
112
113impl<'a> NdrDecoder<'a> {
114    pub fn new(buf: &'a [u8]) -> Self {
115        NdrDecoder { buf, pos: 0 }
116    }
117
118    pub fn align(&mut self, a: usize) {
119        if a <= 1 {
120            return;
121        }
122        let remainder = self.pos % a;
123        if remainder != 0 {
124            self.pos = self.pos.saturating_add(a - remainder);
125        }
126    }
127    fn take(&mut self, n: usize) -> Result<&'a [u8]> {
128        let end = self.pos.checked_add(n).ok_or(NdrError::Underrun {
129            need: n,
130            pos: self.pos,
131        })?;
132        if end > self.buf.len() {
133            return Err(NdrError::Underrun {
134                need: n,
135                pos: self.pos,
136            });
137        }
138        let s = &self.buf[self.pos..end];
139        self.pos = end;
140        Ok(s)
141    }
142
143    pub fn u8(&mut self) -> Result<u8> {
144        Ok(self.take(1)?[0])
145    }
146    pub fn u16(&mut self) -> Result<u16> {
147        self.align(2);
148        Ok(u16::from_le_bytes(self.take(2)?.try_into().unwrap()))
149    }
150    pub fn u32(&mut self) -> Result<u32> {
151        self.align(4);
152        Ok(u32::from_le_bytes(self.take(4)?.try_into().unwrap()))
153    }
154    pub fn u64(&mut self) -> Result<u64> {
155        self.align(8);
156        Ok(u64::from_le_bytes(self.take(8)?.try_into().unwrap()))
157    }
158    pub fn uuid(&mut self) -> Result<[u8; 16]> {
159        self.align(4);
160        Ok(self.take(16)?.try_into().unwrap())
161    }
162
163    /// Read a conformant+varying wide string; returns it without the trailing NUL.
164    ///
165    /// # Bounded allocation
166    ///
167    /// The `actual_count` field is attacker-controlled (comes off the wire).
168    /// A naive `Vec::with_capacity(actual)` here would let a remote peer
169    /// trigger a multi-GB allocation before we notice the buffer is short.
170    /// This decoder preflights the size against the remaining buffer: if
171    /// `actual * 2` bytes aren't physically available, we return
172    /// `NdrError::Underrun` before touching the allocator.
173    pub fn conformant_varying_wstr(&mut self) -> Result<String> {
174        let max = self.u32()?;
175        let offset = self.u32()?;
176        let actual = self.u32()? as usize;
177        if offset > max || actual > (max - offset) as usize {
178            return Err(NdrError::Underrun {
179                need: actual,
180                pos: self.pos,
181            });
182        }
183        // Bounded preflight: refuse to allocate for more bytes than the
184        // buffer can supply. Also guards against overflow on 32-bit hosts.
185        let need = actual.checked_mul(2).ok_or(NdrError::Underrun {
186            need: usize::MAX,
187            pos: self.pos,
188        })?;
189        if need > self.remaining() {
190            return Err(NdrError::Underrun {
191                need,
192                pos: self.pos,
193            });
194        }
195        let mut units = Vec::with_capacity(actual);
196        for _ in 0..actual {
197            units.push(self.u16()?);
198        }
199        while units.last() == Some(&0) {
200            units.pop();
201        }
202        Ok(String::from_utf16_lossy(&units))
203    }
204
205    /// Read `n` raw bytes with no alignment (e.g. a fixed context handle or tower blob).
206    pub fn read_bytes(&mut self, n: usize) -> Result<&'a [u8]> {
207        self.take(n)
208    }
209
210    pub fn remaining(&self) -> usize {
211        self.buf.len().saturating_sub(self.pos)
212    }
213    pub fn position(&self) -> usize {
214        self.pos
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn primitive_alignment() {
224        let mut e = NdrEncoder::new();
225        e.u8(0x01);
226        e.u32(0xAABB_CCDD); // must align to 4 → 3 pad bytes
227        assert_eq!(e.into_bytes(), vec![0x01, 0, 0, 0, 0xDD, 0xCC, 0xBB, 0xAA]);
228    }
229
230    #[test]
231    fn wstr_roundtrip() {
232        let mut e = NdrEncoder::new();
233        e.conformant_varying_wstr("ADHAMMER");
234        let bytes = e.into_bytes();
235        // max/offset/actual each 4 bytes = 12, then 9 u16 (8 chars + NUL) = 18 → 30 bytes.
236        assert_eq!(bytes.len(), 30);
237        let mut d = NdrDecoder::new(&bytes);
238        assert_eq!(d.conformant_varying_wstr().unwrap(), "ADHAMMER");
239        assert_eq!(d.remaining(), 0);
240    }
241
242    #[test]
243    fn referent_ids_are_nonzero_and_advance() {
244        let mut e = NdrEncoder::new();
245        let a = e.referent();
246        let b = e.referent();
247        assert_ne!(a, 0);
248        assert_eq!(b, a + 4);
249    }
250
251    #[test]
252    fn underrun_reports_offset() {
253        let bytes = [0x01, 0x02]; // only 2 bytes
254        let mut d = NdrDecoder::new(&bytes);
255        assert!(matches!(
256            d.u32(),
257            Err(NdrError::Underrun { need: 4, pos: 0 })
258        ));
259    }
260
261    /// Regression: a hostile wire response with `actual_count = 0x7FFF_FFFF`
262    /// used to reach `Vec::with_capacity(2_147_483_647)` — a multi-GB
263    /// allocation for zero payload bytes. The bounded preflight in
264    /// `conformant_varying_wstr` must reject this in microseconds, not
265    /// after touching the allocator.
266    #[test]
267    fn wstr_rejects_hostile_actual_count() {
268        let mut bytes = Vec::new();
269        bytes.extend_from_slice(&0u32.to_le_bytes()); // max
270        bytes.extend_from_slice(&0u32.to_le_bytes()); // offset
271        bytes.extend_from_slice(&0x7FFF_FFFFu32.to_le_bytes()); // actual (hostile)
272                                                                // no wchar payload follows
273        let mut d = NdrDecoder::new(&bytes);
274        let start = std::time::Instant::now();
275        let r = d.conformant_varying_wstr();
276        let elapsed_ms = start.elapsed().as_millis();
277        assert!(
278            matches!(r, Err(NdrError::Underrun { .. })),
279            "expected Underrun, got {r:?}"
280        );
281        assert!(
282            elapsed_ms < 50,
283            "preflight must reject in <50ms, took {elapsed_ms}ms — probably allocated"
284        );
285    }
286
287    /// Boundary: `actual_count` that would overflow `usize::checked_mul(2)`
288    /// on 32-bit targets. On 64-bit it's just a very large `need`; both
289    /// must return Underrun cleanly, never panic.
290    #[test]
291    fn wstr_rejects_actual_count_overflow() {
292        let mut bytes = Vec::new();
293        bytes.extend_from_slice(&0u32.to_le_bytes());
294        bytes.extend_from_slice(&0u32.to_le_bytes());
295        bytes.extend_from_slice(&0xFFFF_FFFFu32.to_le_bytes()); // max u32
296        let mut d = NdrDecoder::new(&bytes);
297        assert!(matches!(
298            d.conformant_varying_wstr(),
299            Err(NdrError::Underrun { .. })
300        ));
301    }
302
303    #[test]
304    fn zero_alignment_is_a_noop() {
305        let mut e = NdrEncoder::new();
306        e.u8(1);
307        e.align(0);
308        assert_eq!(e.into_bytes(), vec![1]);
309
310        let bytes = [1u8];
311        let mut d = NdrDecoder::new(&bytes);
312        d.align(0);
313        assert_eq!(d.u8().unwrap(), 1);
314    }
315
316    #[test]
317    fn alignment_past_end_reports_underrun() {
318        let bytes = [1u8];
319        let mut d = NdrDecoder::new(&bytes);
320        assert_eq!(d.u8().unwrap(), 1);
321        d.align(4);
322        assert_eq!(d.remaining(), 0);
323        assert!(matches!(
324            d.u32(),
325            Err(NdrError::Underrun { need: 4, pos: 4 })
326        ));
327    }
328
329    #[test]
330    fn wstr_rejects_inconsistent_varying_bounds() {
331        let mut bytes = Vec::new();
332        bytes.extend_from_slice(&2u32.to_le_bytes()); // max
333        bytes.extend_from_slice(&2u32.to_le_bytes()); // offset == max
334        bytes.extend_from_slice(&1u32.to_le_bytes()); // actual exceeds max - offset
335        bytes.extend_from_slice(&0u16.to_le_bytes());
336        let mut d = NdrDecoder::new(&bytes);
337        assert!(d.conformant_varying_wstr().is_err());
338    }
339}