Compare commits

...

1 Commits

Author SHA1 Message Date
Andrew Gunnerson 461392c226 [BROKEN] Proof of concept implementation of option 5
This adds a wrapper for /init that spawns a child process to ptrace the
parent. When stage 1 execs stage 2, the tracer will bind mount the new
otacerts.zip, detach, and exit.

The mount process works, but stage 2 panics and reboots to the
bootloader for unknown reasons. The conditions that lead to the reboot
don't result in the kernel log being preserved.

Issue: #225

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-16 00:20:36 -05:00
5 changed files with 424 additions and 15 deletions
+80 -13
View File
@@ -452,25 +452,28 @@ impl OtaCertPatcher {
Ok(certificates)
}
/// Create a new otacerts archive. The old certs are ignored since flashing
/// a stock OTA will render the device unbootable.
fn create_zip(cert: &Certificate) -> Result<Vec<u8>> {
let raw_writer = Cursor::new(Vec::new());
let mut writer = ZipWriter::new(raw_writer);
let options = FileOptions::default().compression_method(CompressionMethod::Stored);
writer.start_file("ota.x509.pem", options)?;
crypto::write_pem_cert(&mut writer, cert)?;
let raw_writer = writer.finish()?;
Ok(raw_writer.into_inner())
}
fn patch_ramdisk(&self, data: &mut Vec<u8>, cancel_signal: &AtomicBool) -> Result<bool> {
let (mut entries, ramdisk_format) = load_ramdisk(data, cancel_signal)?;
let Some(entry) = entries.iter_mut().find(|e| e.path == Self::OTACERTS_PATH) else {
return Ok(false);
};
// Create a new otacerts archive. The old certs are ignored since
// flashing a stock OTA will render the device unbootable.
{
let raw_writer = Cursor::new(Vec::new());
let mut writer = ZipWriter::new(raw_writer);
let options = FileOptions::default().compression_method(CompressionMethod::Stored);
writer.start_file("ota.x509.pem", options)?;
crypto::write_pem_cert(&mut writer, &self.cert)?;
let raw_writer = writer.finish()?;
entry.data = CpioEntryData::Data(raw_writer.into_inner());
}
entry.data = CpioEntryData::Data(Self::create_zip(&self.cert)?);
// Repack ramdisk.
*data = save_ramdisk(&entries, ramdisk_format, cancel_signal)?;
@@ -511,6 +514,70 @@ impl BootImagePatcher for OtaCertPatcher {
}
}
/// Replace /init with a wrapper that will bind mount otacerts.zip containing
/// the custom OTA certificate into the system partition.
pub struct InitWrapperPatcher {
cert: Certificate,
}
impl InitWrapperPatcher {
pub fn new(cert: Certificate) -> Self {
Self { cert }
}
fn patch_ramdisk(&self, data: &mut Vec<u8>, cancel_signal: &AtomicBool) -> Result<bool> {
let (mut entries, ramdisk_format) = load_ramdisk(data, cancel_signal)?;
if let Some(entry) = entries.iter_mut().find(|e| e.path == b"init") {
entry.path = b"avbroot/init.orig".to_vec();
} else {
return Ok(false);
};
entries.push(CpioEntry::new_directory(b"avbroot", 0o755));
entries.push(CpioEntry::new_file(b"avbroot/otacerts.zip", 0o644,
CpioEntryData::Data(OtaCertPatcher::create_zip(&self.cert)?)));
// TODO
// How do we pick ABI
let init_data = std::fs::read("/home/chenxiaolong/git/github/avbroot/init/libs/arm64-v8a/init").unwrap();
entries.push(CpioEntry::new_file(b"init", 0o750, CpioEntryData::Data(init_data)));
cpio::sort(&mut entries);
*data = save_ramdisk(&entries, ramdisk_format, cancel_signal)?;
Ok(true)
}
}
impl BootImagePatcher for InitWrapperPatcher {
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &AtomicBool) -> Result<()> {
let patched_any = match boot_image {
BootImage::V0Through2(b) => self.patch_ramdisk(&mut b.ramdisk, cancel_signal)?,
BootImage::V3Through4(b) => self.patch_ramdisk(&mut b.ramdisk, cancel_signal)?,
BootImage::VendorV3Through4(b) => {
let mut patched = false;
for ramdisk in &mut b.ramdisks {
if self.patch_ramdisk(ramdisk, cancel_signal)? {
patched = true;
break;
}
}
patched
}
};
if !patched_any {
return Err(Error::Validation("No ramdisk contains init".to_owned()));
}
Ok(())
}
}
/// Replace the boot image with a prepatched boot image if it is compatible.
///
/// An image is compatible if all the non-size-related header fields are
+8 -2
View File
@@ -28,7 +28,7 @@ use x509_cert::Certificate;
use zip::{write::FileOptions, CompressionMethod, ZipArchive, ZipWriter};
use crate::{
boot::{self, BootImagePatcher, MagiskRootPatcher, OtaCertPatcher, PrepatchedImagePatcher},
boot::{self, BootImagePatcher, MagiskRootPatcher, OtaCertPatcher, PrepatchedImagePatcher, InitWrapperPatcher},
cli::{self, status, warning},
crypto::{self, PassphraseSource},
format::{
@@ -127,7 +127,7 @@ pub fn get_required_images(
let mut images = HashMap::new();
for (k, v) in &by_type {
if k == "@otacerts" || k.starts_with("@vbmeta:") {
if k == "@otacerts" || k == "@gki_ramdisk" || k.starts_with("@vbmeta:") {
images.insert(k.clone(), v.clone());
}
}
@@ -214,6 +214,12 @@ fn patch_boot_images(
.push(p);
}
// We want our init wrapper to be the entry point, so do this last.
boot_patchers
.entry(&required_images["@gki_ramdisk"])
.or_default()
.push(Box::new(InitWrapperPatcher::new(cert_ota.clone())));
status!(
"Patching boot images: {}",
joined(sorted(boot_patchers.keys()))
+2
View File
@@ -0,0 +1,2 @@
/libs/
/obj/
+7
View File
@@ -0,0 +1,7 @@
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := init
LOCAL_SRC_FILES := init.c
LOCAL_LDFLAGS := -static
include $(BUILD_EXECUTABLE)
+327
View File
@@ -0,0 +1,327 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/ptrace.h>
#include <sys/sendfile.h>
#include <sys/stat.h>
#include <sys/sysmacros.h>
#include <sys/wait.h>
#include <unistd.h>
#ifndef DEBUG_PREFIX
#define DEBUG_PREFIX ""
#endif
#define AVBROOT_DIR DEBUG_PREFIX "/avbroot"
#define DEV_DIR DEBUG_PREFIX "/dev"
#define SAFE_DIR DEBUG_PREFIX "/acct"
#define STAGE1_DIR DEBUG_PREFIX "/first_stage_ramdisk"
#define DEV_KMSG DEV_DIR "/kmsg"
#define DEV_NULL DEV_DIR "/null"
#define INIT DEBUG_PREFIX "/init"
#define INIT_ORIG AVBROOT_DIR "/init.orig"
#define OTACERTS DEBUG_PREFIX "/system/etc/security/otacerts.zip"
#define OTACERTS_AVBROOT AVBROOT_DIR "/otacerts.zip"
#define OTACERTS_TMPFS SAFE_DIR "/otacerts.zip"
#define LOG(level, fmt, ...) \
fprintf(stderr, "<%d>[%d] " fmt, level, getpid(), ##__VA_ARGS__)
#define LOGE(...) LOG(3, __VA_ARGS__)
#define LOGI(...) LOG(6, __VA_ARGS__)
// Best effort attempt to output to the kernel log.
static void prepare_output()
{
mknod(DEV_NULL, S_IFCHR | 0666, makedev(1, 3));
mknod(DEV_KMSG, S_IFCHR | 0600, makedev(1, 11));
int fd = open(DEV_NULL, O_RDWR);
if (fd >= 0) {
dup2(fd, STDIN_FILENO);
close(fd);
}
fd = open(DEV_KMSG, O_WRONLY);
if (fd >= 0) {
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
close(fd);
}
unlink(DEV_NULL);
unlink(DEV_KMSG);
setlinebuf(stdout);
}
static void auto_close(int *fd)
{
if (*fd >= 0) {
int saved_errno = errno;
close(*fd);
errno = saved_errno;
}
}
static int copy_file(const char *source, const char *target)
{
__attribute__((cleanup(auto_close))) int fd_source =
open(source, O_RDONLY | O_CLOEXEC);
if (fd_source < 0) {
LOGE("%s: Failed to open file: %s\n", source, strerror(errno));
return -1;
}
struct stat sb;
if (fstat(fd_source, &sb) < 0) {
LOGE("%s: Failed to stat file: %s\n", source, strerror(errno));
return -1;
}
__attribute__((cleanup(auto_close))) int fd_target =
open(target, O_CREAT | O_TRUNC | O_WRONLY | O_CLOEXEC,
sb.st_mode & ~S_IFMT);
if (fd_target < 0) {
LOGE("%s: Failed to open file: %s\n", target, strerror(errno));
return -1;
}
uint64_t remain = sb.st_size;
while (remain > 0) {
size_t to_copy = remain > 0x7ffff000 ? 0x7ffff000 : remain;
ssize_t n = sendfile(fd_target, fd_source, NULL, to_copy);
if (n < 0) {
LOGE("%s -> %s: Failed to copy data: %s\n",
source, target, strerror(errno));
return -1;
}
remain -= n;
}
return 0;
}
// Mount a tmpfs at SAFE_DIR and copy the files we need to it. AOSP init will
// preserve mount points when switching roots (first /first_stage_ramdisk and
// then the system partition), so we'll be able to access the files during the
// stage 1 -> stage 2 transition. The safe directory must be a directory that
// exists in the system partition and is unused for stage 1 init.
static int prepare_safe_dir()
{
int flags = MS_NOSUID | MS_NODEV | MS_NOEXEC;
if (mount("avbroot", SAFE_DIR, "tmpfs", flags, "mode=755") < 0) {
LOGE("%s: Failed to mount tmpfs: %s\n", SAFE_DIR, strerror(errno));
return -1;
}
if (copy_file(OTACERTS_AVBROOT, OTACERTS_TMPFS) < 0) {
return -1;
}
if (mount(NULL, SAFE_DIR, NULL, MS_REMOUNT | MS_RDONLY | flags, NULL) < 0) {
LOGE("%s: Failed to remount read-only: %s\n",
SAFE_DIR, strerror(errno));
return -1;
}
return 0;
}
// Trace the parent process across execve() calls until otacerts.zip exists.
// Then, bind mount the replacement and detach. Only the parent (TID 1) needs to
// be traced. All other threads and child processes are irrelevant since we only
// care about the transition point between stage 1 and stage 2 init.
static int trace_parent()
{
bool first_group_stop = true;
pid_t parent_pid = getppid();
LOGI("Tracing parent PID: %d\n", parent_pid);
long options = PTRACE_O_TRACESYSGOOD | PTRACE_O_TRACEEXEC;
if (ptrace(PTRACE_SEIZE, parent_pid, NULL, options) < 0) {
LOGE("Failed to trace process: %s\n", strerror(errno));
return -1;
}
while (1) {
int status;
if (waitpid(parent_pid, &status, __WALL | __WNOTHREAD) == -1) {
if (errno == EINTR) {
continue;
} else {
LOGE("waitpid failed: %s\n", strerror(errno));
return -1;
}
}
#ifdef __GLIBC__
enum __ptrace_request
#else
int
#endif
action = PTRACE_CONT;
int forward_signal = 0;
if (WIFEXITED(status)) {
LOGE("%d exited with status %d\n", parent_pid, WEXITSTATUS(status));
break;
} else if (WIFSIGNALED(status)) {
LOGE("%d killed by signal %d\n", parent_pid, WTERMSIG(status));
break;
} else if (WIFSTOPPED(status)) {
int ptrace_event = status >> 16;
int signal = WSTOPSIG(status);
switch (ptrace_event) {
case PTRACE_EVENT_EXEC: {
LOGI("Tracee is about to exec\n");
// If /first_stage_ramdisk exists, then init hasn't switched
// roots yet. otacerts.zip may still exist on devices that
// use shared ramdisks for normal and recovery boot.
if ((access(STAGE1_DIR, F_OK) != 0 && errno == ENOENT)
&& access(OTACERTS, F_OK) == 0) {
LOGI("Conditions satisfied; applying override\n");
action = PTRACE_DETACH;
if (mount(OTACERTS_TMPFS, OTACERTS, NULL,
MS_BIND | MS_RDONLY, "") < 0) {
LOGE("Failed to bind mount %s -> %s: %s\n",
OTACERTS_TMPFS, OTACERTS, strerror(errno));
return -1;
}
if (umount2(SAFE_DIR, MNT_DETACH) < 0) {
LOGE("Failed to detach mount %s: %s\n",
SAFE_DIR, strerror(errno));
return -1;
}
} else {
LOGE("Conditions not yet satisfied\n");
}
break;
}
case PTRACE_EVENT_STOP: {
if (signal == SIGSTOP || signal == SIGTSTP
|| signal == SIGTTIN || signal == SIGTTOU) {
if (first_group_stop) {
LOGI("Resuming tracee\n");
kill(parent_pid, SIGCONT);
first_group_stop = false;
} else {
action = PTRACE_LISTEN;
}
} else {
// We get spurious SIGTRAP signals when SIGCONT'ing a
// process. rr seem to be running into this as well:
// https://github.com/mozilla/rr/issues/2095
// strace handles PTRACE_EVENT_STOP + non-group-stop signal
// by restarting the process with PTRACE_SYSCALL:
// https://github.com/strace/strace/blob/b1e1eb7731e50900bb4591a3a71b96ab37e106a8/strace.c#L2360
// https://github.com/strace/strace/blob/b1e1eb7731e50900bb4591a3a71b96ab37e106a8/strace.c#L2408-L2409
}
break;
}
default: {
if (signal == (SIGTRAP | 0x80)) {
// Syscall enter/exit stop
} else {
// Signal delivery stop
forward_signal = signal;
}
break;
}
}
if (ptrace(action, parent_pid, NULL, forward_signal) < 0) {
LOGE("Failed to perform action %d (signal %d): %s\n",
action, forward_signal, strerror(errno));
return -1;
}
if (action == PTRACE_DETACH) {
LOGI("Detaching tracee\n");
break;
}
} else {
LOGE("Invalid waitpid status: 0x%x\n", status);
}
}
return 0;
}
static int prepare_tracing()
{
pid_t pid = fork();
if (pid < 0) {
LOGE("Failed to fork: %s\n", strerror(errno));
return -1;
} else if (pid == 0) {
int ret = trace_parent();
if (ret < 0) {
// Make sure parent doesn't hang forever.
kill(getppid(), SIGCONT);
_exit(EXIT_FAILURE);
}
_exit(EXIT_SUCCESS);
} else {
LOGI("Waiting for tracer to be ready\n");
kill(getpid(), SIGSTOP);
LOGI("Tracer is ready\n");
}
return 0;
}
int main(int argc, char *argv[], char *envp[])
{
(void) argc;
prepare_output();
if (rename(INIT_ORIG, INIT) < 0) {
LOGE("Failed to restore original init: %s\n", strerror(errno));
return EXIT_FAILURE;
}
if (prepare_safe_dir() < 0) {
LOGE("Failed to set up safe directory %s\n", SAFE_DIR);
} else if (prepare_tracing() < 0) {
LOGE("Failed to set up tracer child process\n");
} else {
LOGI("Exec hook is ready\n");
}
LOGI("Executing %s\n", INIT);
execve(INIT, argv, envp);
LOGE("Failed to exec %s: %s\n", INIT, strerror(errno));
return EXIT_FAILURE;
}