War of the Worlds - Hijacking the Linux Kernel from QSEE

After seeing a full QSEE vulnerability and exploit in the previous blog post, I thought it might be nice to see some QSEE shellcode in action.

As we've previously discussed, QSEE is extremely privileged - not only can it interact directly with the TrustZone kernel and access the hardware-secured TrustZone file-system (SFS), but it also has some direct form of access to the system's memory.

In this blog post we'll see how we can make use of this direct memory access in the "Secure World" in order to hijack the Linux Kernel running in the "Normal World", without even requiring a kernel vulnerability.

Interacting with QSEE


As we've seen in the previous blog post, when a user-space Android application would like to interact with a trustlet running in QSEE, it must do so by using a special Linux Kernel device, "qseecom". This device issues SMC calls which are handled by QSEOS, and are passed on to the requested trustlet in order to be handled.

Each command issued to a trustlet has a pair of associated input and output buffers, which are usually used to convey all the information to and from the "Normal World" and the trustlet.

However, there are some special use-cases in which a faster mode of communication is required  - for example, when decrypting (or encrypting) large DRM-protected media files, the communication cost must be as small as possible in order to enable "smooth" playback.

Moreover, some devices include trustlets which are meant to assure the device's integrity (mostly in corporate settings).  For example, Samsung provides "TrustZone-based Integrity Measurement Architecture" (TIMA) - a framework used to assure device integrity. According to Samsung, TIMA performs (among other things) periodic measurements of the "Normal World" kernel, and verifies that they match the original factory kernel.

So... Trustlets need fast communication with the "Normal World" and also need some ability to inspect the system's memory - sounds dangerous! Let's take a closer look.

Sharing (Memory) is Caring

Continuing our research on the "widevine" trustlet, let's take a look at the command used to DRM-encrypt a chunk of memory:

As we can see above, the function receives two pointers denoting the "input" and "output" buffers, respectively. These can be any arbitrary buffers provided by the user, so it stands to reason that some preparation would be needed in order to access them.  Indeed, we can see that the preparation is done by calling "cacheflush_register", and, once the encryption process is done, the buffers are released by calling "cacheflush_deregister".

Upon closer inspection, "cacheflush_register" and "cacheflush_deregister" are simple wrappers around a couple of QSEE syscalls (each):

cacheflush_register          cacheflush_deregister         

So what do these syscalls do?

Looking at the relevant handling code in QSEOS reveals that these names are a little misleading - in fact, "qsee_prepare_shared_buf_for_secure_read" merely invalidates the given range in data cache (so that QSEE will observe the updated data), and similarly "qsee_prepare_shared_buf_for_nosecure_read" clears the given range from the data cache (so that the "Normal World" will see the changes made by QSEE).

As for "qsee_register_shared_buffer" - this syscall is used to actually map the given ranges into QSEE. Let's see what it does:

After some sanity checks, the function checks whether the given memory region is within the "Secure World". If that's the case, it could be that the trustlet is trying to attack the TrustZone kernel by mapping-in and modifying memory regions used by TZBSP or QSEOS. Since that would be extremely dangerous, only a select few (six) specific regions within the "Secure World" can be mapped into QSEE. If the given address range is not within any of these special "tagged" regions, the operation is denied.

However - for any address in the "Normal World", there are no extra checks made! This means that QSEOS will happily allow us to use "qsee_register_shared_buffer" in order to map in any physical address in the "Normal World".

...Are you pondering what I'm pondering?

Hijacking the Linux Kernel

Since QSEE has read-write access to all of the "Normal World"'s memory (all it needs to do is ask), we should theoretically be able to locate the running Linux Kernel in the "Normal World" directly in physical memory and inject code into it.

As a fun exercise, let's create a QSEE shellcode that doesn't require any kernel symbols - this way it can be used in any QSEE context in order to locate and hijack the running kernel.

Recall that after booting the device, the bootloader uses the data specified in the Android boot image in order to extract the Linux Kernel into a given physical address and execute it:

The physical load address of the Linux Kernel is then available to any process via the world-readable file /proc/iomem:

However, simply knowing where the kernel is loaded does not absolve us from the need to find kernel symbols - there is a large amount of kernel images and an equally large amount of symbols per kernel. As such, we need some way to find the all of the kernel's symbols dynamically using the running kernel's memory. However, all is not lost - remember that the Linux Kernel keeps a list of all kernel symbols internally (!), and allows kernel modules to lookup these symbols using a special lookup function - "kallsyms_lookup_name". So how does this work?

As we've previously seen - the names in the kernel's symbol table are compressed using a 256-entry huffman coding generated at build time. The huffman table is stored within the kernel's image, alongside the descriptors for each symbol denoting the indices in the huffman table used to decompress it's name. And, of course, the actual addresses for all of the symbols are similarly stored in the kernel's image.

In order to access all the information in the symbol table, we must first find it within the kernel's image.

As luck would have it, the first region of the symbol table - the "Symbol Address Table", always begins with two pointers to the kernel's virtual load address (which can be easily calculated from the kernel's physical load address since there's no KASLR). Moreover, the symbol addresses in the table are monotonically nondecreasing addresses within the kernel's virtual address range - a fact which we can use to confirm our suspicion whenever we find two such consecutive pointers to the kernel's virtual load address.

Symbol Address Table

Now that we can find the symbol table within the kernel's image, all we need to do is implement the decompression scheme in order to be able to iterate over it and lookup any symbol. Great!

Using the method above to find the kernel's symbol table, we can now locate and hijack any kernel function from QSEE. Following the tradition from the previous kernel exploits, let's hijack an easily accessible function pointer from a very rarely-used network protocol - PPPOLAC.

The function pointers relating to this protocol are stored in the following kernel structure:

Overwriting the "release" pointer in this structure would cause the kernel to execute our crafted function pointer whenever a PPPOLAC socket is closed.

Putting it all together

Now that we have all the pieces, all we need to do to gain code execution within the Linux Kernel is to:
  • Achieve QSEE code execution
  • Map-in all the kernel's memory in QSEE using "qsee_register_shared_buffer"
  • Find the kernel's symbol table
  • Lookup the "pppolac_proto_ops" symbol in the symbol table
  • Overwrite any function pointer to our user-supplied function address 
  • Flush the changes made in QSEE using "qsee_prepare_shared_buf_for_nosecure_read"
  • Cause the kernel to call our user-supplied function by using a PPPOLAC socket
I've written some QSEE code which performs all of these steps and exports an easy-to-use interface to allow kernel code execution, like so:

As always, you can find the full code here:


I should note that the code currently only reads memory one DWORD at a time, making it quite slow. I didn't bother to speed it up, but any and all improvements are more than welcome (for example, reading large chunks of memory at a time would be much faster).

In the next blog post, we'll continue our journey from zero-to-TrustZone, and attempt to gain code execution within the TrustZone kernel.


  1. Great stuff, seriously, seriously good content!

  2. Hi, me again! Would you mind explaining to me how I could put this together for the Sprint LG G5? If email/Google hangouts is better for you please let me know!

  3. Wow, This is great! I'm wondering if this method can be used to hot swap/hot reboot running kernel. Just like kexec would do. I should get cracking ;)
    Thanks for the excellent research you do. Looking forward to the next post in the series. :)

    1. Thanks! Glad you enjoyed it :)

      Hotswaping should definitely be possible (but a little tricky!). A few suggestions: first prepare the whole new kernel somewhere else in physical memory, then hotswap it completely from QSEE (by calling qsee_memcpy). This is just to make sure that you don't get inconsistent data while swapping (since all other processors will be halted while the Secure World is executing). And don't forget to invalidate and flush the data cache. And most importantly (!) make sure to fixup the return address from QSEE so that when you return from the SMC call, your kernel won't crash.

    2. Thank you for the insight. I'm going to link this over XDA and try to find someone less busy to work on this. It might be a few weeks before I can start myself.
      Thanks again :)

    3. Gal, I just started trying this out on my fire phone, How did you find the secapp-region start address for your phone?
      Plus can the the exploit run as a standalone binary under root or should it be loaded into mediaserver process somehow?

    4. Madushan, the exploit can be ran standalone as root (or media). As for the secure apps region - it is part of the kernel's dtb. Also, when the device boots up, theres a message printed to dmesg along the lines of "secure apps region..." which contains the address and size of the region.

    5. Thanks gal, I found the values and tried the exploit with them but no luck. Looks like I'm going to have to dig a little bit more.

    6. Make sure you adjust all the symbols to make your version of Widevine. Did you manage to find the WV application in the secapp region?

    7. No, I couldn't find the WV app. Maybe my phone have a older version of WV. I'm going to try your procedure from the beginning and try to find what's wrong this weekend. :)

  4. Thanks for all the great posts and research, I have only just started on the road to understanding how this stuff works and have learned so much from reading your blog. Look forward to the next post :)

    1. Thanks Joel! Next post should be up this weekend :)

  5. i always get this: sush: ./wotw: can't execute: Permission denied

  6. how can this be molded into a root for android 6.0.1

  7. How did you compute the CALC_STACK_TOP_OFFSET vaule for your phone ?

    1. Very good question!

      In the application's first function (sub_0) there's a function that returns the stack top address (see http://bits-please.blogspot.co.il/2016/04/exploring-qualcomms-secure-execution.html?m=1).

      From there I just counted the number of times things were pushed onto the stack from the main function up to the stack frame of the command handling function.

      I didn't see a lot of changes between versions/devices, so if you want to skip that, you can just manually try offsets from the stack top until you find the right value.

  8. Where can i find the qsee_register_shared_buffer function?

    1. Look for the string "qsee_register_shared_buffer" and XREF from there - it's just a wrapper around qsee_syscall with the appropriate command code (0xFFFFFF04).

  9. looking for the function body of qsee_syscall 0xFFFFFF04 command code

  10. This comment has been removed by the author.