/src/suricata/rust/src/smb/smb2.rs
Line | Count | Source |
1 | | /* Copyright (C) 2017-2026 Open Information Security Foundation |
2 | | * |
3 | | * You can copy, redistribute or modify this Program under the terms of |
4 | | * the GNU General Public License version 2 as published by the Free |
5 | | * Software Foundation. |
6 | | * |
7 | | * This program is distributed in the hope that it will be useful, |
8 | | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
9 | | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
10 | | * GNU General Public License for more details. |
11 | | * |
12 | | * You should have received a copy of the GNU General Public License |
13 | | * version 2 along with this program; if not, write to the Free Software |
14 | | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
15 | | * 02110-1301, USA. |
16 | | */ |
17 | | |
18 | | use nom8::Err; |
19 | | |
20 | | use crate::direction::Direction; |
21 | | use crate::smb::dcerpc::*; |
22 | | use crate::smb::events::*; |
23 | | use crate::smb::files::*; |
24 | | use crate::smb::smb::*; |
25 | | use crate::smb::smb2_ioctl::*; |
26 | | use crate::smb::smb2_records::*; |
27 | | use crate::smb::smb2_session::*; |
28 | | use crate::smb::smb_status::*; |
29 | | |
30 | | pub const SMB2_COMMAND_NEGOTIATE_PROTOCOL: u16 = 0; |
31 | | pub const SMB2_COMMAND_SESSION_SETUP: u16 = 1; |
32 | | pub const SMB2_COMMAND_SESSION_LOGOFF: u16 = 2; |
33 | | pub const SMB2_COMMAND_TREE_CONNECT: u16 = 3; |
34 | | pub const SMB2_COMMAND_TREE_DISCONNECT: u16 = 4; |
35 | | pub const SMB2_COMMAND_CREATE: u16 = 5; |
36 | | pub const SMB2_COMMAND_CLOSE: u16 = 6; |
37 | | pub const SMB2_COMMAND_FLUSH: u16 = 7; |
38 | | pub const SMB2_COMMAND_READ: u16 = 8; |
39 | | pub const SMB2_COMMAND_WRITE: u16 = 9; |
40 | | pub const SMB2_COMMAND_LOCK: u16 = 10; |
41 | | pub const SMB2_COMMAND_IOCTL: u16 = 11; |
42 | | pub const SMB2_COMMAND_CANCEL: u16 = 12; |
43 | | pub const SMB2_COMMAND_KEEPALIVE: u16 = 13; |
44 | | pub const SMB2_COMMAND_FIND: u16 = 14; |
45 | | pub const SMB2_COMMAND_CHANGE_NOTIFY: u16 = 15; |
46 | | pub const SMB2_COMMAND_GET_INFO: u16 = 16; |
47 | | pub const SMB2_COMMAND_SET_INFO: u16 = 17; |
48 | | pub const SMB2_COMMAND_OPLOCK_BREAK: u16 = 18; |
49 | | |
50 | 71.7k | pub fn smb2_command_string(c: u16) -> String { |
51 | 71.7k | match c { |
52 | 1.69k | SMB2_COMMAND_NEGOTIATE_PROTOCOL => "SMB2_COMMAND_NEGOTIATE_PROTOCOL", |
53 | 2.52k | SMB2_COMMAND_SESSION_SETUP => "SMB2_COMMAND_SESSION_SETUP", |
54 | 340 | SMB2_COMMAND_SESSION_LOGOFF => "SMB2_COMMAND_SESSION_LOGOFF", |
55 | 1.56k | SMB2_COMMAND_TREE_CONNECT => "SMB2_COMMAND_TREE_CONNECT", |
56 | 782 | SMB2_COMMAND_TREE_DISCONNECT => "SMB2_COMMAND_TREE_DISCONNECT", |
57 | 20.8k | SMB2_COMMAND_CREATE => "SMB2_COMMAND_CREATE", |
58 | 18.9k | SMB2_COMMAND_CLOSE => "SMB2_COMMAND_CLOSE", |
59 | 2.91k | SMB2_COMMAND_READ => "SMB2_COMMAND_READ", |
60 | 238 | SMB2_COMMAND_FLUSH => "SMB2_COMMAND_FLUSH", |
61 | 2.56k | SMB2_COMMAND_WRITE => "SMB2_COMMAND_WRITE", |
62 | 0 | SMB2_COMMAND_LOCK => "SMB2_COMMAND_LOCK", |
63 | 8.85k | SMB2_COMMAND_IOCTL => "SMB2_COMMAND_IOCTL", |
64 | 0 | SMB2_COMMAND_CANCEL => "SMB2_COMMAND_CANCEL", |
65 | 2.74k | SMB2_COMMAND_KEEPALIVE => "SMB2_COMMAND_KEEPALIVE", |
66 | 6.50k | SMB2_COMMAND_FIND => "SMB2_COMMAND_FIND", |
67 | 393 | SMB2_COMMAND_CHANGE_NOTIFY => "SMB2_COMMAND_CHANGE_NOTIFY", |
68 | 0 | SMB2_COMMAND_GET_INFO => "SMB2_COMMAND_GET_INFO", |
69 | 274 | SMB2_COMMAND_SET_INFO => "SMB2_COMMAND_SET_INFO", |
70 | 491 | SMB2_COMMAND_OPLOCK_BREAK => "SMB2_COMMAND_OPLOCK_BREAK", |
71 | | _ => { |
72 | 45 | return (c).to_string(); |
73 | | } |
74 | | } |
75 | 71.7k | .to_string() |
76 | 71.7k | } |
77 | | |
78 | 138k | pub fn smb2_dialect_string(d: u16) -> String { |
79 | 138k | match d { |
80 | 16.8k | 0x0202 => "2.02", |
81 | 7.20k | 0x0210 => "2.10", |
82 | 96 | 0x0222 => "2.22", |
83 | 153 | 0x0224 => "2.24", |
84 | 2.63k | 0x02ff => "2.??", |
85 | 1.67k | 0x0300 => "3.00", |
86 | 51.4k | 0x0302 => "3.02", |
87 | 155 | 0x0310 => "3.10", |
88 | 1.44k | 0x0311 => "3.11", |
89 | | _ => { |
90 | 56.8k | return (d).to_string(); |
91 | | } |
92 | | } |
93 | 81.6k | .to_string() |
94 | 138k | } |
95 | | |
96 | | // later we'll use this to determine if we need to |
97 | | // track a ssn per type |
98 | 115k | fn smb2_create_new_tx(cmd: u16) -> bool { |
99 | 115k | match cmd { |
100 | | SMB2_COMMAND_READ | SMB2_COMMAND_WRITE | SMB2_COMMAND_GET_INFO | SMB2_COMMAND_SET_INFO => { |
101 | 72.3k | false |
102 | | } |
103 | 42.9k | _ => true, |
104 | | } |
105 | 115k | } |
106 | | |
107 | 11.0k | fn smb2_read_response_record_generic(state: &mut SMBState, r: &Smb2Record) { |
108 | 11.0k | if smb2_create_new_tx(r.command) { |
109 | 0 | let tx_hdr = SMBCommonHdr::from2(r, SMBHDR_TYPE_GENERICTX); |
110 | 0 | let tx = state.get_generic_tx(2, r.command, &tx_hdr); |
111 | 0 | if let Some(tx) = tx { |
112 | 0 | tx.set_status(r.nt_status, false); |
113 | 0 | tx.response_done = true; |
114 | 0 | } |
115 | 11.0k | } |
116 | 11.0k | } |
117 | | |
118 | 11.0k | pub fn smb2_read_response_record(state: &mut SMBState, r: &Smb2Record, nbss_remaining: u32) { |
119 | 11.0k | smb2_read_response_record_generic(state, r); |
120 | | |
121 | 11.0k | match parse_smb2_response_read(r.data) { |
122 | 10.1k | Ok((_, rd)) => { |
123 | 10.1k | if rd.len - rd.data.len() as u32 > nbss_remaining { |
124 | | // Record claims more bytes than are in NBSS record... |
125 | 1.49k | state.set_event(SMBEvent::ReadResponseTooLarge); |
126 | | // Skip the remaining bytes of the record. |
127 | 1.49k | state.set_skip(Direction::ToClient, nbss_remaining); |
128 | 1.49k | return; |
129 | 8.66k | } |
130 | 8.66k | if r.nt_status == SMB_NTSTATUS_BUFFER_OVERFLOW { |
131 | 3.41k | SCLogDebug!("SMBv2/READ: incomplete record, expecting a follow up"); |
132 | 3.41k | // fall through |
133 | 5.24k | } else if r.nt_status != SMB_NTSTATUS_SUCCESS { |
134 | | SCLogDebug!("SMBv2: read response error code received: skip record"); |
135 | 4 | state.set_skip(Direction::ToClient, nbss_remaining); |
136 | 4 | return; |
137 | 5.24k | } |
138 | | |
139 | 8.66k | if (state.max_read_size != 0 && rd.len > state.max_read_size) |
140 | 8.65k | || (unsafe { SMB_CFG_MAX_READ_SIZE != 0 && SMB_CFG_MAX_READ_SIZE < rd.len }) |
141 | | { |
142 | 5 | state.set_event(SMBEvent::ReadResponseTooLarge); |
143 | 5 | state.set_skip(Direction::ToClient, nbss_remaining); |
144 | 5 | return; |
145 | 8.65k | } |
146 | | |
147 | | SCLogDebug!("SMBv2: read response => {:?}", rd); |
148 | | |
149 | | // get the request info. If we don't have it, there is nothing |
150 | | // we can do except skip this record. |
151 | 8.65k | let guid_key = SMBCommonHdr::from2_notree(r, SMBHDR_TYPE_OFFSET); |
152 | 8.65k | let (offset, file_guid) = match state.read_offset_cache.pop(&guid_key) { |
153 | 7.38k | Some(o) => (o.offset, o.guid), |
154 | | None => { |
155 | | SCLogDebug!("SMBv2 READ response: reply to unknown request {:?}", rd); |
156 | 1.27k | state.set_skip(Direction::ToClient, nbss_remaining); |
157 | 1.27k | return; |
158 | | } |
159 | | }; |
160 | | SCLogDebug!("SMBv2 READ: GUID {:?} offset {}", file_guid, offset); |
161 | | |
162 | 7.38k | let mut set_event_fileoverlap = false; |
163 | | // set_file_left must not claim bytes a skip already consumed |
164 | 7.38k | let mut rejected = false; |
165 | | // look up existing tracker and if we have it update it |
166 | 7.38k | let found = if let Some(tx) = |
167 | 7.38k | state.get_file_tx_by_fuid_with_open_file(&file_guid, Direction::ToClient) |
168 | | { |
169 | 1.60k | if let Some(SMBTransactionTypeData::FILE(ref mut tdf)) = tx.type_data { |
170 | 1.60k | let file_id: u32 = tx.id as u32; |
171 | 1.60k | if offset < tdf.file_tracker.tracked { |
172 | 695 | set_event_fileoverlap = true; |
173 | 908 | } |
174 | 0 | if let Some(ev) = |
175 | 1.60k | smb_queue_limit_event(&tdf.file_tracker, offset, rd.len as u64, true) |
176 | 0 | { |
177 | 0 | // Attach to the affected file tx, not the newest tx. |
178 | 0 | tx.set_event(ev); |
179 | 0 | tx.tx_data.0.updated_ts = true; |
180 | 0 | tx.tx_data.0.updated_tc = true; |
181 | 0 | state.set_skip(Direction::ToClient, nbss_remaining); |
182 | 0 | rejected = true; |
183 | 1.60k | } else { |
184 | 1.60k | filetracker_newchunk( |
185 | 1.60k | &mut tdf.file_tracker, |
186 | 1.60k | &tdf.file_name, |
187 | 1.60k | rd.data, |
188 | 1.60k | offset, |
189 | 1.60k | rd.len, |
190 | 1.60k | false, |
191 | 1.60k | &file_id, |
192 | 1.60k | ); |
193 | 1.60k | } |
194 | 0 | } |
195 | 1.60k | true |
196 | | } else { |
197 | 5.77k | false |
198 | | }; |
199 | | SCLogDebug!("existing file tx? {}", found); |
200 | 7.38k | if !found { |
201 | 5.77k | let tree_key = SMBCommonHdr::from2(r, SMBHDR_TYPE_SHARE); |
202 | 5.77k | let (share_name, mut is_pipe) = match state.ssn2tree_cache.get(&tree_key) { |
203 | 4.66k | Some(n) => (n.name.to_vec(), n.is_pipe), |
204 | 1.11k | _ => (Vec::new(), false), |
205 | | }; |
206 | 5.77k | let mut is_dcerpc = if is_pipe || share_name.is_empty() { |
207 | 4.42k | state.get_service_for_guid(&file_guid).1 |
208 | | } else { |
209 | 1.35k | false |
210 | | }; |
211 | | SCLogDebug!( |
212 | | "SMBv2/READ: share_name {:?} is_pipe {} is_dcerpc {}", |
213 | | share_name, |
214 | | is_pipe, |
215 | | is_dcerpc |
216 | | ); |
217 | | |
218 | 5.77k | if share_name.is_empty() && !is_pipe { |
219 | | SCLogDebug!("SMBv2/READ: no tree connect seen, we don't know if we are a pipe"); |
220 | | |
221 | 1.12k | if smb_dcerpc_probe(rd.data) { |
222 | | SCLogDebug!("SMBv2/READ: looks like dcerpc"); |
223 | | // insert fake tree to assist in follow up lookups |
224 | 444 | let tree = SMBTree::new(b"suricata::dcerpc".to_vec(), true); |
225 | 444 | state.ssn2tree_cache.put(tree_key, tree); |
226 | 444 | if !is_dcerpc { |
227 | 384 | _ = state |
228 | 384 | .guid2name_cache |
229 | 384 | .put(file_guid.to_vec(), b"suricata::dcerpc".to_vec()); |
230 | 384 | } |
231 | 444 | is_pipe = true; |
232 | 444 | is_dcerpc = true; |
233 | 681 | } else { |
234 | 681 | SCLogDebug!("SMBv2/READ: not DCERPC"); |
235 | 681 | } |
236 | 4.65k | } |
237 | | |
238 | 5.77k | if is_pipe && is_dcerpc { |
239 | 3.49k | SCLogDebug!("SMBv2 DCERPC read"); |
240 | 3.49k | let hdr = SMBCommonHdr::from2(r, SMBHDR_TYPE_HEADER); |
241 | 3.49k | let vercmd = SMBVerCmdStat::new2_with_ntstatus(SMB2_COMMAND_READ, r.nt_status); |
242 | 3.49k | smb_read_dcerpc_record(state, vercmd, hdr, &file_guid, rd.data); |
243 | 3.49k | rejected = true; |
244 | 3.49k | } else if is_pipe { |
245 | 244 | SCLogDebug!("non-DCERPC pipe"); |
246 | 244 | state.set_skip(Direction::ToClient, nbss_remaining); |
247 | 244 | rejected = true; |
248 | 244 | } else { |
249 | 2.03k | let file_name = match state.guid2name_cache.get(&file_guid) { |
250 | 1.25k | Some(n) => n.to_vec(), |
251 | 781 | None => b"<unknown>".to_vec(), |
252 | | }; |
253 | | |
254 | 2.03k | let Some(tx) = state.new_file_tx(&file_guid, &file_name, Direction::ToClient) |
255 | | else { |
256 | 0 | return; |
257 | | }; |
258 | 2.03k | tx.vercmd.set_smb2_cmd(SMB2_COMMAND_READ); |
259 | 2.03k | tx.hdr = SMBCommonHdr::new(SMBHDR_TYPE_HEADER, r.session_id, r.tree_id, 0); // TODO move into new_file_tx |
260 | 2.03k | if let Some(SMBTransactionTypeData::FILE(ref mut tdf)) = tx.type_data { |
261 | 2.03k | tdf.share_name = share_name; |
262 | 2.03k | let file_id: u32 = tx.id as u32; |
263 | 2.03k | if offset < tdf.file_tracker.tracked { |
264 | 0 | set_event_fileoverlap = true; |
265 | 2.03k | } |
266 | 0 | if let Some(ev) = |
267 | 2.03k | smb_queue_limit_event(&tdf.file_tracker, offset, rd.len as u64, true) |
268 | 0 | { |
269 | 0 | // tx just created: set the event on it directly. |
270 | 0 | tx.set_event(ev); |
271 | 0 | tx.tx_data.0.updated_ts = true; |
272 | 0 | tx.tx_data.0.updated_tc = true; |
273 | 0 | // Chunk rejected before the file was opened: complete + close the |
274 | 0 | // tx and zero files_opened, else it dangles until teardown. |
275 | 0 | tx.request_done = true; |
276 | 0 | tx.response_done = true; |
277 | 0 | tx.tx_data.0.files_opened = 0; |
278 | 0 | state.set_skip(Direction::ToClient, nbss_remaining); |
279 | 0 | rejected = true; |
280 | 2.03k | } else { |
281 | 2.03k | filetracker_newchunk( |
282 | 2.03k | &mut tdf.file_tracker, |
283 | 2.03k | &file_name, |
284 | 2.03k | rd.data, |
285 | 2.03k | offset, |
286 | 2.03k | rd.len, |
287 | 2.03k | false, |
288 | 2.03k | &file_id, |
289 | 2.03k | ); |
290 | 2.03k | } |
291 | 0 | } |
292 | | } |
293 | 1.60k | } |
294 | | |
295 | 7.38k | if set_event_fileoverlap { |
296 | 695 | state.set_event(SMBEvent::FileOverlap); |
297 | 6.68k | } |
298 | 7.38k | if !rejected { |
299 | 3.63k | state.set_file_left( |
300 | 3.63k | Direction::ToClient, |
301 | 3.63k | rd.len, |
302 | 3.63k | rd.data.len() as u32, |
303 | 3.63k | file_guid.to_vec(), |
304 | 3.63k | ); |
305 | 3.74k | } |
306 | | } |
307 | 877 | _ => { |
308 | 877 | SCLogDebug!("SMBv2: failed to parse read response"); |
309 | 877 | state.set_event(SMBEvent::MalformedData); |
310 | 877 | } |
311 | | } |
312 | 11.0k | } |
313 | | |
314 | 35.4k | pub fn smb2_write_request_record(state: &mut SMBState, r: &Smb2Record, nbss_remaining: u32) { |
315 | | SCLogDebug!("SMBv2/WRITE: request record"); |
316 | 35.4k | if smb2_create_new_tx(r.command) { |
317 | 0 | let tx_key = SMBCommonHdr::from2(r, SMBHDR_TYPE_GENERICTX); |
318 | 0 | let Some(tx) = state.new_generic_tx(2, r.command, tx_key) else { |
319 | 0 | return; |
320 | | }; |
321 | 0 | tx.request_done = true; |
322 | 35.4k | } |
323 | 35.4k | match parse_smb2_request_write(r.data) { |
324 | 34.0k | Ok((_, wr)) => { |
325 | 34.0k | if wr.wr_len - wr.data.len() as u32 > nbss_remaining { |
326 | | // Record claims more bytes than are in NBSS record... |
327 | 5.10k | state.set_event(SMBEvent::WriteRequestTooLarge); |
328 | | // Skip the remaining bytes of the record. |
329 | 5.10k | state.set_skip(Direction::ToServer, nbss_remaining); |
330 | 5.10k | return; |
331 | 28.9k | } |
332 | 28.9k | if (state.max_write_size != 0 && wr.wr_len > state.max_write_size) |
333 | 28.5k | || (unsafe { SMB_CFG_MAX_WRITE_SIZE != 0 && SMB_CFG_MAX_WRITE_SIZE < wr.wr_len }) |
334 | | { |
335 | 373 | state.set_event(SMBEvent::WriteRequestTooLarge); |
336 | 373 | state.set_skip(Direction::ToServer, nbss_remaining); |
337 | 373 | return; |
338 | 28.5k | } |
339 | | |
340 | | /* update key-guid map */ |
341 | 28.5k | let guid_key = SMBCommonHdr::from2(r, SMBHDR_TYPE_GUID); |
342 | 28.5k | state.ssn2vec_cache.put(guid_key, wr.guid.to_vec()); |
343 | | |
344 | 28.5k | let file_guid = wr.guid.to_vec(); |
345 | 28.5k | let file_name = match state.guid2name_cache.get(&file_guid) { |
346 | 16.3k | Some(n) => n.to_vec(), |
347 | 12.2k | None => Vec::new(), |
348 | | }; |
349 | | |
350 | 28.5k | let mut set_event_fileoverlap = false; |
351 | | // set_file_left must not claim bytes a skip already consumed |
352 | 28.5k | let mut rejected = false; |
353 | 28.5k | let found = if let Some(tx) = |
354 | 28.5k | state.get_file_tx_by_fuid_with_open_file(&file_guid, Direction::ToServer) |
355 | | { |
356 | 7.37k | if let Some(SMBTransactionTypeData::FILE(ref mut tdf)) = tx.type_data { |
357 | 7.37k | let file_id: u32 = tx.id as u32; |
358 | 7.37k | if wr.wr_offset < tdf.file_tracker.tracked { |
359 | 480 | set_event_fileoverlap = true; |
360 | 6.89k | } |
361 | 7.37k | if let Some(ev) = smb_queue_limit_event( |
362 | 7.37k | &tdf.file_tracker, |
363 | 7.37k | wr.wr_offset, |
364 | 7.37k | wr.wr_len as u64, |
365 | 7.37k | false, |
366 | 7.37k | ) { |
367 | 985 | // Attach to the affected file tx, not the newest tx. |
368 | 985 | tx.set_event(ev); |
369 | 985 | tx.tx_data.0.updated_ts = true; |
370 | 985 | tx.tx_data.0.updated_tc = true; |
371 | 985 | state.set_skip(Direction::ToServer, nbss_remaining); |
372 | 985 | rejected = true; |
373 | 6.39k | } else { |
374 | 6.39k | filetracker_newchunk( |
375 | 6.39k | &mut tdf.file_tracker, |
376 | 6.39k | &file_name, |
377 | 6.39k | wr.data, |
378 | 6.39k | wr.wr_offset, |
379 | 6.39k | wr.wr_len, |
380 | 6.39k | false, |
381 | 6.39k | &file_id, |
382 | 6.39k | ); |
383 | 6.39k | } |
384 | 0 | } |
385 | 7.37k | true |
386 | | } else { |
387 | 21.2k | false |
388 | | }; |
389 | 28.5k | if !found { |
390 | 21.2k | let tree_key = SMBCommonHdr::from2(r, SMBHDR_TYPE_SHARE); |
391 | 21.2k | let (share_name, mut is_pipe) = match state.ssn2tree_cache.get(&tree_key) { |
392 | 17.1k | Some(n) => (n.name.to_vec(), n.is_pipe), |
393 | 4.09k | _ => (Vec::new(), false), |
394 | | }; |
395 | 21.2k | let mut is_dcerpc = if is_pipe || share_name.is_empty() { |
396 | 20.2k | state.get_service_for_guid(wr.guid).1 |
397 | | } else { |
398 | 971 | false |
399 | | }; |
400 | | SCLogDebug!( |
401 | | "SMBv2/WRITE: share_name {:?} is_pipe {} is_dcerpc {}", |
402 | | share_name, |
403 | | is_pipe, |
404 | | is_dcerpc |
405 | | ); |
406 | | |
407 | | // if we missed the TREE connect we can't be sure if 'is_dcerpc' is correct |
408 | 21.2k | if share_name.is_empty() && !is_pipe { |
409 | | SCLogDebug!( |
410 | | "SMBv2/WRITE: no tree connect seen, we don't know if we are a pipe" |
411 | | ); |
412 | | |
413 | 4.13k | if smb_dcerpc_probe(wr.data) { |
414 | | SCLogDebug!("SMBv2/WRITE: looks like we have dcerpc"); |
415 | | |
416 | 776 | let tree = SMBTree::new(b"suricata::dcerpc".to_vec(), true); |
417 | 776 | state.ssn2tree_cache.put(tree_key, tree); |
418 | 776 | if !is_dcerpc { |
419 | 627 | _ = state |
420 | 627 | .guid2name_cache |
421 | 627 | .put(file_guid.to_vec(), b"suricata::dcerpc".to_vec()); |
422 | 627 | } |
423 | 776 | is_pipe = true; |
424 | 776 | is_dcerpc = true; |
425 | 3.35k | } else { |
426 | 3.35k | SCLogDebug!("SMBv2/WRITE: not DCERPC"); |
427 | 3.35k | } |
428 | 17.0k | } |
429 | 21.2k | if is_pipe && is_dcerpc { |
430 | 15.7k | SCLogDebug!("SMBv2 DCERPC write"); |
431 | 15.7k | let hdr = SMBCommonHdr::from2(r, SMBHDR_TYPE_HEADER); |
432 | 15.7k | let vercmd = SMBVerCmdStat::new2(SMB2_COMMAND_WRITE); |
433 | 15.7k | smb_write_dcerpc_record(state, vercmd, hdr, wr.data); |
434 | 15.7k | rejected = true; |
435 | 15.7k | } else if is_pipe { |
436 | 1.10k | SCLogDebug!("non-DCERPC pipe: skip rest of the record"); |
437 | 1.10k | state.set_skip(Direction::ToServer, nbss_remaining); |
438 | 1.10k | rejected = true; |
439 | 1.10k | } else { |
440 | 4.33k | let Some(tx) = state.new_file_tx(&file_guid, &file_name, Direction::ToServer) |
441 | | else { |
442 | 0 | return; |
443 | | }; |
444 | 4.33k | tx.vercmd.set_smb2_cmd(SMB2_COMMAND_WRITE); |
445 | 4.33k | tx.hdr = SMBCommonHdr::new(SMBHDR_TYPE_HEADER, r.session_id, r.tree_id, 0); // TODO move into new_file_tx |
446 | 4.33k | if let Some(SMBTransactionTypeData::FILE(ref mut tdf)) = tx.type_data { |
447 | 4.33k | let file_id: u32 = tx.id as u32; |
448 | 4.33k | if wr.wr_offset < tdf.file_tracker.tracked { |
449 | 0 | set_event_fileoverlap = true; |
450 | 4.33k | } |
451 | | |
452 | 4.33k | if let Some(ev) = smb_queue_limit_event( |
453 | 4.33k | &tdf.file_tracker, |
454 | 4.33k | wr.wr_offset, |
455 | 4.33k | wr.wr_len as u64, |
456 | 4.33k | false, |
457 | 4.33k | ) { |
458 | 0 | // tx just created: set the event on it directly. |
459 | 0 | tx.set_event(ev); |
460 | 0 | tx.tx_data.0.updated_ts = true; |
461 | 0 | tx.tx_data.0.updated_tc = true; |
462 | 0 | // Chunk rejected before the file was opened: complete + close the |
463 | 0 | // tx and zero files_opened, else it dangles until teardown. |
464 | 0 | tx.request_done = true; |
465 | 0 | tx.response_done = true; |
466 | 0 | tx.tx_data.0.files_opened = 0; |
467 | 0 | state.set_skip(Direction::ToServer, nbss_remaining); |
468 | 0 | rejected = true; |
469 | 4.33k | } else { |
470 | 4.33k | filetracker_newchunk( |
471 | 4.33k | &mut tdf.file_tracker, |
472 | 4.33k | &file_name, |
473 | 4.33k | wr.data, |
474 | 4.33k | wr.wr_offset, |
475 | 4.33k | wr.wr_len, |
476 | 4.33k | false, |
477 | 4.33k | &file_id, |
478 | 4.33k | ); |
479 | 4.33k | } |
480 | 0 | } |
481 | | } |
482 | 7.37k | } |
483 | | |
484 | 28.5k | if set_event_fileoverlap { |
485 | 480 | state.set_event(SMBEvent::FileOverlap); |
486 | 28.1k | } |
487 | 28.5k | if !rejected { |
488 | 10.7k | state.set_file_left( |
489 | 10.7k | Direction::ToServer, |
490 | 10.7k | wr.wr_len, |
491 | 10.7k | wr.data.len() as u32, |
492 | 10.7k | file_guid.to_vec(), |
493 | 10.7k | ); |
494 | 17.8k | } |
495 | | } |
496 | 1.38k | _ => { |
497 | 1.38k | state.set_event(SMBEvent::MalformedData); |
498 | 1.38k | } |
499 | | } |
500 | 35.4k | } |
501 | | |
502 | 201k | pub fn smb2_request_record(state: &mut SMBState, r: &Smb2Record) { |
503 | | SCLogDebug!( |
504 | | "SMBv2 request record, command {} tree {} session {}", |
505 | | &smb2_command_string(r.command), |
506 | | r.tree_id, |
507 | | r.session_id |
508 | | ); |
509 | | |
510 | 201k | let mut events: Vec<SMBEvent> = Vec::new(); |
511 | | |
512 | 201k | let have_tx = match r.command { |
513 | | SMB2_COMMAND_SET_INFO => { |
514 | | SCLogDebug!("SMB2_COMMAND_SET_INFO: {:?}", r); |
515 | 3.75k | let have_si_tx = match parse_smb2_request_setinfo(r.data) { |
516 | 2.26k | Ok((_, rd)) => { |
517 | | SCLogDebug!("SMB2_COMMAND_SET_INFO: {:?}", rd); |
518 | | |
519 | 2.26k | match rd.data { |
520 | 174 | Smb2SetInfoRequestData::RENAME(ref ren) => { |
521 | 174 | let tx_hdr = SMBCommonHdr::from2(r, SMBHDR_TYPE_GENERICTX); |
522 | 174 | let mut newname = ren.name.to_vec(); |
523 | 2.66k | newname.retain(|&i| i != 0x00); |
524 | 174 | let oldname = match state.guid2name_cache.get(rd.guid) { |
525 | 11 | Some(n) => n.to_vec(), |
526 | 163 | None => b"<unknown>".to_vec(), |
527 | | }; |
528 | 174 | let Some(tx) = state.new_rename_tx(rd.guid.to_vec(), oldname, newname) |
529 | | else { |
530 | 0 | return; |
531 | | }; |
532 | 174 | tx.hdr = tx_hdr; |
533 | 174 | tx.request_done = true; |
534 | 174 | tx.vercmd.set_smb2_cmd(SMB2_COMMAND_SET_INFO); |
535 | 174 | true |
536 | | } |
537 | 438 | Smb2SetInfoRequestData::DISPOSITION(ref dis) => { |
538 | 438 | let tx_hdr = SMBCommonHdr::from2(r, SMBHDR_TYPE_GENERICTX); |
539 | 438 | let fname = match state.guid2name_cache.get(rd.guid) { |
540 | 17 | Some(n) => n.to_vec(), |
541 | | None => { |
542 | | // try to find latest created file in case of chained commands |
543 | 421 | let mut guid_key = |
544 | 421 | SMBCommonHdr::from2_notree(r, SMBHDR_TYPE_FILENAME); |
545 | 421 | if guid_key.msg_id == 0 { |
546 | 0 | b"<unknown>".to_vec() |
547 | | } else { |
548 | 421 | guid_key.msg_id -= 1; |
549 | 421 | match state.ssn2vec_cache.get(&guid_key) { |
550 | 273 | Some(n) => n.to_vec(), |
551 | 148 | None => b"<unknown>".to_vec(), |
552 | | } |
553 | | } |
554 | | } |
555 | | }; |
556 | 438 | let Some(tx) = state.new_setfileinfo_tx( |
557 | 438 | fname, |
558 | 438 | rd.guid.to_vec(), |
559 | 438 | rd.class as u16, |
560 | 438 | rd.infolvl as u16, |
561 | 438 | dis.delete, |
562 | 438 | ) else { |
563 | 0 | return; |
564 | | }; |
565 | 438 | tx.hdr = tx_hdr; |
566 | 438 | tx.request_done = true; |
567 | 438 | tx.vercmd.set_smb2_cmd(SMB2_COMMAND_SET_INFO); |
568 | 438 | true |
569 | | } |
570 | 1.65k | _ => false, |
571 | | } |
572 | | } |
573 | 1.48k | Err(Err::Incomplete(_n)) => { |
574 | | SCLogDebug!("SMB2_COMMAND_SET_INFO: {:?}", _n); |
575 | 1.48k | events.push(SMBEvent::MalformedData); |
576 | 1.48k | false |
577 | | } |
578 | 0 | Err(Err::Error(_e)) | Err(Err::Failure(_e)) => { |
579 | | SCLogDebug!("SMB2_COMMAND_SET_INFO: {:?}", _e); |
580 | 0 | events.push(SMBEvent::MalformedData); |
581 | 0 | false |
582 | | } |
583 | | }; |
584 | 3.75k | have_si_tx |
585 | | } |
586 | | SMB2_COMMAND_IOCTL => { |
587 | 14.0k | smb2_ioctl_request_record(state, r); |
588 | 14.0k | true |
589 | | } |
590 | | SMB2_COMMAND_TREE_DISCONNECT => { |
591 | 1.20k | let tree_key = SMBCommonHdr::from2(r, SMBHDR_TYPE_SHARE); |
592 | 1.20k | state.ssn2tree_cache.pop(&tree_key); |
593 | 1.20k | false |
594 | | } |
595 | | SMB2_COMMAND_NEGOTIATE_PROTOCOL => { |
596 | 9.61k | if let Ok((_, rd)) = parse_smb2_request_negotiate_protocol(r.data) { |
597 | 6.06k | let mut dialects: Vec<Vec<u8>> = Vec::new(); |
598 | 78.9k | for d in rd.dialects_vec { |
599 | 72.8k | SCLogDebug!("dialect {:x} => {}", d, &smb2_dialect_string(d)); |
600 | 72.8k | let dvec = smb2_dialect_string(d).as_bytes().to_vec(); |
601 | 72.8k | dialects.push(dvec); |
602 | 72.8k | } |
603 | | |
604 | 6.06k | if state.get_negotiate_tx(2).is_none() { |
605 | 4.51k | let Some(tx) = state.new_negotiate_tx(2) else { |
606 | 0 | return; |
607 | | }; |
608 | 4.51k | if let Some(SMBTransactionTypeData::NEGOTIATE(ref mut tdn)) = tx.type_data { |
609 | 4.51k | tdn.dialects2 = dialects; |
610 | 4.51k | tdn.client_guid = Some(rd.client_guid.to_vec()); |
611 | 4.51k | } |
612 | 4.51k | tx.request_done = true; |
613 | 1.55k | } |
614 | 6.06k | true |
615 | | } else { |
616 | 3.54k | events.push(SMBEvent::MalformedData); |
617 | 3.54k | false |
618 | | } |
619 | | } |
620 | | SMB2_COMMAND_SESSION_SETUP => { |
621 | 43.9k | smb2_session_setup_request(state, r); |
622 | 43.9k | true |
623 | | } |
624 | | SMB2_COMMAND_TREE_CONNECT => { |
625 | 5.37k | if let Ok((_, tr)) = parse_smb2_request_tree_connect(r.data) { |
626 | 5.28k | let name_key = SMBCommonHdr::from2(r, SMBHDR_TYPE_TREE); |
627 | 5.28k | let mut name_val = tr.share_name.to_vec(); |
628 | 264k | name_val.retain(|&i| i != 0x00); |
629 | 5.28k | if name_val.len() > 1 { |
630 | 3.50k | name_val = name_val[1..].to_vec(); |
631 | 3.50k | } |
632 | | |
633 | 5.28k | let Some(tx) = state.new_treeconnect_tx(name_key, name_val) else { |
634 | 0 | return; |
635 | | }; |
636 | 5.28k | tx.request_done = true; |
637 | 5.28k | tx.vercmd.set_smb2_cmd(SMB2_COMMAND_TREE_CONNECT); |
638 | 5.28k | true |
639 | | } else { |
640 | 94 | events.push(SMBEvent::MalformedData); |
641 | 94 | false |
642 | | } |
643 | | } |
644 | | SMB2_COMMAND_READ => { |
645 | 11.0k | if let Ok((_, rd)) = parse_smb2_request_read(r.data) { |
646 | 9.28k | if (state.max_read_size != 0 && rd.rd_len > state.max_read_size) |
647 | 8.69k | || (unsafe { SMB_CFG_MAX_READ_SIZE != 0 && SMB_CFG_MAX_READ_SIZE < rd.rd_len }) |
648 | 843 | { |
649 | 843 | events.push(SMBEvent::ReadRequestTooLarge); |
650 | 8.44k | } else { |
651 | 8.44k | SCLogDebug!( |
652 | 8.44k | "SMBv2 READ: GUID {:?} requesting {} bytes at offset {}", |
653 | 8.44k | rd.guid, |
654 | 8.44k | rd.rd_len, |
655 | 8.44k | rd.rd_offset |
656 | 8.44k | ); |
657 | 8.44k | |
658 | 8.44k | // store read guid,offset in map |
659 | 8.44k | let guid_key = SMBCommonHdr::from2_notree(r, SMBHDR_TYPE_OFFSET); |
660 | 8.44k | let guidoff = SMBFileGUIDOffset::new(rd.guid.to_vec(), rd.rd_offset); |
661 | 8.44k | state.read_offset_cache.put(guid_key, guidoff); |
662 | 8.44k | } |
663 | 1.72k | } else { |
664 | 1.72k | events.push(SMBEvent::MalformedData); |
665 | 1.72k | } |
666 | 11.0k | false |
667 | | } |
668 | | SMB2_COMMAND_CREATE => { |
669 | 30.2k | if let Ok((_, cr)) = parse_smb2_request_create(r.data) { |
670 | 28.0k | let del = cr.create_options & 0x0000_1000 != 0; |
671 | 28.0k | let dir = cr.create_options & 0x0000_0001 != 0; |
672 | | SCLogDebug!("create_options {:08x}", cr.create_options); |
673 | | |
674 | 28.0k | let name_key = SMBCommonHdr::from2_notree(r, SMBHDR_TYPE_FILENAME); |
675 | 28.0k | state.ssn2vec_cache.put(name_key, cr.data.to_vec()); |
676 | | |
677 | 28.0k | let tx_hdr = SMBCommonHdr::from2(r, SMBHDR_TYPE_GENERICTX); |
678 | 28.0k | let Some(tx) = state.new_create_tx(cr.data, cr.disposition, del, dir, tx_hdr) |
679 | | else { |
680 | 0 | return; |
681 | | }; |
682 | 28.0k | tx.vercmd.set_smb2_cmd(r.command); |
683 | | SCLogDebug!("TS CREATE TX {} created", tx.id); |
684 | 28.0k | true |
685 | | } else { |
686 | 2.21k | events.push(SMBEvent::MalformedData); |
687 | 2.21k | false |
688 | | } |
689 | | } |
690 | | SMB2_COMMAND_WRITE => { |
691 | 34.8k | smb2_write_request_record(state, r, 0); |
692 | 34.8k | true // write handling creates both file tx and generic tx |
693 | | } |
694 | | SMB2_COMMAND_CLOSE => { |
695 | 21.8k | if let Ok((_, cd)) = parse_smb2_request_close(r.data) { |
696 | 21.7k | let _name = state.guid2name_cache.pop(cd.guid); |
697 | | |
698 | 21.7k | let found_ts = |
699 | 21.7k | if let Some(tx) = state.get_file_tx_by_fuid(cd.guid, Direction::ToServer) { |
700 | 1.16k | if !tx.request_done { |
701 | 1.13k | if let Some(SMBTransactionTypeData::FILE(ref mut tdf)) = tx.type_data { |
702 | 1.13k | filetracker_close(&mut tdf.file_tracker); |
703 | 1.13k | } |
704 | 26 | } |
705 | 1.16k | tx.request_done = true; |
706 | 1.16k | tx.response_done = true; |
707 | 1.16k | tx.set_status(SMB_NTSTATUS_SUCCESS, false); |
708 | 1.16k | true |
709 | | } else { |
710 | 20.5k | false |
711 | | }; |
712 | 21.7k | let found_tc = |
713 | 21.7k | if let Some(tx) = state.get_file_tx_by_fuid(cd.guid, Direction::ToClient) { |
714 | 1.43k | if !tx.request_done { |
715 | 1.29k | if let Some(SMBTransactionTypeData::FILE(ref mut tdf)) = tx.type_data { |
716 | 1.29k | filetracker_close(&mut tdf.file_tracker); |
717 | 1.29k | } |
718 | 146 | } |
719 | 1.43k | tx.request_done = true; |
720 | 1.43k | tx.response_done = true; |
721 | 1.43k | tx.set_status(SMB_NTSTATUS_SUCCESS, false); |
722 | 1.43k | true |
723 | | } else { |
724 | 20.2k | false |
725 | | }; |
726 | 21.7k | if !found_ts && !found_tc { |
727 | 19.4k | SCLogDebug!("SMBv2: CLOSE(TS): no TX at GUID {:?}", cd.guid); |
728 | 19.4k | } |
729 | 158 | } else { |
730 | 158 | events.push(SMBEvent::MalformedData); |
731 | 158 | } |
732 | 21.8k | false |
733 | | } |
734 | 25.7k | _ => false, |
735 | | }; |
736 | | /* if we don't have a tx, create it here (maybe) */ |
737 | 201k | if !have_tx && smb2_create_new_tx(r.command) { |
738 | 42.9k | let tx_key = SMBCommonHdr::from2(r, SMBHDR_TYPE_GENERICTX); |
739 | 42.9k | let Some(tx) = state.new_generic_tx(2, r.command, tx_key) else { |
740 | 2 | return; |
741 | | }; |
742 | | SCLogDebug!( |
743 | | "TS TX {} command {} created with session_id {} tree_id {} message_id {}", |
744 | | tx.id, |
745 | | r.command, |
746 | | r.session_id, |
747 | | r.tree_id, |
748 | | r.message_id |
749 | | ); |
750 | 42.9k | tx.set_events(events); |
751 | 158k | } |
752 | 201k | } |
753 | | |
754 | 122k | pub fn smb2_response_record(state: &mut SMBState, r: &Smb2Record) { |
755 | | SCLogDebug!( |
756 | | "SMBv2 response record, command {} status {} tree {} session {} message {}", |
757 | | &smb2_command_string(r.command), |
758 | | r.nt_status, |
759 | | r.tree_id, |
760 | | r.session_id, |
761 | | r.message_id |
762 | | ); |
763 | | |
764 | 122k | let mut events: Vec<SMBEvent> = Vec::new(); |
765 | | |
766 | 122k | let have_tx = match r.command { |
767 | | SMB2_COMMAND_IOCTL => { |
768 | 16.0k | smb2_ioctl_response_record(state, r); |
769 | 16.0k | true |
770 | | } |
771 | | SMB2_COMMAND_SESSION_SETUP => { |
772 | 5.12k | smb2_session_setup_response(state, r); |
773 | 5.12k | true |
774 | | } |
775 | | SMB2_COMMAND_WRITE => { |
776 | 3.30k | if r.nt_status == SMB_NTSTATUS_SUCCESS { |
777 | 3.18k | if let Ok((_, _wr)) = parse_smb2_response_write(r.data) { |
778 | 2.53k | SCLogDebug!("SMBv2: Write response => {:?}", _wr); |
779 | 2.53k | |
780 | 2.53k | /* search key-guid map */ |
781 | 2.53k | let guid_key = |
782 | 2.53k | SMBCommonHdr::new(SMBHDR_TYPE_GUID, r.session_id, r.tree_id, r.message_id); |
783 | 2.53k | let _guid_vec = state.ssn2vec_cache.pop(&guid_key).unwrap_or_default(); |
784 | 2.53k | SCLogDebug!("SMBv2 write response for GUID {:?}", _guid_vec); |
785 | 2.53k | } else { |
786 | 653 | events.push(SMBEvent::MalformedData); |
787 | 653 | } |
788 | 121 | } |
789 | 3.30k | false // the request may have created a generic tx, so handle that here |
790 | | } |
791 | | SMB2_COMMAND_READ => { |
792 | 10.6k | if r.nt_status == SMB_NTSTATUS_SUCCESS || r.nt_status == SMB_NTSTATUS_BUFFER_OVERFLOW { |
793 | 7.63k | smb2_read_response_record(state, r, 0); |
794 | 7.63k | } else if r.nt_status == SMB_NTSTATUS_END_OF_FILE { |
795 | | SCLogDebug!("SMBv2: read response => EOF"); |
796 | | |
797 | 1.56k | let guid_key = SMBCommonHdr::from2_notree(r, SMBHDR_TYPE_OFFSET); |
798 | 1.56k | let file_guid = if let Some(o) = state.read_offset_cache.pop(&guid_key) { |
799 | 443 | o.guid |
800 | | } else { |
801 | | SCLogDebug!("SMBv2 READ response: reply to unknown request"); |
802 | 1.12k | Vec::new() |
803 | | }; |
804 | 1.56k | if let Some(tx) = state.get_file_tx_by_fuid(&file_guid, Direction::ToClient) { |
805 | 272 | if !tx.request_done { |
806 | 145 | if let Some(SMBTransactionTypeData::FILE(ref mut tdf)) = tx.type_data { |
807 | 145 | filetracker_close(&mut tdf.file_tracker); |
808 | 145 | } |
809 | 127 | } |
810 | 272 | tx.set_status(r.nt_status, false); |
811 | 272 | tx.request_done = true; |
812 | 1.29k | } |
813 | 1.40k | } else { |
814 | 1.40k | SCLogDebug!("SMBv2 READ: status {}", r.nt_status); |
815 | 1.40k | } |
816 | 10.6k | false |
817 | | } |
818 | | SMB2_COMMAND_CREATE => { |
819 | 29.4k | if r.nt_status == SMB_NTSTATUS_SUCCESS { |
820 | 27.6k | if let Ok((_, cr)) = parse_smb2_response_create(r.data) { |
821 | | SCLogDebug!("SMBv2: Create response => {:?}", cr); |
822 | | |
823 | 26.4k | let guid_key = SMBCommonHdr::from2_notree(r, SMBHDR_TYPE_FILENAME); |
824 | 26.4k | if let Some(mut p) = state.ssn2vec_cache.pop(&guid_key) { |
825 | 175k | p.retain(|&i| i != 0x00); |
826 | 20.0k | _ = state.guid2name_cache.put(cr.guid.to_vec(), p); |
827 | 6.41k | } else { |
828 | 6.41k | SCLogDebug!("SMBv2 response: GUID NOT FOUND"); |
829 | 6.41k | } |
830 | | |
831 | 26.4k | let tx_hdr = SMBCommonHdr::from2(r, SMBHDR_TYPE_GENERICTX); |
832 | 26.4k | if let Some(tx) = state.get_generic_tx(2, r.command, &tx_hdr) { |
833 | | SCLogDebug!( |
834 | | "tx {} with {}/{} marked as done", |
835 | | tx.id, |
836 | | r.command, |
837 | | &smb2_command_string(r.command) |
838 | | ); |
839 | 18.9k | tx.set_status(r.nt_status, false); |
840 | 18.9k | tx.response_done = true; |
841 | | |
842 | 18.2k | if let Some(SMBTransactionTypeData::CREATE(ref mut tdn)) = tx.type_data { |
843 | 18.2k | tdn.create_ts = cr.create_ts.as_unix(); |
844 | 18.2k | tdn.last_access_ts = cr.last_access_ts.as_unix(); |
845 | 18.2k | tdn.last_write_ts = cr.last_write_ts.as_unix(); |
846 | 18.2k | tdn.last_change_ts = cr.last_change_ts.as_unix(); |
847 | 18.2k | tdn.size = cr.size; |
848 | 18.2k | tdn.guid = cr.guid.to_vec(); |
849 | 18.2k | } |
850 | 7.48k | } |
851 | 1.20k | } else { |
852 | 1.20k | events.push(SMBEvent::MalformedData); |
853 | 1.20k | } |
854 | 27.6k | true |
855 | | } else { |
856 | 1.80k | false |
857 | | } |
858 | | } |
859 | | SMB2_COMMAND_TREE_DISCONNECT => { |
860 | | // normally removed when processing request, |
861 | | // but in case we missed that try again here |
862 | 613 | let tree_key = SMBCommonHdr::from2(r, SMBHDR_TYPE_SHARE); |
863 | 613 | state.ssn2tree_cache.pop(&tree_key); |
864 | 613 | false |
865 | | } |
866 | | SMB2_COMMAND_TREE_CONNECT => { |
867 | 4.60k | if r.nt_status == SMB_NTSTATUS_SUCCESS { |
868 | 3.39k | if let Ok((_, tr)) = parse_smb2_response_tree_connect(r.data) { |
869 | 2.83k | let name_key = SMBCommonHdr::from2(r, SMBHDR_TYPE_TREE); |
870 | 2.83k | let mut share_name = Vec::new(); |
871 | 2.83k | let is_pipe = tr.share_type == 2; |
872 | 2.83k | let found = match state.get_treeconnect_tx(name_key) { |
873 | 1.72k | Some(tx) => { |
874 | 1.72k | if let Some(SMBTransactionTypeData::TREECONNECT(ref mut tdn)) = |
875 | 1.72k | tx.type_data |
876 | 1.72k | { |
877 | 1.72k | tdn.share_type = tr.share_type; |
878 | 1.72k | tdn.is_pipe = is_pipe; |
879 | 1.72k | tdn.tree_id = r.tree_id; |
880 | 1.72k | share_name = tdn.share_name.to_vec(); |
881 | 1.72k | } |
882 | | // update hdr now that we have a tree_id |
883 | 1.72k | tx.hdr = SMBCommonHdr::from2(r, SMBHDR_TYPE_HEADER); |
884 | 1.72k | tx.response_done = true; |
885 | 1.72k | tx.set_status(r.nt_status, false); |
886 | 1.72k | true |
887 | | } |
888 | 1.10k | None => false, |
889 | | }; |
890 | 2.83k | if found { |
891 | 1.72k | let tree = SMBTree::new(share_name.to_vec(), is_pipe); |
892 | 1.72k | let tree_key = SMBCommonHdr::from2(r, SMBHDR_TYPE_SHARE); |
893 | 1.72k | state.ssn2tree_cache.put(tree_key, tree); |
894 | 1.72k | } |
895 | 2.83k | true |
896 | | } else { |
897 | 569 | events.push(SMBEvent::MalformedData); |
898 | 569 | false |
899 | | } |
900 | | } else { |
901 | 1.20k | let name_key = SMBCommonHdr::from2(r, SMBHDR_TYPE_TREE); |
902 | 1.20k | if let Some(tx) = state.get_treeconnect_tx(name_key) { |
903 | 492 | tx.response_done = true; |
904 | 492 | tx.set_status(r.nt_status, false); |
905 | 492 | true |
906 | | } else { |
907 | 715 | false |
908 | | } |
909 | | } |
910 | | } |
911 | | SMB2_COMMAND_NEGOTIATE_PROTOCOL => { |
912 | 8.57k | let res = if r.nt_status == SMB_NTSTATUS_SUCCESS { |
913 | 7.37k | parse_smb2_response_negotiate_protocol(r.data) |
914 | | } else { |
915 | 1.20k | parse_smb2_response_negotiate_protocol_error(r.data) |
916 | | }; |
917 | 8.57k | if let Ok((_, rd)) = res { |
918 | | SCLogDebug!("SERVER dialect => {}", &smb2_dialect_string(rd.dialect)); |
919 | | |
920 | 6.95k | let smb_cfg_max_read_size = unsafe { SMB_CFG_MAX_READ_SIZE }; |
921 | 6.95k | if smb_cfg_max_read_size != 0 && rd.max_read_size > smb_cfg_max_read_size { |
922 | 2.73k | state.set_event(SMBEvent::NegotiateMaxReadSizeTooLarge); |
923 | 4.21k | } |
924 | 6.95k | let smb_cfg_max_write_size = unsafe { SMB_CFG_MAX_WRITE_SIZE }; |
925 | 6.95k | if smb_cfg_max_write_size != 0 && rd.max_write_size > smb_cfg_max_write_size { |
926 | 2.43k | state.set_event(SMBEvent::NegotiateMaxWriteSizeTooLarge); |
927 | 4.51k | } |
928 | | |
929 | 6.95k | state.dialect = rd.dialect; |
930 | 6.95k | state.max_read_size = rd.max_read_size; |
931 | 6.95k | state.max_write_size = rd.max_write_size; |
932 | | |
933 | 6.95k | let found2 = match state.get_negotiate_tx(2) { |
934 | 1.94k | Some(tx) => { |
935 | 1.94k | if let Some(SMBTransactionTypeData::NEGOTIATE(ref mut tdn)) = tx.type_data { |
936 | 1.94k | tdn.server_guid = rd.server_guid.to_vec(); |
937 | 1.94k | } |
938 | 1.94k | tx.set_status(r.nt_status, false); |
939 | 1.94k | tx.response_done = true; |
940 | 1.94k | true |
941 | | } |
942 | 5.00k | None => false, |
943 | | }; |
944 | | // SMB2 response to SMB1 request? |
945 | 6.95k | let found1 = !found2 |
946 | 5.00k | && match state.get_negotiate_tx(1) { |
947 | 1.14k | Some(tx) => { |
948 | 1.14k | if let Some(SMBTransactionTypeData::NEGOTIATE(ref mut tdn)) = |
949 | 1.14k | tx.type_data |
950 | 1.14k | { |
951 | 1.14k | tdn.server_guid = rd.server_guid.to_vec(); |
952 | 1.14k | } |
953 | 1.14k | tx.set_status(r.nt_status, false); |
954 | 1.14k | tx.response_done = true; |
955 | 1.14k | true |
956 | | } |
957 | 3.85k | None => false, |
958 | | }; |
959 | 6.95k | found1 || found2 |
960 | | } else { |
961 | 1.62k | events.push(SMBEvent::MalformedData); |
962 | 1.62k | false |
963 | | } |
964 | | } |
965 | | _ => { |
966 | | SCLogDebug!("default case: no TX"); |
967 | 44.2k | false |
968 | | } |
969 | | }; |
970 | 122k | if !have_tx { |
971 | 67.3k | let tx_hdr = SMBCommonHdr::from2(r, SMBHDR_TYPE_GENERICTX); |
972 | | SCLogDebug!( |
973 | | "looking for TX {} with session_id {} tree_id {} message_id {}", |
974 | | &smb2_command_string(r.command), |
975 | | r.session_id, |
976 | | r.tree_id, |
977 | | r.message_id |
978 | | ); |
979 | 67.3k | let _found = match state.get_generic_tx(2, r.command, &tx_hdr) { |
980 | 27.8k | Some(tx) => { |
981 | | SCLogDebug!( |
982 | | "tx {} with {}/{} marked as done", |
983 | | tx.id, |
984 | | r.command, |
985 | | &smb2_command_string(r.command) |
986 | | ); |
987 | 27.8k | if r.nt_status != SMB_NTSTATUS_PENDING { |
988 | 27.3k | tx.response_done = true; |
989 | 27.3k | } |
990 | 27.8k | tx.set_status(r.nt_status, false); |
991 | 27.8k | tx.set_events(events); |
992 | 27.8k | true |
993 | | } |
994 | | _ => { |
995 | | SCLogDebug!("no tx found for {:?}", r); |
996 | 39.4k | false |
997 | | } |
998 | | }; |
999 | 55.2k | } |
1000 | 122k | } |
1001 | | |
1002 | | #[cfg(test)] |
1003 | | mod tests { |
1004 | | use super::*; |
1005 | | |
1006 | | // link stub: the C file-flags helper has no Rust definition; the tests |
1007 | | // never rely on its value |
1008 | | #[no_mangle] |
1009 | | pub extern "C" fn SCFileFlowFlagsToFlags(_f: u16, _d: u8) -> u16 { |
1010 | | 0 |
1011 | | } |
1012 | | |
1013 | | // the FFI file ops are no-ops under cfg(test), so a default context is |
1014 | | // safe (the filetracker needs it set to track chunks) |
1015 | | static TEST_SBCFG: StreamingBufferConfig = StreamingBufferConfig { |
1016 | | buf_size: 2048, |
1017 | | max_regions: 8, |
1018 | | region_gap: 0, |
1019 | | Calloc: None, |
1020 | | Realloc: None, |
1021 | | Free: None, |
1022 | | }; |
1023 | | static TEST_FC: SuricataFileContext = SuricataFileContext { |
1024 | | files_sbcfg: &TEST_SBCFG, |
1025 | | }; |
1026 | | |
1027 | | // SMB2 write body (parse_smb2_request_write layout): skip4, len, offset, |
1028 | | // guid, channel, remaining, write_flags, skip4, data. |
1029 | | use crate::core::{StreamingBufferConfig, SuricataFileContext}; |
1030 | | |
1031 | | fn write_body(offset: u64, guid: &[u8], data: &[u8], declared: u32) -> Vec<u8> { |
1032 | | let mut b = vec![0u8; 4]; |
1033 | | b.extend(declared.to_le_bytes()); |
1034 | | b.extend(offset.to_le_bytes()); |
1035 | | b.extend(guid); |
1036 | | b.extend([0u8; 16]); // channel + remaining + write_flags + skip |
1037 | | b.extend(data); |
1038 | | b |
1039 | | } |
1040 | | |
1041 | | fn write_rec(data: &[u8], mid: u64) -> Smb2Record<'_> { |
1042 | | Smb2Record { |
1043 | | direction: 0, |
1044 | | header_len: 64, |
1045 | | nt_status: 0, |
1046 | | command: SMB2_COMMAND_WRITE, |
1047 | | message_id: mid, |
1048 | | tree_id: 1, |
1049 | | async_id: 0, |
1050 | | session_id: 1, |
1051 | | data, |
1052 | | } |
1053 | | } |
1054 | | |
1055 | | // A queue-limit-rejected record must not arm the trailing file-data |
1056 | | // promise: its remainder is consumed by the skip, so the promise would |
1057 | | // otherwise claim the following record's bytes. |
1058 | | #[test] |
1059 | | fn test_smb2_write_rejected_partial_arms_no_promise() { |
1060 | | let _lock = crate::smb::files::SMB_CFG_TEST_LOCK.lock().unwrap(); |
1061 | | // restores the cfg statics this test mutates; leaking them made later |
1062 | | // write-queue-dependent tests order-dependent |
1063 | | struct CfgGuard; |
1064 | | impl Drop for CfgGuard { |
1065 | | fn drop(&mut self) { |
1066 | | unsafe { |
1067 | | SURICATA_SMB_FILE_CONFIG = None; |
1068 | | } |
1069 | | unsafe { |
1070 | | SMB_CFG_MAX_WRITE_QUEUE_CNT = 64; |
1071 | | } |
1072 | | unsafe { |
1073 | | SMB_CFG_MAX_WRITE_QUEUE_SIZE = 67108864; |
1074 | | } |
1075 | | } |
1076 | | } |
1077 | | let _cfg_guard = CfgGuard; |
1078 | | unsafe { |
1079 | | SURICATA_SMB_FILE_CONFIG = Some(&TEST_FC); |
1080 | | } |
1081 | | let mut state = SMBState::new(); |
1082 | | let guid: Vec<u8> = [0xAAu8, 0xBB, 0xCC, 0xDD] |
1083 | | .into_iter() |
1084 | | .chain(std::iter::repeat_n(0, 12)) |
1085 | | .collect(); |
1086 | | // w1 (200 declared, 100 carried, nbss 0): rejected by the NBSS guard |
1087 | | // (200 - 100 > 0) before the tracker is fed, so it queues nothing. |
1088 | | let d1: Vec<u8> = vec![b'A'; 100]; |
1089 | | let b1 = write_body(0, &guid, &d1, 200); |
1090 | | smb2_write_request_record(&mut state, &write_rec(&b1, 1), 0); |
1091 | | // w2 (100 declared, 100 carried at offset 300): creates the file tx |
1092 | | // and queues one OOO chunk (inflight count 1). |
1093 | | let d2: Vec<u8> = vec![b'B'; 100]; |
1094 | | let b2 = write_body(300, &guid, &d2, 100); |
1095 | | smb2_write_request_record(&mut state, &write_rec(&b2, 2), 0); |
1096 | | // w3 (700 declared, 444 carried, 256 nbss, OOO at 600): a new OOO |
1097 | | // entry would make the inflight count 1 + 1 > 1, so the queue count |
1098 | | // limit rejects it on the partial path. |
1099 | | unsafe { |
1100 | | SMB_CFG_MAX_WRITE_QUEUE_CNT = 1; |
1101 | | } |
1102 | | unsafe { |
1103 | | SMB_CFG_MAX_WRITE_QUEUE_SIZE = 0; |
1104 | | } |
1105 | | let d3: Vec<u8> = vec![b'X'; 444]; |
1106 | | let b3 = write_body(600, &guid, &d3, 700); |
1107 | | // sentinel: a regressed guard would overwrite it with the armed 256 |
1108 | | state.file_ts_left = 0x1234; |
1109 | | smb2_write_request_record(&mut state, &write_rec(&b3, 3), 256); |
1110 | | assert_eq!( |
1111 | | state.file_ts_left, 0x1234, |
1112 | | "rejected record must not arm the file-data promise" |
1113 | | ); |
1114 | | } |
1115 | | } |