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, system_time_to_us};
25use crate::time_src::{OffsetMicros, TimeSource, TimeSourceError};
26
27pub struct NtlmSource;
28
29// SMB2 capabilities
30const SMB2_CAPABILITIES: u32 = 0x7F;
31
32impl TimeSource for NtlmSource {
33    fn name(&self) -> &'static str {
34        "ntlm"
35    }
36
37    fn fetch(
38        &self,
39        target: SocketAddr,
40        timeout: Duration,
41    ) -> Result<OffsetMicros, TimeSourceError> {
42        let smb_addr: SocketAddr = (target.ip(), 445).into();
43        fetch_ntlm(smb_addr, timeout)
44    }
45}
46
47fn fetch_ntlm(addr: SocketAddr, timeout: Duration) -> Result<OffsetMicros, TimeSourceError> {
48    let mut stream = TcpStream::connect_timeout(&addr, timeout).map_err(map_io_err)?;
49    stream
50        .set_read_timeout(Some(timeout))
51        .map_err(|e| TimeSourceError::Protocol(e.to_string()))?;
52    stream
53        .set_write_timeout(Some(timeout))
54        .map_err(|e| TimeSourceError::Protocol(e.to_string()))?;
55
56    let t_send = Instant::now();
57    let t_send_sys = SystemTime::now();
58
59    // 1. Send SMB2 NEGOTIATE
60    let negotiate_req = build_negotiate_request();
61    stream
62        .write_all(&negotiate_req)
63        .map_err(|e| TimeSourceError::Protocol(e.to_string()))?;
64
65    // Read NEGOTIATE response (ignore contents, we just need to consume it)
66    let _neg_resp = read_smb_message(&mut stream)?;
67
68    // 2. Send SMB2 SESSION_SETUP with NTLM Type 1
69    let session_setup_req = build_session_setup_type1();
70    stream
71        .write_all(&session_setup_req)
72        .map_err(|e| TimeSourceError::Protocol(e.to_string()))?;
73
74    // Read SESSION_SETUP response (contains NTLM Type 2)
75    let setup_resp = read_smb_message(&mut stream)?;
76
77    // 3. IMPORTANT OPSEC: Disconnect immediately! Do not send Type 3.
78    // This aborts the NTLM handshake before the DC's LSA validates credentials,
79    // avoiding Event ID 4625 (Logon Failure).
80    drop(stream);
81
82    let rtt = t_send.elapsed();
83
84    let server_time = parse_session_setup_response(&setup_resp)?;
85
86    let t_mid_us = system_time_to_us(t_send_sys)? + (rtt.as_micros() as i64) / 2;
87    let server_us = system_time_to_us(server_time)?;
88
89    Ok(server_us - t_mid_us)
90}
91
92fn build_negotiate_request() -> Vec<u8> {
93    let dialects: &[u16] = &[0x0300, 0x0210, 0x0202];
94    let dialect_count = dialects.len() as u16;
95
96    let body_size = 36 + (2 * dialect_count as usize);
97    let smb2_header_size = 64usize;
98    let total = smb2_header_size + body_size;
99
100    let mut pkt = vec![0u8; 4 + total];
101    pkt[1] = ((total >> 16) & 0xFF) as u8;
102    pkt[2] = ((total >> 8) & 0xFF) as u8;
103    pkt[3] = (total & 0xFF) as u8;
104
105    let h = &mut pkt[4..4 + smb2_header_size];
106    h[0..4].copy_from_slice(b"\xfeSMB");
107    h[4..6].copy_from_slice(&64u16.to_le_bytes());
108    h[12..14].copy_from_slice(&0u16.to_le_bytes()); // NEGOTIATE
109    h[18..20].copy_from_slice(&1u16.to_le_bytes()); // CreditRequest
110    h[28..36].copy_from_slice(&1u64.to_le_bytes()); // MessageId
111
112    let b = &mut pkt[4 + smb2_header_size..];
113    b[0..2].copy_from_slice(&36u16.to_le_bytes());
114    b[2..4].copy_from_slice(&dialect_count.to_le_bytes());
115    b[4..6].copy_from_slice(&1u16.to_le_bytes()); // SecurityMode=1
116    b[8..12].copy_from_slice(&SMB2_CAPABILITIES.to_le_bytes());
117
118    // OPSEC: Random ClientGuid (UUIDv4)
119    let mut guid = [0u8; 16];
120    for b_out in guid.iter_mut() {
121        *b_out = rand::random();
122    }
123    guid[6] = (guid[6] & 0x0F) | 0x40; // Version 4
124    guid[8] = (guid[8] & 0x3F) | 0x80; // Variant 10xx
125    b[12..28].copy_from_slice(&guid);
126
127    for (i, &d) in dialects.iter().enumerate() {
128        let off = 36 + i * 2;
129        b[off..off + 2].copy_from_slice(&d.to_le_bytes());
130    }
131
132    pkt
133}
134
135fn build_session_setup_type1() -> Vec<u8> {
136    // Build NTLMSSP Type 1 (MS-NLMP §2.2.1.1)
137    let mut ntlm = vec![];
138    ntlm.extend_from_slice(b"NTLMSSP\0");
139    ntlm.extend_from_slice(&1u32.to_le_bytes()); // MessageType = 1 (Negotiate)
140
141    // OPSEC Rationale: Generic minimal client flags. Real bitmask choice
142    // requires Phase 5 packet captures to match observed Windows traffic.
143    // Current set: NEGOTIATE_56 | NEGOTIATE_KEY_EXCH | NEGOTIATE_128 |
144    // NEGOTIATE_NTLM | REQUEST_TARGET | NEGOTIATE_UNICODE.
145    let flags: u32 = 0xE0000205;
146    ntlm.extend_from_slice(&flags.to_le_bytes());
147
148    // DomainName/WorkstationName omitted (empty fields)
149    ntlm.extend_from_slice(&[0u8; 16]); // 8 bytes domain, 8 bytes workstation
150
151    let ntlm_len = ntlm.len();
152
153    // SMB2 SESSION_SETUP body size is 24 + length of security buffer
154    // But structure size field is always 25.
155    let body_size = 24 + ntlm_len;
156    let smb2_header_size = 64usize;
157    let total = smb2_header_size + body_size;
158
159    let mut pkt = vec![0u8; 4 + total];
160    pkt[1] = ((total >> 16) & 0xFF) as u8;
161    pkt[2] = ((total >> 8) & 0xFF) as u8;
162    pkt[3] = (total & 0xFF) as u8;
163
164    let h = &mut pkt[4..4 + smb2_header_size];
165    h[0..4].copy_from_slice(b"\xfeSMB");
166    h[4..6].copy_from_slice(&64u16.to_le_bytes());
167    h[12..14].copy_from_slice(&1u16.to_le_bytes()); // SESSION_SETUP (0x0001)
168    h[18..20].copy_from_slice(&1u16.to_le_bytes()); // CreditRequest
169    h[28..36].copy_from_slice(&2u64.to_le_bytes()); // MessageId = 2
170
171    let b = &mut pkt[4 + smb2_header_size..];
172    b[0..2].copy_from_slice(&25u16.to_le_bytes()); // StructureSize = 25
173    b[2] = 0; // Flags
174    b[3] = 1; // SecurityMode
175    b[4..8].copy_from_slice(&0u32.to_le_bytes()); // Capabilities
176    b[8..12].copy_from_slice(&0u32.to_le_bytes()); // Channel
177    b[12..14].copy_from_slice(&88u16.to_le_bytes()); // SecurityBufferOffset = 64 + 24
178    b[14..16].copy_from_slice(&(ntlm_len as u16).to_le_bytes()); // SecurityBufferLength
179    b[16..24].copy_from_slice(&0u64.to_le_bytes()); // PreviousSessionId
180
181    b[24..24 + ntlm_len].copy_from_slice(&ntlm);
182
183    pkt
184}
185
186/// Parse SMB2 SESSION_SETUP_RESPONSE and extract NTLM Type 2 TargetInfo MsvAvTimestamp.
187fn parse_session_setup_response(b: &[u8]) -> Result<SystemTime, TimeSourceError> {
188    // Check SMB2 Header Status (offset 8)
189    if b.len() < 64 {
190        return Err(TimeSourceError::Parse("SMB2 response too short".into()));
191    }
192    let status = u32::from_le_bytes([b[8], b[9], b[10], b[11]]);
193    if status != 0xC0000016 {
194        // STATUS_MORE_PROCESSING_REQUIRED
195        return Err(TimeSourceError::Protocol(format!(
196            "Expected MORE_PROCESSING_REQUIRED, got 0x{:08X}",
197            status
198        )));
199    }
200
201    // SMB2 SESSION_SETUP_RESPONSE body starts at offset 64
202    let body = &b[64..];
203    if body.len() < 9 {
204        return Err(TimeSourceError::Parse(
205            "SMB2 SESSION_SETUP_RESPONSE body too short".into(),
206        ));
207    }
208
209    let struct_size = u16::from_le_bytes([body[0], body[1]]);
210    if struct_size != 9 {
211        return Err(TimeSourceError::Protocol(
212            "Unexpected SESSION_SETUP_RESPONSE structure size".into(),
213        ));
214    }
215
216    let sec_offset = u16::from_le_bytes([body[4], body[5]]) as usize;
217    let sec_len = u16::from_le_bytes([body[6], body[7]]) as usize;
218
219    let sec_end = sec_offset
220        .checked_add(sec_len)
221        .ok_or_else(|| TimeSourceError::Parse("SecurityBuffer overflow".into()))?;
222    if sec_offset < 64 || sec_end > b.len() {
223        return Err(TimeSourceError::Parse(
224            "SecurityBuffer out of bounds".into(),
225        ));
226    }
227
228    let ntlm = &b[sec_offset..sec_end];
229    parse_ntlm_type2(ntlm)
230}
231
232/// MS-NLMP §2.2.1.2 CHALLENGE_MESSAGE
233fn parse_ntlm_type2(ntlm: &[u8]) -> Result<SystemTime, TimeSourceError> {
234    if ntlm.len() < 48 {
235        return Err(TimeSourceError::Parse(
236            "NTLM Type 2 too short for TargetInfoFields".into(),
237        ));
238    }
239    if &ntlm[0..8] != b"NTLMSSP\0" {
240        return Err(TimeSourceError::Parse("Invalid NTLMSSP signature".into()));
241    }
242    let msg_type = u32::from_le_bytes([ntlm[8], ntlm[9], ntlm[10], ntlm[11]]);
243    if msg_type != 2 {
244        return Err(TimeSourceError::Parse(format!(
245            "Expected NTLM Type 2, got {}",
246            msg_type
247        )));
248    }
249
250    // TargetInfoFields is at offset 40
251    let target_info_len = u16::from_le_bytes([ntlm[40], ntlm[41]]) as usize;
252    let target_info_offset = u32::from_le_bytes([ntlm[44], ntlm[45], ntlm[46], ntlm[47]]) as usize;
253
254    let target_info_end = target_info_offset
255        .checked_add(target_info_len)
256        .ok_or_else(|| TimeSourceError::Parse("TargetInfo overflow".into()))?;
257    if target_info_end > ntlm.len() {
258        return Err(TimeSourceError::Parse(
259            "TargetInfo out of bounds in NTLM".into(),
260        ));
261    }
262
263    let target_info = &ntlm[target_info_offset..target_info_end];
264
265    // MS-NLMP §2.2.2.1 AV_PAIR
266    let mut pos: usize = 0;
267    while let Some(end_check) = pos.checked_add(4) {
268        if end_check > target_info.len() {
269            break;
270        }
271
272        let av_id = u16::from_le_bytes([target_info[pos], target_info[pos + 1]]);
273        let av_len = u16::from_le_bytes([target_info[pos + 2], target_info[pos + 3]]) as usize;
274        pos += 4;
275
276        let av_end = pos
277            .checked_add(av_len)
278            .ok_or_else(|| TimeSourceError::Parse("AV_PAIR overflow".into()))?;
279        if av_end > target_info.len() {
280            return Err(TimeSourceError::Parse(
281                "AV_PAIR length out of bounds".into(),
282            ));
283        }
284
285        if av_id == 7 {
286            // MsvAvTimestamp
287            if av_len != 8 {
288                return Err(TimeSourceError::Parse(
289                    "MsvAvTimestamp has invalid length".into(),
290                ));
291            }
292            let filetime = u64::from_le_bytes([
293                target_info[pos],
294                target_info[pos + 1],
295                target_info[pos + 2],
296                target_info[pos + 3],
297                target_info[pos + 4],
298                target_info[pos + 5],
299                target_info[pos + 6],
300                target_info[pos + 7],
301            ]);
302            return filetime_to_system_time(filetime);
303        } else if av_id == 0 {
304            // MsvAvEOL
305            break;
306        }
307
308        pos += av_len;
309    }
310
311    Err(TimeSourceError::Parse(
312        "MsvAvTimestamp (AV_PAIR 7) not found in NTLM TargetInfo".into(),
313    ))
314}
315
316fn read_smb_message(stream: &mut TcpStream) -> Result<Vec<u8>, TimeSourceError> {
317    let mut nb_header = [0u8; 4];
318    stream.read_exact(&mut nb_header).map_err(map_io_err)?;
319    let msg_len = u32::from_be_bytes(nb_header) & 0x00FF_FFFF;
320    if msg_len > 65536 {
321        return Err(TimeSourceError::Protocol(format!(
322            "SMB2 response too large: {}",
323            msg_len
324        )));
325    }
326    let mut body = vec![0u8; msg_len as usize];
327    stream.read_exact(&mut body).map_err(map_io_err)?;
328    Ok(body)
329}
330
331fn map_io_err(e: std::io::Error) -> TimeSourceError {
332    use std::io::ErrorKind::*;
333    match e.kind() {
334        TimedOut | WouldBlock => TimeSourceError::Timeout,
335        ConnectionRefused => TimeSourceError::Refused,
336        _ => TimeSourceError::Protocol(e.to_string()),
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343    use std::time::UNIX_EPOCH;
344
345    #[test]
346    fn build_session_setup_type1_has_correct_structure() {
347        let req = build_session_setup_type1();
348        // NetBIOS length
349        assert_eq!(req[0], 0);
350        let len = u32::from_be_bytes([0, req[1], req[2], req[3]]);
351        assert_eq!(len as usize, req.len() - 4);
352
353        // SMB2 Header
354        assert_eq!(&req[4..8], b"\xfeSMB");
355        // Command == 1
356        assert_eq!(&req[16..18], &[1, 0]);
357
358        // SecurityBufferOffset = 88
359        assert_eq!(&req[80..82], &[88, 0]);
360        // NTLMSSP
361        assert_eq!(&req[92..100], b"NTLMSSP\0");
362    }
363
364    // Mocking a real NTLM Type 2 parsing requires a real packet structure.
365    // For now, we verify the logic manually with synthetic data.
366    #[test]
367    fn parse_ntlm_type2_extracts_timestamp() {
368        let mut ntlm = vec![0u8; 60];
369        ntlm[0..8].copy_from_slice(b"NTLMSSP\0");
370        ntlm[8..12].copy_from_slice(&2u32.to_le_bytes()); // Type 2
371
372        // TargetInfoFields
373        ntlm[40..42].copy_from_slice(&12u16.to_le_bytes()); // Len = 12
374        ntlm[42..44].copy_from_slice(&12u16.to_le_bytes()); // MaxLen = 12
375        ntlm[44..48].copy_from_slice(&48u32.to_le_bytes()); // Offset = 48
376
377        // TargetInfoBuffer at 48: AvId 7, AvLen 8, Value = 133485408000000000 (2024-01-01)
378        ntlm[48..50].copy_from_slice(&7u16.to_le_bytes());
379        ntlm[50..52].copy_from_slice(&8u16.to_le_bytes());
380        let ft: u64 = 133_485_408_000_000_000;
381        ntlm[52..60].copy_from_slice(&ft.to_le_bytes());
382
383        let st = parse_ntlm_type2(&ntlm).unwrap();
384        let unix_secs = st.duration_since(UNIX_EPOCH).unwrap().as_secs();
385        assert_eq!(unix_secs, 1_704_067_200);
386    }
387}