Part I: CTFZone Paper: Trust Area — Backend Part
Part III: CTFZone Paper: Trust Area — Infra
About application architecture
The primary task of the Android client was to proxy the calls from other clients in the emulator, including the checker, to the team’s respective backend server. For this purpose, the following was implemented on the client side:
- caching to reduce the backend load,
- ability to create data backups.
The client architecture is presented in the diagram below:
How we developed the client and the problems we bumped into
The service client was written in Kotlin, which is used for the development of real mobile apps. (Although Java is also used, we opted for Kotlin so that our task embraced the latest trends in mobile software).
It was not without snags along the way.
From as early as at the concept of our task, it was clear that we wouldn’t be able to provide access to the app’s visual part. This led to certain limitations to Client-Side vulnerabilities and made it evident that our app will run exclusively on IntentService services. This is where the first problem lay. The latest phone models from Samsung, Google, Xiaomi and other vendors have a battery optimisation feature: the OS may turn off a background service, which consumes excessive resources or is not being interacted with (or can do this for no reason). What’s more, the service doesn’t respond to a reboot via adb and can’t be called with an Intent. Given that we used a real device, we ran into this issue at the development stage. Every time after such a halt, we had to reboot the app directly from the smartphone.
The solution we arrived at was to disable battery optimisation for a particular app and run a critical service as a foreground service (only available in Android API 26). However, we didn’t have a chance to use this solution ourselves: the testing process and the final solution were implemented in the emulators and emulators have no batteries, hence no optimisation problems :)
The second problem we faced: we had to somehow distribute the shared resources in the emulator among the team’s clients so that not a single team could cause a DoS to others by exhausting the resources.
But we didn’t have to think of a solution: we found out that the required limitations were embedded in the OS:
- heap limitations — 2–36 MB,
- an app can’t store more than 3.5 GB on
/sdcard/Android/data/
, - it can’t use more than 200 MB on
/data/data/
, - network traffic per app is not limited (but we would have seen the source in traffic dumps).
We were okay with these limitations and could move forward to developing and implementing the bugs.
However, on the day of the competition, one of the teams accidentally caused a DoS to the other clients: it created several thousand BroadcastReceivers responsible for client-to-client communication. We figured out the source of the incident by the number of registered components of the flawed app in the emulator. After a warning, the team quickly fixed its app. And while they were busy with their app, we had to stop their client manually every round.
About client-to-client communication
While everything was clear as far as the communication between the Android client and the backend server was concerned (via an HTTP protocol), the client-to-client communication did not appear obvious.
The thing is that network communication uses a NAT mechanism, which does not allow to distinguish the IP checker from the rest of the clients. However, there’s no such mechanism when it comes to communication via intents.
The first solution that we found was to write our own NAT (IntentProxy), specifically for intents. It was supposed to work as follows:
- Client1, with an intent to send a request to Client 2, sends a PendingIntent to the IntentProxy specifying the recipient and data.
- The IntentProxy replaces the sender with itself and forwards the PendingIntent to Client 2.
- Client2 is unaware which of the clients has sent a PendingIntent since every client interacts solely with the IntentProxy by default.
This solution was considered as the only possible one for quite a while, despite its drawbacks:
- The IntentProxy was a single point of load — hence, a single point of failure.
- This was another service for us to write and administer.
- A simple functionality concealed a number of problems — we had to decide how to deal with:
- intent queues,
- asynchronous delivery of responses,
-a mechanism for processing such events as the absence of a response from the client, incorrect response formats, etc.
In the last minute before the competition kick-off (for international contest finals ‘several weeks’ = ‘last-minute’), another awesome solution popped up — dynamic BroadcastReceivers. So, the clients’ communication transformed as follows:
- Client1, with an intent to send a request to Client 2, registers in the system a BroadcastReceiver with a random action.
- Client1 sends a standard intent with a request and the said random action to Client 2.
- Client2 doesn’t see the sender in the standard intent, but the action indicates where to send the response (as a Broadcast Intent).
- Client1, upon receiving the response, closes the BroadcastReceiver.
Thus, all we had to do was provide the teams with an interface enabling such communication, rather than create a whole special app.
We implemented a separate class in the IntentMessagingSystem.kt for the communication through BroadcastReceivers with coroutines, callbacks and other smoothies.
In addition to the IntentMessagingSystem, an Echo-service was rolled out on each client. This service received an echo request from the checker at regular intervals. It was designed to help the teams launch their exploits (by default, the client was functioning only when interacted with by the other clients or the checker).
You can find the entire code for the Android client on Github.
About vulnerabilities
It’s essential for a good app to have no vulnerabilities unless it’s a CTF app.
We came across loads of snags when searching and embedding vulnerabilities to the app’s client side, where we managed to implement only two out of the three projected bugs.
Naturally, the first vulnerability we were going to build into the client, had to do with a quote.
On the client side, the information was stored in the SQLite database, with a popular Room framework as a program interface. And it proved to be pretty damn secure! The only way to make an injection was to write an extension for the class. We thought it was way too simple to detect. So, we ended up with another bug, which was easier to implement:
For example, the username field could take a zfr%
value and the search would extend to several users. Together with the logical vulnerability that allowed to read the flag undisguised from the local cache (its role was performed by the SQLite database), this also enabled the players to bypass the access rights and read the flag.
While the cache part of the vulnerability was not so obvious, the part which used LIKE was the first fixed by the teams. Unfortunately, this screwed up the entire attack vector without a single quote inserted :(
The second vulnerability that we aspired to implement involved the use of a PendingIntent and standard intents.
PendingIntents did not fit into the architecture. Instead, we considered two known vulnerabilities:
- the use of empty intents (Intent() as basic, during PendingIntent initialisation),
- Intent Redirection.
However, the experiment showed that the first vulnerability didn't work (for quite some time, as it turned out). The second one just fell away when we switched to the BroadcastReceiver and moved away from using the PendingIntent.
That's why we didn't create a single vulnerability related to intents. Sometime later maybe :)
The third vulnerability — Directory Traversal. You can easily spot it in the BackupRepository.kt. It allowed the players to get access to the rivals' backups and grab cache from the apps.
About the teams' authenticity
During the competition, we monitored the vulnerabilities exploited by the participants. The screenshot below demonstrates the most bizarre code snippets.
Source code:
- The Client Part: https://github.com/ctf-zone/CTFZone-TrustArea-Client-1
- The Backend: https://github.com/ctf-zone/CTFZone-2020-TrustArea-Backend
- The Emulators: https://github.com/ctf-zone/CTFZone-2020-Trust-Area-Infra
- The Checker Agent: https://github.com/ctf-zone/CTFZone-2020-Trust-Area-Checker-Agent