Rolling Code with Alzheimer’s

Decoding ASK/OOK and pwning a garage door opener

I’ve been getting into Software Defined Radio, and decide to have a go at hacking a simple radio device. First up I choose to play with a garage door opener. I bought a cheap kit from ebay from a UK based company. The system was advertised as ‘rolling code secure’, so I thought id get one Here’s a video from the sellers.

Rolling codes use algorithms like keyloq which using a rolling code system to ensure that codes cannot be captured and replayed. So the first thing I tried was to use a SDR to record the signal from the fobs and replay it. Surprisingly it worked.

The video below shows my HackRF playing a handful of recorded fob presses in a loop.

My first thought was that this meant that someone had lied, and actually it was using a static code. But it turned out to be a little more complex than that.

First thing i did was locate the signal using GQCX, I found it at 433.92 MHz. Pretty common to find this kind of device ~433 MHz as this is an unlicensed band.


Having it found it I used a low pass filter to record a good sample and opened it in Baudline.


Despite the frequency shifts in the waterfall image, this is ASK/OOK, which uses the presence or absence of a signal to signal a 1 or a 0. You can see the start of signal starts with a 010101010101010101010101 preamble, showing the start of the packet. This is followed by short and long square pulses which look a bit like Morse code. It’s pretty common to find somekind of encoding within digital signals, otherwise a long run of zeros and ones can lead to the clock falling out sync.

I decided Id need to see a list of codes trying different buttons and remotes to figure out exactly what was going on, so I wrote a simple ASK/OOK demodulator with clock recovery in GNURadio Companion.


Basically this flow cleans up the signal, then turns the square signals into triangles using a square FIR filter. Square signals arnt very good for Clock Recovery MM because theres no peak to find. After that the slicer produces zeros and ones and throws them into a TCP Sink, which I connect to a Python Script

To write the decoder I thought of the dots and dashes as 0b100 and 0b110. Three symbols per bit. Just take the middle bit as the actual value transmitted then as an error check ensure the preceding bit is 1 and the proceeding bit is 0. Link here for the code.

Here’s the output, my comments in square brackets.

id, last seen, counts, code
0, 11s 552m, 14, 	0x1739269c69b3e7fecL, 	010111001110010010011010011100011010011011001111100111111111101100 [Remote 1 Button 1]
1, 12s 818m, 1, 	0x3093baeb29b3e7ff5L, 	110000100100111011101011101011001010011011001111100111111111110101 [Remote 1 Button 2]
2, 15s 109m, 13, 	0x3093baeb29b3e7ff4L, 	110000100100111011101011101011001010011011001111100111111111110100 [Remote 1 Button 2]
3, 26s 443m, 1, 	0x1d38538729b3e7fe5L, 	011101001110000101001110000111001010011011001111100111111111100101 [Remote 1 both buttons]
4, 28s 100m, 9, 	0x1d38538729b3e7fe4L, 	011101001110000101001110000111001010011011001111100111111111100100 [Remote 1 both buttons]
5, 40s 690m, 15, 	0x1a5f6b41d45e7fec, 	000001101001011111011010110100000111010100010111100111111111101100 [Remote 2 Button 1]
6, 41s 446m, 1, 	0x32fb274e5d45e7ff5L, 	110010111110110010011101001110010111010100010111100111111111110101 [Remote 2 Button 2]
7, 44s 212m, 13, 	0x32fb274e5d45e7ff4L, 	110010111110110010011101001110010111010100010111100111111111110100 [Remote 2 Button 2]
8, 45s 298m, 1, 	0x16e96fde1d45e7fe5L, 	010110111010010110111111011110000111010100010111100111111111100101 [Remote 2 both buttons]
9, 47s 546m, 11, 	0x16e96fde1d45e7fe4L, 	010110111010010110111111011110000111010100010111100111111111100100 [Remote 2 both buttons]
                 	                	[-Rolling Code-----------------][--serial---][??????????????]12?FF []                                                           

From this it’s pretty easy to identify the rolling code and the serial that goes with each remote. Towards the end you see two bits (marked 12) which are used to specific which garage door to open, and at the end the last two bits are always 01 on the first message and 00 on subsequent copies of the code. So given theres a rolling code in there, why on earth does the replay attack work? After a lot of messing around I found that the when replaying several recorded codes the first code never works. I recorded a bunch of codes and split them into separate sample files, I found as long as the code comes after the previously transmitted code in the rolling sequence, it’ll work. While this is what happens on a normal rolling code system, transmitting an older coder resets device into an earlier position within the rolling code sequence. In effect the device loses track of where it is in the sequence if receives a valid but out of sequence code.

Therefore if you have captured two codes, playing them alternately opens the garage door, bypassing the anti-replay security feature normally associated with rolling codes.

Finally I wrote a script which generated an RF signal for two previously recorded codes. The script just writes a file with a bunch of floats matching the provided string and adds in the preamble and the dot/dash encoding, then a GNURadio script turns that into a simple square signal.

./ codex 11000000100001000100100110100000101001101100111110011111111110110; ~/
./ codex 00011110111011111010111110100111101001101100111110011111111110110; ~/