In this post I will reverse engineer the Costco Android app to find out how the digital membership card works, and how to generate the time-based token encoded in its QR code. I reimplemented it in plain HTML/JS. As a result the card can be cloned to any number of devices, bypassing the app’s security mechanism that attempted to tie the card to only one device.
2022-10-11 Three years later, this hack still works, only with minor changes. The class that fetches ANDROID_ID is now MembershipCardUtilImpl. I used apktool to decode the Costco Android app APK file version 6.7.0; I applied this new patch (just make sure to edit ANDROID_ID to a unique random value of your choosing); I rebuilt the APK and installed it. The app still generates the same QR code as my card generator. I find it easier to simply hardcode the ANDROID_ID, as this avoid the need to then log it after installing the app.
- Why?
- Costco goes digital
- Reverse engineering the app
- Custom card generator
- Final thoughts
- Disclosure timeline
Why?
I reverse engineered the app mainly out of curiosity; but also for efficiency: my reimplementation of the card loads instantly compared to their app, also I can factory reset or replace my phone and the card will continue to work whereas the Costco app will force you to “transfer” it to a new device (and it allows only one transfer, afterwards customer service must be contacted.)
In the end this project was trivial and serves as a nice introduction to Android reverse engineering.
But first, a story:
In 2004 shortly after I moved to the US, I was driving around my neighborhood and saw a big-box store that I decided to visit. It was a Costco and I did not know it was a membership-only store. I managed to get in through sheer luck. I did not notice shoppers were required to flash their membership cards at the greeter at the entrance. In fact I barely noticed her, as she was standing discretely at the side of the oversized entrance. She did not stop me.
So I got in, shopped for a bit, and when it was my turn to pay at the checkout counter the clerk asked for my card.
«My what?»
«Sir, you don’t have a membership card?»
Fast forward 15 years later, I love Costco. I take my family there to shop for items in bulk that I probably do not need in bulk, but it is just so convenient. However I am always looking to carry fewer things in my wallet, and Costco has been annoying in that regard, as one must carry and present this stinking plastic magnetic stripe membership card.
Costco goes digital
Two months ago they finally introduced a digital membership card on their Costco smartphone app:
So I install their Android app (version: 4.2.1, APK SHA256: 29b10840e299b31213b89788554da0038901ef878e2a076e45b221a65fc4f222.) My first impression is that it is very heavy given how little it does: 68 MB and it is mostly a WebView shim of their online store. It is slow to load and takes ~6 seconds just to get to the digital membership card view which is showing:
- Membership type icon (standard, executive…)
- First and last name
- Membership number
- Membership start and expiration
- Photo
- QR code
Let’s try to take a screenshot… it does not work. The app does not allow it as it sets FLAG_SECURE on this window.
Reverse engineering the app
I grab the APK file, run apktool decode
to unpack and disassemble
it, and recursively grep for setFlags
to look for a call passing the value
FLAG_SECURE (0x2000).
The app is not obfuscated so it is easy to find the relevant
setFlags
call that needs to be modified. It is in the file MembershipCardActivity.smali
,
method onCreate()
. Just zero out the flags to disable FLAG_SECURE:
I rebuild the APK with apktool build
, sign it, zipalign it. I uninstall Costco’s
official APK, install my patched APK, and launch the app.
It asks me if I want to “transfer” my digital membership card to this new device, hmm… My rebuild caused that? (I will find out later the technical reason why this happened.) Anyway, I allow it, open the digital card, and taking a screenshot is now possible:
(All my data in this screenshot has been altered for privacy. My real Costco photo is a lot worse ☺)
The QR code contains a numeric value where MMM… is the membership number:
96000MMMMMMMMMMMMM000114362136
So is cloning the digital membership card as easy as taking a screenshot? Nope because the last 8 digits of the QR code value appear to change every once in a while.
Let’s investigate how the app builds the QR code.
I fire up the jadx decompiler as it produces Java code more readable
than smali code. All the relevant code
seems to be under com.costco.app.android.digitalmembership
;
in particular MembershipCardFragment.setMembershipQRCode()
:
And MembershipCardUtils.generateDynamicToken()
generates the last digits
of the QR code:
A device identifier is obtained from getDeviceId()
which simply returns
the Android platform identifier ANDROID_ID.
SALT
is the literal string "SCOTTJOHN"
(if you are John Scott and wrote this code, let’s have a beer one day!)
From the code above it is obvious how the QR code numeric value is generated. There are 4 constant fields and 2 variable fields:
96000MMMMMMMMMMMMM0001TTTTTTTT
96 ........................... APPLICATION_IDENTIFIER
000 ........................ SUB_TYPE
MMMMMMMMMMMMM ........... membership number padded to 13 digits
00 ......... RESERVED_FOR_FUTURE
01 ....... DIGITAL_TOKEN_VERSION
TTTTTTTT dynamic token padedd to 8 digits
The dynamic token is generated by computing the SHA-256 hash of the concatenation of 3 strings:
- ANDROID_ID value
- literal string
"SCOTTJOHN"
- decimal integer representation of
System.currentTimeMillis() / 300000
The hexadecimal hash is truncated to 6 hex digits, converted to decimal, and padded to 8 digits. The dynamic token is in essence a time-based 24-bit token with a granularity of 300 seconds or 5 minutes.
Using ANDROID_ID to calculate the token serves as a primitive security mechanism to tie the card to only one device.
Now this explains why the app asked me earlier if I wanted to “transfer” the card to my recompiled APK. The Android platform, since Android 8.0, generates ANDROID_ID values unique to each combination of app-signing key, user, and device. The patched APK is signed with my key, different from the key of the official APK, so the two APKs see different values. When Costco’s server-side infrastructure notices an ANDROID_ID reported by the app that is different from what it expects, it prompts to transfer the card to “the new device.”
In order to generate the token myself, I need to know ANDROID_ID. So I
patch the method getDeviceId()
to log the value (stored in register p0)
using System.out.println()
:
I build and push the APK. Now when launching the app adb logcat
reveals ANDROID_ID:
10-07 21:16:41.414 26112 26112 I System.out: android_id=3d2axxxxxxxxxxxx
Custom card generator
Armed with my ANDROID_ID value, and a description of the time-based token algorithm, I was able to reimplement a digital membership card generator in plain HTML/JS:
Costco digital membership card generator
To use the generator:
- Download the Costco app APK, either from your phone using
adb pull
, or from an APK archive site - Decode the APK with apktool, apply my two patches, and rebuild it
- Install the app on a phone, launch it, take a screenshot of the digital membership card
- Run
adb logcat
and write down ANDROID_ID - Save my card generator and host it somewhere (1 HTML, 2 JS files, and 1 template screenshot)
- Replace my template screenshot with your screenshot (crop it to remove the Android status bar)
- Open the HTML file, type your ANDROID_ID and membership when prompted (they will be saved in the URL fragment,) and a real-looking card with a valid QR code is displayed
- Tip: scroll down by a few pixels to hide the Chrome address bar; or use “Add to Home screen” and the page will load in fullscreen mode
An implementation detail to be aware of: my QR code will always decode to the exact same numerical value as the app’s QR code, but may look different. This is because QR codes can use 8 mask patterns for data encoding. A QR implementation is supposed to pick the best mask pattern based a certain logic, such as minimizing the number of consecutive same-color pixels. My QR library and the app’s library simply have slightly differing logic in that regard. But for the sole purpose of visually confirming whether the QR codes encode the same data, I added a feature so that clicking the QR code cycles through the 8 possible mask patterns.
Final thoughts
The proper way to implement a digital membership card tied to a single device would have been to leverage Android’s hardware-backed keystore with key attestation, and to put the asymmetric cryptographic signature of a timestamp in the QR code.
So what happened 15 years ago when I did not have a membership card? The clerk called a security guard to escort me out. What an embarassing moment.
Nowadays I present my phone to the clerk and when he scans the QR code he does not even realize he is scanning my card generator.
Disclosure timeline
I chose full disclosure given the nature of the vulnerability. I am also reaching out to Costco to advise them on remediation.
2019-10-09 I send an email to the Costco app developer contact listed on the Play Store (webservice@contactcostco.com).
2019-10-10 I receive an automated reply “we will no longer be responding to requests via email.”
2019-10-10 Finding no security contact information whatsoever, I tweet @Costco. I also tentatively send a message to Director of IT Security Andrew Tuck through LinkedIn and to a handful of guesses of what might be his corporate email address. I call the corporate headquarters and leave a voicemail to Tuck.