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, 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 let negotiate_req = build_negotiate_request();
60 stream
61 .write_all(&negotiate_req)
62 .map_err(|e| TimeSourceError::Protocol(e.to_string()))?;
63
64 let _neg_resp = read_smb_message(&mut stream)?;
66
67 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 let setup_resp = read_smb_message(&mut stream)?;
75
76 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 let mut ntlm = vec![];
94 ntlm.extend_from_slice(b"NTLMSSP\0");
95 ntlm.extend_from_slice(&1u32.to_le_bytes()); let flags: u32 = 0xE2888233;
109 ntlm.extend_from_slice(&flags.to_le_bytes());
110
111 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 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 ntlm.extend_from_slice(&[0x0A, 0x00, 0x61, 0x4A, 0x00, 0x00, 0x00, 0x0F]);
128
129 let ntlm_len = ntlm.len();
130
131 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()); 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..];
150 b[0..2].copy_from_slice(&25u16.to_le_bytes()); b[2] = 0; b[3] = 1; b[4..8].copy_from_slice(&0x01u32.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);
160
161 pkt
162}
163
164fn parse_session_setup_response(b: &[u8]) -> Result<SystemTime, TimeSourceError> {
166 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 return Err(TimeSourceError::Protocol(format!(
174 "Expected MORE_PROCESSING_REQUIRED, got 0x{:08X}",
175 status
176 )));
177 }
178
179 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
210fn 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 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 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 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 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 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 assert_eq!(&req[4..8], b"\xfeSMB");
328 assert_eq!(&req[16..18], &[1, 0]);
330
331 assert_eq!(&req[80..82], &[88, 0]);
333 assert_eq!(&req[92..100], b"NTLMSSP\0");
335 }
336
337 #[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()); 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());
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}