From 1fbb804f3550263e8a22cafadb17c984af4eef8a Mon Sep 17 00:00:00 2001 From: bjorn3 <17426603+bjorn3@users.noreply.github.com> Date: Mon, 14 Apr 2025 20:16:33 +0200 Subject: [PATCH] Introduce a sudo daemon as replacement for suid/escalated This daemon will request the password of the user, check that the user is in the sudo group and if everything checks out elevate the sudo process to root after which the sudo process can exec the target process. --- Cargo.lock | 23 ++++- Cargo.toml | 5 +- src/bin/sudo.rs | 255 ++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 235 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1fbddde057..920ed0df7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ansi_term" @@ -150,9 +150,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.159" +version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" [[package]] name = "libredox" @@ -211,6 +211,16 @@ dependencies = [ "libredox", ] +[[package]] +name = "redox-scheme" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96b9cfb034251dfb0aaa66a67059a7f0ea344039904d1d70cd36266af9c8a2f" +dependencies = [ + "libredox", + "redox_syscall", +] + [[package]] name = "redox_event" version = "0.4.1" @@ -236,9 +246,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" dependencies = [ "bitflags 2.6.0", ] @@ -382,11 +392,14 @@ version = "0.1.0" dependencies = [ "clap", "extra", + "libc", "libredox", "orbclient", "redox-daemon", + "redox-scheme", "redox_event", "redox_liner", + "redox_syscall", "redox_termios", "redox_users", "termion", diff --git a/Cargo.toml b/Cargo.toml index ef04651fa3..a9ec5d7caa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,11 +55,14 @@ path = "src/bin/usermod.rs" clap = "2.33.0" extra = { git = "https://gitlab.redox-os.org/redox-os/libextra.git" } orbclient = "0.3.47" -redox-daemon = "0.1.2" +redox-daemon = "0.1.3" redox_liner = "0.5.2" +libc = "0.2.171" libredox = "0.1.3" redox_termios = "0.1.3" redox_event = "0.4" +redox-scheme = "0.5.0" +redox_syscall = "0.5" redox_users = "0.4.6" termion = "4" diff --git a/src/bin/sudo.rs b/src/bin/sudo.rs index 2f81f4fd3a..3114a74f8a 100644 --- a/src/bin/sudo.rs +++ b/src/bin/sudo.rs @@ -1,10 +1,20 @@ +use std::collections::HashMap; use std::env; use std::io::{self, Write}; +use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}; use std::os::unix::process::CommandExt; use std::process::{Command, exit}; use extra::option::OptionalExt; +use libredox::flag::O_CLOEXEC; +use redox_scheme::scheme::SchemeSync; +use redox_scheme::{ + CallerCtx, OpenResult, RequestKind, Response, SendFdRequest, SignalBehavior, Socket, +}; use redox_users::{All, AllGroups, AllUsers, Config, get_uid}; +use syscall::error::*; +use syscall::flag::*; +use syscall::schemev2::NewFdFlags; use termion::input::TermRead; const MAX_ATTEMPTS: u16 = 3; @@ -27,42 +37,30 @@ EXIT STATUS the exit status will be >0. AUTHOR - Written by Jeremy Soller, Jose Narvaez. + Written by Jeremy Soller, Jose Narvaez, bjorn3. "#; /* @MANEND */ pub fn main() { + if env::args().nth(1).as_deref() == Some("--daemon") { + daemon_main(); + } + let mut args = env::args().skip(1); let cmd = args.next().unwrap_or_else(|| { eprintln!("sudo: no command provided"); exit(1); }); - let users = AllUsers::authenticator(Config::default()).unwrap_or_exit(1); - let groups = AllGroups::new(Config::default()).unwrap_or_exit(1); - + let users = AllUsers::basic(Config::default()).unwrap_or_exit(1); let uid = get_uid().unwrap_or_exit(1); - let user = users.get_by_id(uid).unwrap_or_exit(1); if uid == 0 { - run_command_as_root(&cmd, &args.collect::>()); - exit(0); + // We are root already. No need to elevate privileges + run_command_as_root(&cmd, &args.collect()); } - let sudo_group = groups.get_by_name("sudo").unwrap_or_exit(1); - if !sudo_group.users.iter().any(|name| name == &user.user) { - eprintln!("sudo: '{}' not in sudo group", user.user); - exit(1); - } - - if user.is_passwd_blank() { - // FIXME: We should not be doing this as provides access to any - // user w/o auth to run stuff as root. We should be doing something like: - // eprintln!("sudo: '{}' is in sudo group but does not have a password set", user.user); - // exit(1); - run_command_as_root(&cmd, &args.collect::>()); - exit(0); - } + let file = libredox::call::open("/scheme/sudo", O_CLOEXEC, 0).unwrap(); let mut attempts = 0; @@ -74,14 +72,19 @@ pub fn main() { Some(password) => { println!(); - if user.verify_passwd(&password) { - break; - } else { - attempts += 1; - eprintln!("sudo: incorrect password ({}/{})", attempts, MAX_ATTEMPTS); - if attempts >= MAX_ATTEMPTS { - exit(1); + match libredox::call::write(file, password.as_bytes()) { + Ok(_) => break, + Err(err) if err.errno() == EPERM => { + attempts += 1; + eprintln!( + "sudo: incorrect password or not in sudo group ({}/{})", + attempts, MAX_ATTEMPTS, + ); + if attempts >= MAX_ATTEMPTS { + exit(1); + } } + Err(err) => panic!("{err}"), } } None => { @@ -91,11 +94,33 @@ pub fn main() { } } - run_command_as_root(&cmd, &args.collect::>()); - exit(0); + // Elevate privileges of our own process with help from the sudo daemon + let self_proc = syscall::open("/scheme/thisproc/current/open_via_dup", 0).unwrap(); + syscall::sendfd(file, self_proc, 0, 0).unwrap(); + + run_command_as_root(&cmd, &args.collect()); } -fn run_command_as_root(cmd: &str, args: &Vec) { +enum Policy { + Deny, + Authenticate, +} + +fn policy_for_user(uid: u32) -> Policy { + let users = AllUsers::authenticator(Config::default()).unwrap_or_exit(1); + let groups = AllGroups::new(Config::default()).unwrap_or_exit(1); + + let user = users.get_by_id(uid as usize).unwrap_or_exit(1); + + let sudo_group = groups.get_by_name("sudo").unwrap_or_exit(1); + if !sudo_group.users.iter().any(|name| name == &user.user) { + return Policy::Deny; + } + + Policy::Authenticate +} + +fn run_command_as_root(cmd: &str, args: &Vec) -> ! { let mut command = Command::new(&cmd); for arg in args { command.arg(&arg); @@ -107,17 +132,163 @@ fn run_command_as_root(cmd: &str, args: &Vec) { command.env("UID", "0"); command.env("GROUPS", "0"); - match command.spawn() { - Ok(mut child) => match child.wait() { - Ok(status) => exit(status.code().unwrap_or(0)), - Err(err) => { - eprintln!("sudo: failed to wait for {}: {}", cmd, err); - exit(1); - } - }, - Err(err) => { - eprintln!("sudo: failed to execute {}: {}", cmd, err); - exit(1); + let err = command.exec(); + + eprintln!("sudo: failed to execute {}: {}", cmd, err); + exit(1); +} + +struct Scheme { + next_fd: usize, + handles: HashMap, +} +enum Handle { + AwaitingPassword { uid: u32 }, + AwaitingContextFd, + Placeholder, +} + +impl SchemeSync for Scheme { + fn open(&mut self, path: &str, _flags: usize, ctx: &CallerCtx) -> Result { + // Path must be empty + if path.trim_start_matches('/') != "" { + return Err(Error::new(ENOENT)); } + let fd = self.next_fd; + self.next_fd = self.next_fd.checked_add(1).ok_or(Error::new(EMFILE))?; + self.handles + .insert(fd, Handle::AwaitingPassword { uid: ctx.uid }); + + Ok(OpenResult::ThisScheme { + number: fd, + flags: NewFdFlags::empty(), + }) + } + + fn write( + &mut self, + id: usize, + buf: &[u8], + _off: u64, + _flags: u32, + _ctx: &CallerCtx, + ) -> Result { + let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?; + + let validate_utf8 = |buf| std::str::from_utf8(buf).map_err(|_| Error::new(EINVAL)); + + match std::mem::replace(handle, Handle::Placeholder) { + Handle::AwaitingPassword { uid } => { + let users = AllUsers::authenticator(Config::default()).unwrap_or_exit(1); + let user = users.get_by_id(uid as usize).unwrap_or_exit(1); + + match policy_for_user(uid) { + Policy::Deny => { + *handle = Handle::AwaitingPassword { uid }; + return Err(Error::new(EPERM)); + } + Policy::Authenticate => { + let password = validate_utf8(buf)?; + if user.verify_passwd(&password) { + *handle = Handle::AwaitingContextFd + } else { + *handle = Handle::AwaitingPassword { uid }; + return Err(Error::new(EPERM)); + } + } + } + } + Handle::AwaitingContextFd => { + *handle = Handle::AwaitingContextFd; + return Err(Error::new(EINVAL)); + } + + Handle::Placeholder => { + eprintln!("sudo: found placeholder handle with ID {id}"); + return Err(Error::new(EBADFD)); + } + } + Ok(buf.len()) } } +impl Scheme { + fn on_close(&mut self, id: usize) { + self.handles.remove(&id); + } + + fn on_sendfd(&mut self, socket: &Socket, req: &SendFdRequest) -> Result { + let handle = self.handles.get_mut(&req.id()).ok_or(Error::new(EBADF))?; + match std::mem::replace(handle, Handle::Placeholder) { + Handle::AwaitingContextFd => { + let mut context_file = usize::MAX; + req.obtain_fd(socket, FobtainFdFlags::empty(), Err(&mut context_file))?; + let context_file = unsafe { OwnedFd::from_raw_fd(context_file as RawFd) }; + + let context_uid_fd = unsafe { + OwnedFd::from_raw_fd( + syscall::dup(context_file.as_raw_fd() as usize, b"uid")? as RawFd + ) + }; + let context_gid_fd = unsafe { + OwnedFd::from_raw_fd( + syscall::dup(context_file.as_raw_fd() as usize, b"gid")? as RawFd + ) + }; + + // The caller was previously authenticated as member of the sudo group. Allow it to + // elevate privileges of a single process (likely itself). + syscall::write(context_uid_fd.as_raw_fd() as usize, b"0") + .map_err(|_| Error::new(EIO))?; + + syscall::write(context_gid_fd.as_raw_fd() as usize, b"0") + .map_err(|_| Error::new(EIO))?; + } + old => { + *handle = old; + return Err(Error::new(EBADF)); + } + } + Ok(0) + } +} + +fn daemon_main() -> ! { + redox_daemon::Daemon::new(move |daemon| { + // TODO: Linux kernel audit-like logging? + let socket = Socket::create("sudo").expect("failed to open scheme socket"); + + let mut scheme = Scheme { + next_fd: 1, + handles: HashMap::new(), + }; + + daemon + .ready() + .expect("failed to signal sudo scheme readiness"); + + loop { + let Some(req) = socket + .next_request(SignalBehavior::Restart) + .expect("failed to get request") + else { + break; + }; + + let response = match req.kind() { + RequestKind::Call(call) => call.handle_sync(&mut scheme), + RequestKind::SendFd(req) => Response::new(scheme.on_sendfd(&socket, &req), req), + RequestKind::OnClose { id } => { + scheme.on_close(id); + continue; + } + _ => continue, + }; + + socket + .write_response(response, SignalBehavior::Restart) + .expect("sudo: scheme write failed"); + } + std::process::exit(0) + }) + .expect("failed to start sudo daemon"); +}