Skip to content
 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

95 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

My avbroot setup

Fork notice. This repository is a compatible fork of chenxiaolong/my-avbroot-setup used by the PixeneOS build pipeline for LineageOS / non-Pixel ROM compatibility. The fork strategy and the per-hunk decomposition of its delta against upstream are documented in MAINTAINERS.md and docs/. Upstream-bound PR drafts live under docs/upstream-prs/.

This repo describes my personal setup for modifying the OS on my Android devices.

Unlike the norm in the Android modding community, I do not use runtime modifications and instead, prefer to modify the Android image directly. This eliminates the need for privileged code to live on writable storage and avoids giving potential malware a place to persist.

This repo includes the script I use for modifying Android OTAs. Folks should probably not use the script as-is and instead, adapt it to their needs.

Supported patch modules and their compatibility metadata are enumerated by the declarative module catalog. Catalog entries remain metadata-only: they neither enable modules nor execute installer scripts. Modules can be selected explicitly with --module-<name> for unlocked workflows, or through patch.py's canonical locked profile/lock boundary.

Requirements

  • Host must run Linux or an Android device must be connected via adb
    • Needed for running a statically-linked Android executable
  • python3
  • avbroot (>= version 3.12.0)
  • afsr (>= commit adcae036b68684828edf5eb90be1500abd5cf491)
  • Custota (>= version 5.2)
  • MSD (>= version 1.8)
  • BCR (>= version 1.65)
  • OEMUnlockOnBoot (>= version 1.1)
  • AlterInstaller (>= version 2.0)

The avbroot, afsr, and custota-tool commands must exist in PATH. This legacy behavior remains the default. Integrators that authenticate those tools through a separate runner can instead pass an exact JSON argv prefix:

python3 patch.py \
    --tool-runner-prefix-json \
    '["/usr/bin/python3","/opt/pixene/bootstrap.py","--workdir","/var/tmp/pixene","run"]' \
    --input ota.zip --output ota.patched.zip

For each external tool invocation, the helper appends the allowlisted tool name, a -- separator, and the original arguments. The prefix executable must be an absolute path. No shell parsing is performed, and loader/debugging environment variables are removed before an authenticated runner starts.

Usage

Install uv or manually set up a Python virtualenv.

The staged module preparation CLI keeps catalog resolution, lock updates, artifact acquisition, verification, and archive inspection separate from OTA mutation:

python3 module-tool.py catalog list --format json
python3 module-tool.py lock verify --lock locks/artifacts.lock.json
python3 module-tool.py artifacts fetch \
    --lock locks/artifacts.lock.json --cache .artifact-cache
python3 module-tool.py artifacts verify \
    --lock locks/artifacts.lock.json --cache .artifact-cache
python3 module-tool.py resolve \
    --profile profiles/lineage.toml --lock locks/artifacts.lock.json \
    --format json

Normal builds consume checked-in locks and never resolve “latest” versions. Only the explicit lock update <module> command may use a reviewed provider for floating upstream metadata; this Phase 1 foundation intentionally has no such provider yet. See the module-tool guide for the complete command contract and offline/security boundaries.

Then, run the patch script:

uv run patch.py \
    --input ota.zip \
    --verify-public-key-avb verify_avb_pkmd.bin \
    --verify-cert-ota verify_ota.crt \
    --sign-key-avb sign_avb.key \
    --sign-key-ota sign_ota.key \
    --sign-cert-ota sign_cert.key \
    --module-custota Custota-<version>-release.zip \
    --module-msd MSD-<version>-release.zip \
    --module-bcr BCR-<version>-release.zip \
    --module-oemunlockonboot OEMUnlockOnBoot-<version>-release.zip \
    --module-alterinstaller AlterInstaller-<version>-release.zip

This will:

  1. Verify the original OTA signatures against the specified verification keys. This includes the OTA signature, the payload.bin signature, and the signatures of every AVB-enabled partition image.
  2. Extract the system, vendor, and vendor_boot partitions. This is all done in userspace. Root access is not needed as nothing is ever mounted by the kernel.
  3. Verify the signatures of BCR and the other modules and copy the necessary files. Commands that would traditionally be run during boot via the module's service.sh and post-fs-data.sh scripts are added as proper init services.
  4. Repack the system, vendor, and vendor_boot partitions, re-signing them with the specified AVB key if necessary.
  5. Patch the OTA using avbroot's --rootless mode, replacing the 3 modified partitions. This will re-sign other partitions (eg. vbmeta) needed to reestablish AVB's chain of trust and also sign the patched OTA.
  6. Generate the metadata needed to install the OTA using Custota.

Why not root?

When folks refer to "rooting" in the Android community, they're usually not just referring to a process running as UID 0. The term generally also includes the functionality for making runtime code patches (eg. with Zygisk) and making runtime filesystem modifications (eg. Magisk modules).

UID 0

This is probably where my usage differs from most. I use root access purely for reverse engineering and debugging. As long as I can run a process as UID 0 via adb, that's good enough for me. I don't allow any Android apps to gain root access. In the past, I used my own soft-fork of Magisk with the relevant code and SELinux rules that allows such access completely removed.

Out of the many root-enabled apps I've studied or reverse engineered, the vast majority fail to handle arbitrary inputs properly (especially filenames). For example, some root-supporting file managers turn a seemingly benign action like listing a directory into local privilege escalation. This is trivially exploitable, especially with browsers auto-downloading files with server-provided filenames to /sdcard/Download/.

To avoid repeated root access UI prompts, some apps spawn a long-running shell session, write commands to stdin, and rely on parsing stdout and searching for the shell prompt to determine when commands complete. This approach is prone to desync, which can lead to commands being skipped or other inputs being interpreted as commands.

All in all, I simply do not trust most root-enabled apps to not leave a gaping security hole, so I avoid them entirely. There are apps that do handle root access in what I would consider a more proper way, by spawning a daemon as root and then talking to the daemon over a well defined binary protocol. Unfortunately, this approach is the extreme minority.

For situations where I actually do need to run a process as UID 0, I use Android's official way of getting root access: adb root and /system/xbin/su. To accomplish that, I make a userdebug build of GrapheneOS and set the ro.adb.secure=1 property to retain adb's host key verification: https://github.com/chenxiaolong/grapheneos-patches#secure-adb-for-userdebug-builds.

Runtime code patching

In the past, I used Zygisk + LSPosed for one reason only: to disable Android 12+'s verified links "feature". With GrapheneOS, I no longer have a use for this because the functionality can effectively be disabled by removing the network permission from the Intent Filter Verification Service system app. This also means I no longer need to flip between enabling and disabling Zygisk because it conflicts with some debugger functionality.

Modules

This is, by far, the main reason I "rooted" my device in the past. I've written several apps that only require system app privileges, not root, but Magisk modules are the easiest and most convenient way to install them. Modules allow running scripts during boot and overriding arbitrary filesystem paths via bind mounts (or overlayfs). Unfortunately, while this is incredibly convenient, it also makes it easy for potential malware to persist.

I wanted to have a way to install system apps without breaking Android's security model. My initial thought was to modify Magisk so that it would only load modules from the AVB-protected boot image ramdisk. Since I no longer used Magisk's other functionality, I didn't end up pursuing this approach. Instead, I wrote afsr to make it easy to unpack, modify, and repack ext2/3/4 images (byte-for-byte reproducibly). This way, I can add system apps in a way that preserves the guarantees of Android Verified Boot.

I could accomplish the exact same result by adding the apps I care about to the AOSP build system when building GrapheneOS. I intentionally don't do that because the iteration time for building OTAs with AOSP's build system is so long even when nothing has changed.

Root detection, SafetyNet, Play Integrity

Apps that do these things don't remain installed on my devices :)

License

This repo is licensed under GPL-3.0-only. Please see LICENSE for the full license text.

About

My personal avbroot setup for my Android devices

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages