From 78fcd766b2a43d4e01b5dd11ebecefa86e5fe348 Mon Sep 17 00:00:00 2001 From: LeChatP Date: Wed, 10 Jul 2024 15:07:50 +0200 Subject: [PATCH] WIP: Let's introduce new capable features --- capable-common/Cargo.toml | 3 + capable-common/src/lib.rs | 96 +++++++++++++++++++ capable-ebpf/Cargo.toml | 2 +- capable-ebpf/src/capable.rs | 78 ++++++++++++++++ capable-ebpf/src/ebpf_util.rs | 36 ++++++++ capable-ebpf/src/main.rs | 112 +++++++---------------- capable-ebpf/src/open.rs | 148 ++++++++++++++++++++++++++++++ capable-ebpf/src/ret_open.rs | 38 ++++++++ capable/Cargo.toml | 4 + capable/build.rs | 22 +++++ capable/src/main.rs | 167 ++++++++++++++++------------------ capable/src/version.rs | 1 + 12 files changed, 539 insertions(+), 168 deletions(-) create mode 100644 capable-ebpf/src/capable.rs create mode 100644 capable-ebpf/src/ebpf_util.rs create mode 100644 capable-ebpf/src/open.rs create mode 100644 capable-ebpf/src/ret_open.rs create mode 100644 capable/build.rs create mode 100644 capable/src/version.rs diff --git a/capable-common/Cargo.toml b/capable-common/Cargo.toml index c44b06c3..6dbb0aa7 100644 --- a/capable-common/Cargo.toml +++ b/capable-common/Cargo.toml @@ -7,9 +7,12 @@ edition = "2021" [features] default = [] user = ["aya"] +kernel = ["aya-ebpf"] [dependencies] aya = { git = "https://github.com/aya-rs/aya", optional = true } +aya-ebpf = { git = "https://github.com/aya-rs/aya", optional = true } +bitflags = "2.6.0" [lib] path = "src/lib.rs" diff --git a/capable-common/src/lib.rs b/capable-common/src/lib.rs index 0c9ac1ac..6a9351b5 100644 --- a/capable-common/src/lib.rs +++ b/capable-common/src/lib.rs @@ -1 +1,97 @@ #![no_std] + +#[cfg(feature = "aya")] +use aya::Pod; + +use bitflags::bitflags; + +pub enum OpenMode { + Read, + Write, + LSeek, + PRead, + PWrite, + Exec, + WriteRestricted, + Hash32, + Hash64, +} + +pub enum FileOperation { + Open, + Create, + Delete, + LinkAt, + Lookup, + FollowLink, +} + +bitflags! { + #[derive(PartialEq, Clone, Copy)] + pub struct Access: u32 { + + // open modes + const READ = 0b1; + const WRITE = 0b10; + const LSEEK = 0b100; + const PREAD = 0b1000; + const PWRITE = 0b10000; + const EXEC = 0b100000; + const WRITE_RESTRICTED = 0b1000000; + const HASH32 = 0b10000000; + const HASH64 = 0b100000000; + + // File operations + const OPEN = 0b1000000000; + const CREATE = 0b10000000000; + const DELETE = 0b100000000000; + const LINKAT = 0b1000000000000; + const LOOKUP = 0b10000000000000; + const FOLLOW_LINK = 0b100000000000000; + } +} + +pub type NsId = u32; + +impl Access { + pub fn open_mode(&self) -> Option { + match self { + &Self::READ => Some(OpenMode::Read), + &Self::WRITE => Some(OpenMode::Write), + &Self::LSEEK => Some(OpenMode::LSeek), + &Self::PREAD => Some(OpenMode::PRead), + &Self::PWRITE => Some(OpenMode::PWrite), + &Self::EXEC => Some(OpenMode::Exec), + &Self::WRITE_RESTRICTED => Some(OpenMode::WriteRestricted), + &Self::HASH32 => Some(OpenMode::Hash32), + &Self::HASH64 => Some(OpenMode::Hash64), + //ignore others + _ => None, + } + } + + pub fn file_operation(&self) -> Option { + match self { + &Self::OPEN => Some(FileOperation::Open), + &Self::CREATE => Some(FileOperation::Create), + &Self::DELETE => Some(FileOperation::Delete), + &Self::LINKAT => Some(FileOperation::LinkAt), + &Self::LOOKUP => Some(FileOperation::Lookup), + &Self::FOLLOW_LINK => Some(FileOperation::FollowLink), + //ignore others + _ => None, + } + } +} + + + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct Request { + pub f_mode: Access, + pub f_path: [u8; 8188], +} + +#[cfg(feature = "aya")] +unsafe impl Pod for Request {} diff --git a/capable-ebpf/Cargo.toml b/capable-ebpf/Cargo.toml index 7ba8c4bb..dbf8b902 100644 --- a/capable-ebpf/Cargo.toml +++ b/capable-ebpf/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] aya-ebpf = { git = "https://github.com/aya-rs/aya" } aya-log-ebpf = { git = "https://github.com/aya-rs/aya" } -capable-common = { path = "../capable-common" } +capable-common = { path = "../capable-common", features = ["kernel"] } [[bin]] name = "capable" diff --git a/capable-ebpf/src/capable.rs b/capable-ebpf/src/capable.rs new file mode 100644 index 00000000..13ffe326 --- /dev/null +++ b/capable-ebpf/src/capable.rs @@ -0,0 +1,78 @@ +use aya_ebpf::{ + helpers::{bpf_get_current_task, bpf_get_current_uid_gid, bpf_probe_read_kernel}, + macros::map, + maps::HashMap, + programs::ProbeContext, +}; + +use crate::ebpf_util::{get_ns_inode, TaskStructPtr, MAX_PID}; + +use aya_log_ebpf::{debug, info}; + +type Key = i32; + +#[map] +static mut CAPABILITIES_MAP: HashMap = HashMap::with_max_entries(MAX_PID, 0); +#[map] +static mut UID_GID_MAP: HashMap = HashMap::with_max_entries(MAX_PID, 0); +#[map] +static mut PPID_MAP: HashMap = HashMap::with_max_entries(MAX_PID, 0); +#[map] +static mut PNSID_NSID_MAP: HashMap = HashMap::with_max_entries(MAX_PID, 0); + + +pub fn try_capable(ctx: &ProbeContext) -> Result { + info!(ctx, "capable"); + unsafe { + let task: TaskStructPtr = bpf_get_current_task() as TaskStructPtr; + debug!(ctx, "debug1"); + let task = bpf_probe_read_kernel(&task)?; + debug!(ctx, "debug2"); + let ppid: i32 = get_ppid(task)?; + debug!(ctx, "debug3"); + let pid: i32 = bpf_probe_read_kernel(&(*task).pid)? as i32; + debug!(ctx, "debug4"); + let cap: u64 = (1 << ctx.arg::(2).unwrap()) as u64; + debug!(ctx, "debug5"); + let uid: u64 = bpf_get_current_uid_gid(); + debug!(ctx, "debug6"); + let zero = 0; + let capval: u64 = *CAPABILITIES_MAP.get(&pid).unwrap_or(&zero); + debug!(ctx, "debug7"); + let pinum_inum: u64 = Into::::into(get_parent_ns_inode(task)?) << 32 + | Into::::into(get_ns_inode(task)?); + debug!(ctx, "debug8"); + UID_GID_MAP + .insert(&pid, &uid, 0) + .expect("failed to insert uid"); + debug!(ctx, "debug9"); + PNSID_NSID_MAP + .insert(&pid, &pinum_inum, 0) + .expect("failed to insert pnsid"); + debug!(ctx, "debug10"); + PPID_MAP + .insert(&pid, &ppid, 0) + .expect("failed to insert ppid"); + debug!(ctx, "debug11"); + CAPABILITIES_MAP + .insert(&pid, &(capval | cap), 0) + .expect("failed to insert cap"); + } + Ok(0) +} + +unsafe fn get_ppid(task: TaskStructPtr) -> Result { + let parent_task: TaskStructPtr = get_parent_task(task)?; + bpf_probe_read_kernel(&(*parent_task).pid) +} + +unsafe fn get_parent_task(task: TaskStructPtr) -> Result { + bpf_probe_read_kernel(&(*task).parent) +} + +unsafe fn get_parent_ns_inode(task: TaskStructPtr) -> Result { + let parent_task: TaskStructPtr = get_parent_task(task)?; + get_ns_inode(parent_task) +} + + diff --git a/capable-ebpf/src/ebpf_util.rs b/capable-ebpf/src/ebpf_util.rs new file mode 100644 index 00000000..bac6dba6 --- /dev/null +++ b/capable-ebpf/src/ebpf_util.rs @@ -0,0 +1,36 @@ +use aya_ebpf::{ + helpers::{bpf_probe_read_kernel,bpf_get_current_task}, + macros::map, + maps::array::Array, +}; +use crate::open::PidPtr; +use crate::vmlinux::{ns_common, nsproxy, pid_namespace, task_struct}; + +#[map] +static mut NAMESPACE_ID: Array = Array::with_max_entries(1, 0); + +pub unsafe fn is_namespace_ok() -> bool { + NAMESPACE_ID.get(0).map_or(false,|namespace| { + let task: TaskStructPtr = bpf_get_current_task() as TaskStructPtr; + let current_namespace = get_ns_inode(task); + current_namespace.ok().map_or(false, |ns| ns == *namespace) + }) +} + +pub type TaskStructPtr = *mut task_struct; +pub const MAX_PID: u32 = 4 * 1024 * 1024; +pub const EPERM : i32 = 1; + +pub unsafe fn get_thread_pid(task: TaskStructPtr) -> Result { + let pid: PidPtr = bpf_probe_read_kernel(&(*task).thread_pid)? as PidPtr; + let pid = bpf_probe_read_kernel(&(*pid).ino)? as u64; + Ok(pid) +} + +pub unsafe fn get_ns_inode(task: TaskStructPtr) -> Result { + let nsp: *mut nsproxy = bpf_probe_read_kernel(&(*task).nsproxy).map_err(|e| e as u32)?; + let pns: *mut pid_namespace = + bpf_probe_read_kernel(&(*nsp).pid_ns_for_children).map_err(|e| e as u32)?; + let nsc: ns_common = bpf_probe_read_kernel(&(*pns).ns).map_err(|e| e as u32)?; + bpf_probe_read_kernel(&nsc.inum) +} \ No newline at end of file diff --git a/capable-ebpf/src/main.rs b/capable-ebpf/src/main.rs index c9cf3002..3418f645 100644 --- a/capable-ebpf/src/main.rs +++ b/capable-ebpf/src/main.rs @@ -7,99 +7,57 @@ #[allow(dead_code)] mod vmlinux; +mod capable; +mod open; +pub mod ebpf_util; +mod ret_open; + use aya_ebpf::{ - helpers::{bpf_get_current_task, bpf_get_current_uid_gid, bpf_probe_read_kernel}, - macros::{kprobe, map}, - maps::HashMap, - programs::ProbeContext, + macros::{kretprobe, kprobe}, + programs::{RetProbeContext, ProbeContext}, }; -use aya_log_ebpf::{debug, info}; -use vmlinux::{ns_common, nsproxy, pid_namespace, task_struct}; - -const MAX_PID: u32 = 4 * 1024 * 1024; +use crate::capable::try_capable; +use crate::open::{try_acl_may_create, try_acl_may_open, try_acl_may_delete, try_acl_may_linkat, try_acl_may_lookup, try_acl_may_follow_link}; +use crate::ret_open::try_ret_acl_may_action; -type Key = i32; -type TaskStructPtr = *mut task_struct; +#[kprobe] +pub fn acl_may_open(ctx: ProbeContext) -> u32 { + try_acl_may_open(&ctx).unwrap_or_else(|ret| ret as u32) +} -#[map] -static mut KALLSYMS_MAP: HashMap = HashMap::with_max_entries(MAX_PID, 0); -#[map] -static mut CAPABILITIES_MAP: HashMap = HashMap::with_max_entries(MAX_PID, 0); -#[map] -static mut UID_GID_MAP: HashMap = HashMap::with_max_entries(MAX_PID, 0); -#[map] -static mut PPID_MAP: HashMap = HashMap::with_max_entries(MAX_PID, 0); -#[map] -static mut PNSID_NSID_MAP: HashMap = HashMap::with_max_entries(MAX_PID, 0); +#[kprobe] +pub fn acl_may_create(ctx: ProbeContext) -> u32 { + try_acl_may_create(&ctx).unwrap_or_else(|ret| ret as u32) +} #[kprobe] -pub fn capable(ctx: ProbeContext) -> u32 { - match try_capable(&ctx) { - Ok(ret) => ret, - Err(ret) => ret as u32, - } +pub fn acl_may_delete(ctx: ProbeContext) -> u32 { + try_acl_may_delete(&ctx).unwrap_or_else(|ret| ret as u32) } -fn try_capable(ctx: &ProbeContext) -> Result { - info!(ctx, "capable"); - unsafe { - let task: TaskStructPtr = bpf_get_current_task() as TaskStructPtr; - debug!(ctx, "debug1"); - let task = bpf_probe_read_kernel(&task)?; - debug!(ctx, "debug2"); - let ppid: i32 = get_ppid(task)?; - debug!(ctx, "debug3"); - let pid: i32 = bpf_probe_read_kernel(&(*task).pid)? as i32; - debug!(ctx, "debug4"); - let cap: u64 = (1 << ctx.arg::(2).unwrap()) as u64; - debug!(ctx, "debug5"); - let uid: u64 = bpf_get_current_uid_gid(); - debug!(ctx, "debug6"); - let zero = 0; - let capval: u64 = *CAPABILITIES_MAP.get(&pid).unwrap_or(&zero); - debug!(ctx, "debug7"); - let pinum_inum: u64 = Into::::into(get_parent_ns_inode(task)?) << 32 - | Into::::into(get_ns_inode(task)?); - debug!(ctx, "debug8"); - UID_GID_MAP - .insert(&pid, &uid, 0) - .expect("failed to insert uid"); - debug!(ctx, "debug9"); - PNSID_NSID_MAP - .insert(&pid, &pinum_inum, 0) - .expect("failed to insert pnsid"); - debug!(ctx, "debug10"); - PPID_MAP - .insert(&pid, &ppid, 0) - .expect("failed to insert ppid"); - debug!(ctx, "debug11"); - CAPABILITIES_MAP - .insert(&pid, &(capval | cap), 0) - .expect("failed to insert cap"); - } - Ok(0) +#[kprobe] +pub fn acl_may_linkat(ctx: ProbeContext) -> u32 { + try_acl_may_linkat(&ctx).unwrap_or_else(|ret| ret as u32) } -unsafe fn get_ppid(task: TaskStructPtr) -> Result { - let parent_task: TaskStructPtr = get_parent_task(task)?; - bpf_probe_read_kernel(&(*parent_task).pid) +#[kprobe] +pub fn acl_may_lookup(ctx: ProbeContext) -> u32 { + try_acl_may_lookup(&ctx).unwrap_or_else(|ret| ret as u32) } -unsafe fn get_parent_task(task: TaskStructPtr) -> Result { - bpf_probe_read_kernel(&(*task).parent) +#[kprobe] +pub fn acl_may_follow_link(ctx: ProbeContext) -> u32 { + try_acl_may_follow_link(&ctx).unwrap_or_else(|ret| ret as u32) } -unsafe fn get_parent_ns_inode(task: TaskStructPtr) -> Result { - let parent_task: TaskStructPtr = get_parent_task(task)?; - get_ns_inode(parent_task) +#[kretprobe] +pub fn acl_may_ret(ctx: RetProbeContext) -> u32 { + try_ret_acl_may_action(&ctx).unwrap_or_else(|ret| ret as u32) } -unsafe fn get_ns_inode(task: TaskStructPtr) -> Result { - let nsp: *mut nsproxy = bpf_probe_read_kernel(&(*task).nsproxy).map_err(|e| e as u32)?; - let pns: *mut pid_namespace = - bpf_probe_read_kernel(&(*nsp).pid_ns_for_children).map_err(|e| e as u32)?; - let nsc: ns_common = bpf_probe_read_kernel(&(*pns).ns).map_err(|e| e as u32)?; - bpf_probe_read_kernel(&nsc.inum) +#[kprobe] +pub fn capable(ctx: ProbeContext) -> u32 { + try_capable(&ctx).unwrap_or_else(|ret| ret as u32) } #[panic_handler] diff --git a/capable-ebpf/src/open.rs b/capable-ebpf/src/open.rs new file mode 100644 index 00000000..3c652abb --- /dev/null +++ b/capable-ebpf/src/open.rs @@ -0,0 +1,148 @@ +use core::ffi::{c_long, c_void}; +use aya_ebpf::{ + helpers::{bpf_get_current_task, bpf_probe_read_kernel}, + macros::map, + maps::HashMap, + programs::ProbeContext, +}; +use aya_ebpf::helpers::gen::bpf_probe_read_kernel_str; +use aya_log_ebpf::info; +use crate::ebpf_util::{TaskStructPtr, is_namespace_ok, MAX_PID, get_thread_pid}; +use crate::vmlinux::{dentry, nameidata, path, pid}; +use capable_common::{Access, Request}; +pub type DentryPtr = *mut dentry; +pub type PathPtr = *mut path; +pub type NameidataPtr = *mut nameidata; +pub type PidPtr = *mut pid; + +#[map] +pub static mut PENDING_REQUESTS: HashMap = HashMap::with_max_entries(MAX_PID, 0); + + + +pub fn try_acl_may_create(ctx: &ProbeContext) -> Result { + info!(ctx, "may_create"); + try_acl_may_action( + ctx, + Request { + f_path: unsafe { get_full_path(ctx.arg::(2).expect("DentryPtr should be here"))? }, + f_mode: Access::CREATE, + }, + ) +} + +pub fn try_acl_may_open(ctx: &ProbeContext) -> Result { + info!(ctx, "may_open"); + try_acl_may_action( + ctx, + Request { + f_path: unsafe { get_full_path(get_dentry_from_pathptr(*ctx.arg::(1).expect("PathPtr should be here"))?)? }, + f_mode: Access::from_bits(ctx.arg::(2).expect("bits")).expect("Should be valid Access type") | Access::OPEN, + }, + ) +} + +pub fn try_acl_may_delete(ctx: &ProbeContext) -> Result { + info!(ctx, "may_delete"); + try_acl_may_action( + ctx, + Request { + f_path: unsafe { get_full_path(ctx.arg::(2).expect("DentryPtr should be here"))? }, + f_mode: Access::DELETE, + }, + ) +} + +pub fn try_acl_may_linkat(ctx: &ProbeContext) -> Result { + info!(ctx, "may_linkat"); + try_acl_may_action( + ctx, + Request { + f_path: unsafe { get_full_path(get_dentry_from_pathptr(*ctx.arg::(2).expect("PathPtr should be here"))?)? }, + f_mode: Access::LINKAT, + }, + ) +} + +pub fn try_acl_may_lookup(ctx: &ProbeContext) -> Result { + info!(ctx, "may_lookup"); + try_acl_may_action( + ctx, + Request { + f_path: unsafe { + get_full_path(get_dentry_from_nameidata(ctx.arg::(1).expect("Nameidata should be here"))?)? + }, + f_mode: Access::LOOKUP, + }, + ) +} + +pub fn try_acl_may_follow_link(ctx: &ProbeContext) -> Result { + info!(ctx, "may_follow_link"); + try_acl_may_action( + ctx, + Request { + f_path: unsafe { + get_full_path(get_dentry_from_nameidata(ctx.arg::(0).expect("Nameidata should be here"))?)? + }, + f_mode: Access::FOLLOW_LINK, + }, + ) +} + +const LOOP_MAX: u32 = 25; + +unsafe fn get_dentry_from_pathptr(path: path) -> Result { + bpf_probe_read_kernel(&path.dentry) +} + +unsafe fn get_dentry_from_nameidata(path: NameidataPtr) -> Result { + get_dentry_from_pathptr(bpf_probe_read_kernel(&(*path).path)?) +} + +const SIZE: usize = 8188; +unsafe fn get_full_path(dentry: DentryPtr) -> Result<[u8;SIZE], i64> { + let mut result = [0u8; SIZE]; + let mut length = read_kernel_str(result[0] as *mut c_void, SIZE as u32, get_path_str_ptr(dentry)?)?; + let mut parent: DentryPtr = bpf_probe_read_kernel(&(*dentry).d_parent)?; + let mut i = 0; + while length < 8187 || parent != 0 as DentryPtr || LOOP_MAX < i { + result[length] = b'/'; + length += 1; + length += read_kernel_str(result[length] as *mut c_void, (SIZE - length) as u32, get_path_str_ptr(parent)?)?; + parent = bpf_probe_read_kernel(&(*parent).d_parent)?; + i += 1; + } + Ok(result) +} + +fn try_acl_may_action(ctx: &ProbeContext, request: Request) -> Result { + unsafe { + if !is_namespace_ok() { + return Ok(0); + } + info!(ctx, "may_action"); + let task: TaskStructPtr = bpf_get_current_task() as TaskStructPtr; + let task = bpf_probe_read_kernel(&task)?; + let pid = get_thread_pid(task)?; + + // if access denied, then find out which user and group can access the file and add it to the UACL_MAP and GACL_MAP + PENDING_REQUESTS.insert(&pid,&request,0).expect("failed to insert request"); + Ok(0) + } +} + +unsafe fn get_path_str_ptr(dentry: DentryPtr) -> Result<*mut c_void, i64> { + Ok(bpf_probe_read_kernel(bpf_probe_read_kernel(&(*dentry).d_name)?.name)? as *mut c_void) +} +fn result_kernel_str(result : c_long) -> Result { + if result < 0 { + Err(result as i64) + } else { + Ok(result as usize) + } +} + +fn read_kernel_str(dest: *mut c_void, size: u32, src: *mut c_void) -> Result { + result_kernel_str(unsafe { bpf_probe_read_kernel_str(dest, size, src) }) +} diff --git a/capable-ebpf/src/ret_open.rs b/capable-ebpf/src/ret_open.rs new file mode 100644 index 00000000..6a69cea7 --- /dev/null +++ b/capable-ebpf/src/ret_open.rs @@ -0,0 +1,38 @@ +use aya_ebpf::{ + helpers::{bpf_get_current_task, bpf_probe_read_kernel}, + macros::map, + programs::RetProbeContext, +}; +use aya_ebpf::maps::Stack; +use crate::ebpf_util::{TaskStructPtr, MAX_PID, is_namespace_ok, get_thread_pid, EPERM}; +use crate::open::PENDING_REQUESTS; +use crate::vmlinux::{dentry, file, inode, nameidata, path}; +use capable_common::Request; +use aya_log_ebpf::info; + +#[map] +static mut REQUESTS: Stack = Stack::with_max_entries(MAX_PID, 0); + + +pub fn try_ret_acl_may_action(ctx: &RetProbeContext) -> Result { + unsafe { + if !is_namespace_ok() { + return Ok(0); + } + info!(ctx, "may_action"); + let task: TaskStructPtr = bpf_get_current_task() as TaskStructPtr; + let task = bpf_probe_read_kernel(&task)?; + let pid = get_thread_pid(task)?; + + let ret : i32 = ctx.ret().unwrap(); + + // if access denied, then find out which user and group can access the file and add it to the UACL_MAP and GACL_MAP + if ret == -EPERM { + let request = PENDING_REQUESTS.get(&pid).expect("request not found"); + REQUESTS.push(&request.clone(), 0)?; + } + PENDING_REQUESTS.remove(&pid).expect("Impossible"); + } + Ok(0) + +} diff --git a/capable/Cargo.toml b/capable/Cargo.toml index 56821f7a..9ad57a40 100644 --- a/capable/Cargo.toml +++ b/capable/Cargo.toml @@ -21,6 +21,10 @@ shell-words = "1.1.0" serde = { version = "1.0.203", features=["rc", "derive"] } serde_json = "1.0.117" unshare = { version = "0.7.0" } +lazy_static = "1.5.0" + +[build-dependencies] +aya = { git = "https://github.com/aya-rs/aya" } [[bin]] name = "capable" diff --git a/capable/build.rs b/capable/build.rs new file mode 100644 index 00000000..dd387de7 --- /dev/null +++ b/capable/build.rs @@ -0,0 +1,22 @@ +use std::{error::Error, fs, io}; + +use aya::util::KernelVersion; + +fn main() { + // get kernel version + match kernel_version() { + Ok(version) => { + // create version.rs file + fs::write( + "src/version.rs", + format!("pub const LINUX_VERSION_CODE: u32 = {};", version.code()), + ) + .unwrap(); + } + Err(e) => eprintln!("Error: {}", e), + } +} + +fn kernel_version() -> Result { + aya::util::KernelVersion::current() +} diff --git a/capable/src/main.rs b/capable/src/main.rs index fd820964..0c2db6d8 100644 --- a/capable/src/main.rs +++ b/capable/src/main.rs @@ -1,6 +1,7 @@ -use std::borrow::Borrow; +use std::borrow::{Borrow, BorrowMut}; use std::cell::RefCell; use std::error::Error; +use std::ffi::CStr; use std::fs::{canonicalize, metadata}; use std::os::unix::prelude::MetadataExt; use std::path::{Path, PathBuf}; @@ -11,10 +12,12 @@ use std::sync::{Arc, Mutex}; use std::time::Duration; use std::{env, thread, vec}; -use aya::maps::{HashMap, MapData}; -use aya::programs::KProbe; -use aya::{include_bytes_aligned, Ebpf}; +use aya::maps::{Array, HashMap, Map, MapData}; +use aya::programs::{KProbe, Program}; +use aya::util::KernelVersion; +use aya::{include_bytes_aligned, Ebpf, Pod}; use aya_log::EbpfLogger; +use capable_common::{NsId, Request}; use capctl::{ambient, Cap, CapSet, CapState, ParseCapError}; use log::{debug, warn}; use nix::sys::wait::{WaitPidFlag, WaitStatus}; @@ -29,6 +32,8 @@ use tokio::signal; type Key = i32; +mod version; + struct Cli { /// Specify a delay before killing the process sleep: Option, @@ -171,6 +176,7 @@ where { let mut graph = std::collections::HashMap::new(); let mut init = CapSet::empty(); + setbpf_effective(true); for key in capabilities_map.keys() { let pid = key.inspect_err(|err| { eprintln!("Failed to get pid : {:?}", err.to_string()); @@ -187,6 +193,7 @@ where init = caps_from_u64(capabilities_map.get(&pid, 0).unwrap_or(0)); } } + setbpf_effective(false); let result = init.union(union_all_childs(*nsinode, &graph)); if json { println!("{}", serde_json::to_string(&capset_to_vec(&result))?); @@ -371,7 +378,7 @@ fn setptrace_effective(enable: bool) -> Result<(), capctl::Error> { }) } -fn getopt(s: I) -> Result> +fn getopt(s: I) -> Result where I: IntoIterator, S: AsRef, @@ -403,7 +410,7 @@ where } _ => { if arg.as_ref().starts_with('-') { - return Err(format!("Unknown option: {}", arg.as_ref()).into()); + return Err(anyhow::anyhow!("Unknown option: {}", arg.as_ref())); } else { args.command.push(escape_parser_string(arg)); break; @@ -417,11 +424,16 @@ where Ok(args) } -fn run_command(cli_args: &mut Cli, nsclone: Rc>) -> Result { +fn run_command( + cli_args: &mut Cli, + nsclone: Rc>, + config_map: Rc>>, +) -> Result { let (path, args) = get_exec_and_args(&mut cli_args.command); let namespaces = vec![&unshare::Namespace::Pid]; let capabilities = cli_args.capabilities.clone(); let mut cmd = unshare::Command::new(path); + unsafe { cmd.pre_exec(move || { let mut capstate = CapState::empty(); @@ -436,6 +448,7 @@ fn run_command(cli_args: &mut Cli, nsclone: Rc>) -> Result> = Arc::new(Mutex::new( cmd.args(&args) @@ -445,6 +458,10 @@ fn run_command(cli_args: &mut Cli, nsclone: Rc>) -> Result>) -> Result>) -> Result<(), anyhow::Error> { +fn load_and_attach_program( + bpf: &mut Ebpf, + call: &str, + fn_name: &str, + offset: u64, +) -> Result<(), anyhow::Error> { setbpf_effective(true)?; setadmin_effective(true)?; - let mut binding = bpf.as_ref().borrow_mut(); - let program: &mut KProbe = binding.program_mut("capable").unwrap().try_into()?; - program.unload()?; + let program: &mut KProbe = bpf.program_mut(call).unwrap().try_into()?; + program.load()?; + program.attach(fn_name, offset)?; setbpf_effective(false)?; setadmin_effective(false)?; Ok(()) @@ -538,6 +560,20 @@ fn unload_program(bpf: &Rc>) -> Result<(), anyhow::Error> { async fn main() -> Result<(), anyhow::Error> { env_logger::init(); ambient::clear().expect("Failed to clear ambiant caps"); + + if KernelVersion::current()?.code() != version::LINUX_VERSION_CODE { + let major = version::LINUX_VERSION_CODE >> 16; + let minor = (version::LINUX_VERSION_CODE >> 8) & 0xff; + let patch = version::LINUX_VERSION_CODE & 0xff; + let current = KernelVersion::current()?.code(); + let current_major = current >> 16; + let current_minor = (current >> 8) & 0xff; + let current_patch = current & 0xff; + warn!("This program was compiled for kernel version {}.{}.{}, but the current kernel version is {}.{}.{}", + major, minor, patch, current_major, current_minor, current_patch); + warn!("This may cause the program to fail or behave unexpectedly"); + } + let mut capstate = CapState::get_current().expect("Failed to get current cap"); capstate.inheritable = CapSet::empty(); capstate.effective = CapSet::empty(); @@ -564,114 +600,65 @@ async fn main() -> Result<(), anyhow::Error> { // like to specify the eBPF program at runtime rather than at compile-time, you can // reach for `Bpf::load_file` instead. #[cfg(debug_assertions)] - let bpf = Rc::new(RefCell::new(Ebpf::load(include_bytes_aligned!( + let mut bpf = Ebpf::load(include_bytes_aligned!( "../../target/bpfel-unknown-none/debug/capable" - ))?)); + ))?; #[cfg(not(debug_assertions))] - let bpf = Rc::new(RefCell::new(Ebpf::load(include_bytes_aligned!( + let mut bpf = Ebpf::load(include_bytes_aligned!( "../../target/bpfel-unknown-none/release/capable" - ))?)); + ))?; - if let Err(e) = EbpfLogger::init(&mut bpf.as_ref().borrow_mut()) { + if let Err(e) = EbpfLogger::init(&mut bpf) { // This can happen if you remove all log statements from your eBPF program. warn!("failed to initialize eBPF {}", e); } - let link = { - let mut binding = bpf.as_ref().borrow_mut(); - let program: &mut KProbe = binding.program_mut("capable").unwrap().try_into()?; - program.load()?; - program.attach("cap_capable", 0)? - }; - let err_bpf = bpf.clone(); - setbpf_effective(false).inspect_err(move |_| { - unload_program(&err_bpf).expect("failed to unload program"); - })?; - let err_bpf = bpf.clone(); - setadmin_effective(false).inspect_err(move |_| { - unload_program(&err_bpf).expect("failed to unload program"); - })?; - let err_bpf = bpf.clone(); - let mut cli_args = getopt(std::env::args()) - .inspect_err(move |_| { - unload_program(&err_bpf).expect("failed to unload program"); - }) - .map_err(|e| { - eprintln!("{}", e); + load_and_attach_program(&mut bpf, "capable", "try_capable", 0)?; + load_and_attach_program(&mut bpf, "may_open", "acl_may_open", 0)?; + load_and_attach_program(&mut bpf, "may_create", "acl_may_create", 0)?; + load_and_attach_program(&mut bpf, "may_delete", "acl_may_delete", 0)?; + load_and_attach_program(&mut bpf, "may_linkat", "acl_may_linkat", 0)?; + load_and_attach_program(&mut bpf, "may_lookup", "acl_may_lookup", 0)?; + load_and_attach_program(&mut bpf, "may_follow_link", "acl_may_follow_link", 0)?; + let config_map: Rc>> = Rc::new(RefCell::new(Array::try_from( + bpf.take_map("NAMESPACE_ID").unwrap(), + )?)); + let requests: Array<_, Request> = Array::try_from(bpf.map("REQUESTS").unwrap())?; + let capabilities_map: HashMap<_, Key, u64> = + HashMap::try_from(bpf.borrow().map("CAPABILITIES_MAP").unwrap())?; + let pnsid_nsid_map: HashMap<_, Key, u64> = + HashMap::try_from(bpf.borrow().map("PNSID_NSID_MAP").unwrap())?; + let uid_gid_map: HashMap<_, Key, u64> = + HashMap::try_from(bpf.borrow().map("UID_GID_MAP").unwrap())?; + let ppid_map: HashMap<_, Key, i32> = HashMap::try_from(bpf.map("PPID_MAP").unwrap())?; - exit(-1); - }) - .unwrap(); + setbpf_effective(false)?; + setadmin_effective(false)?; + let mut cli_args = getopt(std::env::args())?; { - let binding = bpf.as_ref().borrow(); - let err_bpf = bpf.clone(); - let capabilities_map: HashMap<_, Key, u64> = - HashMap::try_from(binding.map("CAPABILITIES_MAP").unwrap()).inspect_err(move |_| { - unload_program(&err_bpf).expect("failed to unload program"); - })?; - let err_bpf = bpf.clone(); - let pnsid_nsid_map: HashMap<_, Key, u64> = - HashMap::try_from(binding.map("PNSID_NSID_MAP").unwrap()).inspect_err(move |_| { - unload_program(&err_bpf).expect("failed to unload program"); - })?; - let err_bpf = bpf.clone(); - let uid_gid_map: HashMap<_, Key, u64> = - HashMap::try_from(binding.map("UID_GID_MAP").unwrap()).inspect_err(move |_| { - unload_program(&err_bpf).expect("failed to unload program"); - })?; - let err_bpf = bpf.clone(); - let ppid_map: HashMap<_, Key, i32> = HashMap::try_from(binding.map("PPID_MAP").unwrap()) - .inspect_err(move |_| { - unload_program(&err_bpf).expect("failed to unload program"); - })?; if cli_args.daemon || cli_args.command.is_empty() { println!("Waiting for Ctrl-C..."); - let err_bpf = bpf.clone(); - signal::ctrl_c().await.inspect_err(move |_| { - unload_program(&err_bpf).expect("failed to unload program"); - })?; - let err_bpf = bpf.clone(); + signal::ctrl_c().await?; print_all( &capabilities_map, &pnsid_nsid_map, &uid_gid_map, &ppid_map, cli_args.json, - ) - .inspect_err(move |_| { - unload_program(&err_bpf).expect("failed to unload program"); - })?; + )?; } else { let nsinode: Rc> = Rc::new(0.into()); - let err_bpf = bpf.clone(); - run_command(&mut cli_args, nsinode.clone()).inspect_err(move |_| { - unload_program(&err_bpf).expect("failed to unload program"); - })?; - let err_bpf = bpf.clone(); + run_command(&mut cli_args, nsinode.clone(), config_map)?; + print_program_capabilities( &nsinode.as_ref().borrow(), &capabilities_map, &pnsid_nsid_map, cli_args.json, ) - .inspect_err(move |_| { - unload_program(&err_bpf).expect("failed to unload program"); - }) .expect("failed to print capabilities"); } } - debug!("unloading program"); - let mut binding = bpf.as_ref().borrow_mut(); - let err_bpf = bpf.clone(); - let program: &mut KProbe = binding - .program_mut("capable") - .unwrap() - .try_into() - .inspect_err(move |_| { - unload_program(&err_bpf).expect("failed to unload program"); - })?; - program.detach(link)?; - program.unload()?; Ok(()) } diff --git a/capable/src/version.rs b/capable/src/version.rs new file mode 100644 index 00000000..2a035fe8 --- /dev/null +++ b/capable/src/version.rs @@ -0,0 +1 @@ +pub const LINUX_VERSION_CODE: u32 = 395521; \ No newline at end of file