RE: Reverse Engineering McDonalds App

2019/01/18

Title Image

I was made aware by a friend that a blog post written by Jacopo Jannone, which went into detail on their efforts in reverse engineering the Italian McDonalds mobile application. As someone who’s has gone and reverse engineered a few android applications myself, including the Australian MyMaccas application, I felt it would be interesting to do the same. I only bothered with this “project” as I wanted cheaper food that was next to our university campus, without having to deal with the the garbage android app. Therefore, I have only documented anything to do with the offers part of the application, as these do not require transactions made from the phone.

MyMaccas, a marvel of mistakes

It’s a hot piece of garbage so they say. The playstore suggests this and is full of horror stories.
Many of the main problems I hear about it are it double charging customers for orders, the app crashing or logging people out, and it’s pretty horrendous to both use and look at.

break this link *Canadian app has very minor differences from the Australian version

In order to redeem a deal on the application, you supposedly need to have you GPS enabled. This is so that the system can determine any deals that would be available to a limited area. Additionally, according to comments on OzBargain, some users also appear to have different prices given to their deals, saving sometimes fifty cents or a dollar. Deals generally last from a range from a single day, or around a week or two (depending in their promotions), as well as a “Buy five coffees and get one free” deal that resets every month. There is also a special bonus deal for new accounts that is supposedly only available for the first week an account is active, I have found that this lasts around three months instead, I’m not sure why.

When deals are redeemed on the app, two options are presented on how the user wants to apply it. This is either by adding it to a mobile order, or as a redeemable code which is also displayed as an Aztec Barcode. Codes can be redeemed both over self-service kiosks or regularly over the counter. A minor problem with this I have found is a massive reoccurring problem where offers that are input on a self-service machine break the checkout system, you will be unable to pass the menu where you pick “Eat In” or “Take Away”, as pressing any of these do not proceed to the payment screen. As codes are made invalidated immediately upon use, you have to generate another code from the app, as you can generate as many codes as you can until you make a payment.

Pwnd

Packet Capture on android is a mixed bag in my experience, in many cases it can be due to my own user error, but also SSL certificates that are being verified. Luckily for me though, the MyMaccas application did not seem to verify their certificates. I used a free application from the play store called Packet Capture, which I have had on my phone for quite a while. What was very helpful with this application is that it also allowed saving dumps to a file readable with Wireshark. This is able to give us some basic commonly used API endpoints, being able to see authentication methods, POST form data and anything else that is required for any actions I would make on the phone.

With subsequent requests, I was easily able to determine what a regular request looked like. I have to take liberty in some assumptions that are related to any system that may be outside of production use though. All requests that are made to an API endpoint use X-NewRelic-ID and mcd_apikey header and MarketId items, none of these charge for any given reason. And upon login, authentication is based on a Token header, that is stored within application storage upon login and sent alongside any request where authentication is required. On top of this, sent JSON objects also contain consistent information just as marketId, application, languageName, platform.

A lot of this information is region specific. Therefore, I have decompiled the appropriate information using JADX. It’s pretty disgusting how this is stored, I’m not going to convert this, but configurations for each region are stored in a <String, String> hashmap, with the value being stringified JSON. These a lot of configurations that apply to each region, such as what features are available, but most importantly the regional API Keys.

Alongside this, login requests are sent with a hash item within their JSON data. This is usually used to make sure all data within the request match that of the request. Usually, but not here, for some archaic reason, this has not been a part of the implementation within this app. MyMaccas uses RDI software libraries for their crypto, of course this just more Java and easily decompiled.

public final String computeHash(String applicationId, String applicationVersion, String applicationNonce) {
	return base64StringEncode(md5("=PA64E47237FC34714AF852B795DAF8DEC\\o/" + applicationVersion + "o|o" + applicationId + "=/" + applicationNonce));
}

and in Python3

def computeHash(Id, Version, Nonce):
	return b64encode(md5("=PA64E47237FC34714AF852B795DAF8DEC\\o/{0}o|o{1}=/{2}".format(Version, Id, Nonce).encode('utf-8')).hexdigest().encode('utf-8')).decode('utf-8')

the interesting thing about the MyMaccas application is that the applicationId, applicationVersion, and applicationNonce are all hardcoded

public void generateHash() {
	String nonce = "happybaby";
	String versionId = (String) Configuration.getSharedInstance().getValueForKey(this.mConfigBasePath + ".versionId");
	String hash = RdiSecurity.computeHash((String) get("application"), versionId, "happybaby");
	put("versionId", versionId);
	put("nonce", "happybaby");
	put("hash", hash);
}

Therefore, a hash will always have the value of ODUwNjEzMmY3MGRkM2ZlMTQzMjE0YmJlYzllNWNjZjI= 🤦‍♀️

Logging in

Generating an authentication token was not a tedious task, it just turns out that the same endpoint is used for both changing a password and logging in.

Request URI Path:
	/v3/customer/session/sign-in-and-authenticate
JavaScript Object Notation: application/json
- member Key: marketId 
	String value: 
- member Key: application 
	String value: MOT
- member Key: languageName 
	String value: en-AU
- member Key: platform 
	String value: android 
- member Key: versionld 
	String value: 0.0.1.I 
- member Key: nonce 
	String value: happybaby 
- member Key: hash 
	String value: ODIJwNjEzmmY3hGRkm2Z1mTQzmjEßYmJIYz11WNjZj1= 
- member Key: userName 
	String value: dobdruo@nbox.notif.me 
- member Key: password 
	String value: hackme 
- member Key: newPassword 
	Null value 

Getting Offers

This was the tricky part of understanding how the offer system works. When using the mobile application, they have a requirement of enabling the GPS permission for offers. Later I found out this is due to offers being implemented based on the individual store, and the store lookup system requires a latitude and longitude reference for stores. To get an active list of offers though, another API call is made as a GET request to the server. This for some reason also requires GPS co-ordinates as a parameter.

Request URI Path: 
	/v3/customer/offer
Request URI Query: 
- Parameter Value: marketId
	AU 
- Parameter Value: application
	MOT
- Parameter Value: languageName
	en-AU
- Parameter Value: platform
	android
- Parameter Value: latitude
	##Redacted##
- Parameter Value: longitude
	##Redacted##
- Parameter Value: storeId
	[##Redacted##] 

This returns quite a lot of data, and I’m not going to go over the minute details of this one. The only data that really matters here is the offerId of each offer. Redeeming an offer is a POST request, which also now requires you to put your email in the JSON object for some reason, it makes no sense to me.

At this point, we can then send a redemption request to the server Wireshark window showing the information of a redemption request

This returns a response containing the letter code, and a Base64 encoded image of the Aztec Barcode.

Returned JSON object showing an encoded base64 barcode and response code *This code did not work, I had to call a friend over Slack to generate a new one

That’s not all

So the plan from the start was all about programming easy to access offers. If a code can only be used once, then there’s not going to be a lot of savings. So what about automating account creation? As it turns out, the MyMaccas system does not do a 2-Step email verification system. As in, an account does not need to be verified over email. This also applies to SMS, which is only a required field when making an account from the mobile app. I gave it only a little bit of an attempt, as I didn’t want to litter the account system with real phone numbers, and the guaranteed safe numbers for our country didn’t seem to work. Toward the end of the student semester, while chowing down on a cheap meal with a friends from university, we noticed that the restaurants Wi-Fi uses the same MyMaccas accounts to login to the service. Immediately we used this opportunity for more research and attempted to make another account on the system. When using a known temporary email service, it straight up failed but we took a look at the browsers network log anyway. We noticed a strange request to a separate entity outside of of the domain, which appeared to be responsible for email verification.

Here’s that ajax request in it’s entirety

$.ajax({
	url: "https://api.experianmarketingservices.com/sync/queryresult/EmailValidate/1.0/",
	type: "POST",
	dataType: "json",
	crossDomain: "true",
	contentType: "application/json",
	headers: {
		"Auth-Token": exp_key
	},
	async: true,

	// There's other params
	// For more info, please see this ticket [MMABSM-167]
	data: JSON.stringify({
		"Email": key
	}),

	complete: function(response) {
		var is_valid;

		// Note: when the response isn't 200, we purposely set is_valid to true, we don't want our customer
		// stuck in the page, if our third party verification server down etc...
		if (response.status != '200') {
			is_valid = true;
			console.log('Error occurred when trying to verify email');
		} else {
			var response_json = response.responseJSON;

			// if the email isn't verified or unknown, display error
			// note: we allow unknown email because in some cases it's some company's security software that makes
			// us can't verify if the email is exist or not
			if (response_json.Certainty != 'verified' && response_json.Certainty != 'unknown') {
				validate.showErrors(element_obj);
				is_valid = false;
			} else {

				// otherwise, all good.
				is_valid = true;
			}
		}
		defer.resolve(is_valid);
	}
});

So well, that’s a lot of hot shite. Why the fuck is this on the client and not the server? Who got paid for this garbage? I have heard about services in the past that would attempt a login to the email server to check if an account exists. This is probably an API that does this for you. But any provider with good security has this protected, which makes sense as to why the developer has opted to allow validity on an invalid response. But as this is a client-side verification, it’s very easy to spoof. We overwrote the script and made all responses true. Low and behold the invalid email addresses now work, and you can login with them on the phone as well.

Now What?

I’ve been enjoying reduced prices on food close to uni. I might have over 3000 accounts on the system, but I still eat there on moderation, there’s a fantastic Japanese place in the Melbourne CBD that’s got more bang for your buck. I have attempted a few times at making a basic android app that just supports multiple accounts for redeeming offers, but I’m not that great at sticking to projects. Every time I require a code, I’d just SSH into a server where some hacky python code makes the requests. You can find an outdated version on my Github profile. I will probably update it one day, but it’s not a priority. Execution of a python script returning a valid offer code

One more thing…

Maccas are trying really hard to enforce precise GPS data. It’s is a near persistent message for you to enable it. I found a bypass for this, but it appeared to be patched out in the latest UI update. You’d be able to remove the GPS alert from the store map screen to manually enter a store location. Any required location data would then be based off the store’s location

One more thing……

Redeemed offers are not bound to any given store. The codes you get from the system will work on anywhere the offer is accepted. I find myself being asked for codes on a weekly basis. In the past I resorted to using a Post Code lookup system, but in theory, it’s not needed. Hardcoding GPS co-ordinates will also work for any re-implementation

One more thing………

Data Collection. Data Collection

P.S. Your app sucks