Investigating Binary Exploitation for JNI on Android
This post aims to be an introduction into a blog series about binary exploitation on Android. It tries to describe how the environment that runs vulnerable modules is set up and how the damnvulnerableapp supports the process of binary exploitation on Android.
Warning
The following app is intended to be vulnerable to specific attacks and can result in arbitrary code execution in the context of the app. Therefore, beware of this and do not use this app on a device/emulator that contains personal information whatsoever. Always launch the app in a controlled environment. No authentication is necessary to connect to the app and talk to vulnerable modules. Assuming the app is free of bugs, there is a guarantee that only one client can connect at a time.
E2VA, the damnvulnerableapp
In order to properly investigate binary exploitation on Android, an app has been written that allows for running custom vulnerable modules, i.e. Java classes with one entry point, in a separate process. It is remotely controllable and constructed in a way that allows to (re-)run a module multiple times even when it crashed.
The app is named E2VA, i.e. Exploitation Experience (with) Vulnerable App. Within this blog series, E2VA and damnvulnerableapp will be used interchangably, so do not get confused!
The core of the damnvulnerableapp is a background service, called manager, that handles communication with external users (only one at a time) and translates the messages received into actions to perform. Among other things, the most important actions are:
- Selecting a vulnerable module to be run in a separate process. This has to be done, because it is very likely that the vulnerable module will crash in case we mess up with an exploit.
- Exiting a vulnerable module. This will shutdown the process that hosts the vulnerable module and revert back to a selection state.
- Forwarding messages to the vulnerable module. It is possible to forward arbitrary binary data. Of course it is up to the module to accept this or not. E.g. if a vulnerable module internally calls
strcpy
, sending arbitrary binary data will probably not do the trick. - Fetching messages from the vulnerable module. When sending a fetch request, the manager will try to read data from the vulnerable module. Depending on the configurations, this can time out or block forever.
Therefore, the usual steps are:
- Select a module
- Forward and fetch data until done, i.e. either until the process crashes or exits by itself or is instructed by an external user to terminate.
- Optionally, when trying to terminate the hosting process, manager can be instructed to do so.
As regards selecting a module, the following diagram tries to illustrate this process:
Notice that the Zygote process is responsible for creating a new activity by forking. Therefore, the vulnerable process will contain e.g. the same canary as the manager app, which was also forked from Zygote.
In addition to selecting a module, the next diagram describes how data is fetched from a module and sent to an external user:
If a vulnerable module crashes, e.g. due to a failed exploitation attempt, then the manager will detect this and revert back to the selection state. Therefore, one may select a new module immediately after the old module crashed. It is advised to not flood manager with commands as it takes time to spawn a process or detect that a process died. The latter heavily depends on the configurations and the module’s content.
Also, the app requires specific privileges in order to avoid being rendered irresponsive after some time (often after 10s). To that end the app requests ACTION_MANAGE_OVERLAY_PERMISSION
(which is a runtime permission that can be dangerous, so please run the app on a device/emulator that does not contain personal information whatsoever, just in case damnvulnerableapp gets hijacked by someone other than you). This permission seems to keep the manager alive.
Architecture
damnvulnerableapp is tested on an x86_64 Pixel 3 emulator that runs Android 12.0.0. The build number is SE1A.220203.002.A1
. Therefore, all exploits that involve shellcode will contain x86_64 assembly.
Running Vulnerable Modules
Assuming there is a vulnerable module to be run, the manager can be started from Android Studio
or via adb
. Also damnvulnerableapp should be launched in debug mode. Technically speaking, there is no need to start the app from Android Studio other than being able to attach lldb to the vulnerable module, as well as to adjust configurations to avoid timeouts etc. In order to get to more realistic binary exploitation, one should start with the .apk
file, start the app from console and go from there.
Another thing to consider is that one should not try to call e.g. execve
in the vulnerable process. This comes from the fact that e.g. execve
will “destroy” the actual vulnerable process, thus shutting down the connection to manager. As manager will assume the process to be dead, because the connection broke, it will attempt to fully kill remnants of the vulnerable process and then revert back to a select state. Thus, calling e.g. execve
dooms the vulnerabe process to be destroyed by manager. One may think of this as an additional security mechanism, or just a reminder that stealthy exploits are cooler than loud one - shot exploits.
Types of vulnerable modules
In order to allow for as many perspectives as possible for binary exploitation on Android, each vulnerable module encapsulates one of the following:
- a completely different vulnerability class than all the other modules. E.g. buffer - overflow vs. use - after - free.
- a slightly modified version of a fixed vulnerability class. E.g. a use - after - free vulnerability can result in a Write - What - Where condition or in an attacker being able to execute a chosen function, depending on the implementation.
Consider the composition of a vulnerable module:
As can be seen in the above diagram, every (currently) module uses JNI functions to introduce vulnerabilities to be exploited. This is where binary exploitation becomes applicable to Java, namely due to native function calls.
Communication with vulnerable modules
damnvulnerableapp will listen for incoming connections on port 8080
. If it is run on an emulator, an external user may connect through nc 127.0.0.1 8080
. Before this is possible, one needs to run
$ adb forward tcp:8080 tcp:8080
Otherwise, establishing a connection is refused. When trying to create a callback (or reverse shell etc.) in an emulator, i.e. establishing a connection from the emulator to the host, use nc 10.0.2.2 <port>
. According to docs
, 10.0.2.2
is a “special alias to your host loopback interface”.
The manager will only react to messages from an external user, i.e. it uses a request - response model to handle communication. Therefore, an external agent must not assume that it will be informed if the vulnerable module has a non - empty output queue. An external user always has to explicitly ask the manager to fetch available output data.
In order to ease communication with the damnvulnerableapp and therefore the vulnerable modules, a client emerged that wraps the most important functionalities required to interact with the modules. The client is based on pwntools
, but can easily be translated to work with plain sockets
aswell.
The following is the implementation of the pwntools
- based client (no guarantees for correctness and completeness):
#!/usr/bin/env python3
from pwn import *
from typing import Tuple
TIMEOUT = 5
class PwnClient:
def __init__(self, host : str, port : int):
self.io = remote(host, port)
self.handshake()
def handshake(self) -> None:
self.send(b'USER', b'INIT', capsule_type=b'INIT')
self.receive()
def close(self) -> None:
self.send(b'', b'SHUTDOWN')
self.receive()
self.send(b'', b'', capsule_type=b'ACK')
self.io.close()
def send(self, message : bytes, operation, capsule_type=b'CONTENT') -> None:
capsule = capsule_type + b' ' + operation + b' CONTENT ' + message
length = len(capsule)
self.io.send(p32(length, endian='big'))
self.io.send(capsule)
def block_receive(self, num_bytes : int) -> bytes:
message = b''
while (len(message) < num_bytes):
received = self.io.recv(1, timeout=TIMEOUT)
if (received and len(received) > 0):
message += received
return message
"""
Returns:
(length, capsule_type, operation, content)
"""
def receive(self) -> Tuple[int, bytes, bytes, bytes]:
length = u32(self.block_receive(4), endian='big')
message = self.block_receive(length)
split_message = message.split(b' ')
operation = None
if (len(split_message) >= 2):
operation = split_message[1]
content = None
if (len(split_message) >= 4):
content = b' '.join(split_message[3:])
return (length, split_message[0], operation, content)
def select(self, module_name : str) -> str:
self.send(module_name.encode('utf-8'), b'SELECT')
return self.receive()[3].decode()
def forward(self, message : bytes) -> str:
self.send(message, b'FORWARD')
return self.receive()[3].decode()
def fetch(self) -> bytes:
self.send(b'', b'FETCH')
return self.receive()[3]
def exit(self) -> str:
self.send(b'', b'EXIT')
res = self.receive()[3].decode()
self.io.close()
return res
A sample program could look like this:
...
def main():
io = PwnClient('127.0.0.1', 8080)
print(io.fetch())
io.forward(b'test123')
print(io.fetch())
io.exit()
if (__name__ == '__main__'):
main()
Summary
In this post, damnvulnerableapp aka E2VA was presented as an Android app that manages custom vulnerable modules that can be used for vulnerability research on Android OS’s. To that end, the modules try to cover different vulnerability classes to allow for discovery of Android - specific difficulities in binary exploitation. In our next post we dive into the first vulnerability and how to exploit it. Stay tuned.
Getting started
E2VA can be downloaded here: https://github.com/fkie-cad/eeva