ad_time/protocols/
ntlm.rs1use 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
29const 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 let negotiate_req = build_negotiate_request();
61 stream
62 .write_all(&negotiate_req)
63 .map_err(|e| TimeSourceError::Protocol(e.to_string()))?;
64
65 let _neg_resp = read_smb_message(&mut stream)?;
67
68 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 let setup_resp = read_smb_message(&mut stream)?;
76
77 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()); h[18..20].copy_from_slice(&1u16.to_le_bytes()); h[28..36].copy_from_slice(&1u64.to_le_bytes()); 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()); b[8..12].copy_from_slice(&SMB2_CAPABILITIES.to_le_bytes());
117
118 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; guid[8] = (guid[8] & 0x3F) | 0x80; 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 let mut ntlm = vec![];
138 ntlm.extend_from_slice(b"NTLMSSP\0");
139 ntlm.extend_from_slice(&1u32.to_le_bytes()); let flags: u32 = 0xE0000205;
146 ntlm.extend_from_slice(&flags.to_le_bytes());
147
148 ntlm.extend_from_slice(&[0u8; 16]); let ntlm_len = ntlm.len();
152
153 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()); h[18..20].copy_from_slice(&1u16.to_le_bytes()); h[28..36].copy_from_slice(&2u64.to_le_bytes()); let b = &mut pkt[4 + smb2_header_size..];
172 b[0..2].copy_from_slice(&25u16.to_le_bytes()); b[2] = 0; b[3] = 1; b[4..8].copy_from_slice(&0u32.to_le_bytes()); b[8..12].copy_from_slice(&0u32.to_le_bytes()); b[12..14].copy_from_slice(&88u16.to_le_bytes()); b[14..16].copy_from_slice(&(ntlm_len as u16).to_le_bytes()); b[16..24].copy_from_slice(&0u64.to_le_bytes()); b[24..24 + ntlm_len].copy_from_slice(&ntlm);
182
183 pkt
184}
185
186fn parse_session_setup_response(b: &[u8]) -> Result<SystemTime, TimeSourceError> {
188 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 return Err(TimeSourceError::Protocol(format!(
196 "Expected MORE_PROCESSING_REQUIRED, got 0x{:08X}",
197 status
198 )));
199 }
200
201 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
232fn 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 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 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 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 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 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 assert_eq!(&req[4..8], b"\xfeSMB");
355 assert_eq!(&req[16..18], &[1, 0]);
357
358 assert_eq!(&req[80..82], &[88, 0]);
360 assert_eq!(&req[92..100], b"NTLMSSP\0");
362 }
363
364 #[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()); ntlm[40..42].copy_from_slice(&12u16.to_le_bytes()); ntlm[42..44].copy_from_slice(&12u16.to_le_bytes()); ntlm[44..48].copy_from_slice(&48u32.to_le_bytes()); 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}