BLE_CTF write up
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