Ph0wn 2018 - Healing the Toothbrush 2

CTF URL: http://ph0wn.org/

Category: Misc

Challenge description

This challenge was the second stage of Healing the toothbrush 1.

Now that we had the encryption key, we had to communicate with it. Fortunately most of the required information was provided and we did not have to reverse the app.

Now that you know how events are encrypted in my toothbrush’s subconscious mind, you are ready to have it talk. Fortunately, the psychiatrist helps you.

The toothbrush’s subconscious mind consists, apparently, in a rolling table of 256 events, where each event is indexed by its index in the table. You cannot directly read an event entry, but you can ask information for that index. For that, you need to:

The format of an event is:

Total: 16 bytes

Then, the bytes of the event are swapped reverse (first byte becomes last) and encrypted (see stage 1).

Finally, the following might help you:

Author: cryptax

You can download the example file.

Challenge resolution

We first had to find the Bluetooth Low Energy MAC address of the toothbrush to connect to it:

Then we adapted the example script for our need. You can download our solution script.

It took us a lot of time but we finally managed to meticulously implement the protocol as described in the challenge description. Let’s review some of the interesting parts.

Cipher

The cipher configuration in Python that corresponds to the Java code previously reversed was:

cipher = AES.new("e02b90e8e50be5b001c299a5039462c2".decode("hex"), AES.MODE_ECB)

Ask information for an index

The first step was to ask the toothbrush information for each index.

self.enable_notif(0x0026)
print "[+] Enabled notif"

for i in range(0, 256):
    self.enable_notif(0x0026)
    event = str(bytearray([0x41, 0x41, 0x41, 0x41, 0x41,        # raw data: 5 bytes (not used)
                           0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x00,  # start date: 6 bytes (YY MM DD HH MM SS)
                           0xFF, 0xFF, 0xFF, 0xFF,              # brushing duration: 4 bytes
                           i,                                   # index: 1 byte
                           ]))

    event = cipher.encrypt(event)
    event = event[::-1]  # reverse
    self.req.write_by_handle(0x0028, event)

We enable the notifications via handle 0x26, then iterate on all 256 index values by sending dummy events as advised. All values are meaningless in the dummy event except the index.

We wasted a lot of time since we understood that the challenge description asked us to encrypt then reverse the bytes, but it was actually the opposite that was required 🤦…

Receive notification

After each request for information, the toothbrush answered via a notification that we handled.

We were initially surprised to receive a payload of 19 bytes which is not normal for an encrypted block (should be a multiple of 16 in our case). We observed that all payloads began with the same 3 bytes 1B 25 00. Wireshark helped us understand that the payload actually contained a prefix that we had to discard:

Here is the handling code:

class MyRequester(GATTRequester):
    def on_notification(self, handle, data):
        data = str(data)  # change type
        data = data[3:]  # remove first 3 bytes (only keep payload)
        data = data[::-1]  # reverse

        if len(data) % 16 == 0:  # only try to reverse if it's a block
            decrypted = cipher.decrypt(data)
            print decrypted

Get flag

Finally we discovered the flag that was split accross several indexes: ph0wn{brushUrTeeth2mins}

Author: cnotin Clément Notin | @cnotin

Post date: 2018-12-16