Skip to main content

Analysis of the UTS app

Introduction

The UTS (Unreserved Ticketing System) is an initiative by the CRIS division of the Ministry of Railways allowing people to book paperless unreserved (a seat/berth is not guaranteed) train tickets in advance. The goal of the app is to reduce long queues at railway stations for physical tickets. The UTS project boasts of handling approximately 2,34,000 tickets daily, with 6333 stations as the originating points.

The number of people travelling ticketless in Indian Railways has consistently been increasing over the years, with the number touching 3.6 crore as of 2022-23. One of the several reasons people travel ticketless besides not wanting to pay is the inconvenience of standing behind long queues at the stations just to get the ticket. The introduction of the UTS app in 2018 (across all zones) was a welcome move to tackle this problem.

Flow

The ticket booking and login flow has been extensively analysed and critiqued at this and this blog.

Problems

  1. Location Restrictions: To combat the issue of people only booking tickets on the spot in the event th ey are asked for one, the UTS app does not allow ticket booking inside a fixed radius from the centre (?) of the railway station. This has resulted in a lot of problems which are widely reported on social media.

  2. Tamper Detection: In an age where vendor ROMs make android phones barely usable, detecting root or custom ROMs is an unnecessary mechanism with the exception of maybe banking applications.

The Architecture

The android application for UTS has the package name com.cris.utsmobile. The version being analysed in this post is 15.1.50. The application logic is mostly handled by the Java layer with some security specific stuff and endpoints being fetched from the native layer, which is very common to see in mobile applications.

It is always a good idea to look at the APK contents before dropping it into a disassembly framework. In UTS’ case, there is an interesting observation to be found.

Hidden deep under a path in the assets/fonts folder is a database file named UTS1124.db


/images/7zFM_wl2pnbZ788.png

There are some references to SQLite (more on that later) in the java layer, but opening the database with an SQLite viewer leads to an error, indicating that the DB is either using a custom format or is somehow encrypted.

In the lib folder where the native binaries are found, we encounter two shared libraries named libnative-lib.so and libsqlcipher.so , confirming our suspicion that the DB file is actually encrypted.

/images/javaw_3Gnknt2qr6.png

The main java code for the program lies in the package com.cris

com.cris.uts deals with utility stuff such as database handling and encryption while the other package contains most of the actual app code.

The Database

Performing a string lookup for “UTS1124” returns a reference to this location in the package com.cris.uts.database


/images/javaw_ynyJoqbSpn.png

We find several functions in the class responsible for loading elements such as Station Names, latitudes and longitudes. Each function calls the method getWritableDatabase which in turn calls down the line calls sqlciphers openDatabase.

The second parameter to this function is the passphrase that is used to derive a key and decrypt the database.


/images/javaw_ha0o3WO5ot.png

getWritableDatabase takes only one argument which is the passphrase.

The “key” is retrieved from the native lib by calling the function Java_decode_code_stringForDB


/images/javaw_uaSSyCg62R.png

Opening up the library "libnative-lib.so" in IDA, and navigating to the function Java_assetdbhelperlib_in_org_cris_com_assetdbhelperlib_Utils_Java_1decode_1code_1stringForDB, we see from the decompilation that the function logic is somewhat obfuscated and seems to be building a string by combining a bunch of binary values.


/images/ida_BrlrClMi6S.png

However, we need not attempt to understand or even deobfuscate this function. We can simply retrieve the static value by hooking the java function on app startup and capturing the return value. It is ensured that the function is called since the app has to read the database at least once after startup.

/images/DB_Browser_for_SQLite_JR2SRdUTRc.png

Doing exactly that, the decryption passphrase was captured and the decrypted database was dumped through the sqlcipher utility.

The dumped SQLite database can be viewed normally with a SQLite browser.

There are several tables, most of them are self explanatory. However, one notable table is the MUSER_STATION table, which contains exact latitudes and longitudes of (almost) all the railway stations in India, along with their station code. These are the exact coordinates which are evaluated in the geofencing procedure when one tries to book a ticket.

The contents of this table are available here in a .csv format and can be analysed. Most of these coordinates lie exactly in the centre of the station when viewed through Google Maps imagery, however some are outliers and might lie at extreme ends. Having these coordinates handy can be helpful in urgent booking situations since the app does not inform the user directly of the coordinates.

Besides holding these values, the database is also responsible for acting as a central repository for all the booked tickets that get saved to the mobile phone and can be viewed even when offline. Updates/patches to the database can also be pushed dynamically by the UTS server whenever needed.

Station QRs

The UTS app offers a QR booking mode as an alternative to the journey mode, with the difference that the app enforces a maximum radius of distance from the station coordinates in contrast to the minimum radius restriction while booking through the journey mode. Therefore, a ticket through this mode can only be booked while inside the train station.

These QR codes are pasted at various locations at all railway stations and are encrypted. To save time, people have exploited the static nature of QR codes and maintained a repository of crowdsourced QR codes from railway stations across the country and hosted them across various websites. However, this is overkill since the decryption logic for the QR codes is present in the UTS app itself.

The function generateQRKeyIn located in the class assetdbhelperlib.in.org.cris.com.assetdbhelperlib.VersionComparator contains the key building logic for the QR decryption. Base64 Decoding the contents of a QR code and decrypting it using AES-ECB with the aforementioned key churns out the plaintext contents which are of the following format:


ASANGAON : ASO : 19.4393835000000 : 73.3077903000000

Station Name : Station Code : Latitude : Longitude


These values are consistent with the details already stored in the in-app database, therefore, it remains a daunting question why the redundant coordinates in the QR codes exist in the first place.

A PoC is available at poc.

It turns out that only the station name and code are verified during the ticket booking procedure, therefore it is possible that one could simply change the coordinates to their current location, re-encrypt the contents (AES is symmetric) and base64 encode the same to generate a QR code, which can be scanned using the app and ultimately allows anyone to book a ticket for any station in the country (which is already possible anyway through the non-QR workflow).

UART on PayTM Soundbox (Part 2)

Understanding the chipset

Quectel EC200U-CN is a low-cost LTE Development Board, which is perfect for the use case scenario of these soundboxes since they can’t just be connected to a geographically tethered access point; and need to be connected to the internet while on-the-move.

The datasheet for this chipset is available on Quectel’s website and can be downloaded from here.

The chipset houses a lot of components under its metal cover, including an SPI NOR Flash with a size of 64 megabytes.


/images/paytm2_1.png

To analyze how the MQTT (presumably) communication takes place between the soundbox and PayTM servers, one needs to either:

  1. Sniff the communication traffic

  2. Find out a location where debug data is logged; or

  3. Dump and analyze the firmware

Since the soundbox uses a SIM (4G-enabled) and not WiFi to connect to the internet, it is difficult and might not even be possible to sniff the LTE traffic in most cases; therefore option 1 is implausible.

Getting access to the UART shell for this device might help us in executing both option 2 and 3.

The UART pinout

The datasheet describes that the EC200U chipset supports the UART protocol over pin 67 and 68.


/images/paytm2_2.png

Pin 67 and 68 are located on the periphery of the chip and therefore are accessible directly through the PCB without carrying out the disassembly or desoldering of the chip from the board.


/images/paytm2_3.png

After hooking these pins up with a trusty 10$ chinese logic sniffer which works perfectly with the Saleae Logic software, one can obtain a boot log on the UART console with a baud rate of 115200.


/images/paytm2_4.png

Some interesting strings down the log can be seen:

  1. APP_VERSION V3.6.0_EN , LIB VERSION: OAK4GQ01

  2. Base Firmware Release Date: 29/11  10:30:41:00

  3. RTOS VERSION: EC200UC

  4. PUBHOST          : iot-prefix-suffix-sounds.paytm.in

  5. [src\PayTM/awsMqttPublishQueue.c][92][INF][MQTT][TraceLog] --> gMqttPublishQueue initialized success.

It can be ascertained from the above that the soundbox runs a custom fork of FreeRTOS and does use MQTT for communication.

I tried to interact with the UART console by trying to send keystrokes and see if they were mirrored, however failed which could be because the UART is somehow locked down to basic debug messages for internal purposes or I had committed a wiring misconfiguration.

Since the UART dump was mostly a failure, the firmware will need to be dumped for analysis by sniffing the SPI flash lines, which requires desoldering the chip and is unfortunately out of my scope.


Analysis of the Learnyst DRM scheme (Part 1)


Disclaimer: The research included in this post is solely intended for educational purposes and does not in any way condone, promote or incite piracy. The consumption of this information shall be carried out at your own risk and the author cannot be held responsible for any damages caused, if any.


Learnyst is a content-serving service catering to small to medium-sized educational creators to help protect their courses from unauthorised sharing and piracy.

The popularity of Learnyst seems to have increased after a period of rampant course piracy by actors who seem to resell such courses at low prices, thereby directly harming the sales of said content providers.

In addition to being a framework provider for educators, Learnyst has a homegrown DRM scheme built on top of their existing Google Widevine / Apple Fairplay framework.

To my knowledge, only 3 versions of Learnyst DRM exist, namely v2, v4 and v6 (only seen being used on web browsers).

Presumably, the difficulty for each one of them increases with the incrementing version number. This post focuses on the analysis of the v2 version only (later versions might be covered in the future posts).


Discourse - Brief explanation of DRM

Digital Rights Management (DRM) is a technique to prevent unauthorised misuse and copyright infringement of media and software. In the case of media, raw files are “encrypted” by conventional encryption algorithms (such as AES), and the decryption key is kept secure during the transfer flow of the key from the server to the client (in this case, the device playing the media) under secured enveloped structures with complex key derivation algorithms. Playback is either carried out in a hardware-secured enclave such as Qualcomm Trustzone and the Apple Secure Enclave to prevent the interception of decrypted frames, or under a heavily obfuscated userland library if hardware support is not available.

Google Widevine, Microsoft Playready and Apple’s Fairplay Streaming are the examples of media DRM schemes which have sophisticated implementations for both hardware and software protections.


The curious case of Learnyst

Learnyst has devised an even stricter protection scheme to protect their customers’ content. They have developed a custom “DRM” scheme of their own which wraps around the Widevine license messages and encrypts them to make an attack on the same even more difficult than it already is.

Why they have implemented this, beside Widevine already being secure enough, is unknown. Although, it can be speculated that due to the recent public exploits that target Widevine L3 might have rendered it not as effective as it used to be, calling for a revised/improved scheme.


The technicals of Learnyst DRM

There are two ways to implement any sort of DRM.

  1. Protection in a web-app

  2. Protection in a native android application

For (1), Learnyst seems to have implemented a WASM-JS approach to implement their DRM. For (2), a native library has been embedded inside an application protected by Learnyst. This post focuses on the (2)nd way.

I have not been able to analyse the implementation on Apple devices due to the lack of an iOS device.


Obtaining the native library

To obtain the embedded library of Learnyst, one has to obtain and decompress the APK of a Learnyst-supported app. There are many such examples, consisting of mostly educational institutions. However, Learnyst itself offers an app named Learnyst University which offers a couple of free sample courses and are protected by Learnyst DRM!


/images/app.jpg

This seemed to be a good start, but alas, the app actually appears to be broken and neither plays any video, nor is any license request sent to the DRM servers. This approach seems to be a dead end, though the website for the same appears to be functional.

In spite of this, it is wise to isolate the Learnyst Native library responsible for handling all the DRM operations.

To confirm that the DRM operations are handled at the native and not the java layer, the APK was unzipped and baksmali’d with apktool.


It was observed that com.learnyst.lstdrmv2 has very clear references to the native library named lstsec.

/images/ref1.png


/images/ref2.png

And indeed, it was found that a library named liblstsec.so exists in the APK. Let’s look at it now.


Analysing the native library

Loading the library up in IDA and taking a quick look at the function table reveals the JNI functions, and also indicates an interesting string which probably refers to the Version 2 of Learnyst DRM.


/images/ref3.png

While analysing the JNI functions, some interesting secret keys are uncovered, but they are not important for now and will be documented later.


Paytm SoundBox (Part 1)

Analysis of the PayTM Soundbox

Background

Online payments in India have taken a massive boost ever since Unified Payment Interface (UPI) was introduced in 2016. In addition to NEFT, IMPS and RTGS, and the exception of private “wallet” providers such as Paytm, people were now able to transfer money however small the amount be, instantly, just by scanning QR Codes, resulting in the money instantly getting debited/credited to their bank accounts.

The introduction of UPI was fundamentally a change to the Indian payments system and has transformed the way people view cash. People no longer needed to look for exact cash change and could pay in arbitrary amounts to arbitrary people.


/images/Digital_1.png

However, naturally, this led to some issues, mainly with the verification of the authenticity of a payment. Merchants who transacted huge volumes of amounts daily were overwhelmed with the number of credit SMS, or checking their account statements.

This called for the introduction of a mechanism which could, in real-time, convey the authenticity of a transaction to a merchant in the lowest time possible.

One of those solutions was the SoundBox, first developed by PayTM, and followed by PhonePe and others.


/images/paytmbanner.webp

The SoundBox was a revolutionary product that went popular with Indian merchants in no time. Over time, several revised versions of the SoundBox with added functions were released, however the fundamental design remains the same.

The SoundBox consists of a QR Code, which resolves to a unique UPI URL (with the upi:// prefix) per merchant, which is meant to be scanned by the customer. A SIM (3G/4G) connected to PayTM’s servers to fetch UPI transactions, a PCB, and a speaker which spoke aloud the receipt of a payment along with the amount sent.


Into the Soundbox

The following findings are based on the product called PayTM SoundBox 3.0 4G.

Before ordering a SoundBox for myself, I looked up information online on the internals of one. Luckily, a blog by Pallav Aggarwal details the teardown of the SoundBox 2.0, and has some research on the components on the PCB. However, no more analysis on the PCB has been performed publicly. This post tries to detail some of the internals of how the SoundBox functions at the software level.

Opening up the SoundBox is relatively simple, the speaker and PCB is housed inside a plastic casing along with an antenna, held together by a few screws. An SD Card (?) and a SIM is attached to the back of the PCB.


/images/teardown_1.jpg

The PCB for SoundBox 3.0 appears to be the same as the 2.0 4G on the surface with only the informative version differing (V1.25 - 3.0 and V1.08 - 2.0), however there might be design variations which I have not taken a look at, and are out of the scope and relevance of this post.

The brain of the PCB is the Quectel EC200U (CN Variant) which is described as “a series of the latest LTE Cat 1 modules optimised specially for M2M and IoT applications”. The CN specialisation just pertains to the LTE frequency band regulations in the South Asian regions.

The EC200U chip here not only handles the LTE operations but also everything else and functions as the CPU on this board.


SD Card

Besides the EC200U Datasheet, not much can be known about the SoundBox simply by “looking” at it. A way to communicate with the PCB is needed.

The obvious entry point seems to be the SD card. Although no binaries are found, there seem to be all the necessary audio files responsible for announcing the payment receipt, which can be downloaded from here.

In addition, there are a few non-verbose log files that reveal close to nothing about the actual software on the chip.

A config file is also located in the SD Card which contains a few endpoints for OTA updates. However, the exact usage of these endpoints cannot be derived just from the information in here, we need to somehow dump the firmware used on the chip, and is discussed in part 2.

aSUS

Improper protection of DRM Root of Trust (OTA UPDATE) on ASUS ROG 3

Device Fingerprint:

asus/WW_I003D/ASUS_I003_1:11/RKQ1.200710.002/18.0410.2201.192-0:user/release-keys

… aaand, newer firmware versions


What is Widevine?

Widevine is a DRM scheme owned by Google used to protect content delivered by OTTs (ex. Amazon Prime Video, Netflix, etc.) and is also used by educational content providers to prevent piracy. Widevine uses a robust mechanism for the encryption of delivered content and offers a secure mechanism for the CEK (AES-128) delivery.

Widevine has three security levels,

L1 – Content decryption and cryptographic operations are carried out in a secure enclave such as TEE.

L2 – Cryptographic operations are carried out in a secure enclave.

L3 – Both decoding and cryptographic operations are carried out in the userland.


The Issue

ASUS ROG 3, on its release, was a Widevine L1 certified device. But due to some unknown reasons, many random devices of the model had their Widevine level downgraded to L3, which resulted in many customers not being able to stream content in HD quality.

ASUS pushed out a software update to install L1 in the affected devices to address the issue.

Although it is recommended that phones be physically brought to the factory to install the L1 keyboxes (root of trust, more on that later) in the device TEEs, OEMs sometimes make provisional arrangements to deliver these secrets through OTA, especially when the affected number of devices is high.

Google recommends that OEMs shall have some type of secure cryptographic secret to encrypt these keyboxes through OTA delivery, and it is solely the OEM’s responsibility to take care of it. This is where ASUS went wrong.


The Widevine Keybox

Widevine uses a 128/132 byte secret key unique to a device which it authenticates with its provisioning servers to securely install a corresponding certificate and a private key to the device which could then be used to authenticate with Widevine license servers to retrieve licenses.

A device can provision an unlimited amount of different private keys which are all valid and can be used for License retrieval.


The Mechanism

In the newer firmware versions with which the fix was shipped, one can, on close inspection, find a bash script named ‘ReInsWDKey’, which I think stands for ‘ReInstall WiDevine Key’. Anyways, it performs the following steps:

  1. Look for /ADF/keybox.bin

  2. If it exists, call a binary /vendor/bin/install_key_server with the argument decryptWD

  3. Delete /ADF/keybox.bin


Naturally, the next step was to look what this ‘install_key_server’ does, and what I found was this:

We will come to server_main() later


/images/media1.png

Relevant part of function decrypt_WDkey():


/images/media3.png

Here, we see that the binary opens the file situated at /ADF/WDKEY, performs bitwise operations on it, and saves it into /ADF/keybox.bin, for which ReInsWDKey checks for in the bash script.

And guess what? The output of the bitwise operations is the PLAINTEXT KEYBOX!



Where aAsus is simply the string "ASUS".

Yes, the OTA “encryption” (which might not be the right word) key of ROG 3 keyboxes is ‘ASUS’, which is beyond absurd for an international OEM of a scale this big as ASUS.

To confirm this finding, porting the relevant bitwise operation code to Python reveals the plaintext keybox.


/images/media2.png

Which is a 128 byte value, and is installed to the TEE later with the help of the OEMCrypto API using the function server_main() we saw earlier.

I confirmed that it was indeed the plaintext keybox since it contains the magic ‘kbox’ which is defined in the keybox format by Google, followed by a crc32 checksum.

I am not sure if the same “encrypted” keybox (WDKEY) has been provided to each device.

Going by the rules, every device should have a different keybox, but I cannot confirm the same because of a lack of another phone of the same model.

wa.me bug

What?

A hot WhatsApp forward bug is on the rise, which causes the Android App to crash if the message contains a URI with the string "wa.me/settings".

A lot of solutions have been offered online, but why does the crash get triggered? I searched a lot online for this, but didn't find anything technical. Naturally, it was time to take a look at it closely.


How?

/images/wa.me_settings.png

To trigger the bug, I sent the message "wa.me/settings" on WhatsApp Web to myself. Opening the WA app on my android phone caused it to crash automatically. So, this bug is indeed real.


Why?

/images/wa.me_1.png

Looking at the crash logs, we can see that the bug is a IndexOutOfBoundsException triggered because of an anonymous internal class function of the WA app named X.0kg.A0Y. (Note that this name can differ over WA versions)

wa.me links in WA are called Deep Links, and are used to quickly access a certain Activity in the app.

Navigating to the above mentioned code path (which handles these deep links), we find that the wa.me URI Path Segment is actually supposed to contain a sub-path of the /settings path.




/images/wa.me_2.png

if (pathSegments.size() >= 1 && "settings".equals(A04(pathSegments, locale, 0))) { return A0A(uri,

C12340ko.A0k(pathSegments, 1));}


Let us take a look at the A0k function:

public static List A0k(List list, int i) {

return list.subList(i, list.size());

}


Clearly, it can be seen that it returns a subList of the List supplied to it, starting with the index i, which is passed as the second argument to the function.

But since our string "wa.me/settings" clearly doesn't have a sub-path, the returned list will be an empty list.




This effectively means that we are passing an empty list to the function A0A as its second argument.

Relevant part of the A0A function:

public final int A0A(Uri uri, List list) {

if (this.A02.A0Z(C55062jy.A02, 504) && A0E()) {

Locale locale = Locale.US;

String lowerCase = ((String) AnonymousClass0kg.A0Y(list)).toLowerCase(locale);




Relevant part of the A0Y function:

public static Object A0Y(List list) {

return list.get(0);

}

Which translates to, "return the first element of the given list", but since the given list is an empty list, an Out of Bounds Exception is trigerred, causing a crash.

To verify the correct usage of the wa.me settings path, I tried sending the string "wa.me/settings/chats", which correctly renders the correct app pane, without causing any sort of crash.


In short, whoever found this bug (if not accidentally), has a sharp eye for number comparisons ;D

P.S. Personally tested this on WA version 2.23.10.77

pee-praghna

Context

/images/coaching_sts.jpg

It must be said that at this point, India has more coaching institutes than the number of cultures, languages and religions it houses. And naturally, more coaching institutes means more websites, and more websites means more vulnerabilities.

One such case is Sri Chaitanya's ePraghna.

A friend of mine was enrolled at Sri Chaitanya Institute for his JEE preparation. In spite of being a fairly popular institute in South India, producing toppers since time immemorial, and claiming to have "taught" more than 7,50,000 students, its online presence security, unfortunately, falls way shorter.


Strike One!

/images/strike_one.png

Upon login, the website presents a very innocent looking login page. At the time, I did not have the correct password to my friend's account as he had changed it recently, so I began to look at the 'Forgot Password' section of the page.

Although I was not very adept at browser traffic sniffing, I noticed something very unusual, which was that the mobile number check prompt was pretty fast, and it seemed like no server sided validation was taking place to validate the phone number.

This was step one. I fired up burpsuite and found that the mobile number was returned in full and in plaintext, and validated client side, on the GET call to:

https://www.epraghna.com/forgotPassword? admNoVal={admissionNumber}

Thus, a vulnerability that could be (hypothetically, of course) used to scrape the phone numbers of everyone registered to the institute.


Strike Two!

/images/strike_two.png

After bypassing the first check, there lay the problem of validating the actual OTP, but I found out that it could be controlled client side as well.

The OTP request call was a GET call to:

https://www.epraghna.com/getForgotSmsOtp? phoneNo={arbitraryPhoneNumber}

No association of the OTP with the student account whatsoever, which is utterly absurd and doesn't even make sense.

Thus, a vulnerability that could be (hypothetically, of course) used to spam OTPs to arbitrary phone numbers, essentially becoming an SMS bomber.


Game Over!

There exists a third GET call to:

https://www.epraghna.com/checkforgotSmsOtp?phoneNo={phoneNumber}&otpVal={otp}

which seems as if it is validating the OTP in some way, which, yes, it actually is, but looking closely at the JS, it serves no purpose. The check could simply be skipped over, either by some console fuckery, or replicating the subsequent, or the MAIN attack vector of this post.

https://www.epraghna.com/changeStudentPassword?newPasswordVal={newPassword}&stuPassword= {newPassword}&admNoVal={admissionNumber}&mobileNoVal={mobileNumber}&otp={otp}

Yes, arbitrary values work and have no association with the student account.

Thus, a vulnerability that could be (hypothetically, of course) used to abuse student auth for malicious purposes!