Open-source project
hackgnar/ble_ctf avatar
hackgnar/ble_ctf

BLE CTF turns an ESP32 into a GATT server you have to read carefully

A Bluetooth low energy capture the flag

784 stars120 forksCLicense varies

At a glance

What is it?
BLE CTF is a capture the flag built around an ESP32 running a GATT server, where every flag is behind a Bluetooth Low Energy handle that you read or write one at a time. The 30 challenges walk through the GATT concepts that matter in practice, from discovering handles to notification ordering and MTU limits, using hardware you own.
Who is it for?
Adopt BLE CTF if you want to learn Bluetooth Low Energy by touching a real GATT server rather than by reading about it, since the challenge order takes you from finding a device to handling queued long writes and MTU sizing. Skip it if you have no Bluetooth adapter and no spare microcontroller, because the setup assumes an ESP32 and a Linux machine with a Bluetooth controller or USB dongle.
Can I use it commercially?
Not without permission. GitHub finds no licence file in the repository, and without a licence all rights are reserved by default: you may read the code but not reuse it. Check the README, or ask the authors, before using it.
Is it still maintained?
Yes. The repository last received commits 79 days ago.
What is it written in?
Mainly C, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The goal is to be ready to fiddle with any GATT device

The stated purpose of BLE CTF is to teach the core concepts of Bluetooth Low Energy client and server interactions. The documentation is candid that it has also been built to be fun, but the intent is teaching and reinforcing the concepts needed to plunge into the world of Bluetooth hacking. The claim made at the end is that after completing the CTF you should have everything you need to start fiddling with any BLE GATT device you find.

That framing places it firmly on the right side of the line for security education. The target is a GATT server running on hardware you flash yourself, and the skills being taught are the protocol mechanics, not the tooling for attacking someone else's device.

The implementation is C, with the firmware built as an ESP32 project. The repository tree shows the Espressif build system: CMakeLists.txt, sdkconfig.defaults, and separate sdkconfig defaults for the esp32s3 and esp32c6 variants, plus a main/ directory holding the application code and a build/ directory. There is a Dockerfile based on the espressif/idf image at release-v5.5, which gives you the toolchain without installing it by hand, and a Vagrantfile with a vagrant_bluetooth_dev_setup.sh script for bringing up a Linux environment with Bluetooth support.

The last push was on 2026-08-07 and there is one release tag, v1.0 from 2023-06-27, so the project is complete and stable rather than actively changing.

Setup needs an ESP32, a Linux box and a way to reach the air

The setup requirements are listed plainly and they are the real cost of this challenge.

You need the pre-compiled firmware or the source code in the repository to build and flash an ESP32 with the CTF GATT server. The supported chips are esp32, esp32s3 and esp32c6. You need the microcontroller itself. And you need a Linux box with a Bluetooth controller or a Bluetooth USB dongle, with the note that OS X or Windows plus a Linux virtual machine works.

On the software side you need Bluetooth tools such as gatttool from bluez-tools, gratttool, which is the author's own project, or other BLE tools like hcitool.

The documentation for compiling and flashing your own firmware, or flashing the provided pre-compiled firmware, lives in a separate setup document. That separation is sensible, since flashing an ESP32 and setting up a Bluetooth-capable virtual machine are two different rabbit holes.

The repository has a helper script, gatttool_enum.sh, for enumerating GATT characteristics, and the repository also carries sdkconfig.example alongside the defaults files, which is what you edit when the stock configuration is not what your chip or board wants.

Two practical notes for anyone budgeting time. The firmware has to be flashed before there is anything to talk to, and the Linux side has to actually have a working Bluetooth controller, which on a virtual machine means USB passthrough rather than an emulated adapter.

Finding the device and reading your score

Before any flag can be submitted, you need the Bluetooth MAC address of the device. The documentation gives an example command for finding it with hcitool:

bash
sudo hcitool lescan

Once you have the address, you can talk to the device. The score is readable from handle 42, and the documentation shows the example command with a placeholder MAC address that you are told to replace with your own:

bash
gatttool -b de:ad:be:ef:be:f1 --char-read -a 0x002a|awk -F: '{print $2}'|tr -d ' '|xxd -r -p;printf '\n'

That command is worth reading rather than copying, because it shows the shape of every interaction in the challenge. gatttool is given a device address with -b, a characteristic handle with -a, and an operation. The output of gatttool is hex with colons, so it is piped through awk to take the value field, tr to strip the spaces, and xxd to convert the hex back into text.

That pipeline is the first real lesson. Bluetooth tool output is not readable by default, and the whole challenge is built on the idea that you have to read raw bytes and convert them yourself.

Flags are truncated MD5 sums written to handle 44

The flag format has a stated reason. All flags are MD5 sums truncated to 20 characters, and the explanation given is to avoid MTU limits imposed by some hardware.

You submit a flag to the GATT server on handle 44:

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

Two things are happening. The write uses --char-write-req, which is a write with response, and the value is converted to hex with xxd -ps before being sent, since BLE writes are binary. The handle 0x002c is the write handle, distinct from the 0x002a read handle used for the score.

One flag is a special case. Flag 1 is described as a gift that can only be obtained from reading the hint, which points at a separate document in the hints directory. That is a hint for the reader about where the documentation structure lives, and it doubles as a check that you are reading the docs rather than guessing handles.

The whole design is that the answer to each challenge is not a secret string but a technique. Flag 0x002e teaches you to read handles. Flag 0x002c is the handle you write to. The flags are named by the handle they live on, so the challenge list itself is a map of the GATT database.

Thirty flags, ordered from reading handles to write fuzzing

The challenge list is the best curriculum in the repository, because each entry names the concept it teaches and the handle it sits on.

The early ones are the fundamentals. Flag 0x002e is about learning how to read handles. Flag 0x0016 teaches discoverable device attributes. Flag 0x0032 is reading and writing to handles, 0x0034 the same for ASCII, and 0x0036 for hex. Flag 0x0038 teaches reading and writing to handles differently, which is about the different write primitives rather than different data.

Then it gets into the parts that trip people up. Flag 0x003c is about write fuzzing. Flag 0x003e covers read and write speeds. Flag 0x0040 and 0x0046 are single response and multi response notifications, and 0x0042 and 0x0048 are the indicate equivalents, so the four together separate the two axes of the GATT notification model.

Flag 0x004c is about BT client device attributes, and 0x004e is about message sizes and MTU, which connects directly back to why the flags are truncated to 20 characters. Flag 0x0052 is a hidden notify property, 0x0054 is about using multiple handle properties, and 0x0056 asks you to find information about the author, an open source intelligence task rather than a protocol one.

The late challenges are the most technical. Flag 0x005a is that a characteristic is more than its value. Flag 0x005d is that the server can see who you are, which is about the privacy angle. Flag 0x005f is that you do not always need to connect, pointing at advertising and broadcast data. Flag 0x0061 is about reading the error rather than just the failure, and 0x0063 is about finding things by UUID rather than by handle. Flag 0x0068 covers queued long writes, 0x006a that notifications are not ordered, and 0x006d that a value is not a fixed property of a handle. The last, 0x006f, is challenge and response.

What the challenge teaches that a spec does not

The reason to run this rather than read the Bluetooth specification is that the specification describes the protocol while the challenge reproduces the failure modes.

Three of the later flags are about things that only bite you in practice. Notifications being unordered means a client that assumes order will misbehave, and the flag makes you confront that. A value not being a fixed property of a handle is the rule that trips up anyone who treats a read as a field access. And read the error rather than just the failure is about the difference between an operation that returned a status code and one that silently did nothing.

Two more are worth singling out. Finding things by UUID instead of handle is the difference between a script that works and a script that breaks when the firmware is rebuilt, since handles are assigned and UUIDs are not. And the MTU flag connects the flag format to the transport, since a 20 character truncated value fits where a longer one might not on some hardware.

The other side of the challenge is that it is a bounded, safe target. The GATT server runs on a microcontroller you flashed, on your desk, with your own adapter. That is the difference between understanding notification semantics and probing someone else's fitness tracker, and it is the reason this challenge is worth the hardware cost.

One limitation is worth stating plainly. Everything here is a simulated server. The BLE CTF server implements the GATT behaviours the challenges test, so it teaches the protocol mechanics faithfully, but it does not reproduce the quirks of a specific commercial device, which is where the last mile of real device work happens.

Editorial conclusion

Adopt BLE CTF if you want to learn Bluetooth Low Energy by touching a real GATT server rather than by reading about it, since the challenge order takes you from finding a device to handling queued long writes and MTU sizing. Skip it if you have no Bluetooth adapter and no spare microcontroller, because the setup assumes an ESP32 and a Linux machine with a Bluetooth controller or USB dongle. Verify first that your adapter can scan and connect, that gatttool or a replacement is installed, and that the firmware variant matches your chip, since the tree carries separate sdkconfig defaults for the esp32, esp32s3 and esp32c6.

Frequently asked questions

What hardware do I need to run BLE CTF?

An ESP32 microcontroller, one of the esp32, esp32s3 or esp32c6, flashed with the CTF GATT server firmware, and a Linux box with a Bluetooth controller or Bluetooth USB dongle. OS X or Windows plus a Linux virtual machine also works.

How do I submit a flag in BLE CTF?

Flags are submitted to the GATT server on handle 44 by writing to handle 0x002c with gatttool, converting the flag to hex first. All flags are MD5 sums truncated to 20 characters, which the documentation says avoids MTU limits on some hardware.

What Bluetooth tools does BLE CTF need?

Bluetooth tools such as gatttool from bluez-tools, gratttool, which is the author's own project, or other BLE tools like hcitool. You use hcitool lescan to find the Bluetooth MAC address of the flashed device before you can read or write to it.

Official sources

  1. hackgnar/ble_ctf on GitHub
  2. Issues
  3. README
  4. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/hackgnar-ble-ctf.svg)](https://hysenlabs.com/projects/hackgnar-ble-ctf)

Was this useful?