2024-05-10-suricata

I'm programmer

Posted by 大狗 on May 10, 2024

Suricate

0 Suricata原理解释

0.1 Suricata提供的App检测关键字解释

Suricata有非常多方便的检测关键字,这些关键字可以针对Payload或者Applayer的内容。下面列出来这些关键字的含义,我理解如果基于这些关键字做匹配,那么需要知道能提供的能力范畴

  • content:conten关键字提供匹配能力,可打印的自负可以直接匹配,比方说content:”abc”,不可匹配字符使用 xx 来匹配,这里的   就类比十六进制的0x。一些特殊字符只能使用这种方式匹配,参考下面的内容。这里可能需要注意,content默认区分大小写
    "     |22|
    ;     |3B|
    :     |3A|
    |     |7C|
      
    #用法范例
    content:"a|0D|bc";
    content:"|61 0D 62 63|";
    content:"a|0D|b|63|";
      
    
  • nocase:用于指示不区分大小写,常常和content一块使用

  • depth:depth关键字必须放在content之后,depth后面必须跟一个数字,depth后面的数字表示将从代检查的数据的开头检查多少字节,包含content的内容。

    # Payload
    # abcdefhjlj
      
    # 这个命中不了,depth为3,只检查头三位,这个明显头三位不包含def
    content: "def"; depth:3;
    # 这个可以命中,depth为3,只检查头三位,包含abc
    content: "abc"; depth:3;
    
  • startswith:和depth类似,不过这个要求匹配必须就在payload的开始部分。该关键字必须跟着content的内容,且这个关键字不能和 depth, offset, within or distance组合使用。

  • endswith:顾名思义,和startswith类似,就不解释了

  • offset:offset关键字指定将从payload中的哪个字节开始检查以查找匹配项。这个和depth组合到一起的时候注意下,offset是从某个字符往后面找匹配,depth是从某个字符前面找。

    # Payload
    # abcdefhjlj
      
    # 这个命中不了,depth为3,只检查头三位,这个明显头三位不包含def
    content: "def"; offset:3; depth:6;
      
    
  • distance:distance表明content匹配的内容与之前content匹配的内容之间的关系。content紧跟的值决定了将从payload中的哪个字节开始相对于前一个匹配进行检查以查找匹配项。比方说distance:5;”意味着可以在前一个匹配的content+ 5 字节之后的任何位置匹配。distance紧跟的值可以为负数

  • within:和distance不同,distance是第一个content匹配之后多少去找匹配第二个content,within是第一个content之内多少的字符去匹配第二个content。

  • rawbytes:没啥用,纯粹兼容snort

  • isdataat:“isdataat”关键字的目的是查看payload的特定部分是否仍有数据。该关键字以一个数字(位置)开头,然后可选地跟着由逗号分隔的“relative”以及“rawbytes”选项。使用“relative”这个词是为了了解有效负载的特定部分相对于上次匹配是否仍有数据。

    # Payload
    # abcdefghij
      
    # 这个可以命中,相对abc的第六个字符是i,还有数据
    content: "abc"; isdataat:6; relative; 
    
  • bsize: 用于检测detection buffer的长度是否满足特定条件,具体写法如下

    bsize:<number>;
    bsize:=<number>;
    bsize:<<number>;
    bsize:><number>;
    bsize:<lo-number><><hi-number>;
    
  • dsize:用来检查payload/data的总的长度,用法为dsize:[<>!]number; || dsize:min<>max;

  • byte_test:“byte_test”关键字提取的内容,并且用和位于偏移做比较。的值会先做一遍mask。这里注意它的用法,byte_test是可以用来做relative的,也就是说它可以相对前面match的内容,做relative比较。比方说我可以直接先content匹配一个报文里面的key,然后在相对这个content匹配内容

    byte_test:<num of bytes> | <variable_name>, [!]<operator>, <test value>, <offset> [,relative] \
    [,<endian>][, string, <num type>][, dce][, bitmask <bitmask value>];
    
       
    The number of bytes selected from the packet to be converted or the name of a byte_extract/byte_math variable.
    [!] Negation can prefix other operators
    < less than
    > greater than
    = equal
    <= less than or equal
    >= greater than or equal
    & bitwise AND
    ^ bitwise OR
    Value to test the converted value against [hex or decimal accepted]
    Number of bytes into the payload
    [relative] Offset relative to last content match
    [endian] Type of number being read: - big (Most significant byte at lowest address) - little (Most significant byte at the highest address)
    [string] hex - Converted string represented in hex
    dec - Converted string represented in decimal
    oct - Converted string represented in octal
    [dce] Allow the DCE module to determine the byte order
    [bitmask] Applies the AND operator on the bytes converted
  • byte_math:用来对提取出来的关键字做数学运算,用法为,就不多讲了。这里可能注意一点,它的oper的操作对象可以是使用byte_extract出来的对象

    byte_math:bytes <num of bytes> | <variable-name> , offset <offset>, oper <operator>, rvalue <rvalue>, \
          result <result_var> [, relative] [, endian <endian>] [, string <number-type>] \
          [, dce] [, bitmask <value>];
    
  • byte_jump:

  • byte_extract:byte_extract关键字在特定的<num of bytes><offset>处提取,并将其存储在<var_name>中。<var_name>中的值可用于参与计算,作为byte_test & byte_math的运算符。

  • replace:这个说实话基本没用过,不提

  • pcre:pcre这个正则匹配对性能往往有负面影响,因此一般推荐和content一起使用,至于pcre可以提供什么匹配参考http://en.wikipedia.org/wiki/Regular_expression。语法参考

    pcre:"/<regex>/opts";
    
    • Suricata新添加的改动

1 Suricata支持新协议

调用scripts/setup-app-layer.py生成新的基于TCP或者UDP协议的应用层协议解析,比方说,我现在想生成一个叫做IOT的协议,生成新协议支持时默认生成–logger(协议记录日志),–parser(协议解析)。还需要生成buffer,buffer可以理解为对IOT协议里面的协议字段匹配的名称。buffer可以多次调用生成不同的协议。

使用下面的代码,可以看到生成的一些新文件,rust文件夹里面的代码,是协议解析器,detect-iot-iot_header_type文件是C语言注册的Buffer类型。所以这里我们需要首先完成rust协议解析器的部分。

# 调用命令
[root@iZ2ze7889ommtwxghsyd0iZ]/home/ops/vgdog/suricata_2# scripts/setup-app-layer.py --logger --parser --detect IOT iot_header_type
 
# git查看新文件/修改文件,可以看到下面的内容
On branch vgdog/support_iot
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   rust/src/lib.rs
	modified:   src/Makefile.am
	modified:   src/app-layer-parser.c
	modified:   src/app-layer-protos.c
	modified:   src/app-layer-protos.h
	modified:   src/detect-engine-register.h
	modified:   src/output.c
	modified:   suricata.yaml.in

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	rust/src/applayeriot/
	src/detect-iot-iot_header_type.c
	src/detect-iot-iot_header_type.h
	src/output-json-iot.c
	src/output-json-iot.h

no changes added to commit (use "git add" and/or "git commit -a")

1.1 Rust部分代码的编写

打开文件夹rust/src/applayeriot,可以发现里面包含四个文件,每个文件的功能为:

  • mod.rs总结该文件夹下的rust模块如何组织
  • logger.rs针对IOT报文的协议transcation变化或者解析做日志记录,我只做一些type转换的时候记录,其它基本不做什么
  • parser.rs是针对单个IOT报文解析的关键文件,诸如IOT报文的结构都在这里完成,这里的数据结构需要暴露出来给同文件夹下面的其它文件使用。
  • iot.rs是针对IOT报文做transcation转换的代码文件,这里transcation可以理解为请求 & 回答的模式,不过处于简单,我这里认为一个单个报文就是一个完整的transcation,DHCP协议的transcation也是单报文transcation。

将来写针对MPM的从transcation里面提取关键字的时候,还要再增加一个detect.rs,detect.rs负责从一个transcation里面提取出来待匹配的关键字内容。

[root@iZ2ze7889ommtwxghsyd0iZ]/home/ops/vgdog/suricata_2# ll rust/src/applayeriot
total 28
-rw-r--r-- 1 root root  1347 Jul  9 08:58 logger.rs
-rw-r--r-- 1 root root   807 Jul  9 08:58 mod.rs
-rw-r--r-- 1 root root  2022 Jul  9 08:58 parser.rs
-rw-r--r-- 1 root root 15927 Jul  9 08:58 iot.rs

mod.rs内部是对其它组件可见性的定义,其中detect的定义被注释掉,将来写MPM的时候再涉及到这个。

pub mod iot;
pub mod logger;
// pub mod detect;
mod parser;

1.1.1 parser.rs的编写

先完成parser的编写,这里假设IOT协议包含两部分,header和payload,我们基本就是完成两部分的编解码操作。


use nom7::{
    bytes::streaming::take,
    combinator::map_res,
    IResult,
    Needed,
    error::{ErrorKind, ParseError},
    error::Error,
    Err,
    error::make_error,
    bytes::streaming::tag,
};
use std;
use serde_json;
use serde_json::Value;
use serde::{Serialize, Deserialize};

#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct IOTHeader {
    ...
}

#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct IOTPayload {
    ...
}

#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct IOTMessage {
    ...
}

pub fn parse_header(i: &[u8]) -> IResult<&[u8], IOTHeader> {
    // For IOT, first char must be 0x5f, aka 0b01011111
    let (i, _) = tag([0xff])(i)?;
    let (i, version_string) = ...(i)?;
    let (i, iot_type_string) = ...(i)?;
    let iot_header = IOTHeader {
        ...
    };
    return Ok((i, iot_header));
    
}

pub fn parse_payload(i: &[u8]) -> IResult<&[u8], IOTPayload> {
    ...
}

pub fn parse_message(i: &[u8]) -> IResult<&[u8], IOTMessage> {
    let (i, header) = parse_header(i)?;
    let (i, payload) = parse_payload(i)?;
    let message = IOTMessage {
        ...
    };
    return Ok((i, message));
}

1.1.2 对iot.rs的编写

接下来,完成对协议状态转换的代码编写,很多协议transcation一般是一问一答,这么一个来回才算是完成,出于简单维度考虑,这里只实现单个报文就是一个transcation的代码,或者说一个报文就会导致iot完成一个transcation,对应的英文术语为uni-direction。

默认生成的IOTTransaction的结构就是内部包含一个一个request和response,这里内部替换为单个的IOTMessage就可以了。


pub struct IOTTransaction {
    tx_id: u64,
    
    // pub request: Option<String>,
    // pub response: Option<String>,
    pub message: Option<IOTMessage>,
    tx_data: AppLayerTxData,
}

接下来就是每一个IOT报文的消息转换,这里我们直接将IOT报文的具体类型,比方说Hello,Deny啥的报文类型认为是一个TranscationEvent即可。自然就可以得到下面的内容,这里的Event可以理解为transcation发生了状态转换,当transcation发生了状态转换,对tx做匹配就会发生。

这里我们还需要注意的是IOTState,里面保存着一系列相同状态的Transcation,检索事务时,它会匹配出来具体的tx是谁。这里要注意的是,在IOTState里面,完成了对报文的匹配,如果发现了一个报文,就会推动Event的变化,再将Transcation存储进入IOTState里面。

因为只认为包含单个消息,且状态必然转换,因此这里实际上删除掉了用不到的函数

#[derive(AppLayerEvent)]
enum IOTEvent {
    TooManyTransactions,
    IOTHelloEvent,
    IOTHelloResEvent,
    ...
    IOTAlertEvent,
}



impl IOTState {
    ......

    fn parse(&mut self, input: &[u8]) -> AppLayerResult {
        // We're not interested in empty requests.
        if input.is_empty() {
            return AppLayerResult::ok();
        }

        let mut start = input;
        while !start.is_empty() {
            match parser::parse_message(start) {
                Ok((rem, message)) => {
                    start = rem;

                    // SCLogNotice!("Message: {:?}", message);
                    let mut tx = self.new_tx();
                    if self.transactions.len() >= unsafe { IOT_MAX_TX } {
                        tx.tx_data.set_event(IOTEvent::TooManyTransactions as u8);
                    }
                    match message.header.iot_type.as_ref() {
                        "Hello" => tx.tx_data.set_event(IOTEvent::IOTHelloEvent as u8),
                        ...
                        _ => {
                            SCLogNotice!("Inpossible message header cmd not valid: {:?}", message);
                        },
                    }
                    tx.message = Some(message);

                    self.transactions.push_back(tx);
                    if self.transactions.len() >= unsafe { IOT_MAX_TX } {
                        return AppLayerResult::err();
                    }
                }
                Err(nom::Err::Incomplete(_)) => {
                    // Not enough data. This parser doesn't give us a good indication
                    // of how much data is missing so just ask for one more byte so the
                    // parse is called as soon as more data is received.
                    let consumed = input.len() - start.len();
                    let needed = start.len() + 1;
                    return AppLayerResult::incomplete(consumed as u32, needed as u32);
                }
                Err(_) => {
                    return AppLayerResult::err();
                }
            }
        }

        // Input was fully consumed.
        return AppLayerResult::ok();
    }
    ...
}

接下来就要在iot.rs里面写上注册parser,和判断是否为iot协议的代码了。注册parser就是写明白底层是TCP还是UDP,而判断用户态代码就是看一下magic number是否匹配,这里可以严格一些,比方说判断Header是否可以正常解析出来。

rs_iot_probing_parser用于检查报文是不是一个合法的iot报文。

rs_iot_tx_get_alstate_progress用于通知是否发生协议变化。

rs_iot_register_parser就是注册对iot的解析器。

/// C entry point for a probing parser.
unsafe extern "C" fn rs_iot_probing_parser(
    _flow: *const Flow, _direction: u8, input: *const u8, input_len: u32, _rdir: *mut u8,
) -> AppProto {
    // SCLogError!("Inside iot probing parser");
    // Need at least 2 bytes.
    if input_len > 1 && !input.is_null() {
        // SCLogError!("Hit parse input");
        let slice = build_slice!(input, input_len as usize);
        if parse_header(slice).is_ok() {
            // SCLogNotice!("Parse iot message success");
            return ALPROTO_IOT;
        }
    }
    return ALPROTO_UNKNOWN;
}


unsafe extern "C" fn rs_iot_tx_get_alstate_progress(tx: *mut c_void, _direction: u8) -> c_int {
    // uni-direction, stateless parser, simply use 1.
    return 1;
}


#[no_mangle]
pub unsafe extern "C" fn rs_iot_register_parser() {
    let default_port = CString::new("[7000]").unwrap();
    let parser = RustParser {
        name: PARSER_NAME.as_ptr() as *const c_char,
        default_port: default_port.as_ptr(),
        ipproto: IPPROTO_TCP,
        probe_ts: Some(rs_iot_probing_parser),
        probe_tc: Some(rs_iot_probing_parser),
        min_depth: 0,
        max_depth: 16,
        state_new: rs_iot_state_new,
        state_free: rs_iot_state_free,
        tx_free: rs_iot_state_tx_free,
        parse_ts: rs_iot_parse,
        parse_tc: rs_iot_parse,
        get_tx_count: rs_iot_state_get_tx_count,
        get_tx: rs_iot_state_get_tx,
        tx_comp_st_ts: 1,
        tx_comp_st_tc: 1,
        tx_get_progress: rs_iot_tx_get_alstate_progress,
        get_eventinfo: Some(IOTEvent::get_event_info),
        get_eventinfo_byid: Some(IOTEvent::get_event_info_by_id),
        localstorage_new: None,
        localstorage_free: None,
        get_tx_files: None,
        get_tx_iterator: Some(applayer::state_get_tx_iterator::<IOTState, IOTTransaction>),
        get_tx_data: rs_iot_get_tx_data,
        get_state_data: rs_iot_get_state_data,
        apply_tx_config: None,
        flags: APP_LAYER_PARSER_OPT_ACCEPT_GAPS,
        truncate: None,
        get_frame_id_by_name: None,
        get_frame_name_by_id: None,
    };

    let ip_proto_str = CString::new("tcp").unwrap();

    if AppLayerProtoDetectConfProtoDetectionEnabled(ip_proto_str.as_ptr(), parser.name) != 0 {
        let alproto = AppLayerRegisterProtocolDetection(&parser, 1);
        ALPROTO_IOT = alproto;
        if AppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 {
            let _ = AppLayerRegisterParser(&parser, alproto);
        }
        if let Some(val) = conf_get("app-layer.protocols.iot.max-tx") {
            if let Ok(v) = val.parse::<usize>() {
                IOT_MAX_TX = v;
            } else {
                SCLogError!("Invalid value for iot.max-tx");
            }
        }
        SCLogNotice!("Rust iot parser registered.");
    } else {
        SCLogNotice!("Protocol detector and parser disabled for IOT.");
    }
}


对iot.rs最后一步,就是提取具体的buffer,这个buffer可以理解为SPM(单模态匹配)提取关键字,这一步完成对iot.rs的改造就进入了最后一步。


/// Get the header type buffer for a transaction from C.
///
/// No required for parsing, but an example function for retrieving a
/// pointer to the request buffer from C for detection.
#[no_mangle]
pub unsafe extern "C" fn rs_iot_get_header_type_buffer(
    tx: *mut c_void, buf: *mut *const u8, len: *mut u32,
) -> u8 {
    let tx = cast_pointer!(tx, IOTTransaction);
    if let Some(ref message) = tx.message {
        if !message.header.iot_type.is_empty() {
            *len = message.header.iot_type.len() as u32;
            *buf = message.header.iot_type.as_ptr();
            return 1;
        }
    }
    return 0;
}

1.1.3 对logger.rs的编写

logger.rs就没什么好说了,可以认为就是记录一个json对象,只需要状态转变时记录一下内容即可


fn log_iot(tx: &IOTTransaction, js: &mut JsonBuilder) -> Result<(), JsonError> {
    js.open_object("iot")?;
    if let Some(ref message) = tx.message {
        match message.header.iot_type.as_ref() {
            "Hello" => js.set_string("type", "Connect")?,
            ...
            _ => js.set_string("cmd", "unknown")?
        };
    }
    ...
    js.close()?;
    Ok(())
}

1.1.4 Rust部分代码的编译和测试

Rust要求代码必须都正常编译才能执行具体的测试,所以直接在parser.rs里面写个UT,然后进入Suricata下面的Rust目录,执行Cargo test就可以测试代码是否正确了,比方说我的代码里面有一个编码网络字节序的函数,那么我就写上一个编码比较即可。


#[cfg(test)]
mod tests {
    use super::*;
    use nom7::Err;
    use std::time::{Instant, Duration};

    #[test]
    fn test_encode_bigendian_u16() {
        let size = 2;
...
        let u16_1024 = 1024;
        let expect_u16_1024_bigendian: [u8; 2] = [0x04, 0x00];
...
        match encode_bigendian_u16(u16_1024) {
            Ok((_, array)) => {
                assert_eq!(&array[..size], &expect_u16_1024_bigendian);
            },
            Err(e) => panic!("Encoding u16 1024 failed: {:?}", e),
        }
    }
}

这种调用cargo test test_encode_bigendian_u16,如果函数编写都正常,test也没问题就会出现下面的内容

warning: `suricata` (lib test) generated 27 warnings (run `cargo fix --lib -p suricata --tests` to apply 20 suggestions)
    Finished test [unoptimized + debuginfo] target(s) in 9.78s
     Running unittests src/lib.rs (target/debug/deps/suricata-08d6b4a748e9dd8e)

running 1 test
test applayeriot::parser::tests::test_encode_bigendian_u16 ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 430 filtered out; finished in 0.00s

1.2 C语言部分代码的编写

现在我们回来看C语言部分的代码怎么写,还有如下文件需要修改。

使用git diff就可以发现,这里diff的的文件实际上是加了一些通用的定义或者声明:

  • 比方说定义protocol iot的({ ALPROTO_IOT, “iot” },)
  • 注册协议解析器rs_iot_register_parser
  • 注册Detection Buffer :DETECT_AL_IOT_IOT_HEADER_TYPE
  • 注册日志JsonIOTLogRegister

真正需要做的东西要么在untracked files里面,要么没有显示在diff文件里面。我们一点一点看

[root@iZ2ze7889ommtwxghsyd0iZ]/home/ops/vgdog/suricata_2# git status
On branch vgdog/support_iot
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   src/Makefile.am
        modified:   src/app-layer-parser.c
        modified:   src/app-layer-protos.c
        modified:   src/app-layer-protos.h
        modified:   src/detect-engine-register.h
        modified:   src/output.c
        modified:   suricata.yaml.in

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        src/detect-iot-iot_header_type.c
        src/detect-iot-iot_header_type.h
        src/output-json-iot.c
        src/output-json-iot.h

no changes added to commit (use "git add" and/or "git commit -a")

先看Detection Buffer注册解析器,函数为

// 注册解析器
void DetectIOTiot_header_typeRegister(void)
{
    sigmatch_table[DETECT_AL_IOT_BUFFER].name = "iot_iot_header_type";
    sigmatch_table[DETECT_AL_IOT_BUFFER].desc =
            "IOT content modifier to match on the iot buffers";
    sigmatch_table[DETECT_AL_IOT_BUFFER].Setup = DetectIOTiot_header_typeSetup;
#ifdef UNITTESTS
    sigmatch_table[DETECT_AL_IOT_BUFFER].RegisterTests = DetectIOTiot_header_typeRegisterTests;
#endif
    sigmatch_table[DETECT_AL_IOT_BUFFER].flags |= SIGMATCH_NOOPT;

    /* register inspect engines */
    DetectAppLayerInspectEngineRegister("iot_buffer", ALPROTO_IOT, SIG_FLAG_TOSERVER, 0,
            DetectEngineInspectIOTiot_header_type, NULL);
    DetectAppLayerInspectEngineRegister("iot_buffer", ALPROTO_IOT, SIG_FLAG_TOCLIENT, 0,
            DetectEngineInspectIOTiot_header_type, NULL);

    g_iot_rust_id = DetectBufferTypeGetByName("iot_buffer");

    SCLogNotice("IOT application layer detect registered.");
}

//真正的解析器
static uint8_t DetectEngineInspectIOTiot_header_type(DetectEngineCtx *de_ctx,
        DetectEngineThreadCtx *det_ctx, const struct DetectEngineAppInspectionEngine_ *engine,
        const Signature *s, Flow *f, uint8_t flags, void *alstate, void *txv, uint64_t tx_id)
{
    uint8_t ret = DETECT_ENGINE_INSPECT_SIG_NO_MATCH;
    const uint8_t *data = NULL;
    uint32_t data_len = 0;

    if (flags & STREAM_TOSERVER) {
        rs_iot_get_request_buffer(txv, &data, &data_len);
    } else if (flags & STREAM_TOCLIENT) {
        rs_iot_get_response_buffer(txv, &data, &data_len);
    }

    if (data != NULL) {
        const bool match = DetectEngineContentInspection(de_ctx, det_ctx, s, engine->smd, NULL, f,
                data, data_len, 0, DETECT_CI_FLAGS_SINGLE,
                DETECT_ENGINE_CONTENT_INSPECTION_MODE_STATE);
        if (match) {
            ret = DETECT_ENGINE_INSPECT_SIG_MATCH;
        }
    }

    SCLogNotice("Returning %u.", ret);
    return ret;
}

有很多地方需要改动:

  • ”注册解析器“函数里面将通用的DETECT_AL_IOT_BUFFER改为具体的Detection Buffer名字,这里比方说我要检查的是IOT里面Header的type字段,那就改名为DETECT_AL_IOT_IOT_HEADER_TYPE
  • ”注册解析器“函数里面把iot_buffer这个通用的名字,改成具体的类型名字
  • ”真正的解析器“里面,把rs_iot_get_request_buffer,改成我们刚才在rust里面写的提取函数
  • 在两个函数之后,写上对IOT报文范例解析的Unit Test。

这两个地方改完了就高枕无忧,万事大吉了吗?并非如此还有如下操作要做:

  • untracked files的两个文件实际上没在编译路径里面,还需要添加到Makefile.am里面
  • DetectIOTiot_header_typeRegister函数需要添加到SigTableSetup函数里,注册这个detection buffer之后才能让signature做真正的匹配。这里记得把函数的头文件加到代码里面,否则就会报错

上面修改完成了,Suricata的SPM匹配就算是完成了,调用make执行构建即可。

因为我们开启了Unittest,所以可以通过验证UnitTest是否运行正常,来判断我们的代码是否正确

# 好的,这里看来我们新加入的Unit Test正确地注册了
[root@iZ2ze7889ommtwxghsyd0iZ]/home/ops/vgdog/suricata_2# ./src/suricata --list-unittests | grep IOT
Notice: detect-iot-iot_header_type: IOT application layer detect registered. [DetectIOTiot_header_typeRegister:detect-iot-iot_header_type.c:73]
Notice: output-json-iot: IOT JSON logger registered. [JsonIOTLogRegister:output-json-iot.c:170]
DetectIOTiot_header_typeTest

# 好的,看来我们的UT正确执行了
[root@iZ2ze7889ommtwxghsyd0iZ]/home/ops/vgdog/suricata_2# ./src/suricata -u -U DetectIOTiot_header_typeTest
Notice: limesh: Rust limesh parser registered. [suricata::applayerlimesh::limesh::rs_limesh_register_parser:limesh.rs:409]
Notice: iot: Rust iot parser registered. [suricata::applayeriot::iot::rs_iot_register_parser:iot.rs:338]

...

pass
==== TEST RESULTS ====
PASSED: 1
FAILED: 0
======================

2 Suricata支持多模式匹配

3 Suricata规则加载流程

规则处理顺序

按照顺序注册Signature注册函数,先注册的比较函数会挂载到比较函数前面,

SigLoadSignatures {
	...
  SCSigRegisterSignatureOrderingFuncs {
  	# 挂载一串函数到de_ctx->sc_sig_order_funcs,后面挂载的后面用。先挂载OrderByAction,再挂载OrderByPriority
  	SCSigRegisterSignatureOrderingFunc(de_ctx, SCSigOrderByActionCompare);
  	...
  	SCSigRegisterSignatureOrderingFunc(de_ctx, SCSigOrderByPriorityCompare);
  }
  ...
  SCSigOrderSignatures {
  	# 排序,递归比较时使用de_ctx->sc_sig_order_funcs上面注册的顺序判断,用第一个能判断出来大小的比较。都相等的话用SID比较小的ID在前面。最终挂载到检测上下文det_ctx
  	sigw_list = SCSigOrder(sigw_list, de_ctx->sc_sig_order_funcs)
  	...
  }
  ...
  # 真正构建signature组
  # 需了解SGH和MPM的含义,参考https://forum.suricata.io/t/mpm-context-explanation/1463/3
  # SGH理解为Signature HEAD Group,共享pattern的一堆signature
  SigGroupBuild {
  	# 针对每条规则使用RetrieveFPForSig提取single pattern,用于后面构建MPM。这里注意s->init_data->mpm_sm->ctx,这个就是fast pattern。
  	SigPrepareStage1
  	# 按照TCP/UDP/IP做大的分组。开始初步分配sgh
  	SigPrepareStage2
  	# 针对无法按照协议,端口,地址分类构建SGH分组
  	SigPrepareStage3
  	# 在这里真正调用每个detection buffer注册的mpm & spm的函数,挂载prefilter engine。开始在sgh的tx_engine里面挂prefilter engine
  	SigPrepareStage4 {
  		PrefilterSetupRuleGroup {
  			PatternMatchPrepareGroup {
  				# 给已经建好的sgh的pktpayload分配mpm_store结构,包着一个mpm_ctx,准备对per sig附加signature
  				MpmStorePrepareBuffer
  				# 注册pktpayload的mpm
  				PrefilterPktPayloadRegister {
  					# 每个函数
  					PrefilterAppendPayloadEngine
  				}
  				# 该注册applayer的tx mpm了
  				PrepareMpms {
  					for 遍历 DetectBufferMpmRegistry *a = de_ctx->app_mpms_list {
  						# 如果有对应的同样direction呀,协议呀啥的mpm直接利用mpm。把自己的fast pattern加到mpm ctx里面
							MpmStorePrepareBufferAppLayer {
								# 这里开始将每个提取出来的fp添加到de_ctx的mpm store里面,可以理解为往hyperscan的db里面添加patter,存储id
								MpmStoreSetup {
									# 
									for s = de_ctx->sig_array[sig] {
										# 添加fp
										PopulateMpmHelperAddPattern		
									}
								}
								# 把内部mpmstore加到de_ctx->mpm_hash_table
								MpmStoreAdd
							}
							# 这里实际上是spm,单模匹配加函数
              a->PrefilterRegisterWithListId(PrefilterGenericMpmRegister) {
                # 挂载到sgh->init->tx_engines上面
                PrefilterAppendTxEngine {
                  # 分配prefilter引擎,附加到de_ctx的enginelist
                }
              }					
  					}
  				}
  			}
  		}
  	}
  	...
  }
}

至于规则自己,为了支持MPM需要,下面在不断的注册具体的Detection Buffer的MPM引擎(可以理解为注册回调函数用于提取内容)。

DetectAppLayerMpmRegister {
	# 注册PrefilterRegisterWithListId,实际上注册的是PrefilterGenericMpmRegister。PrefilterRegisterWithListId在PrepareMpms里面调用
	# PrefilterGenericMpmRegister在运行时调用PrefilterAppendTxEngine,然后调用PrefilterGenericMpmFree
	
	# 这里挂载buffer id到sm list,用于一会拿fastpattern
	SupportFastPatternForSigMatchList
}

4 suricata检测模式

真正的检测流程如下,这里注意我写的是针对app layer的检测函数。这里注意一点上面是PrepareMpms,这里是PrepareMpm别搞混了。。。

// 参考src/detect-engine-prefilter.c

// 单模式匹配用的是DetectRunPrefilterTx
DetectFlow {
	DetectRun {
		DetectRunTx {
			# 判断下是否发生了一次完整的事务,
			GetDetectTx {
				# 调用prefilterengine来先过滤一批检测,这里实际上调用的是engine->cb.PrefilterTx来处理函数,这里的engine->pectx是一个mpm_ctx
        DetectRunPrefilterTx {
        	# 这里才是真正的执行MPM prefilter的地方
        	engine->cb.PrefilterTx(PrefilterMpm) {
        		# 这里实际上是真正执行search的操作,说白了就是hyperscan的多模式匹配
        		mpm_table[mpm_ctx->mpm_type].Search
        	}
        	# 已经确定了命中的规则后,这里来了一次quicksort排序
        	QuickSortSigIntId
        }
        # 请注意,这里返回的时候实际上是prefilter,还都是所有的规则都有的状态,也就是说即使命中的规则的action是pass,也有返回值
        # 这里开始单条单条匹配,逐个规则判断是不是命中。
        for (uint32_t i = 0; i < array_idx {
        	# 逐条检测命中,如果命中了还要把det_ctx->alert_queue
					DetectRunTxInspectRule{
						# 追加alert,增加alert_queue_size,追加alert到alert_queue
						AlertQueueAppend
        	}	
        }
        	
			}
			...
			# 这里开始整理alert了
			DetectRunPostRules {
        #在这里处理了pass类型的函数
        PacketAlertFinalize {
        	# 先排序,这个排序
    			qsort(det_ctx->alert_queue, det_ctx->alert_queue_size, sizeof(PacketAlert),
            AlertQueueSortHelper);
        }
      }
		}
	}
}

结尾

唉,尴尬

狗头的赞赏码.jpg