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

678 lines
22 KiB
Rust

/* Copyright (C) 2019-2024 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.
*/
use super::dns::{DNSRcode, DNSRecordType, DNSTransaction, ALPROTO_DNS};
use crate::core::{DetectEngineThreadCtx, STREAM_TOCLIENT, STREAM_TOSERVER};
use crate::detect::uint::{
detect_match_uint, detect_parse_uint_enum, DetectUintData, SCDetectU16Free, SCDetectU8Free,
SCDetectU8Parse,
};
use crate::detect::{
helper_keyword_register_sticky_buffer, DetectHelperBufferRegister,
DetectHelperKeywordAliasRegister, DetectHelperKeywordRegister, DetectSignatureSetAppProto,
SCSigTableAppLiteElmt, SigMatchAppendSMToList, SigTableElmtStickyBuffer,
};
use crate::direction::Direction;
use std::ffi::CStr;
use std::os::raw::{c_int, c_void};
use suricata_sys::sys::{
DetectEngineCtx, SCDetectBufferSetActiveList, SCDetectHelperMultiBufferProgressMpmRegister,
Signature,
};
/// Perform the DNS opcode match.
///
/// 1 will be returned on match, otherwise 0 will be returned.
unsafe extern "C" fn dns_opcode_match(
_de: *mut c_void, _f: *mut c_void, flags: u8, _state: *mut c_void, tx: *mut c_void,
_sig: *const c_void, ctx: *const c_void,
) -> c_int {
let tx = cast_pointer!(tx, DNSTransaction);
let ctx = cast_pointer!(ctx, DetectUintData<u8>);
let header_flags = if flags & Direction::ToServer as u8 != 0 {
if let Some(request) = &tx.request {
request.header.flags
} else {
return 0;
}
} else if flags & Direction::ToClient as u8 != 0 {
if let Some(response) = &tx.response {
response.header.flags
} else {
return 0;
}
} else {
// Not to server or to client??
return 0;
};
let opcode = ((header_flags >> 11) & 0xf) as u8;
if detect_match_uint(ctx, opcode) {
return 1;
}
return 0;
}
/// Perform the DNS rcode match.
///
/// 1 will be returned on match, otherwise 0 will be returned.
unsafe extern "C" fn dns_rcode_match(
_de: *mut c_void, _f: *mut c_void, flags: u8, _state: *mut c_void, tx: *mut c_void,
_sig: *const c_void, ctx: *const c_void,
) -> c_int {
let tx = cast_pointer!(tx, DNSTransaction);
let ctx = cast_pointer!(ctx, DetectUintData<u16>);
let header_flags = if flags & Direction::ToServer as u8 != 0 {
if let Some(request) = &tx.request {
request.header.flags
} else {
return 0;
}
} else if let Some(response) = &tx.response {
response.header.flags
} else {
return 0;
};
let rcode = header_flags & 0xf;
if detect_match_uint(ctx, rcode) {
return 1;
}
return 0;
}
/// Perform the DNS rrtype match.
/// 1 will be returned on match, otherwise 0 will be returned.
unsafe extern "C" fn dns_rrtype_match(
_de: *mut c_void, _f: *mut c_void, flags: u8, _state: *mut c_void, tx: *mut c_void,
_sig: *const c_void, ctx: *const c_void,
) -> c_int {
let tx = cast_pointer!(tx, DNSTransaction);
let ctx = cast_pointer!(ctx, DetectUintData<u16>);
if flags & Direction::ToServer as u8 != 0 {
if let Some(request) = &tx.request {
for i in 0..request.queries.len() {
if detect_match_uint(ctx, request.queries[i].rrtype) {
return 1;
}
}
}
} else if flags & Direction::ToClient as u8 != 0 {
if let Some(response) = &tx.response {
for i in 0..response.answers.len() {
if detect_match_uint(ctx, response.answers[i].rrtype) {
return 1;
}
}
}
}
return 0;
}
static mut G_DNS_ANSWER_NAME_BUFFER_ID: c_int = 0;
static mut G_DNS_QUERY_NAME_BUFFER_ID: c_int = 0;
static mut G_DNS_QUERY_BUFFER_ID: c_int = 0;
static mut G_DNS_OPCODE_KW_ID: c_int = 0;
static mut G_DNS_OPCODE_BUFFER_ID: c_int = 0;
static mut G_DNS_RCODE_KW_ID: c_int = 0;
static mut G_DNS_RCODE_BUFFER_ID: c_int = 0;
static mut G_DNS_RRTYPE_KW_ID: c_int = 0;
static mut G_DNS_RRTYPE_BUFFER_ID: c_int = 0;
unsafe extern "C" fn dns_opcode_setup(
de: *mut DetectEngineCtx, s: *mut Signature, raw: *const libc::c_char,
) -> c_int {
if DetectSignatureSetAppProto(s, ALPROTO_DNS) != 0 {
return -1;
}
let ctx = SCDetectU8Parse(raw) as *mut c_void;
if ctx.is_null() {
return -1;
}
if SigMatchAppendSMToList(de, s, G_DNS_OPCODE_KW_ID, ctx, G_DNS_OPCODE_BUFFER_ID).is_null() {
dns_opcode_free(std::ptr::null_mut(), ctx);
return -1;
}
return 0;
}
unsafe extern "C" fn dns_opcode_free(_de: *mut c_void, ctx: *mut c_void) {
// Just unbox...
let ctx = cast_pointer!(ctx, DetectUintData<u8>);
SCDetectU8Free(ctx);
}
unsafe extern "C" fn dns_rcode_parse(ustr: *const std::os::raw::c_char) -> *mut DetectUintData<u8> {
let ft_name: &CStr = CStr::from_ptr(ustr); //unsafe
if let Ok(s) = ft_name.to_str() {
if let Some(ctx) = detect_parse_uint_enum::<u16, DNSRcode>(s) {
let boxed = Box::new(ctx);
return Box::into_raw(boxed) as *mut _;
}
}
return std::ptr::null_mut();
}
unsafe extern "C" fn dns_rcode_setup(
de: *mut DetectEngineCtx, s: *mut Signature, raw: *const libc::c_char,
) -> c_int {
if DetectSignatureSetAppProto(s, ALPROTO_DNS) != 0 {
return -1;
}
let ctx = dns_rcode_parse(raw) as *mut c_void;
if ctx.is_null() {
return -1;
}
if SigMatchAppendSMToList(de, s, G_DNS_RCODE_KW_ID, ctx, G_DNS_RCODE_BUFFER_ID).is_null() {
dns_rcode_free(std::ptr::null_mut(), ctx);
return -1;
}
return 0;
}
unsafe extern "C" fn dns_rcode_free(_de: *mut c_void, ctx: *mut c_void) {
// Just unbox...
let ctx = cast_pointer!(ctx, DetectUintData<u16>);
SCDetectU16Free(ctx);
}
unsafe extern "C" fn dns_rrtype_parse(
ustr: *const std::os::raw::c_char,
) -> *mut DetectUintData<u8> {
let ft_name: &CStr = CStr::from_ptr(ustr); //unsafe
if let Ok(s) = ft_name.to_str() {
if let Some(ctx) = detect_parse_uint_enum::<u16, DNSRecordType>(s) {
let boxed = Box::new(ctx);
return Box::into_raw(boxed) as *mut _;
}
}
return std::ptr::null_mut();
}
unsafe extern "C" fn dns_rrtype_setup(
de: *mut DetectEngineCtx, s: *mut Signature, raw: *const libc::c_char,
) -> c_int {
if DetectSignatureSetAppProto(s, ALPROTO_DNS) != 0 {
return -1;
}
let ctx = dns_rrtype_parse(raw) as *mut c_void;
if ctx.is_null() {
return -1;
}
if SigMatchAppendSMToList(de, s, G_DNS_RRTYPE_KW_ID, ctx, G_DNS_RRTYPE_BUFFER_ID).is_null() {
dns_rrtype_free(std::ptr::null_mut(), ctx);
return -1;
}
return 0;
}
unsafe extern "C" fn dns_rrtype_free(_de: *mut c_void, ctx: *mut c_void) {
// Just unbox...
let ctx = cast_pointer!(ctx, DetectUintData<u16>);
SCDetectU16Free(ctx);
}
unsafe extern "C" fn dns_detect_answer_name_setup(
de: *mut DetectEngineCtx, s: *mut Signature, _raw: *const std::os::raw::c_char,
) -> c_int {
if DetectSignatureSetAppProto(s, ALPROTO_DNS) != 0 {
return -1;
}
if SCDetectBufferSetActiveList(de, s, G_DNS_ANSWER_NAME_BUFFER_ID) < 0 {
return -1;
}
return 0;
}
/// Get the DNS response answer name and index i.
unsafe extern "C" fn dns_tx_get_answer_name(
_de: *mut DetectEngineThreadCtx, tx: *const c_void, flags: u8, i: u32, buf: *mut *const u8,
len: *mut u32,
) -> bool {
let tx = cast_pointer!(tx, DNSTransaction);
let answers = if flags & Direction::ToClient as u8 != 0 {
tx.response.as_ref().map(|response| &response.answers)
} else {
tx.request.as_ref().map(|request| &request.answers)
};
let index = i as usize;
if let Some(answers) = answers {
if let Some(answer) = answers.get(index) {
if !answer.name.value.is_empty() {
*buf = answer.name.value.as_ptr();
*len = answer.name.value.len() as u32;
return true;
}
}
}
false
}
unsafe extern "C" fn dns_detect_query_name_setup(
de: *mut DetectEngineCtx, s: *mut Signature, _raw: *const std::os::raw::c_char,
) -> c_int {
if DetectSignatureSetAppProto(s, ALPROTO_DNS) != 0 {
return -1;
}
if SCDetectBufferSetActiveList(de, s, G_DNS_QUERY_NAME_BUFFER_ID) < 0 {
return -1;
}
return 0;
}
/// Get the DNS response answer name and index i.
unsafe extern "C" fn dns_tx_get_query_name(
_de: *mut DetectEngineThreadCtx, tx: *const c_void, flags: u8, i: u32, buf: *mut *const u8,
len: *mut u32,
) -> bool {
let tx = cast_pointer!(tx, DNSTransaction);
let queries = if flags & Direction::ToClient as u8 != 0 {
tx.response.as_ref().map(|response| &response.queries)
} else {
tx.request.as_ref().map(|request| &request.queries)
};
let index = i as usize;
if let Some(queries) = queries {
if let Some(query) = queries.get(index) {
if !query.name.value.is_empty() {
*buf = query.name.value.as_ptr();
*len = query.name.value.len() as u32;
return true;
}
}
}
false
}
unsafe extern "C" fn dns_tx_get_query(
_de: *mut DetectEngineThreadCtx, tx: *const c_void, _flags: u8, i: u32, buf: *mut *const u8,
len: *mut u32,
) -> bool {
return dns_tx_get_query_name(_de, tx, Direction::ToServer as u8, i, buf, len);
}
unsafe extern "C" fn dns_detect_query_setup(
de: *mut DetectEngineCtx, s: *mut Signature, _raw: *const std::os::raw::c_char,
) -> c_int {
if DetectSignatureSetAppProto(s, ALPROTO_DNS) != 0 {
return -1;
}
if SCDetectBufferSetActiveList(de, s, G_DNS_QUERY_BUFFER_ID) < 0 {
return -1;
}
return 0;
}
#[no_mangle]
pub unsafe extern "C" fn SCDetectDNSRegister() {
let kw = SigTableElmtStickyBuffer {
name: String::from("dns.answer.name"),
desc: String::from("DNS answer name sticky buffer"),
url: String::from("/rules/dns-keywords.html#dns-answer-name"),
setup: dns_detect_answer_name_setup,
};
let _g_dns_answer_name_kw_id = helper_keyword_register_sticky_buffer(&kw);
G_DNS_ANSWER_NAME_BUFFER_ID = SCDetectHelperMultiBufferProgressMpmRegister(
b"dns.answer.name\0".as_ptr() as *const libc::c_char,
b"dns answer name\0".as_ptr() as *const libc::c_char,
ALPROTO_DNS,
STREAM_TOSERVER | STREAM_TOCLIENT,
/* Register also in the TO_SERVER direction, even though this is not
normal, it could be provided as part of a request. */
Some(dns_tx_get_answer_name),
1, // response complete
);
let kw = SCSigTableAppLiteElmt {
name: b"dns.opcode\0".as_ptr() as *const libc::c_char,
desc: b"Match the DNS header opcode flag.\0".as_ptr() as *const libc::c_char,
url: b"rules/dns-keywords.html#dns-opcode\0".as_ptr() as *const libc::c_char,
AppLayerTxMatch: Some(dns_opcode_match),
Setup: dns_opcode_setup,
Free: Some(dns_opcode_free),
flags: 0,
};
G_DNS_OPCODE_KW_ID = DetectHelperKeywordRegister(&kw);
G_DNS_OPCODE_BUFFER_ID = DetectHelperBufferRegister(
b"dns.opcode\0".as_ptr() as *const libc::c_char,
ALPROTO_DNS,
STREAM_TOSERVER | STREAM_TOCLIENT,
);
let kw = SigTableElmtStickyBuffer {
name: String::from("dns.query.name"),
desc: String::from("DNS query name sticky buffer"),
url: String::from("/rules/dns-keywords.html#dns-query-name"),
setup: dns_detect_query_name_setup,
};
let _g_dns_query_name_kw_id = helper_keyword_register_sticky_buffer(&kw);
G_DNS_QUERY_NAME_BUFFER_ID = SCDetectHelperMultiBufferProgressMpmRegister(
b"dns.query.name\0".as_ptr() as *const libc::c_char,
b"dns query name\0".as_ptr() as *const libc::c_char,
ALPROTO_DNS,
STREAM_TOSERVER | STREAM_TOCLIENT,
/* Register in both directions as the query is usually echoed back
in the response. */
Some(dns_tx_get_query_name),
1, // request or response complete
);
let kw = SCSigTableAppLiteElmt {
name: b"dns.rcode\0".as_ptr() as *const libc::c_char,
desc: b"Match the DNS header rcode flag.\0".as_ptr() as *const libc::c_char,
url: b"rules/dns-keywords.html#dns-rcode\0".as_ptr() as *const libc::c_char,
AppLayerTxMatch: Some(dns_rcode_match),
Setup: dns_rcode_setup,
Free: Some(dns_rcode_free),
flags: 0,
};
G_DNS_RCODE_KW_ID = DetectHelperKeywordRegister(&kw);
G_DNS_RCODE_BUFFER_ID = DetectHelperBufferRegister(
b"dns.rcode\0".as_ptr() as *const libc::c_char,
ALPROTO_DNS,
STREAM_TOSERVER | STREAM_TOCLIENT,
);
let kw = SCSigTableAppLiteElmt {
name: b"dns.rrtype\0".as_ptr() as *const libc::c_char,
desc: b"Match the DNS rrtype in message body.\0".as_ptr() as *const libc::c_char,
url: b"rules/dns-keywords.html#dns-rrtype\0".as_ptr() as *const libc::c_char,
AppLayerTxMatch: Some(dns_rrtype_match),
Setup: dns_rrtype_setup,
Free: Some(dns_rrtype_free),
flags: 0,
};
G_DNS_RRTYPE_KW_ID = DetectHelperKeywordRegister(&kw);
G_DNS_RRTYPE_BUFFER_ID = DetectHelperBufferRegister(
b"dns.rrtype\0".as_ptr() as *const libc::c_char,
ALPROTO_DNS,
STREAM_TOSERVER | STREAM_TOCLIENT,
);
let kw = SigTableElmtStickyBuffer {
name: String::from("dns.query"),
desc: String::from("sticky buffer to match DNS query-buffer"),
url: String::from("/rules/dns-keywords.html#dns-query"),
setup: dns_detect_query_setup,
};
let g_dns_query_name_kw_id = helper_keyword_register_sticky_buffer(&kw);
DetectHelperKeywordAliasRegister(
g_dns_query_name_kw_id,
b"dns_query\0".as_ptr() as *const libc::c_char,
);
G_DNS_QUERY_BUFFER_ID = SCDetectHelperMultiBufferProgressMpmRegister(
b"dns_query\0".as_ptr() as *const libc::c_char,
b"dns request query\0".as_ptr() as *const libc::c_char,
ALPROTO_DNS,
STREAM_TOSERVER,
Some(dns_tx_get_query), // reuse, will be called only toserver
1, // request complete
);
}
#[cfg(test)]
mod test {
use super::*;
use crate::detect::uint::{detect_parse_uint, DetectUintMode};
#[test]
fn parse_opcode_good() {
assert_eq!(
detect_parse_uint::<u8>("1").unwrap().1,
DetectUintData {
mode: DetectUintMode::DetectUintModeEqual,
arg1: 1,
arg2: 0,
}
);
assert_eq!(
detect_parse_uint::<u8>("123").unwrap().1,
DetectUintData {
mode: DetectUintMode::DetectUintModeEqual,
arg1: 123,
arg2: 0,
}
);
assert_eq!(
detect_parse_uint::<u8>("!123").unwrap().1,
DetectUintData {
mode: DetectUintMode::DetectUintModeNe,
arg1: 123,
arg2: 0,
}
);
assert!(detect_parse_uint::<u8>("").is_err());
assert!(detect_parse_uint::<u8>("!").is_err());
assert!(detect_parse_uint::<u8>("! ").is_err());
assert!(detect_parse_uint::<u8>("!asdf").is_err());
}
#[test]
fn test_match_opcode() {
assert!(detect_match_uint(
&DetectUintData {
mode: DetectUintMode::DetectUintModeEqual,
arg1: 0,
arg2: 0,
},
0b0000_0000_0000_0000,
));
assert!(!detect_match_uint(
&DetectUintData {
mode: DetectUintMode::DetectUintModeNe,
arg1: 0,
arg2: 0,
},
0b0000_0000_0000_0000,
));
assert!(detect_match_uint(
&DetectUintData {
mode: DetectUintMode::DetectUintModeEqual,
arg1: 4,
arg2: 0,
},
((0b0010_0000_0000_0000 >> 11) & 0xf) as u8,
));
assert!(!detect_match_uint(
&DetectUintData {
mode: DetectUintMode::DetectUintModeNe,
arg1: 4,
arg2: 0,
},
((0b0010_0000_0000_0000 >> 11) & 0xf) as u8,
));
}
#[test]
fn parse_rcode_good() {
assert_eq!(
detect_parse_uint_enum::<u16, DNSRcode>("1").unwrap(),
DetectUintData {
mode: DetectUintMode::DetectUintModeEqual,
arg1: 1,
arg2: 0,
}
);
assert_eq!(
detect_parse_uint_enum::<u16, DNSRcode>("123").unwrap(),
DetectUintData {
mode: DetectUintMode::DetectUintModeEqual,
arg1: 123,
arg2: 0,
}
);
assert_eq!(
detect_parse_uint_enum::<u16, DNSRcode>("!123").unwrap(),
DetectUintData {
mode: DetectUintMode::DetectUintModeNe,
arg1: 123,
arg2: 0,
}
);
assert_eq!(
detect_parse_uint_enum::<u16, DNSRcode>("7-15").unwrap(),
DetectUintData {
mode: DetectUintMode::DetectUintModeRange,
arg1: 7,
arg2: 15,
}
);
assert_eq!(
detect_parse_uint_enum::<u16, DNSRcode>("nxdomain").unwrap(),
DetectUintData {
mode: DetectUintMode::DetectUintModeEqual,
arg1: DNSRcode::NXDOMAIN as u16,
arg2: 0,
}
);
assert!(detect_parse_uint_enum::<u16, DNSRcode>("").is_none());
assert!(detect_parse_uint_enum::<u16, DNSRcode>("!").is_none());
assert!(detect_parse_uint_enum::<u16, DNSRcode>("! ").is_none());
assert!(detect_parse_uint_enum::<u16, DNSRcode>("!asdf").is_none());
}
#[test]
fn test_match_rcode() {
assert!(detect_match_uint(
&DetectUintData {
mode: DetectUintMode::DetectUintModeEqual,
arg1: 0,
arg2: 0,
},
0b0000_0000_0000_0000,
));
assert!(!detect_match_uint(
&DetectUintData {
mode: DetectUintMode::DetectUintModeNe,
arg1: 0,
arg2: 0,
},
0b0000_0000_0000_0000,
));
assert!(detect_match_uint(
&DetectUintData {
mode: DetectUintMode::DetectUintModeEqual,
arg1: 4,
arg2: 0,
},
4u8,
));
assert!(!detect_match_uint(
&DetectUintData {
mode: DetectUintMode::DetectUintModeNe,
arg1: 4,
arg2: 0,
},
4u8,
));
}
#[test]
fn parse_rrtype_good() {
assert_eq!(
detect_parse_uint_enum::<u16, DNSRecordType>("1").unwrap(),
DetectUintData {
mode: DetectUintMode::DetectUintModeEqual,
arg1: 1,
arg2: 0,
}
);
assert_eq!(
detect_parse_uint_enum::<u16, DNSRecordType>("123").unwrap(),
DetectUintData {
mode: DetectUintMode::DetectUintModeEqual,
arg1: 123,
arg2: 0,
}
);
assert_eq!(
detect_parse_uint_enum::<u16, DNSRecordType>("!123").unwrap(),
DetectUintData {
mode: DetectUintMode::DetectUintModeNe,
arg1: 123,
arg2: 0,
}
);
assert_eq!(
detect_parse_uint_enum::<u16, DNSRecordType>("7-15").unwrap(),
DetectUintData {
mode: DetectUintMode::DetectUintModeRange,
arg1: 7,
arg2: 15,
}
);
assert_eq!(
detect_parse_uint_enum::<u16, DNSRecordType>("a").unwrap(),
DetectUintData {
mode: DetectUintMode::DetectUintModeEqual,
arg1: DNSRecordType::A as u16,
arg2: 0,
}
);
assert!(detect_parse_uint_enum::<u16, DNSRecordType>("").is_none());
assert!(detect_parse_uint_enum::<u16, DNSRecordType>("!").is_none());
assert!(detect_parse_uint_enum::<u16, DNSRecordType>("! ").is_none());
assert!(detect_parse_uint_enum::<u16, DNSRecordType>("!asdf").is_none());
}
#[test]
fn test_match_rrtype() {
assert!(detect_match_uint(
&DetectUintData {
mode: DetectUintMode::DetectUintModeEqual,
arg1: 0,
arg2: 0,
},
0b0000_0000_0000_0000,
));
assert!(!detect_match_uint(
&DetectUintData {
mode: DetectUintMode::DetectUintModeNe,
arg1: 0,
arg2: 0,
},
0b0000_0000_0000_0000,
));
assert!(detect_match_uint(
&DetectUintData {
mode: DetectUintMode::DetectUintModeEqual,
arg1: 4,
arg2: 0,
},
4u16,
));
assert!(!detect_match_uint(
&DetectUintData {
mode: DetectUintMode::DetectUintModeNe,
arg1: 4,
arg2: 0,
},
4u16,
));
}
}