mrb's blog

Reverse Engineering and Cloning the Costco Digital Membership Card

Keywords: android hack

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.

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:

Costco digital membership card

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:

diff -Nur com.costco.app.android_4.2.1.orig/smali/com/costco/app/android/digitalmembership/MembershipCardActivity.smali com.costco.app.android_4.2.1/smali/com/costco/app/android/digitalmembership/MembershipCardActivity.smali
--- com.costco.app.android_4.2.1.orig/smali/com/costco/app/android/digitalmembership/MembershipCardActivity.smali       2019-10-07 21:06:54.186134497 -0700
+++ com.costco.app.android_4.2.1/smali/com/costco/app/android/digitalmembership/MembershipCardActivity.smali    2019-10-06 15:57:21.298978426 -0700
@@ -258,7 +258,7 @@
 
     move-result-object p1
 
-    const/16 v0, 0x2000
+    const/16 v0, 0x0000
 
     invoke-virtual {p1, v0, v0}, Landroid/view/Window;->setFlags(II)V
 

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:

Screenshot of Costco digital membership card

(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():

private void setMembershipQRCode() {
    if (this.card != null && getContext() != null) {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(APPLICATION_IDENTIFIER);
        stringBuilder.append(SUB_TYPE);
        int length = this.card.getMemberCardNumber().length();
        if (length <= 13) {
            while (length < 13) {
                stringBuilder.append("0");
                length++;
            }
        }
        if (!StringUtils.isNullOrEmpty(this.card.getMemberCardNumber())) {
            stringBuilder.append(this.card.getMemberCardNumber());
            stringBuilder.append(MembershipCardUtils.generateDynamicToken(getContext()));
            this.qrCodeImage.setImageBitmap(MembershipCardUtils.encodeAsBitmap(
              getContext(), stringBuilder.toString()));
        }
    }
}

And MembershipCardUtils.generateDynamicToken() generates the last digits of the QR code:

public static String generateDynamicToken(Context context) {
    MessageDigest instance;
    String deviceId = getDeviceId(context);
    long currentTimeMillis = System.currentTimeMillis() / 300000;
    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.append(deviceId);
    stringBuilder.append(SALT);
    stringBuilder.append(currentTimeMillis);
    deviceId = stringBuilder.toString();
    try {
	instance = MessageDigest.getInstance("SHA-256");
    } catch (NoSuchAlgorithmException e) {
	e.printStackTrace();
	instance = null;
    }
    deviceId = Integer.toString(Integer.parseInt(bytesToHex(instance.digest(
        deviceId.getBytes(StandardCharsets.UTF_8))).substring(0, 6), 16));
    StringBuilder stringBuilder2 = new StringBuilder();
    stringBuilder2.append(RESERVED_FOR_FUTURE);
    stringBuilder2.append(DIGITAL_TOKEN_VERSION);
    while (deviceId.length() < TOKEN_LENGTH) {
	StringBuilder stringBuilder3 = new StringBuilder();
	stringBuilder3.append(ApiErrorCode.UNKNOWN);
	stringBuilder3.append(deviceId);
	deviceId = stringBuilder3.toString();
    }
    stringBuilder2.append(deviceId);
    return stringBuilder2.toString();
}

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:

  1. ANDROID_ID value
  2. literal string "SCOTTJOHN"
  3. 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():

diff -Nur com.costco.app.android_4.2.1.orig/smali/com/costco/app/android/digitalmembership/MembershipCardUtils.smali com.costco.app.android_4.2.1/smali/com/costco/app/android/digitalmembership/MembershipCardUtils.smali
--- com.costco.app.android_4.2.1.orig/smali/com/costco/app/android/digitalmembership/MembershipCardUtils.smali  2019-10-07 21:06:54.190134496 -0700
+++ com.costco.app.android_4.2.1/smali/com/costco/app/android/digitalmembership/MembershipCardUtils.smali       2019-10-07 20:42:26.474302400 -0700
@@ -485,7 +485,7 @@
 .end method
 
 .method public static getDeviceId(Landroid/content/Context;)Ljava/lang/String;
-    .locals 1
+    .locals 3
     .annotation build Landroid/annotation/SuppressLint;
         value = {
             "HardwareIds"
@@ -503,6 +503,11 @@
 
     move-result-object p0
 
+    sget-object v1, Ljava/lang/System;->out:Ljava/io/PrintStream;
+    const-string v2, "android_id="
+    invoke-virtual {v1, v2}, Ljava/io/PrintStream;->print(Ljava/lang/String;)V
+    invoke-virtual {v1, p0}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
+
     return-object p0
 .end method
 

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:

  1. Download the Costco app APK, either from your phone using adb pull, or from an APK archive site
  2. Decode the APK with apktool, apply my two patches, and rebuild it
  3. Install the app on a phone, launch it, take a screenshot of the digital membership card
  4. Run adb logcat and write down ANDROID_ID
  5. Save my card generator and host it somewhere (1 HTML, 2 JS files, and 1 template screenshot)
  6. Replace my template screenshot with your screenshot (crop it to remove the Android status bar)
  7. 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
  8. 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.

Comments