Sunday, December 17, 2023

The Shuttle Schedule Shuffle


Here's a tale of reverse engineering an app to automate my work shuttle bookings. It was a curious dance of technicalities and hurdles, with a little sprinkle of triumph. 


The Issue

Here's the issue: my workplace provides a shuttle service, whose app interface was... let's say, less than user-friendly. However, with only six seats and booking available just 7 days in advance, securing a ride isn't always straightforward, particularly since I only commute certain days each week and those days are heavy in demand. The app's user interface is far from intuitive, which made the booking process cumbersome. 


The Goal

As a software engineer and tinkerer at heart, I couldn’t resist the urge to streamline this small friction point in my daily workflow.  This got me to into this project: reverse engineering the application to automate shuttle booking based on my Google Calendar schedule. The Goal I aimed to identify the authentication method and server calls, then replicate them on my home automation server. Rather than manually controlling the schedule, I wanted the system to check my calendar and automatically book or cancel, accounting for company holidays. Only time I would need to intervene would be when I need to change the schedule or handle any exceptions.

Sounds like a fun challenge, right? 

Plan of attack

  1. Run the App in a Controlled Environment: The first step will be to run the shuttle booking app in a controlled environment. This will allow me to observe the app's behavior and identify the API calls it makes to the server.
  1. Intercept the Traffic: Once the app is running in the controlled environment, I will use a proxy to intercept the traffic between the app and the server.
  1. Handle HTTPS Communication: The communication between the app and the server will invariably use HTTPS, which will make it harder to intercept the traffic. I will need to find a way to make the app trust the proxy certificate, allowing me to intercept the traffic in clear text.
  1. Automate the Booking Process: With the API calls identified and intercepted, I will then automate the booking process. This will involve writing a script that checks my calendar for the days I am going to work, ignores holidays, and automatically adjusts the bookings a week in advance.
  1. Handle Failures: In case of any failures in the automation process, notify me so I can manually take care of it.


The Reverse Engineering Process

Let's dive into the steps : 

1. To begin the reverse engineering process, I loaded apk in jadx-ui to examine the app's source code. My aim was to identify any pinned certificates that could hinder intercepting API calls. Pinned certificate, though not impossible, but are a pain to bypass. After discovering the use of the okhttp library, I searched for references to okhttp's certificate pinning method but found none. So surprisingly app didn't pin certificates and relied on OS-provided ones. One hurdle down. 

2. The next task was to examine the lowest version of Android supported by the app. Why? Because post Android Nougat, user-installed certificates are untrusted. So, even if I installed a proxy certificate, the app wouldn't use it, making it impossible to intercept the API calls. 

3. Next step, check if app will work on pre Nougat version. Luckily upon checking the app's manifest file, I saw the app was certificate to work on old android version. An pre-Android Nougat emulator seemed like the solution and I was able to see traffic from the app ! Well, success was shortlived, since I hit another roadblock... The app used my company's Single Sign-On (SSO), which was incompatible with lower Android versions AND required device enrollment in the company system. So that means, I would not be able to log in to the app. Without login, I would not be able to intercept any traffic at all. :( 

4. What if I use my actual android phone ? My actual android device was already enrolled in company device management software, but alas, since my real phone is up to date on android version (ironically, to safeguard against security threats) the app would not trust user-installed certificates on it. It felt like a classic catch-22. 

5. But then, I found a glimmer of hope. Android documentation suggested that an app could trust user certificates if app author specifically allows it in the Android manifest file. 

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>  
      <base-config>  
            <trust-anchors>  
                <!-- Trust preinstalled CAs -->  
                <certificates src="system" />  
                <!-- Additionally trust user added CAs -->  
                <certificates src="user" />  
           </trust-anchors>  
      </base-config>  
 </network-security-config>

6. Armed with this information, I set my eyes of decompiling and patching the app to honor user certificates. 

7. While I was getting ready to manually patching the app, I stumbled up an app called Apklab which automates the process of decompiling and even patching the manifest file. First half of decompiling and patching went fine, but then, another hiccup - it was unable to compile and signing again due to an old XML parser that would break on any '&' in XML. This app hasn't been updated in some time, so had to find another way.

8. Fortunately, upon searching app's XML files, it contained only one occurrence of the "&" character, which was within a string label tag. Which means even if I update that string, it won't get in the way of any functionality or interfere with patching. By manually editing the file to remove the "&" character from that string, I successfully created a new APK.

9. The next step, to intercept the traffic, was to install open source Zap proxy on my laptop, and generate a certificate. That certificate was to be installed on android phone as a user certificate. Since our app is now patched, it would trust this certificate and allow communication through proxy.

10. I fired up the app, and voila! All API calls were visible in zap. 

11. I then ran through all usual operations (authentication, getting list of current bookings, available slots, booking, and canceling appointments) so all rest calls with input and output would be recorded in zap proxy history. 

12. So it worked. Kinda... Another potential problem arose when I noticed that the auth token had a 30-day validity. How would I get a new one after this period? Would I have to run app again and fetch new token ? Nah.. too much work for a lazy engineer. The app must be renewing it's token somehow. But missing piece of puzzle was, there was no reference to 'refresh token' in any of the calls, yet the patched app was able to send new tokens after every few calls with new validity period. How could it do it ? A client had to make a call to get new token, and yet there was no such call ! Was client generating it's own token on the fly which was trusted by server ? Though possible, but no self respecting (security) engineer would write such code. The new token had to be generated on server side and sent to client somehow.

13. After scouring though the rest request/responses, I finally found that some of the REST call responses returned an extra header containing new token ! What it meant was, the server was occasionally updating the token and sending it to client in response headers for it to use, extending its 30-day life cycle. Now I could monitor for that header in responses and store the updated token for next use. Final hurdle crossed !

With this information, I built an automation script (in node-red, which is my choice of home automation server) that checks my Google Calendar for my work schedule, and adjusts the bookings a week in advance. It automatically cancels any previous reservations if I decide not to go on certain day. It also reserves the shuttle for any non-routine day if I add it to my calendar. And to top it off, in case of any failures, it sends me a message on Telegram so I can handle it manually (or further automate it :-) ). 

There you have it. Mission accomplished. I hereby conclude from my this coding saga that to an engineers, each friction seems nothing but a nail in want of a Thor's Mjölnir.