smb2/dcerpc: probe if response data is dcerpc

If we missed the tree connect we can't know for sure if we're
reading from a (DCERPC) PIPE or not. In this case probe the data
to see if it looks like DCERPC.

If the detection succeeds, use a special 'suricata::dcerpc' service
in the TX.

Simplify handling of DCERPC records that cross records

Update logging for the response only TXs.
pull/3498/head
Victor Julien 8 years ago
parent 9dd7c38113
commit ac4e888597

@ -149,6 +149,7 @@ pub fn dcerpc_uuid_to_string(i: &DCERPCIface) -> String {
pub struct SMBTransactionDCERPC {
pub opnum: u16,
pub req_cmd: u8,
pub req_set: bool,
pub res_cmd: u8,
pub res_set: bool,
pub call_id: u32,
@ -159,10 +160,25 @@ pub struct SMBTransactionDCERPC {
}
impl SMBTransactionDCERPC {
pub fn new(req: u8, call_id: u32) -> SMBTransactionDCERPC {
fn new_request(req: u8, call_id: u32) -> SMBTransactionDCERPC {
return SMBTransactionDCERPC {
opnum: 0,
req_cmd: req,
req_set: true,
res_cmd: 0,
res_set: false,
call_id: call_id,
frag_cnt_ts: 0,
frag_cnt_tc: 0,
stub_data_ts:Vec::new(),
stub_data_tc:Vec::new(),
}
}
fn new_response(call_id: u32) -> SMBTransactionDCERPC {
return SMBTransactionDCERPC {
opnum: 0,
req_cmd: 0,
req_set: false,
res_cmd: 0,
res_set: false,
call_id: call_id,
@ -179,14 +195,14 @@ impl SMBTransactionDCERPC {
}
impl SMBState {
pub fn new_dcerpc_tx(&mut self, hdr: SMBCommonHdr, vercmd: SMBVerCmdStat, cmd: u8, call_id: u32)
fn new_dcerpc_tx(&mut self, hdr: SMBCommonHdr, vercmd: SMBVerCmdStat, cmd: u8, call_id: u32)
-> (&mut SMBTransaction)
{
let mut tx = self.new_tx();
tx.hdr = hdr;
tx.vercmd = vercmd;
tx.type_data = Some(SMBTransactionTypeData::DCERPC(
SMBTransactionDCERPC::new(cmd, call_id)));
SMBTransactionDCERPC::new_request(cmd, call_id)));
SCLogDebug!("SMB: TX DCERPC created: ID {} hdr {:?}", tx.id, tx.hdr);
self.transactions.push(tx);
@ -194,7 +210,22 @@ impl SMBState {
return tx_ref.unwrap();
}
pub fn get_dcerpc_tx(&mut self, hdr: &SMBCommonHdr, vercmd: &SMBVerCmdStat, call_id: u32)
fn new_dcerpc_tx_for_response(&mut self, hdr: SMBCommonHdr, vercmd: SMBVerCmdStat, call_id: u32)
-> (&mut SMBTransaction)
{
let mut tx = self.new_tx();
tx.hdr = hdr;
tx.vercmd = vercmd;
tx.type_data = Some(SMBTransactionTypeData::DCERPC(
SMBTransactionDCERPC::new_response(call_id)));
SCLogDebug!("SMB: TX DCERPC created: ID {} hdr {:?}", tx.id, tx.hdr);
self.transactions.push(tx);
let tx_ref = self.transactions.last_mut();
return tx_ref.unwrap();
}
fn get_dcerpc_tx(&mut self, hdr: &SMBCommonHdr, vercmd: &SMBVerCmdStat, call_id: u32)
-> Option<&mut SMBTransaction>
{
let dce_hdr = hdr.to_dcerpc(vercmd);
@ -422,6 +453,51 @@ fn smb_read_dcerpc_record_error(state: &mut SMBState,
return found;
}
fn dcerpc_response_handle<'b>(tx: &mut SMBTransaction,
vercmd: SMBVerCmdStat,
dcer: &DceRpcRecord)
{
let (_, ntstatus) = vercmd.get_ntstatus();
match dcer.packet_type {
DCERPC_TYPE_RESPONSE => {
match parse_dcerpc_response_record(dcer.data, dcer.frag_len) {
IResult::Done(_, respr) => {
SCLogDebug!("SMBv1 READ RESPONSE {:?}", respr);
if let Some(SMBTransactionTypeData::DCERPC(ref mut tdn)) = tx.type_data {
SCLogDebug!("CMD 11 found at tx {}", tx.id);
tdn.set_result(DCERPC_TYPE_RESPONSE);
tdn.stub_data_tc.extend_from_slice(&respr.data);
tdn.frag_cnt_tc += 1;
}
tx.vercmd.set_ntstatus(ntstatus);
tx.response_done = dcer.last_frag;
},
_ => {
tx.set_event(SMBEvent::MalformedData);
},
}
},
DCERPC_TYPE_BINDACK => {
// handled elsewhere
},
21...255 => {
if let Some(SMBTransactionTypeData::DCERPC(ref mut tdn)) = tx.type_data {
tdn.set_result(dcer.packet_type);
}
tx.vercmd.set_ntstatus(ntstatus);
tx.response_done = true;
tx.set_event(SMBEvent::MalformedData);
}
_ => { // valid type w/o special processing
if let Some(SMBTransactionTypeData::DCERPC(ref mut tdn)) = tx.type_data {
tdn.set_result(dcer.packet_type);
}
tx.vercmd.set_ntstatus(ntstatus);
tx.response_done = true;
},
}
}
/// Handle DCERPC reply record. Called for READ, TRANS, IOCTL
///
pub fn smb_read_dcerpc_record<'b>(state: &mut SMBState,
@ -432,19 +508,17 @@ pub fn smb_read_dcerpc_record<'b>(state: &mut SMBState,
{
let (_, ntstatus) = vercmd.get_ntstatus();
if ntstatus != SMB_NTSTATUS_SUCCESS {
if ntstatus != SMB_NTSTATUS_SUCCESS && ntstatus != SMB_NTSTATUS_BUFFER_OVERFLOW {
return smb_read_dcerpc_record_error(state, hdr, vercmd, ntstatus);
}
SCLogDebug!("lets first see if we have prior data");
// msg_id 0 as this data crosses cmd/reply pairs
let ehdr = SMBCommonHdr::new(SMBHDR_TYPE_TRANS_FRAG,
hdr.ssn_id as u64, hdr.tree_id as u32, 0 as u64);
let ekey = SMBHashKeyHdrGuid::new(ehdr, guid.to_vec());
SCLogDebug!("ekey {:?}", ekey);
let mut prevdata = match state.ssnguid2vec_map.remove(&ekey) {
let ehdr = SMBHashKeyHdrGuid::new(SMBCommonHdr::new(SMBHDR_TYPE_TRANS_FRAG,
hdr.ssn_id as u64, hdr.tree_id as u32, 0 as u64), guid.to_vec());
let mut prevdata = match state.ssnguid2vec_map.remove(&ehdr) {
Some(s) => s,
None => Vec::new(),
None => Vec::new(),
};
SCLogDebug!("indata {} prevdata {}", indata.len(), prevdata.len());
prevdata.extend_from_slice(&indata);
@ -464,31 +538,10 @@ pub fn smb_read_dcerpc_record<'b>(state: &mut SMBState,
dcer.version_major, dcer.version_minor, dcer.data.len(), dcer);
if ntstatus == SMB_NTSTATUS_BUFFER_OVERFLOW && data.len() < dcer.frag_len as usize {
SCLogDebug!("short record {} < {}: lets see if we expected this", data.len(), dcer.frag_len);
let ehdr = SMBCommonHdr::new(SMBHDR_TYPE_MAX_SIZE,
hdr.ssn_id as u64, hdr.tree_id as u32, hdr.msg_id as u64);
let max_size = match state.ssn2maxsize_map.remove(&ehdr) {
Some(s) => s,
None => 0,
};
if max_size as usize == data.len() {
SCLogDebug!("YES this is expected. We should now see a follow up READ for {} bytes",
dcer.frag_len as usize - data.len());
let store = match state.get_dcerpc_tx(&hdr, &vercmd, dcer.call_id) {
Some(_) => true,
None => {
SCLogDebug!("no TX found");
false
},
};
if store {
SCLogDebug!("storing partial data in state");
let ehdr = SMBCommonHdr::new(SMBHDR_TYPE_MAX_SIZE,
hdr.ssn_id as u64, hdr.tree_id as u32, 0 as u64);
state.ssn2vec_map.insert(ehdr, data.to_vec());
}
return true; // TODO review
}
SCLogDebug!("short record {} < {}: storing partial data in state",
data.len(), dcer.frag_len);
state.ssnguid2vec_map.insert(ehdr, data.to_vec());
return true; // TODO review
}
if dcer.packet_type == DCERPC_TYPE_BINDACK {
@ -496,50 +549,20 @@ pub fn smb_read_dcerpc_record<'b>(state: &mut SMBState,
return true;
}
let tx = match state.get_dcerpc_tx(&hdr, &vercmd, dcer.call_id) {
Some(tx) => tx,
let found = match state.get_dcerpc_tx(&hdr, &vercmd, dcer.call_id) {
Some(tx) => {
dcerpc_response_handle(tx, vercmd.clone(), &dcer);
true
},
None => {
SCLogDebug!("no tx");
return false; },
};
match dcer.packet_type {
DCERPC_TYPE_RESPONSE => {
match parse_dcerpc_response_record(dcer.data, dcer.frag_len) {
IResult::Done(_, respr) => {
SCLogDebug!("SMBv1 READ RESPONSE {:?}", respr);
if let Some(SMBTransactionTypeData::DCERPC(ref mut tdn)) = tx.type_data {
SCLogDebug!("CMD 11 found at tx {}", tx.id);
tdn.set_result(DCERPC_TYPE_RESPONSE);
tdn.stub_data_tc.extend_from_slice(&respr.data);
tdn.frag_cnt_tc += 1;
}
tx.vercmd.set_ntstatus(ntstatus);
tx.response_done = dcer.last_frag;
},
_ => {
tx.set_event(SMBEvent::MalformedData);
},
}
},
DCERPC_TYPE_BINDACK => {
// handled above
},
21...255 => {
if let Some(SMBTransactionTypeData::DCERPC(ref mut tdn)) = tx.type_data {
tdn.set_result(dcer.packet_type);
}
tx.vercmd.set_ntstatus(ntstatus);
tx.response_done = true;
tx.set_event(SMBEvent::MalformedData);
}
_ => { // valid type w/o special processing
if let Some(SMBTransactionTypeData::DCERPC(ref mut tdn)) = tx.type_data {
tdn.set_result(dcer.packet_type);
}
tx.vercmd.set_ntstatus(ntstatus);
tx.response_done = true;
false
},
};
if !found {
// pick up DCERPC tx even if we missed the request
let tx = state.new_dcerpc_tx_for_response(hdr, vercmd.clone(), dcer.call_id);
dcerpc_response_handle(tx, vercmd, &dcer);
}
},
_ => {

@ -312,49 +312,63 @@ fn smb_common_header(state: &SMBState, tx: &SMBTransaction) -> Json
},
Some(SMBTransactionTypeData::DCERPC(ref x)) => {
let jsd = Json::object();
jsd.set_string("request", &dcerpc_type_string(x.req_cmd));
if x.req_set {
jsd.set_string("request", &dcerpc_type_string(x.req_cmd));
} else {
jsd.set_string("request", "REQUEST_LOST");
}
if x.res_set {
jsd.set_string("response", &dcerpc_type_string(x.res_cmd));
} else {
jsd.set_string("response", "UNREPLIED");
}
match x.req_cmd {
DCERPC_TYPE_REQUEST => {
jsd.set_integer("opnum", x.opnum as u64);
let req = Json::object();
req.set_integer("frag_cnt", x.frag_cnt_ts as u64);
req.set_integer("stub_data_size", x.stub_data_ts.len() as u64);
jsd.set("req", req);
let res = Json::object();
res.set_integer("frag_cnt", x.frag_cnt_tc as u64);
res.set_integer("stub_data_size", x.stub_data_tc.len() as u64);
jsd.set("res", res);
},
DCERPC_TYPE_BIND => {
match state.dcerpc_ifaces {
Some(ref ifaces) => {
let jsa = Json::array();
for i in ifaces {
let jso = Json::object();
let ifstr = dcerpc_uuid_to_string(&i);
jso.set_string("uuid", &ifstr);
let vstr = format!("{}.{}", i.ver, i.ver_min);
jso.set_string("version", &vstr);
if i.acked {
jso.set_integer("ack_result", i.ack_result as u64);
jso.set_integer("ack_reason", i.ack_reason as u64);
if x.req_set {
match x.req_cmd {
DCERPC_TYPE_REQUEST => {
jsd.set_integer("opnum", x.opnum as u64);
let req = Json::object();
req.set_integer("frag_cnt", x.frag_cnt_ts as u64);
req.set_integer("stub_data_size", x.stub_data_ts.len() as u64);
jsd.set("req", req);
},
DCERPC_TYPE_BIND => {
match state.dcerpc_ifaces {
Some(ref ifaces) => {
let jsa = Json::array();
for i in ifaces {
let jso = Json::object();
let ifstr = dcerpc_uuid_to_string(&i);
jso.set_string("uuid", &ifstr);
let vstr = format!("{}.{}", i.ver, i.ver_min);
jso.set_string("version", &vstr);
if i.acked {
jso.set_integer("ack_result", i.ack_result as u64);
jso.set_integer("ack_reason", i.ack_reason as u64);
}
jsa.array_append(jso);
}
jsa.array_append(jso);
}
jsd.set("interfaces", jsa);
},
_ => {},
}
},
_ => {},
jsd.set("interfaces", jsa);
},
_ => {},
}
},
_ => {},
}
}
if x.res_set {
match x.res_cmd {
DCERPC_TYPE_RESPONSE => {
let res = Json::object();
res.set_integer("frag_cnt", x.frag_cnt_tc as u64);
res.set_integer("stub_data_size", x.stub_data_tc.len() as u64);
jsd.set("res", res);
},
// we don't handle BINDACK w/o BIND
_ => {},
}
}
jsd.set_integer("call_id", x.call_id as u64);
js.set("dcerpc", jsd);

@ -1119,6 +1119,7 @@ impl SMBState {
Ok("lsarpc") => ("lsarpc", true),
Ok("samr") => ("samr", true),
Ok("spoolss") => ("spoolss", true),
Ok("suricata::dcerpc") => ("unknown", true),
Err(_) => ("MALFORMED", false),
Ok(&_) => {
SCLogDebug!("don't know {}", String::from_utf8_lossy(&n));

@ -869,9 +869,9 @@ pub fn smb1_trans_response_record<'b>(state: &mut SMBState, r: &SmbRecord<'b>)
// if we get status 'BUFFER_OVERFLOW' this is only a part of
// the data. Store it in the ssn/tree for later use.
if r.nt_status == SMB_NTSTATUS_BUFFER_OVERFLOW {
state.ssnguid2vec_map.insert(SMBHashKeyHdrGuid::new(
SMBCommonHdr::from1(r, SMBHDR_TYPE_TRANS_FRAG), fid),
rd.data.to_vec());
let key = SMBHashKeyHdrGuid::new(SMBCommonHdr::from1(r, SMBHDR_TYPE_TRANS_FRAG), fid);
SCLogDebug!("SMBv1/TRANS: queueing data for len {} key {:?}", rd.data.len(), key);
state.ssnguid2vec_map.insert(key, rd.data.to_vec());
} else if is_dcerpc {
SCLogDebug!("SMBv1 TRANS TO PIPE");
let hdr = SMBCommonHdr::from1(r, SMBHDR_TYPE_HEADER);

@ -19,6 +19,7 @@ use core::*;
use log::*;
use nom::IResult;
use smb;
use smb::smb::*;
use smb::smb2_records::*;
use smb::smb2_session::*;
@ -118,7 +119,11 @@ pub fn smb2_read_response_record<'b>(state: &mut SMBState, r: &Smb2Record<'b>)
match parse_smb2_response_read(r.data) {
IResult::Done(_, rd) => {
if r.nt_status != SMB_NTSTATUS_SUCCESS {
if r.nt_status == SMB_NTSTATUS_BUFFER_OVERFLOW {
SCLogDebug!("SMBv2/READ: incomplete record, expecting a follow up");
// fall through
} else if r.nt_status != SMB_NTSTATUS_SUCCESS {
SCLogDebug!("SMBv2: read response error code received: skip record");
state.set_skip(STREAM_TOCLIENT, rd.len, rd.data.len() as u32);
return;
@ -152,19 +157,48 @@ pub fn smb2_read_response_record<'b>(state: &mut SMBState, r: &Smb2Record<'b>)
},
None => { false },
};
SCLogDebug!("existing file tx? {}", found);
if !found {
let tree_key = SMBCommonHdr::from2(r, SMBHDR_TYPE_SHARE);
let (share_name, is_pipe) = match state.ssn2tree_map.get(&tree_key) {
let (share_name, mut is_pipe) = match state.ssn2tree_map.get(&tree_key) {
Some(n) => (n.name.to_vec(), n.is_pipe),
_ => { (Vec::new(), false) },
};
let is_dcerpc = is_pipe && match state.get_service_for_guid(&file_guid) {
let mut is_dcerpc = is_pipe && match state.get_service_for_guid(&file_guid) {
(_, x) => x,
};
SCLogDebug!("SMBv2/READ: share_name {:?} is_pipe {} is_dcerpc {}", share_name, is_pipe, is_dcerpc);
if share_name.len() == 0 && !is_pipe {
SCLogDebug!("SMBv2/READ: no tree connect seen, we don't know if we are a pipe");
match smb::dcerpc_records::parse_dcerpc_record(rd.data) {
IResult::Done(_, recr) => {
SCLogDebug!("SMBv2/READ: could be DCERPC {:?}", recr);
if recr.version_major == 5 && recr.version_minor < 3 &&
recr.frag_len > 0 && recr.packet_type <= 20 {
SCLogDebug!("SMBv2/READ: looks like dcerpc");
// insert fake tree to assist in follow up lookups
let tree = SMBTree::new(b"suricata::dcerpc".to_vec(), true);
state.ssn2tree_map.insert(tree_key, tree);
state.guid2name_map.insert(file_guid.to_vec(), b"suricata::dcerpc".to_vec());
is_pipe = true;
is_dcerpc = true;
} else {
SCLogDebug!("SMBv2/READ: not DCERPC");
}
},
_ => {
SCLogDebug!("SMBv2/READ: not DCERPC");
},
}
}
if is_pipe && is_dcerpc {
SCLogDebug!("SMBv2 DCERPC read");
let hdr = SMBCommonHdr::from2(r, SMBHDR_TYPE_HEADER);
let vercmd = SMBVerCmdStat::new2(SMB2_COMMAND_READ);
let vercmd = SMBVerCmdStat::new2_with_ntstatus(SMB2_COMMAND_READ, r.nt_status);
smb_read_dcerpc_record(state, vercmd, hdr, &file_guid, rd.data);
} else if is_pipe {
SCLogDebug!("non-DCERPC pipe");
@ -191,6 +225,7 @@ pub fn smb2_read_response_record<'b>(state: &mut SMBState, r: &Smb2Record<'b>)
state.set_file_left(STREAM_TOCLIENT, rd.len, rd.data.len() as u32, file_guid.to_vec());
}
_ => {
SCLogDebug!("SMBv2: failed to parse read response");
state.set_event(SMBEvent::MalformedData);
}
}
@ -198,6 +233,7 @@ pub fn smb2_read_response_record<'b>(state: &mut SMBState, r: &Smb2Record<'b>)
pub fn smb2_write_request_record<'b>(state: &mut SMBState, r: &Smb2Record<'b>)
{
SCLogDebug!("SMBv2/WRITE: request record");
if smb2_create_new_tx(r.command) {
let tx_key = SMBCommonHdr::from2(r, SMBHDR_TYPE_GENERICTX);
let tx = state.new_generic_tx(2, r.command, tx_key);
@ -229,13 +265,47 @@ pub fn smb2_write_request_record<'b>(state: &mut SMBState, r: &Smb2Record<'b>)
};
if !found {
let tree_key = SMBCommonHdr::from2(r, SMBHDR_TYPE_SHARE);
let is_pipe = match state.ssn2tree_map.get(&tree_key) {
Some(n) => { n.is_pipe },
_ => { false },
let (share_name, mut is_pipe) = match state.ssn2tree_map.get(&tree_key) {
Some(n) => { (n.name.to_vec(), n.is_pipe) },
_ => { (Vec::new(), false) },
};
let is_dcerpc = is_pipe && match state.get_service_for_guid(&wr.guid) {
(_, x) => x,
let mut is_dcerpc = if is_pipe || (share_name.len() == 0 && !is_pipe) {
match state.get_service_for_guid(&wr.guid) {
(_, x) => x,
}
} else {
false
};
SCLogDebug!("share_name {:?} is_pipe {} is_dcerpc {}", share_name, is_pipe, is_dcerpc);
// if we missed the TREE connect we can't be sure if 'is_dcerpc' is correct
if share_name.len() == 0 && !is_pipe {
SCLogDebug!("SMBv2/WRITE: no tree connect seen, we don't know if we are a pipe");
match smb::dcerpc_records::parse_dcerpc_record(wr.data) {
IResult::Done(_, recr) => {
SCLogDebug!("SMBv2/WRITE: could be DCERPC {:?}", recr);
if recr.version_major == 5 && recr.version_minor < 3 &&
recr.frag_len > 0 && recr.packet_type <= 20 {
SCLogDebug!("SMBv2/WRITE: looks like we have dcerpc");
let tree = SMBTree::new(b"suricata::dcerpc".to_vec(), true);
state.ssn2tree_map.insert(tree_key, tree);
if !is_dcerpc {
state.guid2name_map.insert(file_guid.to_vec(),
b"suricata::dcerpc".to_vec());
}
is_pipe = true;
is_dcerpc = true;
} else {
SCLogDebug!("SMBv2/WRITE: not DCERPC");
}
},
_ => {
SCLogDebug!("SMBv2/WRITE: not DCERPC");
},
}
}
if is_pipe && is_dcerpc {
SCLogDebug!("SMBv2 DCERPC write");
let hdr = SMBCommonHdr::from2(r, SMBHDR_TYPE_HEADER);
@ -522,7 +592,8 @@ pub fn smb2_response_record<'b>(state: &mut SMBState, r: &Smb2Record<'b>)
false // the request may have created a generic tx, so handle that here
},
SMB2_COMMAND_READ => {
if r.nt_status == SMB_NTSTATUS_SUCCESS {
if r.nt_status == SMB_NTSTATUS_SUCCESS ||
r.nt_status == SMB_NTSTATUS_BUFFER_OVERFLOW {
smb2_read_response_record(state, &r);
false

Loading…
Cancel
Save