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():
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:

View File

@ -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"
)
}
}
}

View File

@ -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)

View File

@ -8,6 +8,18 @@
#include <utils.hpp>
#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"
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;

View File

@ -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

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_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);
}
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