diff --git a/app/core/src/main/java/com/topjohnwu/magisk/core/model/su/SuPolicy.kt b/app/core/src/main/java/com/topjohnwu/magisk/core/model/su/SuPolicy.kt index f4d1864fe..f39b28902 100644 --- a/app/core/src/main/java/com/topjohnwu/magisk/core/model/su/SuPolicy.kt +++ b/app/core/src/main/java/com/topjohnwu/magisk/core/model/su/SuPolicy.kt @@ -4,15 +4,16 @@ import com.topjohnwu.magisk.core.data.magiskdb.MagiskDB class SuPolicy( val uid: Int, - var policy: Int = INTERACTIVE, + var policy: Int = QUERY, var remain: Long = -1L, var logging: Boolean = true, var notification: Boolean = true, ) { companion object { - const val INTERACTIVE = 0 + const val QUERY = 0 const val DENY = 1 const val ALLOW = 2 + const val RESTRICT = 3 } fun toMap(): MutableMap { diff --git a/native/src/base/misc.cpp b/native/src/base/misc.cpp index b2a8ca2ca..39fc53657 100644 --- a/native/src/base/misc.cpp +++ b/native/src/base/misc.cpp @@ -184,6 +184,10 @@ int parse_int(string_view s) { return parse_num(s); } +uint32_t parse_uint32_hex(string_view s) { + return parse_num(s); +} + int switch_mnt_ns(int pid) { int ret = -1; int fd = syscall(__NR_pidfd_open, pid, 0); diff --git a/native/src/base/misc.hpp b/native/src/base/misc.hpp index affeb5226..ebd3de1fb 100644 --- a/native/src/base/misc.hpp +++ b/native/src/base/misc.hpp @@ -175,6 +175,7 @@ rust::Vec mut_u8_patch( rust::Slice from, rust::Slice to); +uint32_t parse_uint32_hex(std::string_view s); int parse_int(std::string_view s); using thread_entry = void *(*)(void *); diff --git a/native/src/core/lib.rs b/native/src/core/lib.rs index 5f9f5b7c2..e1a896c7b 100644 --- a/native/src/core/lib.rs +++ b/native/src/core/lib.rs @@ -88,6 +88,7 @@ pub mod ffi { Query, Deny, Allow, + Restrict, } struct ModuleInfo { @@ -117,6 +118,7 @@ pub mod ffi { target_pid: i32, login: bool, keep_env: bool, + drop_cap: bool, shell: String, command: String, context: String, diff --git a/native/src/core/selinux.rs b/native/src/core/selinux.rs index dd07f583e..5122ed23a 100644 --- a/native/src/core/selinux.rs +++ b/native/src/core/selinux.rs @@ -1,4 +1,4 @@ -use crate::consts::{DATABIN, LOG_PIPE, MAGISK_LOG_CON, MODULEROOT, SECURE_DIR}; +use crate::consts::{DATABIN, LOG_PIPE, MAGISK_LOG_CON, MAGISKDB, MODULEROOT, SECURE_DIR}; use crate::ffi::get_magisk_tmp; use base::libc::{O_CLOEXEC, O_WRONLY}; use base::{Directory, FsPathBuilder, LoggedResult, ResultExt, Utf8CStr, Utf8CStrBuf, cstr, libc}; @@ -69,6 +69,7 @@ pub(crate) fn restorecon() { path.clear(); path.push_str(DATABIN); restore_syscon(&mut path).log_ok(); + unsafe { libc::chmod(cstr!(MAGISKDB).as_ptr(), 0o000) }; } pub(crate) fn restore_tmpcon() -> LoggedResult<()> { diff --git a/native/src/core/su/daemon.rs b/native/src/core/su/daemon.rs index 35edd1e57..9074c5840 100644 --- a/native/src/core/su/daemon.rs +++ b/native/src/core/su/daemon.rs @@ -22,6 +22,7 @@ impl Default for SuRequest { target_pid: -1, login: false, keep_env: false, + drop_cap: false, shell: DEFAULT_SHELL.to_string(), command: "".to_string(), context: "".to_string(), @@ -168,7 +169,10 @@ impl MagiskD { // Before unlocking, refresh the timestamp access.refresh(); - // Fail fast + if access.settings.policy == SuPolicy::Restrict { + req.drop_cap = true; + } + if access.settings.policy == SuPolicy::Deny { warn!("su: request rejected ({})", info.uid); client.write_pod(&SuPolicy::Deny.repr).ok(); diff --git a/native/src/core/su/su.cpp b/native/src/core/su/su.cpp index b6add5eb5..b281ccce2 100644 --- a/native/src/core/su/su.cpp +++ b/native/src/core/su/su.cpp @@ -9,6 +9,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -46,6 +49,7 @@ int quit_signals[] = { SIGALRM, SIGABRT, SIGHUP, SIGPIPE, SIGQUIT, SIGTERM, SIGI " as a primary group if the option -g is not specified\n" " -Z, --context CONTEXT Change SELinux context\n" " -t, --target PID PID to take mount namespace from\n" + " -d, --drop-cap Drop all Linux capabilities\n" " -h, --help Display this help message and exit\n" " -, -l, --login Pretend the shell to be a login shell\n" " -m, -p,\n" @@ -105,6 +109,7 @@ int su_client_main(int argc, char *argv[]) { { "group", required_argument, nullptr, 'g' }, { "supp-group", required_argument, nullptr, 'G' }, { "interactive", no_argument, nullptr, 'i' }, + { "drop-cap", no_argument, nullptr, 'd' }, { nullptr, 0, nullptr, 0 }, }; @@ -121,7 +126,7 @@ int su_client_main(int argc, char *argv[]) { bool interactive = false; - while ((c = getopt_long(argc, argv, "c:hlimps:VvuZ:Mt:g:G:", long_opts, nullptr)) != -1) { + while ((c = getopt_long(argc, argv, "c:hlimpds:VvuZ:Mt:g:G:", long_opts, nullptr)) != -1) { switch (c) { case 'c': { string command; @@ -146,6 +151,9 @@ int su_client_main(int argc, char *argv[]) { case 'p': req.keep_env = true; break; + case 'd': + req.drop_cap = true; + break; case 's': req.shell = optarg; break; @@ -259,11 +267,66 @@ int su_client_main(int argc, char *argv[]) { return code; } -// Set effective uid back to root, otherwise setres[ug]id will fail if uid isn't root -static void set_identity(int uid, const rust::Vec &groups) { - if (seteuid(0)) { - PLOGE("seteuid (root)"); +static void drop_caps() { + static auto last_valid_cap = []() { + uint32_t cap = CAP_WAKE_ALARM; + while (prctl(PR_CAPBSET_READ, cap) >= 0) { + cap++; + } + return cap - 1; + }(); + // Drop bounding set + for (uint32_t cap = 0; cap <= last_valid_cap; cap++) { + if (cap != CAP_SETUID) { + prctl(PR_CAPBSET_DROP, cap); + } } + // Clean inheritable set + __user_cap_header_struct header = {.version = _LINUX_CAPABILITY_VERSION_3}; + __user_cap_data_struct data[_LINUX_CAPABILITY_U32S_3] = {}; + if (capget(&header, &data[0]) == 0) { + for (size_t i = 0; i < _LINUX_CAPABILITY_U32S_3; i++) { + data[i].inheritable = 0; + } + capset(&header, &data[0]); + } + // All capabilities will be lost after exec + prctl(PR_SET_SECUREBITS, SECBIT_NOROOT); + // Except CAP_SETUID in bounding set, it is a marker for restricted process +} + +static bool proc_is_restricted(pid_t pid) { + char buf[32] = {}; + auto bnd = "CapBnd:"sv; + uint32_t data[_LINUX_CAPABILITY_U32S_3] = {}; + ssprintf(buf, sizeof(buf), "/proc/%d/status", pid); + file_readline(buf, [&](string_view line) -> bool { + if (line.starts_with(bnd)) { + auto p = line.begin(); + advance(p, bnd.size()); + while (isspace(*p)) advance(p, 1); + line.remove_prefix(distance(line.begin(), p)); + for (int i = 0; i < _LINUX_CAPABILITY_U32S_3; i++) { + auto cap = line.substr((_LINUX_CAPABILITY_U32S_3 - 1 - i) * 8, 8); + data[i] = parse_uint32_hex(cap); + } + return false; + } + return true; + }); + + bool equal = true; + for (int i = 0; i < _LINUX_CAPABILITY_U32S_3; i++) { + if (i == CAP_TO_INDEX(CAP_SETUID)) { + if (data[i] != CAP_TO_MASK(CAP_SETUID)) equal = false; + } else { + if (data[i] != 0) equal = false; + } + } + return equal; +} + +static void set_identity(int uid, const rust::Vec &groups) { gid_t gid; if (!groups.empty()) { if (setgroups(groups.size(), groups.data())) { @@ -404,15 +467,21 @@ void exec_root_shell(int client, int pid, SuRequest &req, MntNsMode mode) { } } - // Unblock all signals - sigset_t block_set; - sigemptyset(&block_set); - sigprocmask(SIG_SETMASK, &block_set, nullptr); + // Config privileges if (!req.context.empty()) { auto f = xopen_file("/proc/self/attr/exec", "we"); if (f) fprintf(f.get(), "%s", req.context.c_str()); } - set_identity(req.target_uid, req.gids); + if (req.target_uid != AID_ROOT || req.drop_cap || proc_is_restricted(pid)) + drop_caps(); + if (req.target_uid != AID_ROOT || req.gids.size() > 0) + set_identity(req.target_uid, req.gids); + + // Unblock all signals + sigset_t block_set; + sigemptyset(&block_set); + sigprocmask(SIG_SETMASK, &block_set, nullptr); + execvp(req.shell.c_str(), (char **) argv); fprintf(stderr, "Cannot execute %s: %s\n", req.shell.c_str(), strerror(errno)); PLOGE("exec"); diff --git a/native/src/include/consts.rs b/native/src/include/consts.rs index 746735e71..789f909f5 100644 --- a/native/src/include/consts.rs +++ b/native/src/include/consts.rs @@ -16,6 +16,7 @@ pub const LOGFILE: &str = "/cache/magisk.log"; pub const SECURE_DIR: &str = "/data/adb"; pub const MODULEROOT: &str = concatcp!(SECURE_DIR, "/modules"); pub const DATABIN: &str = concatcp!(SECURE_DIR, "/magisk"); +pub const MAGISKDB: &str = concatcp!(SECURE_DIR, "/magisk.db"); // tmpfs paths const INTERNAL_DIR: &str = ".magisk";