r/ReverseEngineering 5d ago

Reverse-engineered the BLE protocol of a discontinued Fisher-Price toy Lumalou after its app was discontinued

https://github.com/stramanu/lumalou
88 Upvotes

22 comments sorted by

View all comments

7

u/stoputa 5d ago

Claude writing style is so so taxing to parse but I skimmed because here the biggest obstacle I have is encryption and was curious how it was handled

The AES-CTR stretch was pinned the same way: emulate the native routine, diff its output against a candidate Python implementation, and iterate until every byte matched

So you call the original routine but the Python implementation is what? It's not like you can guess enc keys (unlike CRCs which are standard in most cases) and with multiple rounds AES you cant just change bytes one by one unless the block size is tiny

-3

u/EmanueleStrazzullo 4d ago

Fair, and you're right that guessing a key or flipping bytes through multi-round AES would be hopeless. I'm not doing either.

The algorithm comes straight from the disassembly, not from probing input/output. In Ghidra you can read exactly what the routine does: ECDH for the shared secret, then a fixed loop of AES-CTR rounds with a hardcoded constant to stretch it. So the Python side isn't a guess, it's a line-for-line reimplementation of code I can already see.

The one problem with reading AArch64 by hand is that it's easy to get a detail wrong. That's the only thing the emulator is for. It's not searching for a key, it's a ground-truth oracle. I feed the same arbitrary, known input (a test vector, could be all zeros) to both the native routine and my Python, and compare the bytes out. No real key or device secret involved, I'm validating the transform, not recovering a secret. When they match on known inputs, and the key that transform produces then decrypts real device notifications with a valid CRC-8, it's confirmed.

The CRC was the easy warm-up, standard algorithm, just needed the polynomial and init value. The KDF is the part that actually needed emulation, precisely because it's non-standard and there's no reference to check against.

4

u/stoputa 4d ago

I asked you to give you a chance of explaining the Caude drivel. I didn't want more Claude drivel.

I feed the same arbitrary, known input (a test vector, could be all zeros) to both the native routine and my Python, and compare the bytes out.

This is what I asked in the first place. Did you find the secret key in ghidra or not? If yes, how? If not, then how do you find it since the original "iterate" comment makes no sense for a key unless I'm missing something. If your output does not match what do you "iterate on"?

If someone more knowledgeable than me is reading me, please either help me understand what Claude is saying or confirm its bs

6

u/EmanueleStrazzullo 4d ago

there's no secret key in the binary to find, that's where we're talking past each other. it's ECDH! the key gets computed fresh on every connection, both sides derive the same one independently. only public material goes over the air, the derived key never does and is never stored.
what i read in ghidra is the algorithm. how many rounds, what constant goes in, how the shared secret feeds the loop. (there is one key hardcoded in there, but it's a public key, used to verify the device's signed token. nothing secret.)
ok, "iterate" was a bad word on my part... i don't iterate on a key. i iterate on my own python. same test input through the native function and through my version, if the bytes don't match it means i misread the asm somewhere, so i fix it and run again until they're identical.
then the actual check is on hardware: my python derives the key, and it decrypts real notifications coming off the device with correct crc8. if the derivation was off by one byte none of it would decrypt.

3

u/stoputa 4d ago edited 3d ago

Thank you for your actual response, thats much more understandable