Cactuscon 12 Badge Reverse Engineering
Hello and welcome to the reverse engineering guide of the CactusCon 12 (2024) badge.
The badge uses an ESP32-S3-WROOM-1 chip.
The data sheet can be found here: espressif.com/sites/default/files/documentation/esp32-s3-wroom-1_wroom-1u_datasheet_en.pdf
The chip is programmable through either ESP-IDF or PlatformIO in Visual Studio Code.
Presented here are the steps in the order they were revealed/figured out.
Note: These are the steps that I executed on a Mac. Some adjustment may be necessary for *nix and Windows.
Reading the badge
Start by turning on the badge and plugging it into your computer with a USB-C cable that supports data transfer. Most cables that come with smartphones or cost more than $10 should work.
Note: If you want to connect the badge to WiFi you will need to broadcast an SSID with the name ‘CactusCon’ and the password ‘Cactus2024!’. This can be done either through a WiFi router or a personal hotspot on a smart phone.
To confirm that the badge is found on your computer you can run the following command in your terminal which will list all USB serial devices connected to your computer:
ls /dev/cu.usb*
You should see output similar to the following:
/dev/cu.usbserial-1410
The numbers in usbserial-1410
will be different based on the USB port that you plug your badge into, so I would recommend using the same USB port for convenience and to avoid errors.
The badge program can be initially read using Python with the following script that was generated by asking ChatGPT for “a Python script to read the serial port of a USB device”. I added argparse
for the port
and baud_rate
, for more rapid testing.
Here is the script:
Note: You will need install pyserial with
pip install pyserial
before running the script.
import argparse
import serial
import time
parser = argparse.ArgumentParser(description='Read data from a serial port.')
parser.add_argument('port', help='Serial port name (e.g., /dev/cu.usbserial-XXXX)')
parser.add_argument('--baud', type=int, default=9600, help='Baud rate (default: 9600)')
args = parser.parse_args()
print("starting ...")
serial_port = serial.Serial(args.port, args.baud)
try:
while True:
try:
raw_data = serial_port.readline()
decoded_data = raw_data.decode('utf-8', 'replace').rstrip()
print(f"{decoded_data}")
except UnicodeDecodeError as e:
print(f"Error decoding data: {e}")
error_message = str(e)
print(f"Error Message: {error_message}")
except Exception as e:
print(f"An unexpected error occurred: {e}")
exit(1)
except KeyboardInterrupt:
serial_port.close()
I saved this to a file called read_badge.py
.
You can now run the script using the following command in your terminal (where “XXXX” is replaced with the numbers generated by your computer):
python read_badge.py --baud 115200 /dev/cu.usbserial-XXXX
If you did not set up an SSID for the badge beforehand
After running the script, resetting the badge, and waiting a little bit, your output will look like the following:
starting
[Wifi] Connecting to WiFi......................... failed!
connected![AWSReg] Loaded client keys
88.640625
[ 27431][E][WiFiGeneric.cpp:1476] hostByName(): DNS Failed for a2d0832l0cyiow-ats.iot.us-east-1.amazonaws.com
[ 27432][E][WiFiClientSecure.cpp:135] connect(): start_ssl_client: -1
[AWSMQTT] Failed connect, retrying.
(abbreviated for brevity)
[AWSMQTT] Connect to MQTT failed!
[AWSMQTT] Failed subscribe to global/indicators![Main] Offline mode. You must restart your device to reconnect.
This is not a problem for now and I will mention when you will need WiFi to progress further.
If you set up an SSID beforehand
Your output will look like the following:
starting ...
[AWSMQTT] MQTT connected as cc12-ABCDEF123456
[AWSMQTT] Subscribed to INGSOC/citizen/cc12-ABCDEF123456
[EventHandler] Got a message on topic INGSOC/citizen/cc12-ABCDEF123456
is device 1 INGSOC/citizen/cc12-ABCDEF123456
[EventHandler] Restarting advertising with new hmac GEvJFQvwazHCZ88/cLNRJmOVqDU=
[EventHandler] Got new advert (len 20 28 and res 0)... 18:4b:c9:15:b:f0:6b:31:c2:67:cf:3f:70:b3:51:26:63:95:a8:35:
Advertisement: 434312184BC9150BF06B31C267CF3F70B351266395A835303B8A0000000000
[TamperFault]: reporting invalid device status to ministry: 15242
[AWSMQTT] Publishing advertisement
[EventHandler] Got a message on topic global/indicators
::::::::::::::::::::::::::::::is device 0 INGSOC/citizen/cc12-ABCDEF123456
[EventHandler] Got a message on topic INGSOC/citizen/cc12-ABCDEF123456
is device 1 INGSOC/citizen/cc12-ABCDEF123456
[EventHandler] Server response <received>: Processed 184BC9150BF06B31C267CF3F70B351266395A835!
More output will be generated over time as the badge sends and receives messages from MQTT, more on that later on.
On the back of the badge hold Reset
and press Boot
.
starting ...
ESP-ROM:esp32s3-20210327
Build:Mar 27 2021
rst:0x1 (POWERON),boot:0x8 (SPI_FAST_FLASH_BOOT)
SPIWP:0xee
mode:DIO, clock div:1
load:0x3fce3808,len:0x44c
load:0x403c9700,len:0xbe4
load:0x403cc700,len:0x2a38
entry 0x403c98d4
[ 1739][E][Wire.cpp:499] requestFrom(): i2cWriteReadNonStop returned Error -1
[MAIN] This is a hacked badge. Nice job.
[Wifi] Connecting to WiFi....connected![+] BSSID : 76:02:A4:61:58:CA
If you see [MAIN] This is a hacked badge. Nice job.
the firmware of the badge is ready to be downloaded!
Downloading the badge firmware
You will need to install esptool.
Open another terminal window and run the following:
pip install esptool
Before the badge will let you download the firmware, you will need to manually put it into firmware download mode.
You can do so by pressing and holding the boot
button, and without letting go of the boot
button, push the reset
button.
If you still have the read_badge.py
script running you should see the following output:
starting ...
ESP-ROM:esp32s3-20210327
Build:Mar 27 2021
rst:0x1 (POWERON),boot:0x0 (DOWNLOAD(USB/UART0))
waiting for download
You can then run the following (where “XXXX” is replaced with the numbers generated by your computer):
esptool.py -c esp32s3 -p /dev/cu.usbserial-XXXX -b 115200 read_flash 0 0x800000 flash_download.bin
You should then see the following output:
esptool.py v4.7.0
Serial port /dev/cu.usbserial-1410
Connecting.....
Detecting chip type... ESP32-S3
Chip is ESP32-S3 (QFN56) (revision v0.2)
Features: WiFi, BLE
Crystal is 40MHz
Uploading stub...
Running stub...
Stub running...
Detected flash size: 8MB
<download progress>
Note: the download can take upwards of ten minutes to complete!
Reading from MQTT
The badge program subscribes and publishes to an MQTT topic.
You can see the output in the Python program that reads the badge:
[EventHandler] Got a message on topic INGSOC/citizen/cc12-ABCDEF123456
is device 1 INGSOC/citizen/cc12-ABCDEF123456
We can also subscribe to the topic with the following Python script (also partially generated by ChatGPT):
Note: You will need install paho-mqtt with
pip install paho-mqtt
before running the script.
import argparse
import paho.mqtt.client as mqtt
import ssl
# Parse command-line arguments
parser = argparse.ArgumentParser(description="MQTT Subscriber")
parser.add_argument("--broker", required=True, help="MQTT broker address")
parser.add_argument("--topic", required=True, help="MQTT topic to subscribe to")
parser.add_argument("--client-id", required=True, help="MQTT client id")
args = parser.parse_args()
# Paths to your certificate and private key files
cert_path = "./certificate.pem"
key_path = "./key.pem"
# Callback function when the client is connected to the broker
def on_connect(client, userdata, flags, reason_code, properties):
print(f"Connected with result code {reason_code}")
# Subscribe to the specified topic
client.subscribe(args.topic)
# Callback function when a message is received
def on_message(client, userdata, message):
print(f"Received message: {message.payload.decode()}")
# Create an MQTT client instance
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, args.client_id)
# Set the callback functions
client.on_connect = on_connect
client.on_message = on_message
# Set up TLS/SSL
client.tls_set(ca_certs=None, certfile=cert_path, keyfile=key_path, cert_reqs=ssl.CERT_REQUIRED, tls_version=ssl.PROTOCOL_TLS)
# Connect to the broker
client.connect(args.broker, 8883) # Port 8883 is for secure MQTT
# Loop to listen for messages
client.loop_forever()
I saved this to a file called mqtt_subscriber.py
.
This won’t run yet though because you will need a certificate.pem
and a key.pem
for authentication.
You can pull these from the firmware.bin
that was downloaded from the badge earlier.
One way to find the certificate is by using the following command:
strings firmware.bin | awk '/BEGIN CERTIFICATE/,/END CERTIFICATE/' | head -n 30
And for the key this command:
strings firmware.bin | awk '/BEGIN RSA PRIVATE KEY/,/END RSA PRIVATE KEY/' | head -n 30
Or, on macOS, you can download the open-source application Cutter and look in the strings
tab:
Then take everything from -----BEGIN CERTIFICATE-----
to -----END CERTIFICATE-----
and save it to a file called certificate.pem
.
Additionally, take everything from -----BEGIN PRIVATE KEY-----
to -----END PRIVATE KEY-----
and save it to a file called key.pem
.
There is also a broker address needed in the firmware.bin
that can be found by running this command in the terminal:
strings firmware.bin | awk '/amazon/'
With all the information gathered you can use this command to subscribe to the topic:
python3 mqtt_subscriber.py --broker [broker address goes here] --topic INGSOC/citizen/cc12-ABCDEFG123456 --client-id INGSOC/citizen/cc12-ABCDEFG123456
If everything works correctly you should get output that looks like the following:
Received message: {"hmac": "zxNAOw5EgAtXFM52QeciNFQ5TgE=", "tick": 10267, "score": 52, "trend": -4, "crime": false, "tamper": true}
Received message: {"hmac": "AEj0mtsKx8AmMDJMLq+JIgpE6gM=", "tick": 10268, "score": 52, "trend": -4, "crime": false, "tamper": true}
Received message: {"hmac": "wubdR/GRGLpXvQ/vL3O7M+uyQ2k=", "tick": 10269, "score": 52, "trend": -4, "crime": false, "tamper": true}