Creating QMI traces from Android phones with Frida to reverse engineer Voice over LTE

June 17, 20253 min. read

phone running postmarketOS showing the OpenIMSD homepage

This is a guest blog post by Alexander 'lynxis' Couzens, one of the people behind the OpenIMSd NLnet project funded by NGI Zero Core. At postmarketOS we helped them create a grant proposal, and continue helping with project planning. For more details, check the project website. We thank NLnet and the NGI Zero Core fund for supporting this initiative.

Introduction

To investigate how the Voice over LTE stack in Qualcomm based phones works, I have to look into the QMI interface. QMI is a structured interface using TLV. Furthermore, QMI is a protocol which can be used over different interface and offers communication between services. A service usually handles a logical component of the baseband, e.g. the communication with the simcard is handled by the UIM (User Identity Module). Or the NAS handles the high level Network Access. QMI also supports multiple clients at the same time. It allows two daemons to communicate via QMI at the same time without the need to synchronize with each other.

QMI supports at least communication over the following transports (not a full list):

To research the QMI VoLTE messages of a stock Android, we need to able to capture those messages. When I first encountered QMI and researched how the "legacy" voice calls are working on Qualcomm based phones, I used Qualcomm Diag for it using the osmo-qcdiag project. Sadly osmo-qcdiag only supports Qualcomm Diag when exposed as TTY on Linux via /dev/ttyXXXX, but not (as many other projects by now) directly via USB without a kernel driver. The issue with the kernel driver is the limited support of devices. When enabling Qualcomm Diag for a new phone, it requires a small patch to add the USB ID to the driver. Other projects like SCAT or QCSuper can use libusb to directly communicate with the phone.

Because of this I tried to add QMI sniffing in SCAT, but it didn't work with the phone. I didn't receive any messages after subscribing to the QC Diag messages.

But there are other methods to get the QMI messages:

I've noticed Frida which was used to capture QMI message on iPhones, described in this paper. To summarize: they used Frida, which attaches similar as a debugger to processes and injects code into the running binary to sniff the QMI messages.

iPhones communicate in a different way than Android, but I could use the same approach. The idea is:

How does it work?

Frida has two parts:

The client will instruct the server over adb to inject a JavaScript engine into a process, then connect to the JavaScript engine and upload a script. The script contains callbacks and code to extract the QMI message, Frida will transfer those to the client and the client will encapsulate QMI into GSMTAPv3.

How to use qmi-frida-tracer

Requirement: use a rooted LineageOS on an Android (OnePlus 6T).

qmi-frida-tracer was developed with Frida 16.6.6!

Download the server from github.

# ensure adb is able to run as root
adb root

# copy and run Frida server
adb push frida-server-16.6.6-android-arm64 /tmp/
adb shell chmod 700 /tmp/frida-server-16.6.6-android-arm64
# keep the server running
adb shell /tmp/frida-server-16.6.6-android-arm64 &

Now prepare and run qmi-frida-tracer:

git clone https://gitlab.postmarketos.org/modem/openimsd/qmi-frida-tracer
cd qmi-frida-tracer

# create a virtualenv and install frida
virtualenv venv
. venv/bin/activate
pip install -r requirements.txt

# like a `ps`. Show all processes
frida-ps -U
# use rild pid
./qmi-frida-tracer.py -p 9595

qmi-frida-tracer is sniffing all QMI messages of a process and encapsulate it into GSMTAPv3 and transmits it as UDP to localhost:4729.

Capture the QMI traffic as pcap and decode QMI messages:

tcpdump -i lo udp port 4729 -w /tmp/qmi.pcap
# and/or
# compile & run qmi-gsmtap-decode
cd ./qmi-gsmtap-decode/
mkdir build
cd build
cmake .. ; make
./qmi-gsmtap-decode | tee -a qmi.log

There are still some smaller parts open to improve.

Example captures

<<<< QMI
QMI QRTR:
QMI   length  = 21
QMI   service = "nas"
QMI   client  = 255
QMI QMI:
QMI   flags       = "indication"
QMI   transaction = 227
QMI   tlv_length  = 9
QMI   message     = "Signal Info" (0x0051)
QMI TLV:
QMI   type       = "LTE Signal Strength" (0x14)
QMI   length     = 6
QMI   value      = B4:F6:90:FF:2E:00
QMI   translated = [ rssi = '-76' rsrq = '-10' rsrp = '-112' snr = '46' ]
>>>> QMI

or

<<<< QMI
QMI QRTR:
QMI   length  = 16
QMI   service = "wds"
QMI   client  = 255
QMI QMI:
QMI   flags       = "none"
QMI   transaction = 3
QMI   tlv_length  = 4
QMI   message     = "Set IP Family" (0x004D)
QMI TLV:
QMI   type       = "Preference" (0x01)
QMI   length     = 1
QMI   value      = 06
QMI   translated = ipv6
>>>> QMI

<<<< QMI
QMI QRTR:
QMI   length  = 19
QMI   service = "wds"
QMI   client  = 255
QMI QMI:
QMI   flags       = "response"
QMI   transaction = 3
QMI   tlv_length  = 7
QMI   message     = "Set IP Family" (0x004D)
QMI TLV:
QMI   type       = "Result" (0x02)
QMI   length     = 4
QMI   value      = 00:00:00:00
QMI   translated = SUCCESS
>>>> QMI

More information

If you would like to contribute to OpenIMSD or follow its development, see these links: