ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
smb_client.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file smb_client.cpp
6 * @brief SMB2 client dialogue engine (see smb_client.h). Drives the wire codecs through the real
7 * NEGOTIATE / NTLMv2 SESSION_SETUP / TREE_CONNECT / CREATE exchange over a send/recv seam.
8 */
9
10#include "smb_client.h"
11
12#if PC_ENABLE_SMB
13
14#include "ntlm.h"
15#include "ntlmssp.h"
16#include "smb2.h"
17#include "spnego.h"
18#include <Arduino.h> // esp_fill_random() (real on device, mocked on native)
19#include <string.h>
20
21// Every request this engine builds has to fit the shared tx buffer, and the request builders report
22// that by returning 0. Pin the relationship instead of leaving each `if (!mlen)` to hope for it:
23// PC_SMB_BUF is a plain #ifndef with no floor, so without this a small override would silently turn
24// every exchange into SMB_ERR_OVERFLOW at run time rather than failing the build.
25//
26// CREATE is the binding case - it frames a 64-byte SMB2 header + a 56-byte body around a path that
27// utf16le() may fill to sizeof(s_smb.utf16) == PC_SMB_BUF/2, and the builders are handed
28// sizeof(tx) - 4 (the 4-byte NetBIOS length prefix is written separately):
29static_assert(64 + 56 + PC_SMB_BUF / 2 <= PC_SMB_BUF - 4,
30 "PC_SMB_BUF is too small to frame a CREATE around a full-length path "
31 "(64B header + 56B body + PC_SMB_BUF/2 path must fit PC_SMB_BUF-4)");
32// WRITE is the other payload-bearing case; its chunk_max already backs off by 128 bytes.
33static_assert(64 + 48 + (PC_SMB_BUF - 128) <= PC_SMB_BUF - 4,
34 "PC_SMB_BUF is too small to frame a WRITE around a full chunk");
35// SESSION_SETUP round 2 wraps the NTLMSSP AUTHENTICATE blob, capped at sizeof(s_smb.sp2).
36static_assert(64 + 24 + PC_SMB_BUF / 2 <= PC_SMB_BUF - 4,
37 "PC_SMB_BUF is too small to frame a SESSION_SETUP around a full security blob");
38
39// ASCII/Latin-1 -> UTF-16LE (SMB paths are ASCII); returns byte length (2 * chars), 0 on null/overflow.
40static size_t utf16le(const char *s, uint8_t *out, size_t cap)
41{
42 // Dead guard: utf16le is static with exactly two call sites (the share in smb_tree_connect and
43 // the path in smb_create), both reached only through smb_open(), which rejects a null
44 // cfg->share / cfg->path up front.
45 if (!s) // GCOVR_EXCL_LINE - both callers are guarded by smb_open, see above
46 {
47 return 0; // GCOVR_EXCL_LINE - unreachable body of the guard above
48 }
49 size_t n = 0;
50 for (; s[n]; n++)
51 {
52 if ((n * 2 + 2) > cap)
53 {
54 return 0;
55 }
56 out[n * 2] = (uint8_t)s[n];
57 out[n * 2 + 1] = 0;
58 }
59 return n * 2;
60}
61
62// Find the MsvAvTimestamp (AvId 7) FILETIME in a CHALLENGE target-info blob; copy 8 bytes, else 0-fill.
63static void find_av_timestamp(const uint8_t *ti, size_t ti_len, uint8_t out[8])
64{
65 memset(out, 0, 8);
66 size_t p = 0;
67 while (p + 4 <= ti_len)
68 {
69 uint16_t id = (uint16_t)(ti[p] | (ti[p + 1] << 8));
70 uint16_t len = (uint16_t)(ti[p + 2] | (ti[p + 3] << 8));
71 p += 4;
72 if (id == 0) // MsvAvEOL
73 {
74 break;
75 }
76 if (id == 7 && len == 8 && p + 8 <= ti_len)
77 {
78 memcpy(out, ti + p, 8);
79 return;
80 }
81 p += len;
82 }
83}
84
85static bool read_exact(SmbRecvFn recv, void *ctx, uint8_t *buf, size_t n)
86{
87 size_t got = 0;
88 while (got < n)
89 {
90 int r = recv(ctx, buf + got, n - got);
91 if (r <= 0)
92 {
93 return false;
94 }
95 got += (size_t)r;
96 }
97 return true;
98}
99
100// Frame the SMB2 message that sits at frame+4 (msg_len bytes) with the Direct-TCP prefix and send it.
101static bool send_msg(SmbSendFn send, void *ctx, uint8_t *frame, size_t msg_len)
102{
103 frame[0] = 0x00;
104 frame[1] = (uint8_t)(msg_len >> 16);
105 frame[2] = (uint8_t)(msg_len >> 8);
106 frame[3] = (uint8_t)msg_len;
107 size_t total = 4 + msg_len;
108 return send(ctx, frame, total) == (int)total;
109}
110
111// Receive one Direct-TCP-framed SMB2 message into rx; return its length, -2 on overflow, -1 on IO.
112static int recv_msg(SmbRecvFn recv, void *ctx, uint8_t *rx, size_t cap)
113{
114 uint8_t pre[4];
115 if (!read_exact(recv, ctx, pre, 4) || pre[0] != 0x00)
116 {
117 return -1;
118 }
119 size_t len = ((size_t)pre[1] << 16) | ((size_t)pre[2] << 8) | pre[3];
120 if (len == 0 || len > cap)
121 {
122 return len ? -2 : -1;
123 }
124 if (!read_exact(recv, ctx, rx, len))
125 {
126 return -1;
127 }
128 return (int)len;
129}
130
131// SMB dialogue working buffers, kept off the caller's stack: smb_open alone needs ~4 KB, which
132// overflows the default 8 KB Arduino loopTask (seen on HW as "Stack canary watchpoint triggered").
133// The client drives one sequential dialogue at a time (open -> read/write -> close), so a single
134// owned working set is correct; it is not reentrant across two concurrent SMB connections.
135struct SmbClientCtx
136{
137 uint8_t tx[PC_SMB_BUF];
138 uint8_t rx[PC_SMB_BUF];
139 uint8_t nt_resp[PC_SMB_BUF / 2];
140 uint8_t ntauth[PC_SMB_BUF / 2];
141 uint8_t sp2[PC_SMB_BUF / 2];
142 uint8_t utf16[PC_SMB_BUF / 2];
143 uint8_t ti[PC_SMB_BUF / 2]; ///< the CHALLENGE target-info with the MsvAvFlags MIC bit set (NTLMv2 input)
144};
145static SmbClientCtx s_smb;
146
147// SMB message-signing state for a session: the algorithm (HMAC-SHA256 for SMB 2.x, AES-CMAC for
148// SMB 3.x), the 16-byte signing key, and whether the server required signing. When active, every
149// request this engine sends is signed in place and every response must carry a matching signature
150// (MS-SMB2 §3.1.4.1 / §3.1.5.1).
151struct SmbSign
152{
153 bool active;
154 Smb2SignAlgo algo;
155 uint8_t key[16];
156};
157
158// SMB 3.x transport-encryption state for a session. Once @ref active, every request is wrapped in a
159// TRANSFORM_HEADER (the negotiated AEAD: AES-128/256-GCM or AES-128/256-CCM) instead of signed, and every
160// response must decrypt (MS-SMB2 §3.1.4.3/4). @ref available means the cipher keys were derived (the server
161// negotiated a cipher) even before encryption is required. @ref cipher is the negotiated Smb2Cipher id (it
162// selects the key + nonce length). @ref nonce is a monotonic per-session counter - it MUST never repeat for a
163// given key, so callers persist it across requests (on the SmbHandle for read/write/close).
164struct SmbCrypt
165{
166 bool active;
167 bool available;
168 uint16_t cipher;
169 uint8_t c2s[PC_SMB2_MAX_CIPHER_KEY_LEN];
170 uint8_t s2c[PC_SMB2_MAX_CIPHER_KEY_LEN];
171 uint64_t session_id;
172 uint64_t nonce;
173};
174
175// Sign / verify a message with the session's negotiated algorithm.
176static void smb_apply_sign(const SmbSign *s, uint8_t *msg, size_t len)
177{
178 if (s->algo == Smb2SignAlgo::AES_CMAC)
179 {
180 pc_smb2_sign_cmac(s->key, msg, len);
181 }
182 else
183 {
184 pc_smb2_sign(s->key, msg, len);
185 }
186}
187static bool smb_check_sign(const SmbSign *s, uint8_t *msg, size_t len)
188{
189 return s->algo == Smb2SignAlgo::AES_CMAC ? pc_smb2_verify_cmac(s->key, msg, len) : pc_smb2_verify(s->key, msg, len);
190}
191
192// Send the framed message currently in s_smb.tx (mlen bytes at tx+4) and receive the reply into s_smb.rx.
193// When @p sign is active the request is signed before sending and the response signature is verified
194// (a missing or wrong signature fails closed as SMB_ERR_PROTOCOL). Returns the reply length (>=0), or
195// -1 with *res set to the mapped IO / overflow / protocol error.
196static int smb_round_trip(SmbSendFn send, SmbRecvFn recv, void *ctx, size_t mlen, const SmbSign *sign, SmbCrypt *crypt,
197 SmbResult *res)
198{
199 // Encrypted path (SMB 3.x): wrap the plaintext request (tx+4) in a TRANSFORM_HEADER into rx+4, send it,
200 // receive the wrapped reply into rx, and decrypt it in place. Encryption supersedes signing (the AEAD tag
201 // is the integrity check), so a message is never both encrypted and signed.
202 if (crypt && crypt->active)
203 {
204 uint8_t nonce[PC_SMB2_NONCE_FIELD_LEN] = {0};
205 const uint64_t ctr = crypt->nonce++; // unique per key: never reuse a nonce, so advance every message
206 for (int i = 0; i < 8; i++)
207 {
208 nonce[i] = (uint8_t)(ctr >> (8 * i));
209 }
210 size_t tlen = pc_smb2_encrypt(crypt->cipher, crypt->c2s, nonce, crypt->session_id, s_smb.tx + 4, mlen,
211 s_smb.rx + 4, sizeof(s_smb.rx) - 4);
212 if (tlen == 0)
213 {
214 *res = SmbResult::SMB_ERR_OVERFLOW;
215 return -1;
216 }
217 if (!send_msg(send, ctx, s_smb.rx, tlen))
218 {
219 *res = SmbResult::SMB_ERR_IO;
220 return -1;
221 }
222 int rl = recv_msg(recv, ctx, s_smb.rx, sizeof(s_smb.rx));
223 if (rl < 0)
224 {
225 *res = (rl == -2) ? SmbResult::SMB_ERR_OVERFLOW : SmbResult::SMB_ERR_IO;
226 return -1;
227 }
228 // Decrypt in place: pc_smb2_decrypt GHASHes the whole ciphertext before the CTR pass, so an
229 // out == in (backward-shifted) overlap is safe. Fails closed on a bad tag / non-TRANSFORM reply.
230 size_t plen = pc_smb2_decrypt(crypt->cipher, crypt->s2c, s_smb.rx, (size_t)rl, s_smb.rx, sizeof(s_smb.rx));
231 if (plen == 0)
232 {
233 *res = SmbResult::SMB_ERR_PROTOCOL;
234 return -1;
235 }
236 return (int)plen;
237 }
238
239 if (sign && sign->active)
240 {
241 smb_apply_sign(sign, s_smb.tx + 4, mlen);
242 }
243 if (!send_msg(send, ctx, s_smb.tx, mlen))
244 {
245 *res = SmbResult::SMB_ERR_IO;
246 return -1;
247 }
248 int rl = recv_msg(recv, ctx, s_smb.rx, sizeof(s_smb.rx));
249 if (rl < 0)
250 {
251 *res = (rl == -2) ? SmbResult::SMB_ERR_OVERFLOW : SmbResult::SMB_ERR_IO;
252 return -1;
253 }
254 if (sign && sign->active && !smb_check_sign(sign, s_smb.rx, (size_t)rl))
255 {
256 *res = SmbResult::SMB_ERR_PROTOCOL;
257 return -1;
258 }
259 return rl;
260}
261
262// Step 1 - NEGOTIATE: advertise SMB 2.0.2 .. 3.1.1 (with the mandatory 3.1.1 preauth-integrity + an
263// AES-CMAC signing context) and confirm the server's negotiate response parses. Reports the server's
264// SecurityMode in *sec_mode and the chosen DialectRevision in *dialect (so smb_open picks HMAC vs
265// AES-CMAC signing), and seeds + folds the NEGOTIATE request/response into the 3.1.1 preauth-integrity
266// hash chain (MS-SMB2 §3.1.5.2). The salt is a fresh random blob; it lives only in the request bytes
267// that feed the hash, so it needs no separate storage.
268static SmbResult smb_negotiate(SmbSendFn send, SmbRecvFn recv, void *ctx, uint16_t *sec_mode, uint16_t *dialect,
269 uint16_t *cipher, SmbPreauth *preauth, const uint16_t *offer_ciphers, size_t offer_count)
270{
271 uint8_t guid[16];
272 uint8_t salt[32];
273 esp_fill_random(guid, 16);
274 esp_fill_random(salt, sizeof(salt));
275 size_t mlen = pc_smb2_build_negotiate_311(s_smb.tx + 4, sizeof(s_smb.tx) - 4, guid,
276 Smb2SecurityMode::SMB2_NEGOTIATE_SIGNING_ENABLED, salt, sizeof(salt),
277 offer_ciphers, offer_count);
278 if (!mlen) // GCOVR_EXCL_LINE - the static_assert at the top of this file makes this unreachable
279 {
280 return SmbResult::SMB_ERR_OVERFLOW; // GCOVR_EXCL_LINE - unreachable body of the guard above
281 }
282
283 // Seed the preauth-integrity hash and fold the NEGOTIATE request (the bytes are final - NEGOTIATE is
284 // never signed). The chain is only consumed when the server chooses 3.1.1, but folding is harmless
285 // otherwise.
286 pc_smb_preauth_init(preauth);
287 pc_smb_preauth_update(preauth, s_smb.tx + 4, mlen);
288
289 SmbResult rt = SmbResult::SMB_ERR_IO;
290 // NEGOTIATE precedes authentication, so there is no session key yet: never signed.
291 int rl = smb_round_trip(send, recv, ctx, mlen, nullptr, nullptr, &rt);
292 if (rl < 0)
293 {
294 return rt;
295 }
296 pc_smb_preauth_update(preauth, s_smb.rx, (size_t)rl); // fold the NEGOTIATE response
297 Smb2NegotiateResp neg;
298 if (!pc_smb2_parse_negotiate_response(s_smb.rx, (size_t)rl, &neg))
299 {
300 return SmbResult::SMB_ERR_PROTOCOL;
301 }
302 *sec_mode = neg.security_mode;
303 *dialect = neg.dialect;
304 // The negotiated encryption cipher lives in the 3.1.1 ENCRYPTION_CAPABILITIES context. 0 = none offered /
305 // accepted -> the session stays unencrypted. The server picks one of the ciphers we offered (any of the
306 // four SMB 3.1.1 ciphers - AES-128/256-GCM, AES-128/256-CCM), in our preference order.
307 *cipher = 0;
308 if (neg.dialect == (uint16_t)Smb2Dialect::SMB2_DIALECT_0311)
309 {
310 Smb2NegotiateContexts nc;
311 if (pc_smb2_parse_negotiate_contexts(s_smb.rx, (size_t)rl, &nc) && nc.have_encryption)
312 {
313 *cipher = nc.cipher;
314 }
315 }
316 return SmbResult::SMB_OK;
317}
318
319// Steps 2-4 - NTLMv2 SESSION_SETUP: SPNEGO/NTLMSSP negotiate, compute the NTLMv2 response to the server
320// challenge, then authenticate. Fills *session_id from the server's SessionId, threads the SESSION_SETUP
321// messages through the 3.1.1 preauth-integrity chain (@p preauth, seeded by smb_negotiate), and - when
322// the server required signing (@p want_signing) and the session is not GUEST/NULL - fills *sign with the
323// per-dialect signer: HMAC-SHA256 over the NTLMv2 session key for SMB 2.x, or AES-CMAC over the
324// SP800-108-derived signing key (from the final preauth hash) for SMB 3.x, so every later request signs.
325static SmbResult smb_session_setup(const SmbConfig *cfg, const char *domain, bool want_signing, uint16_t dialect,
326 uint16_t cipher, SmbPreauth *preauth, SmbSendFn send, SmbRecvFn recv, void *ctx,
327 uint64_t *session_id, SmbSign *sign, SmbCrypt *crypt)
328{
329 // 2. SESSION_SETUP round 1: NTLMSSP NEGOTIATE wrapped in SPNEGO
330 uint8_t ntneg[64];
331 uint8_t sp1[128];
332 size_t ntneg_n = pc_ntlmssp_build_negotiate(ntneg, sizeof(ntneg), NtlmsspFlags::NTLMSSP_CLIENT_DEFAULT_FLAGS);
333 size_t sp1_n = pc_spnego_wrap_negotiate(ntneg, ntneg_n, sp1, sizeof(sp1));
334 size_t mlen = pc_smb2_build_session_setup(s_smb.tx + 4, sizeof(s_smb.tx) - 4, 1, 0,
335 Smb2SecurityMode::SMB2_NEGOTIATE_SIGNING_ENABLED, sp1, sp1_n);
336 if (!mlen) // GCOVR_EXCL_LINE - the static_assert at the top of this file makes this unreachable
337 {
338 return SmbResult::SMB_ERR_OVERFLOW; // GCOVR_EXCL_LINE - unreachable body of the guard above
339 }
340 pc_smb_preauth_update(preauth, s_smb.tx + 4, mlen); // fold SESSION_SETUP request 1 (unsigned)
341 SmbResult rt = SmbResult::SMB_ERR_IO;
342 // Round 1 precedes the session key, so it is never signed.
343 int rl = smb_round_trip(send, recv, ctx, mlen, nullptr, nullptr, &rt);
344 if (rl < 0)
345 {
346 return rt;
347 }
348 pc_smb_preauth_update(preauth, s_smb.rx, (size_t)rl); // fold SESSION_SETUP response 1
349 Smb2Header h1;
350 if (!pc_smb2_parse_header(s_smb.rx, (size_t)rl, &h1) ||
351 h1.status != Smb2Status::SMB2_STATUS_MORE_PROCESSING_REQUIRED)
352 {
353 return SmbResult::SMB_ERR_AUTH;
354 }
355 *session_id = h1.session_id;
356 Smb2SessionSetupResp ss1;
357 if (!pc_smb2_parse_session_setup_response(s_smb.rx, (size_t)rl, &ss1) || !ss1.sec_buf)
358 {
359 return SmbResult::SMB_ERR_PROTOCOL;
360 }
361 const uint8_t *chal_tok = nullptr;
362 size_t chal_len = 0;
363 if (!pc_spnego_parse_response(ss1.sec_buf, ss1.sec_buf_len, &chal_tok, &chal_len))
364 {
365 return SmbResult::SMB_ERR_PROTOCOL;
366 }
367 NtlmChallenge ch;
368 if (!pc_ntlmssp_parse_challenge(chal_tok, chal_len, &ch))
369 {
370 return SmbResult::SMB_ERR_PROTOCOL;
371 }
372
373 // 3. Compute the NTLMv2 response and build the AUTHENTICATE with a MIC (MS-NLMP §3.1.5.1.2).
374 uint8_t nt_hash[16];
375 uint8_t owf[16];
376 pc_ntlm_nt_hash(cfg->pass, nt_hash);
377 if (!pc_ntlm_ntowfv2(nt_hash, cfg->user, domain, owf))
378 {
379 return SmbResult::SMB_ERR_OVERFLOW;
380 }
381 uint8_t cli_chal[8];
382 uint8_t ts[8];
383 uint8_t skey[16];
384 esp_fill_random(cli_chal, 8);
385 find_av_timestamp(ch.target_info, ch.target_info_len, ts);
386 // Set the MsvAvFlags "MIC provided" bit in the target-info the NTLMv2 response is computed over, so a
387 // server that enforces the MIC accepts it and verifies the digest attached below.
388 size_t ti_len = pc_ntlm_set_mic_flag(ch.target_info, ch.target_info_len, s_smb.ti, sizeof(s_smb.ti));
389 if (!ti_len)
390 {
391 return SmbResult::SMB_ERR_OVERFLOW;
392 }
393 size_t nt_len = pc_ntlm_v2_response(owf, ch.server_challenge, cli_chal, ts, s_smb.ti, ti_len, s_smb.nt_resp,
394 sizeof(s_smb.nt_resp), skey);
395 if (!nt_len)
396 {
397 return SmbResult::SMB_ERR_OVERFLOW;
398 }
399 size_t ntauth_n = pc_ntlmssp_build_authenticate(s_smb.ntauth, sizeof(s_smb.ntauth), nullptr, 0, s_smb.nt_resp,
400 nt_len, domain, cfg->user, cfg->workstation, ch.flags, true);
401 if (!ntauth_n)
402 {
403 return SmbResult::SMB_ERR_OVERFLOW;
404 }
405 // MIC = HMAC-MD5(session key, NEGOTIATE || CHALLENGE || AUTHENTICATE); write it into the zeroed field.
406 uint8_t mic[PC_NTLMSSP_MIC_LEN];
407 pc_ntlm_mic(skey, ntneg, ntneg_n, chal_tok, chal_len, s_smb.ntauth, ntauth_n, mic);
408 memcpy(s_smb.ntauth + PC_NTLMSSP_MIC_OFFSET, mic, PC_NTLMSSP_MIC_LEN);
409 size_t sp2_n = pc_spnego_wrap_authenticate(s_smb.ntauth, ntauth_n, s_smb.sp2, sizeof(s_smb.sp2));
410 if (!sp2_n)
411 {
412 return SmbResult::SMB_ERR_OVERFLOW;
413 }
414
415 // 4. SESSION_SETUP round 2 (echo the server SessionId). This request completes authentication and is
416 // folded into the preauth chain (unsigned), whose final value derives the SMB 3.x signing key.
417 mlen = pc_smb2_build_session_setup(s_smb.tx + 4, sizeof(s_smb.tx) - 4, 2, *session_id,
418 Smb2SecurityMode::SMB2_NEGOTIATE_SIGNING_ENABLED, s_smb.sp2, sp2_n);
419 if (!mlen) // GCOVR_EXCL_LINE - the static_assert at the top of this file makes this unreachable
420 {
421 return SmbResult::SMB_ERR_OVERFLOW; // GCOVR_EXCL_LINE - unreachable body of the guard above
422 }
423 pc_smb_preauth_update(preauth, s_smb.tx + 4, mlen); // fold request 2 -> the key-derivation hash is now final
424
425 // Select the session signer from the negotiated dialect: SMB 3.x (>= 3.0) signs with AES-CMAC over the
426 // SP800-108-derived key (3.1.1 mixes in the preauth hash); SMB 2.x signs with HMAC-SHA256 over the
427 // NTLMv2 session key. For SMB 2.x we sign request 2 with that key so a signing-required 2.x server
428 // accepts it; SMB 3.x leaves request 2 unsigned (the derived key signs from TREE_CONNECT onward,
429 // matching Windows / Samba / impacket).
430 const Smb2SignAlgo algo =
431 dialect >= (uint16_t)Smb2Dialect::SMB2_DIALECT_0300 ? Smb2SignAlgo::AES_CMAC : Smb2SignAlgo::HMAC_SHA256;
432 uint8_t sign_key[16];
433 if (algo == Smb2SignAlgo::AES_CMAC)
434 {
435 const bool is_311 = dialect == (uint16_t)Smb2Dialect::SMB2_DIALECT_0311;
436 pc_smb3_derive_signing_key(skey, dialect, is_311 ? preauth->hash : nullptr, sign_key);
437 }
438 else
439 {
440 memcpy(sign_key, skey, sizeof(sign_key));
441 if (want_signing)
442 {
443 pc_smb2_sign(skey, s_smb.tx + 4, mlen);
444 }
445 }
446
447 rl = smb_round_trip(send, recv, ctx, mlen, nullptr, nullptr, &rt);
448 if (rl < 0)
449 {
450 return rt;
451 }
452 Smb2Header h2;
453 if (!pc_smb2_parse_header(s_smb.rx, (size_t)rl, &h2))
454 {
455 return SmbResult::SMB_ERR_PROTOCOL;
456 }
457 if (h2.status != Smb2Status::SMB2_STATUS_SUCCESS)
458 {
459 return SmbResult::SMB_ERR_AUTH;
460 }
461 // A GUEST or anonymous (NULL) session is never signed even if signing was negotiated (MS-SMB2
462 // §3.2.5.3.1); anything else with the server requiring signing signs the rest of the session.
463 Smb2SessionSetupResp ss2;
464 bool guest_or_null = false;
465 uint16_t sess_flags = 0;
466 if (pc_smb2_parse_session_setup_response(s_smb.rx, (size_t)rl, &ss2))
467 {
468 sess_flags = ss2.session_flags;
469 guest_or_null = (sess_flags & (Smb2SessionFlags::SMB2_SESSION_FLAG_IS_GUEST |
470 Smb2SessionFlags::SMB2_SESSION_FLAG_IS_NULL)) != 0;
471 }
472 sign->active = want_signing && !guest_or_null;
473 sign->algo = algo;
474 memcpy(sign->key, sign_key, sizeof(sign->key));
475
476 // SMB 3.x transport encryption: derive the C2S/S2C cipher keys if the server negotiated a cipher (SMB 3.x,
477 // non-guest), sized by the cipher (16 for -128, 32 for -256). A server that requires encryption globally
478 // flags the session encrypt-required; a share can also require it (checked at TREE_CONNECT). But a share
479 // that requires encryption rejects the unencrypted TREE_CONNECT before it can advertise the share flag, so
480 // a caller that wants such a share sets cfg->encrypt to force encryption on from TREE_CONNECT (client-forced,
481 // like smbclient -e; MS-SMB2 §3.2.4.1.5). Otherwise the keys sit ready (available) but inactive.
482 crypt->active = false;
483 crypt->available = false;
484 crypt->cipher = 0;
485 crypt->session_id = *session_id;
486 crypt->nonce = 0;
487 const size_t cipher_key_len = pc_smb2_cipher_key_len(cipher);
488 if (cipher_key_len != 0 && dialect >= (uint16_t)Smb2Dialect::SMB2_DIALECT_0300 && !guest_or_null)
489 {
490 const bool is_311 = dialect == (uint16_t)Smb2Dialect::SMB2_DIALECT_0311;
491 crypt->available = pc_smb3_derive_encryption_keys(skey, dialect, is_311 ? preauth->hash : nullptr,
492 cipher_key_len, crypt->c2s, crypt->s2c);
493 if (crypt->available)
494 {
495 crypt->cipher = cipher;
496 if ((sess_flags & Smb2SessionFlags::SMB2_SESSION_FLAG_ENCRYPT_DATA) || cfg->encrypt)
497 {
498 crypt->active = true;
499 }
500 }
501 }
502 return SmbResult::SMB_OK;
503}
504
505// Step 5 - TREE_CONNECT to \\server\share. Fills *tree_id.
506static SmbResult smb_tree_connect(const SmbConfig *cfg, uint64_t session_id, const SmbSign *sign, SmbSendFn send,
507 SmbRecvFn recv, void *ctx, uint32_t *tree_id, SmbCrypt *crypt)
508{
509 size_t utf16_n = utf16le(cfg->share, s_smb.utf16, sizeof(s_smb.utf16));
510 if (!utf16_n)
511 {
512 return SmbResult::SMB_ERR_OVERFLOW;
513 }
514 size_t mlen = pc_smb2_build_tree_connect(s_smb.tx + 4, sizeof(s_smb.tx) - 4, 3, session_id, s_smb.utf16, utf16_n);
515 if (!mlen) // GCOVR_EXCL_LINE - the static_assert at the top of this file makes this unreachable
516 {
517 return SmbResult::SMB_ERR_OVERFLOW; // GCOVR_EXCL_LINE - unreachable body of the guard above
518 }
519 SmbResult rt = SmbResult::SMB_ERR_IO;
520 int rl = smb_round_trip(send, recv, ctx, mlen, sign, crypt, &rt);
521 if (rl < 0)
522 {
523 return rt;
524 }
525 Smb2Header h3;
526 Smb2TreeConnectResp tc;
527 if (!pc_smb2_parse_header(s_smb.rx, (size_t)rl, &h3) || h3.status != Smb2Status::SMB2_STATUS_SUCCESS)
528 {
529 return SmbResult::SMB_ERR_PROTOCOL;
530 }
531 if (!pc_smb2_parse_tree_connect_response(s_smb.rx, (size_t)rl, &tc))
532 {
533 return SmbResult::SMB_ERR_PROTOCOL;
534 }
535 *tree_id = h3.tree_id;
536 // A share flagged encrypt-data turns encryption on for everything from CREATE onward (MS-SMB2 §3.2.5.5),
537 // provided the cipher keys were derived at session setup.
538 if (crypt && crypt->available && (tc.share_flags & Smb2ShareFlags::SMB2_SHAREFLAG_ENCRYPT_DATA))
539 {
540 crypt->active = true;
541 }
542 return SmbResult::SMB_OK;
543}
544
545// Step 6 - CREATE (open) the file; fills the handle h on success.
546static SmbResult smb_create(const SmbConfig *cfg, SmbHandle *h, uint64_t session_id, uint32_t tree_id,
547 const SmbSign *sign, SmbCrypt *crypt, SmbSendFn send, SmbRecvFn recv, void *ctx)
548{
549 size_t utf16_n = utf16le(cfg->path, s_smb.utf16, sizeof(s_smb.utf16));
550 if (!utf16_n)
551 {
552 return SmbResult::SMB_ERR_OVERFLOW;
553 }
554 size_t mlen =
555 pc_smb2_build_create(s_smb.tx + 4, sizeof(s_smb.tx) - 4, 4, session_id, tree_id, cfg->desired_access,
556 Smb2ShareAccess::SMB2_FILE_SHARE_READ | Smb2ShareAccess::SMB2_FILE_SHARE_WRITE,
557 cfg->disposition, Smb2CreateOptions::SMB2_FILE_NON_DIRECTORY_FILE, s_smb.utf16, utf16_n);
558 if (!mlen) // GCOVR_EXCL_LINE - the static_assert at the top of this file makes this unreachable
559 {
560 return SmbResult::SMB_ERR_OVERFLOW; // GCOVR_EXCL_LINE - unreachable body of the guard above
561 }
562 SmbResult rt = SmbResult::SMB_ERR_IO;
563 int rl = smb_round_trip(send, recv, ctx, mlen, sign, crypt, &rt);
564 if (rl < 0)
565 {
566 return rt;
567 }
568 Smb2Header h4;
569 Smb2CreateResp cr;
570 if (!pc_smb2_parse_header(s_smb.rx, (size_t)rl, &h4) || h4.status != Smb2Status::SMB2_STATUS_SUCCESS)
571 {
572 return SmbResult::SMB_ERR_PROTOCOL;
573 }
574 if (!pc_smb2_parse_create_response(s_smb.rx, (size_t)rl, &cr))
575 {
576 return SmbResult::SMB_ERR_PROTOCOL;
577 }
578 h->session_id = session_id;
579 h->tree_id = tree_id;
580 memcpy(h->file_id, cr.file_id, 16);
581 h->file_size = cr.end_of_file;
582 h->next_message_id = 5;
583 h->signing_active = sign->active;
584 h->signing_algo = sign->algo;
585 memcpy(h->signing_key, sign->key, sizeof(h->signing_key));
586 // Carry the encryption state onto the handle so read/write/close keep encrypting with the same keys and a
587 // continuing nonce (the counter must not restart, or a nonce would repeat under the same key).
588 h->encrypt_active = crypt->active;
589 h->enc_cipher = crypt->cipher;
590 memcpy(h->enc_c2s, crypt->c2s, sizeof(h->enc_c2s));
591 memcpy(h->enc_s2c, crypt->s2c, sizeof(h->enc_s2c));
592 h->enc_nonce = crypt->nonce;
593 return SmbResult::SMB_OK;
594}
595
596SmbResult smb_open(const SmbConfig *cfg, SmbHandle *h, SmbSendFn send, SmbRecvFn recv, void *ctx)
597{
598 if (!cfg || !h || !send || !recv || !cfg->user || !cfg->pass || !cfg->share || !cfg->path)
599 {
600 return SmbResult::SMB_ERR_ARG;
601 }
602
603 const char *domain = cfg->domain ? cfg->domain : "";
604
605 // Offer all four SMB 3.1.1 ciphers in preference order (a server selects the first it supports). cfg->
606 // cipher_pref, when set, is moved to the front so a caller can pin a specific cipher (used to exercise
607 // each one against a real server).
608 uint16_t offer[PC_SMB2_MAX_OFFER_CIPHERS] = {
609 Smb2Cipher::SMB2_ENCRYPTION_AES128_GCM, Smb2Cipher::SMB2_ENCRYPTION_AES256_GCM,
610 Smb2Cipher::SMB2_ENCRYPTION_AES128_CCM, Smb2Cipher::SMB2_ENCRYPTION_AES256_CCM};
611 if (cfg->cipher_pref != 0 && pc_smb2_cipher_key_len(cfg->cipher_pref) != 0)
612 {
613 for (size_t i = 1; i < PC_SMB2_MAX_OFFER_CIPHERS; i++)
614 {
615 if (offer[i] == cfg->cipher_pref)
616 {
617 for (size_t j = i; j > 0; j--)
618 {
619 offer[j] = offer[j - 1];
620 }
621 offer[0] = cfg->cipher_pref;
622 break;
623 }
624 }
625 }
626
627 uint16_t sec_mode = 0;
628 uint16_t dialect = 0;
629 uint16_t cipher = 0;
630 SmbPreauth preauth;
631 SmbResult r =
632 smb_negotiate(send, recv, ctx, &sec_mode, &dialect, &cipher, &preauth, offer, PC_SMB2_MAX_OFFER_CIPHERS);
633 if (r != SmbResult::SMB_OK)
634 {
635 return r;
636 }
637 // The client advertises SIGNING_ENABLED, so the session is signed exactly when the server requires it.
638 bool want_signing = (sec_mode & Smb2SecurityMode::SMB2_NEGOTIATE_SIGNING_REQUIRED) != 0;
639
640 SmbSign sign = {false, Smb2SignAlgo::HMAC_SHA256, {0}};
641 SmbCrypt crypt = {false, false, 0, {0}, {0}, 0, 0};
642 uint64_t session_id = 0;
643 r = smb_session_setup(cfg, domain, want_signing, dialect, cipher, &preauth, send, recv, ctx, &session_id, &sign,
644 &crypt);
645 if (r != SmbResult::SMB_OK)
646 {
647 return r;
648 }
649
650 uint32_t tree_id = 0;
651 r = smb_tree_connect(cfg, session_id, &sign, send, recv, ctx, &tree_id, &crypt);
652 if (r != SmbResult::SMB_OK)
653 {
654 return r;
655 }
656
657 return smb_create(cfg, h, session_id, tree_id, &sign, &crypt, send, recv, ctx);
658}
659
660SmbResult smb_close(SmbHandle *h, SmbSendFn send, SmbRecvFn recv, void *ctx)
661{
662 if (!h || !send || !recv)
663 {
664 return SmbResult::SMB_ERR_ARG;
665 }
666 size_t mlen = pc_smb2_build_close(s_smb.tx + 4, sizeof(s_smb.tx) - 4, h->next_message_id, h->session_id, h->tree_id,
667 h->file_id);
668 if (!mlen) // GCOVR_EXCL_LINE - the static_assert at the top of this file makes this unreachable
669 {
670 return SmbResult::SMB_ERR_OVERFLOW; // GCOVR_EXCL_LINE - unreachable body of the guard above
671 }
672 SmbSign sign = {h->signing_active, h->signing_algo, {0}};
673 memcpy(sign.key, h->signing_key, sizeof(sign.key));
674 SmbCrypt crypt = {h->encrypt_active, h->encrypt_active, h->enc_cipher, {0}, {0}, h->session_id, h->enc_nonce};
675 memcpy(crypt.c2s, h->enc_c2s, sizeof(crypt.c2s));
676 memcpy(crypt.s2c, h->enc_s2c, sizeof(crypt.s2c));
677 SmbResult rt = SmbResult::SMB_ERR_IO;
678 int rl = smb_round_trip(send, recv, ctx, mlen, &sign, &crypt, &rt);
679 h->enc_nonce = crypt.nonce; // persist the advanced nonce (must never repeat under the same key)
680 if (rl < 0)
681 {
682 return rt;
683 }
684 Smb2Header hd;
685 Smb2CloseResp cl;
686 if (!pc_smb2_parse_header(s_smb.rx, (size_t)rl, &hd) || hd.status != Smb2Status::SMB2_STATUS_SUCCESS)
687 {
688 return SmbResult::SMB_ERR_PROTOCOL;
689 }
690 if (!pc_smb2_parse_close_response(s_smb.rx, (size_t)rl, &cl))
691 {
692 return SmbResult::SMB_ERR_PROTOCOL;
693 }
694 h->next_message_id++;
695 return SmbResult::SMB_OK;
696}
697
698SmbResult smb_read(SmbHandle *h, uint64_t offset, uint8_t *out, size_t cap, size_t *out_len, SmbSendFn send,
699 SmbRecvFn recv, void *ctx)
700{
701 if (!h || !out || !out_len || !send || !recv)
702 {
703 return SmbResult::SMB_ERR_ARG;
704 }
705 *out_len = 0;
706 SmbSign sign = {h->signing_active, h->signing_algo, {0}};
707 memcpy(sign.key, h->signing_key, sizeof(sign.key));
708 SmbCrypt crypt = {h->encrypt_active, h->encrypt_active, h->enc_cipher, {0}, {0}, h->session_id, h->enc_nonce};
709 memcpy(crypt.c2s, h->enc_c2s, sizeof(crypt.c2s));
710 memcpy(crypt.s2c, h->enc_s2c, sizeof(crypt.s2c));
711 const size_t chunk_max = PC_SMB_BUF - 96; // room for the header + READ response body
712 size_t total = 0;
713 while (total < cap)
714 {
715 size_t want = cap - total;
716 if (want > chunk_max)
717 {
718 want = chunk_max;
719 }
720 size_t mlen = pc_smb2_build_read(s_smb.tx + 4, sizeof(s_smb.tx) - 4, h->next_message_id, h->session_id,
721 h->tree_id, h->file_id, (uint32_t)want, offset + total);
722 if (!mlen) // GCOVR_EXCL_LINE - the static_assert at the top of this file makes this unreachable
723 {
724 return SmbResult::SMB_ERR_OVERFLOW; // GCOVR_EXCL_LINE - unreachable body of the guard above
725 }
726 SmbResult rt = SmbResult::SMB_ERR_IO;
727 int rl = smb_round_trip(send, recv, ctx, mlen, &sign, &crypt, &rt);
728 h->enc_nonce = crypt.nonce; // persist immediately so the nonce never repeats, even on an error return
729 if (rl < 0)
730 {
731 return rt;
732 }
733 Smb2Header hd;
734 if (!pc_smb2_parse_header(s_smb.rx, (size_t)rl, &hd))
735 {
736 return SmbResult::SMB_ERR_PROTOCOL;
737 }
738 h->next_message_id++;
739 if (hd.status == Smb2Status::SMB2_STATUS_END_OF_FILE)
740 {
741 break;
742 }
743 if (hd.status != Smb2Status::SMB2_STATUS_SUCCESS)
744 {
745 return SmbResult::SMB_ERR_PROTOCOL;
746 }
747 Smb2ReadResp r;
748 if (!pc_smb2_parse_read_response(s_smb.rx, (size_t)rl, &r) || r.data_len > want)
749 {
750 return SmbResult::SMB_ERR_PROTOCOL;
751 }
752 if (r.data_len == 0)
753 {
754 break;
755 }
756 memcpy(out + total, r.data, r.data_len);
757 total += r.data_len;
758 if (r.data_len < want)
759 {
760 break; // a short read means we reached the end of the file
761 }
762 }
763 *out_len = total;
764 return SmbResult::SMB_OK;
765}
766
767SmbResult smb_write(SmbHandle *h, uint64_t offset, const uint8_t *data, size_t len, size_t *written, SmbSendFn send,
768 SmbRecvFn recv, void *ctx)
769{
770 if (!h || !data || !written || !send || !recv)
771 {
772 return SmbResult::SMB_ERR_ARG;
773 }
774 *written = 0;
775 SmbSign sign = {h->signing_active, h->signing_algo, {0}};
776 memcpy(sign.key, h->signing_key, sizeof(sign.key));
777 SmbCrypt crypt = {h->encrypt_active, h->encrypt_active, h->enc_cipher, {0}, {0}, h->session_id, h->enc_nonce};
778 memcpy(crypt.c2s, h->enc_c2s, sizeof(crypt.c2s));
779 memcpy(crypt.s2c, h->enc_s2c, sizeof(crypt.s2c));
780 const size_t chunk_max = PC_SMB_BUF - 128; // room for the header + WRITE request body
781 size_t total = 0;
782 while (total < len)
783 {
784 size_t want = len - total;
785 if (want > chunk_max)
786 {
787 want = chunk_max;
788 }
789 size_t mlen = pc_smb2_build_write(s_smb.tx + 4, sizeof(s_smb.tx) - 4, h->next_message_id, h->session_id,
790 h->tree_id, h->file_id, data + total, want, offset + total);
791 if (!mlen) // GCOVR_EXCL_LINE - the static_assert at the top of this file makes this unreachable
792 {
793 return SmbResult::SMB_ERR_OVERFLOW; // GCOVR_EXCL_LINE - unreachable body of the guard above
794 }
795 SmbResult rt = SmbResult::SMB_ERR_IO;
796 int rl = smb_round_trip(send, recv, ctx, mlen, &sign, &crypt, &rt);
797 h->enc_nonce = crypt.nonce; // persist immediately so the nonce never repeats, even on an error return
798 if (rl < 0)
799 {
800 return rt;
801 }
802 Smb2Header hd;
803 if (!pc_smb2_parse_header(s_smb.rx, (size_t)rl, &hd))
804 {
805 return SmbResult::SMB_ERR_PROTOCOL;
806 }
807 h->next_message_id++;
808 if (hd.status != Smb2Status::SMB2_STATUS_SUCCESS)
809 {
810 return SmbResult::SMB_ERR_PROTOCOL;
811 }
812 Smb2WriteResp w;
813 if (!pc_smb2_parse_write_response(s_smb.rx, (size_t)rl, &w) || w.count == 0 || w.count > want)
814 {
815 return SmbResult::SMB_ERR_PROTOCOL; // no progress or a bogus count
816 }
817 total += w.count;
818 }
819 if (offset + total > h->file_size)
820 {
821 h->file_size = offset + total;
822 }
823 *written = total;
824 return SmbResult::SMB_OK;
825}
826
827#endif // PC_ENABLE_SMB
NTLMv2 response computation (MS-NLMP §3.3.2) for the SMB2 client (PC_ENABLE_SMB).
NTLMSSP message codec (MS-NLMP §2.2.1) for the SMB2 client (PC_ENABLE_SMB).
#define PC_SMB_BUF
SMB2 client work-buffer size (bytes) for smb_client's request/response framing.
SMB2 client wire codec (MS-SMB2), PC_ENABLE_SMB - increment 1: the transport frame,...
SMB2 client dialogue engine (PC_ENABLE_SMB) - drives the smb2 / ntlm / spnego wire codecs through a r...
SPNEGO (RFC 4178) GSS-API wrapping of the NTLMSSP tokens for the SMB2 client (PC_ENABLE_SMB).