Skip to content

Commit

Permalink
feat: support SocketBindDeny systemd option
Browse files Browse the repository at this point in the history
  • Loading branch information
desbma-s1n committed Sep 29, 2023
1 parent f995ed2 commit 4927217
Show file tree
Hide file tree
Showing 6 changed files with 304 additions and 48 deletions.
8 changes: 8 additions & 0 deletions src/strace/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ impl IntegerExpression {
_ => false, // if it was a flag field, strace would have decoded it with named consts
}
}

pub fn flags(&self) -> Vec<String> {
match self {
IntegerExpression::NamedConst(v) => vec![v.clone()],
IntegerExpression::BinaryOr(vs) => vs.iter().flat_map(|v| v.flags()).collect(),
_ => vec![],
}
}
}

pub type SyscallRetVal = i128; // allows holding both signed and unsigned 64 bit integers
45 changes: 44 additions & 1 deletion src/summarize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::path::{Path, PathBuf};
use lazy_static::lazy_static;

use crate::strace::{BufferType, IntegerExpression, Syscall, SyscallArg};
use crate::systemd::{SocketFamily, SocketProtocol};

/// A high level program runtime action
/// This does *not* map 1-1 with a syscall, and does *not* necessarily respect chronology
Expand All @@ -19,12 +20,17 @@ pub enum ProgramAction {
Write(PathBuf),
/// Path was created
Create(PathBuf),
/// Network (socket) activity
/// Generic network (socket) activity
NetworkActivity { af: String },
/// Memory mapping with write and execute bits
WriteExecuteMemoryMapping,
/// Set scheduler to a real time one
SetRealtimeScheduler,
/// Bind socket
SocketBind {
af: SocketFamily,
proto: SocketProtocol,
},
/// Names of the syscalls made by the program
Syscalls(HashSet<String>),
}
Expand Down Expand Up @@ -205,6 +211,9 @@ where
{
let mut actions = Vec::new();
let mut stats: HashMap<String, u64> = HashMap::new();
// keep known socket protocols for bind handling, we don't care for the socket closings
// because the fd will be reused or never bound again
let mut known_sockets_proto: HashMap<i128, SocketProtocol> = HashMap::new();
for syscall in syscalls {
let syscall = syscall?;
log::trace!("{syscall:?}");
Expand Down Expand Up @@ -374,6 +383,27 @@ where
}
_ => (),
}

if name == "bind" {
let fd = if let Some(SyscallArg::Integer {
value: IntegerExpression::Literal(fd),
..
}) = syscall.args.get(0)
{
fd
} else {
anyhow::bail!("Unexpected args for {}: {:?}", name, syscall.args);
};
if let (Some(af), Some(proto)) = (
SocketFamily::from_syscall_arg(af),
known_sockets_proto.get(fd),
) {
actions.push(ProgramAction::SocketBind {
af,
proto: proto.clone(),
});
}
}
}
Some(SyscallInfo::SetScheduler) => {
let policy = if let Some(SyscallArg::Integer { value, .. }) = syscall.args.get(1) {
Expand All @@ -396,6 +426,19 @@ where
anyhow::bail!("Unexpected args for {}: {:?}", name, syscall.args);
};
actions.push(ProgramAction::NetworkActivity { af });

let proto_flags =
if let Some(SyscallArg::Integer { value, .. }) = syscall.args.get(1) {
value.flags()
} else {
anyhow::bail!("Unexpected args for {}: {:?}", name, syscall.args);
};
for proto in proto_flags {
if let Some(known_proto) = SocketProtocol::from_syscall_arg(&proto) {
known_sockets_proto.insert(syscall.ret_val, known_proto);
break;
}
}
}
Some(SyscallInfo::Mmap { prot_idx }) => {
let prot = if let Some(SyscallArg::Integer { value: prot, .. }) =
Expand Down
2 changes: 1 addition & 1 deletion src/systemd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ mod resolver;
mod service;
mod version;

pub use options::{build_options, OptionDescription};
pub use options::{build_options, OptionDescription, SocketFamily, SocketProtocol};
pub use resolver::resolve;
pub use service::Service;
pub use version::{KernelVersion, SystemdVersion};
Expand Down
156 changes: 131 additions & 25 deletions src/systemd/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};
use std::str::FromStr;

use itertools::Itertools;
use lazy_static::lazy_static;
use strum::IntoEnumIterator;

use crate::cl::HardeningMode;
use crate::systemd::{KernelVersion, SystemdVersion};
Expand All @@ -25,13 +27,24 @@ impl fmt::Display for OptionDescription {
}
}

#[derive(Debug, Clone)]
pub enum ListMode {
WhiteList,
BlackList,
}

/// Systemd option value
#[derive(Debug, Clone)]
pub enum OptionValue {
Boolean(bool), // In most case we only model the 'true' value, because false is no-op and the default
String(String), // enum-like, or free string
DenyList(Vec<String>),
AllowList(Vec<String>),
List {
values: Vec<String>,
value_if_empty: Option<String>,
negation_prefix: bool,
repeat_option: bool,
mode: ListMode,
},
}

impl FromStr for OptionValue {
Expand Down Expand Up @@ -99,6 +112,11 @@ pub enum OptionValueEffect {
DenyWriteExecuteMemoryMapping,
/// Deny real time scheduling
DenyRealtimeScheduler,
/// Deny a socket family and protocol socket bind
DenySocketBind {
af: SocketFamily,
proto: SocketProtocol,
},
/// Union of multiple effects
Multiple(Vec<OptionValueEffect>),
}
Expand All @@ -111,6 +129,43 @@ pub enum DenySyscalls {
Single(String),
}

// Not a complete enumeration, only used with SocketBindDeny
#[derive(Debug, Clone, Eq, PartialEq, strum::EnumIter, strum::Display)]
#[strum(serialize_all = "snake_case")]
pub enum SocketFamily {
Ipv4,
Ipv6,
}

impl SocketFamily {
pub fn from_syscall_arg(s: &str) -> Option<Self> {
match s {
"AF_INET" => Some(Self::Ipv4),
"AF_INET6" => Some(Self::Ipv6),
_ => None,
}
}
}

// Not a complete enumeration, only used with SocketBindDeny
#[derive(Debug, Clone, Eq, PartialEq, strum::EnumIter, strum::Display)]
#[strum(serialize_all = "snake_case")]
pub enum SocketProtocol {
Tcp,
Udp,
}

impl SocketProtocol {
pub fn from_syscall_arg(s: &str) -> Option<Self> {
// Only makes sense for IP addresses
match s {
"SOCK_STREAM" => Some(Self::Tcp),
"SOCK_DGRAM" => Some(Self::Udp),
_ => None,
}
}
}

impl DenySyscalls {
/// Get denied syscall names
pub fn syscalls(&self) -> HashSet<String> {
Expand Down Expand Up @@ -159,30 +214,42 @@ impl FromStr for OptionWithValue {

impl fmt::Display for OptionWithValue {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}=", self.name)?;
match &self.value {
OptionValue::Boolean(value) => {
write!(f, "{}", if *value { "true" } else { "false" })
}
OptionValue::String(value) => {
write!(f, "{value}")
write!(f, "{}={}", self.name, if *value { "true" } else { "false" })
}
OptionValue::DenyList(values) => {
debug_assert!(!values.is_empty());
write!(
f,
"~{}",
values
.iter()
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(" ")
)
}
OptionValue::AllowList(values) => {
OptionValue::String(value) => write!(f, "{}={}", self.name, value),
OptionValue::List {
values,
value_if_empty,
negation_prefix,
repeat_option,
..
} => {
if values.is_empty() {
write!(f, "none")
write!(f, "{}=", self.name)?;
if let Some(value_if_empty) = value_if_empty {
write!(f, "{value_if_empty}")
} else {
unreachable!()
}
} else if *repeat_option {
for (i, value) in values.iter().enumerate() {
write!(f, "{}=", self.name)?;
if *negation_prefix {
write!(f, "~")?;
}
write!(f, "{value}")?;
if i < values.len() - 1 {
writeln!(f)?;
}
}
Ok(())
} else {
write!(f, "{}=", self.name)?;
if *negation_prefix {
write!(f, "~")?;
}
write!(
f,
"{}",
Expand Down Expand Up @@ -1083,7 +1150,13 @@ pub fn build_options(
options.push(OptionDescription {
name: "RestrictAddressFamilies".to_string(),
possible_values: vec![OptionValueDescription {
value: OptionValue::AllowList(afs.iter().map(|s| s.to_string()).collect()),
value: OptionValue::List {
values: afs.iter().map(|s| s.to_string()).collect(),
value_if_empty: Some("none".to_string()),
negation_prefix: false,
repeat_option: false,
mode: ListMode::WhiteList,
},
desc: OptionEffect::Cumulative(
afs.into_iter()
.map(|af| OptionValueEffect::DenySocketFamily(af.to_string()))
Expand Down Expand Up @@ -1111,6 +1184,35 @@ pub fn build_options(
});
}

// https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html#SocketBindAllow=bind-rule
//
// We don't go as far as allowing/denying individual ports, as that would easily break for example if a port is changed
// in a server configuration
let deny_binds: Vec<_> = SocketFamily::iter()
.cartesian_product(SocketProtocol::iter())
.collect();
options.push(OptionDescription {
name: "SocketBindDeny".to_string(),
possible_values: vec![OptionValueDescription {
value: OptionValue::List {
values: deny_binds
.iter()
.map(|(af, proto)| format!("{af}:{proto}"))
.collect(),
value_if_empty: None,
negation_prefix: false,
repeat_option: true,
mode: ListMode::BlackList,
},
desc: OptionEffect::Cumulative(
deny_binds
.into_iter()
.map(|(af, proto)| OptionValueEffect::DenySocketBind { af, proto })
.collect(),
),
}],
});

// https://www.freedesktop.org/software/systemd/man/systemd.exec.html#LockPersonality=
options.push(OptionDescription {
name: "LockPersonality".to_string(),
Expand Down Expand Up @@ -1161,12 +1263,16 @@ pub fn build_options(
options.push(OptionDescription {
name: "SystemCallFilter".to_string(),
possible_values: vec![OptionValueDescription {
value: OptionValue::DenyList(
syscall_classes
value: OptionValue::List {
values: syscall_classes
.iter()
.map(|c| format!("@{c}:EPERM"))
.collect(),
),
value_if_empty: None,
negation_prefix: true,
repeat_option: false,
mode: ListMode::BlackList,
},
desc: OptionEffect::Cumulative(
syscall_classes
.into_iter()
Expand Down
Loading

0 comments on commit 4927217

Please sign in to comment.