chenxiaolong / avbroot

Sign (and root) Android A/B OTAs with custom keys while preserving Android Verified Boot
GNU General Public License v3.0
536 stars 42 forks source link

Would a honest to god homebrew OTA server be feasible? [feature request, understood to be unlikely] #118

Closed nad0vs closed 1 year ago

nad0vs commented 1 year ago

The general idea is to make one's own home-hosted OTA server instead of native one. I realize it is way too much work probably but with root (and magisk "strategically" substituting some files) it seems like it might be conceptually possible.

I understand chenxiaolong is probably busy enough just keeping all other projects up to date :-) so a whole custom OTA server + OTA agent may bee too much work, but maybe someone else will get inspired to write and maintain that as a separate project (at least for Pixels which have pretty documented and "standardized" process of updating)

chenxiaolong commented 1 year ago

This was actually one of my "shower thoughts" recently (yes, I have a problem...), so I did some research. I won't be making an OTA updater app myself, but here are my notes in case someone else is interested in doing so:

I definitely think this is possible to do and there's no reason that fancy features, like incremental OTAs or streaming downloads wouldn't work. The hard part is getting something like this built and packaged as a Magisk module. OTA updaters are very much designed to be built as part of the AOSP build.

pixincreate commented 1 year ago

https://github.com/MuratovAS/grapheneos-magisk I found this golden repo.

nad0vs commented 1 year ago

There's also this thing https://github.com/ur0/UpdateEngineInterface which is horribly out of date but conceptually seems pretty close

chenxiaolong commented 1 year ago

I ended up getting more interested in this, so I'm working on an app to do this now :slightly_smiling_face:

No promises when or if it'll be released. I'm not to the point of testing things yet, so I'm not sure if I'll run into something that makes this completely unfeasible.

As far what to expect, UI and functionality-wise, it'll be very similar to my other project, BCR. Just a basic settings UI, with the progress bar and pause/resume/cancel options shown in a notification only. It'll exclusively use update_engine's streaming install method, meaning the download and install run at the same time and the download process is managed by update_engine itself. It's possible to pause/resume during this process, but pre-downloading an update to install later will never be supported.

For the server side, the only thing required is a static file host that supports the standard HTTP Range header. Even a simple python -m http.server 8080 will work. The server will need to serve a JSON file containing metadata about the latest OTA available for the device. The JSON file contains either a relative path to the .zip or a full URL in case you want to host the large files elsewhere.

Finally, assuming I'm able to get this to work, it'll end up being a separate project living in a separate repo (still open source under GPLv3). It should work with any A/B-style OTA, regardless if it was created by avbroot or not, so it probably doesn't make much sense to put it in this repo.

chenxiaolong commented 1 year ago

OK, I just ran into the first limitation:

Both gmscore (Google Play Services' OTA updater) and GrapheneOS' updater are sandboxed via a separate SELinux domain (gmscore_app and updater_app respectively). These sandboxed domains are the only things allowed to talk to update_engine.

Trying to do the same for an app via a Magisk module is impossible. The way an app as assigned an SELinux domain is via seapp_contexts files, which contain lines like:

# AOSP / gmscore
user=_app isPrivApp=true name=com.google.android.gms domain=gmscore_app type=privapp_data_file levelFrom=user

# GrapheneOS
user=_app isPrivApp=true name=app.seamlessupdate.client domain=updater_app type=app_data_file levelFrom=user

The seapp_contexts recognized by Android 13 are:

/system/etc/selinux/plat_seapp_contexts
/dev/selinux/apex_seapp_contexts
/system_ext/etc/selinux/system_ext_seapp_contexts
/product/etc/selinux/product_seapp_contexts
/vendor/etc/selinux/vendor_seapp_contexts
/odm/etc/selinux/odm_seapp_contexts

These files are loaded by /system/bin/init very, very early in the boot process before /data and thus, any Magisk modules, are even mounted. These files are also never reloaded. There's no easy or sane way to hook into init to modify these files at the right point in time, even if we were to modify the boot image.


With that out the window, the best we can do is add some rules for priv_app (the default SELinux domain for anything in /system/priv-app). This would allow any system app to talk to update_engine. This doesn't inherently make things more insecure because update_engine is always going to verify signatures for data given to it, but it does slightly increase the potential attack surface. Now a weak/insecure system app could potentially be used to find and exploit vulnerabilities in update_engine.

It's not the ideal solution, but unless I'm missing something, it's probably the best solution we have.


EDIT: I'm dumb. Yes, init loads all these seapp_contexts files very early, but init's copy of this information in memory doesn't matter. What matters is when zygote's loads these files into memory itself. And Magisk modules can definitely do things before that happens.

chenxiaolong commented 1 year ago

Creating a new SELinux domain at runtime is a royal pain in the ass (and not because of Magisk--magiskpolicy is wonderful).

If the updater app's SELinux policy were included in an AOSP build, things would be very easy. It's just a few rules:

type test_app, domain;
typeattribute test_app coredomain;
app_domain(test_app)
untrusted_app_domain(test_app)
net_domain(test_app)
allow test_app ota_package_file:dir rw_dir_perms;
allow test_app ota_package_file:file create_file_perms;
binder_call(test_app, update_engine)
allow test_app update_engine_service:service_manager find;
binder_call(update_engine, test_app)

Directly translating these to magiskpolicy commands is pretty simple. After evaluating the macros (like binder_call(...)), it's only 20 rules or so. However, the AOSP SELinux policy has tons of statements like these:

allowxperm { appdomain -bluetooth } self:{ rawip_socket tcp_socket udp_socket } ioctl { unpriv_sock_ioctls unpriv_tty_ioctls };

("allow everything with the appdomain attribute except bluetooth to perform some network socket operations")

These everything except <blah> rules are what's incredibly painful. The "everything" needs to include the new SELinux domain for the updater app, resulting in several hundred additional rules after the policy is compiled.

Replicating that outside out of the AOSP build isn't really possible:

I'm probably going to go back this approach:

With that out the window, the best we can do is add some rules for priv_app (the default SELinux domain for anything in /system/priv-app). This would allow any system app to talk to update_engine. This doesn't inherently make things more insecure because update_engine is always going to verify signatures for data given to it, but it does slightly increase the potential attack surface. Now a weak/insecure system app could potentially be used to find and exploit vulnerabilities in update_engine.

pixincreate commented 1 year ago

Instead, can we modify the existing Updater such that, it checks for Official OTAs (GrapheneOS as of now), if exist, download that, instead of installing, the updater applies the patches to the ROM that is downloaded, and install them. Would that go? I feel like the things that you mentioned are complicated.

Given the existing updater is open-source, we can modify that.

Also, I personally, now prefer KernelSU over Magisk since it allows more robust rooting mechanism and hides root is better and safer way (which is what I believe).
So, patching kernel on every OTA automatically would be really feasible IMO.

What do you think? @chenxiaolong

chenxiaolong commented 1 year ago

screenshot of custom OTA updater with OTA update in progress screenshot of OTA update notification at 13% screenshot of OTA update success notification

:slightly_smiling_face:

All SELinux issues have been worked around with no compromises on security.

What works:

Performance on my Pixel 7 Pro (plugged in, 100% battery, screen off):

(It will always be slower than adb sideload because Android deprioritizes update_engine to avoid slowing down the device. The post install script also skips the dexopt step if it detects that the update is being sideloaded.)

Remaining work:


The server side metadata is a JSON file that looks like the example below. The server can be any plain old HTTP server that supports the Range header. I was wrong in my previous post about python -m http.server working, but any of the normal webservers, like Apache, Nginx, or Caddy would work fine. For testing, Caddy is great because you can just run caddy file-server -access-log -listen :8080 inside the folder containing the metadata and OTA zips--no need to mess with config files.

// (A script will be provided to generate this file.)
{
    // The location for the full OTA.
    "full": {
        // The location can be a relative path or a full URL.
        "location": "cheetah-ota-tq3a.230705.001-dfcb7951.zip.patched",

        // The values of the `metadata` entry from `ota-property-files` inside
        // the OTA's META-INF/com/android/metadata file.
        "metadata_offset": 2343312763,
        "metadata_size": 646
    }
}

EDIT: Remove incremental options from JSON example. I don't plan on implementing it unless/until someone implements a way to actually generate incremental updates from two zips. (I do not plan on adding this functionality to avbroot.)

chenxiaolong commented 1 year ago

Custota has been released! https://github.com/chenxiaolong/Custota

I've tested it pretty extensively on my device, so hopefully there aren't too many bugs. If you run into issues, feel free to open a bug report over there.

pascallj commented 1 year ago

Very cool, will have a look at this in the future!