ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
ntlmssp.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 ntlmssp.cpp
6 * @brief NTLMSSP message codec implementation (see ntlmssp.h). Little-endian; text UTF-16LE.
7 */
8
9#include "ntlmssp.h"
10
11#if PC_ENABLE_SMB
12
13#include <string.h>
14
16
17static const uint8_t NTLMSSP_SIG[8] = {'N', 'T', 'L', 'M', 'S', 'S', 'P', 0};
18
19// Write a Len/MaxLen/BufferOffset field triplet at @p f.
20static void wr_field(uint8_t *f, uint16_t len, uint32_t off)
21{
22 pc_wr16le(f + 0, len);
23 pc_wr16le(f + 2, len); // MaxLen == Len
24 pc_wr32le(f + 4, off);
25}
26
27size_t pc_ntlmssp_build_negotiate(uint8_t *buf, size_t cap, uint32_t flags)
28{
29 if (!buf || cap < 32)
30 {
31 return 0;
32 }
33 memset(buf, 0, 32);
34 memcpy(buf + 0, NTLMSSP_SIG, 8); // Signature
35 pc_wr32le(buf + 8, 1); // MessageType = NEGOTIATE
36 pc_wr32le(buf + 12, flags); // NegotiateFlags
37 wr_field(buf + 16, 0, 32); // DomainNameFields (empty; offset = end of header)
38 wr_field(buf + 24, 0, 32); // WorkstationFields (empty)
39 return 32;
40}
41
42bool pc_ntlmssp_parse_challenge(const uint8_t *msg, size_t len, NtlmChallenge *out)
43{
44 if (!msg || !out || len < 48) // through TargetInfoFields
45 {
46 return false;
47 }
48 if (memcmp(msg, NTLMSSP_SIG, 8) != 0 || pc_rd32le(msg + 8) != 2)
49 {
50 return false;
51 }
52 out->flags = pc_rd32le(msg + 20);
53 memcpy(out->server_challenge, msg + 24, 8);
54 uint16_t ti_len = pc_rd16le(msg + 40);
55 uint32_t ti_off = pc_rd32le(msg + 44);
56 if (ti_len == 0)
57 {
58 out->target_info = nullptr;
59 out->target_info_len = 0;
60 return true;
61 }
62 if ((size_t)ti_off + ti_len > len) // target info out of bounds -> fail closed
63 {
64 return false;
65 }
66 out->target_info = msg + ti_off;
67 out->target_info_len = ti_len;
68 return true;
69}
70
71// Append the UTF-16LE encoding of @p s to buf[at..]; returns the byte count (2 * strlen).
72static size_t put_utf16le(uint8_t *buf, const char *s)
73{
74 size_t n = 0;
75 if (s)
76 {
77 for (const char *p = s; *p; p++)
78 {
79 buf[n++] = (uint8_t)*p;
80 buf[n++] = 0;
81 }
82 }
83 return n;
84}
85static size_t utf16_len(const char *s)
86{
87 size_t n = 0;
88 if (s)
89 {
90 while (s[n])
91 {
92 n++;
93 }
94 }
95 return n * 2;
96}
97
98size_t pc_ntlmssp_build_authenticate(uint8_t *buf, size_t cap, const uint8_t *lm_resp, size_t lm_len,
99 const uint8_t *nt_resp, size_t nt_len, const char *domain, const char *user,
100 const char *workstation, uint32_t flags, bool with_mic)
101{
102 // With a MIC the fixed part carries an 8-byte Version + a 16-byte MIC before the payload (MS-NLMP
103 // §2.2.1.3); NTLMSSP_NEGOTIATE_VERSION must then be set so the server knows the Version is present.
104 const size_t HDR = with_mic ? 88 : 64;
105 if (with_mic)
106 {
107 flags |= NtlmsspFlags::NTLMSSP_NEGOTIATE_VERSION;
108 }
109 size_t dlen = utf16_len(domain);
110 size_t ulen = utf16_len(user);
111 size_t wlen = utf16_len(workstation);
112 size_t total = HDR + lm_len + nt_len + dlen + ulen + wlen; // session key empty
113 if (!buf || total > cap)
114 {
115 return 0;
116 }
117
118 memset(buf, 0, HDR);
119 memcpy(buf + 0, NTLMSSP_SIG, 8); // Signature
120 pc_wr32le(buf + 8, 3); // MessageType = AUTHENTICATE
121 if (with_mic)
122 {
123 // Version (offset 64): a plausible Windows build; servers do not validate the value. MIC (offset
124 // 72) stays zero here - the caller writes it after taking HMAC-MD5 over the three messages.
125 buf[64] = 6; // ProductMajorVersion
126 buf[65] = 1; // ProductMinorVersion
127 pc_wr16le(buf + 66, 7601); // ProductBuild
128 buf[71] = 15; // NTLMRevisionCurrent (NTLMSSP_REVISION_W2K3)
129 }
130
131 // Lay out the payload after the fixed header, then point each field at it.
132 size_t off = HDR;
133 size_t lm_off = off;
134 if (lm_resp && lm_len)
135 {
136 memcpy(buf + off, lm_resp, lm_len);
137 }
138 off += lm_len;
139 size_t nt_off = off;
140 if (nt_resp && nt_len)
141 {
142 memcpy(buf + off, nt_resp, nt_len);
143 }
144 off += nt_len;
145 size_t dom_off = off;
146 off += put_utf16le(buf + off, domain);
147 size_t usr_off = off;
148 off += put_utf16le(buf + off, user);
149 size_t wks_off = off;
150 off += put_utf16le(buf + off, workstation);
151 size_t key_off = off; // EncryptedRandomSessionKey empty
152
153 wr_field(buf + 12, (uint16_t)lm_len, (uint32_t)lm_off); // LmChallengeResponseFields
154 wr_field(buf + 20, (uint16_t)nt_len, (uint32_t)nt_off); // NtChallengeResponseFields
155 wr_field(buf + 28, (uint16_t)dlen, (uint32_t)dom_off); // DomainNameFields
156 wr_field(buf + 36, (uint16_t)ulen, (uint32_t)usr_off); // UserNameFields
157 wr_field(buf + 44, (uint16_t)wlen, (uint32_t)wks_off); // WorkstationFields
158 wr_field(buf + 52, 0, (uint32_t)key_off); // EncryptedRandomSessionKeyFields
159 pc_wr32le(buf + 60, flags); // NegotiateFlags
160 return total;
161}
162
163#endif // PC_ENABLE_SMB
Fixed-width integer serializers into a raw uint8_t* buffer - one source of truth.
uint32_t pc_rd32le(const uint8_t *p)
Read a little-endian u32 at p.
Definition endian.h:66
size_t pc_wr32le(uint8_t *p, uint32_t v)
Write v little-endian at p.
Definition endian.h:40
uint16_t pc_rd16le(const uint8_t *p)
Read a little-endian u16 at p.
Definition endian.h:60
size_t pc_wr16le(uint8_t *p, uint16_t v)
Write v little-endian at p.
Definition endian.h:32
NTLMSSP message codec (MS-NLMP §2.2.1) for the SMB2 client (PC_ENABLE_SMB).