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.

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?

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

NotJohnScott wrote: Great and fun read :)

You should contact also John Scott ;-)
09 Dec 2019 11:43 UTC

user_x wrote: Awesome stuff 15 Mar 2020 11:04 UTC

cwa wrote: Or you know you could just take a picture of your Costco card. 17 Jul 2020 21:30 UTC

mrb wrote: cwa: Costco doesn't accept pictures of physical cards. 14 Oct 2020 06:04 UTC

Feetsdr wrote: Sorry, I’m a little late to the conversation here.

I was in my iOS app and remembered trying to use a screen capture of the membership card soon after installing the app (and had already seen the QR code change so I knew it was time based (I know a little, certainly not enough to make the webpage you did)

Here in Nj we have self checkouts along with manned checkouts at some warehouses (how common is that where you are?).

So I scanned the picture from a day or so earlier. Rather than just balking that it was invalid, even the employee working the self checkouts couldn’t reset it. A manager type had to come over. They know me at that warehouse (we’re in there 4-5 x a week sometimes?!)

So it was no big deal. But interesting to see behind the curtain of security for a company.

All that said, as cwa said - I was thinking an easier solution would be to take a pic of the membership card bar code. But yeah, that would only work at self checkout where you scan your membership yourself ; )

And all that said, sometime now (w COVID?), there 2 -3 workers in the self checkout area of 6 registers. They will scan my card // app and the couple / few items I have ( I make a point to put things in cart w upc face up and only 1 layer in anticipation of that at register) / help them.

Side note - another glimpse of security at Costco: Like membership, Rx and food court, self checkout uses a different color register tape. So people at door know to look a little closer at that huge cart of stuff you checked out yourself!!!

But yeah, relying on picture of card would fail w them helping at self checkout. (which I think they do just to move things along, not so much security).

Marc- thanks for the look behind the curtain!!

And do you think you could / will do the same web page for iOS? Or Apple locks things up better?

Could I transfer the app to an android, get the android I’d then transfer app back to iOS? No, right? since the android id is used to create the QR code. When transferred back To Apple, the seed Apple ID is different / wrong QR code will be created : (
23 Nov 2020 14:34 UTC

cwa wrote: Looks like Costco made some improvements. Once I was logged into their app, viewing my membership card was lightening fast. 06 Dec 2020 07:35 UTC

Rewinder wrote: Do you know what bar code format Costco uses for the membership number that is on the back of the physical card? Is it type 39 or interleaved one of two or...? 09 Nov 2021 10:18 UTC

matt wrote: Does this still work?? I've never set up an APK before and don't want to try it unless it still works 11 Oct 2022 14:36 UTC

mrb wrote: Yes, as of today with v6.7.0 of the Costco app, the hack still works. I updated my blog post with a slightly changed patch (see second paragraph). 12 Oct 2022 06:15 UTC

notAndrew wrote: Can you make a iOS version? 05 Jan 2023 00:47 UTC