Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Applayer plugin 5053 v9 #12465

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,16 @@ jobs:
cat eve.json | jq -c 'select(.dns)'
test $(cat eve.json | jq -c 'select(.dns)' | wc -l) = "1"

- name: Test app-layer plugin
working-directory: examples/plugins/altemplate
# test with RUSTFLAGS different than suricata build to test runtime compatibility
run: |
RUSTFLAGS="--cfg fuzzing -Cdebuginfo=1 -Cforce-frame-pointers -Cpasses=sancov-module -Cllvm-args=-sanitizer-coverage-level=4 -Cllvm-args=-sanitizer-coverage-trace-compares -Cllvm-args=-sanitizer-coverage-inline-8bit-counters -Cllvm-args=-sanitizer-coverage-trace-geps -Cllvm-args=-sanitizer-coverage-prune-blocks=0 -Cllvm-args=-sanitizer-coverage-pc-table -Clink-dead-code -Cllvm-args=-sanitizer-coverage-stack-depth" cargo build
../../../src/suricata -S altemplate.rules --set plugins.0=./target/debug/libsuricata_altemplate.so --runmode=single -l . -c altemplate.yaml -k none -r ../../../rust/src/applayertemplate/template.pcap
cat eve.json | jq -c 'select(.altemplate)'
test $(cat eve.json | jq -c 'select(.altemplate)' | wc -l) = "3"
# we get 2 alerts and 1 altemplate events

- name: Test library build in tree
working-directory: examples/lib/simple
run: make clean all
Expand Down
60 changes: 58 additions & 2 deletions doc/userguide/devguide/libsuricata/index.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.. _libsuricata:

LibSuricata
===========
LibSuricata and Plugins
=======================

Using Suricata as a Library
---------------------------
Expand All @@ -10,5 +10,61 @@ The ability to turn Suricata into a library that can be utilized in other tools
is currently a work in progress, tracked by Redmine Ticket #2693:
https://redmine.openinfosecfoundation.org/issues/2693.

Plugins
-------

A related work are Suricata plugins, also in progress and tracked by Redmine
Ticket #4101: https://redmine.openinfosecfoundation.org/issues/4101.

Plugins can be used by modifying suricata.yaml ``plugins`` section to include
the path of the dynamic library to load.

Plugins should export a ``SCPluginRegister`` function that will be the entry point
used by Suricata.

Application-layer plugins
~~~~~~~~~~~~~~~~~~~~~~~~~

Application layer plugins can be added as demonstrated by example
https://github.com/OISF/suricata/blob/master/examples/plugins/altemplate/

The plugin code contains the same files as an application layer in the source tree:
- alname.rs
- detect.rs
- lib.rs
- log.rs
- parser.rs

These files will have different ``use`` statements, targetting the suricata crate.

.. attention:: A plugin should not use rust structures from suricata crate if they are not repr(C), especially JsonBuilder.

This is because the rust compiler does not guarantee the structure layout unless you specify this representation.
Thus, the plugin may expect the ``JsonBuilder`` fields at different offsets that they are supplied by Suricata at runtime.
The solution is to go through the ``JsonBuilder`` C API which uses an opaque pointer.

And the plugin contains also additional files:
- plugin.rs : defines the entry point of the plugin ``SCPluginRegister``

``SCPluginRegister`` should register callback that should then call ``SCPluginRegisterAppLayer``
passing a ``SCAppLayerPlugin`` structure to suricata.
It should also call ``suricata::plugin::init();`` to ensure the plugin has initialized
its value of the Suricata Context, a structure needed by rust, to call some C functions,
that cannot be found at compile time because of circular dependencies, and are therefore
resolved at runtime.

This ``SCAppLayerPlugin`` begins by a version number ``SC_PLUGIN_API_VERSION`` for runtime compatibility
between Suricata and the plugin.

Known limitations are:

- Plugins can only use simple logging as defined by ``EveJsonSimpleTxLogFunc``
without suricata.yaml configuration, see https://github.com/OISF/suricata/pull/11160
- Keywords cannot use validate callbacks, see https://redmine.openinfosecfoundation.org/issues/5634
- Plugins cannot have keywords matching on mulitple protocols (like ja4),
see https://redmine.openinfosecfoundation.org/issues/7304

.. attention:: A pure rust plugin needs to be compiled with ``RUSTFLAGS=-Clink-args=-Wl,-undefined,dynamic_lookup``

This is because the plugin will link dynamically at runtime the functions defined in Suricata runtime.
You can define this rust flags in a ``.cargo/config.toml`` file.
5 changes: 5 additions & 0 deletions examples/plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ is useful if you want to send EVE output to custom destinations.

A minimal capture plugin that can be used as a template, but also used
for testing capture plugin loading and registration in CI.

## altemplate

An app-layer template plugin with logging and detection.
Most code copied from rust/src/applayertemplate
3 changes: 3 additions & 0 deletions examples/plugins/altemplate/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build]
# custom flags to pass to all compiler invocations
rustflags = ["-Clink-args=-Wl,-undefined,dynamic_lookup"]
16 changes: 16 additions & 0 deletions examples/plugins/altemplate/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "suricata-altemplate"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
nom7 = { version="7.0", package="nom" }
libc = "~0.2.82"
suricata = { path = "../../../rust/" }

[features]
default = ["suricata8"]
suricata8 = []
2 changes: 2 additions & 0 deletions examples/plugins/altemplate/altemplate.rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
alert altemplate any any -> any any (msg:"TEST"; altemplate.buffer; content:"Hello"; flow:established,to_server; sid:1; rev:1;)
alert altemplate any any -> any any (msg:"TEST"; altemplate.buffer; content:"Bye"; flow:established,to_client; sid:2; rev:1;)
17 changes: 17 additions & 0 deletions examples/plugins/altemplate/altemplate.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
%YAML 1.1
---

outputs:
- eve-log:
enabled: yes
types:
- altemplate
- alert
- flow

app-layer:
protocols:
altemplate:
enabled: yes
detection-ports:
dp: 7000
105 changes: 105 additions & 0 deletions examples/plugins/altemplate/src/detect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/* Copyright (C) 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.
*/

// same file as rust/src/applayertemplate/detect.rs except
// TEMPLATE_START_REMOVE removed
// different paths for use statements
// keywords prefixed with altemplate instead of just template

use super::template::{TemplateTransaction, ALPROTO_TEMPLATE};
use std::os::raw::{c_int, c_void};
use suricata::cast_pointer;
use suricata::detect::{
DetectBufferSetActiveList, DetectHelperBufferMpmRegister, DetectHelperGetData,
DetectHelperKeywordRegister, DetectSignatureSetAppProto, SCSigTableElmt,
SIGMATCH_INFO_STICKY_BUFFER, SIGMATCH_NOOPT,
};
use suricata::direction::Direction;

static mut G_TEMPLATE_BUFFER_BUFFER_ID: c_int = 0;

unsafe extern "C" fn template_buffer_setup(
de: *mut c_void, s: *mut c_void, _raw: *const std::os::raw::c_char,
) -> c_int {
if DetectSignatureSetAppProto(s, ALPROTO_TEMPLATE) != 0 {
return -1;
}
if DetectBufferSetActiveList(de, s, G_TEMPLATE_BUFFER_BUFFER_ID) < 0 {
return -1;
}
return 0;
}

/// Get the request/response buffer for a transaction from C.
unsafe extern "C" fn template_buffer_get_data(
tx: *const c_void, flags: u8, buf: *mut *const u8, len: *mut u32,
) -> bool {
let tx = cast_pointer!(tx, TemplateTransaction);
if flags & Direction::ToClient as u8 != 0 {
if let Some(ref response) = tx.response {
*len = response.len() as u32;
*buf = response.as_ptr();
return true;
}
} else if let Some(ref request) = tx.request {
*len = request.len() as u32;
*buf = request.as_ptr();
return true;
}
return false;
}

unsafe extern "C" fn template_buffer_get(
de: *mut c_void, transforms: *const c_void, flow: *const c_void, flow_flags: u8,
tx: *const c_void, list_id: c_int,
) -> *mut c_void {
return DetectHelperGetData(
de,
transforms,
flow,
flow_flags,
tx,
list_id,
template_buffer_get_data,
);
}

#[no_mangle]
pub unsafe extern "C" fn ScDetectTemplateRegister() {
// TODO create a suricata-verify test
// Setup a keyword structure and register it
let kw = SCSigTableElmt {
name: b"altemplate.buffer\0".as_ptr() as *const libc::c_char,
desc: b"Template content modifier to match on the template buffer\0".as_ptr()
as *const libc::c_char,
// TODO use the right anchor for url and write doc
url: b"/rules/template-keywords.html#buffer\0".as_ptr() as *const libc::c_char,
Setup: template_buffer_setup,
flags: SIGMATCH_NOOPT | SIGMATCH_INFO_STICKY_BUFFER,
AppLayerTxMatch: None,
Free: None,
};
let _g_template_buffer_kw_id = DetectHelperKeywordRegister(&kw);
G_TEMPLATE_BUFFER_BUFFER_ID = DetectHelperBufferMpmRegister(
b"altemplate.buffer\0".as_ptr() as *const libc::c_char,
b"template.buffer intern description\0".as_ptr() as *const libc::c_char,
ALPROTO_TEMPLATE,
true, //toclient
true, //toserver
template_buffer_get,
);
}
5 changes: 5 additions & 0 deletions examples/plugins/altemplate/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod detect;
mod log;
mod parser;
pub mod plugin;
mod template;
85 changes: 85 additions & 0 deletions examples/plugins/altemplate/src/log.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/* Copyright (C) 2018 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.
*/

// same file as rust/src/applayertemplate/logger.rs except
// different paths for use statements
// open_object using altemplate instead of just template
// Jsonbuilder using C API due to opaque implementation

use super::template::TemplateTransaction;
use std::ffi::{c_char, CString};
use suricata::cast_pointer;
use suricata::jsonbuilder::JsonError;

use std;

// Jsonbuilder opaque with implementation using C API to feel like usual
#[repr(C)]
pub struct JsonBuilder {
_data: [u8; 0],
}

extern "C" {
pub fn jb_set_string(jb: &mut JsonBuilder, key: *const c_char, val: *const c_char) -> bool;
pub fn jb_close(jb: &mut JsonBuilder) -> bool;
pub fn jb_open_object(jb: &mut JsonBuilder, key: *const c_char) -> bool;
}

impl JsonBuilder {
pub fn close(&mut self) -> Result<(), JsonError> {
if unsafe { !jb_close(self) } {
return Err(JsonError::Memory);
}
Ok(())
}
pub fn open_object(&mut self, key: &str) -> Result<(), JsonError> {
let keyc = CString::new(key).unwrap();
if unsafe { !jb_open_object(self, keyc.as_ptr()) } {
return Err(JsonError::Memory);
}
Ok(())
}
pub fn set_string(&mut self, key: &str, val: &str) -> Result<(), JsonError> {
let keyc = CString::new(key).unwrap();
let valc = CString::new(val.escape_default().to_string()).unwrap();
if unsafe { !jb_set_string(self, keyc.as_ptr(), valc.as_ptr()) } {
return Err(JsonError::Memory);
}
Ok(())
}
}

fn log_template(tx: &TemplateTransaction, js: &mut JsonBuilder) -> Result<(), JsonError> {
js.open_object("altemplate")?;
if let Some(ref request) = tx.request {
js.set_string("request", request)?;
}
if let Some(ref response) = tx.response {
js.set_string("response", response)?;
}
js.close()?;
Ok(())
}

#[no_mangle]
pub unsafe extern "C" fn rs_template_logger_log(
tx: *const std::os::raw::c_void, js: *mut std::os::raw::c_void,
) -> bool {
let tx = cast_pointer!(tx, TemplateTransaction);
let js = cast_pointer!(js, JsonBuilder);
log_template(tx, js).is_ok()
}
Loading
Loading