diff --git a/build.py b/build.py index 4cf96de3d..64829287c 100755 --- a/build.py +++ b/build.py @@ -184,7 +184,7 @@ def load_config(args): def collect_binary(): for arch in archs: mkdir_p(op.join('native', 'out', arch)) - for bin in support_targets: + for bin in support_targets + ['libpreload.so']: source = op.join('native', 'libs', arch, bin) target = op.join('native', 'out', arch, bin) mv(source, target) @@ -284,6 +284,11 @@ def dump_bin_header(): with open(stub, 'rb') as src: text = binary_dump(src, 'manager_xz') write_if_diff(op.join(native_gen_path, 'binaries.h'), text) + for arch in archs: + preload = op.join('native', 'out', arch, 'libpreload.so') + with open(preload, 'rb') as src: + text = binary_dump(src, 'preload_xz') + write_if_diff(op.join(native_gen_path, f'{arch}_binaries.h'), text) def dump_flag_header(): @@ -322,6 +327,8 @@ def build_binary(args): dump_flag_header() + # Build shared executables + flag = '' if 'magisk' in args.target: @@ -333,10 +340,15 @@ def build_binary(args): if 'test' in args.target: flag += ' B_TEST=1' + if 'magiskinit' in args.target: + flag += ' B_PRELOAD=1' + if flag: run_ndk_build(flag + ' B_SHARED=1') clean_elf() + # Then build static executables + flag = '' if 'magiskinit' in args.target: diff --git a/native/build.gradle.kts b/native/build.gradle.kts index 8df092837..da5aabf50 100644 --- a/native/build.gradle.kts +++ b/native/build.gradle.kts @@ -15,8 +15,10 @@ android { externalNativeBuild { ndkBuild { // Pass arguments to ndk-build. - arguments("B_MAGISK=1", "B_INIT=1", "B_BOOT=1", "B_TEST=1", "B_POLICY=1", - "MAGISK_DEBUG=1", "MAGISK_VERSION=debug", "MAGISK_VER_CODE=INT_MAX") + arguments( + "B_MAGISK=1", "B_INIT=1", "B_BOOT=1", "B_TEST=1", "B_POLICY=1", "B_PRELOAD=1", + "MAGISK_DEBUG=1", "MAGISK_VERSION=debug", "MAGISK_VER_CODE=INT_MAX" + ) } } } diff --git a/native/jni/Android.mk b/native/jni/Android.mk index a979708cd..643fc0bee 100644 --- a/native/jni/Android.mk +++ b/native/jni/Android.mk @@ -48,6 +48,15 @@ include $(BUILD_EXECUTABLE) endif +ifdef B_PRELOAD + +include $(CLEAR_VARS) +LOCAL_MODULE := preload +LOCAL_SRC_FILES := init/preload.c +include $(BUILD_SHARED_LIBRARY) + +endif + ifdef B_INIT include $(CLEAR_VARS) diff --git a/native/jni/init/init.cpp b/native/jni/init/init.cpp index e248f6c72..713213800 100644 --- a/native/jni/init/init.cpp +++ b/native/jni/init/init.cpp @@ -8,6 +8,18 @@ #include #include +#if defined(__arm__) +#include +#elif defined(__aarch64__) +#include +#elif defined(__i386__) +#include +#elif defined(__x86_64__) +#include +#else +#error Unsupported ABI +#endif + #include "init.hpp" using namespace std; @@ -35,16 +47,24 @@ bool unxz(int fd, const uint8_t *buf, size_t size) { return true; } -int dump_manager(const char *path, mode_t mode) { +static int dump_bin(const uint8_t *buf, size_t sz, const char *path, mode_t mode) { int fd = xopen(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, mode); if (fd < 0) return 1; - if (!unxz(fd, manager_xz, sizeof(manager_xz))) + if (!unxz(fd, buf, sz)) return 1; close(fd); return 0; } +int dump_manager(const char *path, mode_t mode) { + return dump_bin(manager_xz, sizeof(manager_xz), path, mode); +} + +int dump_preload(const char *path, mode_t mode) { + return dump_bin(preload_xz, sizeof(preload_xz), path, mode); +} + class RecoveryInit : public BaseInit { public: using BaseInit::BaseInit; diff --git a/native/jni/init/init.hpp b/native/jni/init/init.hpp index 192bbc2f9..f87cdaa29 100644 --- a/native/jni/init/init.hpp +++ b/native/jni/init/init.hpp @@ -33,6 +33,7 @@ bool check_two_stage(); void setup_klog(); const char *backup_init(); int dump_manager(const char *path, mode_t mode); +int dump_preload(const char *path, mode_t mode); /*************** * Base classes diff --git a/native/jni/init/preload.c b/native/jni/init/preload.c new file mode 100644 index 000000000..2751b366c --- /dev/null +++ b/native/jni/init/preload.c @@ -0,0 +1,23 @@ +#include +#include +#include +#include + +int security_load_policy(void *data, size_t len) { + // Make sure our next exec won't get bugged + unsetenv("LD_PRELOAD"); + unlink("/dev/preload.so"); + + int (*load_policy)(void *, size_t) = dlsym(RTLD_NEXT, "security_load_policy"); + // Skip checking errors, because if we cannot find the symbol, there + // isn't much we can do other than crashing anyways. + int result = load_policy(data, len); + + // Wait for ack + int fd = open("/sys/fs/selinux/enforce", O_RDONLY); + char c; + read(fd, &c, 1); + close(fd); + + return result; +} diff --git a/native/jni/init/selinux.cpp b/native/jni/init/selinux.cpp index ead93ffcf..e5261c844 100644 --- a/native/jni/init/selinux.cpp +++ b/native/jni/init/selinux.cpp @@ -39,37 +39,19 @@ void MagiskInit::patch_sepolicy(const char *file) { #define MOCK_COMPAT SELINUXMOCK "/compatible" #define MOCK_LOAD SELINUXMOCK "/load" -#define MOCK_BLOCKING SELINUXMOCK "/blocking" +#define MOCK_ENFORCE SELINUXMOCK "/enforce" #define REAL_SELINUXFS SELINUXMOCK "/fs" bool MagiskInit::hijack_sepolicy() { - const char *blocking_target; - string actual_content; - xmkdir(SELINUXMOCK, 0); - if (access("/system/etc/selinux/apex", F_OK) == 0) { - // On devices with apex sepolicy, it runs restorecon before enforcing SELinux. - // To block control flow before that happens, we will have to hijack the - // plat_file_contexts file to achieve that. - - if (access("/system/etc/selinux/plat_file_contexts", F_OK) == 0) { - blocking_target = "/system/etc/selinux/plat_file_contexts"; - } else if (access("/plat_file_contexts", F_OK) == 0) { - blocking_target = "/plat_file_contexts"; - } else { - // Error, should never happen - LOGE("! Cannot find plat_file_contexts\n"); - return false; - } - actual_content = full_read(blocking_target); - - LOGD("Hijack [%s]\n", blocking_target); - mkfifo(MOCK_BLOCKING, 0644); - xmount(MOCK_BLOCKING, blocking_target, nullptr, MS_BIND, nullptr); - } else { - // We block using the "enforce" node - blocking_target = SELINUX_ENFORCE; + if (access("/system/bin/init", F_OK) == 0) { + // On 2SI devices, the 2nd stage init file is always a dynamic executable. + // This meant that instead of going through convoluted methods trying to alter + // and block init's control flow, we can just LD_PRELOAD and replace the + // security_load_policy function with our own implementation. + dump_preload("/dev/preload.so", 0644); + setenv("LD_PRELOAD", "/dev/preload.so", 1); } // Hijack the "load" and "enforce" node in selinuxfs to manipulate @@ -78,31 +60,28 @@ bool MagiskInit::hijack_sepolicy() { LOGD("Hijack [" SELINUX_LOAD "]\n"); mkfifo(MOCK_LOAD, 0600); xmount(MOCK_LOAD, SELINUX_LOAD, nullptr, MS_BIND, nullptr); - if (strcmp(blocking_target, SELINUX_ENFORCE) == 0) { - LOGD("Hijack [" SELINUX_ENFORCE "]\n"); - mkfifo(MOCK_BLOCKING, 0644); - xmount(MOCK_BLOCKING, SELINUX_ENFORCE, nullptr, MS_BIND, nullptr); - } + LOGD("Hijack [" SELINUX_ENFORCE "]\n"); + mkfifo(MOCK_ENFORCE, 0644); + xmount(MOCK_ENFORCE, SELINUX_ENFORCE, nullptr, MS_BIND, nullptr); }; string dt_compat; if (access(SELINUX_ENFORCE, F_OK) != 0) { // selinuxfs not mounted yet. Hijack the dt fstab nodes first - // and let the original init mount selinuxfs for us + // and let the original init mount selinuxfs for us. // This only happens on Android 8.0 - 9.0 - // Remount procfs with proper options - xmount(nullptr, "/proc", nullptr, MS_REMOUNT, "hidepid=2,gid=3009"); - char buf[4096]; snprintf(buf, sizeof(buf), "%s/fstab/compatible", config->dt_dir); dt_compat = full_read(buf); - if (dt_compat.empty()) { - // Device does not do early mount and apparently use monolithic policy + // Device does not do early mount and uses monolithic policy return false; } + // Remount procfs with proper options + xmount(nullptr, "/proc", nullptr, MS_REMOUNT, "hidepid=2,gid=3009"); + LOGD("Hijack [%s]\n", buf); // Preserve sysfs and procfs for hijacking @@ -161,31 +140,28 @@ bool MagiskInit::hijack_sepolicy() { sepol->magisk_rules(); sepol->load_rules(rules); - // This open will block until init calls security_getenforce or selinux_android_restorecon - fd = xopen(MOCK_BLOCKING, O_WRONLY); + // This open will block until init calls security_getenforce + fd = xopen(MOCK_ENFORCE, O_WRONLY); // Cleanup the hijacks umount2("/init", MNT_DETACH); xumount2(SELINUX_LOAD, MNT_DETACH); - xumount2(blocking_target, MNT_DETACH); + xumount2(SELINUX_ENFORCE, MNT_DETACH); // Load patched policy xmkdir(REAL_SELINUXFS, 0755); xmount("selinuxfs", REAL_SELINUXFS, "selinuxfs", 0, nullptr); sepol->to_file(REAL_SELINUXFS "/load"); + string enforce = full_read(SELINUX_ENFORCE); - if (strcmp(blocking_target, SELINUX_ENFORCE) == 0) { - actual_content = full_read(SELINUX_ENFORCE); - } - - // Write to mock blocking target ONLY after sepolicy is loaded. We need to make sure + // Write to the enforce node ONLY after sepolicy is loaded. We need to make sure // the actual init process is blocked until sepolicy is loaded, or else // restorecon will fail and re-exec won't change context, causing boot failure. - // We (ab)use the fact that init reads from our blocking target, and - // because it has been replaced with our FIFO file, init will block until we + // We (ab)use the fact that init reads the enforce node, and because + // it has been replaced with our FIFO file, init will block until we // write something into the pipe, effectively hijacking its control flow. - xwrite(fd, actual_content.data(), actual_content.length()); + xwrite(fd, enforce.data(), enforce.length()); close(fd); // At this point, the init process will be unblocked