Reverse Engineering Stagecoach

This is a blog about how I reverse-engineered Stagecoach

· 3 min read

  • Reverse Engineering
  • ssl pinning
  • android
  • mobile-security

Scope & ethics. This documents a security research performed on my own device on my own account and my own ticket, for the purpose of understanding how the app is made and how it works. This isn’t a “step by step to get free bus rides” It’s a example of how you shouldn’t always trust the servers.

If you are stagecoach don’t be scared to get in touch, I’ll tell you how bad your system is.

The Setup

  • Rooted Xiaomi 11T Pro
  • USB debugging enabled and connected to my mac
  • mitmproxy to intercept and inspect HTTPS traffic
  • Frida + objection to bypass certificate pinning at runtime
  • jadx-gui for static analysis
  • Stagecoach Bus app version 4.48.0.777

The plan here was to use jadx-gui for a base overview and understanding on how it works then focus on dynamic to intercept the requests and test around.

Static Analysis

Using static analysis from the apk I learned that the backend url relating to orders lived in strings.xml under the key backend_ws_order_url: https://android.stagecoachbus.com/retail/v2/order/

Off this route there was multiple order-service actions that can be used

  • getCustomersRecentMobileTickets-json
  • getCustomersRecentCorporateMobileTickets-json
  • getQrTickets-json
  • getCustomerOrders-json

The ticket UI was made by MyTicketsFragment, Which goes through a OrderService to talk to the backend For looking at what is the active mobile tickets I focused on the getCustomersRecentMobileTickets-json.

Setup for Dynamic Analysis

While I started to move to Dynamic Analysis I moved into issues with certificate pinning. I noticed this when I had to disable the proxy to log in due to the requests being blocked.

The traffic was pinned in two different places

  • A custom TrustManager
  • OkHttp’s CertificatePinner

Both of these had to be bypassed before mitmproxy started working, This is what objection and Frida was used to bypass. Pinning did still do it’s job here though as it did act as a speed bump in my goal.

Client Side Trusting

Now that I managed to get around the pinning I could see what the server was responding with this showed me that it returned qrTicketItems array that contained things like status, etc.

From poking around and changing these values and having to do more static analysis I found there was a few fields missing that i had to add before the app would fully show it like normal. Once I did this the app worked as if the ticket was real.

The client found if a ticket was real using whatever the server said. This made it incredibly easy to bypass, all that needed changed was the backend_ws_order_url as discussed earlier to a server I controlled that could just return my custom json.

The APK

I decided once I managed to make this work on a rooted phone I should try and make a APK that works with this bypass as well, The original part of changing the url it asks was easy but then I encountered another issue with Google maps not working to fix this issue I had to go into google cloud console and make my own key to use, I recompiled after this and the app worked fully (excl google payments)

Takeaways

  1. Don’t trust what the server says, add extra protections.
  2. Pinning is a hurdle but doesn’t stop it.
  3. Assume someone has the tools to try and do this.

All writing

Have a project or a role in mind?

Client work goes through Estopia Engineering. For anything else, email me directly.

jay@imaoreo.dev