Freyta's Little Notebook

+---------+
| H O M E |
+---------+

Introduction:

The 7-Eleven Fuel app is an app that lets you "lock" in a fuel price while the price is still low at the end of the fuel cycle (usually 2 weeks or so). What this means is that even if your local petrol station costs $1.50 a litre, you can fill your car up at any 7-Eleven service station in Australia for the price that you have "locked" into the app.

Normally you can only lock in the cheapest price of your five closest 7-Elevens fuel stations, but this is not always ideal because a petrol station on the other side of town or even in another state may be cheaper than your closest station, so you could potentially save even more money.

7-Eleven added a mock location check to the app so you can't simply load up a fake GPS, set a location and then lock in. I originally figured out how to get around this which you can read about here.

So sit down, buckle up because this is a break down on how I reverse engineered the app allow you to check fuel prices and lock them in using Python.

The process:

The first thing I did was setup Fiddler2 so I could capture the network data. Unfortunately 7-Eleven uses SSL certificate pinning which means that if you try and do any sort of HTTP data request (whether that is logging in, searching for your closest store or even trying to lock in a fuel price) you get an error response from the app saying "An unknown error occured. Please try again later".

My next idea was to load up my trusty Java decompiler (JADX) and open up the 7-Eleven Fuel 1.5.0 app, which 7-Eleven for my sake thankfully forgot to obscure. A search for "https://" revealed the following

public static final String API_BASE_URL = "https://711-goodcall.api.tigerspike.com/api/";

Simply changing the URL from https to http was enough to view the network data.
note: I have since learned that you can remove some of the SSL Certificate Pinning following this guide.

After recompiling the APK I got to work. The first thing I noticed after trying to view the price of a few different stores was that there was a custom Authorization header. The header is split into either 5 or 6 parts, depending on whether you are sending a POST request (i.e. logging in) or a GET request (i.e. checking a fuel price).

Opening up JADX and searching for "tssa" led me to a file called AuthHeaderInfo. What this class does is set the custom header needed to make any valid request to the 7-Eleven server. For a basic GET request there are only 5 variables needed, first is the deobfuscated APP_ID, the method we are requesting (GET) in uppercase, the URL, a timestamp and an UUID. If you are making a POST request you need to change the requesting method to POST and you need one more variable, and that is the JSON payload you are sending. All of these variables are joined together, then HMAC265 encrypted with key2 being the special key.

The first thing we need to do is figure out what the deobfuscated APP_ID is. The first part of the getKey function is hexdigesting the SHA1 encoding of the OBFUSCATION_KEY (om.sevenel) and then splitting it into an array which results in

def getKey(encryptedKey):
  hex_string = hashlib.sha1("om.sevenel").hexdigest()
  hex_array = [hex_string[i:i+2] for i in range(0,len(hex_string),2)]
  # Result is ['10', 'e0', '30', 'b2', '48', '0a', '17', 'a8', '11', 'ec', '1d', '4e', '69', '68', '45', 'a9', 'a4', 'c2', 'd5', 'd3']

The next part is getting a character from the input (either obfuscated APP_ID for key or API_ID for key2) which in Python is

    length = i%(len(hex_array))
    key += chr( int(hex_array[length], 16) ^ int ( encryptedKey[i] ))

Our complete getKey function should now look like this:

def getKey(encryptedKey):
  hex_string = hashlib.sha1("om.sevenel").hexdigest()
  hex_array = [hex_string[i:i+2] for i in range(0,len(hex_string),2)]

  key = ""
  i = 0
  while(i < len(encryptedKey)):
    length = i%(len(hex_array))
    key += chr( int(hex_array[length], 16) ^ int ( encryptedKey[i] ))

    i = i + 1
  return key

The start of our script should look like this:

import hashlib, hmac, base64, time, uuid, urllib, requests, json, random

# key is the OBFUSCATED_APP_ID
key       = getKey([36, 132, 5, 129, 42, 105, 114, 152, 34, 137, 126, 125, 93, 11, 117, 200, 157, 243, 228, 226, 40, 210, 84, 134, 43, 56, 37, 144, 116, 137, 43, 45])
# key2 is the OBFUSCATED_API_ID
key2      = getKey([81, 217, 3, 192, 45, 88, 67, 253, 91, 164, 110, 13, 28, 57, 22, 225, 246, 233, 153, 224, 87, 152, 65, 253, 2, 115, 83, 197, 64, 156, 94, 41, 25, 27, 116, 153, 150, 161, 188, 166, 113, 130, 83, 143])
# storeid is the number of the store you want to get the fuel price from.
storeid = "1023"
# URL needs to be a https link, whereas replace needs to be a http link.
url       = "https://711-goodcall.api.tigerspike.com/api/v1/FuelPrice/FuelPriceForStore/" + storeid
replace   = url.replace("https", "http").lower()
# The final 2 variables needed for str3
timeNow = int(time.time())
uuidVar   = str(uuid.uuid4())

Next thing we need to do is encrypt str3. Looking at the original code it says that str3 is formated as follows

[key][HTTP method (GET or POST)][replace][timestamp][UUID][str3]

Str3 at the end of the above string is the encrypted JSON data when you are sending a POST request, if it is a GET request then str3 is empty. Since at this we are not currently sending any data str3 will be empty. Str3 in the tssa headers is HMAC256 encrypted with key2 as the secret and the above str3 as the message. To implement this in Python we first need to create the HMAC and then base64 encrypt it. Our resulting code is as follows:


# Join the key, the HTTP method, the url, timestamp and the UUID
str3 = key + "GET" + replace + str(timestamp) + uuidVar

# Create a base64 encoded HMAC256 with str3 as the message and key2 and the secret
signature = base64.b64encode(hmac.new(key2, str3, digestmod=hashlib.sha256).digest())

# What our final tssa header looks like
tssa = "tssa " + key + ":" + str(signature) + ":" + uuidVar + ":" + str(timestamp)

Looking back at our Fiddler2 request we can see that there are now only four other custom headers we need to implement. They are X-OsVersion, X-OsName, X-DeviceID and X-AppVersion. You can directly copy these valued from Fiddler2 and paste them into our Python script as a custom header as well. It doesn't seem to make a difference what the values are, as long as you have included them.


# Generate a random 15 character deviceID
# Using a random deviceID is useful because we are only allowed 150 requests for a fuel price per day.
deviceID = ''.join(random.choice('0123456789abcdef') for i in range(15))

# The headers we will need to send.
headers = {'Authorization': tssa,
           'X-OsVersion':'Android 8.1.0',
           'X-OsName':'Android',
           'X-DeviceID':deviceID,
           'X-AppVersion':'1.6.0.1967'}


response = requests.get(url, headers=headers)

So our complete code to check a fuel price will look like this


import hashlib, hmac, base64, time, uuid, urllib, requests, json, random

def getKey(encryptedKey):
  # get the hex from the encrypted secret key and then split every 2nd character into an array row
  hex_string = hashlib.sha1("om.sevenel").hexdigest()
  hex_array = [hex_string[i:i+2] for i in range(0,len(hex_string),2)]

  # Key is the returned key
  key = ""
  i = 0

  # Get the unobfuscated key
  while(i < len(encryptedKey)):
    length = i%(len(hex_array))
    key += chr( int(hex_array[length], 16) ^ int ( encryptedKey[i] ))

    i = i + 1
  return key

# key is the OBFUSCATED_APP_ID
key       = getKey([36, 132, 5, 129, 42, 105, 114, 152, 34, 137, 126, 125, 93, 11, 117, 200, 157, 243, 228, 226, 40, 210, 84, 134, 43, 56, 37, 144, 116, 137, 43, 45])
# key2 is the OBFUSCATED_API_ID
key2      = base64.b64decode(getKey([81, 217, 3, 192, 45, 88, 67, 253, 91, 164, 110, 13, 28, 57, 22, 225, 246, 233, 153, 224, 87, 152, 65, 253, 2, 115, 83, 197, 64, 156, 94, 41, 25, 27, 116, 153, 150, 161, 188, 166, 113, 130, 83, 143]))
# storeid is the number of the store you want to get the fuel price from.
storeid = "1023"
# URL needs to be a https link, whereas replace needs to be a http link.
url       = "https://711-goodcall.api.tigerspike.com/api/v1/FuelPrice/FuelPriceForStore/" + storeid

replace   = url.replace("https", "http").lower()
# The final 2 variables needed for str3
timeNow = int(time.time())
uuidVar   = str(uuid.uuid4())
str3      = key + "GET" + replace + str(timeNow) + uuidVar

# message = str3 , secret = key2
signature = base64.b64encode(hmac.new(key2, str3, digestmod=hashlib.sha256).digest())

tssa = "tssa " + key + ":" + str(signature) + ":" + uuidVar + ":" + str(timeNow)


# Generate a random 15 character deviceID
# Using a random deviceID is useful because we are only allowed 150 requests for a fuel price per day.
deviceID = ''.join(random.choice('0123456789abcdef') for i in range(15))

headers = {'Authorization': tssa,
           'X-OsVersion':'Android 8.1.0',
           'X-OsName':'Android',
           'X-DeviceID':deviceID,
           'X-AppVersion':'1.6.0.1967'}

# Send our request with our custom headers
response = requests.get(url, headers=headers)

print response.content

+-------+
| E N D |
+-------+