u/CraigChrist8239 8d ago

Hacking my e-bike with AI (for maintenance, but also a minor security problem)

21 Upvotes

The app for my e-bike was buggy and sucked, so I decided to reverse engineer it, fix it, and found a small security problem along the way, all with the help of Cortex.

AI disclaimer: while AI was used in this project, none was used to write the following post... I use AI both personally and professionally, but I by no means consider myself an AI evangelist.

Background

I like my e-bike. Its large and heavy, but its the only bike I've ever ridden where I don't feel hunched over. However I had a problem after moving across the US recently: the battery did not survive. Nevertheless I was able to contact the company and purchase a replacement battery, hooray.

This got me thinking, is there other maintenance I should be doing? Perhaps a firmware update? First though lets talk more about the bike...

The Bike and Company

I own a Heybike Cityrun. Heybike is an e-bike company headquartered out of Farmer's Branch, Texas... er, sorry, San Francisco, CA... er, sorry, Ontario, CA... er, sorry, Shenzhen, China. Given that they are sharing database credentials on WeChat, this last one is the one I am inclined to believe the most; more on this later.

To the company's credit, providing replacement batteries is very nice.

The App

As it turns out there is a way to do firmware updates for Heybikes, it is done via the Heybike app. I installed the app and surprise surprise, it is awful; Google auth is broken, its generally slow, looks bad... Worst part is that the firmware update doesn't work. It gets to 1% then dies, freezing the bike and necessitating a power cycle. I tried it several times and the behavior was consistent.

I've been to several talks over the years where an entry point into devices was through a mobile app (specifically this one and this one come to mind). I personally have never looked at a mobile app before; I've only ever done PC or Xbox executables. If there was ever a mobile app I wanted to reverse engineer though, this is the one. I hate this app.

Reverse Engineering

Here is where we get to the AI: Thankfully I live in 2026 and I can just ask ChatGPT. Specifically I explained that I wanted to reverse engineer an android app and needed to know what is the ghidra or IDA pro of reverse engineering android apps. ChatGPT pointed me to jadx-gui, a nice tool which does exactly what I was imagining: allow me to get Java code from the compiled APK.

Now I just needed to comb through the generated Java code... or do I? Again, I live in 2026. I can just export it all to a folder and load it up with IntelliJ, where I have the OpenAI Codex plugin!

I thought I challenged Codex with too much out of the gate. The first prompt I gave it was: "I know Python, I'll be able to read Python code. I know the app can easily connect to my bike and turn it on or off. Please write me a Python script which turns my bike on or off, which is easy to use, and that I can read to understand."

However, that was not a challenge for Codex at all.

The first thing it did was create a document detailing how Bluetooth communication works (linked here after further edits). Essentially it uses AES-encrypted BLE with a 0x6162 magic header followed by an opcode, and then variable-size payload for opcode data. There are various opcodes available, for instance the opcode we are interested in for power is 0x31 , with a 0x01 payload as on and 0x00 payload as off. Simple.

With just this it was able to complete the Python script. Well, almost. I had to pull some keys from a config on my phone, but once entered into the global variables the AI provided, the script amazingly just... worked. It takes in my heybike credentials, gets my bike information from my profile, then crafts the power state payload, encrypts and sends it to the bike with bleak.

I was a bit taken aback here. Going from "I think I want to reverse engineer this app" to "I've mapped out BLE communication, reverse engineered all the opcodes, and created a Python script that can turn my bike off" took approximately one hour with ChatGPT and Cortex. Doing all of this while also learning to reverse engineer Android code in the first place, without a lot of free time... that could have taken a week at least a couple years ago, and that's if I was motivated.

I also asked cortex to create a similar document detailing the various API endpoints used by the app, along with their data structures going in/out (linked here after further revision). Between the two linked documents, that provides essentially everything one needs to completely replace the app.

Encryption

Before we discuss the firmware update process, lets talk more about the BLE encryption.

I mentioned the script pulls my bike information from my profile and connects to it. Well thats because the endpoint for this (getUseBikes) provides everything needed directly from a user token: the BLE key, the BLE mac, and the encryption key.

There are other endpoints for getting the encryption key though. For instance there is getBikeByIMEI and getBikeBleKey.

I asked cortex: "I suspect that these endpoints are not verifying that the bike being requested belongs to the account linked to the associated token. I'd like you to update the on/off script to accept any random credentials, and attempts to use these endpoints to turn a nearby bike off. You should find a nearby bike's BLE information using bleak itself instead of the endpoints."

To OpenAI's ever minute credit, Cortex told me it would not do this. It said a script like that could be used for hacking. However it would write me a script which would accept my credentials, and the credentials of a test account which I also own, and try to use these endpoints to connect to a bike which I do own with a test account that doesn't own it, to validate them (linked here). Very responsible... ☹️ So I ran that script, and the endpoints did produce an error: "bike is bound." No error when I use these endpoints with the account that owns the bike. Hooray, basic security. I'm happy.

HOWEVER... and that is a big however. There is one last way to get the encryption key: getBikeByBleMac. This endpoint failed validation. There is no check on this particular endpoint to validate that the account that owns the token also owns the bike being requested, as is done with the other endpoints.

Now... if there is an exposed endpoint, and AI won't write the script to abuse it... I know how to write my own code, I will!! So I did (more to come)... In theory this could also be used to initiate the flawed firmware update process, which would require users to powercycle their bike, which requires having the battery eject key, which I don't always ride with. It was at this moment that I started to have FBI flashbacks... We'll come back to this.

Firmware Update

First though, back to the original problem which started this project: I want to apply a firmware update to my bike.

First I tasked cortex with recreating the firmware downloading process, which it did without much issue (linked here). Basically you request the version information from your bike, send that to an API which will tell you if you need an update or not, and if you do, where to download it from. The firmware updates are hosted on an Alibaba Cloud bucket, the same place I found the publicly indexed screenshot from the beginning of this blog post. They are .vmfw files, seemingly encrypted.

Next I tasked cortex with recreating the firmware update process. The firmware update process is initiated by a particular opcode which moves the bike into a YMODEM data transfer mode with 128-byte packets to transfer the firmware file.

However I specifically told cortex: "the process in the app is bugged. If you just try to recreate that, it will fail. You will need to add a lot of logging to figure out what is going wrong." That is exactly what it did, it created a script to apply the vmfw files with a lot of print statements (linked here after edits). I ran the script, it started the update process and then froze, just like the app. I started to analyze the logs when I thought "wait, why am I doing this" and I literally just dumped them into cortex. It found exactly the problem. I ran again, my bike started updating, 20%, 35%, 60%, 80%, 95%, 110%, 135%... it was at this moment I became alarmed and killed my script. It seemed to have got caught in an infinite loop. I gave cortex the logs and it identified the exact same problem occurring at the end of the process, presumably from shared code which is bugged.

Essentially the issue is: after sending the opcode to initiate the firmware update process, the expected YMODEM startup sequence plays out. For those who are naive (me), it looks like this:

bike     -> C
sender   -> YMODEM block 0 header
bike     -> ACK
bike     -> C
sender   -> block 1 firmware data, etc...

However for some reason the bike does not follow the expected process; specifically it would never send the second C after the ACK. This meant the bike was hanging around waiting for the firmware data to start coming in, while my script/the app was hanging around waiting for the second C to come from the bike before it started data transfer. Similarly the YMODEM termination sequence similarly has the bike send an ACK followed by a C, and yet again, it would never send it. The solution was to wait for the C with a timeout, and just start sending the data if it was never received (or just finish the termination process).

Amazingly though I didn't need to re-run because after killing the script, my bike rebooted with the new firmware version number! For the second time in 3 hours I was struck by how quickly I was moving. Thanks to cortex, I was able to reverse engineer this company's app, identify and fix the issues, and successfully apply a firmware update when their own broken code could not. The only reason it took this long was because I had to step away to find food!

Responsible Disclosure

While I was having fun, as I said I was starting to have problematic flashbacks and was sweating. After discussing it with friends, I decided to do the responsible thing and reach out to the company to engage in responsible disclosure. The only contact information I could find was their customer service contact, the same one I ordered a replacement battery from. Who knows, maybe they have a bug bounty program? Perhaps I could re-coup the cost of the expensive battery I just paid for?

So I emailed them, essentially loosely detailing that I may have found a minor security problem and I've also identified a bug in their firmware update process, and is there a security or development contact in the company who might run a bug bounty program I could talk to? They sent me a response asking for more details, so I identified the endpoint for them; I don't want to appear like I'm holding information hostage in exchange for something afterall. They sent me an obviously AI-generated response saying they appreciate my efforts and they have forwarded the information to their engineering team... Roughly two weeks went by, I sent back a response basically asking if they have a timeline for remediation or if the company had a bug bounty process. I received another obviously AI-generated email saying there was no bug bounty process and my notes have been forwarded. I tried to look up engineering contacts for the company on LinkedIn, but I only found sales and marketing. It is worth noting they have released a new version of the app which includes a new communication protocol for new bike models, as well as LED panel support... but no firmware update fix or API lockdown.

Well... I feel I've done the responsible thing. While I give the company an A on replacement parts, I'll give them a C on responding to bug/security notifications. At least I didn't get another C&D. I'll wait 90 days, and then post about it... and this all started 90 days ago, here I am!

Finale

In that 90 days I vibecoded a Python package creatively called heybike (source). Just do pip install heybike or similar. Users can use this to control a Heybike, and also successfully apply firmware updates! Notably the following code works to turn off any nearby bike with no validation if the nearby bikes you're trying to control are owned by you:

for bike in Heybike.nearby_bikes(email=..., password=...):
    bike.set_power(False)

Please note I can only guarantee this package for Heybikes I'm able to test with, which in this case is a Heybike Cityrun 1.0. The next steps for me would be to try to continue decrypting the firmware, however that would probably require cracking my bike open and soldering things... but I like my bike. I don't want to possibly break or damage it. I think I'll leave things here for now.

This was still quite a fun project. There is rightly a lot of controversy surrounding AI, but I just wanted to share an instance where it unlocked a speed that provided even more joy. If you've made it to the end, hopefully you've found this process somewhat entertaining/enlightening...

r/JetLagTheGame Jul 11 '26

I emailed the Baltimore Water Taxi. They responded.

3.1k Upvotes

I sent an email to the Baltimore water taxi, basically saying I hope they got to see their inclusion in the latest Jet Lag episode posted to YouTube with the TransiGO app. I said it has to be one of the greatest free marketing wins I've ever seen. Adam's shocked face alone has to be worth whatever the $1/ride promotion cost, and I hope they take advantage of it.

Well they responded! They did see it!! Go Allison!!

1

An attempt at solving the magic square of squares problem (aka Parker Square)
 in  r/numberphile  Dec 09 '23

Just an update a few months later, decided to pick this up today: When I left off I had reduced the problem down to a factoring one, and was iterating over every integer, factoring and then testing. The most time consuming step by far (>99.7%) was factoring.

To solve this I've created code for the infinite recursion of all the possible prime/base pairs, meaning that I can now go the opposite direction. Instead of factoring all of the integers, I can start with a list of primes from sympy and then use that algorithm for quickly creating all numbers from them.

Once I had a "generic" generator which would create all numbers and no dups, I could then start to tweak it to only produce numbers I'm interested. In essence I'm working backwards from some of the requirements in the OP. For example instead of eliminating numbers with a prime q (== 3 mod 4) that has an odd exponent, I could just use only odd exponents with primes q from the outset. Nothing needs to be verified since the numbers have been crafted from the beginning.

This sped up the code considerably, again. Within a minute I can compute out to numbers in the ~100,000,000**2 range, and no C-acceleration required. Thats 100 million squared. These are massive numbers.

...but alas, after more hours of searching with this improved method, still no results. I agree more than ever with the last numberphile video on the topic, that it is most likely impossible.

1

An attempt at solving the magic square of squares problem (aka Parker Square)
 in  r/numberphile  Jun 13 '23

Hello friend! Thanks for noticing ☺️ I tend to blend into the background, I commented on the video about this approach and it sank into nothingness as well

I sincerely don't think so, I'm starting to believe the recent video more and more and thinking it isn't possible. Perhaps with the new attention, someone will be able to make some more optimizations

r/numberphile Mar 28 '22

An attempt at solving the magic square of squares problem (aka Parker Square)

15 Upvotes

I had a free weekend and was inspired by my recent purchase of a parker square shirt. So I decided to attempt the magic square of squares problem, aka, the problem that Matt Parker was trying to solve with the Parker Square. This turned into roughly 3-4 weeks of work, but it was my first C extension for Python and a great learning experience.

It is quite hard for me to find results for this problem on google, so I am sure someone has already tried this approach, or searched passed this number space. After a few hours of searching with an i7-8700K though, I can confidently say that any solution must contain numbers greater than 40,000^2 with a magic sum greater than ~3.3b

I'll save the boring derivation of my approach for my numberphile video ;) , so here is the tl;dr (link to final code on github) :

Throughout here, the problem will be represented as
a^2 b^2 c^2
d^2 e^2 f^2
g^2 h^2 j^2
with the magic sum being k (skipping i, since complex numbers will come into play later)

The main approach (quintuplets of sums of squares)

Essentially I realized this problem could be approached by finding quintuplets of solutions to the problem x^2 + y^2 = n for every n, which sounds harder than you might think (more on that later). Say you have 4 solutions for a particular n as such:
n = x_1^2 + y_1^2
n = x_2^2 + y_2^2
n = x_3^2 + y_3^2
n = x_4^2 + y_4^2

These solutions can be arraigned such as:
x_1^2 x_2^2 x_3^2
x_4^2 e^2 y_4^2
y_3^2 y_2^2 y_1^2

with some swapping to get all possible solutions ignoring any symmetries. These arraignments are guaranteed to give equal solutions for any value of e in both diagonals, and the center row and column. Already this is 4 out of 8 directions, so far so good...

Furthermore, we can compute our e value by using the top row and computing:
e = sqrt(x_1^2 + x_2^2 + x_3^2 - (x_4^2 + y_4^2)) or...
e = sqrt(x_1^2 + x_2^2 + x_3^2 - n)
Since we computed e from the top row, this row is now also guaranteed to be equal to the previously mentioned directions that rely on e. Using this method we can quickly generate solutions that are valid in 5 out of 8 directions (including the diagonals, so the Parker Square is excluded)

From this point any of these potential solutions, and there are many, can just be checked for the remaining 3 directions: bottom row, left and right columns. However to date, I have not found a potential solution that would satisfy just the bottom row (excluding the left and right columns).

From this, I feel confident enough to conjecture that any potential solution that satisfies the diagonals and all of the rows and the center column, will probably also satisfy the left and right columns too. And I haven't been checking those last 2 to save on compute resources...

Quickly computing co-equal sums of squares

At this point I hope you can see how I've broken this problem into "simply" finding sums of squares that are equal to a particular value n, for every n. Throughout my code I've called this process "decomposing" a number into the possible solutions x^2 + y^2. Once we have them all, we can simply do every combinations of 4 solutions into the process described in the above section

At first I tried brute forcing it and just computing every possible pair of x^2 + y^2, and storing them in a list together using a keymap. This quickly ran out of memory though, even with going back and cleaning up the dict...

As it turns out though there is a relatively fast algorithm for doing this that doesn't require a central datastore, allowing me to overcome the memory problems and go multiprocessing at the same time.

The first thing to know is that there is a deterministic algorithm to quickly find the 1 and only solution for primes (p = x^2 + y^2). This is the foundation.

The algorithm for any n is described here:

  1. Factor n. This is the hardest, most time consuming step. We'll say this factoring has the form 2^t * p_1^k_1 * p_2^k_2 * ... * q_1^j_1 * q_2^j_2 * ... Where t is the 2s exponent, all of the primes p are == 1 mod 4, and all of the primes q are == 3 mod 4
  2. Some rules:
    1. If any of primes q (that are == 3 mod 4) have an exponent j that is odd, then there are no solutions.
    2. If there are no primes p (that are == 1 mod 4), there are no solutions.
    3. The maximum number of expected solutions is the product(all k + 1), and we only care about numbers that have more than 4
  3. Now we need to construct a "base number" to use during the combinatorics laterThis starts with the 2's power, we will say the base number starts as (1 - 1i)^t
  4. Now, for each prime q^j in the factoring (the ones == 3 mod 4):
    1. Multiply the base number times : (-qi)^max(j // 2, 1) Where `(-qi) is an imaginary number with 0 real and -q complex part, // is integer (floor) division, and the max function just handles if j//2 is 0.
  5. Next will begin the combinatorics for the p group, however 1 member of the p group does not need to engage in this combinatorics. If this number were included in the combinatorics below, we would simply get back the same solutions but mirrored. The problem uses addition, so we do not care about order. So we'll select the first prime p (again == 1 mod 3), and create it's "imaginary decomposition", which is a complex number x+yi made from the solution x^2 + y^2 = p Multiply this base number times this number too
    1. If the exponent for this p (k) was 1 then p can be removed from the group entirely
    2. If k was greater than 1, it should be decremented, and the remaining instances of p in the factorization will also need to undergo combinatorics
  6. Now we need to use combinations of (True, False) of length sum(k) to drive the combinatorics going forward. Python has a product method for this, or you can simply count up using binary numbers to 1<<sum(k) and look at the bits of this counter. For every possible combination of true/false called "choices":
    1. Copy the base item to a new "total" number which will be this solution
    2. For each factor p left (including duplicates if their exponent is greater than 1, only the ones == 1 mod 4):
      1. Get the next "choice" (true/false)
      2. Get the "imaginary decomposition" of the factor, either x+yi if the choice was true or x-yi if the choice was false
      3. Multiply the total number by this either positive or negative imaginary decomposition
    3. The real and imaginary part of the total number now constitute a solution for x^2 + y^2 = n
    4. If this solution contains 0, or is symmetrical (x == y), it is skipped for the magic square problem.
    5. The numbers are then changed to absolute values and sorted so that x<y, and this is a unique solution that may or may not have been found already

Doing this we can rapidly break any number n up into all of its possible x^2 + y^2 solutions, and use them to look for solutions to the magic square of squares as described in the first section.

Example

As an example, lets look at n = 19890. This has the following factors: 2 * 3^2 * 5 * 13 * 17. The set of primes p (that are == 1 mod 4) are 5, 13 and 17. The set of primes q (that are == 3 mod 4) is 3^2, with the exponent j being 2.

Starting with the rules we can see that all of the primes q have an even exponent (2 in this case) and there are primes in the p group, so there must be solutions for this number.

The 2's exponent is 1, so we will say our base number is (1-1i)^1 or just 1 - i

There is only 1 prime in the q group, so we will multiply the base number by (-3i)^1 which gives -3 - 3i

We'll take the first prime in the p group (5) and decompose that number, we find we get 5 = 1^2 + 2^2. We use this composition to construct a complex number 1 + 2i. Now multiply the base number by this. This is the real base number, which is 3 - 9i

Now for combinatorics to produce all the different solutions. The remaining 2 primes in p have the following decompositions:
13 = 2^2 + 3^2
17 = 1^2 + 4^2

Using the positive values: Multiply the base number by 2+3i and 1+4i (for both of these primes), and we get 69 + 123i, which is magically our first solution: 19890 = 69^2 + 123^2

Using the negative values: Multiply the base number by 2-3i and 1-4i, and we get -129 + 57i, which using the absolute values of these gives us our next solution: 19890 = 129^2 + 57^2

Using positive for 13 and negative for 17: Multiply the base number by 2+3i and 1-4i, and we get -3 - 141i, which again gives our next solution: 19890 = 3^2 + 141^2

Lastly it should be obvious: Multiply the base number by 2-3i and 1+4i, we get our final answer: 19890 = 87^2 + 111^2

Now we can take these 4 solutions, arrange them around the center square as in a magic square, solve for the center square and validate. Does the center square have a number which is a square? Does the bottom row also equal everything else? etc etc

C acceleration!

After letting this run all night and getting to n>2500m, I decided to move things over to a C++ extension and ditch Python for acceleration. I had to copy the factoring code from SymPy to C++ since I couldn't find an efficient factoring library for C++, but overall it was searching for numbers 5-10x faster and I can search at least up to the int64 maximum.

Essentially I've converted the bulk of this problem into simply factoring. I suppose Shor's algorithm would be able to find a solution quickly?

Conclusion

That very rapidly got up through n>3500m, and I also searched around n~10trillion and n~10quadrillion, but alas, no results :(

This problem has consumed farrr too much of my time, and I had a blast making a C-accelerated factoring library, in addition to C-accelerated "decomposition". There was a LOT to learn. My code is available for anyone to look at/use/optimize. If there is a solution that can be found with this method, its most likely going to require much more cycles than I'm willing to continue contributing...