Skip to content

Instantly share code, notes, and snippets.

@kayrus
Last active February 21, 2017 20:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kayrus/f4df08b4096748d7cad73774320e3c13 to your computer and use it in GitHub Desktop.
Save kayrus/f4df08b4096748d7cad73774320e3c13 to your computer and use it in GitHub Desktop.
Hacking my Android phone. How deep the rabbit hole goes

image

My first Android phone Galaxy Note N7000 was bought just after the announcement in October 2011. Thanks to one German guy called bauner, I had an opportunity to use the latest version of CyanogenMod (now LineageOS). Unfortunately, my phone died after using a cheap Chinese car charger about one and a half year ago.

I spent a lot of time looking for a replacement and stopped at Kyocera (yes, they produce phones) KC-S701. It looks quite brutal and doesn't have touch buttons. I did not even thought about the root access to the phone. I was sure that nowadays every phone had a possibility to obtain root. And there will be always some guy who can port CyanogenMod to it. I was mistaken.

Within one and a half year only one kernel update was released - fix resolved kernel failure on a specially crafted ping packet. Besides, one year ago Android KitKat was already considered to be pritty old. Unfortunately, there was neigher info nor possibility to get the root access. The same hardware is used in the American version of the phone called Kyocera Brigadier E6782. It has fastboot mode by default and there is no limit to boot unsigned kernels (boot only, not flash, and only using an old vulnerable bootloader, CVE-2014-4325) and besides it provides an opportunity to boot into fastboot and recovery modes by pressing the phone's buttons. Through the efforts of Verizon (or maybe Kyocera?) Android version of Brigadier has been updated to Lollipop.

So, I decided to deal with the process of obtaining root on Android myself.

Two months ago I did not know anything about the Android internals (actually, now I don't know even more). Most of the knowledge was gained by reading the source code and by experiments, since there is lack of information about the Android hacking in the Internet. The information below works on Android 4.4 KitKat, but I assume there is also a possibility to apply it on newer versions.

I would like to pay your attention to the fact that the info described in this article is only my personal experience of Android hacking on a particular phone model, so be careful using it if you do not want to get the bricked phone. If you wish to root your phone using this article I recommend you to forget that you are using your phone in everyday life and make a backup and then hard reset. This will protect your data in case of fatal mistakes.

The article describes not only the actions that led to success, but also mistakes. I hope that my attempts to get to the desired result and numerous fails will be interesting to you.

The research was conducted in the Linux environment.

Dirtycow (CVE-2016-5195)

In simple words dirtycow (working exploit for Android) allows you to replace the memory of any process (useful if you are familiar with the assembly), or any file available for reading, even if it is on the readonly filesystem. It is recommended to spoof the file which size is greater or equal to the size of the replacement. The main attack in dirtycow for Android is the /system/bin/run-as spoofing. It is some kind of sudo in Android which allows to debug applications. Since the android-19 API (see the table of matching API and Android versions) /system/bin/run-as has CAP_SETUID and CAP_SETGID capabilities flags (in older versions suid bit is used - 6755):

$ getcap bin/run-as 
bin/run-as = cap_setgid,cap_setuid+ep

If the file system is mounted in read-write mode, everything which was spoofed by dirtycow will be written to the file system. Thus you have to make a backup of the original file and restore it after gaining the root access, or just don't remount the file system in read-write mode. Generally the /system partition in Android is mounted in read-only mode by default.

That is why dirtycow is considered to be one of the most serious vulnerabilities found in Linux. And using the appropriate knowledge you can bypass all security levels of the Kernel, including SELinux.

SELinux

Beginners should know how the SELinux context works. There is a good article in the Gentoo wiki: https://wiki.gentoo.org/wiki/SELinux/Tutorials/How_does_a_process_get_into_a_certain_context

In a nutshell, you should know the following:

  • SELinux process context can be changed if such an operation is described in sepolicy rules (context transition). Android 4.4 (KitKat) provides the possibility to elevate privileges by changing the SELinux context. Since Android 5.x it is not possible anymore.
  • There are file contexts.
  • In addition to the process and file contexts, Android implemented its own property_contexts.
  • The rules look as follows: source context (application) is allowed to access target context (filesystem). In SELinux enforcement mode applications are allowed to do only the explicitly granted operations. The rest is forbidden.

Adbd and console

The only possible way to obtain a partly privileged shell in production Android devices - developer mode. Developer mode starts adbd daemon which can act as some kind of ssh / telnet server. In Android KitKat /sbin/adbd binary is located in the initramfs and it is not readable for non-root users. Initially adbd is executed as the root user and it runs in u:r:init:s0 SELinux context (used by init and usually has more privileges than other contexts). If the /init.rc has the explicitly specified process context, such as seclabel u:r:adbd:s0, the process starts immediately in this context. In case of initializing, depending on compile options (user, userdebug or eng and Android settings (properties), adbd lowers the privileges: it changes the current user to shell, sets the SELinux context to u:r:shell:s0 and trims all system capabilities except CAP_SETUID and CAP_SETGID (that is required for debugging applications via run-as). The Capability Bounding Set does not allow applications to escalate the capabilities, only to drop them. These privileges allow you to do a little bit more than nothing. You can view capabilities of the current process with the following command cat /proc/self/status | grep CapBnd. And decrypt them with the capsh command (not available on Android), for example.:

$ capsh --decode=0000001fffffffff
0x0000001fffffffff=cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend

You can view the current SELinux context using id or cat /proc/self/attr/current commands. Previous context could be viewed by cat /proc/self/attr/prev.

View context of the files: ls -Z View context of the running processes: ps -Z

Get root access

Root, but not the one

The first thing I did was to use dirtycow for its intended purpose - to spoof the /system/bin/run-as, which allows me to set UID / GID to 0 (same stuff su does). However, I could not mount the file systems (as well as tmpfs), could not load kernel modules and view dmesg. I could not even browse the directory which had the 0700 permissions and belonged to other system users. I could only read and write to a block device. Viewing the files or directories was possible only when appropriate UID / GID were set to a specified user (invented my own wheel - su alternative, which sets SELinux context and user / group. This helped me to understand internals).

Then I made the dump of the entire firmware, boot and recovery:

$ dd if=/dev/block/mmcblk0 of=/storage/sdcard1/mmcblk0.img
$ dd if=/dev/block/platform/msm_sdcc.1/by-name/boot of=/storage/sdcard1/boot.img
$ dd if=/dev/block/platform/msm_sdcc.1/by-name/recovery of=/storage/sdcard1/recovery.img

You can examine the full dump using utilities such as kpartx and unpackbootimg. The kpartx -a mmbblk0.img command creates a virtual block device which is available by /dev/mapper/loop0 path. You can work with your dump like with a regular block device. Dumps of the boot and recovery partitions can be unpacked using the unpackbootimg.

Then I tried to zero the recovery, just to check whether write operations work, and then immediately restored recovery from the dump.

If I could write into block devices, then I could also write the custom recovery. I found the TWRP of Brigadier, flashed it into recovery partition and restarted the phone: adb reboot recovery. I didn't see anything related to TWRP, only Android icon with an exclamation mark. It looked like a standard Android recovery, not like TWRP.

I rebooted my phone into normal mode, ran the exploit and checked the hash of the recovery partiton - hash matched the original. I tried to write the data again - hash changed! Then I remembered about the Linux page cache and flushed it (echo 3 > /proc/sys/vm/drop_caches) - hash value matched the original again. Thus everything I wrote to a block device was "redirected" into /dev/null without any error and sometimes settled in the Linux cache. But how does the firmware update work? And how is the user data stored into the internal memory? I had to dig further.

Trying to disable SELinux

At that time I thought that all the restrictions were caused by the lack of SELinux privileges (I totally forgot about the dropped capabilities). I could not view dmesg, logcat didn't show anything relevant. I started to think how to disable SELinux.

The first clue I could find:

$ grep -A2 reload_policy boot_initramfs/init.rc
on property: selinux.reload_policy = 1
     restart ueventd
     restart installd

Source code says that when you change this option, init rereads and reloads the SELinux policy from the /sepolicy file.

Since I'm using dirtycow I can overwrite the /sepolicy and execute the setprop selinux.reload_policy 1 command to reload the updated policy.

First of all you have to figure out what is inside the /sepolicy file. You can read its rules using the sesearch command (setools package in Debian).

$ sesearch --allow sepolicy
$ sesearch --neverallow sepolicy
$ sesearch --auditallow sepolicy
$ sesearch --dontaudit sepolicy

In my case the /sepolicy file contained only allow rules which means - when SELinux is in Enforcement mode applications are allowed to do only what is granted in the policy. And thus init process is allowed to reload the policy, but it is not allowed to change the enforcement mode:

$ sesearch --allow sepolicy | grep 'load_policy'
   allow init kernel: security load_policy;

My goal was to allow init context to set the enforcement mode into permissive (setenforce 0).

The first step I did: built a standard policy from the stock Android KitKat, replaced the original /sepolicy, loaded (being root: setprop selinux.reload_policy 1) and received a message in the status bar that the phone was in the unprotected mode (more details about this notification later). After that the phone refused to run applications, became very thoughtful, besides I was still unable to set the permissive mode and thus the phone eventually rebooted. A negative result is also a result,the /sepolicy replacement worked.

My first thought: the stock policy does not fit this phone and it starts glitching due to lack of permissions.

Then I decided to rebuild the original policy and add as much privileges to the shell context as possible.

I found an article which explained how to "reverse engineer" the policy. I was able to resolve all the dependencies and run the sedump utility. As a result I received a text file which I was able to compile back into the binary format (checkpolicy -M -c 26 -o sepolicy.new policy.conf for KitKat) and even got the file with exactly the same size as the original sepolicy, but different hex content. Loading of the new policy file caused exactly the same results as before - the phone rebooted after a couple of minutes.

I decided to compile two policies from the following files: original decompiled policy.conf and policy.conf with all the privileges inside the allow init kernel: security, including setenforce. The comparison of these files could tell me which bytes I have to replace in the original sepolicy binary file.

It turned out that only two bytes were changed. I tried to find the match in the original sepolicy but I could not. Then I just wrote a brute force script which replaces two bytes to "0xFF, 0xFF", launches sesearch --allow | grep "desired result" and if it doesn't meet the result, tries to replace the bytes on the next incremented offset and so on. After a couple of minutes the script found the necessary offset in the original policy. I replaced the bytes, spoofed the original policy on the phone. At this time it worked fine and didn't reboot. But I still could not disable the SELinux enforcement mode.

A bit later I found a sepolicy-inject utility which could modify binary sepolicy file. It could add the new permissive SELinux context or add capability into the existing rule. While adding the permissive context increases the sepolicy file, modification of the existing rule does not increase the size. Unfortunately, the utility adds only one permission per run. I had to write another script which granted all the capabilities to each rule. The new policy file size matched the original one. But again, the policy reload did not help.

Then I noticed that Android had load_policy command which reloaded the policy from any path:

# it has to be executed under system user since /sys/fs/selinux/policy is owned by system user in my phone
adb shell run-as /data/local/tmp/run -u system -c u:r:init:s0 load_policy /data/local/tmp/sepolicy.new

or this way:

run-as /data/local/tmp/run -u system -c u:r:init:s0 sh -c "cat my_policy > /sys/fs/selinux/load"

You can add any permissive domain, load the new policy and work in the context of this domain (by the way, supersu from chainfire works in the same manner for new Android versions). But even this didn't give me the possibility to disable SELinux. I decided to dig in another direction.

Investigating recovery

Image

I started from checking the difference between boot and recovery partitions. They were identical except for initramfs. The initramfs of the recovery partition has init.rc which has only one service which executes /sbin/recovery. Investigation of the strings sbin/recovery | less output and reading of the original recovery source code gave the following results:

  • the default recovery simply displays the Android logo and then reboots after timeout
  • if you want to "enter" the recovery, you have to create the /cache/recovery/command file with corresponding command inside, i.e. --show_text which will show the recovery menu.

I wrote the file and executed the adb reboot recovery command. The phone rebooted and I was able to see the standard recovery menu. At least some result. I tried to flash the supersu ZIP file via adb sideload. The operation terminated with an error. I did not really look at this error and started to investigate the recovery code responsible for the ZIP digital signature verification.

It turns out that the recovery initramfs contains a res/keys public key in minicrypt format which checks the ZIP file digital signature. The public key appeared to be a standard Android test key and so I can sign any ZIP file using this key. You can check this key using the commands below::

java -jar dumpkey.jar android/bootable/recovery/testdata/testkey.x509.pem > mykey
diff -u mykey res/keys

I tried to install the ZIP directly from sdcard, but the recovery caused an error while mounting the sdcard. Investigation of the etc/recovery.fstab file showed the actual problem: the sdcard in the recovery mode was mounted as vfat:

$ grep mmcblk1 recovery/ramfs/etc/recovery.fstab
/dev/block/mmcblk1p1/sdcard vfat nosuid, nodev, barrier = 1, data = ordered, nodelalloc wait

My 64Gb sdcard was formatted in exFAT. I found an old 2Gb sdcard, reformatted it as vfat, wrote the ZIP and inserted it into the phone. This time the recovery was able to mount the volume and I could view its contents on the phone. However, the ZIP installation caused an error again: E: failed to set up expected mounts for install; aborting.

The strings recovery | less command showed that this recovery had custom Kyocera strings, at least there were strings related to the /data partition wipe command. After reading the original source code I found that the error was caused in the setup_install_mounts function of the [roots.cpp](https://android.googlesource.com/platform/bootable/recovery/+/android-4.4.2_r2 /roots.cpp#206) file. For some reason the recovery failed to unmount all the partitions listed in recovery/ramfs/etc/recovery.fstab.

Investigating kernel sources

Unlike the AOSP's Apache license, the GPLv2 license requires from smartphones' manufacturers to publish the Linux kernel source. Thanks to Linus and Stallman for this opportunity. Sometimes manufacturers publish a fake source code, sometimes a correct one, but without the defconfig file, sometimes with defconfig and very rarely with the instructions on how to build the source code (e.g. LG).

In my case the source code was distributed with the correct defconfig but without instructions. Thus I spent some time to build the kernel.

After a long time of the source code investigation I stopped at two files:

hooks

To increase the phone's security Kyocera just implemented custom SELinux hooks on potentially dangerous operations: mount, umount, insmod (the only module allowed to be loadedone is wlan and only if it is loaded by init process or non-root user) and some others. That's the actual reason why the recovery failed. It could not unmount the /system partition! The mount/unmount operations of the /system partition are allowed only for the init process. In particular, I could not disable SELinux because this feature was disabled at the kernel compilation. These hooks could be bypassed only if the kernel was loaded with certain boot parameters (kcdroidboot.mode=f-ksg or androidboot.mode=kcfactory).

restart

This file describes possible reboot options for the phone:

  • adb reboot bootloader - fastboot mode, not available in my phone (0x77665500 - 00556677 hex mark in sbl1 partition)
  • adb reboot recovery - default Android recovery mode (0x77665502 - 02556677 hex mark in sbl1 partition)
  • adb reboot rtc - the so-called ALARM_BOOT. I did not understand what this was for, there was no hex mark in sbl1. Probably it relates to https://developer.android.com/reference/android/app/AlarmManager.html
  • adb reboot oem-X (in my case oem-1, 0x6f656d01 - 016d656f hex mark in sbl1 partition). The manufacturer defines what happens in this mode. According to the sources, the phone restarts into this mode in case of failed firmware files' verification which are located in modem partition.
  • adb reboot edl - emergency download, reboots into a default Qualcomm download mode. The phone is identified as QHSUSB__BULK COM port, which can be used to boot a custom bootloader (but it should be signed by the private key corresponding to the phone model ), and perform low-level operations with your phone, including flashing, unlocking, etc. Usually used in conjunction with QPST application. For some phones these bootloaders have already leaked into the Internet.
  • A certain download mode which could be triggered by kernel boot parameter. Looks quite interesting.

Some info on how Qualcomm based phones are booted:

Built-in ROM Qualcomm bootloader (pbl - primary bootloader) verifies and boots sbl1 partition (secondary bootloader). sbl1 verifies and boots tz (trust zone), then aboot (Android boot, little kernel, lk). Then aboot can boot into built-in fastboot maintenance mode, do the normal boot or boot into recovery or fota.

Partition description involved at boot:

  • tz - Qualcomm Trust Zone. It performs low-level operations, including working with QFuses (rpmb secured mmc partition).
  • rpm - Resource and Power Manager firmware. Firmware for specialized SoC, responsible for resources and power.
  • sdi - trust zone storage partition. The data which is used by Trust Zone.

All of these partitions are signed by a certificate chain.

fota

In some cases it is useful to ignore firmware updates.

FOTA - firmware over the air. Unlike the boot or recovery, fota is an unofficial Android boot mode. Fota task is to update the firmware. Kyocera uses Red Bend proprietary solution which fits the whole update in 35Mb. It includes boot, tz, recovery, fota and even system partitions' updated. That is why the /system partition is available in read-only mode. If you modify this partition, the diff-based fota update can brick the phone.

An update for my phone was available since September 2015. I didn't update my phone because I was afraid to loose an opportunity to root my phone. Now I can easily execute the update process since I have full access to /cache partition and abort the update procedure at any time.

After examining the source code of the responsible Java based update tool, it became clear to me how it functioned:

  • Java app downloads a special delta file into /cache/delta/boot_delta.bin, creates a /cache/delta/Alt-OTA_dlcomplete file and verifies whether delta file was successfully downloaded.
  • When you confirm the update procedure it verifies the file again.
  • If previous verification succeeds then fotamng partition is modified using the libjnialtota.so dynamic library.
  • Phone reboots.

The reboot does not happen instantly, so I can delete a file before the reboot and see what will happen with the fotamng partition.

I wrote the script which continuously made fotamng partition dump and renamed /cache/delta/boot_delta.bin file. I ran it immediately after the update confirmation. The phone rebooted into FOTA mode, showed an error and rebooted into normal boot mode.

I started to investigate the dumped data. The /cache partition also contained a bonus: fota and dmseg logs! It appeared that fota boot could be initialized by couple of bytes set to "1" in fotamng partition:

$ dd if=/data/local/tmp/one_bit.bin of=/dev/block/platform/msm_sdcc.1/by-name/fotamng seek=16 bs=1 count=1
$ dd if=/data/local/tmp/one_bit.bin of=/dev/block/platform/msm_sdcc.1/by-name/fotamng seek=24 bs=1 count=1
$ dd if=/data/local/tmp/one_bit.bin of=/dev/block/platform/msm_sdcc.1/by-name/fotamng seek=131088 bs=1 count=1
$ dd if=/data/local/tmp/one_bit.bin of=/dev/block/platform/msm_sdcc.1/by-name/fotamng seek=131096 bs=1 count=1

These bytes are cleared after the reboot. I also noticed the kcdroidboot.mode=f-ksg kernel parameter in fota dmesg logs. Here it is! Thus, bootloader removes the phone protection for fota boot and theoretically if I write a regular boot partiton instead of fota and reboot the phone into this mode, I will get the kernel with disabled Kyocera protection. But I still don't have a write access to the system-related partitions.

Investigating the little kernel (lk) sources

Little kernel or Android bootloader is located inside the aboot partition. Vanilla source code is available at: https://source.codeaurora.org/quic/la/kernel/lk/

There you can find information on how to boot into some of the modes. For example, if you write the boot-recovery into the misc partition, the next boot will be the recovery mode and there is no need to execute adb reboot recovery. When you boot into the recovery using this method the boot-recovery label will be reset. If the recovery can not be booted, the phone will get the boot loop and you'll lose it. So be careful and preferably avoid this option to reboot.

You can also find the code which enables [read-only protection](https://source.codeaurora.org/quic/la/kernel/lk/tree/platform/msm_shared/mmc.c?h=LA .BR.1.3.3_rb2.29#n2572) of the the system-related emmc area. This is an answer to the question why it is impossible to rewrite the recovery partition. This protection can be disabled in the Linux kernel ~~, if you write the appropriate kernel module ~~. And such a module was already written by another guy who was interested in Kyocera phones. This module works from time to time and sometimes hangs on mmc claim function. Ideally it requires the detailed investigation.

Here is how aboot verifies boot partitions: https://source.codeaurora.org/quic/la/kernel/lk/tree/platform/msm_shared/image_verify.c?h=LA.BR.1.3.3_rb2.29

The first successes

dmesg

Google helped me to answer the question why I could not read the kernel log: /proc/sys/kernel/dmesg_restrict. The value of this parameter is set to 1 while the phone boots. If the user does not have CAP_SYS_ADMIN capability, the logs are not available for it.

uevent_helper

In my case, surprisingly, I had the possibility to write into /sys/kernel/uevent_helper. If you write the path to some executable, it will be executed under root user, init SELinux context and most importantly with full capabilities (shell script also works)

I wrote the following script:

#!/system/bin/sh
echo 0 > /proc/sys/kernel/dmesg_restrict

Uploaded it on the phone, wrote its path into /sys/kernel/uevent_helper and I got the possibility to read dmesg logs!

Patched adbd

Image

Since I could not easily investigate the phone's internals because of the capabilities restriction I decided to build my own adbd ~~with blackjack and hookers~~~. To do this I had to download 70 Gb of Android source code (I didn't want to mess with each dependency individually). I removed the check which drops the capabilities, compiled adbd, replaced the /sbin/adbd and received a full root console. Now I can mount filesystems, read dmesg logs without messing with dmesg_restrict, easily view or edit files which are not owned by root, and much more. But I still can not mount the /system partition and load modules into the kernel.

By the way, this procedure can be avoided by compiling the lsh and writing its path into the /sys/kernel/uevent_helper. I suggest starting lsh wrapped in a script which sets the PATH environment, otherwise you'll have to specify the full path to each command.

WiFi

WiFi in my phone works through the kernel module. When WiFi is turned on - the module is loaded. When WiFi is turned off - the module is unloaded. If you replace the module file with your own and turn on WiFi - your spoofed module must be loaded. Fortunately, my phone doesn't check modules' digital signatures. The first thing I tried was to compile and load the module which disabled the SELinux by replacing the kernel memory. The module was initially written for Amazon Fire Phone: https://github.com/chaosmaster/ford_selinux_permissive

You need to have a more or less appropriate kernel sources and Module.symvers file in order to compile the module. If the source code explicitly corresponds to the kernel which is used in the phone, then you can use the Module.symvers which is automatically generated during the kernel compilation process.

If the kernel module complains on disagrees about version of symbol module_layout, you will need to extract the Module.symvers from the boot partition. This could be done using https://github.com/glandium/extract-symvers script:

$ unpackbootimg -i boot.img -o boot
$ extract-symvers.py -e le -B 0xc0008000 boot/boot.img-zImage > %PATH_TO_KERNEL%/Module.symvers

Image

Do you remember this list? The module should be called wlan. Here is how I resolved this problem:

  • Created a wlan.c symlink
  • Modified Makefile
...
MODULE_NAME = wlan
...

After applying these tricks and executing the svc wifi disable && svc wifi enable the module has successfully loaded (memory used by the wlan module reduced, it could be checked using lsmod command), but SELinux was not disabled.

The dmesg logs did not contain any information related to the new module. It was caused by another kernel option: /proc/sys/kernel/printk which filters INFO logs including modules' logs. I lowered the threshold for all logs: echo '8 8 8 8' > /proc/sys/kernel/printk, reloaded the module and it appeared that the module just could not find the required bytes pattern. I decided to write own kernel module.

Writing a module

Disabling security protection

I failed to disable SELinux, but by analogy with https://github.com/chaosmaster/ford_selinux_permissive module I could try to disable Kyocera hooks. I just needed to set a kc_bootmode or kc_kbfm variable using the Linux kernel module.

The Linux kernel has the possibility to get the pointers' addresses of all the functions and variables: cat /proc/kallsyms. By default, these addresses are displayed as 0. It is yet another kernel protection and can be disabled by the following command: echo 0 > /proc/sys/kernel/kptr_restrict.

Once you get the address of the desired function, you can call it with the appropriate parameter and function will set the corresponding variable to 1. I noticed that not all Linux kernels display addresses for the variables (d or D types, case says whether the variable is public or not) that is why I used function pointers, but not actual variable pointers. Perhaps it is determined by the CONFIG_KALLSYMS_ALL option during the kernel compilation.

$ adb shell "grep kc_bootmode_setup /proc/kallsyms"
c0d19d84 t kc_bootmode_setup

First of all I had to declare the kernel function I'd like to call in the module:

int (* _kc_bootmode_setup) (char * buf) = (int (*) ()) 0xc0d19d84;

And then call it:

_kc_bootmode_setup("f-ksg")

You can also determine the addresses dynamically:

_kc_bootmode_setup = (int (*) (char * buf)) kallsyms_lookup_name("kc_bootmode_setup");

I loaded the module and it disabled the protection! Now I can mount the /system and load any kernel module regardless of its name.

Protected eMMC area is still in read-only mode and it does not allow you to modify /system partition on a regular basis. The files can be edited, but when you clean the kernel cache everything goes to its original state.

Finally disabling SELinux

It was already not required but just for fun, I decided to finally disable the SELinux. I could not modify the defined selinux_enabled constant, but I could dereference the security_ops structure with hooks pointers.

This can be done by calling the reset_security_ops function:

void (* _reset_security_ops) (void) = NULL;
... ... ...
_reset_security_ops = (void (*) (void)) kallsyms_lookup_name("reset_security_ops");
if (_reset_security_ops! = NULL) {
  _reset_security_ops ();
}

It disables all the SELinux hooks and functions, but the system still thinks that SELinux is enabled since selinux_enabled contains 1 integer value. Thus there may be some issues related to SELinux functions, i.e. incorrect ls -Z output.

Reboot into download mode

int (* _enable_dload_mode) (char * str) = (int (*) ()) 0xc0d0cc18;
... ... ...
_enable_dload_mode("dload_mode");

The same operation works with download_mode I wrote above. After the module loading the phone reboot will boot it in a special mode which operates as USB mass storage device. Thus I have full access to all the phone's partitions! I tried to overwrite the recovery partition and it worked even after clearing the kernel cache.

Actually regular dd doesn't work and phone's USB mass storage device disconnects and the writing stops. Perhaps this is a result of an internal cache overflow of the mass storage loader. I had to write a workaround. An advanced script for the heavy system partition is available here: https://github.com/kayrus/kc_s701_break_free/blob/master/inject_supersu/write_rooted_system.sh. The primary trick is to read the data which you'd like to overwrite. Reading is much faster than writing, so I compare the source and destination hashes and if they differ I write new data. This saves lot of time and 1.2Gb partition can be flashed in 2-3 minutes instead of 35 minutes.

Using this method I installed the supersu binaries into the downloaded /system partition and flashed it back into the phone. The phone booted, but I got the following message in the notification bar: Low security level. Inappropriate application may have been installed. Please uninstall it and reboot the phone.. I had already seen this message before when I loaded insecure SELinux policy. This notification also causes the permanent red LED blinking and the phone doesn't turn off the screen on timeout (I guess to pay the user's attention that the phone was hacked). I spent some time and figured out that this notification was triggered by /system/vendor/bin/akscd daemon. It is a small daemon which monitors su/sudo binaries which can be found within PATH, monitors SELinux state and whether /system partition is mounted in read-write mode. It writes the security states into the /data/system/akscd/out_%s.dat and triggers the notification mentioned above. I just disabled this service, but there should be a better fix to disable su/sudo detection only and allow to monitor the /system state since I still would like to control my phone's security.

The initial task is completed: permanent root access and possibility to write into the external sdcard were obtained. In addition I wrote a utility which sets system UID with the CAP_SYS_MODULE capability, uloads original wlan module then loads my false wlan module which disables the security and finally loads the original wlan module again.

My next goal was to boot custom kernel. I hoped there was no digital signature verification and I decided to boot my custom boot partition. Since it is dangerous to flash regular boot partition, I decided to flash it into the recovery partition and reboot into the recovery using adb reboot recovery command. Remember the misc partition, it is not recommended to boot into recovery through the boot-recovery entry in this partition, it can cause a boot loop. Unfortunately, the phone could not boot into recovery, it just vibrated and then rebooted into normal mode.

Looks like I have to unlock the phone's bootloader. There is not much info on how to do this, but I found a couple of methods which worked for old phones:

Digital signatures of the aboot and boot partitions

I was curious how exactly aboot verifies the boot partitions. So I unpacked all the certificates from aboot partition (binwalk -e aboot), extracted images signatures and looped over all the public keys trying to decrypt the signature. It turned out that all the boot images were signed using the same key.

#!/bin/bash

# mkdir boot
# unpackbootimg -i 09-boot.img -o boot
# cd boot
# mkbootimg --kernel 09-boot.img-zImage --ramdisk 09-boot.img-ramdisk.gz --cmdline "`cat 09-boot.img-cmdline`" --base `cat 09-boot.img-base` --pagesize `cat 09-boot.img-pagesize` --dt 09-boot.img-dtb --kernel_offset `cat 09-boot.img-kerneloff` --ramdisk_offset `cat 09-boot.img-ramdiskoff` --tags_offset `cat 09-boot.img-tagsoff` --output mynew.img
# dd if=../09-boot.img of=signature.bin bs=1 count=256 skip=$(ls -la mynew.img | awk '{print $5}')
# cd ..
# binwalk -e 05-aboot.img
# openssl rsautl -raw -inkey <(openssl x509 -pubkey -noout -inform der -in _05-aboot.img.extracted/4D8D8.crt 2>/dev/null) -pubin -in signature.bin 2>/dev/null | hd
# print cert in text mode: openssl x509 -inform der -in 1768B.crt -text -noout

NAME=$1
IMG=${NAME}/mynew.img
SIG=${NAME}/signature.bin

CALC_SHA256=$(sha256sum ${IMG} | awk '{print $1}')

for i in `find . -name *.crt`; do
  ORIG_SHA256=$(openssl rsautl -inkey <(openssl x509 -pubkey -noout -inform der -in ${i} 2>/dev/null) -pubin -in ${SIG} 2>/dev/null | hexdump -ve '/1 "%02x"')
  if [ "${ORIG_SHA256}" != "" ]; then
    echo "sha256 was decrypted using ${i} key - ${ORIG_SHA256}"
  fi
  if [ "${ORIG_SHA256}" = "${CALC_SHA256}" ]; then
    echo "sha256 matched the calculated sha256 ${ORIG_SHA256}"
    echo "$i"
  fi
done

This script prints the following output:

$ ./verify.sh boot
sha256 was decrypted using ./_05-aboot.img.extracted/31464.crt key - 91642909810cde935881d1656f6290ebf32e19975d99d739bd03162f79e000d7
sha256 matched the calculated sha256 91642909810cde935881d1656f6290ebf32e19975d99d739bd03162f79e000d7
./_05-aboot.img.extracted/31464.crt

Verification of the aboot partition appeared to be more complicated. I was able to extract and decrypt the sha256 signature of the image. But could not calculate this hash myself. Fortunately, Nikolay Elenkov, the author of the Android security internals helped and forwarded me to Qualcomm whitepaper: https://www.qualcomm.com/media/documents/files/secure-boot-and-image-authentication-technical-overview.pdf. It explains how target sha256 is calculated. The hash depends on HW_ID and SW_ID which are defined inside the certificate's subject, i.e.

Subject: C=US, ST=CA, L=San Diego, OU=07 0001 SHA256, OU=06 001E MODEL_ID, OU=05 00002000 SW_SIZE, OU=04 0039 OEM_ID, OU=03 0000000000000002 DEBUG, OU=02 009180E10039001E HW_ID, OU=01 0000000000000009 SW_ID, O=Kyocera wireless corp CSMS, CN=Ayano Nakamura

The working script is available in Nikolay's github repository.

Experimenting with the fota partition

Since I know that boot/recovery and fota partitions are signed with the same key and the fota partition is booted with the disabled kernel security it's worth trying to check whether flashing the boot partition into the fota partition can work.

It was quite risky and I could get bootloop similar to recovery bootloop. The boot-in-fota sign is written into the fotamng partition and if aboot did not boot the fota, it could try to boot it in the endless loop.

Unfortunately, the boot partition written in fota could not be loaded, but fortunately I did not face the bootloop. It is not clear why it doesn't work, perhaps because of the different ramdisk and tag offsets (most probably they are hardcoded inside the LK):

boot / recovery:

  ramdisk: 0x01000000
  tags: 0x00000100

fota:

  ramdisk: 0x02000000
  tags: 0x01e00000

Experimenting with the Brigadier bootloader

For the experiments I ordered a Kyocera Brigadier with a broken screen.

I checked the digital signatures of KC-S701 and Brigadier aboot partitions and it appeared that they were signed by the certificates with the same subject, so aboot partitions should be interchangeable. I decided to make an experiment: flash aboot from KC-S701 into the Brigadier. The bootloader successfully booted. Surprisingly, the eMMC write protection was not activated and I could easily restore the original bootloader.

Then I tried to flash the aboot from Brigadier into the KC-S701. I could get the opportunity to use fastboot and boot any unsigned kernel. At this time the phone didn't boot.

At this point the story could end with the "phone didn't boot" and a black screen. But, fortunately, this black screen was the "download mode". I was able to flash the original aboot partition and the phone was resurrected. I still wonder why it didn't boot. Both certificate chains are valid and theoretically should be interchangeable.

What should still be clarified

Sepolicy recompilation

Why sepolicy recompilation doesn't work? Imperfect sepolicy decompiler?

What does aboot partition hide?

What hides behind the oem-1 reboot mode? Aboot partition contains the fastboot code, at least: flash, erase, oem device-info, preflash, oem enable-charger-screen and oem disable-charger-screen commands. How can I enter the fastboot mode?

How to disable camera shutter sound?

I've found at least 3 options which somehow control camera shutter sound but none of these options works. The only way to disable it is to replace the /system/media/audio/ui/camera_click.ogg file. But I don't like this solution and still want to find the way to disable this option in a more elegant way. Most probably it is controlled by proprietary Kyocera properties.

Explanation Kyocera properties

Kyocera along with Android system properties uses its own internal properties. I'm pretty sure there can be some tricky options which can influence the removal of bootloader protection or camera shutter sound which can not be disabled (oh man, I just realized how many restrictions are there in KC-S701). The phone has a libkcjprop_jni.so dynamic library and the kcjprop_daemon daemon. I can write an app which will use this library, but comparing to other TODOs this task has low priority.

Options are written into the filesystem and look like binary data:

$ ls -la /sysprop/kcjprop/rw/8d9d788ddd5fecfdbc6c5f7c5cecfc
-rw-rw ---- root root 16 1970-01-22 21:01 8d9d788ddd5fecfdbc6c5f7c5cecfc

Kexec

Kexec allows the Linux kernel to load another kernel. By default, the production kernel releases don't support Kexec, but it is possible to enable it using the kernel module. Then, using the user-end utility you can load any custom kernel which will replace the current one. It looks like a hack, but if you want to load your custom kernel and bypass digital signature verification - this is an option.

QSEE vulnerability

QSEE - Qualcomm TrustZone protection which could have a vulnerability to execute custom code within the TrustZone context: https://bits-please.blogspot.com/2016/05/qsee-privilege-escalation-vulnerability.html. Looks like I have to build a specially formatted SCM command to burn the corresponding QFuse and unlock the bootloader. Still to be done.

Conclusion

Image

Every problem I resolve causes an amount of new problems and it is hard to predict how deep the rabbit hole goes.

Modules' source code, aboot loaders and the library to work with the Kyocera Propertiies are available in my github repo: https://github.com/kayrus/kc_s701_break_free.

I would like to express my gratitude to the Kyocera developers for the excellent devices and high security. Otherwise, this article would not have been written. On the other hand, the lack of regular updates makes me really upset. If Kyocera has a new phone model with the ability to unlock the bootloader, I will certainly buy it.

I still haven't given up to unlock the bootloader. The most profitable time was on Christmas, but now it is hard for me to find some free time and focus on this problem.

P.S. Many thanks to Nikolay Elenkov. He explained to me how Android boot chain works and helped me with the aboot digital signature verification.

P.P.S. Thanks to Justin Case who told me that there was a way to unlock the bootloader (however, he didn't wish to share with me how to do it).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment