
Most of my security work so far had lived on the software side. This one didn't, and that is exactly why it stuck with me. It was an Embedded Systems project at university, built together with my teammate Tomas Marta, and it was my first real hands-on contact with the physical layer: soldering, breadboards, voltage levels, and the quiet realisation that a lot of the devices we take for granted are protected by almost nothing.
The goal was simple to state and surprisingly deep to pull off: build a portable tool that can capture a 433MHz radio signal, store it, and replay it on demand. Garage doors, gates, barrier lifts, a huge number of them talk over the 433.92MHz band, and many of the cheaper or older ones use fixed-code modulation, where the exact same bitstream is sent on every single button press. If you can record that transmission once, you can repeat it forever. That is the whole attack.
Everything here was done in a controlled environment, on hardware we owned or had permission to test against. This is a study of why these systems are weak, not a how-to for messing with someone's gate.
A replay attack is about as pure as attacks get. You don't need to break any encryption, you don't even need to understand the protocol. You listen, you record the raw signal, and later you play it back. If the receiver can't tell the difference between the original remote and your recording, it opens.
The interesting design decision we made early on was to work on raw pulse timings instead of decoding known protocols. Libraries like rc-switch try to recognise specific encoding schemes (PT2262, EV1527, and friends) and hand you a clean decoded value. The problem is that anything using a proprietary or non-standard encoding gets silently dropped, often capturing only a recognisable slice of a much longer bitstream while throwing away the sync pulses. By recording the duration in microseconds of every high and low period, our tool stays completely agnostic to the encoding. It doesn't care what the signal means, it just reproduces it faithfully.
This was the part I had never really done before, and it humbled me fast. The path from the first sketch to a working prototype was basically a series of things going wrong and teaching me something.
The original plan used an Arduino Uno talking to a Raspberry Pi over USB serial. It fell apart for a few reasons. The Uno's 2KB of SRAM simply cannot buffer a long raw capture, and a single garage remote press can produce 200 or more pulses. It also has no Wi-Fi, so every operation needed the serial tether. We replaced it with an ESP32-S3 (a Makerfabs MaTouch 1.9 board), which brought 512KB of SRAM, native Wi-Fi, and microsecond-resolution GPIO interrupt timestamps through the esp_timer API. That timing precision turned out to be the whole ballgame for faithful capture.
I also killed a module along the way. During early prototyping I applied 5V to the SPI data pin of the first transceiver, not realising that on those modules the data lines are power-carrying when active. It died instantly. That mistake is why the final build has a bi-directional logic level shifter in the middle of it: the ESP32-S3 runs at 3.3V logic, while both RF modules want 5V. The shifter bridges the two voltage domains cleanly and keeps me from frying anything else.
The final RF front-end is two separate modules: an RXB6 superheterodyne receiver for capture (chosen over the common XY-MK-5V because it has a much lower noise floor and better sensitivity), and an FS1000A ASK transmitter for replay. Both shipped without antennas, so we soldered on 17.3cm wire antennas, a quarter wavelength at 433MHz, which pushed the usable range from about 3 meters to roughly 15.
When a sniff command arrives, the firmware attaches an interrupt service routine to the receiver's data pin on both rising and falling edges. Every time the line flips, the ISR grabs the current timestamp, computes the delta from the previous edge, and drops it into a circular buffer. No memory allocation, no serial printing inside the interrupt, nothing that would let the operating system's scheduling ruin the timing. Just raw durations.
There are two capture modes:
Once a capture finishes, the buffer is serialised as a plain JSON array of integers and shipped off to the backend.
Rather than one gadget doing everything, the tool is split into three pieces that each do one job. It made the whole thing testable, and honestly it made it feel like a real system instead of a class demo.
The ESP32-S3 handles all the RF work. The Raspberry Pi runs a small FastAPI server that acts as the single source of truth: it holds a command queue and a repository of captured signals. The Android app (Kotlin and Jetpack Compose) is the control panel. Everyone talks over plain HTTP, and both the ESP32 and the phone are polling clients, the ESP32 asks the Pi for commands to run, the phone asks the Pi for signals to display.
The part I liked most is that none of the three needs to be physically near each other. We run them over Tailscale, a WireGuard based mesh VPN, so the ESP32 can sit next to a target while the Pi lives on a home network and the phone controls it all from somewhere else entirely. No port forwarding, no public IPs.
Which leads to the slightly unsettling deployment picture: the whole thing fits in a jacket or a backpack. 433MHz propagates fine through fabric, so the transceiver can stay hidden while you operate it from your phone.
The Android side keeps it simple: one screen to fire commands (auto capture, manual capture, jam, and the recordings list), and a viewer that renders the raw capture as a digital waveform.
Seeing a real captured signal drawn out is where it clicked for me. The long sync pulse, then the repeating high and low pattern of the actual data. You can read the structure of a garage remote right off the screen.
Replay itself is bitbanging: the firmware loads the captured timing array and toggles the transmit pin to reproduce it. One detail we only learned by testing is that sending the sequence a single time usually isn't enough. Real receivers live in a noisy world, so they want to see the same sequence repeated a handful of times (a burst of five or six) before they trust it. So the replay loop just repeats the burst.
Now the surprising part. We didn't have a verified fixed-code remote on hand to test against, so we pointed it at a gate that used a rolling code mechanism, expecting it to fail. It didn't. The replay opened the gate.
That should not happen with a properly implemented rolling code, and it points at a real weakness in some of these systems. If a button press is captured out of range of the receiver, or if the receiver accepts previously generated but unused codes within a certain forward window (a margin some implementations add so the remote doesn't get out of sync when you press it away from the gate), then a plain raw replay can still authenticate. Our bitbanged output, once level shifted to 5V, produced a clean enough carrier on the FS1000A to slip right into that window.
The last mode is jamming. The firmware floods the channel with continuous pseudo-random toggling, randomised pulse widths spread across the demodulator's passband, and it does exactly what you'd fear. Against a garage door receiver, with the jammer active the legitimate remote failed to work past about 2 meters, down from a normal range of around 15. Below 2 meters the real signal was strong enough to punch through.
It is a crude attack, but it's a genuine localised denial of service on a physical access control. The same little board that clones your gate can also just stop it from listening.
More than any single result, this project changed how I look at the physical devices around me. Building the RF repeater and the jammer was a real challenge, and getting there meant frying a module, wiring a level shifter backwards, soldering antennas, and slowly learning how to reason about signals I couldn't see. It was my first proper taste of hardware hacking, and it made abstract ideas like "the airwaves are unauthenticated" feel very concrete.
The takeaway is a little uncomfortable: a lot of the everyday security we rely on assumes nobody is listening or replaying, and a cheap board and some wire is all it takes to break that assumption. Fixed-code systems are wide open by design, and even some rolling-code ones have windows worth worrying about.
If you'd like more detail on the firmware, the hardware pin mapping, or the testing setup, feel free to reach out.