diff --git a/agent/src/bpf/audit.bpf.c b/agent/src/bpf/audit.bpf.c index a6b12b9..7b1a35f 100644 --- a/agent/src/bpf/audit.bpf.c +++ b/agent/src/bpf/audit.bpf.c @@ -10,6 +10,14 @@ bpf_trace_printk ("%s: " format, sizeof("%s: " format), \ __PRETTY_FUNCTION__, __VA_ARGS__) +#define MAX_EVENTS 16 + +struct crypto_auditing_data { + char *key_ptr; + void *value_ptr; + long value_size; +}; + struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 4096 /* one page */); @@ -226,6 +234,126 @@ record_blob_data (struct pt_regs *ctx, long context, const char *key_ptr) return err; } +/* This is similar to record_blob_data but always assumes VALUE_PTR is + * present. + */ +static __always_inline int +record_blob_data_explicit (struct pt_regs *ctx, long context, const char *key_ptr, + const void *value_ptr, long value_size) +{ + int err; + + struct audit_blob_data_event_st *event = + bpf_ringbuf_reserve (&ringbuf, + sizeof(struct audit_blob_data_event_st), + 0); + if (!event) + { + DEBUG ("unable to allocate ringbuf entry: %ld\n", -ENOMEM); + return -ENOMEM; + } + + populate_event_header (&event->base.header, + AUDIT_EVENT_DATA, + sizeof(*event), + context); + + event->base.type = AUDIT_DATA_BLOB; + err = bpf_probe_read_user_str (event->base.key, KEY_SIZE, (void *)key_ptr); + if (err < 0) + { + DEBUG ("unable to read event key: %ld\n", err); + goto error; + } + + if (value_size > 0) + { + value_size &= (VALUE_SIZE - 1); + err = bpf_probe_read_user (event->value, value_size, (void *)value_ptr); + if (err < 0) + { + DEBUG ("unable to read event data: %ld\n", err); + goto error; + } + } + + event->size = value_size; + + bpf_ringbuf_submit (event, 0); + return 0; + + error: + bpf_ringbuf_discard (event, 0); + return err; +} + +static __always_inline int +record_data (struct pt_regs *ctx, + long context, + const struct crypto_auditing_data *array_ptr, + long array_size) +{ + struct crypto_auditing_data events[MAX_EVENTS]; + size_t events_size = array_size & (MAX_EVENTS - 1); + int err = 0; + + err = bpf_probe_read_user (events, + events_size * sizeof (struct crypto_auditing_data), + array_ptr); + if (err < 0) + { + DEBUG ("unable to read from data array: %ld\n", err); + return err; + } + + for (long i = 0; i < events_size; i++) + { + switch (events[i].value_size) + { + case -2: + err = record_word_data (ctx, context, + events[i].key_ptr, + (long) events[i].value_ptr); + if (err < 0) + DEBUG ("unable to process word data: %ld\n", err); + break; + + case -1: + err = record_string_data (ctx, context, + events[i].key_ptr, + (const char *) events[i].value_ptr); + if (err < 0) + DEBUG ("unable to process string data: %ld\n", err); + break; + + default: + err = record_blob_data_explicit (ctx, context, + events[i].key_ptr, + events[i].value_ptr, + events[i].value_size); + if (err < 0) + DEBUG ("unable to process blob data: %ld\n", err); + break; + } + } + + return err; +} + +static __always_inline int +record_new_context_with_data (struct pt_regs *ctx, long context, long parent, + struct crypto_auditing_data *array_ptr, + long array_size) +{ + int err; + + err = record_new_context (ctx, context, parent); + if (err < 0) + return err; + + return record_data (ctx, context, array_ptr, array_size); +} + SEC("usdt") int BPF_USDT(new_context, long context, long parent) @@ -255,4 +383,21 @@ BPF_USDT(blob_data, long context, const char *key_ptr) return record_blob_data(ctx, context, key_ptr); } +SEC("usdt") +int +BPF_USDT(data, long context, const struct crypto_auditing_data *array_ptr, + long array_size) +{ + return record_data(ctx, context, array_ptr, array_size); +} + +SEC("usdt") +int +BPF_USDT(new_context_with_data, long context, long parent, + void *array_ptr, long array_size) +{ + return record_new_context_with_data(ctx, context, parent, array_ptr, + array_size); +} + char LICENSE[] SEC("license") = "GPL"; diff --git a/agent/src/main.rs b/agent/src/main.rs index 85c46c6..4618c77 100644 --- a/agent/src/main.rs +++ b/agent/src/main.rs @@ -146,6 +146,24 @@ fn main() -> Result<(), Box> { ) { links.push(link); } + let prog = progs.data(); + if let Ok(link) = prog.attach_usdt( + -1, // any process + library, + "crypto_auditing", + "data", + ) { + links.push(link); + } + let prog = progs.new_context_with_data(); + if let Ok(link) = prog.attach_usdt( + -1, // any process + library, + "crypto_auditing", + "new_context_with_data", + ) { + links.push(link); + } } let cipher = Cipher::aes_128_ecb(); diff --git a/agent/tests/composite.rs b/agent/tests/composite.rs new file mode 100644 index 0000000..3377704 --- /dev/null +++ b/agent/tests/composite.rs @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2023 The crypto-auditing developers. + +extern crate agenttest; +use agenttest::*; + +use crypto_auditing::types::{Event, EventGroup}; +use libc::{c_long, c_uchar, c_void}; +use probe::probe; +use serde_cbor::de::Deserializer; +use std::env; +use std::path::PathBuf; +use std::process::{Child, Command}; +use std::thread; +use std::time::Duration; +use tempfile::tempdir; + +fn target_dir() -> PathBuf { + env::current_exe() + .ok() + .map(|mut path| { + path.pop(); + if path.ends_with("deps") { + path.pop(); + } + path + }) + .unwrap() +} + +fn fixture_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .join("fixtures") +} + +struct AgentProcess(Child); + +impl Drop for AgentProcess { + fn drop(&mut self) { + self.0.kill().expect("unable to kill event-broker"); + } +} + +#[repr(C)] +struct EventData { + key_ptr: *const c_uchar, + value_ptr: *const c_void, + value_size: c_long, +} + +#[test] +fn test_probe_composite() { + bump_memlock_rlimit().expect("unable to bump memlock rlimit"); + + let agent_path = target_dir().join("crypto-auditing-agent"); + let log_dir = tempdir().expect("unable to create temporary directory"); + let log_path = log_dir.path().join("agent.log"); + let process = Command::new(&agent_path) + .arg("-c") + .arg(fixture_dir().join("conf").join("agent.conf")) + .arg("--log-file") + .arg(&log_path) + .arg("--library") + .arg(&env::current_exe().unwrap()) + .spawn() + .expect("unable to spawn agent"); + + // Make sure the agent process will be killed at exit + let process = AgentProcess(process); + + // Wait until the agent starts up + for _ in 0..5 { + if log_path.exists() { + break; + } + thread::sleep(Duration::from_millis(100)); + } + assert!(log_path.exists()); + + let foo = String::from("foo\0"); + let bar = String::from("bar\0"); + let baz = String::from("baz\0"); + let events = vec![ + EventData { + key_ptr: foo.as_ptr(), + value_ptr: 3 as *const c_void, + value_size: -2, + }, + EventData { + key_ptr: bar.as_ptr(), + value_ptr: bar.as_ptr() as *const c_void, + value_size: -1, + }, + EventData { + key_ptr: baz.as_ptr(), + value_ptr: baz.as_ptr() as *const c_void, + value_size: baz.len() as i64, + }, + ]; + + let (_link, object) = + attach_bpf(&process.0, &agent_path).expect("unable to attach agent.bpf.o"); + let map = object.map("ringbuf").expect("unable to get ringbuf map"); + + let timeout = Duration::from_secs(10); + + let result = with_ringbuffer( + map, + || { + probe!(crypto_auditing, new_context, 1, 2); + }, + timeout, + ) + .expect("unable to exercise probe points"); + assert_eq!(result, 1); + + let result = with_ringbuffer( + map, + || { + probe!(crypto_auditing, data, 1, events.as_ptr(), events.len()); + }, + timeout, + ) + .expect("unable to exercise probe points"); + assert_eq!(result, 1); + + let result = with_ringbuffer( + map, + || { + probe!(crypto_auditing, new_context, 4, 5); + }, + timeout, + ) + .expect("unable to exercise probe points"); + assert_eq!(result, 1); + + let log_file = std::fs::File::open(&log_path) + .expect(&format!("unable to read file `{}`", log_path.display())); + + let groups: Result, _> = Deserializer::from_reader(&log_file) + .into_iter::() + .collect(); + let groups = groups.expect("error deserializing"); + assert_eq!(groups.len(), 5); + assert_eq!(groups[0].events().len(), 1); + assert!(matches!(groups[0].events()[0], Event::NewContext { .. })); + assert_eq!(groups[1].events().len(), 1); + if let Event::Data { key, .. } = &groups[1].events()[0] { + assert_eq!(key, "foo"); + } else { + unreachable!(); + } + assert_eq!(groups[2].events().len(), 1); + if let Event::Data { key, .. } = &groups[2].events()[0] { + assert_eq!(key, "bar"); + } else { + unreachable!(); + } + assert_eq!(groups[3].events().len(), 1); + if let Event::Data { key, .. } = &groups[3].events()[0] { + assert_eq!(key, "baz"); + } else { + unreachable!(); + } + assert_eq!(groups[4].events().len(), 1); + assert!(matches!(groups[4].events()[0], Event::NewContext { .. })); +} diff --git a/dist/audit.h b/dist/audit.h index 1906c2c..d537d13 100644 --- a/dist/audit.h +++ b/dist/audit.h @@ -33,11 +33,59 @@ # define CRYPTO_AUDITING_BLOB_DATA(context, key_ptr, value_ptr, value_size) \ DTRACE_PROBE4(crypto_auditing, blob_data, context, key_ptr, value_ptr, value_size) +struct crypto_auditing_data { + char *key_ptr; + void *value_ptr; + long value_size; +}; + +# define CRYPTO_AUDITING_WORD(key_ptr, value_ptr) \ + { (char *)(key_ptr), (void *)(intptr_t)(value_ptr), -2 } +# define CRYPTO_AUDITING_STRING(key_ptr, value_ptr) \ + { (char *)(key_ptr), (void *)(value_ptr), -1 } +# define CRYPTO_AUDITING_BLOB(key_ptr, value_ptr, value_size) \ + { (char *)(key_ptr), (void *)(value_ptr), value_size } + +/* Assert multiple events (16 at maxiumum) at once as a typed + * array. The VALUE_SIZE field of each element indicates the type of + * event: -2 means a word, -1 means a NUL-terminated string, and any + * other value means a blob with the length of VALUE_SIZE. + */ +# define CRYPTO_AUDITING_DATA(context, array_ptr, array_size) \ + DTRACE_PROBE3(crypto_auditing, data, context, array_ptr, array_size) + +# define CRYPTO_AUDITING_DATAV(context, ...) ({ \ + struct crypto_auditing_data __crypto_auditing_events[] = { __VA_ARGS__ }; \ + CRYPTO_AUDITING_DATA(context, \ + __crypto_auditing_events, \ + sizeof (__crypto_auditing_events) / sizeof (__crypto_auditing_events[0])); \ +}) + +/* Introduce a new context CONTEXT, derived from PARENT, as well as + * assert multiple events. + */ +# define CRYPTO_AUDITING_NEW_CONTEXT_WITH_DATA(context, parent, array_ptr, array_size) \ + DTRACE_PROBE4(crypto_auditing, new_context_with_data, context, parent, array_ptr, array_size) + +# define CRYPTO_AUDITING_NEW_CONTEXT_WITH_DATAV(context, parent, ...) ({ \ + struct crypto_auditing_data __crypto_auditing_events[] = { __VA_ARGS__ }; \ + CRYPTO_AUDITING_NEW_CONTEXT_WITH_DATA(context, parent, \ + __crypto_auditing_events, \ + sizeof (__crypto_auditing_events) / sizeof (__crypto_auditing_events[0])); \ +}) + #else # define CRYPTO_AUDITING_NEW_CONTEXT(context, parent) # define CRYPTO_AUDITING_WORD_DATA(context, key_ptr, value_ptr) # define CRYPTO_AUDITING_STRING_DATA(context, key_ptr, value_ptr) # define CRYPTO_AUDITING_BLOB_DATA(context, key_ptr, value_ptr, value_size) +# define CRYPTO_AUDITING_WORD(key_ptr, value_ptr) +# define CRYPTO_AUDITING_STRING(key_ptr, value_ptr) +# define CRYPTO_AUDITING_BLOB(key_ptr, value_ptr, value_size) +# define CRYPTO_AUDITING_DATA(context, array_ptr, array_size) +# define CRYPTO_AUDITING_DATAV(context, ...) +# define CRYPTO_AUDITING_NEW_CONTEXT_WITH_DATA(context, parent, array_ptr, array_size) +# define CRYPTO_AUDITING_NEW_CONTEXT_WITH_DATAV(context, parent, ...) #endif /* ENABLE_CRYPTO_AUDITING */