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.
This commit is contained in:
bjorn3
2025-04-14 20:16:33 +02:00
parent bd7592270d
commit 1fbb804f35
3 changed files with 235 additions and 48 deletions
Generated
+18 -5
View File
@@ -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",
+4 -1
View File
@@ -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"
+213 -42
View File
@@ -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::<Vec<String>>());
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::<Vec<String>>());
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::<Vec<String>>());
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<String>) {
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<String>) -> ! {
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<String>) {
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<usize, Handle>,
}
enum Handle {
AwaitingPassword { uid: u32 },
AwaitingContextFd,
Placeholder,
}
impl SchemeSync for Scheme {
fn open(&mut self, path: &str, _flags: usize, ctx: &CallerCtx) -> Result<OpenResult> {
// 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<usize> {
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<usize> {
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");
}