Security

RF Signal Hacking with SDR: Capture, Replay, and Remote Device Control

Imagine you’ve captured, intercepted, and decoded a device’s control signal. Now it’s time to spoof it! To do that, you need to learn how to transmit arbitrary signals. Let’s prep a radio transmitter — and onward to victory!

Hardware

For example, we’ll analyze signals from a remote control operating at 434 MHz.

Wireless remote control
Wireless remote control

This type of remote is a good fit for our purposes for two reasons:

  • Its signal is very easy to analyze and is essentially binary code modulated at the required frequency (so‑called OOK—On-Off Keying).
  • Many devices work on this principle, so once you understand one, it’s not hard to control others as well.

To receive the signal, we used an inexpensive RTL-SDR V3 receiver, which you can get for about $30. It’s great for receiving and analyzing signals, but it can’t transmit. For that, you’ll need something more capable, like a LimeSDR or a HackRF.

Software

The key difference between Software Defined Radio and conventional radio is that all signal processing is done in the digital domain. The device’s role is essentially to feed a digital stream to a digital-to-analog converter (DAC), which then puts it on the air. The sampling rate and bandwidth are related by the те­оре­мой Шен­нона — Котель­никова: the higher the number of samples per second, the wider the signal you can transmit.

Modern DAC and ADC speeds let an SDR capture the entire FM broadcast band—from 88 to 108 MHz—in one go. It can also transmit multiple signals at different frequencies simultaneously, as long as those waveforms are correctly generated in software.

Greetings from Holland

As an example of what SDR can do, here’s a recording I made in Amsterdam. It captures all FM stations at once across the 91–105 MHz band; ten seconds of this recording take up 660 MB. You can download the file and hear what’s on Dutch radio.

You can play the file in the free SDR# (SDRSharp) app—you can open the recording and listen to any station as if it were a regular receiver. But unlike a hardware radio, all the demodulation/decoding happens in software, and the input is just a digital stream. I think you can now appreciate how much compute power modern SDRs pack.

Let’s get back to the wireless remote. Our goal is to generate the signal we need, and the SDR will transmit it over the air. The go-to tool for digital signal processing is GNU Radio. It’s not just an application; it’s a full framework with a large set of signal-processing blocks and support for custom modules. It used to be Linux-only, but recent versions also run on 64-bit Windows. macOS users can install GNU Radio with brew.

In short, install GNU Radio, connect your SDR, and start analyzing signals.

Receiving

Before you can transmit a signal, you need to receive it and save it as a reference sample. Launch GNU Radio Companion and assemble a flowgraph from blocks.

Receiving in GNU Radio
Receiving in GNU Radio

GNU Radio is built around stream processing: you assemble a flowgraph from blocks that perform operations. In this case we only need two blocks: a Source and a Sink. I’m using a USRP SDR, so my source is the USRP Source block. Your device may be different—check your SDR’s documentation to find the right source block. For the sink, I’m using an FFT Sink—a visualization block that lets us see whether a signal is present. It isn’t required for recording, but without it you won’t know whether you’re actually receiving a signal.

In the receiver block’s properties, I set the tuning frequency to 434 MHz. I set the sample rate to 128,000, which is sufficient to capture the signal. Run the flowgraph in GNU Radio, bring the remote closer to the antenna, press any button, and if everything is configured correctly you should see a clear spike in the spectrum.

Signal spectrum in GNU Radio
Signal spectrum in GNU Radio

If the signal looks good, we can record it. Add a File Sink block.

GNU Radio flowgraph with a File Sink block
GNU Radio flowgraph with a File Sink block

On Windows, GNU Radio insists on absolute file paths—relative paths simply don’t work. This issue doesn’t occur on Linux.

Launch the program, press the button on the remote, then close the program. In the folder you specified, a file named 433_signal.iq about 5 MB in size should appear, containing our signal. You can open it in any audio editor, for example Cool Edit, by selecting the “stereo” file type (SDR records two channels called I and Q) and the float data type. We’ll open it in Python using the NumPy scientific computing library.

import numpy as np
import matplotlib.pyplot as plt
data = np.fromfile('433_signal.iq', dtype=np.float32)
data_2ch = np.reshape(data, (-1, 2))
data_left = data_2ch[:, -1]
rate = 128000
plt.figure(1)
time = np.linspace(0, len(data_left)/rate, num=len(data_left))
plt.plot(time, data_left)
plt.tight_layout()
plt.show()

The recorded file doesn’t have a wav header; it contains raw float samples, so you need to specify the sampling rate and data type manually. We also use np.reshape to convert the input 1D array into a 2D array since the recording has two channels. For convenience, I’m displaying only one channel.

Run the program and you’ll see our signal.

Signal in Matplotlib
Signal in Matplotlib

To avoid distortion, don’t let the recording level peak at 1.0. At the same time, don’t set it too low, or the transmitted signal quality will suffer. A signal level between 0.5 and 1.0 is ideal. In my case, it was enough to hold the remote just a few centimeters from the antenna while recording.

If you don’t see anything and instead get an error about the NumPy or Matplotlib library being missing, you need to install them:

  • On Windows: C:\Python3\Scripts\pip.exe install numpy matplotlib
  • On Linux: $ sudo pip install numpy scipy matplotlib
  • On recent macOS versions: $ pip3 install numpy scipy matplotlib --user

Transmission

Once we’ve recorded the signal, we need to build the block diagram for transmitting it. The approach is the same, with a few small differences.

GNU Radio transmit flowgraph
GNU Radio transmit flowgraph

The File Source block is the origin of our signal—that’s why we recorded it in the first place. The Throttle block sets the rate at which the signal is fed to the transmitter; in our case, the sample rate is 128,000. Multiply Const slightly boosts the signal level so it’s close to 1, which helps maximize the transmitter’s output power. Finally, the USRP Sink block sends the data to the actual SDR. I set the output gain to 70; this value may differ for other SDR models.

It also depends on how far you are from the controlled device—in my case, the wireless remote controls a lamp in another room. So I set the transmit power a bit higher. The transmit frequency is the same as the one used for reception. The Scope Sink block is optional and is used to monitor the transmitted data.

Time to test. Start the program—the TX LED on the SDR turns on, and the lamp lights up.

Once your flowgraph is built and tested, you don’t need to use GNU Radio Companion anymore. In the project settings, switch WX GUI to No GUI, enable Run to Completion, and remove the Scope Sink block. Then choose Run → Generate; this creates a file named top_block.py, which is the ready-to-run Python application. You can run it from the command line like any other Python program.

You can open the top_block.py file and see that the code is clear and can be modified if you want.

GNU Radio Python Flow Graph
from gnuradio import blocks
from gnuradio import eng_notation
from gnuradio import gr
from gnuradio import uhd
from gnuradio.eng_option import eng_option
from gnuradio.filter import firdes
from optparse import OptionParser
import time
class top_block(gr.top_block):
def __init__(self):
gr.top_block.__init__(self, "Top Block")
# Variables
self.samp_rate = samp_rate = 128000
# Blocks
self.uhd_usrp_sink_0 = uhd.usrp_sink(
",".join(("", "")),
uhd.stream_args(cpu_format="fc32", channels=range(1),),
)
self.uhd_usrp_sink_0.set_samp_rate(samp_rate)
self.uhd_usrp_sink_0.set_center_freq(434000000, 0)
self.uhd_usrp_sink_0.set_gain(70, 0)
self.uhd_usrp_sink_0.set_antenna('TX/RX', 0)
self.blocks_throttle_0 = blocks.throttle(gr.sizeof_gr_complex*1, samp_rate,True)
self.blocks_multiply_const_vxx_0 = blocks.multiply_const_vcc((1.8, ))
self.blocks_file_source_0 = blocks.file_source(gr.sizeof_gr_complex*1, 'D:\\Temp\\1\\433_signal.iq', False)
# Connections
self.connect((self.blocks_file_source_0, 0), (self.blocks_throttle_0, 0))
self.connect((self.blocks_multiply_const_vxx_0, 0), (self.uhd_usrp_sink_0, 0))
self.connect((self.blocks_throttle_0, 0), (self.blocks_multiply_const_vxx_0, 0))
def get_samp_rate(self):
return self.samp_rate
def set_samp_rate(self, samp_rate):
self.samp_rate = samp_rate
self.uhd_usrp_sink_0.set_samp_rate(self.samp_rate)
self.blocks_throttle_0.set_sample_rate(self.samp_rate)
def main(top_block_cls=top_block, options=None):
tb = top_block_cls()
tb.start()
tb.wait()
if __name__ == '__main__':
main()

If you know Python, you can modify the program to your liking. For example, you can pass the recorded file name via a command-line parameter, which will let you simulate pressing different remote buttons.

Verification: open PowerShell and run the program; the LED should light up.

Sending a signal from the command line
Sending a signal from the command line

Using this approach, you can receive and transmit a variety of signals—not just from a remote control, but from any device that operates on the same principle. There’s plenty of room for experimentation.

With an SDR, you can transmit any recorded signal—you could record an FM station in the city and then rebroadcast it at your country house. The signal will go out exactly as-is, including RDS and stereo. You can even increase the sampling rate and transmit several FM stations in parallel. It all depends on how much disk space you’re willing to allocate for the recording and the maximum bandwidth your SDR supports. The only drawback is that SDR output power is quite low, so the range will be limited.

Intercepting a Weak Signal

Now let’s tackle a more challenging scenario. Suppose we don’t have physical access to the remote. As an example, I recorded the signal from a remote located in another room. We’ll load it in Python and inspect the output.

Weak signal
Weak signal

Compared to the previous recording, the signal is 200× weaker and the noise level is higher. Our earlier approach won’t work here: an SDR doesn’t know what it’s supposed to transmit, so it will faithfully reproduce a weak, noisy signal. You can try, but the result is obvious—the lamp won’t turn on.

There are three different ways to solve this problem.

Option 1 — just boost the signal level. It’s SDR, after all: in the digital domain we can do whatever we want. Change the Multiply Const parameter from 1.8 to, say, 300. More precisely, open the Scope Sink and tune the value until the signal sits in the 0.5–0.9 range.

Signal after amplification
Signal after amplification

Result: launch the program—the lamp lights up. But I can’t recommend this approach. The reason is simple: along with the intended signal, we’re transmitting all the amplified noise and junk, which reduces transmission efficiency and clutters the airwaves with unnecessary interference.

Look at what’s on the air: the top traces are from the real remote, and the thick band at the bottom is ours—amplified 300× along with the noise.

Over-the-air signal
Over-the-air signal

Option 2 — add a bandpass filter. It will simply cut out everything outside the desired range.

Back to the signal spectrum: the peak is at 6.5 kHz.

Signal spectrum in GNU Radio
Signal spectrum in GNU Radio

Everything around it is noise that should be filtered out. Replace the Multiply Const block with a Band Pass Filter block, set the lower and upper cutoffs to 4 and 9 kHz—centered on the target frequency with a bit of headroom on both sides. Set the gain to 300 in the Gain parameter.

GNU Radio flowgraph
GNU Radio flowgraph

Fired it up: everything works, the lamp lights. Looking at the spectrum is interesting—the generated signal has a much narrower occupied bandwidth (top trace) than the original remote’s signal (bottom trace).

“Digital” vs. “conventional” signals over the air
“Digital” vs. “conventional” signals over the air

No surprise — cheap Chinese remotes use the most basic sub-$1 components, so you can’t expect much in terms of quality.

We removed the out‑of‑band noise around the signal, but within the signal’s own bandwidth the data is still being transmitted along with the RF noise captured from the air. The radical improvement is to stop rebroadcasting the recorded waveform and regenerate the digital signal from scratch, without the noise.

Option 3 — rebuild the signal from scratch.

GNU Radio flowgraph
GNU Radio flowgraph

Don’t worry. If you look at the signal flow from left to right, it all becomes easier.

We know the remote uses AM, so the first step is to demodulate it. To do that, we shift the original signal—sitting 6.5 kHz off center—down to baseband by multiplying it by a sinusoidal tone. Recall the schoolbook identity for multiplying cosines: the product yields components at the sum and the difference of the two frequencies.

We then clean things up with a low-pass filter and extract the signal amplitude using Complex to Mag. As you may recall, an SDR outputs two data streams called I and Q, which GNU Radio represents as complex samples. At the output of Complex to Mag, we get the AM-demodulated signal.

Now we need to convert our signal to binary, which is done with the Threshold block. This block does exactly what we need—it turns a noisy input signal into a clean output. Take a look at the difference.

Signal before and after the Threshold block
Signal before and after the Threshold block

We have a good margin in terms of the signal-to-noise ratio, which means we could receive even weaker signals.

And finally: transmitting the generated signal back over the air. We shift it back up by the same 6.5 kHz, strip out the excess with a band-pass filter (the original digital waveform is rectangular, so it contains many unwanted harmonics), and send the resulting stream to the USRP Sink.

Final test: run the program, the indicator lights up.

If you remove the band-pass filter, the output shows a broad spectrum, just like the original remote. From this we can conclude the manufacturer cut costs and there’s no output filter in this remote. They simply use a 434 MHz oscillator and modulate it with a digital control signal. Most likely, a basic microcontroller that generates the button-press signals is wired directly to the RF oscillator chip’s Enable pin.

So, without cracking open the remote, using math alone, we figured out what’s inside it.

Can you receive a signal that’s even weaker and coming from farther away? Yes—by taking advantage of the fact that the remote control transmits identical packets repeatedly. GNU Radio alone may not suffice; you’ll need to implement signal accumulation and averaging. That will improve the signal-to-noise ratio. This is roughly how astronomers detect faint signals with radio telescopes. Alternatively, you can use a directional antenna for 433 MHz.

There are ways to do it—both software and hardware—if you really need that signal.

Nothing works—now what?

Let’s say you followed all the steps above, but it still doesn’t work. Where do you start troubleshooting?

Electronics is an exact science—there are no miracles. Start by checking reception: make sure the recorded file actually contains a signal and that its level is good. The signal in the recording should be clearly distinguishable. If there’s no signal, check the receiver’s gain and frequency. It’s possible the remote uses a different frequency, or the signal is infrared rather than radio.

To keep things simple, fire up SDR# and watch the spectrum/waterfall while you press the remote’s buttons. If you don’t know the exact frequency, set your SDR to the widest span you can—say, 10 MHz—and press the remote’s buttons; the signal should show up somewhere. Obviously, there’s no point proceeding until you’ve actually found a signal.

Once you know the frequency and have recorded the signal, you can start testing transmission. The key factors are the antenna and transmit power, which determine the range. Transmit power is set in the SDR settings; this parameter is usually called Gain. Ideally, use a ready-made 433 MHz antenna, for example one from a handheld radio. If you don’t have one, even a piece of wire plugged into the antenna jack will work, but the range will be much shorter. For initial tests, place the lamp (or whatever you’re controlling) next to the SDR on your desk; once you’ve confirmed everything works, you can start testing range.

It’s convenient to check transmit quality with SDR# and a reference RTL‑SDR receiver; you can put the signal spectrum on a second monitor. If there’s no transmission at all, check the GNU Radio console for error messages — you might be missing the correct driver, or the transmitter may be set to a different antenna port. Most importantly, make sure you’re not trying to transmit with an RTL‑SDR.

All of this will likely only work with simple devices that transmit hard‑coded signals. In theory, if the signal doesn’t include any dynamically changing fields, replaying a pre-recorded signal will work; but if the devices exchange data or keys, it won’t.

As for range, with an SDR and a 433 MHz walkie‑talkie antenna, I was able to trigger a lamp at the other end of my apartment. If you build a directional antenna, you can push the range even further.

Conclusion

It’s feasible to control certain wireless devices with SDR, and it can be quite engaging from both a programming and mathematical standpoint.

From a vulnerability standpoint: to reiterate, the simple remotes discussed above offer no security at all. That’s the trade-off for low cost and a straightforward design. On the other hand, people usually don’t connect anything critical to these remotes. The more likely issue isn’t a “hacker with a laptop,” but plain old accidental channel overlap (frequency collision).

I’d only connect a high‑powered heater to an RF remote with great caution, and I’d cut power entirely when leaving for an extended trip. Also, change the control channel in the remote’s settings from the default to something else. And if your friends or neighbors have loaded up on RF‑controlled light bulbs—you know how to prank them on April Fools’ Day.

it? Share: