Skip to main content

smb2_client/
msg.rs

1//! SMB2 request bodies and response parsers (MS-SMB2 §2.2). Offsets in the on-wire
2//! `*Offset` fields are measured from the start of the SMB2 header (i.e. `64 + body_off`).
3
4use crate::{Result, SmbError};
5
6fn utf16le(s: &str) -> Vec<u8> {
7    s.encode_utf16().flat_map(u16::to_le_bytes).collect()
8}
9fn u16(b: &[u8], o: usize) -> u16 {
10    u16::from_le_bytes([b[o], b[o + 1]])
11}
12fn u32(b: &[u8], o: usize) -> u32 {
13    u32::from_le_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]])
14}
15
16// ---- NEGOTIATE (§2.2.3) ---------------------------------------------------
17
18/// Offer dialect 2.1.0 with a random client GUID.
19pub fn negotiate(client_guid: &[u8; 16]) -> Vec<u8> {
20    // Offer SMB 2.0.2 (Server 2008/R2) and 2.1.0. The server picks the highest it supports and
21    // negotiates *down*, so this reaches 2008 through 2025 (2012/2016/2019/2022/2025 all accept
22    // 2.1.0 — validated live against Server 2025). Both sign with HMAC-SHA256.
23    //
24    // SMB 3.0.x (AES-CMAC) support exists in header.rs (sign_v3 / kdf_signing_key) and the
25    // client branches on the negotiated dialect, but 3.x is not offered yet — it's only needed
26    // for servers hardened to refuse SMB2 entirely, and the CMAC path isn't validated.
27    let dialects: [u16; 2] = [0x0202, 0x0210];
28    let mut b = Vec::new();
29    b.extend_from_slice(&36u16.to_le_bytes()); // StructureSize
30    b.extend_from_slice(&(dialects.len() as u16).to_le_bytes()); // DialectCount
31    b.extend_from_slice(&0x0001u16.to_le_bytes()); // SecurityMode = SIGNING_ENABLED
32    b.extend_from_slice(&0u16.to_le_bytes()); // Reserved
33    b.extend_from_slice(&0u32.to_le_bytes()); // Capabilities
34    b.extend_from_slice(client_guid);
35    b.extend_from_slice(&0u64.to_le_bytes()); // ClientStartTime
36    for dialect in dialects {
37        b.extend_from_slice(&dialect.to_le_bytes());
38    }
39    b
40}
41
42// ---- SESSION_SETUP (§2.2.5 / §2.2.6) --------------------------------------
43
44/// The security buffer holds a raw NTLMSSP token.
45pub fn session_setup(token: &[u8]) -> Vec<u8> {
46    let mut b = Vec::new();
47    b.extend_from_slice(&25u16.to_le_bytes()); // StructureSize
48    b.push(0); // Flags
49    b.push(0x01); // SecurityMode = SIGNING_ENABLED
50    b.extend_from_slice(&0u32.to_le_bytes()); // Capabilities
51    b.extend_from_slice(&0u32.to_le_bytes()); // Channel
52    let sec_off = 64u16 + 24; // header + fixed part
53    b.extend_from_slice(&sec_off.to_le_bytes()); // SecurityBufferOffset
54    b.extend_from_slice(&(token.len() as u16).to_le_bytes()); // SecurityBufferLength
55    b.extend_from_slice(&0u64.to_le_bytes()); // PreviousSessionId
56    b.extend_from_slice(token);
57    b
58}
59
60/// Extract the security buffer (server NTLM token) from a SESSION_SETUP response.
61pub fn session_setup_token(msg: &[u8]) -> Result<Vec<u8>> {
62    // body starts at 64; StructureSize(2), SessionFlags(2), SecBufOffset(2), SecBufLength(2)
63    let body = msg.get(64..).ok_or(SmbError::Truncated)?;
64    let off = u16(body, 4) as usize; // from SMB header start
65    let len = u16(body, 6) as usize;
66    msg.get(off..off + len)
67        .map(|s| s.to_vec())
68        .ok_or(SmbError::Truncated)
69}
70
71// ---- TREE_CONNECT (§2.2.9) ------------------------------------------------
72
73pub fn tree_connect(path: &str) -> Vec<u8> {
74    let name = utf16le(path);
75    let mut b = Vec::new();
76    b.extend_from_slice(&9u16.to_le_bytes()); // StructureSize
77    b.extend_from_slice(&0u16.to_le_bytes()); // Reserved/Flags
78    let path_off = 64u16 + 8;
79    b.extend_from_slice(&path_off.to_le_bytes()); // PathOffset
80    b.extend_from_slice(&(name.len() as u16).to_le_bytes()); // PathLength
81    b.extend_from_slice(&name);
82    b
83}
84
85// ---- CREATE (§2.2.13 / §2.2.14) -------------------------------------------
86
87/// Open a named pipe (e.g. "samr") on the IPC$ tree.
88pub fn create_pipe(name: &str) -> Vec<u8> {
89    let n = utf16le(name);
90    let mut b = Vec::new();
91    b.extend_from_slice(&57u16.to_le_bytes()); // StructureSize
92    b.push(0); // SecurityFlags
93    b.push(0); // RequestedOplockLevel
94    b.extend_from_slice(&2u32.to_le_bytes()); // ImpersonationLevel = Impersonation
95    b.extend_from_slice(&0u64.to_le_bytes()); // SmbCreateFlags
96    b.extend_from_slice(&0u64.to_le_bytes()); // Reserved
97    b.extend_from_slice(&0x0012_019Fu32.to_le_bytes()); // DesiredAccess: read+write data/EA/attrs (WRITE needs FILE_WRITE_DATA for a fire-and-forget AUTH3)
98    b.extend_from_slice(&0u32.to_le_bytes()); // FileAttributes
99    b.extend_from_slice(&0x0000_0007u32.to_le_bytes()); // ShareAccess = R|W|D
100    b.extend_from_slice(&0x0000_0001u32.to_le_bytes()); // CreateDisposition = OPEN
101    b.extend_from_slice(&0u32.to_le_bytes()); // CreateOptions
102    let name_off = 64u16 + 56;
103    b.extend_from_slice(&name_off.to_le_bytes()); // NameOffset
104    b.extend_from_slice(&(n.len() as u16).to_le_bytes()); // NameLength
105    b.extend_from_slice(&0u32.to_le_bytes()); // CreateContextsOffset
106    b.extend_from_slice(&0u32.to_le_bytes()); // CreateContextsLength
107    b.extend_from_slice(&n);
108    b
109}
110
111/// Generic disk-file CREATE (§2.2.13). `path` is relative to the connected share root (no
112/// leading backslash). Callers pass the access mask, share mode, disposition, and options.
113pub fn create_file(path: &str, access: u32, share: u32, disposition: u32, options: u32) -> Vec<u8> {
114    let n = utf16le(path);
115    let mut b = Vec::new();
116    b.extend_from_slice(&57u16.to_le_bytes()); // StructureSize
117    b.push(0); // SecurityFlags
118    b.push(0); // RequestedOplockLevel
119    b.extend_from_slice(&2u32.to_le_bytes()); // ImpersonationLevel = Impersonation
120    b.extend_from_slice(&0u64.to_le_bytes()); // SmbCreateFlags
121    b.extend_from_slice(&0u64.to_le_bytes()); // Reserved
122    b.extend_from_slice(&access.to_le_bytes()); // DesiredAccess
123    b.extend_from_slice(&0u32.to_le_bytes()); // FileAttributes (ignored on OPEN)
124    b.extend_from_slice(&share.to_le_bytes()); // ShareAccess
125    b.extend_from_slice(&disposition.to_le_bytes()); // CreateDisposition
126    b.extend_from_slice(&options.to_le_bytes()); // CreateOptions
127    let name_off = 64u16 + 56;
128    b.extend_from_slice(&name_off.to_le_bytes()); // NameOffset
129    b.extend_from_slice(&(n.len() as u16).to_le_bytes()); // NameLength
130    b.extend_from_slice(&0u32.to_le_bytes()); // CreateContextsOffset
131    b.extend_from_slice(&0u32.to_le_bytes()); // CreateContextsLength
132    b.extend_from_slice(&n);
133    b
134}
135
136/// SMB2 READ (§2.2.19): read `length` bytes at `offset` from the open file.
137pub fn read_req(file_id: &[u8; 16], offset: u64, length: u32) -> Vec<u8> {
138    let mut b = Vec::new();
139    b.extend_from_slice(&49u16.to_le_bytes()); // StructureSize
140    b.push(0); // Padding
141    b.push(0); // Flags
142    b.extend_from_slice(&length.to_le_bytes()); // Length
143    b.extend_from_slice(&offset.to_le_bytes()); // Offset
144    b.extend_from_slice(file_id);
145    b.extend_from_slice(&0u32.to_le_bytes()); // MinimumCount
146    b.extend_from_slice(&0u32.to_le_bytes()); // Channel
147    b.extend_from_slice(&0u32.to_le_bytes()); // RemainingBytes
148    b.extend_from_slice(&0u16.to_le_bytes()); // ReadChannelInfoOffset
149    b.extend_from_slice(&0u16.to_le_bytes()); // ReadChannelInfoLength
150    b.push(0); // Buffer (min 1 byte)
151    b
152}
153
154/// Extract the data returned by a READ response (§2.2.20).
155pub fn read_output(msg: &[u8]) -> Result<Vec<u8>> {
156    let body = msg.get(64..).ok_or(SmbError::Truncated)?;
157    let data_off = *body.get(2).ok_or(SmbError::Truncated)? as usize; // DataOffset, from header start
158    let data_len = u32(body, 4) as usize;
159    msg.get(data_off..data_off + data_len)
160        .map(|s| s.to_vec())
161        .ok_or(SmbError::Truncated)
162}
163
164/// SMB2 WRITE (§2.2.21): write `data` to the open handle at `offset`.
165pub fn write_req(file_id: &[u8; 16], offset: u64, data: &[u8]) -> Vec<u8> {
166    let mut b = Vec::new();
167    b.extend_from_slice(&49u16.to_le_bytes()); // StructureSize
168    b.extend_from_slice(&(64u16 + 48).to_le_bytes()); // DataOffset (header + 48-byte body)
169    b.extend_from_slice(&(data.len() as u32).to_le_bytes()); // Length
170    b.extend_from_slice(&offset.to_le_bytes()); // Offset
171    b.extend_from_slice(file_id);
172    b.extend_from_slice(&0u32.to_le_bytes()); // Channel
173    b.extend_from_slice(&0u32.to_le_bytes()); // RemainingBytes
174    b.extend_from_slice(&0u16.to_le_bytes()); // WriteChannelInfoOffset
175    b.extend_from_slice(&0u16.to_le_bytes()); // WriteChannelInfoLength
176    b.extend_from_slice(&0u32.to_le_bytes()); // Flags
177    b.extend_from_slice(data);
178    b
179}
180
181/// SMB2 CLOSE (§2.2.15).
182pub fn close_req(file_id: &[u8; 16]) -> Vec<u8> {
183    let mut b = Vec::new();
184    b.extend_from_slice(&24u16.to_le_bytes()); // StructureSize
185    b.extend_from_slice(&0u16.to_le_bytes()); // Flags
186    b.extend_from_slice(&0u32.to_le_bytes()); // Reserved
187    b.extend_from_slice(file_id);
188    b
189}
190
191/// FileId (16 bytes) from a CREATE response.
192pub fn create_file_id(msg: &[u8]) -> Result<[u8; 16]> {
193    // FileId sits at body offset 64 → absolute 128.
194    msg.get(128..144)
195        .map(|s| s.try_into().unwrap())
196        .ok_or(SmbError::Truncated)
197}
198
199// ---- IOCTL (§2.2.31 / §2.2.32) --------------------------------------------
200
201pub const FSCTL_PIPE_TRANSCEIVE: u32 = 0x0011_C017;
202
203/// Send `input` through the pipe and read the response in one round trip.
204pub fn ioctl_transceive(file_id: &[u8; 16], input: &[u8]) -> Vec<u8> {
205    let mut b = Vec::new();
206    b.extend_from_slice(&57u16.to_le_bytes()); // StructureSize
207    b.extend_from_slice(&0u16.to_le_bytes()); // Reserved
208    b.extend_from_slice(&FSCTL_PIPE_TRANSCEIVE.to_le_bytes()); // CtlCode
209    b.extend_from_slice(file_id);
210    let input_off = 64u32 + 56;
211    b.extend_from_slice(&input_off.to_le_bytes()); // InputOffset
212    b.extend_from_slice(&(input.len() as u32).to_le_bytes()); // InputCount
213    b.extend_from_slice(&0u32.to_le_bytes()); // MaxInputResponse
214    b.extend_from_slice(&input_off.to_le_bytes()); // OutputOffset
215    b.extend_from_slice(&0u32.to_le_bytes()); // OutputCount
216    b.extend_from_slice(&0x0001_0000u32.to_le_bytes()); // MaxOutputResponse (64 KiB — SMB2.1 max transact)
217    b.extend_from_slice(&0x0000_0001u32.to_le_bytes()); // Flags = IS_FSCTL
218    b.extend_from_slice(&0u32.to_le_bytes()); // Reserved2
219    b.extend_from_slice(input);
220    b
221}
222
223/// Extract the pipe output (RPC response bytes) from an IOCTL response.
224pub fn ioctl_output(msg: &[u8]) -> Result<Vec<u8>> {
225    // response body: StructureSize(2) Reserved(2) CtlCode(4) FileId(16)
226    // InputOffset(4) InputCount(4) OutputOffset(4) OutputCount(4) ...
227    let body = msg.get(64..).ok_or(SmbError::Truncated)?;
228    let out_off = u32(body, 32) as usize; // from SMB header start
229    let out_len = u32(body, 36) as usize;
230    msg.get(out_off..out_off + out_len)
231        .map(|s| s.to_vec())
232        .ok_or(SmbError::Truncated)
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    #[test]
240    fn negotiate_offers_dialect_210() {
241        let b = negotiate(&[0; 16]);
242        assert_eq!(u16(&b, 0), 36); // StructureSize
243        assert_eq!(u16(&b, 2), 2); // DialectCount (2.0.2 + 2.1.0)
244                                   // dialects at 36 (fixed part) — after 4+2+2+4+16+8 = 36
245        assert_eq!(u16(&b, 36), 0x0202);
246        assert_eq!(u16(&b, 38), 0x0210);
247    }
248
249    #[test]
250    fn create_pipe_name_offset_correct() {
251        let b = create_pipe("samr");
252        assert_eq!(u16(&b, 0), 57);
253        assert_eq!(u16(&b, 44), 64 + 56); // NameOffset field
254        assert_eq!(u16(&b, 46), 8); // "samr" = 4 wchar * 2
255    }
256
257    #[test]
258    fn ioctl_uses_transceive_ctlcode() {
259        let b = ioctl_transceive(&[0; 16], &[1, 2, 3]);
260        assert_eq!(u32(&b, 4), FSCTL_PIPE_TRANSCEIVE);
261        assert_eq!(u32(&b, 28), 3); // InputCount
262    }
263
264    #[test]
265    fn create_file_carries_access_and_options() {
266        let b = create_file("Windows\\Temp\\x.out", 0x0013_0081, 0x7, 1, 0x1060);
267        assert_eq!(u16(&b, 0), 57); // StructureSize
268        assert_eq!(u32(&b, 24), 0x0013_0081); // DesiredAccess
269        assert_eq!(u32(&b, 32), 0x7); // ShareAccess
270        assert_eq!(u32(&b, 36), 1); // CreateDisposition = FILE_OPEN
271        assert_eq!(u32(&b, 40), 0x1060); // CreateOptions (incl DELETE_ON_CLOSE)
272        assert_eq!(u16(&b, 44), 64 + 56); // NameOffset
273        assert_eq!(
274            u16(&b, 46),
275            "Windows\\Temp\\x.out".chars().count() as u16 * 2
276        );
277    }
278
279    #[test]
280    fn read_req_offset_and_length() {
281        let b = read_req(&[0xAB; 16], 0x1_0000, 0x4000);
282        assert_eq!(u16(&b, 0), 49); // StructureSize
283        assert_eq!(u32(&b, 4), 0x4000); // Length
284        assert_eq!(u32(&b, 8), 0x1_0000); // Offset (low dword)
285        assert_eq!(&b[16..32], &[0xAB; 16]); // FileId
286    }
287
288    #[test]
289    fn close_req_shape() {
290        let b = close_req(&[0xCD; 16]);
291        assert_eq!(u16(&b, 0), 24); // StructureSize
292        assert_eq!(&b[8..24], &[0xCD; 16]); // FileId
293    }
294}