Skip to main content

rustsmb/
srvsvc.rs

1//! srvsvc DCERPC endpoint over named pipes ([MS-RPCE] + [MS-SRVS]).
2//!
3//! Handles pipe open/close, WRITE/READ of DCERPC PDUs, and
4//! NetShareEnumAll (opnum 15) responses for `smbclient -L`.
5
6use std::collections::VecDeque;
7
8use ms_ndr::NdrEncoder;
9
10/// One advertised share for NetShareEnum level 1.
11#[derive(Debug, Clone)]
12pub struct ShareInfo {
13    /// Share name (`NetName`).
14    pub netname: String,
15    /// Share type (`STYPE_*`: disktree, IPC, …).
16    pub shi_type: u32,
17    /// Human-readable share comment.
18    pub remark: String,
19}
20
21/// A virtual named pipe bound to one open file id.
22pub struct Pipe {
23    /// Lowercased pipe name (e.g. `srvsvc`).
24    pub name: String,
25    inbound: Vec<u8>,
26    outbound: VecDeque<Vec<u8>>,
27}
28
29impl Pipe {
30    /// Open a fresh, empty pipe with the given name.
31    pub fn new(name: &str) -> Self {
32        Self { name: name.to_lowercase(), inbound: Vec::new(), outbound: VecDeque::new() }
33    }
34
35    /// Bytes queued in the next outbound DCERPC fragment.
36    pub fn pending(&self) -> usize {
37        self.outbound.front().map_or(0, |v| v.len())
38    }
39
40    /// Dequeue up to `max` bytes of the next outbound fragment.
41    pub fn take(&mut self, max: usize) -> Vec<u8> {
42        match self.outbound.front_mut() {
43            Some(msg) => {
44                let n = msg.len().min(max);
45                let out = msg.drain(..n).collect();
46                if msg.is_empty() { self.outbound.pop_front(); }
47                out
48            }
49            None => Vec::new(),
50        }
51    }
52
53    /// Feed inbound DCERPC bytes, dispatching any complete request PDUs
54    /// (BIND, and NetShareEnumAll opnum 15) and queueing their responses.
55    #[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // DCERPC/NDR wire layout
56    pub fn on_write(&mut self, data: &[u8], shares: &[ShareInfo]) {
57        self.inbound.extend_from_slice(data);
58        loop {
59            if self.inbound.len() < 16 { break; }
60            let frag_len = u16::from_le_bytes([self.inbound[8], self.inbound[9]]) as usize;
61            if self.inbound.len() < frag_len { break; }
62            let pdu: Vec<u8> = self.inbound.drain(..frag_len).collect();
63            let ptype = pdu[2];
64            let call_id = u32::from_le_bytes([pdu[12], pdu[13], pdu[14], pdu[15]]);
65            let reply = match ptype {
66                11 => Some(build_bind_ack(call_id)),
67                14 => None,
68                0 if pdu.len() >= 24 => {
69                    let opnum = u16::from_le_bytes([pdu[22], pdu[23]]);
70                    tracing::debug!(opnum, "rpc request");
71                    let body = match opnum {
72                        15 => netshareenum_stub(shares),
73                        _ => build_fault(call_id),
74                    };
75                    Some(response_pdu(call_id, &body))
76                }
77                _ => None,
78            };
79            if let Some(r) = reply {
80                self.outbound.push_back(r);
81            }
82        }
83    }
84}
85
86#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // DCERPC PDU header ([MS-RPCE])
87fn pdu_header(ptype: u8, call_id: u32, body: &[u8]) -> Vec<u8> {
88    let mut p = Vec::with_capacity(16 + body.len());
89    p.extend_from_slice(&[5, 0]);
90    p.push(ptype);
91    p.push(0x03);
92    p.extend_from_slice(&0x0000_0010u32.to_le_bytes());
93    p.extend_from_slice(&((body.len() + 16) as u16).to_le_bytes());
94    p.extend_from_slice(&0u16.to_le_bytes());
95    p.extend_from_slice(&call_id.to_le_bytes());
96    p.extend_from_slice(body);
97    p
98}
99
100#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // DCERPC response PDU
101fn response_pdu(call_id: u32, stub: &[u8]) -> Vec<u8> {
102    let mut body = Vec::with_capacity(8 + stub.len());
103    body.extend_from_slice(&(stub.len() as u32).to_le_bytes());
104    body.extend_from_slice(&0u16.to_le_bytes());
105    body.push(0); body.push(0);
106    body.extend_from_slice(stub);
107    pdu_header(2, call_id, &body)
108}
109
110#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // DCERPC fault PDU
111fn build_fault(call_id: u32) -> Vec<u8> {
112    let mut body = vec![0u8; 4];
113    body.extend_from_slice(&0u16.to_le_bytes());
114    body.push(0); body.push(0);
115    body.extend_from_slice(&0x1C01_0006u32.to_le_bytes());
116    pdu_header(3, call_id, &body)
117}
118
119#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // DCERPC bind_ack PDU
120fn build_bind_ack(call_id: u32) -> Vec<u8> {
121    let sec_addr = b"\\pipe\\srvsvc\0";
122    let mut b = Vec::new();
123    b.extend_from_slice(&0x10b8u16.to_le_bytes());
124    b.extend_from_slice(&0x10b8u16.to_le_bytes());
125    b.extend_from_slice(&0x12345678u32.to_le_bytes());
126    b.extend_from_slice(&(sec_addr.len() as u16).to_le_bytes());
127    b.extend_from_slice(sec_addr);
128    while b.len() % 4 != 0 { b.push(0); }
129    b.extend_from_slice(&1u16.to_le_bytes());
130    b.push(0); b.push(0);
131    b.extend_from_slice(&0u16.to_le_bytes()); // result
132    b.extend_from_slice(&0u16.to_le_bytes()); // reason
133    b.extend_from_slice(&[
134        0x04, 0x5d, 0x88, 0x8a, 0xeb, 0x1c, 0xc9, 0x11,
135        0x9f, 0xe8, 0x08, 0x00, 0x2b, 0x10, 0x48, 0x60,
136    ]);
137    b.extend_from_slice(&2u32.to_le_bytes());
138    pdu_header(12, call_id, &b)
139}
140
141/// Build the NDR stub for a NetShareEnumAll level-1 response
142/// ([MS-SRVS] §3.1.4.8, §2.2.4.33) marshalled per [MS-RPCE] NDR rules.
143///
144/// The `SHARE_INFO_1_CONTAINER` union arm is emitted in full — the array of
145/// `SHARE_INFO_1` fixed parts (netname pointer, type, remark pointer) first,
146/// then every pointee string deferred in pointer-appearance order — before the
147/// trailing `TotalEntries`, `ResumeHandle` and return-code out-parameters.
148/// Deferring the strings (rather than inlining each after its referent id) is
149/// the NDR rule for pointers embedded in a conformant array; the `ms-ndr`
150/// primitive layer handles referent ids, 4-byte alignment and the
151/// `[size_is][length_is]` wchar payload.
152pub fn netshareenum_stub(shares: &[ShareInfo]) -> Vec<u8> {
153    let n = shares.len() as u32;
154    let mut e = NdrEncoder::new();
155
156    e.u32(1); // Level = 1
157    e.u32(1); // SHARE_INFO union switch (container 1)
158    e.referent(); // Ctr pointer
159    e.u32(n); // Container.EntriesRead
160    e.referent(); // Container.Buffer array pointer
161    e.u32(n); // conformant array MaxCount
162
163    // SHARE_INFO_1 fixed parts: netname ptr, type, remark ptr.
164    for sh in shares {
165        e.referent(); // netname pointer
166        e.u32(sh.shi_type);
167        e.referent(); // remark pointer
168    }
169
170    // Deferred string data, in the order the pointers appeared.
171    for sh in shares {
172        e.conformant_varying_wstr(&sh.netname);
173        e.conformant_varying_wstr(&sh.remark);
174    }
175
176    e.u32(n); // TotalEntries
177    e.referent(); // ResumeHandle pointer
178    e.u32(0); // ResumeHandle value
179    e.u32(0); // return WERROR = ERROR_SUCCESS
180    e.into_bytes()
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use ms_ndr::NdrDecoder;
187
188    fn reference_shares() -> Vec<ShareInfo> {
189        vec![
190            ShareInfo { netname: "public".into(), shi_type: 0, remark: String::new() },
191            ShareInfo {
192                netname: "IPC$".into(),
193                shi_type: 0x8000_0003,
194                remark: "IPC Service (ref)".into(),
195            },
196        ]
197    }
198
199    /// Decode our own stub back through the NDR reader and assert every field
200    /// round-trips. This validates the [MS-SRVS] `SHARE_ENUM_STRUCT` layout
201    /// (level, union switch, container, deferred strings, out-parameters)
202    /// structurally rather than pinning it to one server's referent ids.
203    fn decode_and_check(shares: &[ShareInfo]) {
204        let stub = netshareenum_stub(shares);
205        assert_eq!(stub.len() % 4, 0, "stub stays 4-byte aligned");
206
207        let n = shares.len() as u32;
208        let mut d = NdrDecoder::new(&stub);
209        assert_eq!(d.u32().unwrap(), 1, "Level");
210        assert_eq!(d.u32().unwrap(), 1, "union switch");
211        assert_ne!(d.u32().unwrap(), 0, "container pointer non-null");
212        assert_eq!(d.u32().unwrap(), n, "EntriesRead");
213        assert_ne!(d.u32().unwrap(), 0, "buffer pointer non-null");
214        assert_eq!(d.u32().unwrap(), n, "array MaxCount");
215
216        let mut types = Vec::new();
217        for _ in 0..n {
218            assert_ne!(d.u32().unwrap(), 0, "netname pointer non-null");
219            types.push(d.u32().unwrap());
220            assert_ne!(d.u32().unwrap(), 0, "remark pointer non-null");
221        }
222
223        let mut decoded = Vec::new();
224        for _ in 0..n {
225            let netname = d.conformant_varying_wstr().unwrap();
226            let remark = d.conformant_varying_wstr().unwrap();
227            decoded.push((netname, remark));
228        }
229
230        assert_eq!(d.u32().unwrap(), n, "TotalEntries");
231        assert_ne!(d.u32().unwrap(), 0, "resume-handle pointer non-null");
232        assert_eq!(d.u32().unwrap(), 0, "resume-handle value");
233        assert_eq!(d.u32().unwrap(), 0, "return WERROR = ERROR_SUCCESS");
234        assert_eq!(d.remaining(), 0, "no trailing bytes");
235
236        for (i, sh) in shares.iter().enumerate() {
237            assert_eq!(types[i], sh.shi_type, "share {i} type");
238            assert_eq!(decoded[i].0, sh.netname, "share {i} netname");
239            assert_eq!(decoded[i].1, sh.remark, "share {i} remark");
240        }
241    }
242
243    #[test]
244    fn netshareenum_round_trips_reference_shares() {
245        decode_and_check(&reference_shares());
246    }
247
248    #[test]
249    fn netshareenum_scales_to_more_shares() {
250        let shares = vec![
251            ShareInfo { netname: "alpha".into(), shi_type: 0, remark: "one".into() },
252            ShareInfo { netname: "beta".into(), shi_type: 0, remark: "two".into() },
253            ShareInfo { netname: "IPC$".into(), shi_type: 0x8000_0003, remark: String::new() },
254        ];
255        decode_and_check(&shares);
256
257        // Every embedded pointer must carry a distinct, non-zero referent id.
258        let stub = netshareenum_stub(&shares);
259        let mut refs = vec![
260            u32::from_le_bytes([stub[8], stub[9], stub[10], stub[11]]),
261            u32::from_le_bytes([stub[16], stub[17], stub[18], stub[19]]),
262        ];
263        for i in 0..shares.len() {
264            let base = 24 + i * 12;
265            refs.push(u32::from_le_bytes([stub[base], stub[base + 1], stub[base + 2], stub[base + 3]]));
266            refs.push(u32::from_le_bytes([stub[base + 8], stub[base + 9], stub[base + 10], stub[base + 11]]));
267        }
268        let mut unique = refs.clone();
269        unique.sort_unstable();
270        unique.dedup();
271        assert_eq!(unique.len(), refs.len(), "referent ids must be unique");
272    }
273}