You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
suricata/rust/src/nfs/nfs4.rs

536 lines
22 KiB
Rust

/* Copyright (C) 2018-2020 Open Information Security Foundation
*
* You can copy, redistribute or modify this Program under the terms of
* the GNU General Public License version 2 as published by the Free
* Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* version 2 along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
// written by Victor Julien
use nom7::bytes::streaming::take;
use nom7::error::ErrorKind;
use nom7::number::streaming::be_u32;
use nom7::{Err, IResult};
use crate::direction::Direction;
use crate::flow::Flow;
use crate::nfs::nfs::*;
use crate::nfs::nfs4_records::*;
use crate::nfs::nfs_records::*;
use crate::nfs::rpc_records::*;
use crate::nfs::types::*;
use crate::kerberos::{parse_kerberos5_request, Kerberos5Ticket, SecBlobError};
fn parse_req_gssapi(i: &[u8]) -> IResult<&[u8], Kerberos5Ticket, SecBlobError> {
let (i, len) = be_u32(i)?;
let (i, buf) = take(len as usize)(i)?;
let (_, ap) = parse_kerberos5_request(buf)?;
Ok((i, ap))
}
impl NFSState {
/* normal write: PUTFH (file handle), WRITE (write opts/data). File handle
* is not part of the write record itself so we pass it in here. */
fn write_v4<'b>(&mut self, r: &RpcPacket<'b>, w: &Nfs4RequestWrite<'b>, fh: &'b [u8]) {
// for now assume that stable FILE_SYNC flags means a single chunk
let is_last = w.stable == 2;
SCLogDebug!("is_last {}", is_last);
if w.write_len == 0 {
return;
}
let file_handle = fh.to_vec();
let file_name = if let Some(name) = self.namemap.get(fh) {
SCLogDebug!("WRITE name {:?}", name);
name.to_vec()
} else {
SCLogDebug!("WRITE object {:?} not found", w.stateid.data);
Vec::new()
};
// Reject an oversized WRITE: log it as a tx and skip to the record
// boundary to keep the flow in sync.
let max_write_size = unsafe { NFS_CFG_MAX_WRITE_SIZE };
if max_write_size != 0 && w.write_len > max_write_size {
let tx = self.new_file_tx(&file_handle, &file_name, Direction::ToServer);
tx.procedure = NFSPROC4_WRITE;
tx.xid = r.hdr.xid;
tx.is_first = true;
tx.nfs_version = r.progver as u16;
tx.request_done = true;
tx.response_done = true;
// Close the tx so a later same-handle op cannot reuse it.
tx.is_file_closed = true;
tx.tx_data.set_event(NFSEvent::WriteRequestTooLarge as u8);
// Complete the rejected transaction immediately: without
// this the tx and its event only surface at flow teardown.
tx.tx_data.0.updated_ts = true;
tx.tx_data.0.updated_tc = true;
// Rejected tx never opened a file: zero files_opened, else the tx
// (and file) stays live until teardown and grows unbounded on repeats.
tx.tx_data.0.files_opened = 0;
// Skip to the RPC record boundary: claim vs data can disagree, and
// claimed-count math lands mid-padding or in a later record.
let pending = crate::nfs::nfs::record_tail(r.prog_data_size, r.prog_data.len());
self.set_skip(Direction::ToServer, pending);
self.ts_chunk_xid = 0;
self.ts_chunk_left = 0;
SCLogDebug!(
"WRITEv4 too large (claim {} > {}): skipping {} stream bytes",
w.write_len,
unsafe { NFS_CFG_MAX_WRITE_SIZE },
pending
);
return;
}
let mut queue_exceeded = false;
let found = match self.get_file_tx_by_handle(&file_handle, Direction::ToServer) {
Some(tx) => {
if let Some(NFSTransactionTypeData::FILE(ref mut tdf)) = tx.type_data {
if let Some(event) =
write_queue_limit_event(&tdf.file_tracker, w.offset, w.data.len() as u64)
{
// Attach to the affected file tx, not the newest tx.
tx.tx_data.set_event(event as u8);
tx.tx_data.0.updated_ts = true;
tx.tx_data.0.updated_tc = true;
queue_exceeded = true;
} else {
filetracker_newchunk(
&mut tdf.file_tracker,
&file_name,
w.data,
w.offset,
w.write_len,
0,
is_last,
&r.hdr.xid,
);
tdf.chunk_count += 1;
if is_last {
tdf.file_last_xid = r.hdr.xid;
tx.is_last = true;
tx.response_done = true;
}
}
}
true
}
None => false,
};
if !found {
let tx = self.new_file_tx(&file_handle, &file_name, Direction::ToServer);
if let Some(NFSTransactionTypeData::FILE(ref mut tdf)) = tx.type_data {
if let Some(event) =
write_queue_limit_event(&tdf.file_tracker, w.offset, w.data.len() as u64)
{
// tx just created: set the event on it directly.
tx.tx_data.set_event(event as u8);
tx.tx_data.0.updated_ts = true;
tx.tx_data.0.updated_tc = true;
queue_exceeded = true;
// Chunk rejected before the file was opened: complete + close the
// tx and zero files_opened, else it dangles until teardown.
tx.request_done = true;
tx.response_done = true;
tx.is_file_closed = true;
tx.tx_data.0.files_opened = 0;
} else {
filetracker_newchunk(
&mut tdf.file_tracker,
&file_name,
w.data,
w.offset,
w.write_len,
0,
is_last,
&r.hdr.xid,
);
}
if is_last && !queue_exceeded {
tdf.file_last_xid = r.hdr.xid;
tx.is_last = true;
tx.request_done = true;
tx.is_file_closed = true;
}
}
tx.procedure = NFSPROC4_WRITE;
tx.xid = r.hdr.xid;
tx.is_first = true;
tx.nfs_version = r.progver as u16;
}
if queue_exceeded {
// Don't buffer the rest of the record: skip to its RPC boundary.
let pending = crate::nfs::nfs::record_tail(r.prog_data_size, r.prog_data.len());
self.set_skip(Direction::ToServer, pending);
self.ts_chunk_xid = 0;
self.ts_chunk_left = 0;
self.ts_chunk_tail = 0;
SCLogDebug!("WRITE queue exceeded: skipping {} stream bytes", pending);
} else {
self.ts_chunk_xid = r.hdr.xid;
debug_validate_bug_on!(w.data.len() as u32 > w.write_len);
self.ts_chunk_left = w.write_len - w.data.len() as u32;
// v4 records only reach the parser complete: nothing unbuffered,
// record tail 0 (no aligned-boundary correction like the v3 path)
self.ts_chunk_tail = crate::nfs::nfs::record_tail(r.prog_data_size, r.prog_data.len());
}
}
fn close_v4<'b>(&mut self, r: &RpcPacket<'b>, fh: &'b [u8]) {
self.commit_v4(r, fh)
}
fn commit_v4<'b>(&mut self, r: &RpcPacket<'b>, fh: &'b [u8]) {
SCLogDebug!("COMMIT, closing shop");
let file_handle = fh.to_vec();
if let Some(tx) = self.get_file_tx_by_handle(&file_handle, Direction::ToServer) {
if let Some(NFSTransactionTypeData::FILE(ref mut tdf)) = tx.type_data {
filetracker_close(&mut tdf.file_tracker);
tdf.file_last_xid = r.hdr.xid;
tx.is_last = true;
tx.request_done = true;
tx.is_file_closed = true;
}
}
}
fn new_tx_v4(
&mut self, r: &RpcPacket, xidmap: &NFSRequestXidMap, procedure: u32, _aux_opcodes: &[u32],
) {
let mut tx = self.new_tx();
tx.xid = r.hdr.xid;
tx.procedure = procedure;
tx.request_done = true;
tx.file_name = xidmap.file_name.to_vec();
tx.nfs_version = r.progver as u16;
tx.file_handle = xidmap.file_handle.to_vec();
tx.auth_type = r.creds_flavor;
#[allow(clippy::single_match)]
match r.creds {
RpcRequestCreds::Unix(ref u) => {
tx.request_machine_name = u.machine_name_buf.to_vec();
tx.request_uid = u.uid;
tx.request_gid = u.gid;
}
_ => {}
}
SCLogDebug!(
"NFSv4: TX created: ID {} XID {} PROCEDURE {}",
tx.id,
tx.xid,
tx.procedure
);
self.transactions.push(tx);
}
/* A normal READ request looks like: PUTFH (file handle) READ (read opts).
* We need the file handle for the READ.
*/
fn compound_request<'b>(
&mut self, r: &RpcPacket<'b>, cr: &Nfs4RequestCompoundRecord<'b>,
xidmap: &mut NFSRequestXidMap,
) {
let mut last_putfh: Option<&'b [u8]> = None;
let mut main_opcode: u32 = 0;
let mut aux_opcodes: Vec<u32> = Vec::new();
for c in &cr.commands {
SCLogDebug!("c {:?}", c);
match *c {
Nfs4RequestContent::PutFH(ref rd) => {
last_putfh = Some(rd.value);
aux_opcodes.push(NFSPROC4_PUTFH);
}
Nfs4RequestContent::Read(ref rd) => {
SCLogDebug!("READv4: {:?}", rd);
if let Some(fh) = last_putfh {
xidmap.chunk_offset = rd.offset;
xidmap.file_handle = fh.to_vec();
self.xidmap_handle2name(xidmap);
}
}
Nfs4RequestContent::Open(ref rd) => {
SCLogDebug!("OPENv4: {}", String::from_utf8_lossy(rd.filename));
xidmap.file_name = rd.filename.to_vec();
}
Nfs4RequestContent::Lookup(ref rd) => {
SCLogDebug!("LOOKUPv4: {}", String::from_utf8_lossy(rd.filename));
xidmap.file_name = rd.filename.to_vec();
}
Nfs4RequestContent::Write(ref rd) => {
SCLogDebug!("WRITEv4: {:?}", rd);
if let Some(fh) = last_putfh {
self.write_v4(r, rd, fh);
}
}
Nfs4RequestContent::Commit => {
SCLogDebug!("COMMITv4");
if let Some(fh) = last_putfh {
self.commit_v4(r, fh);
}
}
Nfs4RequestContent::Close(ref _rd) => {
SCLogDebug!("CLOSEv4: {:?}", _rd);
if let Some(fh) = last_putfh {
self.close_v4(r, fh);
}
}
Nfs4RequestContent::Create(ref rd) => {
SCLogDebug!("CREATEv4: {:?}", rd);
if let Some(fh) = last_putfh {
xidmap.file_handle = fh.to_vec();
}
xidmap.file_name = rd.filename.to_vec();
main_opcode = NFSPROC4_CREATE;
}
Nfs4RequestContent::Remove(rd) => {
SCLogDebug!("REMOVEv4: {:?}", rd);
xidmap.file_name = rd.to_vec();
main_opcode = NFSPROC4_REMOVE;
}
Nfs4RequestContent::SetClientId(ref _rd) => {
SCLogDebug!(
"SETCLIENTIDv4: client id {} r_netid {} r_addr {}",
String::from_utf8_lossy(_rd.client_id),
String::from_utf8_lossy(_rd.r_netid),
String::from_utf8_lossy(_rd.r_addr)
);
}
_ => {}
}
}
if main_opcode != 0 {
self.new_tx_v4(r, xidmap, main_opcode, &aux_opcodes);
}
}
/// complete request record
pub fn process_request_record_v4(&mut self, r: &RpcPacket) {
SCLogDebug!(
"NFSv4 REQUEST {} procedure {} ({}) blob size {}",
r.hdr.xid,
r.procedure,
self.requestmap.len(),
r.prog_data.len()
);
let mut xidmap = NFSRequestXidMap::new(r.progver, r.procedure, 0);
if r.procedure == NFSPROC4_NULL {
if let RpcRequestCreds::GssApi(ref creds) = r.creds {
if creds.procedure == 1 {
let _x = parse_req_gssapi(r.prog_data);
SCLogDebug!("RPCSEC_GSS_INIT {:?}", _x);
}
}
} else if r.procedure == NFSPROC4_COMPOUND {
let mut data = r.prog_data;
if let RpcRequestCreds::GssApi(ref creds) = r.creds {
// store proc and serv for the reply; only (0, 2) is
// unwrapped below
xidmap.gssapi_proc = creds.procedure;
xidmap.gssapi_service = creds.service;
// privacy (0, 3) carries an encrypted compound: nothing to
// parse, and it is not malformed: just keep the request state
if creds.procedure == 0 && creds.service == 3 {
self.requestmap.put(r.hdr.xid, xidmap);
return;
}
if creds.procedure == 0 && creds.service == 2 {
SCLogDebug!("GSS INTEGRITY: {:?}", creds);
match parse_rpc_gssapi_integrity(r.prog_data) {
Ok((_rem, rec)) => {
SCLogDebug!("GSS INTEGRITY wrapper: {:?}", rec);
data = rec.data;
}
Err(Err::Incomplete(_n)) => {
SCLogDebug!("NFSPROC4_COMPOUND/GSS INTEGRITY: INCOMPLETE {:?}", _n);
self.set_event(NFSEvent::MalformedData);
return;
}
Err(Err::Error(_e)) | Err(Err::Failure(_e)) => {
SCLogDebug!(
"NFSPROC4_COMPOUND/GSS INTEGRITY: Parsing failed: {:?}",
_e
);
self.set_event(NFSEvent::MalformedData);
return;
}
}
}
}
match parse_nfs4_request_compound(data) {
Ok((_, rd)) => {
SCLogDebug!("NFSPROC4_COMPOUND: {:?}", rd);
self.compound_request(r, &rd, &mut xidmap);
}
Err(Err::Incomplete(_n)) => {
SCLogDebug!("NFSPROC4_COMPOUND: INCOMPLETE {:?}", _n);
self.reject_failed_v4_compound_parse(r, data);
}
Err(Err::Error(ref e)) if e.code == ErrorKind::Tag => {
// a defined v4 op this parser does not implement: the compound is
// valid, just not parsed. No tx of its own: keeps base's event on
// the newest tx, so a lock-only flow logs nothing when it has no
// earlier tx.
SCLogDebug!("NFSPROC4_COMPOUND: unsupported op {:?}", e);
self.set_event(NFSEvent::MalformedData);
}
Err(Err::Error(_e)) | Err(Err::Failure(_e)) => {
SCLogDebug!("NFSPROC4_COMPOUND: Parsing failed: {:?}", _e);
self.reject_failed_v4_compound_parse(r, data);
}
};
}
self.requestmap.put(r.hdr.xid, xidmap);
}
fn compound_response<'b>(
&mut self, flow: *mut Flow, r: &RpcReplyPacket<'b>, cr: &Nfs4ResponseCompoundRecord<'b>,
xidmap: &mut NFSRequestXidMap,
) {
let mut insert_filename_with_getfh = false;
let mut main_opcode_status: u32 = 0;
let mut main_opcode_status_set: bool = false;
for c in &cr.commands {
SCLogDebug!("c {:?}", c);
match *c {
Nfs4ResponseContent::ReadDir(_s, Some(ref rd)) => {
SCLogDebug!("READDIRv4: status {} eof {}", _s, rd.eof);
#[allow(clippy::manual_flatten)]
for d in &rd.listing {
if let Some(_d) = d {
SCLogDebug!("READDIRv4: dir {}", String::from_utf8_lossy(_d.name));
}
}
}
Nfs4ResponseContent::Remove(s) => {
SCLogDebug!("REMOVE4: status {}", s);
main_opcode_status = s;
main_opcode_status_set = true;
}
Nfs4ResponseContent::Create(s) => {
SCLogDebug!("CREATE4: status {}", s);
main_opcode_status = s;
main_opcode_status_set = true;
}
Nfs4ResponseContent::Read(s, Some(ref rd)) => {
SCLogDebug!(
"READ4: xidmap {:?} status {} data {}",
xidmap,
s,
rd.data.len()
);
// convert record to generic read reply
let reply = NfsReplyRead {
status: s,
attr_follows: 0,
attr_blob: &[],
count: rd.count,
eof: rd.eof,
data_len: rd.data.len() as u32,
data: rd.data,
};
self.process_read_record(flow, r, &reply, Some(xidmap), false);
}
Nfs4ResponseContent::Open(_s, Some(ref _rd)) => {
SCLogDebug!("OPENv4: status {} opendata {:?}", _s, _rd);
insert_filename_with_getfh = true;
}
Nfs4ResponseContent::GetFH(_s, Some(ref rd)) if insert_filename_with_getfh => {
self.namemap
.put(rd.value.to_vec(), xidmap.file_name.to_vec());
}
Nfs4ResponseContent::PutRootFH(s)
if s == NFS4_OK && xidmap.file_name.is_empty() =>
{
xidmap.file_name = b"<mount_root>".to_vec();
SCLogDebug!("filename {:?}", xidmap.file_name);
}
_ => {}
}
}
if main_opcode_status_set {
let resp_handle = Vec::new();
self.mark_response_tx_done(
flow,
r.hdr.xid,
r.reply_state,
main_opcode_status,
&resp_handle,
);
}
}
pub fn process_reply_record_v4(
&mut self, flow: *mut Flow, r: &RpcReplyPacket, xidmap: &mut NFSRequestXidMap,
) {
if xidmap.procedure == NFSPROC4_COMPOUND {
let mut data = r.prog_data;
// privacy (0, 3) carries an encrypted response: not parsed,
// and not malformed (the call logged no tx either)
if xidmap.gssapi_proc == 0 && xidmap.gssapi_service == 3 {
return;
}
if xidmap.gssapi_proc == 0 && xidmap.gssapi_service == 2 {
SCLogDebug!("GSS INTEGRITY as set by call: {:?}", xidmap);
match parse_rpc_gssapi_integrity(r.prog_data) {
Ok((_rem, rec)) => {
SCLogDebug!("GSS INTEGRITY wrapper: {:?}", rec);
data = rec.data;
}
Err(Err::Incomplete(_n)) => {
SCLogDebug!("NFSPROC4_COMPOUND/GSS INTEGRITY: INCOMPLETE {:?}", _n);
self.set_event(NFSEvent::MalformedData);
return;
}
Err(Err::Error(_e)) | Err(Err::Failure(_e)) => {
SCLogDebug!("NFSPROC4_COMPOUND/GSS INTEGRITY: Parsing failed: {:?}", _e);
self.set_event(NFSEvent::MalformedData);
return;
}
}
}
match parse_nfs4_response_compound(data) {
Ok((_, rd)) => {
SCLogDebug!("COMPOUNDv4: {:?}", rd);
self.compound_response(flow, r, &rd, xidmap);
}
Err(Err::Incomplete(_)) => {
self.set_event(NFSEvent::MalformedData);
}
Err(Err::Error(_e)) | Err(Err::Failure(_e)) => {
SCLogDebug!("Parsing failed: {:?}", _e);
self.set_event(NFSEvent::MalformedData);
}
};
}
}
}