
Technical writeup for CVE-2024-20154
Classification: CWE-121 — Stack-based Buffer Overflow
Severity: Critical (MediaTek bulletin) · 8.8 High, Attack Vector: Adjacent (CISA-ADP)
Type: Remote Code Execution — no user interaction, no prior association
Disclosure: MediaTek Security Bulletin, January 6, 2025 https://corp.mediatek.com/product-security-bulletin/January-2025
Analyzed target: Samsung Galaxy A14 SM-A145R — MT6769 family (Helio G80), within MediaTek's affected chipset list - Firmware was emulated under safe conditions.
Status: Patched.
This was my first published baseband research. I come from a background far away from telecoms infrastructure, lawful interception mediation layers, stingray and IMSI-catcher analysis, and embedded device security — I had not previously done deep firmware reverse engineering on a cellular modem. I wanted to prove to myself that a structured analytical methodology adapts across targets, and that familiarity with a specific platform can be replaced by rigorous chain-tracing. NB-IoT stood out because it sits at a genuinely dangerous intersection: the protocol is designed for constrained IoT devices, the attack surface is pre-association, and the modem stack processes it regardless of what the handset user is doing.
When the patched firmware was analyzed and the vulnerable pattern confirmed absent, the AI system used for mass analysis of the firmware before targeting specific functions
independently matched the reconstructed bug class, conditions, and affected firmware family to the description of CVE-2024-20154.
The technical conclusions are the analyst's own.
The phone in your pocket contains at least two separate computers. The one you interact with runs Android. The other — the baseband — runs completely independently, handles all radio communication, and is almost entirely invisible to the operating system above it. Android may be fully patched. The browser may be sandboxed. The user may never tap a malicious link. None of that matters if the vulnerable code is in the modem firmware that processes radio signals before the application processor is involved.
CVE-2024-20154 is exactly that kind of vulnerability.
A malformed NB-IoT system-information broadcast causes the MediaTek modem firmware to accept an attacker-controlled scheduling count, carry that count through the RRC-to-L1 configuration path without ever clamping it, and eventually use it as the loop bound for a stack-writing loop inside the NB-IoT broadcast-channel handler. When the count exceeds the destination arrays' capacity, the loop writes past them, reaches saved registers on the stack, and overwrites the saved return address. The function then restores the corrupted value into the return-address register and jumps to it.
What makes the severity what it is:
The vulnerability was published in MediaTek's January 6, 2025 Security Bulletin with a Critical severity rating, affecting the LR12A modem family among others. Samsung incorporated the fix into their February 2025 Security Maintenance Release.
This post does not publish a weaponized exploit and is not reproducible from what is published here. The goal is to show where the chain breaks, why each layer failed to stop it, and what it takes to validate a baseband bug responsibly when you cannot attach a debugger to the live modem.
Primary target: Samsung Galaxy A14 (SM-A145R). The radio subsystem is driven by a MediaTek baseband processor in the MT6769 chipset family (Helio G80). The MT6769 family is explicitly listed in MediaTek's affected chipset list for CVE-2024-20154.
AP/CP firmware: A145RXXU1AWD1
Modem software: MOLY LR12A.R3.TC10.6M.A14.PR.SP.V1.P5
Build date: 2023-04-18
The baseband firmware is not Android code. It is a separate embedded system on the radio subsystem of the SoC with its own CPU, its own RTOS, and its own memory space, outside the Android process sandbox.
Analysis of the extracted binary shows the modem processor runs MIPS32 with MIPS16e2 compressed instructions in little-endian mode. MIPS16e2 is a 16-bit encoding extension for embedded code-size reduction — consistent with MediaTek's approach for Helio-generation basebands, confirmed by independent published baseband research on this SoC family.
The operating system is Nucleus RTOS, providing task scheduling, IPC message queues, and a pool-based memory allocator. There is no kernel/user privilege separation, no memory protection unit enforcement between tasks, and no hardware stack-guard mechanism.
All addresses in this post are virtual addresses, as loaded in Ghidra at base 0x90000000.
Three parallel tracks:
Static analysis. Samsung firmware package → CP partition extraction → md1img.img →
Ghidra (MIPS LE 32-bit, base 0x90000000) with MediaTek engineering symbols recovered from
the firmware debug section using the NCC Group mtk_bp toolset.
Dynamic validation. Unicorn Engine (MIPS32 emulation) was used to execute specific
firmware routines in isolation across two phases. Phase 1 attempted to prove the unclamped
copy of si_count into channel context through the native instruction pair. Phase 2 executed
the vulnerable loop on real firmware bytes and confirmed that the firmware's own instructions
corrupt the saved return address. Where Phase 1 could not run fully natively — because the
RTOS service-object environment required by the CPHY dispatch path was not reconstructed —
the side effect was modeled directly and labeled as such in all output.
Radio-side validation. srsRAN 4G with a ZMQ loopback — software-only, no RF emission — confirmed that the test payload survives NB-IoT PHY encoding and transport-block delivery.
NB-IoT (Narrowband Internet of Things) is 3GPP Release 13, designed to connect constrained IoT devices using existing licensed LTE spectrum. It is implemented in a wide range of modern cellular SoCs including those in consumer smartphones.
While in RRC_IDLE, before any RRC connection is established, a device searching for service will:
At step 3, the modem processes a message from an entity it has not authenticated, before any connection or user interaction. A rogue transmitter satisfying normal cell selection conditions will be processed.
+------------------+ +---------------------+
| Rogue Base Stn | | Target UE (Modem) |
+--------+---------+ +----------+----------+
| |
| NPSS/NSSS sync |
|--------------------------------------->|
| MIB-NB (640 ms cycle) |
|--------------------------------------->|
| SIB1-NB (malformed, si_count > 8) |
|--------------------------------------->| ← vulnerability triggered
| [no RRC connection established] |
SIB1-NB is defined in 3GPP TS 36.331. Its schedulingInfoList field carries how many
System Information messages the cell broadcasts, constrained by the spec to a maximum of 8
entries (1..maxSI-Message-NB-r13 = 8). This is a protocol-layer constraint. The
memory-safety constraint — that the list length must not exceed the capacity of the
destination arrays — must be enforced separately by the firmware.
It was not.
Firmware was obtained from a Samsung CP package and extracted using the NCC Group mtk_bp
toolset:
md1img.img → md1_extract.py → 000_md1rom (17.8 MB code image)
→ 017_md1_dbginfo (XZ-compressed CATI debug symbols)
The CATI debug section was decompressed and parsed with mtk_dbg_extract.py symbols, then
imported into Ghidra via ImportSymbolsScript.py. The result was full internal function
names throughout the modem stack — ERRC layer, L1 channel management, IPC subsystem, and the
NB-IoT BCCH handler chain — allowing semantics-guided chain reconstruction.
All function names in this post come from MediaTek's own embedded debug symbols extracted from the firmware image.
el1_ch_nbcch_resume_req (0x90213940) handles the NB-IoT broadcast-channel resume event.
Its MIPS16e2 function prologue:
90213940: save 0xE8, ra, s0-s1
The SAVE instruction decrements sp by 0xE8 and stores callee-saved registers downward:
old_sp (= new_sp + 0xE8)
new_sp + 0xE4 saved ra ← overflow target
new_sp + 0xE0 saved s1
new_sp + 0xDC saved s0
new_sp + 0x98 si_sched_arr [34 halfwords = 68 bytes]
new_sp + 0x78 si_type_arr [32 bytes]
new_sp + 0x00 ← stack pointer after SAVE
From Ghidra decompile of the actual firmware binary:
for (uVar6 = 0; uVar6 < (byte)param_2[0x40a]; uVar6 = uVar6 + 1) {
si_type_arr[uVar6] = /* SI type byte */; // 1 byte/iter, base new_sp+0x78
si_sched_arr[uVar6] = /* SI schedule halfword */; // 2 bytes/iter, base new_sp+0x98
}
param_2[0x40a] is ch_ctx[+0x40A] — a persistent byte in the channel context BSS struct.
The loop bound is used directly, with no prior comparison against the array capacities.
Stream A (halfword sh writes) starts at new_sp+0x98 and advances 2 bytes per iteration.
It reaches the saved RA at new_sp+0xE4 at iteration 38:
new_sp + 0x98 + i×2 = new_sp + 0xE4
i = (0xE4 - 0x98) / 2 = 0x4C / 2 = 38
Stream B (byte sb writes) starts at new_sp+0x78 and would require iteration 108 to reach
the RA slot:
new_sp + 0x78 + i = new_sp + 0xE4
i = 0xE4 - 0x78 = 108
With si_count = 40 (the demonstrator value, chosen to exceed the overflow threshold of 38)
the loop runs 40 iterations. Stream B never reaches the RA slot. The RA corruption comes
entirely from Stream A.
After 40 iterations the MIPS16e2 RESTORE instruction reloads the corrupted value from the
stack into $ra, and jrc ra transfers control.
ch_ctx[+0x40A] is written by el1_ch_nbcch_start at 0x90213444. Two consecutive MIPS
instructions with nothing between them:
; el1_ch_nbcch_start @ 0x90213444
lbu v0, 0x99(s0) ; read IPC_msg[+0x99] = si_count from CPHY_CFG_REQ
sb v0, 0x40A(s1) ; write to ch_ctx[+0x40A] — no clamp, no mask, no compare
The CPHY_CFG_REQ buffer is built in the ERRC layer from the decoded SIB1-NB. The function
errc_chm_l1_set_bcch_si_reception writes IPC_msg[+0x99]:
// errc_chm_l1_set_bcch_si_reception — Layer A
// Loop bound: decoded schedulingInfoList entry count from SIB1-NB
while (bVar3 < *(byte *)(param_3 + 0x44) && (param_2 != 0)) {
bVar3++;
if (uVar2 != param_2) {
*param_5 = *param_5 + 1; // CPHY_CFG_REQ[+0x99]++ — no upper bound check
}
}
The loop bound is the decoded entry count from SIB1-NB. The counter increments once per decoded entry, for as many entries as were decoded, with no maximum guard.
Once the CPHY_CFG_REQ buffer is populated, ERRC dispatches it to L1 with sap_id = 0x501F
as the routing key:
el1_ch_rcv_ilm (sap 0x501F)
→ el1_chmgm_errc_cfg_req_in_idle ← stores CPHY_CFG_REQ @ L1_ctx[+0x323C]
→ el1_chmgm_nbcch_handler
→ el1_ch_nbcch_main
→ el1_ch_nbcch_cphy_cfg_req_process ← validates cell identity, no si_count check
→ el1_ch_nbcch_start @ 0x90213444 ← Layer B: lbu + sb, no clamp
Any single check at any one layer would have broken the chain.
One violated invariant:
The number of SI scheduling entries must never exceed the destination array capacity.
3GPP provides the intended protocol bound (8 entries). The firmware needed to enforce the memory-safety bound at every layer where the count becomes an index or loop limit. In the vulnerable build, the count traveled from the SIB1-NB broadcast field through the ERRC decoder, into the CPHY_CFG_REQ message, across an IPC boundary into the L1 task, into channel context BSS, and into a stack-writing loop — without any layer clamping it.
Two structurally similar but distinct paths can deliver a 0x760-byte configuration buffer to
el1_ch_nbcch_main:
Path B confirmed the message format — byte +0x99 is the SI count consumed by
el1_ch_nbcch_start. Because its count is always hardcoded to 1, it cannot overflow. The
externally influenced path is Path A.
Pattern-searching the firmware for any instruction writing to offset +0x99 produced noise:
T1 (direct sb, ~100 hits), T2 (split base, 5 hits), T3 (computed offset, 0), T4
(overlapping sh/sw, ~465). The ERRC channel management functions were absent from the
msg_send6 cross-reference list because ERRC uses errc_com_send_msg. An emulation probe
confirmed the write was ERRC-side: running the Unicorn harness from the L1 dispatcher and
watching for writes to the buffer's +0x99 offset captured nothing from the L1 side.
[RUN] dispatcher=0x90214418 watching IPC_msg+0x99
NO writes to IPC_msg+0x99 caught from dispatcher.
Write happened before el1_ch_nbcch_main — confirmed ERRC-side.
Following sap_id = 0x501F in errc_com_send_msg identified the routing to
el1_chmgm_errc_cfg_req_in_idle, which stores the CPHY_CFG_REQ pointer at
L1_ctx[+0x323C] and begins the downstream dispatch.
SIB1-NB schedulingInfoList.count (demonstrator: 40)
│
▼ [ERRC task]
errc_chm_ch_ctrl_req_hdlr
→ errc_chm_call_ctrl
→ errc_chm_l1_main
→ errc_chm_l1_call_ctrl
→ errc_chm_l1_snd_cphy_cfg_req ← allocates 0x760-byte CPHY_CFG_REQ
→ errc_chm_l1_set_cphy_req_nbcch_cfg
→ errc_chm_l1_set_bcch_inf
→ errc_chm_l1_set_bcch_si_reception ← Layer A: CPHY_CFG_REQ[+0x99] = 40
→ errc_com_send_msg(sap=0x501F) ← IPC to L1
│
▼ [L1 task]
el1_ch_rcv_ilm (sap 0x501F)
→ el1_chmgm_errc_cfg_req_in_idle ← stores CPHY_CFG_REQ @ L1_ctx[+0x323C]
→ el1_chmgm_nbcch_handler
→ el1_ch_nbcch_main
→ el1_ch_nbcch_cphy_cfg_req_process ← no si_count validation
→ el1_ch_nbcch_start @ 0x90213444 ← Layer B: lbu + sb, no clamp
│
▼
ch_ctx[0x40A] = 40 (persists in BSS)
│
▼ [state machine advances to 0x0B, event 0x2C]
el1_ch_nbcch_resume_req @ 0x90213940 ← Layer C: loop bound from ch_ctx[0x40A]
│
▼
stack overflow → jrc ra → PC = attacker-controlled
This specific SM-A145R model does not appear to exercise the vulnerable NB-IoT code path in normal ship-build operation, which is why hybrid emulation and static analysis were used rather than direct hardware reproduction.
Proven statically. The firmware binary contains the vulnerable instruction pair. The call chain is reconstructed from symbols, cross-references, and decompiled function bodies.
Executed natively in emulation. The loop inside el1_ch_nbcch_resume_req ran on real
MediaTek firmware bytes in Unicorn Engine (MIPS32). The firmware's own sh instruction at
0x90213B02 wrote to the saved return-address slot. The RESTORE instruction loaded the
corrupted value into $ra, and jrc ra transferred control.
Modeled explicitly. The lbu/sb copy in el1_ch_nbcch_start could not be run fully
natively because the RTOS callback-table dispatch path within el1_ch_nbcch_cphy_cfg_req_ process expected live Nucleus heap objects the flat emulator did not provide. The resume
handler (el1_ch_nbcch_resume_req) in Phase 2 reads only from BSS-resident channel context
and does not hit that same dispatch path, which is why Phase 2 ran natively without
requiring the same scaffolding. The Phase 1 side effect — one byte from IPC_msg[+0x99]
written to ch_ctx[+0x40A] — was modeled directly and labeled [PHASE1-MODEL].
Not claimed. A complete end-to-end over-the-air hardware reproduction.
The harness enforced one rule: no hook was permitted to write the proof marker into the saved return-address slot. Every non-firmware memory write was tracked.
[PROOF-W] saved_RA write: pc=0x90213b02 addr=new_sp+0xE4 value=0xbeef
[PROOF-W] saved_RA write: pc=0x90213b02 addr=new_sp+0xE6 value=0xdead
[ANTI-TAMPER] no hook wrote RA=0xDEADBEEF — firmware only
anti_tamper_fail = False
0x90213B02 is the sh instruction inside the loop body. The firmware placed these bytes at
the predicted stack offset. In little-endian storage, the halfwords 0xBEEF and 0xDEAD
at new_sp+0xE4 and new_sp+0xE6 combine to [ef be ad de] = 0xDEADBEEF.
[REGS]
RA = 0xdeadbeef ← firmware wrote this; decisive proof
PC = 0x00000000 ← Unicorn unmapped-fetch artifact, not a hardware exception vector
[STACK] new_sp+0xE4: [ef be ad de] ← little-endian 0xDEADBEEF
[PC-EXPLAIN] PC=0 is an unmapped-fetch artifact; decisive proof is
RA=0xDEADBEEF + stack bytes [ef be ad de] + [PROOF-JRC]
CONFIRMED: saved RA = 0xDEADBEEF
To confirm the test payload survives NB-IoT PHY encoding and transport-block delivery, srsRAN
4G with a ZMQ loopback (no RF emission) was used. The payload was a deliberately
non-conformant vector — the ASN.1 SIZE constraint on schedulingInfoList was relaxed to
allow 40 entries, with pycrate round-trip decode confirming the count field at byte 14.
SIB1 received
SIB2 activated
exit 0
This confirms transport-layer delivery. Firmware-side ASN.1 behavior is established by the Layer A static analysis.
The el1_ch task has one stack. Phase 1 and Phase 2 are two separate IPC events processed
sequentially by the same task, with the stack fully unwinding between them. The count
persists in channel context BSS, not on the stack:
EVENT: CPHY_CFG_REQ (Phase 1)
el1_ch_nbcch_start
lbu v0, 0x99(s0)
sb v0, 0x40A(s1) ← ch_ctx[0x40A] = attacker value, written to BSS
returns — stack fully unwound
EVENT: resume (state 0x0B, msg 0x2C) (Phase 2)
el1_ch_nbcch_resume_req ← frame 0xE8, reads ch_ctx[0x40A] as loop bound
loop × 40 → saved RA corrupted
jrc ra → attacker-controlled PC
ch_ctx[0x40A] lives in BSS and retains the attacker's value until the modem resets or a
subsequent channel configuration overwrites it.
old_sp (= new_sp + 0xE8)
new_sp + 0xE4 saved ra
new_sp + 0xE0 saved s1
new_sp + 0xDC saved s0
new_sp + 0x98 si_sched_arr [34 halfwords = 68 bytes]
new_sp + 0x78 si_type_arr [32 bytes]
Frame size confirmed by emulation measurement. Array positions confirmed by the emulation
result — RA written at iteration 38, consistent with si_sched_arr starting at new_sp+0x98.
| Build | Modem firmware | Build date |
|---|---|---|
| Vulnerable | A145RXXU1AWD1, MOLY LR12A...V1.P5 | 2023-04-18 |
| Patched | A145RXXUDDZC2, MOLY LR12A...V3.P8 |
Build dates confirmed by extracting md1_dbginfo metadata from both images. Patch ID:
MOLY00720348 · Issue ID: MSV-2392.
el1_ch_nbcch_start (0x90213444 in vulnerable binary): The lbu/sb instruction
pair is absent. The direct copy of IPC_msg[+0x99] into ch_ctx[+0x40A] is gone.
el1_ch_nbcch_resume_req (0x90213940 in vulnerable binary): The stack-writing loop
over ch_ctx[0x40A] is absent. The architecture where an untrusted byte becomes a loop bound
over fixed-size stack arrays no longer exists. The function body is replaced with a different
dispatch structure.
errc_chm_l1_set_bcch_si_reception: The unbounded counter loop is replaced with
validation-oriented helper calls.
Two functions present in the patched binary are absent from the vulnerable binary:
el1_ch_nbcch_param_check
el1_ch_scell_param_check
A single-line bounds check would appear as a few added instructions inside an existing
function at the same address. What the patched binary shows is an architectural revision:
the NBCCH scheduling path was redesigned so that the si_count-as-loop-bound pattern no
longer exists anywhere in the path.
The vulnerability described here matches CVE-2024-20154 as published by MediaTek on January 6, 2025. Confirmation basis:
No weaponized exploit. No firmware binary content. No malformed payload bytes. No step-by-step procedure for triggering the vulnerability against a live device. The information needed to reproduce a working attack — complete payload construction for the specific modem decoder, RTOS heap-object scaffolding for full Phase 1 emulation, over-the-air radio configuration — is deliberately absent.
ZMQ loopback means no signal was ever transmitted over the air. No real device was targeted. No carrier network was involved. The proof runs entirely in a contained software environment on hardware owned by the analyst. This is the correct approach for validating a pre-association radio vulnerability — transmitting a malformed broadcast would affect any device within range.
Unicorn emulation demonstrates that the vulnerable behavior is in the firmware binary itself, reproducibly, independent of any specific device state or radio environment. This is a technically stronger claim than a single hardware crash, and it avoids deploying anything over the air.
All analysis was performed on firmware legitimately obtained from a consumer device and from Samsung's publicly released firmware packages. No proprietary documentation was used. Internal MediaTek struct definitions and IPC message formats are described only to the extent necessary to explain the memory-safety failure. They are not published as specifications.
| Mitigation | Status | Effect |
|---|
| ASLR | Absent | Firmware addresses are static and predictable from the image |
| Stack canary | Absent | SAVE/RESTORE stores callee-saved registers with no guard value |
| NX / W^X | Absent | Stack memory is executable |
| CFI | Absent | Return addresses are not validated against any policy |
| Iteration | sh writes to | Effect |
|---|
| 0–33 | si_sched_arr[0..33] | In bounds |
| 34–35 | new_sp+0xDC — saved s0 | s0 corrupted |
| 36–37 | new_sp+0xE0 — saved s1 | s1 corrupted |
| 38 | new_sp+0xE4 — saved ra [15:0] | RA low halfword |
| 39 | new_sp+0xE6 — saved ra [31:16] | RA high halfword |
| Layer | Function | Address | Clamp present? |
|---|
| A — ERRC builder | errc_chm_l1_set_bcch_si_reception | ERRC range | None |
| B — L1 copy | el1_ch_nbcch_start | 0x90213444 | None |
| C — L1 loop | el1_ch_nbcch_resume_req | 0x90213940 | None |
| Path | Source | [+0x99] value | Relevance |
|---|
| Path A (ERRC → L1 IPC) | ERRC builds CPHY_CFG_REQ from decoded SIB1-NB | schedulingInfoList.count from OTA | The vulnerable path |
| Path B (L1 internal) | el1_ch_scs_ind_send builds internal IPC body | Hardcoded 1 | Not vulnerable |
| Gate | Condition | Where checked |
|---|
| NB-IoT active path | cfg_type == 1 | el1_ch_nbcch_cphy_cfg_req_process |
| Channel not complete | done_flag == 0 | ch_ctx[0x438] |
State machine 0x0B | NBCCH in resume state | el1_ch_nbcch_main dispatch |
| si_count nonzero | ch_ctx[0x40A] > 0 | Loop condition |
| Band class ≤ 2 | Valid NB-IoT mode | Entry of el1_ch_nbcch_resume_req |
el1_chmgm_cell_info_get nonzero | Cell in service table | Called in el1_ch_nbcch_start |
| Function | Address | Role | Clamp? |
|---|
errc_chm_l1_set_bcch_si_reception | ERRC range | Writes CPHY_CFG_REQ[+0x99] | None |
el1_ch_nbcch_cphy_cfg_req_process | 0x90213F80 | Routes CPHY config into L1 | N/A — does not read si_count |
el1_chmgm_cell_info_get | 0x9020E0D0 | Validates EARFCN/PCI | N/A |
el1_ch_nbcch_start | 0x90213444 | Copies count to BSS | None |
el1_ch_nbcch_resume_req | 0x90213940 | Uses count as loop bound | None |
| 2025-04-23 |