Qualcomm ships firmware in pieces. A modem.mdt holds the ELF header and the
program headers, and every byte of actual code lives in modem.b00,
modem.b01, and so on next to it. The kernel puts them back together at boot.
Ghidra does not: open the .mdt and you get an empty database, with no warning
that anything is missing.
QDig gathers the pieces, lays each segment at its real address, picks the right processor, and then digs out the debug metadata the vendor forgot to strip.
git clone https://github.com/NerdTIV/QDig
cd QDig/Ghidra/loader
export GHIDRA_INSTALL_DIR=/opt/ghidra_12.1.2_PUBLIC
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
./build.sh installRestart Ghidra. That is all: no gradle, no Eclipse, just javac and jar.
Needs Ghidra 12.x, a JDK 21, and zip (build.sh packs the extension with it,
and a minimal Debian does not ship it). Older Ghidra works too, but without the
Hexagon processor module, so the modem images will not open.
Drop your .mdt and its .bNN files in the same folder, then open the .mdt
in Ghidra. The importer offers Qualcomm baseband image (MBN / split ELF), with
two checkboxes already ticked. Take both.
On a real EG25-G modem image:
before 0 bytes, nothing to disassemble
after 20 segments, 79 MB of Hexagon, 7418 commented descriptors
That is the whole workflow. No script to run, no address to work out by hand.
One case will look like a failure and is not. If your file is a complete ELF,
one that already carries its own code, QDig does not show up in the import dialog
at all and Ghidra offers you its usual Executable and Linking Format (ELF). Take
it: the stock loader handles relocations, sections and symbols, which this one
does not, so there is nothing to gain from getting in its way. QDig only steps in
where the stock loader would leave you with an empty database.
Quick way to tell which one you have, without opening Ghidra:
python3 Ghidra/scripts/qcom_img.py yourfile.mbnA plain segments : 7 means the file is complete, so use the ELF loader. If the
line reads segments : 20 (decoupe en .bNN), it is split and QDig is the one
you want. The per-segment lines say the same thing: an offset 0x3000 is a byte
range inside the file you opened, a .b00 is a byte range in a file next door.
This is the part worth the trouble. Qualcomm's debug logging compiles every message against a table of descriptors, and that table ships in the firmware. Each entry names a source file, a line number, and often the printf format.
QDig scans for them and drops a comment on each one:
84000000 smd_main.c:1257
840008f0 nvim.c:3462 %s,item:%d
Let Ghidra's analysis run and the references appear, so you know which function sits in which source file at which line. On the EG25-G that is 7418 descriptors, 6126 of them with the format string still in clear, across 472 source files.
The file names belong to the module vendor, not to Qualcomm:
quectel_common_atc.c 564 messages
quectel_ds_tcpip.c 296
quectel_http_client.c 246
Some formats even carry the function name, which is a free extra:
[Max][NDIS][Quectel_Get3GPP2FlagForCDMA] imsi: %s
On a firmware everybody calls stripped.
This reads the Qualcomm PIL format, so it is not tied to one chip. Tested on four unrelated families:
EG25-G modem.mdt Hexagon 20 segments, 79 MB modem baseband
wpss.mbn (WCN6750) Hexagon 7 segments WiFi
wlanmdsp.mbn (WCN3990) Hexagon 3 segments WLAN DSP
q6_fw0.mbn (IPQ5424) Hexagon 11 segments networking DSP
sbl1 / rpm / tz / mba ARM 1 to 8 segments bootloaders
So adsp.mdt, cdsp.mdt, venus.mdt, wcnss.mdt, and the modem of any
MDM9x07 module (Quectel EC2x and EG2x, the SIMCom and Fibocom equivalents) all
land in the same place. The debug scan is not a modem thing either: it finds
242 source files in a WCN6750 WiFi firmware.
A separate script, and it works on any binary that drives a modem, Qualcomm or not. It comments every string carrying an AT command with its family and risk category, renames the anonymous functions, and rebuilds the dispatch tables, which recovers handlers a plain xref never reaches.
It runs through pyghidra, so it needs a venv. analyzeHeadless will not do, it
refuses to run Python scripts and answers "not started with PyGhidra".
python3 -m venv ~/ghidra_venv
~/ghidra_venv/bin/pip install pyghidra
cd Ghidra
~/ghidra_venv/bin/python scripts/ghidra_at.py /usr/sbin/ModemManager --project /tmp/proj --name AT --surface data/surface.jsonOn ModemManager: 13928 strings read, 263 commented, 140 functions renamed, 36
dispatch entries. With surface.json around it also shows whether each command
is really supported on a reference modem, because knowing a function touches
+QFDWL matters mostly if the modem answers it.
It does not decompress anything. Real modem images pack some read-only data with q6zip or CLADE, and QDig measures the entropy of every segment so you are told which ones rather than left hunting for code that is not there. On the EG25-G the executable segments sit at 6.88 to 6.99 bits per byte and two data segments come out at 7.98 and 7.91.
If you need those unpacked, mzakocs/qualcomm_baseband_scripts does it, and
also decodes QSHRINK4 when the strings really were moved out into a .qdb.
Different layer, no overlap.
Beyond the split .mdt, the only other thing it claims is one oddity found on a
real sbl1.mbn, where an MBN header with every field at 0xffffffff hides a
complete ELF at offset 0x2800. No ELF magic at offset 0 means no ELF loader
ever sees that file, so Ghidra would only offer you Raw Binary.
31 checks, no failure. Same venv as above, and the extension has to be installed and Ghidra restarted first, otherwise everything fails at once:
cd Ghidra
~/ghidra_venv/bin/python tests/test_loaders.pyTwo sections run against real firmware. One takes a Hexagon image out of
/usr/lib/firmware, splits it into .mdt plus .bNN the way Qualcomm does,
and verifies the loader reassembles it and that Ghidra disassembles real
instructions from the result. Those skip cleanly on a machine with no Qualcomm
firmware installed.
The EG25-G numbers come from its published recovery image, pulled out of
NON-HLOS.ubi. It is the EC25's sibling, same MDM9x07. An EC25 image itself has
not been tried.
There is an IDA port of both tools, not published here. No IDA licence on this machine, so it has never run for real, and a loader nobody has executed once is how you collect bug reports you cannot reproduce.