This post contains a write-up of the Bluetooth Low Energy Capture the Flag (BLE CTF) as developed by hackgnar. The CTF teaches various core concepts of Bluetooth LE. A more advanced version is now available too, the BLE_CTF_INFINITY.

Prerequisites

After building and flashing the CTF to the target device (see the documentation for the required steps), ensure the bluetooth service is running using systemctl start bluetooth. Next verify the target device is discoverable: sudo hcitool lescan should return an entry like this: 30:AE:A4:26:2B:E6 BLECTF.

I added a few functions to my shell to make common operations a bit quicker:

blemac="30:AE:A4:26:2B:E6"

function ble_get_score() {
        # read score from handle 42
        gatttool -b $blemac --char-read -a 0x002a | awk -F':' '{print $2}' | tr -d ' ' | xxd -r -p;printf '\n'
}

function ble_submit_flag() {
        # submit flags to handle 44
        gatttool -b $blemac --char-write-req -a 0x002c -n $(echo -n "$1" | xxd -ps)
}

function ble_to_hex() {
        echo -n "$@" | xxd -ps
}

function ble_from_hex() {
        echo -n "$@" | tr -d ' ' | xxd -r -p ; echo
}

function ble_read_hnd() {
        ble_from_hex $(gatttool -b $blemac --char-read -a "$@" | cut -d':' -f2)
}

All flags should be written to handle 0x2c as hex, the ble_submit_flag function handles that, but “manual” submissions through gatttool have to be encoded before.

Flag 1

This flag was given in the description:

% ble_submit_flag 12345678901234567890
Characteristic value was written successfully
% ble_get_score
Score:1 /20

Flag 2, 0x002e: Learn how to read handles

Connect interactively and read from the handle:

[30:AE:A4:26:2B:E6][LE]> char-read-hnd 0x002e
Characteristic value/descriptor: 64 32 30 35 33 30 33 65 30 39 39 63 65 66 66 34 34 38 33 35 
[30:AE:A4:26:2B:E6][LE]>

All values are within ASCII range, so let’s convert them:

% echo -n 64 32 30 35 33 30 33 65 30 39 39 63 65 66 66 34 34 38 33 35 | tr -d ' ' | xxd -r -p
d205303e099ceff44835

In the subsequent flags I’ll use ble_read_hnd $handle to read and convert the handle data instead. For example:

% ble_read_hnd 0x2e
d205303e099ceff44835

Now write that flag to handle 0x2c as hex:

[30:AE:A4:26:2B:E6][LE]> char-write-req 0x2c 6432303533303365303939636566663434383335
Characteristic value was written successfully
[30:AE:A4:26:2B:E6][LE]>

And verify the result:

% ble_get_score
Score:2 /20

Flag 3, 0x0030: Read handle puzzle fun

MD5 of Device Name

The device name is BLECTF, so take the MD5 sum of that and cut it at 20 chars (see the ble_ctf README):

% echo -n BLECTF | md5sum | cut -c 1-20
5cd56d74049ae40f442e
% ble_to_hex 5cd56d74049ae40f442e
3563643536643734303439616534306634343265

Now submit the flag:

[30:AE:A4:26:2B:E6][LE]> char-write-req 0x2c 3563643536643734303439616534306634343265
Characteristic value was written successfully

Flag 4, 0x0016: Learn about discoverable device attributes

Bluetooth GATT services provide some extra device attributes. Try finding the value of the Generic Access -> Device Name.

GATT services numbers: https://www.bluetooth.com/specifications/gatt/services/ This document defines the UUIDs for all the defined services. Documentation for those services is found here: https://www.bluetooth.com/specifications/gatt/ Good information is available here on Bluetooth LE profiles which contains services which contain characteristics. All Bluetooth defined characteristics are defined here: https://www.bluetooth.com/specifications/gatt/characteristics/

Referring to the Bluetooth Specification version 5.2, volume 3, part C, chapter 12 on page 1395 it describes the “Device Name” characteristic and states that it has UUID 0x2A00.

Going back to our device let’s use the primary command of gatttool which does a device discovery:

[30:AE:A4:26:2B:E6][LE]> primary
attr handle: 0x0001, end grp handle: 0x0005 uuid: 00001801-0000-1000-8000-00805f9b34fb
attr handle: 0x0014, end grp handle: 0x001c uuid: 00001800-0000-1000-8000-00805f9b34fb
attr handle: 0x0028, end grp handle: 0xffff uuid: 000000ff-0000-1000-8000-00805f9b34fb

Now we have three ranges of handles to go through looking for the UUID:

[30:AE:A4:26:2B:E6][LE]> char-desc 0x1 0x5
handle: 0x0001, uuid: 00002800-0000-1000-8000-00805f9b34fb
handle: 0x0002, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0003, uuid: 00002a05-0000-1000-8000-00805f9b34fb
handle: 0x0004, uuid: 00002902-0000-1000-8000-00805f9b34fb
[30:AE:A4:26:2B:E6][LE]> char-desc 0x14 0x1c
handle: 0x0014, uuid: 00002800-0000-1000-8000-00805f9b34fb
handle: 0x0015, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0016, uuid: 00002a00-0000-1000-8000-00805f9b34fb
handle: 0x0017, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0018, uuid: 00002a01-0000-1000-8000-00805f9b34fb
handle: 0x0019, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x001a, uuid: 00002aa6-0000-1000-8000-00805f9b34fb
[30:AE:A4:26:2B:E6][LE]>

And in the second batch we have the UUID we’re looking for, at handle 0x0016:

[30:AE:A4:26:2B:E6][LE]> char-read-hnd 0x16
Characteristic value/descriptor: 32 62 30 30 30 34 32 66 37 34 38 31 63 37 62 30 35 36 63 34 62 34 31 30 64 32 38 66 33 33 63 66
[30:AE:A4:26:2B:E6][LE]>

If we then decode it, cap it to 20 characters, convert it back to hex and submit it we have another flag.

% echo -n 2b00042f7481c7b056c4b410d28f33cf | cut -c 1-20
2b00042f7481c7b056c4
% ble_to_hex 2b00042f7481c7b056c4
326230303034326637343831633762303536633

Flag 5, 0x0032: Learn about reading and writing to handles

write anything here

So I wrote “anything” and read handle 0x32 again which now gave a different value. That value can then be written to handle 0x2c and we have another flag!

[30:AE:A4:26:2B:E6][LE]> char-write-req 0x32 616e797468696e67   # <-- 'anything'
Characteristic value was written successfully
[30:AE:A4:26:2B:E6][LE]> char-read-hnd 0x32
Characteristic value/descriptor: 33 38 37 33 63 30 32 37 30 37 36 33 35 36 38 63 66 37 61 61
[30:AE:A4:26:2B:E6][LE]> char-write-req 0x2c 3338373363303237303736333536386366376161
Characteristic value was written successfully
[30:AE:A4:26:2B:E6][LE]>

Flag 6, 0x0034: Learn about reading and writing ascii to handles

Write the ascii value “yo” here

Simply convert to hex and write to the handle:

% ble_to_hex yo
796f
% gatttool -b $blemac --handle 0x34 --char-write-req --value 796f
Characteristic value was written successfully

Next read from the handle:

% gatttool -b $blemac --handle 0x34 --char-read
Characteristic value/descriptor: 63 35 35 63 36 33 31 34 62 33 64 62 30 61 36 31 32 38 61 66

Now write that back to handle 0x2c to submit the flag.

[30:AE:A4:26:2B:E6][LE]> char-write-req 0x2c 6335356336333134623364623061363132386166
Characteristic value was written successfully
[30:AE:A4:26:2B:E6][LE]>

Flag 7, 0x0036: Learn about reading and writing hex to handles

Write the hex value 0x07 here

So let’s do that in order to obtain the flag:

[30:AE:A4:26:2B:E6][LE]> char-write-req 0x36 07
Characteristic value was written successfully
[30:AE:A4:26:2B:E6][LE]> char-read-hnd 0x36
Characteristic value/descriptor: 31 31 37 39 30 38 30 62 32 39 66 38 64 61 31 36 61 64 36 36
[30:AE:A4:26:2B:E6][LE]> char-write-req 0x2c 3131373930383062323966386461313661643636
Characteristic value was written successfully

Flag 8, 0x0038: Learn about reading and writing to handles differently

Write 0xC9 to handle 58

As stated, write the requested by to the handle, note that it’s written in decimal and we need to tell gatttool use the hex representation of that handle:

[30:AE:A4:26:2B:E6][LE]> char-write-req 0x3a C9
Characteristic value was written successfully

Now read from 0x38 again to get the flag to be submitted to 0x2c:

[30:AE:A4:26:2B:E6][LE]> char-read-hnd 0x38
Characteristic value/descriptor: 66 38 62 31 33 36 64 39 33 37 66 61 64 36 61 32 62 65 39 66

Flag 9, 0x003c: Learn about write fuzzing

Brute force my value 00 to ff

I wrote a quick’n’dirty Python script to do the bruteforcing:

#!/usr/bin/env python3

import subprocess

blectf_mac='30:AE:A4:26:2B:E6'
    
for i in range(0x00, 0xFF):
    val = format(i,'x').zfill(2)
    print("==> Trying {}".format(val))

    write_cmd = 'gatttool --device={} --char-write-req --handle=0x3c --value={}'.format(blectf_mac, val)
    output = subprocess.Popen(write_cmd, shell=True, stdout=subprocess.PIPE).stdout.read()

    read_cmd = 'gatttool --device={} --char-read --handle=0x3c'.format(blectf_mac)
    output = subprocess.Popen(read_cmd, shell=True, stdout=subprocess.PIPE).stdout.read().strip().decode('utf-8')

    result = output.split(':')[1].strip().replace(' ', '')
    result = bytes.fromhex(result).decode('utf-8')
    if result != 'Brute force my value 00 to ff':
        print("Flag might be: '{}', triggered by '{}'".format(result, val))
        break

Which eventually results in:

==> Trying d1
Flag might be: '933c1fcfa8ed52d2ec05', triggered by 'd1'

Flag 10, 0x003e: Learn about read and write speeds

Read me 1000 times

This approach takes about 92 seconds:

% for n in $(seq 1 1000); do ble_read_hnd 0x3e $n; done
[...]
Read me 1000 times
6ffcd214ffebdc0d069e

This script is slightly faster, although it seems it’s not caching the connection?

#!/usr/bin/env python3

# apt install python3-bluez python3-gattlib
from gattlib import GATTRequester

def main():
    blemac = '30:AE:A4:26:2B:E6'

    req = GATTRequester(blemac)

    for i in range(1000):
        resp = req.read_by_handle(0x3e)[0].decode()
        print(f'{str(i).zfill(4)} => {resp}')

if __name__ == '__main__':
    main()

Alternatively using BLE_GATT which explicitly forces a connect, but requires using the UUIDs for the handles. The UUIDs can be retrieved with gatttool 0000ff0c-0000-1000-8000-00805f9b34fb:

#!/usr/bin/env python3

import BLE_GATT

blemac = '30:AE:A4:26:2B:E6'
dev = BLE_GATT.Central(blemac)
dev.connect()

for i in range(1000):
    resp = dev.char_read('0000ff0b-0000-1000-8000-00805f9b34fb')
    resp_str = ''.join([chr(x) for x in resp])
    print(f'{str(i).zfill(4)} => {resp_str}')

dev.disconnect()

As it turns out this script is in fact a little bit slower with 95 seconds, but that’s within the margin of error.

Actually inspecting the traffic with Wireshark shows the first script (with gattlib) does in fact maintain a connection. Given the fact that library allows for reading handles by UUID and using their short 16-bit number makes it preferable to use.

Flag 11, 0x0040: Learn about single response notifications

Listen to me for a single notification

Finding the correct flags for gatttool took some fiddling, but it turns out that writing any byte to the handle did the trick:

% gatttool -b $blemac --listen --handle 0x40 --char-write-req --value 0x1 
Characteristic value was written successfully
Notification handle = 0x0040 value: 35 65 63 33 37 37 32 62 63 64 30 30 63 66 30 36 64 38 65 62 

The response decodes to the flag: 5ec3772bcd00cf06d8eb.

Flag 12, 0x0042: Learn about single response indicate

Listen to handle 0x0044 for a single indication

This was solved using the same command as with flag 11:

% gatttool -b $blemac --listen --handle 0x44 --char-write-req --value 0x1 
Characteristic value was written successfully
Indication   handle = 0x0044 value: 63 37 62 38 36 64 64 31 32 31 38 34 38 63 37 37 63 31 31 33 

The response decodes to: c7b86dd121848c77c113.

Alternatively this could be implemented in Python with gattlib:

#!/usr/bin/python3 -u
#
# Based on pygattlib/examples/receive_notification.py

import sys
from threading import Event
from gattlib import GATTRequester

BLE_MAC = '30:AE:A4:26:2B:E6'

class Requester(GATTRequester):
    def __init__(self, wakeup, *args):
        GATTRequester.__init__(self, *args)
        self.wakeup = wakeup

    def on_notification(self, handle, data):
        print("- notification on handle: {}\n".format(handle))
        self.wakeup.set()


class ReceiveNotification(object):
    def __init__(self, address):
        self.received = Event()
        self.requester = Requester(self.received, address, False)

        self.connect()
        self.wait_notification()

    def connect(self):
        print("Connecting...", end=' ')
        sys.stdout.flush()

        self.requester.connect(True)
        print("OK!")

    def wait_notification(self):
        self.requester.write_by_handle(0x44, "a")
        self.received.wait()


if __name__ == '__main__':
    ReceiveNotification(BLE_MAC)
    print("Done.")

Which results in the same flag:

% python3 receive_notification.py 
Connecting... OK!
on indication, handle: 0x44 -> 00:63:37:62:38:36:64:64:31:32:31:38:34:38:63:37:37:63:31:31:33:
^C

Flag 13, 0x0046: Learn about multi response notifications

Listen to me for multi notifications

With gatttool we can just keep listening to the handle:

% gatttool -b $blemac --listen --handle 0x46 --char-write-req --value 0x51
Characteristic value was written successfully
Notification handle = 0x0046 value: 55 20 6e 6f 20 77 61 6e 74 20 74 68 69 73 20 6d 73 67 00 00 
Notification handle = 0x0046 value: 63 39 34 35 37 64 65 35 66 64 38 63 61 66 65 33 34 39 66 64 
[...]

These decoded to:

  • U no want this msg
  • c9457de5fd8cafe349fd

Flag 14, 0x0048: Learn about multi response indicate

Listen to handle 0x004a for multi indications

Again with gatttool, this time we receive an indication:

% gatttool -b $blemac --listen --handle 0x4a --char-write-req --value 0x51
Characteristic value was written successfully                                                                
Indication   handle = 0x004a value: 55 20 6e 6f 20 77 61 6e 74 20 74 68 69 73 20 6d 73 67 00 00 
Indication   handle = 0x004a value: 62 36 66 33 61 34 37 66 32 30 37 64 33 38 65 31 36 66 66 61 

These decoded to:

  • U no want this msg
  • b6f3a47f207d38e16ffa

Flag 15, 0x004c: Learn about BT client device attributes

Connect with BT MAC address 11:22:33:44:55:66

With hcitool one can send HCI commands. As per this documentation from Murata there are two HCI commands which are relevant to us:

  • read BD_ADDR: ogf 0x04, ocf 0x0009
  • write BD_ADDR: ogf 0x3f, ocf 0x0001, argument: MAC address in reverse

OGF stands for OpCode Group Field and OCF for OpCode Command Field.

To read the address:

% sudo hcitool -i hci0 cmd 0x04 0x9
< HCI Command: ogf 0x04, ocf 0x0009, plen 0
> HCI Event: 0x0e plen 10
  01 09 10 00 42 42 de ad be ef 

hcidump while issuing this commands provides an easier to read representation:

< HCI Command: Read BD ADDR (0x04|0x0009) plen 0
> HCI Event: Command Complete (0x0e) plen 10
    Read BD ADDR (0x04|0x0009) ncmd 1
    status 0x00 bdaddr de:ad:be:ef:42:42

Writing the new MAC address however doesn’t seem to work as described:

 sudo hcitool cmd 0x3f 0x001 0x66 0x55 0x44 0x33 0x22 0x11
< HCI Command: ogf 0x3f, ocf 0x0001, plen 6
  66 55 44 33 22 11 
> HCI Event: 0x0f plen 4
  01 01 01 FC 

The address isn’t set when reading it back, and hcidump didn’t grok the command either:

< HCI Command: Vendor (0x3f|0x0001) plen 6
> HCI Event: Command Status (0x0f) plen 4
    Vendor (0x3f|0x0001) status 0x01 ncmd 1
    Error: Unknown HCI Command

Looking at a table published by TI it shows the OGF/OCF for reading the BD_ADDR, but there’s no mention of setting it. Digging deeper into the *BLUETOOTH SPECIFICATION Version 5.0, Vol 2, Part E, 5.4.1 HCI Command Packet states:

The OGF of 0x3F is reserved for vendor-specific debug commands.

Following along this blog I resorted to compiling Bluez' bdaddr tool.

Alas, that didn’t work either:

% ./tools/bdaddr -i hci0 11:22:33:44:55:66
Manufacturer:   Intel Corp. (2)
Device address: de:ad:be:ef:42:42 (Intel Corporate)

Unsupported manufacturer

I guess that means setting the client MAC address for this device won’t work and I have no other device to test it on at the moment. Regardless I learned quite a bit about OGF/OCF instead.

Flag 16, 0x004e: Learn about message sizes MTU

Set your connection MTU to 444

The obvious command to use is:

% gatttool -b $blemac --mtu 444 --char-read --handle 0x4e 

Yet that results in the same message.

With the bluepy library we can actually set the MTU correctly and obtain the flag:

#!/usr/bin/env python3

import bluepy.btle as btle

dev = btle.Peripheral('30:AE:A4:26:2B:E6')
dev.setMTU(444)

print(dev.readCharacteristic(0x4e).decode())

Playing a bit more with this library it’s trivial to enumerate all handles of this device:

#!/usr/bin/env python3

import bluepy.btle as btle

dev = btle.Peripheral('30:AE:A4:26:2B:E6')
services = dev.getServices()

for service in services:
    print(f'Service: {service.uuid}')

    for char in service.getCharacteristics():
        # properties are reported like: 'READ WRITE INDICATE '
        properties = char.propertiesToString()
        properties = ','.join(properties.split())
        handle = char.getHandle()

        if char.supportsRead():
            value = char.read().decode()
        else:
            value = '(N/A)'

        print(f'0x{handle:x} {properties.ljust(20)} {value}')

This results in a nice overview like this:

Service: 00001801-0000-1000-8000-00805f9b34fb
0x3 INDICATE             (N/A)
Service: 00001800-0000-1000-8000-00805f9b34fb
0x16 READ                 2b00042f7481c7b056c4b410d28f33cf
0x18 READ                 
0x1a READ                 
Service: 000000ff-0000-1000-8000-00805f9b34fb
0x2a READ                 Score: 0/20
0x2c READ,WRITE           Write Flags Here
0x2e READ                 d205303e099ceff44835
0x30 READ                 MD5 of Device Name
0x32 READ,WRITE           Write anything here
0x34 READ,WRITE           Write the ascii value "yo" here
0x36 READ,WRITE           Write the hex value 0x07 here
0x38 READ                 Write 0xC9 to handle 58
0x3a WRITE                (N/A)
0x3c READ,WRITE           Brute force my value 00 to ff
0x3e READ                 Read me 1000 times
0x40 READ,WRITE,NOTIFY    Listen to me for a single notification
0x42 READ                 Listen to handle 0x0044 for a single indication
0x44 READ,WRITE,INDICATE  Listen to handle 0x0044 for a single indication
0x46 READ,WRITE,NOTIFY    Listen to me for multi notifications
0x48 READ                 Listen to handle 0x004a for multi indications
0x4a READ,WRITE,INDICATE  Listen to handle 0x004a for multi indications
0x4c READ                 Connect with BT MAC address 11:22:33:44:55:66
0x4e READ                 b1e409e5a4eaf9fe5158
0x50 READ,WRITE           Write+resp 'hello'  
0x52 READ,WRITE           No notifications here! really?
0x54 BROADCAST,READ,WRITE,NOTIFY,EXTENDED,PROPERTIES So many properties!
0x56 READ                 md5 of author's twitter handle

Flag 17, 0x50: Learn about write responses

Write+resp ‘hello’

The key here is to use a write request with --char-write-req and not a regular write command with --char-write:

% gatttool -b $blemac --handle 0x50 --char-write-req --value=$(ble_to_hex hello)
Characteristic value was written successfully
% gatttool -b $blemac --handle 0x50 --char-read                                         
Characteristic value/descriptor: 64 34 31 64 38 63 64 39 38 66 30 30 62 32 30 34 65 39 38 30 00

Flag 18, 0x52: Hidden notify property

No notifications here! really?

As we saw earlier there are only READ and WRITE properties advertised on this handle, however the command below which listens for a notification does in fact work and decodes to our flag.

% gatttool -b $blemac --listen --handle 0x52 --char-write-req --value 0x1
Characteristic value was written successfully
Notification handle = 0x0052 value: 66 63 39 32 30 63 36 38 62 36 30 30 36 31 36 39 34 37 37 62 
^C

Flag 19, 0x54: Use multiple handle properties

So many properties!

As demonstrated above, there are a plethora of properties on this handle: BROADCAST READ WRITE NOTIFY EXTENDED PROPERTIES. So let’s start with a write request:

% gatttool -b $blemac --listen --handle 0x54 --char-write-req --value 0x1
Characteristic value was written successfully
Notification handle = 0x0054 value: 30 37 65 34 61 30 63 63 34 38 

That returns 10 bytes via a notification. Doing a read again on the handle returns another 10 bytes:

% gatttool -b $blemac --char-read --handle 0x54         
Characteristic value/descriptor: 66 62 62 39 36 36 39 35 38 66

Combining the two parts (read + write) yields the full flag.

Flag 20, 0x56: OSINT the author!

md5 of author’s twitter handle

Twitter handle is @hackgnar, take the first 20 characters of the MD5 sum for the flag:

% echo -n "@hackgnar" | md5sum | head -c 20