Recently I have received a bluetooth smart tag device. The Apple AirTag kind of thing. It has a dedicated Android and IOs application. No GPS module, yet capable of giving pretty accurate location, and what's more important - playing a nice chiptune enabling you to find your keys/wallet/dog/whatever. You just click a button in your mobile app and it starts its high-fidelity, breathtaking symphony of beeps and boops produced by internal piezo speaker.

My first thought (obviously) was how to do something silly with it. And then I found that you can only pick from 10 available ringtones. So there it is. My quest. I need more ringtones.

I started surfing the web looking for a solution. I found a single reddit post that gave me some information:

Thanks to Reddit user ad2022 ringtone binary format became publicly known. As suggested by monstersgetcreative these songs are signed with a private key, so they cannot be tampered with by others without having a private key to sign a new song. Even if that was true, I could just replace the public key device-side that is used to decrypt a song, and prepare a new ringtone set, signed by me. And maybe it is just a simple CRC that prevents data corruption when uploading new songs.

How do I know that ringtones are not just hardcoded and stored on a tag itself? According to reddit post, ingtones can be found in mobile application. Also, when changing it from the app, it takes a while, which suggests that it's uploading it, not just picking a preset.

So either way if there is a signature involved or a CRC sum, finally it has to be stored somewhere in tags persistent memory. So I can just look for a ringtone, edit a few bytes and hear a difference. Or is there more to it?

First lets examine the hardware. Nice, compact enclosure, pop up plate on the back. One 3V coin cell to power it. Underneath there is a nicely manufactured black PCB. And no screws. Very carefully I pop the enclosure open, making sure it can be reassembled later.

One side reveals a button and a bunch of test pads labeled Exx TPxx and a group of test pads labeled J11. Some of these are probably used for programming, right?

On this side there are two chips and some external components. And also springs connecting piezo with the board.

By looking at traces it seems that the smaller chip has to be the piezo driver. And the big one is labeled NRF52820. It's a Nordic Semiconductor Bluetooth SoC. It's datasheet is publicly available and contains pinout for this package.

This chip has SWD (Serial Wire Debug) interface that can be used to program it. Using multimeter set for continuity testing I traced the available test pads to the chip to determine which lead to programming pads. I got almost every pin.

SWDCLK - E13 test pad or second top-left on J11 pads
SWDIO - directly to mcu (look nrf52 datasheet)
GND - J11 test pads top left when looking so that button is above pads
3.3V - J11 pads, top right, button is above pads - vcc

SWDIO is not connected to MCU, at least not directly. Either way I couldn't locate a test pad for it. So I soldered a tiny wire directly.

Now it's time to read the memory. I wouldn't even start soldering if I didn't have my good old chinese stlink clone laying around:

I used OpenOCD to communicate with the chip. Bingo. It listed out all the available flash banks. I halted the chip and dumped all two available banks. One flash bank is 256kB and the other one is 4kB. The bigger one is flash memory that the chip runs code from, the smaller one is used as an EPROM-like persistent storage.

Now the fun part. Uploading another ringtone to the device and dumping flash. For diffing I use radiff2 - a part of radare2 suite.

There are 6 memory locations (lets call them blocks) that are differrent between dumps. At least one of them has to contain a ringtone. To further inspect I dumped the same firmware twice at different times and see the diff then.

After operations described above I could identify proper locations.

1 and 3 - different between dumps with same ringtone, we can rule them out
2 and 4 - identical
3 and 6 - identical

2 and 4 are too short to contain such long ringtones, that can be determined by the number of notes played times 16 bits. Also, 3 and 6 contain very repetitive data which format looks like a ringtone format described on reddit:

    dump 1:
    0x0003e1f0! 4b0352034b0352034b0352034b035203 K.R.K.R.K.R.K.R.
    0x0003e200! 4b030007520a4f0a4b0a460a430a460a K...R.O.K.F.C.F.
    0x0003e210! 4b0a4d0a4f034b034f034b034f034b03 K.M.O.K.O.K.O.K.
    0x0003e220! 4f034b034f034b03000750034d035003 O.K.O.K...P.M.P.
    0x0003e230! 4d0350034d0350034d0350034d030007 M.P.M.P.M.P.M...
    0x0003e240! 500a4d0a4a0a460a410a460a4a0a4b0a P.M.J.F.A.F.J.K.
    0x0003e250! 4d034a034d034a034d034a034d034a03 M.J.M.J.M.J.M.J.
    0x0003e260! 4d034a0300074f034b034f034b034f03 M.J...O.K.O.K.O.
    0x0003e270! 4b034f034b034f034b0300074f0a4b0a K.O.K.O.K...O.K.

    dump 2:
    0x0003e1f0! 3e043f08420d0004440e000345064206 >.?.B...D...E.B.
    0x0003e200! 45064206450642064506420645064206 E.B.E.B.E.B.E.B.
    0x0003e210! 45064206450646074207460742074607 E.B.E.F.B.F.B.F.
    0x0003e220! 4207460742074607000d3d043e043f09 B.F.B.F...=.>.?.
    0x0003e230! 420d0005440e00034506420645064207 B...D...E.B.E.B.
    0x0003e240! 45064206450642074506420645064206 E.B.E.B.E.B.E.B.
    0x0003e250! 45064607420746074207460742074607 E.F.B.F.B.F.B.F.
    0x0003e260! 42074607000d3d043e043f09420d0005 B.F...=.>.?.B...
    0x0003e270! 440e00034607490746074b11500c0005 D...F.I.F.K.P...

The I patched some data in the block 3 and see what happens. I put single note, described by two bytes (0x5a0a), repeated over most of the song.

Nothing happened. The uploaded ringtone still plays. Then I tried block 6. Still nothing - that is expected. Probably these are stored in two memory locations just to be sure data hasn't beed corrupted. The ringtone might be transmitted two times, once per both of these locations, with saved CRC sum or a signature to make sure that it won't arrive broken.

But what if we modify both ringtones? Either the tag doesn't play anything or it plays a corrupted ringtone, because it can't play anything else?

Well, surprise. It magically played it. Is there a possibility that each time we call it, a ringtone is uploaded again? I deciced to check that theory.

  1. Uploading a ringtone
  2. Dumping the memory
  3. Modifying blocks 3 and 6
  4. Uploading firmware back
  5. Calling the tag

It played the whole unmodified ringtone.
I have dumped the firmware again and ringtone memory blocks were still broken. So we ruled out that by calling the tag we upload a new ringtone and "fix" it.

How does the device know what notes to play even though we wiped them? This was the time I gave up the project for a longer while.

After a few months i found the strength and patience to try again.

Modified block 3 - ringtone plays.
Modified block 6 - ringtone plays
Modified both blocks - ringtone plays

Wait a second. The third one. That's not the ringtone I uploaded... Apparently there is a third, backup ringtone in the memory. It plays when both transmitted ones are broken. I missed it when I tried before, simply because it was the same melody I was modifying and uploading.

After further diffing I found that this backup ringtone is also present in two places in the memory - let's say blocks 7 and 8. So one way to make the modified ringtone work is to:

  1. Invalidate block 3
  2. Invalidate block 6
  3. Put ringtone in block 7
  4. Put ringtone in block 8

This way I ignore the logic behind blocks 7 and 8, as I put the exact copy in both locations it should just work. And it did!
The next step was to prepare a more interesting ringtone:

I have created a simple tool that you can use to prepare your own song and upload it. Code is available at this github repository. Please refer to the README file for more information on how to use it.