Use LD_PRELOAD to intercept sepolicy on 2SI init

This commit is contained in:
topjohnwu 2022-04-08 02:13:31 -07:00
parent f24d52436b
commit ff2513e276
7 changed files with 96 additions and 53 deletions

View File

@ -184,7 +184,7 @@ def load_config(args):
def collect_binary(): def collect_binary():
for arch in archs: for arch in archs:
mkdir_p(op.join('native', 'out', arch)) 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) source = op.join('native', 'libs', arch, bin)
target = op.join('native', 'out', arch, bin) target = op.join('native', 'out', arch, bin)
mv(source, target) mv(source, target)
@ -284,6 +284,11 @@ def dump_bin_header():
with open(stub, 'rb') as src: with open(stub, 'rb') as src:
text = binary_dump(src, 'manager_xz') text = binary_dump(src, 'manager_xz')
write_if_diff(op.join(native_gen_path, 'binaries.h'), text) 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(): def dump_flag_header():
@ -322,6 +327,8 @@ def build_binary(args):
dump_flag_header() dump_flag_header()
# Build shared executables
flag = '' flag = ''
if 'magisk' in args.target: if 'magisk' in args.target:
@ -333,10 +340,15 @@ def build_binary(args):
if 'test' in args.target: if 'test' in args.target:
flag += ' B_TEST=1' flag += ' B_TEST=1'
if 'magiskinit' in args.target:
flag += ' B_PRELOAD=1'
if flag: if flag:
run_ndk_build(flag + ' B_SHARED=1') run_ndk_build(flag + ' B_SHARED=1')
clean_elf() clean_elf()
# Then build static executables
flag = '' flag = ''
if 'magiskinit' in args.target: if 'magiskinit' in args.target:

View File

@ -15,8 +15,10 @@ android {
externalNativeBuild { externalNativeBuild {
ndkBuild { ndkBuild {
// Pass arguments to ndk-build. // Pass arguments to ndk-build.
arguments("B_MAGISK=1", "B_INIT=1", "B_BOOT=1", "B_TEST=1", "B_POLICY=1", arguments(
"MAGISK_DEBUG=1", "MAGISK_VERSION=debug", "MAGISK_VER_CODE=INT_MAX") "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"
)
} }
} }
} }

View File

@ -48,6 +48,15 @@ include $(BUILD_EXECUTABLE)
endif endif
ifdef B_PRELOAD
include $(CLEAR_VARS)
LOCAL_MODULE := preload
LOCAL_SRC_FILES := init/preload.c
include $(BUILD_SHARED_LIBRARY)
endif
ifdef B_INIT ifdef B_INIT
include $(CLEAR_VARS) include $(CLEAR_VARS)

View File

@ -8,6 +8,18 @@
#include <utils.hpp> #include <utils.hpp>
#include <binaries.h> #include <binaries.h>
#if defined(__arm__)
#include <armeabi-v7a_binaries.h>
#elif defined(__aarch64__)
#include <arm64-v8a_binaries.h>
#elif defined(__i386__)
#include <x86_binaries.h>
#elif defined(__x86_64__)
#include <x86_64_binaries.h>
#else
#error Unsupported ABI
#endif
#include "init.hpp" #include "init.hpp"
using namespace std; using namespace std;
@ -35,16 +47,24 @@ bool unxz(int fd, const uint8_t *buf, size_t size) {
return true; 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); int fd = xopen(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, mode);
if (fd < 0) if (fd < 0)
return 1; return 1;
if (!unxz(fd, manager_xz, sizeof(manager_xz))) if (!unxz(fd, buf, sz))
return 1; return 1;
close(fd); close(fd);
return 0; 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 { class RecoveryInit : public BaseInit {
public: public:
using BaseInit::BaseInit; using BaseInit::BaseInit;

View File

@ -33,6 +33,7 @@ bool check_two_stage();
void setup_klog(); void setup_klog();
const char *backup_init(); const char *backup_init();
int dump_manager(const char *path, mode_t mode); int dump_manager(const char *path, mode_t mode);
int dump_preload(const char *path, mode_t mode);
/*************** /***************
* Base classes * Base classes

23
native/jni/init/preload.c Normal file
View File

@ -0,0 +1,23 @@
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <dlfcn.h>
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;
}

View File

@ -39,37 +39,19 @@ void MagiskInit::patch_sepolicy(const char *file) {
#define MOCK_COMPAT SELINUXMOCK "/compatible" #define MOCK_COMPAT SELINUXMOCK "/compatible"
#define MOCK_LOAD SELINUXMOCK "/load" #define MOCK_LOAD SELINUXMOCK "/load"
#define MOCK_BLOCKING SELINUXMOCK "/blocking" #define MOCK_ENFORCE SELINUXMOCK "/enforce"
#define REAL_SELINUXFS SELINUXMOCK "/fs" #define REAL_SELINUXFS SELINUXMOCK "/fs"
bool MagiskInit::hijack_sepolicy() { bool MagiskInit::hijack_sepolicy() {
const char *blocking_target;
string actual_content;
xmkdir(SELINUXMOCK, 0); xmkdir(SELINUXMOCK, 0);
if (access("/system/etc/selinux/apex", F_OK) == 0) { if (access("/system/bin/init", F_OK) == 0) {
// On devices with apex sepolicy, it runs restorecon before enforcing SELinux. // On 2SI devices, the 2nd stage init file is always a dynamic executable.
// To block control flow before that happens, we will have to hijack the // This meant that instead of going through convoluted methods trying to alter
// plat_file_contexts file to achieve that. // and block init's control flow, we can just LD_PRELOAD and replace the
// security_load_policy function with our own implementation.
if (access("/system/etc/selinux/plat_file_contexts", F_OK) == 0) { dump_preload("/dev/preload.so", 0644);
blocking_target = "/system/etc/selinux/plat_file_contexts"; setenv("LD_PRELOAD", "/dev/preload.so", 1);
} 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;
} }
// Hijack the "load" and "enforce" node in selinuxfs to manipulate // Hijack the "load" and "enforce" node in selinuxfs to manipulate
@ -78,31 +60,28 @@ bool MagiskInit::hijack_sepolicy() {
LOGD("Hijack [" SELINUX_LOAD "]\n"); LOGD("Hijack [" SELINUX_LOAD "]\n");
mkfifo(MOCK_LOAD, 0600); mkfifo(MOCK_LOAD, 0600);
xmount(MOCK_LOAD, SELINUX_LOAD, nullptr, MS_BIND, nullptr); xmount(MOCK_LOAD, SELINUX_LOAD, nullptr, MS_BIND, nullptr);
if (strcmp(blocking_target, SELINUX_ENFORCE) == 0) {
LOGD("Hijack [" SELINUX_ENFORCE "]\n"); LOGD("Hijack [" SELINUX_ENFORCE "]\n");
mkfifo(MOCK_BLOCKING, 0644); mkfifo(MOCK_ENFORCE, 0644);
xmount(MOCK_BLOCKING, SELINUX_ENFORCE, nullptr, MS_BIND, nullptr); xmount(MOCK_ENFORCE, SELINUX_ENFORCE, nullptr, MS_BIND, nullptr);
}
}; };
string dt_compat; string dt_compat;
if (access(SELINUX_ENFORCE, F_OK) != 0) { if (access(SELINUX_ENFORCE, F_OK) != 0) {
// selinuxfs not mounted yet. Hijack the dt fstab nodes first // 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 // 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]; char buf[4096];
snprintf(buf, sizeof(buf), "%s/fstab/compatible", config->dt_dir); snprintf(buf, sizeof(buf), "%s/fstab/compatible", config->dt_dir);
dt_compat = full_read(buf); dt_compat = full_read(buf);
if (dt_compat.empty()) { 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; return false;
} }
// Remount procfs with proper options
xmount(nullptr, "/proc", nullptr, MS_REMOUNT, "hidepid=2,gid=3009");
LOGD("Hijack [%s]\n", buf); LOGD("Hijack [%s]\n", buf);
// Preserve sysfs and procfs for hijacking // Preserve sysfs and procfs for hijacking
@ -161,31 +140,28 @@ bool MagiskInit::hijack_sepolicy() {
sepol->magisk_rules(); sepol->magisk_rules();
sepol->load_rules(rules); sepol->load_rules(rules);
// This open will block until init calls security_getenforce or selinux_android_restorecon // This open will block until init calls security_getenforce
fd = xopen(MOCK_BLOCKING, O_WRONLY); fd = xopen(MOCK_ENFORCE, O_WRONLY);
// Cleanup the hijacks // Cleanup the hijacks
umount2("/init", MNT_DETACH); umount2("/init", MNT_DETACH);
xumount2(SELINUX_LOAD, MNT_DETACH); xumount2(SELINUX_LOAD, MNT_DETACH);
xumount2(blocking_target, MNT_DETACH); xumount2(SELINUX_ENFORCE, MNT_DETACH);
// Load patched policy // Load patched policy
xmkdir(REAL_SELINUXFS, 0755); xmkdir(REAL_SELINUXFS, 0755);
xmount("selinuxfs", REAL_SELINUXFS, "selinuxfs", 0, nullptr); xmount("selinuxfs", REAL_SELINUXFS, "selinuxfs", 0, nullptr);
sepol->to_file(REAL_SELINUXFS "/load"); sepol->to_file(REAL_SELINUXFS "/load");
string enforce = full_read(SELINUX_ENFORCE);
if (strcmp(blocking_target, SELINUX_ENFORCE) == 0) { // Write to the enforce node ONLY after sepolicy is loaded. We need to make sure
actual_content = full_read(SELINUX_ENFORCE);
}
// Write to mock blocking target ONLY after sepolicy is loaded. We need to make sure
// the actual init process is blocked until sepolicy is loaded, or else // 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. // 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 // We (ab)use the fact that init reads the enforce node, and because
// because it has been replaced with our FIFO file, init will block until we // it has been replaced with our FIFO file, init will block until we
// write something into the pipe, effectively hijacking its control flow. // 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); close(fd);
// At this point, the init process will be unblocked // At this point, the init process will be unblocked