BLE Hacking

I have recently been given a ESP32 with a BLE CTF on it!

I’m using a basic ESP32 board similar to this.

The CTF is available on GitHub.

I’ve set up a Raspberry Pi 3 which has built in bluetooth and have set a static IP so I am able to SSH into it from my kali box. I’m new to BLE so let’s jump in and see what happens!

Bluetooth Config

First up, we need to check our bluetooth connections, which work a bit like our network connections, but instead of ifconfig we use:

sudo hciconfig -a

We can see in the response that the interface is down. To put this up, we just need to do

sudo hciconfig hci0 up

Now the interface is up and we should be able to use this to do some fun stuffs with bluetooth!

Next we need to find out if it’s possible to see the device, there are a few different tools that can  be used, but for the moment I’m going to stick with the hcitool set. There is a hcitools which allows scanning of bluetooth low energy devices.

sudo hcitool lescan

This scans through and picks up any bluetooth devices around. What we are interested in here is the BLECTF06 device, with the MAC address of:


So we know the MAC address and can use this to connect to the device and try and interact with it.

There are again a number of tools, one of the most popular is bettercap, but I had endless issues with it, which I think was due to the fact my PI has no outbound connectivity and it wouldn’t allow using hci0 as an interface.

If anyone knows how to fix this, please send me the answer over twitter, i’d really appreciate it!

So instead of bettercap, I’m going to use the now depreciated gatttool.

Connecting to the Device

Gatttool has a great interactive mode:

gatttool -I

This provides a command line, doing a help provides some of the main commands:

We need to connect to the device, we can use the MAC address to do this. Once connected, we can get some information from the device, however gatttool doesn’t let me dump all the data, which I want to do.

The options like characteristics give us a lot of information, but not too useful at this point

Device Enumeration

To dump all the data in a readable format, a tool called bleah is needed (or bettercap also does it, if you can get it working!) As bleah has been deprecated I had to download it from a previous commit and download the zip, rather than doing a simple git clone.

Once that is installed, a look at the help menu gives us some hints on usage.

We want to identify the MAC address and enumerate the device.

bleah -b "24:0A:C4:9A:56:96" -e

This brings back a permission error, adding a sudo we get

In addition to this output, the device now has a blue light to indicate that something is connected to it.

So this is great, we now know all the challenges! I guess now is just a case of working through them!

Flag 1

So working down the list, we have the score location, then the location to write flags too.

The next looks like an MD5 hash, I assume that this is the first flag, so let’s try writing that hash to the 002c address.

I think this can be done with bleah, so let’s have a go, relooking at the help, we can write data to the characteritic UUID, I think this should be:

sudo bleah -b 24:0A:C4:9A:56:96 -n "0000ff02-0000-1000-8000-00805f9b34fb" -d "d205303e099ceff44835"

Unfortunately this is wrong, we get “Invalid Handle” so the long code isn’t the UUID.

I had the wrong flag, the “-n” flag is for the charactierstic handle, not the UUID. Tweaking the request we get:

sudo bleah -b 24:0A:C4:9A:56:96 -u "0000ff02-0000-1000-8000-00805f9b34fb" -d "d205303e099ceff44835"

Results in something happening!

I think this is flag 1 done, so we can read the score to see what we have. Doing a full enumeration again (as I’m not sure how to read certain parts in bleah) we get a result!

One flag down! 19 to go!

Flag 2

The second flag seems easy enough “MD5 of Alpha chars in Device Name” So the hostname is “BLECTF06” using cyberchef it’s easy to make a MD5 hash.

So the output we want is: 7ae1f3212f9c3fd33ec2e1040f436c31

Now as bleah has to connect and disconnect each time. It might be better to move back over to gatttools in interactive mode.

gatttool -I

connect  24:0a:c4:9a:56:96

Once connected, again looking at the help, it’s possible to write a handle address (which is the 4 byte location on the left hand side of the table).

char-write-req 002c 7ae1f3212f9c3fd33ec2e1040f436c31

This writes in the “submit flags here” handle of 002c with the MD5 hash of the name of the device.

Flag 2 has been submitted, let’s check the scoreboard and make sure it worked.

char-read-hnd 002a

We got data back, it looks like hex. Putting it into cyberchef we get the result of “Score:1 /20”

So, there are now 2 issues, I don’t want to put all that into cyberchef each time to get the score and submitting the flag didn’t work, or the flag was wrong.

Let’s try and sort out this input! I want to cut the output to data only after the “:” then decode it into plain text.

Trying any sort of awk or cut within interactive mode doesn’t appear to work.

So instead, i’ll need to make the full request outside of interactive mode, however the device is busy within interactive mode. So i’ll have to come out of interactive mode to read the flags!

Reading the guide a bit more, the command is slightly different to use gatttools outside of interactive mode and we need to use the hex value to reference the location, then using cut and xxd I get almost the entire answer (for some reason without the first S, but I know that’s an S so i’m ok with it)

gatttool -b 24:0A:C4:9A:56:96 --char-read -a 0x002a | cut -f3-13 -d ' ' | xxd -r

Onto the second issue, the previous attempt of MD5 hashing the device name didn’t appear to work!

This time, i’ll try to write the code without interactive mode, so we don’t have to keep entering and exiting that mode to get the blooming flags! I’ll also only send the letters MD5’ed.

gatttool -b 24:0A:C4:9A:56:96 --char-write -a 0x002c -n 5cd56d74049ae40f442ece036c6f4f06

After taking an absolute age to run, I cancelled it, not sure why that didn’t run!

Heading back into interactive mode, I could connect into the device, so there wasn’t a connectivity issue, maybe my write command was wrong. Looking at my commands during interactive mode, I think I forgot the “-req” part of the command.

gatttool -b 24:0A:C4:9A:56:96 --char-write-req -a "0x002c" -n "5cd56d74049ae40f442ece036c6f4f06"

This then worked immediately!

Again reading the score, still on 1/20. I wonder why that’s not worked.

As I’m having to decode the data as it’s coming out, do I need to put it into hex format to write it in?

Looking at the github page, this is the case, coupled with the MD5 hash is always only 20 characters long, so from the github page, the submit command is:

gatttool -b de:ad:be:ef:be:f1 --char-write-req -a 0x002c -n $(echo -n "some flag value"|xxd -ps)

So adding in the first 20 characters from the MD5 hash of BLECTF (we ignore the 06 for some reason) the final command is:

gatttool -b 24:0A:C4:9A:56:96 --char-write-req -a 0x002c -n $(echo -n "5cd56d74049ae40f442e" | xxd -ps)

Reading the score, we are now on 2!

Flag 3

The third flag asks to “write anything here” onto handle 0030. This should be straight forward!

gatttool -b 24:0A:C4:9A:56:96 --char-write-req -a 0x0030 -n "write anything here"

This specifies the data being sent, and the handle location.

Reading back to make sure it has been written, creates some problems!

The only way I know to fix that, is to exit the window, which in our case means coming off the ssh session and closing that tmux screen! Annoying!

Doing that and reading without the decoding doesn’t help!

pi@raspberrypi:~ $ gatttool -b 24:0A:C4:9A:56:96 --char-read -a 0x0030
Characteristic value/descriptor: 00 00 0e 0a 00 00 00 00 0e

Decoding that hex, gives us:



I re-enumated the device using bleah and put the table into a spreadsheet which gives me easy access to what’s on which row and I saw the issue. I was meant to write and read from 0032 not 0030!

Let’s try again!

That’s worked better this time!

This seemed to work, however the score was still 2! Looking closer at the MD5 hash, this is only 16 characters, not the 20 that was expected, earlier when I said the missing “S” wasn’t an issue. It’s just become an issue!

Doing it manually with Cyberchef, my theory is proved correct!

The difference in the MD5 hashes:



So the first character and last 3 characters are missing, how strange!

Going back to the github page, they have a better read command:

gatttool -b 24:0A:C4:9A:56:96 --char-read -a 0x0032|awk -F':' '{print $2}'|tr -d ' '|xxd -r -p;printf '\n'

This displays the full hash and on it’s own line, so life will be easier!

Flag 4

The 4th flag requests that we write the ASCII value of “yo” to handler 0034.

This should be similar to the above, however it will have to be converted to hex for it to work, so it’s submitted the same way as a flag.

gatttool -b 24:0A:C4:9A:56:96 --char-write-req -a 0x0034 -n $(echo -n "yo" | xxd -ps)

Reading the value then returns the MD5 hash which can be submitted.

Flag 5

This flag requires us to write the hex value of “0x07” to 0036. As the values that we input get converted to hex, we can skip that part and just put in the hex value directly.

I started off entering “0x07” but it’s worth remembering that the “0x” are just the prefix to inform everyone that the following value is in hex, therefore in this case it’s not needed.

gatttool -b 24:0A:C4:9A:56:96 --char-write-req -a 0x0036 -n 07

This is then accepted and reading the handle provides the MD5 hash!

Flag 6

Writing the hex 0xC9 to handle 58 is this challenge. Again we should be able to do the same as above, just changing the handle value.

gatttool -b 24:0A:C4:9A:56:96 --char-write-req -a 0x0058 -n C9
Characteristic Write Request failed: Invalid handle

However, an unexpected error, the handle doesn’t exist!

The handles we were provided in the enumeration maxed out at handle 56.

We are always providing the hex values for the handles, maybe the actual handle is the plain number.

gatttool -b 24:0A:C4:9A:56:96 --char-write-req -a 58 -n C9
Characteristic value was written successfully

This seems better so far!

Reading 0x0038 which was the location of the challenge has given us the MD5 hash.

So it’s important to note, the handle values are displayed and mostly used in hex from the enumeration, however pure values can be used.

Flag 7

The 7th flag sets an interesting challenge, to brute force values 00-ff.

I think I should be able to do an ascending loop to read each value. We know that:

Hex 00 = 0

Hex FF = 255

So if we can write a script to increment the number, convert it to hex within that range it should work. For this I’m going to have to learn some python, I am crap at coding, so let’s give this a bash.

We need a simple for incrementing loop which is converted into hex each time. Python contains a “hex” coder, so the test script is:

import os
for x in range (0,256):
        y = hex(x)
        print y

This seems to work! (The first time I did it, I only had my loop up to 255, so it didn’t do the last value, upping this to 256 made it work)

Next is adding in the os.system and gatttool command

import os
for x in range (0,256):
        y = hex(x)
        os.system("gatttool -b 24:0A:C4:9A:56:96 --char-read -a " + str(y))

This takes that converted value and adds it into the end of the command. I had a lot of issue with this, as I kept trying %y which resulted in a “TypeError: not all arguments converted during string formatting” issue, so huge shoutout to @AlexisBitsios on twitter for solving this one for me!

After running through it, I didn’t get the flag. Looking back at the test output, the hex is in 2 digit format, whereas we need 4 digit format e.g 0x001a rather than 0x1a.

It turns out this wasn’t too easy in Python and I had to add some extra bits in, so what I got to was:

import os
for x in range (0,256):
        y = ('0x00' + '{0:02X}'.format(int(x)))
        print y
        os.system("gatttool -b 24:0A:C4:9A:56:96 --char-read -a " + str(y))

Although this did as expected, it didn’t get us the flag.

So I’ve understood the question wrongly. I might need to brute force the value of 3c with the hex values from 00-ff rather than just enumerate the list.

This changes our script slightly, I also realised as it’s not locations, it doesn’t need the padding.

import os
for x in range (0,256):
        y = hex(x) 
        print y
        os.system("gatttool -b 24:0A:C4:9A:56:96 --char-write-req -n 0x00c3 -a " + str(y))

Running through this, it also didn’t work, I think now as it’s printing “0x0 – “0xff” whereas I probably only need “00” – “ff”, so again need a format change in python!

After a few more hours, there is a way to format the data, we want 2 digits of x and the data to be formatted, so tweaking the script, we get:

import os
for x in range (0,256):
        y = '{:02x}'.format(x)
        print y

The output of this is:

This looks good, exactly what we need. So adding back in the os.system call, it should cycle through each value bruteforcing the value!

However, it does not work! We get the help menu a lot with errors:

Cannot parse integer value ?ff? for -a

So, I guess anything with a letter, isn’t an integer so can’t be dealt with in this way. The flag we have on the gatttools command is -a, which is the handle and -n is the value. I had them the wrong way round in the script!!

Also as we are now doing the formatting earlier we don’t need the “str” value, so our final script is:

import os
for x in range (0,256):
        y = '{:02x}'.format(x)
        print y
        os.system("gatttool -b 24:0A:C4:9A:56:96 --char-write-req -a 0x00c3 -n " + y)

Running that through, the values are all written successfully, and the flag was obtained!

During this challenge, I did also learn that Python has a built in module for dealing with ble called blepy. I didn’t look into this, but might do once I’ve done all the other challenges!

Flag 8

Flag 8 requires us to read the handle of 003e 1000 times.

Similar to above, my python is still crap (and I still didn’t look up blepy), so I used a similar idea! Feel free to tweet me with an elegant solution!

import os
for x in range (1, 1000):
        os.system("gatttool -b 24:0A:C4:9A:56:96 --char-read -a 0x003e")

Once the script has run, it’s possible to read the value at 003e and we get:

Flag 9

This one has an interesting extra ability within the original enumeration table we saw.

│ 0040 │ ff0c ( 0000ff0c-0000-1000-8000-00805f9b34fb ) │ NOTIFY READ WRITE │ u'Listen to me for a single notification' │

All of the previous have been only included READ or WRITE. Seeing Notify is quite exciting. So what does notify mean?

Well it doesn’t appear to be very well explained anywhere but effectively it’s a push of data to the client.

The challenge itself to listen for a single notification might not require this notify characteristic, I’m not entirely sure. What I did find though, is there is a “–listen” flag, which can be used after a write, to listen for any responses from the device.

Using this, we create a command of:

gatttool -b 24:0A:C4:9A:56:96 --char-write-req -a 0x0040 -n value --listen

This is writing “value” onto the 40 handler, then listening for anything to be returned, which it was quite quickly.

Decoding this hex with cyberchef we get the flag!

Posting that in, we get another mark on that scoreboard!

Flag 10

This one wants us to listen for a single indication on the 44 handler. I assume an indication and notification are the same, or at least pretty similar, so we can use a similar command to the previous flag just changing the handler address.

Again the hex was decoded in cyberchef and posted back in for the score!

Flag 11

This challenge wants us to listen for multiple notifications on 0x0046.

Running the same command and leaving the command running brings back multiple values.

Decoding the first 2 lines with cyberchef, we get:

U no want this msg


Submitting that MD5 hash, we get another score on the door!

Flag 12

Again the same command was sent just with the different handler, letting it run bought back a number of data values!

These give similar messages to before, just with a different MD5 hash. Submit that and boom, flag 12 is done!

This is the end of the notify/indicate challenges, so what is the difference, as they are accessed using the same commands!

These are very similar things, however an indication requires a confirmation of the message, whereas the notify does not. This means that notifications are faster but if it doesn’t get received no-one knows. I guess it’s easiest to think of indication as similar to TCP compared to notify which acts more like UDP!

Flag 13

For this flag, we are asked to connect to the device using the MAC address: 11:22:33:44:55:66

So for this, we have to change our own MAC address, this shouldn’t be too difficult, there is a tool called bdaddr which allows us to do this on the raspberry pi. The issue is my pi doesn’t have an internet connection, so I need to download the tool here and scp it across to my pi

scp bdaddrtar.bz2 pi@

Then once the bz2 file is there, it just needs extracting:

bzip2 -d bdaddrtar.bz2 && tar xf bdaddrtar

Then we need to make the file

cd bdaddr && make

It then errors, because I don’t have the right dependencies:

sudo apt-get install libbluetooth-dev

Once all that is installed, the make runs through without an issue.

Doing a bit more research it looks like the command we need to change our bluetooth MAC address is:

sudo ./bdaddr -i hci0 -r 00:de:ad:be:ef:00

Running hciconfig -a we see that our address is hci0

The current MAC address is: B8:27:EB:4A:E9:C7

However we want it to be: 11:22:33:44:55:66

Our command will therefore be

sudo ./bdaddr -i hci0 -r 11:22:33:44:55:66

Running that, we get:

That looks pretty promising!

Checking that bluetooth still works, I run a lescan using hcitool:

Excellent, that appears to still be working.

However when trying to do a read with gatttools it appears to freeze and not do anything.

Heading into interactive mode, connection is successful to the device. However reading the data isn’t so good!

I’m not sure why this is, so I’m going to use the tool to change my MAC address back and see if I can read from there!

sudo ./bdaddr -i hci0 -r B8:27:EB:4A:E9:C7

The command again worked successfully, although the device address didn’t appear to have changed.

However, the device still doesn’t seem to be reading!

Just to do another check, I re-ran the bleah enumeration command to see if that worked and it connected without an issue, but wasn’t able to enumerate the device.

As the bluetooth was built into the Pi, I couldn’t pull out the dongle to reset it, so I tried what I could with an ethernet connection, I downed the interface and bought it back up.

After that, trying to do a read worked without an issue, and as we had connected to the device via interactive mode. The MD5 hash was returned!

I’m not sure why the device wasn’t readable after the MAC address was changed, or even when it was changed back. If anyone knows more about this, feel free to let me know via twitter!

Flag 14

This flag requires the user to change the MTU (maximum transmission unit) of the bluetooth device to 444. This MTU values controls the lengths of packets that can be sent and received to the device, being able to specific a maximum length could help with limiting the amount of data that is sent to the device.

Gatttools is able to change this MTU value via the interactive option.

gatttools -I

Once in interactive mode, we need to connect to the device

connect 24:0A:C4:9A:56:96

Then once connected, there is a command to change the MTU that I found via the help!

Once the MTU was changed, reading the value at this challenge of 004e returns the flag!


Flag 15

This challenge requests “Write+Resp ‘hello'”. So I assume it’s a write challenge, the handle doesn’t have notify or indicate so it’s not requiring us to use the –listen flag.

Instead let’s try doing a simple write of the word hello, then a read of the data.

gatttool -b 24:0A:C4:9A:56:96 --char-write-req -a 0x0050 -n $(echo -n "hello" | xxd -ps)

Then reading the handle:

gatttool -b 24:0A:C4:9A:56:96 --char-read -a 0x0050|awk -F':' '{print $2}'|tr -d ' '|xxd -r -p;printf '\n'

Oh, it appears to have returned the flag. I didn’t expect that to work, it seemed too similar to the first few tasks. Never mind, submitting that gives me a new high score!

Flag 16

Well well well, this flag’s clue is very cryptic “No notifications here! really?”. What does that mean?

I guess first up, let’s read it and confirm that’s the case. Reading it gives the same result, when trying to write to it, the write is successful but the following read again returns the same message. Not too sure what this means!

So it turns out, it was a lie! There are notifications! Sending data with the –listen flag returned some hex value that when decoded, was the flag!

Submitting that means we are one step closer to all the flags!

Flag 17

This one looks interesting, the clue is “so many properties” and there really are we have:

  • notify
  • broadcast
  • read
  • write
  • extended properties

So let’s start with what we know, we can read handles so let’s try that

That has just returned the same data!

The same happens if we just do a plain write to the value and a read after.

So instead if we try to do a notify, so write some data with the –listen flag:

Success, that looks half of the flag!

What if we then try another read of the data, now that we have written something to it?

Decoding that hex and we get:

That looks like other half of a flag! Excellent!

Putting these together took 2 goes to get right! But submitting the flag and checking the score, another flag down!

Flag 18

The final challenge is an MD5 of the authors twitter handle!

A quick look on Github provides the website

From there his twitter handle is present as @hackgnar

Using cyberchef to make an MD5 of that, we get:


The first 20 characters which all flags are is:


Submitting that as the flag, and there it is!

But wait, only 18 flags have been completed!

What the heck have I missed?!

Flag 1 again (but for us 19!)

Going back to the github page, there is a sneaky first flag for reading the instructions (which I didn’t do!)

The flag is here:

Submitting that gives you an extra point!

But we are still missing one!

Flag 20!

Looking through the guide again, it appears that there might be 2 flags available for MD5ing the device name. The device name is BLECTF which we entered, however looking at the enumeration there was a hidden flag under the device name data!

I’m not sure how I missed that and I’m sure I was meant to do it earlier! But never mind, we made it!

All flags collected! Excellent!

Final Thoughts

This was a great introduction to BLE, from knowing nothing about it and not even being able to connect at the start, I now feel confident in reading, writing and looking at data on BLE which I have no doubt will be useful in the future!

I would like to thank Ross Mark’s who has also done a walkthrough which I glanced at when stuck, especially in the early stages before I fully understood what was going on!

Hopefully you managed to follow this blog. It is a bit all over the place as it was written while doing, with all of the wrong turns included! Overall this took me a few hours over a number of days (with the Score resetting to 0 each time I unplugged the device) with a lot of research and googling in the background, so don’t be disheartened if parts are confusing or you don’t fully get it yet, you aren’t meant to!

Remember, Knowing stuff is cool, but learning just takes longer!

Leave a Reply

Your email address will not be published. Required fields are marked *