Skip to main content

ad_time/protocols/
ntlm.rs

1//! NTLM Type 2 Challenge time source — stealthy extraction over SMB TCP/445.
2//!
3//! Protocol Specifications:
4//! - **MS-SMB2 §2.2.5**: SMB2 SESSION_SETUP Request
5//! - **MS-SMB2 §2.2.6**: SMB2 SESSION_SETUP Response
6//! - **MS-NLMP §2.2.1.1**: NTLMSSP_NEGOTIATE Message (Type 1)
7//! - **MS-NLMP §2.2.1.2**: CHALLENGE_MESSAGE (Type 2)
8//! - **MS-NLMP §2.2.2.1**: AV_PAIR Structure
9//!
10//! Sends an SMB2 NEGOTIATE, followed by an SMB2 SESSION_SETUP containing an NTLM Type 1
11//! (Negotiate) message. The server responds with STATUS_MORE_PROCESSING_REQUIRED and an
12//! NTLM Type 2 (Challenge) message. We extract the `MsvAvTimestamp` (AV_PAIR ID 7) from
13//! the TargetInfo structure.
14//!
15//! This provides 100ns precision. Crucially, we disconnect immediately after receiving
16//! the Type 2 challenge, without ever sending a Type 3 (Authenticate) message. Because
17//! no credentials are submitted, the LSA does not log a logon attempt, preventing
18//! Event IDs 4624/4625.
19
20use std::io::{Read, Write};
21use std::net::{SocketAddr, TcpStream};
22use std::time::{Duration, Instant, SystemTime};
23
24use super::common::{filetime_to_system_time, map_io_err, system_time_to_us};
25use super::smb_common::build_negotiate_request;
26use crate::time_src::{OffsetMicros, TimeSource, TimeSourceError};
27
28pub struct NtlmSource;
29
30impl TimeSource for NtlmSource {
31    fn name(&self) -> &'static str {
32        "ntlm"
33    }
34
35    fn fetch(
36        &self,
37        target: SocketAddr,
38        timeout: Duration,
39    ) -> Result<OffsetMicros, TimeSourceError> {
40        let smb_addr: SocketAddr = (target.ip(), 445).into();
41        fetch_ntlm(smb_addr, timeout)
42    }
43}
44
45fn fetch_ntlm(addr: SocketAddr, timeout: Duration) -> Result<OffsetMicros, TimeSourceError> {
46    let mut stream =
47        TcpStream::connect_timeout(&addr, timeout).map_err(|e| map_io_err(e, "connect"))?;
48    stream
49        .set_read_timeout(Some(timeout))
50        .map_err(|e| TimeSourceError::Protocol(e.to_string()))?;
51    stream
52        .set_write_timeout(Some(timeout))
53        .map_err(|e| TimeSourceError::Protocol(e.to_string()))?;
54
55    let t_send = Instant::now();
56    let t_send_sys = SystemTime::now();
57
58    // 1. Send SMB2 NEGOTIATE
59    let negotiate_req = build_negotiate_request();
60    stream
61        .write_all(&negotiate_req)
62        .map_err(|e| TimeSourceError::Protocol(e.to_string()))?;
63
64    // Read NEGOTIATE response (ignore contents, we just need to consume it)
65    let _neg_resp = read_smb_message(&mut stream)?;
66
67    // 2. Send SMB2 SESSION_SETUP with NTLM Type 1
68    let session_setup_req = build_session_setup_type1();
69    stream
70        .write_all(&session_setup_req)
71        .map_err(|e| TimeSourceError::Protocol(e.to_string()))?;
72
73    // Read SESSION_SETUP response (contains NTLM Type 2)
74    let setup_resp = read_smb_message(&mut stream)?;
75
76    // 3. IMPORTANT OPSEC: Disconnect immediately! Do not send Type 3.
77    // This aborts the NTLM handshake before the DC's LSA validates credentials,
78    // avoiding Event ID 4625 (Logon Failure).
79    drop(stream);
80
81    let rtt = t_send.elapsed();
82
83    let server_time = parse_session_setup_response(&setup_resp)?;
84
85    let t_mid_us = system_time_to_us(t_send_sys)? + (rtt.as_micros() as i64) / 2;
86    let server_us = system_time_to_us(server_time)?;
87
88    Ok(server_us - t_mid_us)
89}
90
91fn build_session_setup_type1() -> Vec<u8> {
92    // Build NTLMSSP Type 1 (MS-NLMP §2.2.1.1)
93    let mut ntlm = vec![];
94    ntlm.extend_from_slice(b"NTLMSSP\0");
95    ntlm.extend_from_slice(&1u32.to_le_bytes()); // MessageType = 1 (Negotiate)
96
97    // NTLM Type 1 negotiate flags. Per MS-NLMP §2.2.2.5, all bits in this value
98    // map to named flags; no MUST-BE-ZERO reserved bits are set:
99    //   0x80000000 NEGOTIATE_56 | 0x40000000 NEGOTIATE_KEY_EXCH
100    //   0x20000000 NEGOTIATE_128 | 0x02000000 NEGOTIATE_VERSION (requires Version field below)
101    //   0x00800000 NEGOTIATE_TARGET_INFO | 0x00080000 NEGOTIATE_EXTENDED_SESSIONSECURITY
102    //   0x00008000 NEGOTIATE_ALWAYS_SIGN | 0x00000200 NEGOTIATE_NTLM
103    //   0x00000020 NEGOTIATE_SEAL | 0x00000010 NEGOTIATE_SIGN
104    //   0x00000002 NEGOTIATE_OEM | 0x00000001 NEGOTIATE_UNICODE
105    // This combination is plausible for Windows 10/11 based on flag analysis.
106    // NOTE: the exact value sent by a real Windows client must be validated against
107    // a live pcap — the MS-NLMP spec does not document a per-version flag constant.
108    let flags: u32 = 0xE2888233;
109    ntlm.extend_from_slice(&flags.to_le_bytes());
110
111    // DomainNameFields: Len=0, MaxLen=0, BufferOffset=40 (size of fixed fields with Version)
112    ntlm.extend_from_slice(&0u16.to_le_bytes());
113    ntlm.extend_from_slice(&0u16.to_le_bytes());
114    ntlm.extend_from_slice(&40u32.to_le_bytes());
115
116    // WorkstationFields: Len=0, MaxLen=0, BufferOffset=40
117    ntlm.extend_from_slice(&0u16.to_le_bytes());
118    ntlm.extend_from_slice(&0u16.to_le_bytes());
119    ntlm.extend_from_slice(&40u32.to_le_bytes());
120
121    // Version (MS-NLMP §2.2.2.10):
122    //   Major=0x0A (10), Minor=0x00 — CONFIRMED for Windows 10/11 per MS-NLMP Appendix B §33.
123    //   Build=0x4A61 (19041, Win10 20H1) — a valid build; actual value varies per install.
124    //   Reserved=0x000000, NTLMRevisionCurrent=0x0F — CONFIRMED: NTLMSSP_REVISION_W2K3
125    //   is the sole defined value per MS-NLMP §2.2.2.10 and applies to all post-W2K3 Windows.
126    // NOTE: ProductBuild should be validated; use any realistic Win10/11 build (18362–26100).
127    ntlm.extend_from_slice(&[0x0A, 0x00, 0x61, 0x4A, 0x00, 0x00, 0x00, 0x0F]);
128
129    let ntlm_len = ntlm.len();
130
131    // SMB2 SESSION_SETUP body size is 24 + length of security buffer
132    // But structure size field is always 25.
133    let body_size = 24 + ntlm_len;
134    let smb2_header_size = 64usize;
135    let total = smb2_header_size + body_size;
136
137    let mut pkt = vec![0u8; 4 + total];
138    pkt[1] = ((total >> 16) & 0xFF) as u8;
139    pkt[2] = ((total >> 8) & 0xFF) as u8;
140    pkt[3] = (total & 0xFF) as u8;
141
142    let h = &mut pkt[4..4 + smb2_header_size];
143    h[0..4].copy_from_slice(b"\xfeSMB");
144    h[4..6].copy_from_slice(&64u16.to_le_bytes());
145    h[12..14].copy_from_slice(&1u16.to_le_bytes()); // SESSION_SETUP (0x0001)
146    h[18..20].copy_from_slice(&1u16.to_le_bytes()); // CreditRequest
147    h[28..36].copy_from_slice(&2u64.to_le_bytes()); // MessageId = 2
148
149    let b = &mut pkt[4 + smb2_header_size..];
150    b[0..2].copy_from_slice(&25u16.to_le_bytes()); // StructureSize = 25
151    b[2] = 0; // Flags
152    b[3] = 1; // SecurityMode
153    b[4..8].copy_from_slice(&0x01u32.to_le_bytes()); // Capabilities: SMB2_GLOBAL_CAP_DFS only (MS-SMB2 §2.2.5; Windows always sends 0x01 in SESSION_SETUP)
154    b[8..12].copy_from_slice(&0u32.to_le_bytes()); // Channel
155    b[12..14].copy_from_slice(&88u16.to_le_bytes()); // SecurityBufferOffset = 64 + 24
156    b[14..16].copy_from_slice(&(ntlm_len as u16).to_le_bytes()); // SecurityBufferLength
157    b[16..24].copy_from_slice(&0u64.to_le_bytes()); // PreviousSessionId
158
159    b[24..24 + ntlm_len].copy_from_slice(&ntlm);
160
161    pkt
162}
163
164/// Parse SMB2 SESSION_SETUP_RESPONSE and extract NTLM Type 2 TargetInfo MsvAvTimestamp.
165fn parse_session_setup_response(b: &[u8]) -> Result<SystemTime, TimeSourceError> {
166    // Check SMB2 Header Status (offset 8)
167    if b.len() < 64 {
168        return Err(TimeSourceError::Parse("SMB2 response too short".into()));
169    }
170    let status = u32::from_le_bytes([b[8], b[9], b[10], b[11]]);
171    if status != 0xC0000016 {
172        // STATUS_MORE_PROCESSING_REQUIRED
173        return Err(TimeSourceError::Protocol(format!(
174            "Expected MORE_PROCESSING_REQUIRED, got 0x{:08X}",
175            status
176        )));
177    }
178
179    // SMB2 SESSION_SETUP_RESPONSE body starts at offset 64
180    let body = &b[64..];
181    if body.len() < 9 {
182        return Err(TimeSourceError::Parse(
183            "SMB2 SESSION_SETUP_RESPONSE body too short".into(),
184        ));
185    }
186
187    let struct_size = u16::from_le_bytes([body[0], body[1]]);
188    if struct_size != 9 {
189        return Err(TimeSourceError::Protocol(
190            "Unexpected SESSION_SETUP_RESPONSE structure size".into(),
191        ));
192    }
193
194    let sec_offset = u16::from_le_bytes([body[4], body[5]]) as usize;
195    let sec_len = u16::from_le_bytes([body[6], body[7]]) as usize;
196
197    let sec_end = sec_offset
198        .checked_add(sec_len)
199        .ok_or_else(|| TimeSourceError::Parse("SecurityBuffer overflow".into()))?;
200    if sec_offset < 64 || sec_end > b.len() {
201        return Err(TimeSourceError::Parse(
202            "SecurityBuffer out of bounds".into(),
203        ));
204    }
205
206    let ntlm = &b[sec_offset..sec_end];
207    parse_ntlm_type2(ntlm)
208}
209
210/// MS-NLMP §2.2.1.2 CHALLENGE_MESSAGE
211fn parse_ntlm_type2(ntlm: &[u8]) -> Result<SystemTime, TimeSourceError> {
212    if ntlm.len() < 48 {
213        return Err(TimeSourceError::Parse(
214            "NTLM Type 2 too short for TargetInfoFields".into(),
215        ));
216    }
217    if &ntlm[0..8] != b"NTLMSSP\0" {
218        return Err(TimeSourceError::Parse("Invalid NTLMSSP signature".into()));
219    }
220    let msg_type = u32::from_le_bytes([ntlm[8], ntlm[9], ntlm[10], ntlm[11]]);
221    if msg_type != 2 {
222        return Err(TimeSourceError::Parse(format!(
223            "Expected NTLM Type 2, got {}",
224            msg_type
225        )));
226    }
227
228    // TargetInfoFields is at offset 40
229    let target_info_len = u16::from_le_bytes([ntlm[40], ntlm[41]]) as usize;
230    let target_info_offset = u32::from_le_bytes([ntlm[44], ntlm[45], ntlm[46], ntlm[47]]) as usize;
231
232    let target_info_end = target_info_offset
233        .checked_add(target_info_len)
234        .ok_or_else(|| TimeSourceError::Parse("TargetInfo overflow".into()))?;
235    if target_info_end > ntlm.len() {
236        return Err(TimeSourceError::Parse(
237            "TargetInfo out of bounds in NTLM".into(),
238        ));
239    }
240
241    let target_info = &ntlm[target_info_offset..target_info_end];
242
243    // MS-NLMP §2.2.2.1 AV_PAIR
244    let mut pos: usize = 0;
245    while let Some(end_check) = pos.checked_add(4) {
246        if end_check > target_info.len() {
247            break;
248        }
249
250        let av_id = u16::from_le_bytes([target_info[pos], target_info[pos + 1]]);
251        let av_len = u16::from_le_bytes([target_info[pos + 2], target_info[pos + 3]]) as usize;
252        pos += 4;
253
254        let av_end = pos
255            .checked_add(av_len)
256            .ok_or_else(|| TimeSourceError::Parse("AV_PAIR overflow".into()))?;
257        if av_end > target_info.len() {
258            return Err(TimeSourceError::Parse(
259                "AV_PAIR length out of bounds".into(),
260            ));
261        }
262
263        if av_id == 7 {
264            // MsvAvTimestamp
265            if av_len != 8 {
266                return Err(TimeSourceError::Parse(
267                    "MsvAvTimestamp has invalid length".into(),
268                ));
269            }
270            let filetime = u64::from_le_bytes([
271                target_info[pos],
272                target_info[pos + 1],
273                target_info[pos + 2],
274                target_info[pos + 3],
275                target_info[pos + 4],
276                target_info[pos + 5],
277                target_info[pos + 6],
278                target_info[pos + 7],
279            ]);
280            return filetime_to_system_time(filetime);
281        } else if av_id == 0 {
282            // MsvAvEOL
283            break;
284        }
285
286        pos += av_len;
287    }
288
289    Err(TimeSourceError::Parse(
290        "MsvAvTimestamp (AV_PAIR 7) not found in NTLM TargetInfo".into(),
291    ))
292}
293
294fn read_smb_message(stream: &mut TcpStream) -> Result<Vec<u8>, TimeSourceError> {
295    let mut nb_header = [0u8; 4];
296    stream
297        .read_exact(&mut nb_header)
298        .map_err(|e| map_io_err(e, "read_header"))?;
299    let msg_len = u32::from_be_bytes(nb_header) & 0x00FF_FFFF;
300    if msg_len > 65536 {
301        return Err(TimeSourceError::Protocol(format!(
302            "SMB2 response too large: {}",
303            msg_len
304        )));
305    }
306    let mut body = vec![0u8; msg_len as usize];
307    stream
308        .read_exact(&mut body)
309        .map_err(|e| map_io_err(e, "read_body"))?;
310    Ok(body)
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316    use std::time::UNIX_EPOCH;
317
318    #[test]
319    fn build_session_setup_type1_has_correct_structure() {
320        let req = build_session_setup_type1();
321        // NetBIOS length
322        assert_eq!(req[0], 0);
323        let len = u32::from_be_bytes([0, req[1], req[2], req[3]]);
324        assert_eq!(len as usize, req.len() - 4);
325
326        // SMB2 Header
327        assert_eq!(&req[4..8], b"\xfeSMB");
328        // Command == 1
329        assert_eq!(&req[16..18], &[1, 0]);
330
331        // SecurityBufferOffset = 88
332        assert_eq!(&req[80..82], &[88, 0]);
333        // NTLMSSP
334        assert_eq!(&req[92..100], b"NTLMSSP\0");
335    }
336
337    // Mocking a real NTLM Type 2 parsing requires a real packet structure.
338    // For now, we verify the logic manually with synthetic data.
339    #[test]
340    fn parse_ntlm_type2_extracts_timestamp() {
341        let mut ntlm = vec![0u8; 60];
342        ntlm[0..8].copy_from_slice(b"NTLMSSP\0");
343        ntlm[8..12].copy_from_slice(&2u32.to_le_bytes()); // Type 2
344
345        // TargetInfoFields
346        ntlm[40..42].copy_from_slice(&12u16.to_le_bytes()); // Len = 12
347        ntlm[42..44].copy_from_slice(&12u16.to_le_bytes()); // MaxLen = 12
348        ntlm[44..48].copy_from_slice(&48u32.to_le_bytes()); // Offset = 48
349
350        // TargetInfoBuffer at 48: AvId 7, AvLen 8, Value = 133485408000000000 (2024-01-01)
351        ntlm[48..50].copy_from_slice(&7u16.to_le_bytes());
352        ntlm[50..52].copy_from_slice(&8u16.to_le_bytes());
353        let ft: u64 = 133_485_408_000_000_000;
354        ntlm[52..60].copy_from_slice(&ft.to_le_bytes());
355
356        let st = parse_ntlm_type2(&ntlm).unwrap();
357        let unix_secs = st.duration_since(UNIX_EPOCH).unwrap().as_secs();
358        assert_eq!(unix_secs, 1_704_067_200);
359    }
360
361    use proptest::prelude::*;
362
363    proptest! {
364        #[test]
365        fn parse_ntlm_type2_never_panics(data in proptest::collection::vec(any::<u8>(), 0..512)) {
366            let _ = parse_ntlm_type2(&data);
367        }
368    }
369}
370
371#[cfg(feature = "fuzzing")]
372pub fn fuzz_parse_ntlm_type2(
373    data: &[u8],
374) -> Result<std::time::SystemTime, crate::time_src::TimeSourceError> {
375    parse_ntlm_type2(data)
376}