Bluetooth Low Energy CTF - Write up
Mardi 3 Juillet 2018 à 20:32

I’ve been interested in Bluetooth Low Energy for some time now, but never took the time to really dive into it or test my own devices. Then I came upon this article which describes a Bluetooth Low Energy CTF which runs on a ESP32. I decided to take my shot at it, and here is the write-up.

Getting the CTF to run

I hav accumulated a few electronic boards over the year, and I happen to have bought a FiPy from Pycom this year, which is a development board supporting 5 different networks : LoRa, Bluetooth, WiFi, Sigfox and LTE-M (NB-IoT and Cat M1). It runs on the ESP32, so I could re-flash it to run the CTF.

First, it is necessary to install the Espressif toolchain for compiling and installing the firmware. Original instructions are here. On Arch, it is possible to install the packages required and the compiler with AUR:

sudo pacman -S gcc git make ncurses flex bison gperf python2-pyserial
pacaur -S gcc-xtensa-esp32-elf-bin

Installing the APIs and libraries necessary for building ESP32 applications amounts to cloning the repository and positioning an environment variable :

cd ~/esp
git clone --recursive https://github.com/espressif/esp-idf.git
echo "export IDF_PATH=~/esp/esp-idf" >> ~/.bashrc
source ~/.bashrc

Now, the firmware for the CTF can be built and flashed. In order to flash it on the FiPy, it must be put in firmware update mode.

  • Unplug the board
  • Connect a cable from G23 to GND
  • Plug the board

Now it is ready to receive a brand new firmware ! But let’s build it first.

git clone https://github.com/hackgnar/ble_ctf.git
cd ble_ctf
make menuconfig # Make sure the serial device entered is the ESP32 etc
make
# The user must be allowed to write on the serial console.
# On some distros, it means being part of the group uucp or dialout
make flash

Now, unplug and replug the board, the CTF should be running.

Tooling

Now, we need to talk BLE, several tools are available to do that. I’m far from having an extensive knowledge in BLE, so I learned gatttool during this CTF. While reading, I also saw the hci* suite of tools (hciattach, hcidump, hcitool, …), bluetoothctl and bleah. I installed the Android app nrfConnect, which can be used to easily scan around and talk to devices (provided you know what to tell them). Part of the CTF can be done using it, but some challenges require brute-forcing a few hundreds of values.

Finally, some libraries are able to communicate in BLE. Golang has gatt, Python has bluepy, Node.js has noble/bleno, …

On the hardware side, I used the bluetooth interface embedded in my laptop, but cheap dongles exist aswell.

In order to scan for BLE devices around, you have to start the bluetooth daemon, activate the device and start the scan.

$ sudo systemctl start bluetooth
$ sudo hciconfig hci0 up
$ sudo hcitool lescan
LE Scan ...
30:AE:A4:2A:54:8A BLECTF
30:AE:A4:2A:54:8A (unknown)
# ...

Bluetooth Low Energy devices send beacons, which enables anyone around to see that they are present and to know some information about them. This is the information seen with hcitool lescan As seen, the BLE CTF is running on the device which has mac address 30:AE:A4:2A:54:8A, which confirms that is has been properly flashed.

First steps with GATT

The author of the CTF provides the command to submit the first flag and to see the score. Before that, a short introduction to GATT.

GATT is the protocol used to communicate with BLE devices. The device exposes a server, and the computer or phone that connects to it is the client. The server exposes services, which contain characteristics, which have a value and possibly some descriptors. This is organised as such :

BLE server
    |-- Service
    |      |-- Characteristic
    |      |      |-- Value
    |      |      |-- Descriptor
    |      |      |-- Descriptor
    |      |     ...
    |      |-- Characteristic
    |      |      |-- Value
    |      |      |-- Descriptor
    |      |      |-- Descriptor
    |      |     ...
    |     ...
    |-- Service
    |      |-- Characteristic
    |      |      |-- Value
    |      |      |-- Descriptor
    |      |      |-- Descriptor
    |      |     ...
    |      |-- Characteristic
    |      |      |-- Value
    |      |      |-- Descriptor
    |      |      |-- Descriptor
    |      |     ...
    |     ...
   ...

The client, after connection, can READ or WRITE the value of a characteristic. There are two additional operations, that are NOTIFY and INDICATE.

Using gatttool, we can enumerate the services and their characteristics:

MAC=30:AE:A4:2A:54:8A
gatttool -b $MAC --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
# Services whose UUID start with 00001801 and 00001800 are special values defined in the norm. The other is a custom one which holds the CTF

gatttool -b $MAC --characteristics
handle = 0x0002, char properties = 0x20, char value handle = 0x0003, uuid = 00002a05-0000-1000-8000-00805f9b34fb
handle = 0x0015, char properties = 0x02, char value handle = 0x0016, uuid = 00002a00-0000-1000-8000-00805f9b34fb
handle = 0x0017, char properties = 0x02, char value handle = 0x0018, uuid = 00002a01-0000-1000-8000-00805f9b34fb
handle = 0x0019, char properties = 0x02, char value handle = 0x001a, uuid = 00002aa6-0000-1000-8000-00805f9b34fb
handle = 0x0029, char properties = 0x02, char value handle = 0x002a, uuid = 0000ff01-0000-1000-8000-00805f9b34fb
handle = 0x002b, char properties = 0x0a, char value handle = 0x002c, uuid = 0000ff02-0000-1000-8000-00805f9b34fb
handle = 0x002d, char properties = 0x02, char value handle = 0x002e, uuid = 0000ff03-0000-1000-8000-00805f9b34fb
handle = 0x002f, char properties = 0x02, char value handle = 0x0030, uuid = 0000ff04-0000-1000-8000-00805f9b34fb
handle = 0x0031, char properties = 0x0a, char value handle = 0x0032, uuid = 0000ff05-0000-1000-8000-00805f9b34fb
handle = 0x0033, char properties = 0x0a, char value handle = 0x0034, uuid = 0000ff06-0000-1000-8000-00805f9b34fb
handle = 0x0035, char properties = 0x0a, char value handle = 0x0036, uuid = 0000ff07-0000-1000-8000-00805f9b34fb
handle = 0x0037, char properties = 0x02, char value handle = 0x0038, uuid = 0000ff08-0000-1000-8000-00805f9b34fb
handle = 0x0039, char properties = 0x08, char value handle = 0x003a, uuid = 0000ff09-0000-1000-8000-00805f9b34fb
handle = 0x003b, char properties = 0x0a, char value handle = 0x003c, uuid = 0000ff0a-0000-1000-8000-00805f9b34fb
handle = 0x003d, char properties = 0x02, char value handle = 0x003e, uuid = 0000ff0b-0000-1000-8000-00805f9b34fb
handle = 0x003f, char properties = 0x1a, char value handle = 0x0040, uuid = 0000ff0c-0000-1000-8000-00805f9b34fb
handle = 0x0041, char properties = 0x02, char value handle = 0x0042, uuid = 0000ff0d-0000-1000-8000-00805f9b34fb
handle = 0x0043, char properties = 0x2a, char value handle = 0x0044, uuid = 0000ff0e-0000-1000-8000-00805f9b34fb
handle = 0x0045, char properties = 0x1a, char value handle = 0x0046, uuid = 0000ff0f-0000-1000-8000-00805f9b34fb
handle = 0x0047, char properties = 0x02, char value handle = 0x0048, uuid = 0000ff10-0000-1000-8000-00805f9b34fb
handle = 0x0049, char properties = 0x2a, char value handle = 0x004a, uuid = 0000ff11-0000-1000-8000-00805f9b34fb
handle = 0x004b, char properties = 0x02, char value handle = 0x004c, uuid = 0000ff12-0000-1000-8000-00805f9b34fb
handle = 0x004d, char properties = 0x02, char value handle = 0x004e, uuid = 0000ff13-0000-1000-8000-00805f9b34fb
handle = 0x004f, char properties = 0x0a, char value handle = 0x0050, uuid = 0000ff14-0000-1000-8000-00805f9b34fb
handle = 0x0051, char properties = 0x0a, char value handle = 0x0052, uuid = 0000ff15-0000-1000-8000-00805f9b34fb
handle = 0x0053, char properties = 0x9b, char value handle = 0x0054, uuid = 0000ff16-0000-1000-8000-00805f9b34fb
handle = 0x0055, char properties = 0x02, char value handle = 0x0056, uuid = 0000ff17-0000-1000-8000-00805f9b34fb

It seems a bit daunting at first to see all those hexadecimal strings, without understanding anything, but really it is possible to see them as pieces of data which you can read or write, with the server and the client agreeing on the role of each one.

As mentioned in the documentation, the score is available by reading the characteristic with handle 42 (0x2a).

$ gatttool -b $MAC --char-read -a 0x002a
Characteristic value/descriptor: 53 63 6f 72 65 3a 20 30 2f 32 30
# Yes, this is the score in hexa

# A small utility to pretty print the score at will :
$ cat <<EOF > score.sh
#!/bin/bash
gatttool -b $MAC --char-read -a 0x002a | awk -F':' '{print \$2}' | tr -d ' ' | xxd -r -p; printf '\n'
EOF
$ chmod u+x score.sh

$ ./score.sh
Score: 0/20

To submit a flag, it must be written to the characteristic with handle 44 (0x2c). The first flag is given in the documentation, it is 3132333435363738393031323334353637383930 (two times “1234567890” in hexa).

# Submit the flag
$ gatttool -b $MAC --char-write-req -a 0x002c -n 3132333435363738393031323334353637383930
# Check the score
$ ./score.sh
Score:1 /20

# Easy submit
$ cat <<EOF > submit.sh
#!/bin/bash
gatttool -b $MAC --char-write-req -a 0x002c -n $(echo -n "\$1" | xxd -ps)
EOF

Challenges

19 challenges remaining, the workflow will always be the same : Read the characteristic, then follow the instructions to get the flag. The list of handles, in order, is :

Flag 2

Easy enough, you just have to read handle 0x002e:

# Yet another utility script
$ cat <<EOF > read_txt.sh
#!/bin/bash
gatttool -b $MAC --char-read -a \$1 | awk -F':' '{print $2}' | tr -d ' ' | xxd -r -p; printf '\n'
EOF

$ chmod u+x read_txt.sh

$ ./read_txt.sh 0x002e
d205303e099ceff44835
$ ./submit d205303e099ceff44835 && ./score.sh
Score:2 / 20

Flag 3

$ ./read_txt.sh 0x0030
MD5 of Device Name

$ ./submit.sh $(echo -n "BLECTF" | md5sum) && ./score.sh
Score:3 / 20

Flag 4

# This challenge requires to read the name of the device
# It is exposed under service 0x1800, characteristic uuid 0x2a00-*
# It also has handle 0x0016 on this device
$ ./read_txt.sh 0x0016
2b00042f7481c7b056c4b410d28f33cf
$ ./submit.sh 2b00042f7481c7b056c4b410d28f33cf && ./score.sh
Score:4 / 20

Flag 5

$ ./read_txt.sh 0x0032
Write anything here

# Easy enough, we'll just write a dummy value
$ gatttool -b 30:AE:A4:2A:54:8A --char-write-req -a 0x0032 -n $(echo -n "More unicorns" | xxd -ps)

# Re-reading the characteristic yields the flag, which can be submitted
$ ./read_txt.sh 0x0032
3873c0270763568cf7aa
$ ./submit.sh 3873c0270763568cf7aa && ./score.sh
Score:5 / 20

Flag 6

$ ./read_txt.sh 0x0034
Write the ascii value "yo" here

$ gatttool -b $MAC --char-write-req -a 0x0034 -n $(echo -n "yo" | xxd -ps)
$ ./read_txt.sh 0x0034
c55c6314b3db0a6128af

$ ./submit.sh $(./read_txt.sh 0x0034) && ./score.sh
Score:6 / 20

Flag 7

$ ./read_txt.sh 0x0036
Write the hex value 0x07 here

$ gatttool -b $MAC --char-write-req -a 0x0036 -n 07

$ ./submit.sh $(./read_txt.sh 0x0036) && ./score.sh
Score:7 /20

Flag 8

$ ./read_txt.sh 0x0038
Write 0xC9 to handle 58

# Just some conversions here
$ gatttool -b $MAC --char-write-req -a 0x00`printf "%x" 58` -n C9

$ ./submit.sh $(./read_txt.sh 0x0038) && ./score.sh
Score:8 /20

Flag 9

$ ./read_txt.sh 0x003c
Brute force my value 00 to ff

# We have to try all values for this, again bash is a friend
for i in {1..255};
do
    gatttool -b $MAC --char-write-req -a 0x003c -n `printf "%x" $i`
done
# Wait a bit ...
# And we can grab the flag !

$ ./submit.sh $(./read_txt.sh 0x003c) && ./score.sh
Score:9 /20

Flag 10

$ ./read_txt.sh 0x003e
Read me 1000 times

for i in {1..1000}
do
    ./read_txt.sh 0x003e
done

# Again, we wait
# At some point, we will see the flag (it appears before
# the 1000th read).

$ ./submit.sh 6ffcd214ffebdc0d069e && ./score.sh
Score:10/20

Flag 11

$ ./read_txt.sh 0x0040
Listen to me for a single notification

# Notifications are enabled when writing a special value on the
# characteristic and using the --listen flag on gatttool
$ gatttool -b $MAC -a 0x0040 --char-write-req --value=0100 --listen
Notification handle = 0x0040 value: 35 65 63 33 37 37 32 62 63 64 30 30 63 66 30 36 64 38 65 62

$ ./submit.sh `echo "35 65 63 33 37 37 32 62 63 64 30 30 63 66 30 36 64 38 65 62" | tr -d ' ' | xxd -r -p; printf '\n'` && ./score.sh
Score:11/20

Flag 12

$ ./read_txt.sh 0x0042
Listen to handle 0x0044 for a single indication

# Again, indications are triggered by writing a special value to the
# characteristic and using the --listen flag
# ( I must admit I haven't sought the difference between notifications and indications)
$ gatttool -b $MAC -a 0x0044 --char-write-req --value=0200 --listen
Indication   handle = 0x0044 value: 63 37 62 38 36 64 64 31 32 31 38 34 38 63 37 37 63 31 31 33

$ ./submit.sh `echo "63 37 62 38 36 64 64 31 32 31 38 34 38 63 37 37 63 31 31 33" | tr -d ' ' | xxd -r -p; printf '\n'` && ./score.sh
Score:12/20

Flag 13

$ ./read_txt.sh 0x0046
Listen to me for multi notifications

# Same as before, but waiting for several notifications to come

$ gatttool -b $MAC -a 0x0046 --char-write-req --value=0100 --listen
Notification handle = 0x0046 value: 55 20 6e 6f 20 77 61 6e 74 20 74 68 69 73 20 6d 73 67 00 00 # Decodes to "U no want this msg"
Notification handle = 0x0046 value: 63 39 34 35 37 64 65 35 66 64 38 63 61 66 65 33 34 39 66 64 # Flag
...

$ ./submit.sh `echo "63 39 34 35 37 64 65 35 66 64 38 63 61 66 65 33 34 39 66 64" | tr -d ' ' | xxd -r -p; printf '\n'` && ./score.sh
Score:13/20

Flag 14

$ ./read_txt.sh 0x0048
Listen to handle 0x004a for multi indications

$ gatttool -b $MAC -a 0x004a --char-write-req --value=0200 --listen
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
...

$ ./submit.sh `echo "62 36 66 33 61 34 37 66 32 30 37 64 33 38 65 31 36 66 66 61" | tr -d ' ' | xxd -r -p; printf '\n'` && ./score.sh
Score:14/20

Flag 15

$ ./read_txt.sh 0x004c
Connect with BT MAC address 11:22:33:44:55:66

# Apparently changing the MAC address can be done using bdaddr:
# `bdaddr -r 11:22:33:44:55:66`
$ bdaddr -r 11:22:33:44:55:66
Manufacturer:   Intel Corp. (2)
Device address: XX:XX:XX:XX:XX:XX (Intel Corporate)

Unsupported manufacturer

# As I can't change it and legitimately solve the challenge,
# I cheated this one and looked directly in the source code of the CTF
$ ./submit.sh aca16920583e42bdcf5f && ./score.sh
Score:15/20

Flag 16

$ ./read_txt.xh 0x004e
Set your connection MTU to 444

# There is a --mtu flag in gatttool which didn't change anything, so
# I started the interactive shell for this one
$ gatttool -b $MAC --interactive
[   ][30:AE:A4:2A:54:8A][LE]> help
help                                           Show this help
exit                                           Exit interactive mode
quit                                           Exit interactive mode
connect         [address [address type]]       Connect to a remote device
disconnect                                     Disconnect from a remote device
primary         [UUID]                         Primary Service Discovery
characteristics [start hnd [end hnd [UUID]]]   Characteristics Discovery
char-desc       [start hnd] [end hnd]          Characteristics Descriptor Discovery
char-read-hnd   <handle> [offset]              Characteristics Value/Descriptor Read by handle
char-read-uuid  <UUID> [start hnd] [end hnd]   Characteristics Value/Descriptor Read by UUID
char-write-req  <handle> <new value>           Characteristic Value Write (Write Request)
char-write-cmd  <handle> <new value>           Characteristic Value Write (No response)
sec-level       [low | medium | high]          Set security level. Default: low
mtu             <value>                        Exchange MTU for GATT/ATT

[   ][30:AE:A4:2A:54:8A][LE]> connect
[CON][30:AE:A4:2A:54:8A][LE]> mtu 444
MTU was exchanged successfully: 444
[CON][30:AE:A4:2A:54:8A][LE]> char-read-hnd 0x004e
Characteristic value/descriptor: 62 31 65 34 30 39 65 35 61 34 65 61 66 39 66 65 35 31 35 38
^D

$ ./submit.sh `echo "62 31 65 34 30 39 65 35 61 34 65 61 66 39 66 65 35 31 35 38" | tr -d ' ' | xxd -r -p; printf '\n'` && ./score.sh
Score:16/20

Flag 17

$ ./read_txt.sh 0x0050
Write+resp 'hello'

# Notice the difference, with the former, here we use --char-write-req
# With char-write, we perform a Write Command and don't expect a response from the server
# With char-write-req, we perform a Write Request and expect a response from the server
$ gatttool -b $MAC -a 0x0050 --char-write-req --value=$(echo -n 'hello' | xxd -p)
d41d8cd98f00b204e980

$ ./submit.sh d41d8cd98f00b204e980 && ./score.sh
Score:17/20

Flag 18

$ ./read_txt.sh 0x0052
No notifications here! really?

# This characteristic advertises the properties 0x0a according to gatttool
$ gatttool -b $MAC --characteristics | grep 0x0052
handle = 0x0051, char properties = 0x0a, char value handle = 0x0052, uuid = 0000ff15-0000-1000-8000-00805f9b34fb

# This field is a bitmap which holds the supported operations. It decodes as such:
# 0x0a = 0b00001010
#          .......1 : Broadcast
#          ......1. : Read
#          .....1.. : Write without response
#          ....1... : Write with response
#          ...1.... : Notify
#          ..1..... : Indicate
#          .1...... : Authenticated Signed Writes
#          1....... : Extended Properties
# As for the solution, it's the same than for flag 11 and  13
$ gatttool -b $MAC -a 0x0052 --char-write-req --value=0100 --listen
Notification handle = 0x0052 value: 66 63 39 32 30 63 36 38 62 36 30 30 36 31 36 39 34 37 37 62
$ ./submit.sh `echo "66 63 39 32 30 63 36 38 62 36 30 30 36 31 36 39 34 37 37 62" | tr -d ' ' | xxd -r -p; printf '\n'` && ./score.sh
Score:18/20

Flag 19

$ ./read_txt.sh 0x0054
So many properties!

# The flag is scattered among the properties so we have to reconstruct it
# First half
$ gatttool -b $MAC -a 0x0054 --char-write-req --value=1122
$ ./read_txt.sh 0x0054
fbb966958f

# Second half
$ gatttool -b $MAC -a 0x0054 --char-write-req --value=0100 --listen
Notification handle = 0x0054 value: 30 37 65 34 61 30 63 63 34 38
# 07e4a0cc48

$ ./submit.sh fbb966958f07e4a0cc48 && ./score.sh
Score:19/20

Flag 20

$ ./read_txt.sh 0x0056
md5 of author's twitter handle

# Thanks hackgnar !
$ ./submit.sh $(echo "@hackgnar" -n | md5sum) && ./score.sh
Score:20/20 # Yay !

Wrap-up

I really liked this CTF. Though not very difficult, it was very didactic. It helps taking a first peek at existing tools and how BLE devices work in general.

Also, as the source code is open, it is possible to hack it to create its own challenges :)


Back to posts