Andante is the multimodal transport ticket used in the city of Porto. On the 28th July 2018, the company behind Andante released the Anda app to allow the users of their public transport system to no longer need a physical card as ticket. Instead, by taking advantage of the GPS, NFC and BLE capabilities of modern smartphones, passengers now only need their mobile phones to travel.
I decided to observe the network packets sent by Anda to understand which kind of requests it was making to its API. Fot that, I used a packet sniffer app that acts as man-in-the-middle listening to all packets in both directions. Since all communications of Anda are performed using HTTPS, the packet sniffer uses a custom certificate for which it has both the public and private keys. Soon I realized this did not work, because Anda was programmed to perform certificate pinning, that is, being limited to a pre-defined certificate and not accepting others. One solution for this is to change the Anda app so that it allows a different certificate to be used. However, sometimes tweaking with the application code to bypass certificate pinning is not enough, if other protection mechanisms are implemented.
Instead of trying to obtain readable access to the network packets sent by Anda, I decided to decompile the .apk file and analyze the Java code to understand how it was communicating with the server API.
The application code was slightly obfuscated. Some of the methods were not implemented in Java but imported as a compiled native library with Java Native Interface (JNI). These methods were:
-
loadNative() - initialization of the JNI environment.
-
verifyBeacon(byte[] b1, byte[] b2, int n) - did not investigate what this method does, probably related to BLE beacons.
-
getServerInfo() - returns an array with 3 hard-coded strings that were obfuscated in the compiled library: the base URL of the API, a username and a password.
-
getPersonalEncryptionkey() - returns a key based on the ANDROID_ID of the device.
In order to better understand the output of each of these functions, I analyzed the decompiled code of Anda and created a small Android application calling them in a similar manner and logging their output. This was the code of my app:
System.loadLibrary("native-lib");
boolean resultA = initializeNative();
Log.d("ANDA-DEBUG", resultA ? "true" : "false");
boolean resultB = verifyBeacon("".getBytes(), "".getBytes(), 1);
Log.d("ANDA-DEBUG", resultB ? "true" : "false");
String[] resultC = getServerInfo();
for (String s : resultC) {
Log.d("ANDA-DEBUG", s + "");
}
String resultD = getPersonalEncryptionPass();
Log.d("ANDA-DEBUG", resultD + "");
This attempt was not successful, as the first two functions returned "false" and the other two returned null values. Something was blocking my app from properly using the methods defined in the native library file.
Disassembling
So I opened a disassembler and searched for what was causing this behavior. I noticed that all 4 functions made a call to another function named "isInitialized", immediately branching when false to a return statement with a zeroed return value. I figured this would most likely be the cause of the "false" and "null" outputs (zero, false and null are often represented the same way in machine code). Therefore, I had to understand why the "loadNative" method was not correctly initializing the JNI environment.
Looking at the initialization instructions, I found that it was getting the signature of my application and comparing it with a pre-defined value to detect tampering. If the signatures did not match, then the program would branch to the end and return 0. This was done with the CBZ instruction (Compare and Branch on Zero).
BLX __aeabi_memclr8
ADD R6, SP, #0xF0+var_C4
MOV R0, R6
BLX j_sha256_init
MOV R0, R6
MOV R1, R8
MOV R2, R5
BLX j_sha256_update
MOV R0, R6
MOV R1, R10
BLX j_sha256_final
LDR R1, =(unk_13B13 - 0x3AF4)
MOV R0, R10
MOVS R2, #0x20
ADD R1, PC
BLX memcmp (1)
CBZ R0, loc_3B24
-
'memcmp': function of the C library that compares two regions of memory of the same size and returns 0 if they are equal
In order to make my app bypass this verification, I patched the binary code so that the CBZ instruction was replaced by a CBNZ (Compare and Branch on Non-Zero). This successfuly allowed me to run the 4 library methods.
In particular, I had all the information necessary to make API calls: the URL and authentication information, obtained from the getServerInfo()
function.
Accessing the API
Turns out that the credentials used for API calls were the same for all users, and hardcoded in the native library I had just decompiled. Therefore, using these credentials, it was possible for me to make any request to the API with unlimited permissions. For example, if I requested the information of a customer, I would get an output similar to the following:
{
"CustomerAccount": {
"ID": "78134aa1-5aff-4fb1-80dd-2a9ae1cf7200",
"Name": "Gustavo Silva",
"Address": "",
"City": "Maia",
"Email": "silva95gustavo@example.com",
"Password": "my-ultra-unsafe-password",
"PostalCode": "",
"FiscalNumber": 0,
"IdentificationNumber": "",
"TIPCode": "0",
"Phone": "+351987654321",
"BillingInformationDetails": {
"ID": "1ef88e57-c75b-4fe1-b12c-bcadc1808a00",
"Name": "Gustavo Rocha da Silva",
"Address": "R. Dr. Augusto Martins 101",
"City": "Maia",
"PostalCode": "1234-567",
"FiscalNumber": 123456789,
"CountryDetails": {
"Id": "30b5fbdd-5044-4219-9ba3-fcfaf716d83d",
"Code": "PT",
"Name": "Portugal",
"InvoicerCode": "PORTUGAL_PT"
}
},
"CustomerPaymentMethod": {
"Name": "Cartão de crédito/débito",
"RefCode": "XXXXX",
"ThumbnailUrl": "https://example.com/images/thumbnail.png",
"InfoToCustomer": "Gustavo Rocha da Silva\r\n**** **** **** 1234\r\n10/2018"
}
}
}
Aftermath
By that time (less than a week since the public release) the app already had over 10 000 installs and it was possible to:
-
View personal data from any user, including name, home address, last 4 digits of the credit card, phone number and fiscal number
-
Read any user’s password in plain-text
-
Perform other actions allowed by the API (I did not investigate further)
The vulnerability was assigned the identifier CVE-2018-13342.
Following a responsible disclosure model, I reported the vulnerability to the developers of the application. Timeline of events:
-
29 June 2018 - Public release of Anda
-
4 July 2018 - Private vulnerability disclosure to vendor
-
7 July 2018 - Vendor acknowledgement
-
~11 July 2018 - Passwords no longer stored in plain-text
-
22 August 2018 - Communication of the vulnerability details to CERT.PT
-
9 September 2018 - New app version released using a safer API
-
23 October 2018 - Shutdown of the old vulnerable API