From: Victor Julien Date: Mon, 22 May 2017 20:10:20 +0000 (+0200) Subject: rust/nfs: NFSv3 parser, logger and detection X-Git-Tag: suricata-4.0.0-beta1~19 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d6592211d0f8373b52ede7d0d5dbfe4a3345678f;p=thirdparty%2Fsuricata.git rust/nfs: NFSv3 parser, logger and detection --- diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 4fc53b9ed2..701df8655e 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -36,3 +36,4 @@ pub mod filetracker; pub mod lua; pub mod dns; +pub mod nfs; diff --git a/rust/src/nfs/log.rs b/rust/src/nfs/log.rs new file mode 100644 index 0000000000..bf98781807 --- /dev/null +++ b/rust/src/nfs/log.rs @@ -0,0 +1,110 @@ +/* Copyright (C) 2017 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. + */ + +extern crate libc; + +use std::string::String; +use json::*; +//use log::*; +use nfs::types::*; +use nfs::nfs3::*; + +#[no_mangle] +pub extern "C" fn rs_nfs3_tx_logging_is_filtered(tx: &mut NFS3Transaction) + -> libc::uint8_t +{ + // TODO probably best to make this configurable + + if tx.procedure == NFSPROC3_GETATTR { + return 1; + } + + return 0; +} + +fn nfs3_creds_object(tx: &NFS3Transaction) -> Json +{ + let js = Json::object(); + let mach_name = String::from_utf8_lossy(&tx.request_machine_name); + js.set_string("machine_name", &mach_name); + js.set_integer("uid", tx.request_uid as u64); + js.set_integer("gid", tx.request_gid as u64); + return js; +} + +fn nfs3_write_object(tx: &NFS3Transaction) -> Json +{ + let js = Json::object(); + js.set_boolean("first", tx.is_first); + js.set_boolean("last", tx.is_last); + js.set_integer("last_xid", tx.file_last_xid as u64); + return js; +} + +fn nfs3_read_object(tx: &NFS3Transaction) -> Json +{ + let js = Json::object(); + js.set_boolean("first", tx.is_first); + js.set_boolean("last", tx.is_last); + js.set_integer("last_xid", tx.file_last_xid as u64); + return js; +} + +fn nfs3_common_header(tx: &NFS3Transaction) -> Json +{ + let js = Json::object(); + js.set_integer("xid", tx.xid as u64); + js.set_string("procedure", &nfs3_procedure_string(tx.procedure)); + let file_name = String::from_utf8_lossy(&tx.file_name); + js.set_string("filename", &file_name); + js.set_integer("id", tx.id as u64); + js.set_boolean("file_tx", tx.is_file_tx); + return js; +} + +#[no_mangle] +pub extern "C" fn rs_nfs3_log_json_request(tx: &mut NFS3Transaction) -> *mut JsonT +{ + let js = nfs3_common_header(tx); + js.set_string("type", "request"); + return js.unwrap(); +} + +#[no_mangle] +pub extern "C" fn rs_nfs3_log_json_response(tx: &mut NFS3Transaction) -> *mut JsonT +{ + let js = nfs3_common_header(tx); + js.set_string("type", "response"); + + js.set_string("status", &nfs3_status_string(tx.response_status)); + + if tx.has_creds { + let creds_js = nfs3_creds_object(tx); + js.set("creds", creds_js); + } + + if tx.procedure == NFSPROC3_READ { + let read_js = nfs3_read_object(tx); + js.set("read", read_js); + } + if tx.procedure == NFSPROC3_WRITE { + let write_js = nfs3_write_object(tx); + js.set("write", write_js); + } + + return js.unwrap(); +} diff --git a/rust/src/nfs/mod.rs b/rust/src/nfs/mod.rs new file mode 100644 index 0000000000..275dee9540 --- /dev/null +++ b/rust/src/nfs/mod.rs @@ -0,0 +1,25 @@ +/* Copyright (C) 2017 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. + */ + +pub mod types; +#[macro_use] +pub mod parser; +pub mod nfs3; +pub mod log; + +//#[cfg(feature = "lua")] +//pub mod lua; diff --git a/rust/src/nfs/nfs3.rs b/rust/src/nfs/nfs3.rs new file mode 100644 index 0000000000..9401970577 --- /dev/null +++ b/rust/src/nfs/nfs3.rs @@ -0,0 +1,1399 @@ +/* Copyright (C) 2017 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 +// TCP buffering code written by Pierre Chifflier + +extern crate libc; +use std; +use std::mem::transmute; + +use nom::IResult; + +use std::collections::{HashMap}; + +use nom; +use log::*; +use applayer::LoggerFlags; +use core::*; +use filetracker::*; +use filecontainer::*; +//use storage::*; + +use nfs::types::*; +use nfs::parser::*; + +/// nom bug leads to this wrappers being necessary +/// TODO for some reason putting these in parser.rs and making them public +/// leads to a compile error wrt an unknown lifetime identifier 'a +//named!(many0_nfs3_request_objects>>, many0!(parse_nfs3_request_object)); +//named!(many0_nfs3_reply_objects>>, many0!(parse_nfs3_reply_object)); +named!(many0_nfs3_response_readdirplus_entries>>, + many0!(parse_nfs3_response_readdirplus_entry_cond)); + +pub static mut suricata_nfs3_file_config: Option<&'static SuricataFileContext> = None; + +/* + * Record parsing. + * + * Incomplete records come in due to TCP splicing. For all record types + * except READ and WRITE, processing only begins when the full record + * is available. For READ/WRITE partial records are processed as well to + * avoid queuing too much data. + * + * Getting file names. + * + * NFS makes heavy use of 'file handles' for operations. In many cases it + * uses a file name just once and after that just the handle. For example, + * if a client did a file listing (e.g. READDIRPLUS) and would READ the + * file afterwards, the name will only appear in the READDIRPLUS answer. + * To be able to log the names we store a mapping between file handles + * and file names in NFS3State::namemap. + * + * Mapping NFS to Suricata's transaction model. + * + * The easiest way to do transactions would be to map each command/reply with + * the same XID to a transaction. This would allow for per XID logging, detect + * etc. However this model doesn't fit well with file tracking. The file + * tracking in Suricata is really expecting to be one or more files to live + * inside a single transaction. Would XID pairs be a transaction however, + * there would be many transactions forming a single file. This will be very + * inefficient. + * + * The model implemented here is as follows: each file transfer is a single + * transaction. All XID pairs unrelated to those file transfers create + * transactions per pair. + * + * A complicating factor is that the procedure matching is per tx, and a + * file transfer may have multiple procedures involved. Currently now only + * a COMMIT after WRITEs. A vector of additional procedures is kept to + * match on this. + * + * File tracking + * + * Files are tracked per 'FileTransferTracker' and are stored in the + * NFS3Transaction where they can be looked up per handle as part of the + * Transaction lookup. + */ + + +#[derive(Debug)] +pub struct NFS3Transaction { + pub id: u64, /// internal id + pub xid: u32, /// nfs3 req/reply pair id + pub procedure: u32, + pub file_name: Vec, + + pub request_machine_name: Vec, + pub request_uid: u32, + pub request_gid: u32, + + pub response_status: u32, + + pub has_creds: bool, + pub is_first: bool, + pub is_last: bool, + + /// for state tracking. false means this side is in progress, true + /// that it's complete. + request_done: bool, + response_done: bool, + + /// is a special file tx that we look up by file_handle instead of XID + pub is_file_tx: bool, + /// file transactions are unidirectional in the sense that they track + /// a single file on one direction + pub file_tx_direction: u8, // STREAM_TOCLIENT or STREAM_TOSERVER + pub file_handle: Vec, + + /// additional procedures part of a single file transfer. Currently + /// only COMMIT on WRITEs. + pub file_additional_procs: Vec, + + /// last xid of this file transfer. Last READ or COMMIT normally. + pub file_last_xid: u32, + + /// file tracker for a single file. Boxed so that we don't use + /// as much space if we're not a file tx. + pub file_tracker: Option>, + + pub logged: LoggerFlags, + pub de_state: Option<*mut DetectEngineState>, + pub events: *mut AppLayerDecoderEvents, +} + +impl NFS3Transaction { + pub fn new() -> NFS3Transaction { + return NFS3Transaction{ + id: 0, + xid: 0, + procedure: 0, + file_name:Vec::new(), + request_machine_name:Vec::new(), + request_uid:0, + request_gid:0, + response_status:0, + has_creds: false, + is_first: false, + is_last: false, + request_done: false, + response_done: false, + is_file_tx: false, + file_tx_direction: 0, + file_handle:Vec::new(), + file_additional_procs:Vec::new(), + file_last_xid: 0, + file_tracker: None, + logged: LoggerFlags::new(), + de_state: None, + events: std::ptr::null_mut(), + } + } + + pub fn free(&mut self) { + if self.events != std::ptr::null_mut() { + sc_app_layer_decoder_events_free_events(&mut self.events); + } + } +} + +#[derive(Debug)] +pub struct NFS3RequestXidMap { + procedure: u32, + chunk_offset: u64, + file_name:Vec, + + /// READ replies can use this to get to the handle the request used + file_handle:Vec, +} + +impl NFS3RequestXidMap { + pub fn new(procedure: u32, chunk_offset: u64) -> NFS3RequestXidMap { + NFS3RequestXidMap { + procedure:procedure, chunk_offset:chunk_offset, + file_name:Vec::new(), + file_handle:Vec::new(), + } + } +} + +#[derive(Debug)] +pub struct NFS3Files { + pub files_ts: FileContainer, + pub files_tc: FileContainer, + pub flags_ts: u16, + pub flags_tc: u16, +} + +impl NFS3Files { + pub fn new() -> NFS3Files { + NFS3Files { + files_ts:FileContainer::default(), + files_tc:FileContainer::default(), + flags_ts:0, + flags_tc:0, + } + } + pub fn free(&mut self) { + self.files_ts.free(); + self.files_tc.free(); + } + + pub fn get(&mut self, direction: u8) -> (&mut FileContainer, u16) + { + if direction == STREAM_TOSERVER { + (&mut self.files_ts, self.flags_ts) + } else { + (&mut self.files_tc, self.flags_tc) + } + } +} + +/// little wrapper around the FileTransferTracker::new_chunk method +fn filetracker_newchunk(ftopt: &mut Option>, files: &mut FileContainer, + flags: u16, name: &Vec, data: &[u8], + chunk_offset: u64, chunk_size: u32, fill_bytes: u8, is_last: bool, xid: &u32) +{ + match ftopt { + &mut Some(ref mut ft) => { + match unsafe {suricata_nfs3_file_config} { + Some(sfcm) => { + ft.new_chunk(sfcm, files, flags, &name, data, chunk_offset, + chunk_size, fill_bytes, is_last, xid); } + None => panic!("BUG"), + } + }, + &mut None => { panic!("BUG"); }, + } +} + +#[derive(Debug)] +pub struct NFS3State { + /// map xid to procedure so replies can lookup the procedure + pub requestmap: HashMap, + + /// map file handle (1) to name (2) + pub namemap: HashMap, Vec>, + + /// transactions list + pub transactions: Vec, + + /// TCP segments defragmentation buffer + pub tcp_buffer_ts: Vec, + pub tcp_buffer_tc: Vec, + + pub files: NFS3Files, + + /// partial record tracking + ts_chunk_xid: u32, + tc_chunk_xid: u32, + /// size of the current chunk that we still need to receive + ts_chunk_left: u32, + tc_chunk_left: u32, + + /// tx counter for assigning incrementing id's to tx's + tx_id: u64, + + pub de_state_count: u64, + + // HACK flag state if tx has been marked complete in a direction + // this way we can skip a lot of looping in output-tx.c + //pub ts_txs_updated: bool, + //pub tc_txs_updated: bool, +} + +impl NFS3State { + /// Allocation function for a new TLS parser instance + pub fn new() -> NFS3State { + NFS3State { + requestmap:HashMap::new(), + namemap:HashMap::new(), + transactions: Vec::new(), + tcp_buffer_ts:Vec::with_capacity(8192), + tcp_buffer_tc:Vec::with_capacity(8192), + files:NFS3Files::new(), + ts_chunk_xid:0, + tc_chunk_xid:0, + ts_chunk_left:0, + tc_chunk_left:0, + tx_id:0, + de_state_count:0, + //ts_txs_updated:false, + //tc_txs_updated:false, + } + } + pub fn free(&mut self) { + self.files.free(); + } + + pub fn new_tx(&mut self) -> NFS3Transaction { + let mut tx = NFS3Transaction::new(); + self.tx_id += 1; + tx.id = self.tx_id; + return tx; + } + + pub fn free_tx(&mut self, tx_id: u64) { + //SCLogNotice!("Freeing TX with ID {}", tx_id); + let len = self.transactions.len(); + let mut found = false; + let mut index = 0; + for i in 0..len { + let tx = &self.transactions[i]; + if tx.id == tx_id + 1 { + found = true; + index = i; + break; + } + } + if found { + SCLogDebug!("freeing TX with ID {} at index {}", tx_id, index); + self.free_tx_at_index(index); + } + } + + fn free_tx_at_index(&mut self, index: usize) { + let tx = self.transactions.remove(index); + match tx.de_state { + Some(state) => { + sc_detect_engine_state_free(state); + self.de_state_count -= 1; + } + _ => {} + } + } + + pub fn get_tx_by_id(&mut self, tx_id: u64) -> Option<&NFS3Transaction> { + SCLogDebug!("get_tx_by_id: tx_id={}", tx_id); + for tx in &mut self.transactions { + if tx.id == tx_id + 1 { + SCLogDebug!("Found NFS3 TX with ID {}", tx_id); + return Some(tx); + } + } + SCLogDebug!("Failed to find NFS3 TX with ID {}", tx_id); + return None; + } + + pub fn get_tx_by_xid(&mut self, tx_xid: u32) -> Option<&mut NFS3Transaction> { + SCLogDebug!("get_tx_by_xid: tx_xid={}", tx_xid); + for tx in &mut self.transactions { + if !tx.is_file_tx && tx.xid == tx_xid { + SCLogDebug!("Found NFS3 TX with ID {} XID {}", tx.id, tx.xid); + return Some(tx); + } + } + SCLogDebug!("Failed to find NFS3 TX with XID {}", tx_xid); + return None; + } + + // TODO maybe not enough users to justify a func + fn mark_response_tx_done(&mut self, xid: u32, status: u32) + { + match self.get_tx_by_xid(xid) { + Some(mut mytx) => { + mytx.response_done = true; + mytx.response_status = status; + + SCLogDebug!("process_reply_record: tx ID {} XID {} REQUEST {} RESPONSE {}", + mytx.id, mytx.xid, mytx.request_done, mytx.response_done); + }, + None => { + //SCLogNotice!("process_reply_record: not TX found for XID {}", r.hdr.xid); + }, + } + + //self.tc_txs_updated = true; + } + + fn process_request_record_lookup<'b>(&mut self, r: &RpcPacket<'b>, xidmap: &mut NFS3RequestXidMap) { + match parse_nfs3_request_lookup(r.prog_data) { + IResult::Done(_, lookup) => { + SCLogDebug!("LOOKUP {:?}", lookup); + xidmap.file_name = lookup.name_vec; + }, + IResult::Incomplete(_) => { panic!("WEIRD"); }, + IResult::Error(e) => { panic!("Parsing failed: {:?}",e); }, + }; + } + + /// complete request record + fn process_request_record<'b>(&mut self, r: &RpcPacket<'b>) -> u32 { + SCLogDebug!("REQUEST {} procedure {} ({}) blob size {}", + r.hdr.xid, r.procedure, self.requestmap.len(), r.prog_data.len()); + + let mut xidmap = NFS3RequestXidMap::new(r.procedure, 0); + + if r.procedure == NFSPROC3_LOOKUP { + self.process_request_record_lookup(r, &mut xidmap); + + } else if r.procedure == NFSPROC3_READ { + match parse_nfs3_request_read(r.prog_data) { + IResult::Done(_, nfs3_read_record) => { + xidmap.chunk_offset = nfs3_read_record.offset; + xidmap.file_handle = nfs3_read_record.handle.value.to_vec(); + + match self.namemap.get(nfs3_read_record.handle.value) { + Some(n) => { + SCLogDebug!("READ name {:?}", n); + xidmap.file_name = n.to_vec(); + }, + _ => { + SCLogDebug!("READ object {:?} not found", + nfs3_read_record.handle.value); + }, + } + }, + IResult::Incomplete(_) => { panic!("WEIRD"); }, + IResult::Error(e) => { panic!("Parsing failed: {:?}",e); }, + }; + } else if r.procedure == NFSPROC3_WRITE { + match parse_nfs3_request_write(r.prog_data) { + IResult::Done(_, w) => { + self.process_write_record(r, &w); + }, + IResult::Incomplete(_) => { panic!("WEIRD"); }, + IResult::Error(e) => { panic!("Parsing failed: {:?}",e); }, + } + } else if r.procedure == NFSPROC3_CREATE { + match parse_nfs3_request_create(r.prog_data) { + IResult::Done(_, nfs3_create_record) => { + xidmap.file_name = nfs3_create_record.name_vec; + }, + IResult::Incomplete(_) => { panic!("WEIRD"); }, + IResult::Error(e) => { panic!("Parsing failed: {:?}",e); }, + }; + + } else if r.procedure == NFSPROC3_COMMIT { + SCLogDebug!("COMMIT, closing shop"); + + match parse_nfs3_request_commit(r.prog_data) { + IResult::Done(_, cr) => { + let file_handle = cr.handle.value.to_vec(); + match self.get_file_tx_by_handle(&file_handle, STREAM_TOSERVER) { + Some((tx, files, flags)) => { + match tx.file_tracker { + Some(ref mut sft) => { // mutable reference to md for updating + sft.close(files, flags); + }, + None => { }, + } + tx.file_last_xid = r.hdr.xid; + tx.request_done = true; + tx.file_additional_procs.push(NFSPROC3_COMMIT); + }, + None => { }, + } + //self.ts_txs_updated = true; + }, + IResult::Incomplete(_) => { panic!("WEIRD"); }, + IResult::Error(e) => { panic!("Parsing failed: {:?}",e); }, + }; + } + + if !(r.procedure == NFSPROC3_COMMIT || // commit handled separately + r.procedure == NFSPROC3_WRITE || // write handled in file tx + r.procedure == NFSPROC3_READ) // read handled in file tx at reply + { + let mut tx = self.new_tx(); + tx.xid = r.hdr.xid; + tx.procedure = r.procedure; + tx.request_done = true; + tx.file_name = xidmap.file_name.to_vec(); + //self.ts_txs_updated = true; + + match &r.creds_unix { + &Some(ref u) => { + tx.request_machine_name = u.machine_name_buf.to_vec(); + tx.request_uid = u.uid; + tx.request_gid = u.gid; + tx.has_creds = true; + }, + _ => { }, + } + SCLogDebug!("TX created: ID {} XID {} PROCEDURE {}", + tx.id, tx.xid, tx.procedure); + self.transactions.push(tx); + } + + self.requestmap.insert(r.hdr.xid, xidmap); + 0 + } + + fn new_file_tx(&mut self, file_handle: &Vec, file_name: &Vec, direction: u8) + -> (&mut NFS3Transaction, &mut FileContainer, u16) + { + let mut tx = self.new_tx(); + tx.file_name = file_name.to_vec(); + tx.file_handle = file_handle.to_vec(); + tx.is_file_tx = true; + tx.file_tx_direction = direction; + + tx.file_tracker = Some(Box::new(FileTransferTracker::new())); + match tx.file_tracker { + Some(ref mut ft) => { ft.tx_id = tx.id; }, + None => { }, + } + + SCLogDebug!("new_file_tx: TX FILE created: ID {} NAME {}", + tx.id, String::from_utf8_lossy(file_name)); + self.transactions.push(tx); + let tx_ref = self.transactions.last_mut(); + let (files, flags) = self.files.get(direction); + return (tx_ref.unwrap(), files, flags) + } + + fn get_file_tx_by_handle(&mut self, file_handle: &Vec, direction: u8) + -> Option<(&mut NFS3Transaction, &mut FileContainer, u16)> + { + let fh = file_handle.to_vec(); + for tx in &mut self.transactions { + if tx.is_file_tx && + direction == tx.file_tx_direction && + tx.file_handle == fh + { + SCLogDebug!("Found NFS3 file TX with ID {} XID {}", tx.id, tx.xid); + let (files, flags) = self.files.get(direction); + return Some((tx, files, flags)); + } + } + SCLogDebug!("Failed to find NFS3 TX with handle {:?}", file_handle); + return None; + } + + fn process_write_record<'b>(&mut self, r: &RpcPacket<'b>, w: &Nfs3RequestWrite<'b>) -> u32 { + // for now assume that stable FILE_SYNC flags means a single chunk + let is_last = if w.stable == 2 { true } else { false }; + + let mut fill_bytes = 0; + let pad = w.file_len % 4; + if pad != 0 { + fill_bytes = 4 - pad; + } + + let file_handle = w.handle.value.to_vec(); + let file_name = match self.namemap.get(w.handle.value) { + Some(n) => { + SCLogDebug!("WRITE name {:?}", n); + n.to_vec() + }, + None => { + SCLogDebug!("WRITE object {:?} not found", w.handle.value); + Vec::new() + }, + }; + + let found = match self.get_file_tx_by_handle(&file_handle, STREAM_TOSERVER) { + Some((tx, files, flags)) => { + filetracker_newchunk(&mut tx.file_tracker, files, flags, + &file_name, w.file_data, w.offset, + w.file_len, fill_bytes as u8, is_last, &r.hdr.xid); + if is_last { + tx.file_last_xid = r.hdr.xid; + tx.is_last = true; + tx.response_done = true; + } + true + }, + None => { false }, + }; + if !found { + let (tx, files, flags) = self.new_file_tx(&file_handle, &file_name, STREAM_TOSERVER); + filetracker_newchunk(&mut tx.file_tracker, files, flags, + &file_name, w.file_data, w.offset, + w.file_len, fill_bytes as u8, is_last, &r.hdr.xid); + tx.procedure = NFSPROC3_WRITE; + tx.xid = r.hdr.xid; + tx.is_first = true; + if is_last { + tx.file_last_xid = r.hdr.xid; + tx.is_last = true; + tx.request_done = true; + } + } + self.ts_chunk_xid = r.hdr.xid; + let file_data_len = w.file_data.len() as u32 - fill_bytes as u32; + self.ts_chunk_left = w.file_len as u32 - file_data_len as u32; + 0 + } + + fn process_partial_write_request_record<'b>(&mut self, r: &RpcPacket<'b>, w: &Nfs3RequestWrite<'b>) -> u32 { + SCLogDebug!("REQUEST {} procedure {} blob size {}", r.hdr.xid, r.procedure, r.prog_data.len()); + + if r.procedure != NFSPROC3_WRITE { + panic!("call me for procedure WRITE *only*"); + } + + let mut xidmap = NFS3RequestXidMap::new(r.procedure, 0); + xidmap.file_handle = w.handle.value.to_vec(); + self.requestmap.insert(r.hdr.xid, xidmap); + + return self.process_write_record(r, w); + } + + fn process_reply_record<'b>(&mut self, r: &RpcReplyPacket<'b>) -> u32 { + let status; + let xidmap; + match self.requestmap.remove(&r.hdr.xid) { + Some(p) => { xidmap = p; }, + _ => { SCLogDebug!("REPLY: xid {} NOT FOUND", r.hdr.xid); return 0; }, + } + + if xidmap.procedure == NFSPROC3_LOOKUP { + match parse_nfs3_response_lookup(r.prog_data) { + IResult::Done(_, lookup) => { + SCLogDebug!("LOOKUP: {:?}", lookup); + SCLogDebug!("RESPONSE LOOKUP file_name {:?}", xidmap.file_name); + + status = lookup.status; + + SCLogDebug!("LOOKUP handle {:?}", lookup.handle); + self.namemap.insert(lookup.handle.value.to_vec(), xidmap.file_name); + }, + IResult::Incomplete(_) => { panic!("WEIRD"); }, + IResult::Error(e) => { panic!("Parsing failed: {:?}",e); }, + }; + } else if xidmap.procedure == NFSPROC3_CREATE { + match parse_nfs3_response_create(r.prog_data) { + IResult::Done(_, nfs3_create_record) => { + SCLogDebug!("nfs3_create_record: {:?}", nfs3_create_record); + + SCLogDebug!("RESPONSE CREATE file_name {:?}", xidmap.file_name); + status = nfs3_create_record.status; + + match nfs3_create_record.handle { + Some(h) => { + SCLogDebug!("handle {:?}", h); + self.namemap.insert(h.value.to_vec(), xidmap.file_name); + }, + _ => { }, + } + + }, + IResult::Incomplete(_) => { panic!("WEIRD"); }, + IResult::Error(e) => { panic!("Parsing failed: {:?}",e); }, + }; + } else if xidmap.procedure == NFSPROC3_READ { + match parse_nfs3_reply_read(r.prog_data) { + IResult::Done(_, ref reply) => { + self.process_read_record(r, reply, Some(&xidmap)); + status = reply.status; + }, + IResult::Incomplete(_) => { panic!("Incomplete!"); }, + IResult::Error(e) => { panic!("Parsing failed: {:?}",e); }, + } + } else if xidmap.procedure == NFSPROC3_READDIRPLUS { + match parse_nfs3_response_readdirplus(r.prog_data) { + IResult::Done(_, ref reply) => { + //SCLogDebug!("READDIRPLUS reply {:?}", reply); + + status = reply.status; + + // cut off final eof field + let d = &reply.data[..reply.data.len()-4 as usize]; + + // store all handle/filename mappings + match many0_nfs3_response_readdirplus_entries(d) { + IResult::Done(_, ref entries) => { + for ce in entries { + SCLogDebug!("ce {:?}", ce); + match ce.entry { + Some(ref e) => { + SCLogDebug!("e {:?}", e); + match e.handle { + Some(ref h) => { + SCLogDebug!("h {:?}", h); + self.namemap.insert(h.value.to_vec(), e.name_vec.to_vec()); + }, + _ => { }, + } + }, + _ => { }, + } + } + + SCLogDebug!("READDIRPLUS ENTRIES reply {:?}", entries); + }, + IResult::Incomplete(_) => { panic!("Incomplete!"); }, + IResult::Error(e) => { panic!("Parsing failed: {:?}",e); }, + } + }, + IResult::Incomplete(_) => { panic!("Incomplete!"); }, + IResult::Error(e) => { panic!("Parsing failed: {:?}",e); }, + } + } + // for all other record types only parse the status + else { + let stat = match nom::be_u32(&r.prog_data) { + nom::IResult::Done(_, stat) => { + stat as u32 + } + _ => 0 as u32 + }; + status = stat; + } + SCLogDebug!("REPLY {} to procedure {} blob size {}", + r.hdr.xid, xidmap.procedure, r.prog_data.len()); + + if xidmap.procedure != NFSPROC3_READ { + self.mark_response_tx_done(r.hdr.xid, status); + } + + 0 + } + + // update in progress chunks for file transfers + // return how much data we consumed + fn filetracker_update(&mut self, direction: u8, data: &[u8]) -> u32 { + let mut chunk_left = if direction == STREAM_TOSERVER { + self.ts_chunk_left + } else { + self.tc_chunk_left + }; + if chunk_left == 0 { + return 0 + } + let xid = if direction == STREAM_TOSERVER { + self.ts_chunk_xid + } else { + self.tc_chunk_xid + }; + SCLogDebug!("chunk left {}, input {}", chunk_left, data.len()); + + let file_handle; + // we have the data that we expect + if chunk_left <= data.len() as u32 { + chunk_left = 0; + + if direction == STREAM_TOSERVER { + self.ts_chunk_xid = 0; + + // see if we have a file handle to work on + match self.requestmap.get(&xid) { + None => { + SCLogDebug!("no file handle found for XID {:04X}", xid); + return 0 + }, + Some(ref xidmap) => { + file_handle = xidmap.file_handle.to_vec(); + }, + } + } else { + self.tc_chunk_xid = 0; + + // chunk done, remove requestmap entry + match self.requestmap.remove(&xid) { + None => { + SCLogDebug!("no file handle found for XID {:04X}", xid); + return 0 + }, + Some(xidmap) => { + file_handle = xidmap.file_handle.to_vec(); + }, + } + } + } else { + chunk_left -= data.len() as u32; + + // see if we have a file handle to work on + match self.requestmap.get(&xid) { + None => { + SCLogDebug!("no file handle found for XID {:04X}", xid); + return 0 }, + Some(xidmap) => { + file_handle = xidmap.file_handle.to_vec(); + }, + } + } + + if direction == STREAM_TOSERVER { + self.ts_chunk_left = chunk_left; + } else { + self.tc_chunk_left = chunk_left; + } + + // get the tx and update it + let consumed = match self.get_file_tx_by_handle(&file_handle, direction) { + Some((tx, files, flags)) => { + let cs1 = match tx.file_tracker { + Some(ref mut ft) => { + let cs2 = ft.update(files, flags, data); + // return number of bytes we consumed + SCLogDebug!("consumed {}", cs2); + cs2 + }, + None => { 0 }, + }; + cs1 + }, + None => { 0 }, + }; + return consumed; + } + + /// xidmapr is an Option as it's already removed from the map if we + /// have a complete record. Otherwise we do a lookup ourselves. + fn process_read_record<'b>(&mut self, r: &RpcReplyPacket<'b>, + reply: &Nfs3ReplyRead<'b>, xidmapr: Option<&NFS3RequestXidMap>) -> u32 + { + let file_name; + let file_handle; + let chunk_offset; + + match xidmapr { + Some(xidmap) => { + file_name = xidmap.file_name.to_vec(); + file_handle = xidmap.file_handle.to_vec(); + chunk_offset = xidmap.chunk_offset; + }, + None => { + match self.requestmap.get(&r.hdr.xid) { + Some(xidmap) => { + file_name = xidmap.file_name.to_vec(); + file_handle = xidmap.file_handle.to_vec(); + chunk_offset = xidmap.chunk_offset; + }, + _ => { panic!("REPLY: xid {} NOT FOUND", r.hdr.xid); }, + } + }, + } + + let is_last = reply.eof; + let mut fill_bytes = 0; + let pad = reply.count % 4; + if pad != 0 { + fill_bytes = 4 - pad; + } + + let found = match self.get_file_tx_by_handle(&file_handle, STREAM_TOCLIENT) { + Some((tx, files, flags)) => { + filetracker_newchunk(&mut tx.file_tracker, files, flags, + &file_name, reply.data, chunk_offset, + reply.count, fill_bytes as u8, reply.eof, &r.hdr.xid); + if is_last { + tx.file_last_xid = r.hdr.xid; + tx.response_status = reply.status; + tx.is_last = true; + tx.response_done = true; + } + true + }, + None => { false }, + }; + if !found { + let (tx, files, flags) = self.new_file_tx(&file_handle, &file_name, STREAM_TOCLIENT); + filetracker_newchunk(&mut tx.file_tracker, files, flags, + &file_name, reply.data, chunk_offset, + reply.count, fill_bytes as u8, reply.eof, &r.hdr.xid); + tx.procedure = NFSPROC3_READ; + tx.xid = r.hdr.xid; + tx.is_first = true; + if is_last { + tx.file_last_xid = r.hdr.xid; + tx.response_status = reply.status; + tx.is_last = true; + tx.response_done = true; + } + } + + //if is_last { + // self.tc_txs_updated = true; + //} + self.tc_chunk_xid = r.hdr.xid; + self.tc_chunk_left = reply.count as u32 - reply.data.len() as u32; + + SCLogDebug!("REPLY {} to procedure {} blob size {} / {}: chunk_left {}", + r.hdr.xid, NFSPROC3_READ, r.prog_data.len(), reply.count, self.tc_chunk_left); + 0 + } + + fn process_partial_read_reply_record<'b>(&mut self, r: &RpcReplyPacket<'b>, reply: &Nfs3ReplyRead<'b>) -> u32 { + SCLogDebug!("REPLY {} to procedure READ blob size {} / {}", + r.hdr.xid, r.prog_data.len(), reply.count); + + return self.process_read_record(r, reply, None); + } + + fn peek_reply_record(&mut self, r: &RpcPacketHeader) -> u32 { + let xidmap; + match self.requestmap.get(&r.xid) { + Some(p) => { xidmap = p; }, + _ => { SCLogDebug!("REPLY: xid {} NOT FOUND", r.xid); return 0; }, + } + + xidmap.procedure + } + + /// Parsing function, handling TCP chunks fragmentation + pub fn parse_tcp_data_ts<'b>(&mut self, i: &'b[u8]) -> u32 { + let mut v : Vec; + let mut status = 0; + SCLogDebug!("parse_tcp_data_ts ({})",i.len()); + //SCLogDebug!("{:?}",i); + // Check if TCP data is being defragmented + let tcp_buffer = match self.tcp_buffer_ts.len() { + 0 => i, + _ => { + v = self.tcp_buffer_ts.split_off(0); + // sanity check vector length to avoid memory exhaustion + if self.tcp_buffer_ts.len() + i.len() > 1000000 { + SCLogNotice!("parse_tcp_data_ts: TS buffer exploded {} {}", + self.tcp_buffer_ts.len(), i.len()); + return 1; + }; + v.extend_from_slice(i); + v.as_slice() + }, + }; + //SCLogDebug!("tcp_buffer ({})",tcp_buffer.len()); + let mut cur_i = tcp_buffer; + if cur_i.len() > 1000000 { + SCLogNotice!("BUG buffer exploded: {}", cur_i.len()); + } + + // take care of in progress file chunk transfers + // and skip buffer beyond it + let consumed = self.filetracker_update(STREAM_TOSERVER, cur_i); + if consumed > 0 { + if consumed > cur_i.len() as u32 { panic!("BUG consumed more than we gave it"); } + cur_i = &cur_i[consumed as usize..]; + } + + while cur_i.len() > 0 { // min record size + match parse_rpc_request_partial(cur_i) { + IResult::Done(_, ref rpc_phdr) => { + let rec_size = (rpc_phdr.hdr.frag_len + 4) as usize; + //SCLogDebug!("rec_size {}/{}", rec_size, cur_i.len()); + //SCLogDebug!("cur_i {:?}", cur_i); + + if rec_size > 40000 { panic!("invalid rec_size"); } + if rec_size > cur_i.len() { + // special case: avoid buffering file write blobs + // as these can be large. + if rec_size >= 512 && cur_i.len() >= 44 { + // large record, likely file xfer + SCLogDebug!("large record {}, likely file xfer", rec_size); + + // quick peek, are in WRITE mode? + if rpc_phdr.procedure == NFSPROC3_WRITE { + SCLogDebug!("CONFIRMED WRITE: large record {}, file chunk xfer", rec_size); + + // lets try to parse the RPC record. Might fail with Incomplete. + match parse_rpc(cur_i) { + IResult::Done(remaining, ref rpc_record) => { + match parse_nfs3_request_write(rpc_record.prog_data) { + IResult::Done(_, ref nfs_request_write) => { + // deal with the partial nfs write data + status |= self.process_partial_write_request_record(rpc_record, nfs_request_write); + cur_i = remaining; // progress input past parsed record + }, + IResult::Incomplete(_) => { + SCLogDebug!("TS WRITE record incomplete"); + }, + IResult::Error(e) => { panic!("Parsing failed: {:?}",e); }, + } + }, + IResult::Incomplete(_) => { + // we just size checked for the minimal record size above, + // so if options are used (creds/verifier), we can still + // have Incomplete data. Fall through to the buffer code + // and try again on our next iteration. + SCLogDebug!("TS data incomplete"); + }, + IResult::Error(e) => { panic!("Parsing failed: {:?}",e); }, + } + } + } + self.tcp_buffer_ts.extend_from_slice(cur_i); + break; + } + + // we have the full records size worth of data, + // let's parse it + match parse_rpc(&cur_i[..rec_size]) { + IResult::Done(_, ref rpc_record) => { + cur_i = &cur_i[rec_size..]; + status |= self.process_request_record(rpc_record); + }, + IResult::Incomplete(x) => { + // should be unreachable unless our rec_size calc is off + panic!("TS data incomplete while we checked for rec_size? BUG {:?}", x); + //self.tcp_buffer_ts.extend_from_slice(cur_i); + //break; + }, + IResult::Error(e) => { panic!("Parsing failed: {:?}",e); //break + }, + } + }, + IResult::Incomplete(_) => { + SCLogDebug!("Fragmentation required (TCP level) 2"); + self.tcp_buffer_ts.extend_from_slice(cur_i); + break; + }, + IResult::Error(e) => { panic!("Parsing failed: {:?}",e); //break + }, + } + }; + status + } + + /// Parsing function, handling TCP chunks fragmentation + pub fn parse_tcp_data_tc<'b>(&mut self, i: &'b[u8]) -> u32 { + let mut v : Vec; + let mut status = 0; + SCLogDebug!("parse_tcp_data_tc ({})",i.len()); + //SCLogDebug!("{:?}",i); + // Check if TCP data is being defragmented + let tcp_buffer = match self.tcp_buffer_tc.len() { + 0 => i, + _ => { + v = self.tcp_buffer_tc.split_off(0); + // sanity check vector length to avoid memory exhaustion + if self.tcp_buffer_tc.len() + i.len() > 100000 { + SCLogDebug!("TC buffer exploded"); + return 1; + }; + v.extend_from_slice(i); + v.as_slice() + }, + }; + SCLogDebug!("tcp_buffer ({})",tcp_buffer.len()); + + let mut cur_i = tcp_buffer; + if cur_i.len() > 100000 { + SCLogNotice!("parse_tcp_data_tc: BUG buffer exploded {}", cur_i.len()); + } + + // take care of in progress file chunk transfers + // and skip buffer beyond it + let consumed = self.filetracker_update(STREAM_TOCLIENT, cur_i); + if consumed > 0 { + if consumed > cur_i.len() as u32 { panic!("BUG consumed more than we gave it"); } + cur_i = &cur_i[consumed as usize..]; + } + + while cur_i.len() > 0 { + match parse_rpc_packet_header(cur_i) { + IResult::Done(_, ref rpc_hdr) => { + let rec_size = (rpc_hdr.frag_len + 4) as usize; + // see if we have all data available + if rec_size > cur_i.len() { + // special case: avoid buffering file read blobs + // as these can be large. + if rec_size >= 512 && cur_i.len() >= 128 {//36 { + // large record, likely file xfer + SCLogDebug!("large record {}, likely file xfer", rec_size); + + // quick peek, are in READ mode? + if self.peek_reply_record(&rpc_hdr) == NFSPROC3_READ { + SCLogDebug!("CONFIRMED large READ record {}, likely file chunk xfer", rec_size); + + // we should have enough data to parse the RPC record + match parse_rpc_reply(cur_i) { + IResult::Done(remaining, ref rpc_record) => { + match parse_nfs3_reply_read(rpc_record.prog_data) { + IResult::Done(_, ref nfs_reply_read) => { + // deal with the partial nfs read data + status |= self.process_partial_read_reply_record(rpc_record, nfs_reply_read); + cur_i = remaining; // progress input past parsed record + }, + IResult::Incomplete(_) => { + SCLogDebug!("TS WRITE record incomplete"); + }, + IResult::Error(e) => { panic!("Parsing failed: {:?}",e); }, + } + }, + IResult::Incomplete(_) => { + // size check was done for MINIMAL record size, + // so Incomplete is normal. + SCLogDebug!("TC data incomplete"); + }, + IResult::Error(e) => { panic!("Parsing failed: {:?}",e); }, + } + } + } + self.tcp_buffer_tc.extend_from_slice(cur_i); + break; + } + + // we have the full data of the record, lets parse + match parse_rpc_reply(&cur_i[..rec_size]) { + IResult::Done(_, ref rpc_record) => { + cur_i = &cur_i[rec_size..]; // progress input past parsed record + status |= self.process_reply_record(rpc_record); + }, + IResult::Incomplete(_) => { + // we shouldn't get incomplete as we have the full data + panic!("TC data incomplete, BUG!"); + //self.tcp_buffer_tc.extend_from_slice(cur_i); + //break; + }, + IResult::Error(e) => { panic!("Parsing failed: {:?}",e); //break + }, + } + }, + IResult::Incomplete(_) => { + SCLogDebug!("REPLY: insufficient data for HDR"); + self.tcp_buffer_tc.extend_from_slice(cur_i); + break; + }, + IResult::Error(e) => { SCLogDebug!("Parsing failed: {:?}",e); break }, + } + }; + status + } + fn getfiles(&mut self, direction: u8) -> * mut FileContainer { + //SCLogDebug!("direction: {}", direction); + if direction == STREAM_TOCLIENT { + &mut self.files.files_tc as *mut FileContainer + } else { + &mut self.files.files_ts as *mut FileContainer + } + } + fn setfileflags(&mut self, direction: u8, flags: u16) { + SCLogDebug!("direction: {}, flags: {}", direction, flags); + if direction == 1 { + self.files.flags_tc = flags; + } else { + self.files.flags_ts = flags; + } + } +} + +/// Returns *mut NFS3State +#[no_mangle] +pub extern "C" fn rs_nfs3_state_new() -> *mut libc::c_void { + let state = NFS3State::new(); + let boxed = Box::new(state); + SCLogDebug!("allocating state"); + return unsafe{transmute(boxed)}; +} + +/// Params: +/// - state: *mut NFS3State as void pointer +#[no_mangle] +pub extern "C" fn rs_nfs3_state_free(state: *mut libc::c_void) { + // Just unbox... + SCLogDebug!("freeing state"); + let mut nfs3_state: Box = unsafe{transmute(state)}; + nfs3_state.free(); +} + +/// C binding parse a DNS request. Returns 1 on success, -1 on failure. +#[no_mangle] +pub extern "C" fn rs_nfs3_parse_request(_flow: *mut Flow, + state: &mut NFS3State, + _pstate: *mut libc::c_void, + input: *mut libc::uint8_t, + input_len: libc::uint32_t, + _data: *mut libc::c_void) + -> libc::int8_t +{ + let buf = unsafe{std::slice::from_raw_parts(input, input_len as usize)}; + SCLogDebug!("parsing {} bytes of request data", input_len); + if state.parse_tcp_data_ts(buf) == 0 { + 1 + } else { + -1 + } +} + +#[no_mangle] +pub extern "C" fn rs_nfs3_parse_response(_flow: *mut Flow, + state: &mut NFS3State, + _pstate: *mut libc::c_void, + input: *mut libc::uint8_t, + input_len: libc::uint32_t, + _data: *mut libc::c_void) + -> libc::int8_t +{ + SCLogDebug!("parsing {} bytes of response data", input_len); + let buf = unsafe{std::slice::from_raw_parts(input, input_len as usize)}; + if state.parse_tcp_data_tc(buf) == 0 { + 1 + } else { + -1 + } +} + +#[no_mangle] +pub extern "C" fn rs_nfs3_state_get_tx_count(state: &mut NFS3State) + -> libc::uint64_t +{ + SCLogDebug!("rs_nfs3_state_get_tx_count: returning {}", state.tx_id); + return state.tx_id; +} + +#[no_mangle] +pub extern "C" fn rs_nfs3_state_get_tx(state: &mut NFS3State, + tx_id: libc::uint64_t) + -> *mut NFS3Transaction +{ + match state.get_tx_by_id(tx_id) { + Some(tx) => { + return unsafe{transmute(tx)}; + } + None => { + return std::ptr::null_mut(); + } + } +} + +#[no_mangle] +pub extern "C" fn rs_nfs3_state_tx_free(state: &mut NFS3State, + tx_id: libc::uint64_t) +{ + state.free_tx(tx_id); +} + +#[no_mangle] +pub extern "C" fn rs_nfs3_state_progress_completion_status( + _direction: libc::uint8_t) + -> libc::c_int +{ + return 1; +} + +#[no_mangle] +pub extern "C" fn rs_nfs3_tx_get_alstate_progress(tx: &mut NFS3Transaction, + direction: libc::uint8_t) + -> libc::uint8_t +{ + if direction == STREAM_TOSERVER && tx.request_done { + //SCLogNotice!("TOSERVER progress 1"); + return 1; + } else if direction == STREAM_TOCLIENT && tx.response_done { + //SCLogNotice!("TOCLIENT progress 1"); + return 1; + } else { + //SCLogNotice!("{} progress 0", direction); + return 0; + } +} + +/* +#[no_mangle] +pub extern "C" fn rs_nfs3_get_txs_updated(state: &mut NFS3State, + direction: u8) -> bool +{ + if direction == STREAM_TOSERVER { + return state.ts_txs_updated; + } else { + return state.tc_txs_updated; + } +} + +#[no_mangle] +pub extern "C" fn rs_nfs3_reset_txs_updated(state: &mut NFS3State, + direction: u8) +{ + if direction == STREAM_TOSERVER { + state.ts_txs_updated = false; + } else { + state.tc_txs_updated = false; + } +} +*/ + +#[no_mangle] +pub extern "C" fn rs_nfs3_tx_set_logged(_state: &mut NFS3State, + tx: &mut NFS3Transaction, + logger: libc::uint32_t) +{ + tx.logged.set_logged(logger); +} + +#[no_mangle] +pub extern "C" fn rs_nfs3_tx_get_logged(_state: &mut NFS3State, + tx: &mut NFS3Transaction, + logger: libc::uint32_t) + -> i8 +{ + if tx.logged.is_logged(logger) { + return 1; + } + return 0; +} + +#[no_mangle] +pub extern "C" fn rs_nfs3_state_has_detect_state(state: &mut NFS3State) -> u8 +{ + if state.de_state_count > 0 { + return 1; + } + return 0; +} + +#[no_mangle] +pub extern "C" fn rs_nfs3_state_set_tx_detect_state( + state: &mut NFS3State, + tx: &mut NFS3Transaction, + de_state: &mut DetectEngineState) +{ + state.de_state_count += 1; + tx.de_state = Some(de_state); +} + +#[no_mangle] +pub extern "C" fn rs_nfs3_state_get_tx_detect_state( + tx: &mut NFS3Transaction) + -> *mut DetectEngineState +{ + match tx.de_state { + Some(ds) => { + return ds; + }, + None => { + return std::ptr::null_mut(); + } + } +} + +/// return procedure(s) in the tx. At 0 return the main proc, +/// otherwise get procs from the 'file_additional_procs'. +/// Keep calling until 0 is returned. +#[no_mangle] +pub extern "C" fn rs_nfs3_tx_get_procedures(tx: &mut NFS3Transaction, + i: libc::uint16_t, + procedure: *mut libc::uint32_t) + -> libc::uint8_t +{ + if i == 0 { + unsafe { + *procedure = tx.procedure as libc::uint32_t; + } + return 1; + } + + let idx = i as usize - 1; + if idx < tx.file_additional_procs.len() { + let p = tx.file_additional_procs[idx]; + unsafe { + *procedure = p as libc::uint32_t; + } + return 1; + } + return 0; +} + +#[no_mangle] +pub extern "C" fn rs_nfs3_init(context: &'static mut SuricataFileContext) +{ + unsafe { + suricata_nfs3_file_config = Some(context); + } +} + +/// TOSERVER probe function +#[no_mangle] +pub extern "C" fn rs_nfs_probe(input: *const libc::uint8_t, len: libc::uint32_t) + -> libc::int8_t +{ + let slice: &[u8] = unsafe { + std::slice::from_raw_parts(input as *mut u8, len as usize) + }; + match parse_rpc(slice) { + IResult::Done(_, ref rpc_hdr) => { + if rpc_hdr.progver == 3 && rpc_hdr.program == 100003 { + return 1; + } else { + return -1; + } + }, + IResult::Incomplete(_) => { + return 0; + }, + IResult::Error(_) => { + return -1; + }, + } +} + +#[no_mangle] +pub extern "C" fn rs_nfs3_getfiles(direction: u8, ptr: *mut NFS3State) -> * mut FileContainer { + if ptr.is_null() { panic!("NULL ptr"); }; + let parser = unsafe { &mut *ptr }; + parser.getfiles(direction) +} +#[no_mangle] +pub extern "C" fn rs_nfs3_setfileflags(direction: u8, ptr: *mut NFS3State, flags: u16) { + if ptr.is_null() { panic!("NULL ptr"); }; + let parser = unsafe { &mut *ptr }; + SCLogDebug!("direction {} flags {}", direction, flags); + parser.setfileflags(direction, flags) +} + diff --git a/rust/src/nfs/parser.rs b/rust/src/nfs/parser.rs new file mode 100644 index 0000000000..4f67cd0203 --- /dev/null +++ b/rust/src/nfs/parser.rs @@ -0,0 +1,531 @@ +/* Copyright (C) 2017 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. + */ + +//! Nom parsers for RPC & NFSv3 + +use nom::{be_u32, be_u64, rest}; + +#[derive(Debug,PartialEq)] +pub struct RpcRequestCredsUnix<'a> { + pub stamp: u32, + pub machine_name_len: u32, + pub machine_name_buf: &'a[u8], + pub uid: u32, + pub gid: u32, + pub aux_gids: Option>, + // list of gids +} + +//named!(parse_rpc_creds_unix_aux_gids>, +// many0!(be_u32) +//); + +named!(pub parse_rfc_request_creds_unix, + do_parse!( + stamp: be_u32 + >> machine_name_len: be_u32 + >> machine_name_buf: take!(machine_name_len) + >> uid: be_u32 + >> gid: be_u32 + //>> aux_gids: parse_rpc_creds_unix_aux_gids + + >> ( + RpcRequestCredsUnix { + stamp:stamp, + machine_name_len:machine_name_len, + machine_name_buf:machine_name_buf, + uid:uid, + gid:gid, + aux_gids:None, + } + )) +); + +#[derive(Debug,PartialEq)] +pub struct Nfs3Handle<'a> { + pub len: u32, + pub value: &'a[u8], +} + +named!(pub parse_nfs3_handle, + do_parse!( + obj_len: be_u32 + >> obj: take!(obj_len) + >> ( + Nfs3Handle { + len:obj_len, + value:obj, + } + )) +); + +#[derive(Debug,PartialEq)] +pub struct Nfs3ReplyCreate<'a> { + pub status: u32, + pub handle: Option>, +} + +named!(pub parse_nfs3_response_create, + do_parse!( + status: be_u32 + >> handle_has_value: be_u32 + >> handle: cond!(handle_has_value == 1, parse_nfs3_handle) + >> ( + Nfs3ReplyCreate { + status:status, + handle:handle, + } + )) +); + +#[derive(Debug,PartialEq)] +pub struct Nfs3ReplyLookup<'a> { + pub status: u32, + pub handle: Nfs3Handle<'a>, +} + +named!(pub parse_nfs3_response_lookup, + do_parse!( + status: be_u32 + >> handle: parse_nfs3_handle + >> ( + Nfs3ReplyLookup { + status:status, + handle:handle, + } + )) +); + +#[derive(Debug,PartialEq)] +pub struct Nfs3RequestCreate<'a> { + pub handle: Nfs3Handle<'a>, + pub name_len: u32, + pub create_mode: u32, + pub verifier: &'a[u8], + pub name_vec: Vec, +} + +named!(pub parse_nfs3_request_create, + do_parse!( + handle: parse_nfs3_handle + >> name_len: be_u32 + >> name: take!(name_len) + >> create_mode: be_u32 + >> verifier: rest + >> ( + Nfs3RequestCreate { + handle:handle, + name_len:name_len, + create_mode:create_mode, + verifier:verifier, + name_vec:name.to_vec(), + } + )) +); + +#[derive(Debug,PartialEq)] +pub struct Nfs3RequestAccess<'a> { + pub handle: Nfs3Handle<'a>, + pub check_access: u32, +} + +named!(pub parse_nfs3_request_access, + do_parse!( + handle: parse_nfs3_handle + >> check_access: be_u32 + >> ( + Nfs3RequestAccess { + handle:handle, + check_access:check_access, + } + )) +); + +#[derive(Debug,PartialEq)] +pub struct Nfs3RequestCommit<'a> { + pub handle: Nfs3Handle<'a>, +} + +named!(pub parse_nfs3_request_commit, + do_parse!( + handle: parse_nfs3_handle + >> offset: be_u64 + >> count: be_u32 + >> ( + Nfs3RequestCommit { + handle:handle, + } + )) +); + +#[derive(Debug,PartialEq)] +pub struct Nfs3RequestRead<'a> { + pub handle: Nfs3Handle<'a>, + pub offset: u64, +} + +named!(pub parse_nfs3_request_read, + do_parse!( + handle: parse_nfs3_handle + >> offset: be_u64 + >> count: be_u32 + >> ( + Nfs3RequestRead { + handle:handle, + offset:offset, + } + )) +); + +#[derive(Debug,PartialEq)] +pub struct Nfs3RequestLookup<'a> { + pub handle: Nfs3Handle<'a>, + + pub name_vec: Vec, +} + +named!(pub parse_nfs3_request_lookup, + do_parse!( + handle: parse_nfs3_handle + >> name_len: be_u32 + >> name_contents: take!(name_len) + >> name_padding: rest + >> ( + Nfs3RequestLookup { + handle:handle, + name_vec:name_contents.to_vec(), + } + )) +); + + +#[derive(Debug,PartialEq)] +pub struct Nfs3ResponseReaddirplusEntryC<'a> { + pub name_vec: Vec, + pub handle: Option>, +} + +named!(pub parse_nfs3_response_readdirplus_entry, + do_parse!( + file_id: be_u64 + >> name_len: be_u32 + >> name_content: take!(name_len) + >> fill_bytes: cond!(name_len % 4 != 0, take!(4 - name_len % 4)) + >> cookie: take!(8) + >> attr_value_follows: be_u32 + >> attr: cond!(attr_value_follows==1, take!(84)) + >> handle_value_follows: be_u32 + >> handle: cond!(handle_value_follows==1, parse_nfs3_handle) + >> ( + Nfs3ResponseReaddirplusEntryC { + name_vec:name_content.to_vec(), + handle:handle, + } + ) + ) +); + +#[derive(Debug,PartialEq)] +pub struct Nfs3ResponseReaddirplusEntry<'a> { + pub entry: Option>, +} + +named!(pub parse_nfs3_response_readdirplus_entry_cond, + do_parse!( + value_follows: be_u32 + >> entry: cond!(value_follows==1, parse_nfs3_response_readdirplus_entry) + >> ( + Nfs3ResponseReaddirplusEntry { + entry:entry, + } + )) +); + +#[derive(Debug,PartialEq)] +pub struct Nfs3ResponseReaddirplus<'a> { + pub status: u32, + pub data: &'a[u8], +} + +named!(pub parse_nfs3_response_readdirplus, + do_parse!( + status: be_u32 + >> dir_attr_follows: be_u32 + >> dir_attr: cond!(dir_attr_follows == 1, take!(84)) + >> verifier: take!(8) + >> data: rest + + >> ( Nfs3ResponseReaddirplus { + status:status, + data:data, + } )) +); + +#[derive(Debug,PartialEq)] +pub struct Nfs3RequestReaddirplus<'a> { + pub handle: Nfs3Handle<'a>, + + pub cookie: u32, + pub verifier: &'a[u8], + pub dircount: u32, + pub maxcount: u32, +} + +named!(pub parse_nfs3_request_readdirplus, + do_parse!( + handle: parse_nfs3_handle + >> cookie: be_u32 + >> verifier: take!(8) + >> dircount: be_u32 + >> maxcount: be_u32 + >> ( + Nfs3RequestReaddirplus { + handle:handle, + cookie:cookie, + verifier:verifier, + dircount:dircount, + maxcount:maxcount, + } + )) +); + +#[derive(Debug,PartialEq)] +pub struct Nfs3RequestWrite<'a> { + pub handle: Nfs3Handle<'a>, + + pub offset: u64, + pub count: u32, + pub stable: u32, + pub file_len: u32, + pub file_data: &'a[u8], +} + +named!(pub parse_nfs3_request_write, + do_parse!( + handle: parse_nfs3_handle + >> offset: be_u64 + >> count: be_u32 + >> stable: be_u32 + >> file_len: be_u32 + >> file_data: rest // likely partial + >> ( + Nfs3RequestWrite { + handle:handle, + offset:offset, + count:count, + stable:stable, + file_len:file_len, + file_data:file_data, + } + )) +); + +#[derive(Debug,PartialEq)] +pub struct Nfs3ReplyRead<'a> { + pub status: u32, + pub attr_follows: u32, + pub attr_blob: &'a[u8], + pub count: u32, + pub eof: bool, + pub data_len: u32, + pub data: &'a[u8], // likely partial +} + +named!(pub parse_nfs3_reply_read, + do_parse!( + status: be_u32 + >> attr_follows: be_u32 + >> attr_blob: take!(84) // fixed size? + >> count: be_u32 + >> eof: be_u32 + >> data_len: be_u32 + >> data_contents: rest + >> ( + Nfs3ReplyRead { + status:status, + attr_follows:attr_follows, + attr_blob:attr_blob, + count:count, + eof:eof != 0, + data_len:data_len, + data:data_contents, + } + )) +); + +#[derive(Debug,PartialEq)] +pub struct RpcPacketHeader<> { + pub frag_is_last: bool, + pub frag_len: u32, + pub xid: u32, + pub msgtype: u32, +} + +named!(pub parse_rpc_packet_header, + do_parse!( + fraghdr: bits!(tuple!( + take_bits!(u8, 1), // is_last + take_bits!(u32, 31))) // len + + >> xid: be_u32 + >> msgtype: be_u32 + >> ( + RpcPacketHeader { + frag_is_last:fraghdr.0 == 1, + frag_len:fraghdr.1, + xid:xid, + msgtype:msgtype, + } + )) +); + +#[derive(Debug,PartialEq)] +pub struct RpcReplyPacket<'a> { + pub hdr: RpcPacketHeader<>, + + pub verifier_flavor: u32, + pub verifier_len: u32, + pub verifier: Option<&'a[u8]>, + + pub reply_state: u32, + pub accept_state: u32, + + pub prog_data: &'a[u8], +} + +// top of request packet, just to get to procedure +#[derive(Debug)] +pub struct RpcRequestPacketPartial { + pub hdr: RpcPacketHeader, + + pub rpcver: u32, + pub program: u32, + pub progver: u32, + pub procedure: u32, +} + +named!(pub parse_rpc_request_partial, + do_parse!( + hdr: parse_rpc_packet_header + >> rpcver: be_u32 + >> program: be_u32 + >> progver: be_u32 + >> procedure: be_u32 + >> ( + RpcRequestPacketPartial { + hdr:hdr, + rpcver:rpcver, + program:program, + progver:progver, + procedure:procedure, + } + )) +); + +#[derive(Debug,PartialEq)] +pub struct RpcPacket<'a> { + pub hdr: RpcPacketHeader<>, + + pub rpcver: u32, + pub program: u32, + pub progver: u32, + pub procedure: u32, + + pub creds_flavor: u32, + pub creds_len: u32, + pub creds: Option<&'a[u8]>, + pub creds_unix:Option>, + + pub verifier_flavor: u32, + pub verifier_len: u32, + pub verifier: Option<&'a[u8]>, + + pub prog_data: &'a[u8], +} + +named!(pub parse_rpc, + do_parse!( + hdr: parse_rpc_packet_header + + >> rpcver: be_u32 + >> program: be_u32 + >> progver: be_u32 + >> procedure: be_u32 + + >> creds_flavor: be_u32 + >> creds_len: be_u32 + >> creds: cond!(creds_flavor != 1 && creds_len > 0, take!(creds_len as usize)) + >> creds_unix: cond!(creds_len > 0 && creds_flavor == 1, flat_map!(take!((creds_len) as usize),parse_rfc_request_creds_unix)) + + >> verifier_flavor: be_u32 + >> verifier_len: be_u32 + >> verifier: cond!(verifier_len > 0, take!(verifier_len as usize)) + + >> pl: rest + + >> ( + RpcPacket { + hdr:hdr, + + rpcver:rpcver, + program:program, + progver:progver, + procedure:procedure, + + creds_flavor:creds_flavor, + creds_len:creds_len, + creds:creds, + creds_unix:creds_unix, + + verifier_flavor:verifier_flavor, + verifier_len:verifier_len, + verifier:verifier, + + prog_data:pl, + } + )) +); + +// to be called with data <= hdr.frag_len + 4. Sending more data is undefined. +named!(pub parse_rpc_reply, + do_parse!( + hdr: parse_rpc_packet_header + + >> verifier_flavor: be_u32 + >> verifier_len: be_u32 + >> verifier: cond!(verifier_len > 0, take!(verifier_len as usize)) + + >> reply_state: be_u32 + >> accept_state: be_u32 + + >> pl: rest + + >> ( + RpcReplyPacket { + hdr:hdr, + + verifier_flavor:verifier_flavor, + verifier_len:verifier_len, + verifier:verifier, + + reply_state:reply_state, + accept_state:accept_state, + + prog_data:pl, + } + )) +); diff --git a/rust/src/nfs/types.rs b/rust/src/nfs/types.rs new file mode 100644 index 0000000000..9e8c00ec58 --- /dev/null +++ b/rust/src/nfs/types.rs @@ -0,0 +1,138 @@ +/* Copyright (C) 2017 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. + */ + +/* RFC 1813, section '3. Server Procedures' */ +pub const NFSPROC3_NULL: u32 = 0; +pub const NFSPROC3_GETATTR: u32 = 1; +pub const NFSPROC3_SETATTR: u32 = 2; +pub const NFSPROC3_LOOKUP: u32 = 3; +pub const NFSPROC3_ACCESS: u32 = 4; +pub const NFSPROC3_READLINK: u32 = 5; +pub const NFSPROC3_READ: u32 = 6; +pub const NFSPROC3_WRITE: u32 = 7; +pub const NFSPROC3_CREATE: u32 = 8; +pub const NFSPROC3_MKDIR: u32 = 9; +pub const NFSPROC3_SYMLINK: u32 = 10; +pub const NFSPROC3_MKNOD: u32 = 11; +pub const NFSPROC3_REMOVE: u32 = 12; +pub const NFSPROC3_RMDIR: u32 = 13; +pub const NFSPROC3_RENAME: u32 = 14; +pub const NFSPROC3_LINK: u32 = 15; +pub const NFSPROC3_READDIR: u32 = 16; +pub const NFSPROC3_READDIRPLUS: u32 = 17; +pub const NFSPROC3_FSSTAT: u32 = 18; +pub const NFSPROC3_FSINFO: u32 = 19; +pub const NFSPROC3_PATHCONF: u32 = 20; +pub const NFSPROC3_COMMIT: u32 = 21; + +pub fn nfs3_procedure_string(procedure: u32) -> String { + match procedure { + NFSPROC3_NULL => "NULL", + NFSPROC3_GETATTR => "GETATTR", + NFSPROC3_SETATTR => "SETATTR", + NFSPROC3_LOOKUP => "LOOKUP", + NFSPROC3_ACCESS => "ACCESS", + NFSPROC3_READLINK => "READLINK", + NFSPROC3_READ => "READ", + NFSPROC3_WRITE => "WRITE", + NFSPROC3_CREATE => "CREATE", + NFSPROC3_MKDIR => "MKDIR", + NFSPROC3_SYMLINK => "SYMLINK", + NFSPROC3_MKNOD => "MKNOD", + NFSPROC3_REMOVE => "REMOVE", + NFSPROC3_RMDIR => "RMDIR", + NFSPROC3_RENAME => "RENAME", + NFSPROC3_LINK => "LINK", + NFSPROC3_READDIR => "READDIR", + NFSPROC3_READDIRPLUS => "READDIRPLUS", + NFSPROC3_FSSTAT => "FSSTAT", + NFSPROC3_FSINFO => "FSINFO", + NFSPROC3_PATHCONF => "PATHCONF", + NFSPROC3_COMMIT => "COMMIT", + _ => { + return (procedure).to_string(); + } + }.to_string() +} + +/* RFC 1813, section '2.6 Defined Error Numbers' */ +pub const NFS3_OK: u32 = 0; +pub const NFS3ERR_PERM: u32 = 1; +pub const NFS3ERR_NOENT: u32 = 2; +pub const NFS3ERR_IO: u32 = 5; +pub const NFS3ERR_NXIO: u32 = 6; +pub const NFS3ERR_ACCES: u32 = 13; +pub const NFS3ERR_EXIST: u32 = 17; +pub const NFS3ERR_XDEV: u32 = 18; +pub const NFS3ERR_NODEV: u32 = 19; +pub const NFS3ERR_NOTDIR: u32 = 20; +pub const NFS3ERR_ISDIR: u32 = 21; +pub const NFS3ERR_INVAL: u32 = 22; +pub const NFS3ERR_FBIG: u32 = 27; +pub const NFS3ERR_NOSPC: u32 = 28; +pub const NFS3ERR_ROFS: u32 = 30; +pub const NFS3ERR_MLINK: u32 = 31; +pub const NFS3ERR_NAMETOOLONG: u32 = 63; +pub const NFS3ERR_NOTEMPTY: u32 = 66; +pub const NFS3ERR_DQUOT: u32 = 69; +pub const NFS3ERR_STALE: u32 = 70; +pub const NFS3ERR_REMOTE: u32 = 71; +pub const NFS3ERR_BADHANDLE: u32 = 10001; +pub const NFS3ERR_NOT_SYNC: u32 = 10002; +pub const NFS3ERR_BAD_COOKIE: u32 = 10003; +pub const NFS3ERR_NOTSUPP: u32 = 10004; +pub const NFS3ERR_TOOSMALL: u32 = 10005; +pub const NFS3ERR_SERVERFAULT: u32 = 10006; +pub const NFS3ERR_BADTYPE: u32 = 10007; +pub const NFS3ERR_JUKEBOX: u32 = 10008; + +pub fn nfs3_status_string(status: u32) -> String { + match status { + NFS3_OK => "OK", + NFS3ERR_PERM => "ERR_PERM", + NFS3ERR_NOENT => "ERR_NOENT", + NFS3ERR_IO => "ERR_IO", + NFS3ERR_NXIO => "ERR_NXIO", + NFS3ERR_ACCES => "ERR_ACCES", + NFS3ERR_EXIST => "ERR_EXIST", + NFS3ERR_XDEV => "ERR_XDEV", + NFS3ERR_NODEV => "ERR_NODEV", + NFS3ERR_NOTDIR => "ERR_NOTDIR", + NFS3ERR_ISDIR => "ERR_ISDIR", + NFS3ERR_INVAL => "ERR_INVAL", + NFS3ERR_FBIG => "ERR_FBIG", + NFS3ERR_NOSPC => "ERR_NOSPC", + NFS3ERR_ROFS => "ERR_ROFS", + NFS3ERR_MLINK => "ERR_MLINK", + NFS3ERR_NAMETOOLONG => "ERR_NAMETOOLONG", + NFS3ERR_NOTEMPTY => "ERR_NOTEMPTY", + NFS3ERR_DQUOT => "ERR_DQUOT", + NFS3ERR_STALE => "ERR_STALE", + NFS3ERR_REMOTE => "ERR_REMOTE", + NFS3ERR_BADHANDLE => "ERR_BADHANDLE", + NFS3ERR_NOT_SYNC => "ERR_NOT_SYNC", + NFS3ERR_BAD_COOKIE => "ERR_BAD_COOKIE", + NFS3ERR_NOTSUPP => "ERR_NOTSUPP", + NFS3ERR_TOOSMALL => "ERR_TOOSMALL", + NFS3ERR_SERVERFAULT => "ERR_SERVERFAULT", + NFS3ERR_BADTYPE => "ERR_BADTYPE", + NFS3ERR_JUKEBOX => "ERR_JUKEBOX", + _ => { + return (status).to_string(); + }, + }.to_string() +} diff --git a/src/Makefile.am b/src/Makefile.am index a8bc12607f..b1d789f2e5 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -39,6 +39,7 @@ app-layer-protos.c app-layer-protos.h \ app-layer-smb2.c app-layer-smb2.h \ app-layer-smb.c app-layer-smb.h \ app-layer-smtp.c app-layer-smtp.h \ +app-layer-nfs3.c app-layer-nfs3.h \ app-layer-template.c app-layer-template.h \ app-layer-ssh.c app-layer-ssh.h \ app-layer-ssl.c app-layer-ssl.h \ @@ -207,6 +208,7 @@ detect-lua-extensions.c detect-lua-extensions.h \ detect-mark.c detect-mark.h \ detect-metadata.c detect-metadata.h \ detect-msg.c detect-msg.h \ +detect-nfs3-procedure.c detect-nfs3-procedure.h \ detect-noalert.c detect-noalert.h \ detect-nocase.c detect-nocase.h \ detect-offset.c detect-offset.h \ @@ -296,6 +298,7 @@ output-json-smtp.c output-json-smtp.h \ output-json-ssh.c output-json-ssh.h \ output-json-stats.c output-json-stats.h \ output-json-tls.c output-json-tls.h \ +output-json-nfs3.c output-json-nfs3.h \ output-json-template.c output-json-template.h \ output-json-vars.c output-json-vars.h \ output-lua.c output-lua.h \ diff --git a/src/app-layer-detect-proto.c b/src/app-layer-detect-proto.c index 8eb92639f7..05c11f368f 100644 --- a/src/app-layer-detect-proto.c +++ b/src/app-layer-detect-proto.c @@ -696,6 +696,8 @@ static void AppLayerProtoDetectPrintProbingParsers(AppLayerProtoDetectProbingPar printf(" alproto: ALPROTO_MODBUS\n"); else if (pp_pe->alproto == ALPROTO_ENIP) printf(" alproto: ALPROTO_ENIP\n"); + else if (pp_pe->alproto == ALPROTO_NFS3) + printf(" alproto: ALPROTO_NFS3\n"); else if (pp_pe->alproto == ALPROTO_TEMPLATE) printf(" alproto: ALPROTO_TEMPLATE\n"); else if (pp_pe->alproto == ALPROTO_DNP3) @@ -753,6 +755,8 @@ static void AppLayerProtoDetectPrintProbingParsers(AppLayerProtoDetectProbingPar printf(" alproto: ALPROTO_MODBUS\n"); else if (pp_pe->alproto == ALPROTO_ENIP) printf(" alproto: ALPROTO_ENIP\n"); + else if (pp_pe->alproto == ALPROTO_NFS3) + printf(" alproto: ALPROTO_NFS3\n"); else if (pp_pe->alproto == ALPROTO_TEMPLATE) printf(" alproto: ALPROTO_TEMPLATE\n"); else if (pp_pe->alproto == ALPROTO_DNP3) diff --git a/src/app-layer-nfs3.c b/src/app-layer-nfs3.c new file mode 100644 index 0000000000..7cdc31485e --- /dev/null +++ b/src/app-layer-nfs3.c @@ -0,0 +1,369 @@ +/* Copyright (C) 2015 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. + */ + +/** + * \file + * + * \author Victor Julien + * + * NFS3 application layer detector and parser for learning and + * nfs3 pruposes. + * + * This nfs3 implements a simple application layer for something + * like the NFS3 protocol running on port 2049. + */ + +#include "suricata-common.h" +#include "stream.h" +#include "conf.h" + +#include "util-unittest.h" + +#include "app-layer-detect-proto.h" +#include "app-layer-parser.h" + +#include "app-layer-nfs3.h" + +#ifndef HAVE_RUST +void RegisterNFS3Parsers(void) +{ +} + +#else + +#include "rust.h" +#include "rust-nfs-nfs3-gen.h" + +/* The default port to probe for echo traffic if not provided in the + * configuration file. */ +#define NFS3_DEFAULT_PORT "2049" + +/* The minimum size for a RFC message. For some protocols this might + * be the size of a header. TODO actual min size is likely larger */ +#define NFS3_MIN_FRAME_LEN 32 + +/* Enum of app-layer events for an echo protocol. Normally you might + * have events for errors in parsing data, like unexpected data being + * received. For echo we'll make something up, and log an app-layer + * level alert if an empty message is received. + * + * Example rule: + * + * alert nfs3 any any -> any any (msg:"SURICATA NFS3 empty message"; \ + * app-layer-event:nfs3.empty_message; sid:X; rev:Y;) + */ +enum { + NFS3_DECODER_EVENT_EMPTY_MESSAGE, +}; + +SCEnumCharMap nfs3_decoder_event_table[] = { + {"EMPTY_MESSAGE", NFS3_DECODER_EVENT_EMPTY_MESSAGE}, + { NULL, 0 } +}; + +static void *NFS3StateAlloc(void) +{ + return rs_nfs3_state_new(); +} + +static void NFS3StateFree(void *state) +{ + rs_nfs3_state_free(state); +} + +/** + * \brief Callback from the application layer to have a transaction freed. + * + * \param state a void pointer to the NFS3State object. + * \param tx_id the transaction ID to free. + */ +static void NFS3StateTxFree(void *state, uint64_t tx_id) +{ + rs_nfs3_state_tx_free(state, tx_id); +} + +#if 0 +static int NFS3StateGetEventInfo(const char *event_name, int *event_id, + AppLayerEventType *event_type) +{ + *event_id = SCMapEnumNameToValue(event_name, nfs3_decoder_event_table); + if (*event_id == -1) { + SCLogError(SC_ERR_INVALID_ENUM_MAP, "event \"%s\" not present in " + "nfs3 enum map table.", event_name); + /* This should be treated as fatal. */ + return -1; + } + + *event_type = APP_LAYER_EVENT_TYPE_TRANSACTION; + + return 0; +} + +static AppLayerDecoderEvents *NFS3GetEvents(void *state, uint64_t tx_id) +{ + NFS3State *nfs3_state = state; + NFS3Transaction *tx; + + TAILQ_FOREACH(tx, &nfs3_state->tx_list, next) { + if (tx->tx_id == tx_id) { + return tx->decoder_events; + } + } + + return NULL; +} + +static int NFS3HasEvents(void *state) +{ + NFS3State *echo = state; + return echo->events; +} +#endif + +/** + * \brief Probe the input to see if it looks like echo. + * + * \retval ALPROTO_NFS3 if it looks like echo, otherwise + * ALPROTO_UNKNOWN. + */ +static AppProto NFS3ProbingParser(uint8_t *input, uint32_t input_len, + uint32_t *offset) +{ + if (input_len < NFS3_MIN_FRAME_LEN) { + return ALPROTO_UNKNOWN; + } + + int8_t r = rs_nfs_probe(input, input_len); + if (r == 1) { + return ALPROTO_NFS3; + } else if (r == -1) { + return ALPROTO_FAILED; + } + + SCLogDebug("Protocol not detected as ALPROTO_NFS3."); + return ALPROTO_UNKNOWN; +} + +static int NFS3ParseRequest(Flow *f, void *state, + AppLayerParserState *pstate, uint8_t *input, uint32_t input_len, + void *local_data) +{ + uint16_t file_flags = FileFlowToFlags(f, STREAM_TOSERVER); + rs_nfs3_setfileflags(0, state, file_flags); + + return rs_nfs3_parse_request(f, state, pstate, input, input_len, local_data); +} + +static int NFS3ParseResponse(Flow *f, void *state, AppLayerParserState *pstate, + uint8_t *input, uint32_t input_len, void *local_data) +{ + uint16_t file_flags = FileFlowToFlags(f, STREAM_TOCLIENT); + rs_nfs3_setfileflags(1, state, file_flags); + + return rs_nfs3_parse_response(f, state, pstate, input, input_len, local_data); +} + +static uint64_t NFS3GetTxCnt(void *state) +{ + return rs_nfs3_state_get_tx_count(state); +} + +static void *NFS3GetTx(void *state, uint64_t tx_id) +{ + return rs_nfs3_state_get_tx(state, tx_id); +} + +static void NFS3SetTxLogged(void *state, void *vtx, uint32_t logger) +{ + rs_nfs3_tx_set_logged(state, vtx, logger); +} + +static int NFS3GetTxLogged(void *state, void *vtx, uint32_t logger) +{ + return rs_nfs3_tx_get_logged(state, vtx, logger); +} + +/** + * \brief Called by the application layer. + * + * In most cases 1 can be returned here. + */ +static int NFS3GetAlstateProgressCompletionStatus(uint8_t direction) { + return rs_nfs3_state_progress_completion_status(direction); +} + +/** + * \brief Return the state of a transaction in a given direction. + * + * In the case of the echo protocol, the existence of a transaction + * means that the request is done. However, some protocols that may + * need multiple chunks of data to complete the request may need more + * than just the existence of a transaction for the request to be + * considered complete. + * + * For the response to be considered done, the response for a request + * needs to be seen. The response_done flag is set on response for + * checking here. + */ +static int NFS3GetStateProgress(void *tx, uint8_t direction) +{ + return rs_nfs3_tx_get_alstate_progress(tx, direction); +} + +/** + * \brief get stored tx detect state + */ +static DetectEngineState *NFS3GetTxDetectState(void *vtx) +{ + return rs_nfs3_state_get_tx_detect_state(vtx); +} + +/** + * \brief set store tx detect state + */ +static int NFS3SetTxDetectState(void *state, void *vtx, + DetectEngineState *s) +{ + rs_nfs3_state_set_tx_detect_state(state, vtx, s); + return 0; +} + +static FileContainer *NFS3GetFiles(void *state, uint8_t direction) +{ + return rs_nfs3_getfiles(direction, state); +} + +static StreamingBufferConfig sbcfg = STREAMING_BUFFER_CONFIG_INITIALIZER; +static SuricataFileContext sfc = { &sbcfg }; + +void RegisterNFS3Parsers(void) +{ + const char *proto_name = "nfs3"; + + /* Check if NFS3 TCP detection is enabled. If it does not exist in + * the configuration file then it will be enabled by default. */ + if (AppLayerProtoDetectConfProtoDetectionEnabled("tcp", proto_name)) { + + rs_nfs3_init(&sfc); + + SCLogDebug("NFS3 TCP protocol detection enabled."); + + AppLayerProtoDetectRegisterProtocol(ALPROTO_NFS3, proto_name); + + if (RunmodeIsUnittests()) { + + SCLogDebug("Unittest mode, registering default configuration."); + AppLayerProtoDetectPPRegister(IPPROTO_TCP, NFS3_DEFAULT_PORT, + ALPROTO_NFS3, 0, NFS3_MIN_FRAME_LEN, STREAM_TOSERVER, + NFS3ProbingParser, NULL); + + } + else { + + if (!AppLayerProtoDetectPPParseConfPorts("tcp", IPPROTO_TCP, + proto_name, ALPROTO_NFS3, 0, NFS3_MIN_FRAME_LEN, + NFS3ProbingParser, NULL)) { + SCLogDebug("No NFS3 app-layer configuration, enabling NFS3" + " detection TCP detection on port %s.", + NFS3_DEFAULT_PORT); + AppLayerProtoDetectPPRegister(IPPROTO_TCP, + NFS3_DEFAULT_PORT, ALPROTO_NFS3, 0, + NFS3_MIN_FRAME_LEN, STREAM_TOSERVER, + NFS3ProbingParser, NULL); + } + + } + + } + + else { + SCLogDebug("Protocol detecter and parser disabled for NFS3."); + return; + } + + if (AppLayerParserConfParserEnabled("tcp", proto_name)) + { + SCLogDebug("Registering NFS3 protocol parser."); + + /* Register functions for state allocation and freeing. A + * state is allocated for every new NFS3 flow. */ + AppLayerParserRegisterStateFuncs(IPPROTO_TCP, ALPROTO_NFS3, + NFS3StateAlloc, NFS3StateFree); + + /* Register request parser for parsing frame from server to client. */ + AppLayerParserRegisterParser(IPPROTO_TCP, ALPROTO_NFS3, + STREAM_TOSERVER, NFS3ParseRequest); + + /* Register response parser for parsing frames from server to client. */ + AppLayerParserRegisterParser(IPPROTO_TCP, ALPROTO_NFS3, + STREAM_TOCLIENT, NFS3ParseResponse); + + /* Register a function to be called by the application layer + * when a transaction is to be freed. */ + AppLayerParserRegisterTxFreeFunc(IPPROTO_TCP, ALPROTO_NFS3, + NFS3StateTxFree); + + AppLayerParserRegisterLoggerFuncs(IPPROTO_TCP, ALPROTO_NFS3, + NFS3GetTxLogged, NFS3SetTxLogged); + + /* Register a function to return the current transaction count. */ + AppLayerParserRegisterGetTxCnt(IPPROTO_TCP, ALPROTO_NFS3, + NFS3GetTxCnt); + + /* Transaction handling. */ + AppLayerParserRegisterGetStateProgressCompletionStatus(ALPROTO_NFS3, + NFS3GetAlstateProgressCompletionStatus); + AppLayerParserRegisterGetStateProgressFunc(IPPROTO_TCP, + ALPROTO_NFS3, NFS3GetStateProgress); + AppLayerParserRegisterGetTx(IPPROTO_TCP, ALPROTO_NFS3, + NFS3GetTx); + + AppLayerParserRegisterGetFilesFunc(IPPROTO_TCP, ALPROTO_NFS3, NFS3GetFiles); + + /* Application layer event handling. */ +// AppLayerParserRegisterHasEventsFunc(IPPROTO_TCP, ALPROTO_NFS3, +// NFS3HasEvents); + + /* What is this being registered for? */ + AppLayerParserRegisterDetectStateFuncs(IPPROTO_TCP, ALPROTO_NFS3, + NULL, NFS3GetTxDetectState, NFS3SetTxDetectState); + +// AppLayerParserRegisterGetEventInfo(IPPROTO_TCP, ALPROTO_NFS3, +// NFS3StateGetEventInfo); +// AppLayerParserRegisterGetEventsFunc(IPPROTO_TCP, ALPROTO_NFS3, +// NFS3GetEvents); + } + else { + SCLogDebug("NFS3 protocol parsing disabled."); + } + +#ifdef UNITTESTS + AppLayerParserRegisterProtocolUnittests(IPPROTO_TCP, ALPROTO_NFS3, + NFS3ParserRegisterTests); +#endif +} + +#ifdef UNITTESTS +#endif + +void NFS3ParserRegisterTests(void) +{ +#ifdef UNITTESTS +#endif +} + +#endif /* HAVE_RUST */ diff --git a/src/app-layer-nfs3.h b/src/app-layer-nfs3.h new file mode 100644 index 0000000000..9386c47a73 --- /dev/null +++ b/src/app-layer-nfs3.h @@ -0,0 +1,34 @@ +/* Copyright (C) 2017 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. + */ + +/** + * \file + * + * \author Victor Julien + */ + +#ifndef __APP_LAYER_NFS3_H__ +#define __APP_LAYER_NFS3_H__ + +void RegisterNFS3Parsers(void); +void NFS3ParserRegisterTests(void); + +/** Opaque Rust types. */ +typedef struct NFS3tate_ NFS3State; +typedef struct NFS3Transaction_ NFS3Transaction; + +#endif /* __APP_LAYER_NFS3_H__ */ diff --git a/src/app-layer-parser.c b/src/app-layer-parser.c index a7840b5a3f..54cb5edde4 100644 --- a/src/app-layer-parser.c +++ b/src/app-layer-parser.c @@ -60,6 +60,7 @@ #include "app-layer-modbus.h" #include "app-layer-enip.h" #include "app-layer-dnp3.h" +#include "app-layer-nfs3.h" #include "app-layer-template.h" #include "conf.h" @@ -1282,6 +1283,7 @@ void AppLayerParserRegisterProtocolParsers(void) RegisterENIPUDPParsers(); RegisterENIPTCPParsers(); RegisterDNP3Parsers(); + RegisterNFS3Parsers(); RegisterTemplateParsers(); /** IMAP */ diff --git a/src/app-layer-protos.c b/src/app-layer-protos.c index 9830af5b2c..fb500d8f7a 100644 --- a/src/app-layer-protos.c +++ b/src/app-layer-protos.c @@ -81,6 +81,9 @@ const char *AppProtoToString(AppProto alproto) case ALPROTO_DNP3: proto_name = "dnp3"; break; + case ALPROTO_NFS3: + proto_name = "nfs3"; + break; case ALPROTO_TEMPLATE: proto_name = "template"; break; diff --git a/src/app-layer-protos.h b/src/app-layer-protos.h index bcecc79577..45a4cf4aa1 100644 --- a/src/app-layer-protos.h +++ b/src/app-layer-protos.h @@ -44,6 +44,7 @@ enum AppProtoEnum { ALPROTO_MODBUS, ALPROTO_ENIP, ALPROTO_DNP3, + ALPROTO_NFS3, ALPROTO_TEMPLATE, /* used by the probing parser when alproto detection fails diff --git a/src/detect-filename.c b/src/detect-filename.c index 273328dec7..180f2f3169 100644 --- a/src/detect-filename.c +++ b/src/detect-filename.c @@ -84,6 +84,13 @@ void DetectFilenameRegister(void) ALPROTO_SMTP, SIG_FLAG_TOSERVER, 0, DetectFileInspectGeneric); + DetectAppLayerInspectEngineRegister("files", + ALPROTO_NFS3, SIG_FLAG_TOSERVER, 0, + DetectFileInspectGeneric); + DetectAppLayerInspectEngineRegister("files", + ALPROTO_NFS3, SIG_FLAG_TOCLIENT, 0, + DetectFileInspectGeneric); + g_file_match_list_id = DetectBufferTypeGetByName("files"); SCLogDebug("registering filename rule option"); diff --git a/src/detect-nfs3-procedure.c b/src/detect-nfs3-procedure.c new file mode 100644 index 0000000000..1812c163a9 --- /dev/null +++ b/src/detect-nfs3-procedure.c @@ -0,0 +1,641 @@ +/* Copyright (C) 2017 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. + */ + +/** + * \file + * + * \author Victor Julien + */ + +#include "suricata-common.h" +#include "threads.h" +#include "debug.h" +#include "decode.h" +#include "detect.h" + +#include "detect-parse.h" +#include "detect-engine.h" +#include "detect-engine-mpm.h" +#include "detect-content.h" +#include "detect-pcre.h" +#include "detect-nfs3-procedure.h" + +#include "flow.h" +#include "flow-util.h" +#include "flow-var.h" + +#include "util-unittest.h" +#include "util-unittest-helper.h" + +#ifndef HAVE_RUST +void DetectNfs3ProcedureRegister(void) +{ +} + +#else + +#include "app-layer-nfs3.h" +#include "rust.h" +#include "rust-nfs-nfs3-gen.h" + +/** + * [nfs3_procedure]:[<|>][<>]; + */ +#define PARSE_REGEX "^\\s*(<=|>=|<|>)?\\s*([0-9]+)\\s*(?:(<>)\\s*([0-9]+))?\\s*$" +static pcre *parse_regex; +static pcre_extra *parse_regex_study; + +enum DetectNfs3ProcedureMode { + PROCEDURE_EQ = 1, /* equal */ + PROCEDURE_LT, /* less than */ + PROCEDURE_LE, /* less than */ + PROCEDURE_GT, /* greater than */ + PROCEDURE_GE, /* greater than */ + PROCEDURE_RA, /* range */ +}; + +typedef struct DetectNfs3ProcedureData_ { + uint32_t lo; + uint32_t hi; + enum DetectNfs3ProcedureMode mode; +} DetectNfs3ProcedureData; + +static DetectNfs3ProcedureData *DetectNfs3ProcedureParse (const char *); +static int DetectNfs3ProcedureSetup (DetectEngineCtx *, Signature *s, const char *str); +static void DetectNfs3ProcedureFree(void *); +static void DetectNfs3ProcedureRegisterTests(void); +static int g_nfs3_request_buffer_id = 0; + +static int DetectEngineInspectNfs3RequestGeneric(ThreadVars *tv, + DetectEngineCtx *de_ctx, DetectEngineThreadCtx *det_ctx, + const Signature *s, const SigMatchData *smd, + Flow *f, uint8_t flags, void *alstate, + void *txv, uint64_t tx_id); + +static int DetectNfs3ProcedureMatch (ThreadVars *, DetectEngineThreadCtx *, Flow *, + uint8_t, void *, void *, const Signature *, + const SigMatchCtx *); + +/** + * \brief Registration function for nfs3_procedure keyword. + */ +void DetectNfs3ProcedureRegister (void) +{ + sigmatch_table[DETECT_AL_NFS3_PROCEDURE].name = "nfs3_procedure"; + sigmatch_table[DETECT_AL_NFS3_PROCEDURE].desc = "match NFSv3 procedure"; + sigmatch_table[DETECT_AL_NFS3_PROCEDURE].url = DOC_URL DOC_VERSION "/rules/nfs3-keywords.html#procedure"; + sigmatch_table[DETECT_AL_NFS3_PROCEDURE].Match = NULL; + sigmatch_table[DETECT_AL_NFS3_PROCEDURE].AppLayerTxMatch = DetectNfs3ProcedureMatch; + sigmatch_table[DETECT_AL_NFS3_PROCEDURE].Setup = DetectNfs3ProcedureSetup; + sigmatch_table[DETECT_AL_NFS3_PROCEDURE].Free = DetectNfs3ProcedureFree; + sigmatch_table[DETECT_AL_NFS3_PROCEDURE].RegisterTests = DetectNfs3ProcedureRegisterTests; + + + DetectSetupParseRegexes(PARSE_REGEX, &parse_regex, &parse_regex_study); + + DetectAppLayerInspectEngineRegister("nfs3_request", + ALPROTO_NFS3, SIG_FLAG_TOSERVER, 0, + DetectEngineInspectNfs3RequestGeneric); + + g_nfs3_request_buffer_id = DetectBufferTypeGetByName("nfs3_request"); + + SCLogDebug("g_nfs3_request_buffer_id %d", g_nfs3_request_buffer_id); +} + +static int DetectEngineInspectNfs3RequestGeneric(ThreadVars *tv, + DetectEngineCtx *de_ctx, DetectEngineThreadCtx *det_ctx, + const Signature *s, const SigMatchData *smd, + Flow *f, uint8_t flags, void *alstate, + void *txv, uint64_t tx_id) +{ + return DetectEngineInspectGenericList(tv, de_ctx, det_ctx, s, smd, + f, flags, alstate, txv, tx_id); +} + +static inline int +ProcedureMatch(const uint32_t procedure, + enum DetectNfs3ProcedureMode mode, uint32_t lo, uint32_t hi) +{ + switch (mode) { + case PROCEDURE_EQ: + if (procedure == lo) + SCReturnInt(1); + break; + case PROCEDURE_LT: + if (procedure < lo) + SCReturnInt(1); + break; + case PROCEDURE_LE: + if (procedure <= lo) + SCReturnInt(1); + break; + case PROCEDURE_GT: + if (procedure > lo) + SCReturnInt(1); + break; + case PROCEDURE_GE: + if (procedure >= lo) + SCReturnInt(1); + break; + case PROCEDURE_RA: + if (procedure >= lo && procedure <= hi) + SCReturnInt(1); + break; + } + SCReturnInt(0); +} + +/** + * \internal + * \brief Function to match procedure of a TX + * + * For 'file txs' + * + * \param t Pointer to thread vars. + * \param det_ctx Pointer to the pattern matcher thread. + * \param f Pointer to the current flow. + * \param flags Flags. + * \param state App layer state. + * \param s Pointer to the Signature. + * \param m Pointer to the sigmatch that we will cast into + * DetectNfs3ProcedureData. + * + * \retval 0 no match. + * \retval 1 match. + */ +static int DetectNfs3ProcedureMatch (ThreadVars *t, DetectEngineThreadCtx *det_ctx, + Flow *f, uint8_t flags, void *state, + void *txv, const Signature *s, + const SigMatchCtx *ctx) +{ + SCEnter(); + + const DetectNfs3ProcedureData *dd = (const DetectNfs3ProcedureData *)ctx; + uint16_t i; + for (i = 0; i < 256; i++) { + uint32_t procedure; + if (rs_nfs3_tx_get_procedures(txv, i, &procedure) == 1) { + SCLogDebug("proc %u mode %u lo %u hi %u", + procedure, dd->mode, dd->lo, dd->hi); + if (ProcedureMatch(procedure, dd->mode, dd->lo, dd->hi)) + SCReturnInt(1); + continue; + } + break; + } + SCReturnInt(0); +} + +/** + * \internal + * \brief Function to parse options passed via tls validity keywords. + * + * \param rawstr Pointer to the user provided options. + * + * \retval dd pointer to DetectNfs3ProcedureData on success. + * \retval NULL on failure. + */ +static DetectNfs3ProcedureData *DetectNfs3ProcedureParse (const char *rawstr) +{ + DetectNfs3ProcedureData *dd = NULL; +#define MAX_SUBSTRINGS 30 + int ret = 0, res = 0; + int ov[MAX_SUBSTRINGS]; + char mode[2] = ""; + char value1[20] = ""; + char value2[20] = ""; + char range[3] = ""; + + ret = pcre_exec(parse_regex, parse_regex_study, rawstr, strlen(rawstr), 0, + 0, ov, MAX_SUBSTRINGS); + if (ret < 3 || ret > 5) { + SCLogError(SC_ERR_PCRE_MATCH, "Parse error %s", rawstr); + goto error; + } + + res = pcre_copy_substring((char *)rawstr, ov, MAX_SUBSTRINGS, 1, mode, + sizeof(mode)); + if (res < 0) { + SCLogError(SC_ERR_PCRE_GET_SUBSTRING, "pcre_copy_substring failed"); + goto error; + } + SCLogDebug("mode \"%s\"", mode); + + res = pcre_copy_substring((char *)rawstr, ov, MAX_SUBSTRINGS, 2, value1, + sizeof(value1)); + if (res < 0) { + SCLogError(SC_ERR_PCRE_GET_SUBSTRING, "pcre_copy_substring failed"); + goto error; + } + SCLogDebug("value1 \"%s\"", value1); + + if (ret > 3) { + res = pcre_copy_substring((char *)rawstr, ov, MAX_SUBSTRINGS, 3, + range, sizeof(range)); + if (res < 0) { + SCLogError(SC_ERR_PCRE_GET_SUBSTRING, "pcre_copy_substring failed"); + goto error; + } + SCLogDebug("range \"%s\"", range); + + if (ret > 4) { + res = pcre_copy_substring((char *)rawstr, ov, MAX_SUBSTRINGS, 4, + value2, sizeof(value2)); + if (res < 0) { + SCLogError(SC_ERR_PCRE_GET_SUBSTRING, + "pcre_copy_substring failed"); + goto error; + } + SCLogDebug("value2 \"%s\"", value2); + } + } + + dd = SCCalloc(1, sizeof(DetectNfs3ProcedureData)); + if (unlikely(dd == NULL)) + goto error; + + if (strlen(mode) == 1) { + if (mode[0] == '<') + dd->mode = PROCEDURE_LT; + else if (mode[0] == '>') + dd->mode = PROCEDURE_GT; + } else if (strlen(mode) == 2) { + if (strcmp(mode, "<=") == 0) + dd->mode = PROCEDURE_LE; + if (strcmp(mode, ">=") == 0) + dd->mode = PROCEDURE_GE; + } + + if (strlen(range) > 0) { + if (strcmp("<>", range) == 0) + dd->mode = PROCEDURE_RA; + } + + if (strlen(range) != 0 && strlen(mode) != 0) { + SCLogError(SC_ERR_INVALID_ARGUMENT, + "Range specified but mode also set"); + goto error; + } + + if (dd->mode == 0) { + dd->mode = PROCEDURE_EQ; + } + + /* set the first value */ + dd->lo = atoi(value1); //TODO + + /* set the second value if specified */ + if (strlen(value2) > 0) { + if (!(dd->mode == PROCEDURE_RA)) { + SCLogError(SC_ERR_INVALID_ARGUMENT, + "Multiple tls validity values specified but mode is not range"); + goto error; + } + + // + dd->hi = atoi(value2); // TODO + + if (dd->hi <= dd->lo) { + SCLogError(SC_ERR_INVALID_ARGUMENT, + "Second value in range must not be smaller than the first"); + goto error; + } + } + return dd; + +error: + if (dd) + SCFree(dd); + return NULL; +} + + + +/** + * \brief Function to add the parsed tls validity field into the current signature. + * + * \param de_ctx Pointer to the Detection Engine Context. + * \param s Pointer to the Current Signature. + * \param rawstr Pointer to the user provided flags options. + * \param type Defines if this is notBefore or notAfter. + * + * \retval 0 on Success. + * \retval -1 on Failure. + */ +static int DetectNfs3ProcedureSetup (DetectEngineCtx *de_ctx, Signature *s, + const char *rawstr) +{ + DetectNfs3ProcedureData *dd = NULL; + SigMatch *sm = NULL; + + SCLogDebug("\'%s\'", rawstr); + + if (DetectSignatureSetAppProto(s, ALPROTO_NFS3) != 0) + return -1; + + dd = DetectNfs3ProcedureParse(rawstr); + if (dd == NULL) { + SCLogError(SC_ERR_INVALID_ARGUMENT,"Parsing \'%s\' failed", rawstr); + goto error; + } + + /* okay so far so good, lets get this into a SigMatch + * and put it in the Signature. */ + sm = SigMatchAlloc(); + if (sm == NULL) + goto error; + + sm->type = DETECT_AL_NFS3_PROCEDURE; + sm->ctx = (void *)dd; + + s->flags |= SIG_FLAG_STATE_MATCH; + SCLogDebug("low %u hi %u", dd->lo, dd->hi); + SigMatchAppendSMToList(s, sm, g_nfs3_request_buffer_id); + return 0; + +error: + DetectNfs3ProcedureFree(dd); + return -1; +} + +/** + * \internal + * \brief Function to free memory associated with DetectNfs3ProcedureData. + * + * \param de_ptr Pointer to DetectNfs3ProcedureData. + */ +void DetectNfs3ProcedureFree(void *ptr) +{ + SCFree(ptr); +} + +#ifdef UNITTESTS + +/** + * \test This is a test for a valid value 1430000000. + * + * \retval 1 on success. + * \retval 0 on failure. + */ +static int ValidityTestParse01 (void) +{ + DetectNfs3ProcedureData *dd = NULL; + dd = DetectNfs3ProcedureParse("1430000000"); + FAIL_IF_NULL(dd); + FAIL_IF_NOT(dd->lo == 1430000000 && dd->mode == PROCEDURE_EQ); + DetectNfs3ProcedureFree(dd); + PASS; +} + +/** + * \test This is a test for a valid value >1430000000. + * + * \retval 1 on success. + * \retval 0 on failure. + */ +static int ValidityTestParse02 (void) +{ + DetectNfs3ProcedureData *dd = NULL; + dd = DetectNfs3ProcedureParse(">1430000000"); + FAIL_IF_NULL(dd); + FAIL_IF_NOT(dd->lo == 1430000000 && dd->mode == PROCEDURE_GT); + DetectNfs3ProcedureFree(dd); + PASS; +} + +/** + * \test This is a test for a valid value <1430000000. + * + * \retval 1 on success. + * \retval 0 on failure. + */ +static int ValidityTestParse03 (void) +{ + DetectNfs3ProcedureData *dd = NULL; + dd = DetectNfs3ProcedureParse("<1430000000"); + FAIL_IF_NULL(dd); + FAIL_IF_NOT(dd->lo == 1430000000 && dd->mode == PROCEDURE_LT); + DetectNfs3ProcedureFree(dd); + PASS; +} + +/** + * \test This is a test for a valid value 1430000000<>1470000000. + * + * \retval 1 on success. + * \retval 0 on failure. + */ +static int ValidityTestParse04 (void) +{ + DetectNfs3ProcedureData *dd = NULL; + dd = DetectNfs3ProcedureParse("1430000000<>1470000000"); + FAIL_IF_NULL(dd); + FAIL_IF_NOT(dd->lo == 1430000000 && dd->hi == 1470000000 && + dd->mode == PROCEDURE_RA); + DetectNfs3ProcedureFree(dd); + PASS; +} + +/** + * \test This is a test for a invalid value A. + * + * \retval 1 on success. + * \retval 0 on failure. + */ +static int ValidityTestParse05 (void) +{ + DetectNfs3ProcedureData *dd = NULL; + dd = DetectNfs3ProcedureParse("A"); + FAIL_IF_NOT_NULL(dd); + PASS; +} + +/** + * \test This is a test for a invalid value >1430000000<>1470000000. + * + * \retval 1 on success. + * \retval 0 on failure. + */ +static int ValidityTestParse06 (void) +{ + DetectNfs3ProcedureData *dd = NULL; + dd = DetectNfs3ProcedureParse(">1430000000<>1470000000"); + FAIL_IF_NOT_NULL(dd); + PASS; +} + +/** + * \test This is a test for a invalid value 1430000000<>. + * + * \retval 1 on success. + * \retval 0 on failure. + */ +static int ValidityTestParse07 (void) +{ + DetectNfs3ProcedureData *dd = NULL; + dd = DetectNfs3ProcedureParse("1430000000<>"); + FAIL_IF_NOT_NULL(dd); + PASS; +} + +/** + * \test This is a test for a invalid value <>1430000000. + * + * \retval 1 on success. + * \retval 0 on failure. + */ +static int ValidityTestParse08 (void) +{ + DetectNfs3ProcedureData *dd = NULL; + dd = DetectNfs3ProcedureParse("<>1430000000"); + FAIL_IF_NOT_NULL(dd); + PASS; +} + +/** + * \test This is a test for a invalid value "". + * + * \retval 1 on success. + * \retval 0 on failure. + */ +static int ValidityTestParse09 (void) +{ + DetectNfs3ProcedureData *dd = NULL; + dd = DetectNfs3ProcedureParse(""); + FAIL_IF_NOT_NULL(dd); + PASS; +} + +/** + * \test This is a test for a invalid value " ". + * + * \retval 1 on success. + * \retval 0 on failure. + */ +static int ValidityTestParse10 (void) +{ + DetectNfs3ProcedureData *dd = NULL; + dd = DetectNfs3ProcedureParse(" "); + FAIL_IF_NOT_NULL(dd); + PASS; +} + +/** + * \test This is a test for a invalid value 1490000000<>1430000000. + * + * \retval 1 on success. + * \retval 0 on failure. + */ +static int ValidityTestParse11 (void) +{ + DetectNfs3ProcedureData *dd = NULL; + dd = DetectNfs3ProcedureParse("1490000000<>1430000000"); + FAIL_IF_NOT_NULL(dd); + PASS; +} + +/** + * \test This is a test for a valid value 1430000000 <> 1490000000. + * + * \retval 1 on success. + * \retval 0 on failure. + */ +static int ValidityTestParse12 (void) +{ + DetectNfs3ProcedureData *dd = NULL; + dd = DetectNfs3ProcedureParse("1430000000 <> 1490000000"); + FAIL_IF_NULL(dd); + FAIL_IF_NOT(dd->lo == 1430000000 && dd->hi == 1490000000 && + dd->mode == PROCEDURE_RA); + DetectNfs3ProcedureFree(dd); + PASS; +} + +/** + * \test This is a test for a valid value > 1430000000. + * + * \retval 1 on success. + * \retval 0 on failure. + */ +static int ValidityTestParse13 (void) +{ + DetectNfs3ProcedureData *dd = NULL; + dd = DetectNfs3ProcedureParse("> 1430000000 "); + FAIL_IF_NULL(dd); + FAIL_IF_NOT(dd->lo == 1430000000 && dd->mode == PROCEDURE_GT); + DetectNfs3ProcedureFree(dd); + PASS; +} + +/** + * \test This is a test for a valid value < 1490000000. + * + * \retval 1 on success. + * \retval 0 on failure. + */ +static int ValidityTestParse14 (void) +{ + DetectNfs3ProcedureData *dd = NULL; + dd = DetectNfs3ProcedureParse("< 1490000000 "); + FAIL_IF_NULL(dd); + FAIL_IF_NOT(dd->lo == 1490000000 && dd->mode == PROCEDURE_LT); + DetectNfs3ProcedureFree(dd); + PASS; +} + +/** + * \test This is a test for a valid value 1490000000. + * + * \retval 1 on success. + * \retval 0 on failure. + */ +static int ValidityTestParse15 (void) +{ + DetectNfs3ProcedureData *dd = NULL; + dd = DetectNfs3ProcedureParse(" 1490000000 "); + FAIL_IF_NULL(dd); + FAIL_IF_NOT(dd->lo == 1490000000 && dd->mode == PROCEDURE_EQ); + DetectNfs3ProcedureFree(dd); + PASS; +} + +#endif /* UNITTESTS */ + +/** + * \brief Register unit tests for nfs3_procedure. + */ +void DetectNfs3ProcedureRegisterTests(void) +{ +#ifdef UNITTESTS /* UNITTESTS */ + UtRegisterTest("ValidityTestParse01", ValidityTestParse01); + UtRegisterTest("ValidityTestParse02", ValidityTestParse02); + UtRegisterTest("ValidityTestParse03", ValidityTestParse03); + UtRegisterTest("ValidityTestParse04", ValidityTestParse04); + UtRegisterTest("ValidityTestParse05", ValidityTestParse05); + UtRegisterTest("ValidityTestParse06", ValidityTestParse06); + UtRegisterTest("ValidityTestParse07", ValidityTestParse07); + UtRegisterTest("ValidityTestParse08", ValidityTestParse08); + UtRegisterTest("ValidityTestParse09", ValidityTestParse09); + UtRegisterTest("ValidityTestParse10", ValidityTestParse10); + UtRegisterTest("ValidityTestParse11", ValidityTestParse11); + UtRegisterTest("ValidityTestParse12", ValidityTestParse12); + UtRegisterTest("ValidityTestParse13", ValidityTestParse13); + UtRegisterTest("ValidityTestParse14", ValidityTestParse14); + UtRegisterTest("ValidityTestParse15", ValidityTestParse15); +#endif /* UNITTESTS */ +} +#endif /* HAVE_RUST */ diff --git a/src/detect-nfs3-procedure.h b/src/detect-nfs3-procedure.h new file mode 100644 index 0000000000..61889d9e21 --- /dev/null +++ b/src/detect-nfs3-procedure.h @@ -0,0 +1,30 @@ +/* Copyright (C) 2017 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. + */ + +/** + * \file + * + * \author Victor Julien + */ + +#ifndef __DETECT_NFS3_PROCEDURE_H__ +#define __DETECT_NFS3_PROCEDURE_H__ + +/* prototypes */ +void DetectNfs3ProcedureRegister (void); + +#endif /* __DETECT_NFS3_PROCEDURE_H__ */ diff --git a/src/detect.c b/src/detect.c index be63e61b36..de83999ce1 100644 --- a/src/detect.c +++ b/src/detect.c @@ -63,6 +63,8 @@ #include "detect-http-hh.h" #include "detect-http-hrh.h" +#include "detect-nfs3-procedure.h" + #include "detect-engine-event.h" #include "decode.h" @@ -3909,6 +3911,7 @@ void SigTableSetup(void) DetectTlsRegister(); DetectTlsValidityRegister(); DetectTlsVersionRegister(); + DetectNfs3ProcedureRegister(); DetectUrilenRegister(); DetectDetectionFilterRegister(); DetectAsn1Register(); diff --git a/src/detect.h b/src/detect.h index 28d703a03b..ccb99a017e 100644 --- a/src/detect.h +++ b/src/detect.h @@ -1289,6 +1289,7 @@ enum { DETECT_AL_HTTP_RAW_HOST, DETECT_AL_HTTP_REQUEST_LINE, DETECT_AL_HTTP_RESPONSE_LINE, + DETECT_AL_NFS3_PROCEDURE, DETECT_AL_SSH_PROTOCOL, DETECT_AL_SSH_PROTOVERSION, DETECT_AL_SSH_SOFTWARE, diff --git a/src/output-json-nfs3.c b/src/output-json-nfs3.c new file mode 100644 index 0000000000..20f1d4d1d2 --- /dev/null +++ b/src/output-json-nfs3.c @@ -0,0 +1,202 @@ +/* Copyright (C) 2015 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. + */ + +/* + * TODO: Update \author in this file and in output-json-nfs3.h. + * TODO: Remove SCLogNotice statements, or convert to debug. + * TODO: Implement your app-layers logging. + */ + +/** + * \file + * + * \author FirstName LastName + * + * Implement JSON/eve logging app-layer NFS3. + */ + +#include "suricata-common.h" +#include "debug.h" +#include "detect.h" +#include "pkt-var.h" +#include "conf.h" + +#include "threads.h" +#include "threadvars.h" +#include "tm-threads.h" + +#include "util-unittest.h" +#include "util-buffer.h" +#include "util-debug.h" +#include "util-byte.h" + +#include "output.h" +#include "output-json.h" + +#include "app-layer.h" +#include "app-layer-parser.h" + +#include "app-layer-nfs3.h" +#include "output-json-nfs3.h" + +#ifdef HAVE_RUST +#ifdef HAVE_LIBJANSSON +#include "rust-nfs-log-gen.h" + +typedef struct LogNFS3FileCtx_ { + LogFileCtx *file_ctx; + uint32_t flags; +} LogNFS3FileCtx; + +typedef struct LogNFS3LogThread_ { + LogNFS3FileCtx *nfs3log_ctx; + uint32_t count; + MemBuffer *buffer; +} LogNFS3LogThread; + +static int JsonNFS3Logger(ThreadVars *tv, void *thread_data, + const Packet *p, Flow *f, void *state, void *tx, uint64_t tx_id) +{ + NFS3Transaction *nfs3tx = tx; + LogNFS3LogThread *thread = thread_data; + json_t *js, *nfs3js; + + if (rs_nfs3_tx_logging_is_filtered(nfs3tx)) + return TM_ECODE_OK; + + js = CreateJSONHeader((Packet *)p, 0, "nfs3"); + if (unlikely(js == NULL)) { + return TM_ECODE_FAILED; + } + + nfs3js = rs_nfs3_log_json_response(tx); + if (unlikely(nfs3js == NULL)) { + goto error; + } + + json_object_set_new(js, "nfs3", nfs3js); + + MemBufferReset(thread->buffer); + OutputJSONBuffer(js, thread->nfs3log_ctx->file_ctx, &thread->buffer); + + json_decref(js); + return TM_ECODE_OK; + +error: + json_decref(js); + return TM_ECODE_FAILED; +} + +static void OutputNFS3LogDeInitCtxSub(OutputCtx *output_ctx) +{ + LogNFS3FileCtx *nfs3log_ctx = (LogNFS3FileCtx *)output_ctx->data; + SCFree(nfs3log_ctx); + SCFree(output_ctx); +} + +static OutputCtx *OutputNFS3LogInitSub(ConfNode *conf, + OutputCtx *parent_ctx) +{ + AlertJsonThread *ajt = parent_ctx->data; + + LogNFS3FileCtx *nfs3log_ctx = SCCalloc(1, sizeof(*nfs3log_ctx)); + if (unlikely(nfs3log_ctx == NULL)) { + return NULL; + } + nfs3log_ctx->file_ctx = ajt->file_ctx; + + OutputCtx *output_ctx = SCCalloc(1, sizeof(*output_ctx)); + if (unlikely(output_ctx == NULL)) { + SCFree(nfs3log_ctx); + return NULL; + } + output_ctx->data = nfs3log_ctx; + output_ctx->DeInit = OutputNFS3LogDeInitCtxSub; + + SCLogDebug("NFS3 log sub-module initialized."); + + AppLayerParserRegisterLogger(IPPROTO_TCP, ALPROTO_NFS3); + + return output_ctx; +} + +#define OUTPUT_BUFFER_SIZE 65535 + +static TmEcode JsonNFS3LogThreadInit(ThreadVars *t, const void *initdata, void **data) +{ + LogNFS3LogThread *thread = SCCalloc(1, sizeof(*thread)); + if (unlikely(thread == NULL)) { + return TM_ECODE_FAILED; + } + + if (initdata == NULL) { + SCLogDebug("Error getting context for EveLogNFS3. \"initdata\" is NULL."); + SCFree(thread); + return TM_ECODE_FAILED; + } + + thread->buffer = MemBufferCreateNew(OUTPUT_BUFFER_SIZE); + if (unlikely(thread->buffer == NULL)) { + SCFree(thread); + return TM_ECODE_FAILED; + } + + thread->nfs3log_ctx = ((OutputCtx *)initdata)->data; + *data = (void *)thread; + + return TM_ECODE_OK; +} + +static TmEcode JsonNFS3LogThreadDeinit(ThreadVars *t, void *data) +{ + LogNFS3LogThread *thread = (LogNFS3LogThread *)data; + if (thread == NULL) { + return TM_ECODE_OK; + } + if (thread->buffer != NULL) { + MemBufferFree(thread->buffer); + } + SCFree(thread); + return TM_ECODE_OK; +} + +void JsonNFS3LogRegister(void) +{ + /* Register as an eve sub-module. */ + OutputRegisterTxSubModule(LOGGER_JSON_NFS3, "eve-log", "JsonNFS3Log", + "eve-log.nfs3", OutputNFS3LogInitSub, ALPROTO_NFS3, + JsonNFS3Logger, JsonNFS3LogThreadInit, + JsonNFS3LogThreadDeinit, NULL); + + SCLogDebug("NFS3 JSON logger registered."); +} + +#else /* No JSON support. */ + +void JsonNFS3LogRegister(void) +{ +} + +#endif /* HAVE_LIBJANSSON */ + +#else /* no rust */ + +void JsonNFS3LogRegister(void) +{ +} + +#endif /* HAVE_RUST */ diff --git a/src/output-json-nfs3.h b/src/output-json-nfs3.h new file mode 100644 index 0000000000..c2301b1eb5 --- /dev/null +++ b/src/output-json-nfs3.h @@ -0,0 +1,29 @@ +/* Copyright (C) 2015 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. + */ + +/** + * \file + * + * \author FirstName LastName + */ + +#ifndef __OUTPUT_JSON_NFS3_H__ +#define __OUTPUT_JSON_NFS3_H__ + +void JsonNFS3LogRegister(void); + +#endif /* __OUTPUT_JSON_NFS3_H__ */ diff --git a/src/output.c b/src/output.c index 8a22b0ed0f..c8f91c4b1e 100644 --- a/src/output.c +++ b/src/output.c @@ -68,6 +68,7 @@ #include "log-tcp-data.h" #include "log-stats.h" #include "output-json.h" +#include "output-json-nfs3.h" #include "output-json-template.h" #include "output-lua.h" #include "output-json-dnp3.h" @@ -1081,6 +1082,8 @@ void OutputRegisterLoggers(void) JsonDNP3LogRegister(); JsonVarsLogRegister(); + /* NFS3 JSON logger. */ + JsonNFS3LogRegister(); /* Template JSON logger. */ JsonTemplateLogRegister(); } diff --git a/src/rust.h b/src/rust.h index f69986008d..aaf47dcef7 100644 --- a/src/rust.h +++ b/src/rust.h @@ -39,4 +39,13 @@ typedef struct SuricataContext_ { } SuricataContext; +typedef struct SuricataFileContext_ { + + const StreamingBufferConfig *sbcfg; + +} SuricataFileContext; + +struct _Store; +typedef struct _Store Store; + #endif /* !__RUST_H__ */ diff --git a/src/suricata-common.h b/src/suricata-common.h index 0af2078d64..181279c357 100644 --- a/src/suricata-common.h +++ b/src/suricata-common.h @@ -396,6 +396,7 @@ typedef enum { LOGGER_JSON_HTTP, LOGGER_JSON_SMTP, LOGGER_JSON_TLS, + LOGGER_JSON_NFS3, LOGGER_JSON_TEMPLATE, LOGGER_TLS_STORE, LOGGER_TLS, diff --git a/suricata.yaml.in b/suricata.yaml.in index e5932030ef..4e87384408 100644 --- a/suricata.yaml.in +++ b/suricata.yaml.in @@ -235,6 +235,8 @@ outputs: # to yes #md5: [body, subject] + #- dnp3 + #- nfs3 - ssh - stats: totals: yes # stats for all threads merged together @@ -244,7 +246,6 @@ outputs: - flow # uni-directional flows #- netflow - #- dnp3 # Vars log flowbits and other packet and flow vars #- vars @@ -675,6 +676,8 @@ pcap-file: # "detection-only" enables protocol detection only (parser disabled). app-layer: protocols: + nfs3: + enabled: yes tls: enabled: yes detection-ports: