Hack the channel: A Deep Dive into DVB Receiver Security
- 08/04/2025 - inMany people have a DVB receiver in their homes, which offers a large attack surface that many don’t suspect. As these devices can require an internet connection, they provide a cool entry point to a local network. In this article, we’ll dive into the internals of the protocol and the flaws in its implementation.
Looking to improve your skills? Discover our trainings sessions! Learn more.
Introduction
During a garage cleaning, we found a DVB receiver and thought it would be a great target for vulnerability research. It's a common device, yet we couldn’t find much prior research on it.

This device is sold by the French company Metronic and is still available on their website. It features an Ethernet port for internet connectivity, which is essential for certain functionalities like weather updates, web radio, and more.
The objective of this research is to achieve code execution on the device through radio waves, which would allow an attacker to gain access to the local area network to which the device is connected.
However, during our cleaning session, we were unable to find the remote controller for the device. It is nearly unusable without it, as the first thing needed when changing the antenna on such a device is a channel synchronization, which requires the remote. The three buttons on the front panel are power, channel increment, and channel decrement, which aren’t sufficient to access the device settings.
The user documentation specifies that it might or might not work with a universal remote. We then tried brute-forcing with the largest IR remote database we could find on the internet, but nothing happened.
Next, we began a journey to find the IR-related code implemented in the device.
Get the firmware
The first step is to obtain the product's firmware. Since the manufacturer doesn’t provide updates, we will need to extract it directly from the device.

We identified two ports on the PCB labeled CN2 and J11, which appear to be debug ports. However, they seem to be inactive, as no activity is observed when using a logic analyzer.
The SoC under the heatsink is an ALI M3626. Very little information about this chip is available online, and its architecture remains unknown. However, one notable detail is the presence of an SPI flash on the PCB, which could contain the device's firmware.
The data extracted from the flash includes some strings, but most of it has very high entropy.
Upon examining the beginning of the dump, it appears to contain a header.

By analyzing the strings and surrounding data, we can infer that a specific data structure is being used. We have identified certain fields that allow us to extract partitions from the dump.
ali_file_hdr = Struct(
"id" / Bytes(4),
"unk" / Int32ub,
"size" / Int32ub,
"crc" / Bytes(4),
"name" / Bytes(0x10),
"version" / Bytes(0x10),
"date" / Bytes(0x10)
)
This gave us these files:
- bootloader
- defaultdb
- HDCPKey
- MAC_ADDR
- maincode
- netdata
- osd_string
- Radioback
- seecode
- userdb
The maincode partition seems interesting, but its contents are either encrypted, compressed, or both, as indicated by its high entropy. By reversing the bootloader, we were able to pinpoint where the main code is handled and discovered that it is simply compressed using LZMA.
However, without the SoC documentation, we haven't been able to fully understand the hardware initialization process. Specifically, we don't know the base address where the main code is executed.
In order to find out, we started reversing the code and looked for a function that formats data, such as printf
, sprintf
, etc. Once we identified one, we searched for cross-references to calls containing at least two hardcoded addresses. Hardcoded addresses are usually strings, which means the first one must contain %s
.
We then listed all the strings in the code and filtered them to find pairs of strings with the correct relative offset, with the first one containing %s
.
This approach gave us about twenty potential results, and only one was aligned to 512 bytes. Using this base address worked perfectly, as all the string cross-references now made sense.
IR Fails
With the firmware at the correct base address, we can begin reversing to find where the IR codes are handled.
The operating system of the SoC is based on a real-time OS (RTOS). The debug strings suggest that it follows the ITRON kernel specification, though it doesn't appear to use the open-source TKernel implementation.
By googling some strings, we found a Github repository containing some source code very similar to the firmware we have. It seems to be for the ALI M3711, which isn’t exactly our SoC, but it was very helpful in understanding some hardware-related parts of the code.
From an attacker’s perspective, the firmware is particularly interesting because there is no address translation, meaning virtual addresses are identical to physical addresses. This allows us to use gadgets from any thread, provided we have a vulnerability that lets us construct a ROP chain.
One of the first threads created during startup is responsible for hardware initialization. The function irc_m6303irc_init
initializes the infrared receiver included in the SoC and registers a callback for a specific interrupt.
void irc_m6303irc_init(struct pan_hw_info *hw_info)
{
irc_nec_mode_set(0, 280, 500000);
/* ... */
WRITE_INF_BYTE(INFRA_IRCCFG, 0);
WRITE_INF_BYTE(INFRA_IRCCFG, 0x80 | ((12*VALUE_CLK_CYC) >> 5));
WRITE_INF_BYTE(INFRA_FIFOCTRL, 0xA0); /* 32 bytes */
WRITE_INF_BYTE(INFRA_TIMETHR, (VALUE_TOUT / (VALUE_CLK_CYC << 7) - 1));
WRITE_INF_BYTE(INFRA_NOISETHR, VALUE_NOISETHR / VALUE_CLK_CYC);
/* ... */
osal_interrupt_register_lsr(27, irc_m6303irc_lsr, 0);
/* ... */
}
Each time the IR LED receives a pulse, irc_m6303irc_lsr
handles the interrupt and calls generate_code
with the number of ticks elapsed since startup as parameter.
Most IR protocols use Pulse Distance Encoding in order to transmit data. Here is an example with the NEC protocol:

The interrupt handler should then keep track of when the last pulse was received and calculate the difference with the current one in order to decode the transmitted bits.
static UINT32 irc_last_tick = 0;
static void generate_code(UINT32 tick)
{
UINT32 code = 0;
/* ... */
code = irc_nec_pulse_to_code((tick - irc_last_tick) * 1000);
irc_last_tick = tick;
/* ... */
}
The name of the function suggests that the NEC protocol is used, but we will see that it is not implemented correctly. This protocol is quite simple, as it utilizes a device address and a command, both of which are encoded using 8 bits each. Here is a complete NEC transmission:

The NEC implementation in the product packs the 32 received bits into an integer and sends it to the user.
static UINT32 code = 0;
static UINT32 irc_nec_state = 0;
UINT32 irc_nec_pulse_to_code(UINT32 pulse_width)
{
UINT8 got_full_status = 32;
accum_width += pulse_width;
/* ... */
if ((pulse_width > IRC_NEC_PULSE_UNIT) &&
(pulse_width < IRC_NEC_PULSE_UNIT * 5))
{
code = (code << 1) + (pulse_width > IRC_NEC_PULSE_UNIT * 3);
/* check if received 32 bits */
if (got_full_status == irc_nec_state)
{
last_code = code;
irc_nec_state = 0;
return code;
}
}
else
{
irc_nec_state = 0;
return PAN_KEY_INVALID;
}
/* ... */
irc_nec_state++;
return PAN_KEY_INVALID;
}
The received code
variable contains the address as the MSB, its logical inverse, the command and its inverse as LSB.
Further in the program, this value is converted to another structure
#define SET_IRCODE(a) ((((a) >> 24) << 8) | ((a) & 0xff))
UINT32 scan_code_to_msg_code(struct pan_key *key_struct)
{
IR_KEY_INFO msg_code;
/* ... */
msg_code.ir_code = SET_IRCODE(key_struct->code);
return *(UINT32 *) (&msg_code);
}
The MSB and the LSB of the original code
variable are extracted, which correspond to the address but do not correspond to the command. Instead, they correspond to its logical inverse.
Moreover, we can see in the NEC protocol picture shown above that the address and command are sent LSB first, meaning the extracted values from the integer don't have the correct endianness.
The ir_code
which corresponds to the menu key is 0x10B7
. To enter the menu using the flipper zero (which implements NEC correctly), we need to send the address 0x08
(endianness swap of 0x10) and the command 0x12
(endianness swap and logical inverse of 0xB7).
After all these steps, we are finally able to use the device and initiate channel synchronization.
DVB internals
The Digital Video Broadcasting (DVB) is a set of standards used to transmit digital television, particularly in Europe. In the early versions of this standards, no authentication method was implemented. In France, this is still true for most channels.
This means that by broadcasting with a stronger signal, it's possible to hijack a channel and control what is parsed and displayed by the decoder. It can be easily done using a HackRF.

Once demodulated, the data is a MPEG transport stream which is a stream containing multiple elementary streams such as channels video, audio and metadata.
The metadata is composed of many tables, such as:
- PAT (Program Association Table) which lists all programs available in the transport stream
- PMT (Program map specific data) which contain information about programs
- CAT (Conditional access specific data) which is mainly used when the stream is encrypted
- EIT (Event Information Table) which describes the channel programs
- etc.
All these tables are parsed almost all the time by the decoder. The complexity of this standards offers a large attack surface and is aimed to contains bugs.
Vulnerabilities journey
During the research, we found multiple vulnerabilities in the DVB implementation of the product. However, none of them appears to be exploitable.
The BAT signal
During the channel synchronization phase, the decoder checks for the Bouquet Association Table (BAT) in the MPEG stream. This table is used to link multiple streams together and is primarily utilized by paid channels to provide access to multiple channels with a single subscription.
The bat_event
function is called each time a BAT table is received. It checks if the bouquet ID is known and stores it if not. Next, the table is parsed and bat_on_bouqname_desc
is called if the table contains a bouquet name entry. In both functions, the attacker can control the content of data
.
static BOOL bat_event(UINT16 pid, struct si_filter_t *filter,UINT8 reason, UINT8 *data, INT32 len)
{
INT16 i = 0;
struct bat_hitmap *hitmap = NULL;
struct bat_section_info *pinfo = bat_info;
/* ... */
// check if bouquet ID is known
for(i = 0; i < pinfo->bouq_cnt; i++)
{
if(((data[3]<<8)|data[4])==pinfo->bouq_id[i])
{
break;
}
}
if(i == pinfo->bouq_cnt)
{
pinfo->bouq_id[i] = (data[3]<<8)|data[4];
pinfo->bouq_cnt++;
}
/* ... */
}
static INT32 bat_on_bouqname_desc(UINT8 tag, UINT8 len, UINT8 *data, void *priv)
{
INT32 i = 0;
struct bat_section_info *b_info = (struct bat_section_info *)priv;
for(i=0; i<b_info->bouq_cnt; i++)
{
// match the current bouquet ID
if(b_info->b_id==b_info->bouq_id[i])
{
if(len <= (2*(MAX_BOUQ_NAME_LENGTH + 1)))
{
MEMCPY(b_info->bouq_name[i], data, len);
}
break;
}
}
if(i == b_info->bouq_cnt)
{
return ERR_FAILUE;
}
return SUCCESS;
}
The value of bouq_cnt
is never checked, and bouq_id
has a fixed size of 16, which could lead to a buffer overflow. However, the maximum length of the memcpy
operation is 32, and the structure is followed by a large unused buffer. While we have been able to trigger this overflow remotely, we have not been able to cause an overflow large enough to make it exploitable.
A NITpick bug
Also, during the channel synchronization phase, the decoder checks for the Network Information Table (NIT). This table is used to map video and audio streams together. It can contain multiple entries of type extension, and the function t2_delivery_system_descriptor_parser
is called for each one. It checks if an entry with the given parameters already exists and adds it if not.
INT32 t2_delivery_system_descriptor_parser(UINT8 tag, UINT8 length, UINT8 *data, void *priv)
{
UINT8 min_length = 4;
UINT16 i = 0;
UINT16 t2_system_id = 0;
struct nit_section_info *n_info = NULL;
t2_delivery_system_descriptor *t2_desc = NULL;
if((data == NULL) || (priv == NULL))
{
return !SI_SUCCESS;
}
n_info = (struct nit_section_info *)priv;
t2_desc = (t2_delivery_system_descriptor *)data;
t2_system_id = (t2_desc->t2_system_id[0]<<8) | (t2_desc->t2_system_id[1]);
for(i=0; i<n_info->t2_info_num; ++i)
{
if( n_info->t2_info[i].plp_id == t2_desc->plp_id && n_info->t2_info[i].t2_system_id == t2_system_id )
{
break; // The mapping already exists
}
}
if(i == n_info->t2_info_num)
{
n_info->t2_info_num++; //Append a mapping.
}
n_info->t2_info[i].plp_id = t2_desc->plp_id;
n_info->t2_info[i].t2_system_id = t2_system_id;
n_info->t2_info[i].onid = n_info->onid;
n_info->t2_info[i].tsid = n_info->tsid;
return SI_SUCCESS;
}
The value of t2_info_num
is never checked and t2_info
has a fixed size, making it possible to trigger a buffer overflow. However, the structure that is overflowed is in the .data
section, and no useful data is placed afterward.
Other vulnerabilities were found in the parsing of different tables, following the same pattern. However, these are not exploitable because sizes are often encoded in a single byte, which prevents many integer overflow or underflow, and in cases where overflow is possible, it is not enough to reach sensitive data.
Conclusion
In conclusion, our vulnerability research on the DVB receiver revealed multiple potential vulnerabilities within the code, primarily due to the lack of sufficient checks. However, these vulnerabilities are not easily exploitable, mainly because of the inherent constraints of the protocol and the relatively small sizes of the data involved.
It is important to note that modern DVB receivers often include HBBTV features, that significantly increase the attack surface by incorporating a browser and enabling web-based interactions. This addition introduces new vectors for potential exploitation. However, the product we reviewed did not implement HBBTV.