Either Qual, or Comm

We already started living in quite a dystopian world. Think of it: almost everyone has a device with main CPU or at least baseband chipset produced by one of the following vendors: MediaTek, Qualcomm, Spreadtrum aka Unisoc (Tsinghua Unigroup which also now owns RDA Semiconductors), Huawei (which owns HiSilicon brand), Intel and - in especially lucky/retro cases - Infineon or Texas Instruments. Everything else is either so small-scale and/or obsolete that it doesn’t deserve attention. I deliberately left out Apple since their basebands are either based upon Qualcomm or - more recently - upon Intel, so they, as usual, have no own development in this area and can be excluded from the list.

So, in total, we now have 7 influential baseband vendors, 2 of which are already almost forgotten. Among the remaining 5, MediaTek and Unisoc rule in the low-budget area, Intel and Huawei control limited amount of brands, and almost every other flagship and mid-budget device, as well as most currently sold low-budget KaiOS devices, are operated under Qualcomm. Not to mention every CDMA1x-capable phone in the world, excluding some recent Huaweis. Effectively making the ones who understands and exploits Qualcomm radiomodules the masters of the current world order.

Can we come a bit closer to this mastery? I genuinely don’t know but we certainly can try…

Most tech resources praise Qualcomm as the most reliable and secure platform nowadays. And I must say, they’re right… from the generally approved point of view. I mean, the entire PBL/SBL/EDL/Firehose story just drives me mad. You can reverse-engineer the Sahara protocol but you just can’t do a thing without a properly signed loader. This loader is what our group is missing - the last artifact to gain a really full control over Nokia 8110 4G. Because however bad your partitions are messed up, you can restore them from EDL mode once you have this loader. Because EDL can’t be messed up in any way, and it’s absolutely failsafe. Not having a proper EDL/Firehose loader is a frustration for me, but it’s definitely a sign of how Qualcomm cares about the security of their systems.

However, when it comes to other security aspects, things don’t look just as well there. And the first obvious failure is holding sensitive NVRAM… oh, sorry, EFS items in the generally accessible partitions. So, here’s what’s going on. Probably some of you already know that the data Qualcomm baseband is working with are stored in the modemst1 and modemst2 partitions, while baseband firmware itself is usually loaded from a partition called just modem. And these partitions, modemst1 and modemst2, are somehow encrypted and not easily readable by anyone except the radiomodule itself. But at the same time, you can safely zero these encrypted partitions out, reboot and see them restored from somewhere! And as I have already shown in my previous post, the data in this “somewhere” is totally unencrypted. Where’s the logic? And by the way, couldn’t they put just put sensitive data like IMEIs into a write-once memory area like QFPROM in order to ensure impossibility of such a trick as the one denoted in that post?

But that’s not the main thing I was going to write about today. There also is another port of entry, at least when speaking of modern Qualcomms, that is much more ubiquitious and common as it can be also accessed on completely unrooted devices. It’s called the diagnostics interface (DM port). For those who cannot afford the luxury installing Faildows and official QXDM anywhere, let me just go over the port and protocol structure.

So, this port is available at USB composite device with PID 9092 (when ADB is disabled) or 9091 (when ADB is enabled). Different devices have different ways to enable it (for instance, for Nokia 8110 4G it’s ###adbg# code, for Redmi Go it’s *#*#13491#*#* code, for CAT B35 it’s just connecting to the cable with Storage (MTP) mode disabled or in the keylocked state). But whatever it is, it can be indicated by the distinctive PID (like 9091 or 9092) and the presence of bulk 512-block interface at Interface 0 and Endpoint 1 for both IN and OUT directions.

Now, what do we send, what’s the frame format? Well, for the DM protocol, Qualcomm uses an ancient format called HDLC (High-level Data Link Control). The rules are simple:

  1. Prepare the byte sequence according to the higher protocol definition (remember, ALL 16-bit and 32-bit values are little-endian, e.g. 550 is 26 02, not 02 26!)
  2. Compute CRC from the byte sequence using CRC-16-CCITT algorithm (with a generator polynomial of 0x8408).
  3. Escape the byte sequence using the following rule: byte 7e gets replaced with 2 bytes 7d 5e, byte 7d gets replaced with 7d 5d, everything else is not changed.
  4. Append the previously computed CRC of the unescaped sequence (in the BIG endian format, i.e. straight byte order!) to the escaped sequence.
  5. Append the frame ending byte (7e) to the result.

Decoding rules are the reverse of the encoding ones:

  1. Read and remove the last frame byte. If it’s not 7e, return “Invalid frame” error.
  2. Read the remaining last 2 bytes as CRC and remove them from the decoded stream.
  3. Unescape the remaining contents by replacing 7d 5e with 7e and 7d 5d with just 7d.
  4. Calculate CRC using the same CRC-16-CCITT algorithm. If it matches, return the unescaped contents. If it doesn’t, return “Invalid frame” error.

Here’s a possible implementation of this HDLC frame codec in JS (both encoding and decoding procedures return an Uint8Array result):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
function crc16(byteSeq, poly=0x8408) {
let crc = 0xFFFF, i, k, l = byteSeq.length
for(i=0;i<l;i++) {
let cur_byte = byteSeq[i]
for(k=0;k<8;k++) {
if((crc & 1) ^ (cur_byte & 1))
crc = (crc >> 1) ^ poly
else crc >>= 1
cur_byte >>= 1
}
}
crc = ~crc & 0xFFFF
crc = (crc << 8) | ((crc >> 8) & 0xFF)
return crc & 0xFFFF
}

function hdlcEncode(byteSeq) {
let crc = crc16(byteSeq), escapedSeq = [], i, l = byteSeq.length
for(i=0;i<l;i++) {
if(byteSeq[i] === 0x7e || byteSeq[i] === 0x7d) {
escapedSeq.push(0x7d, byteSeq[i] ^ 0x20)
}
else escapedSeq.push(byteSeq[i])
}
escapedSeq.push(crc>>8, crc&255, 0x7e)
return Uint8Array.from(escapedSeq)
}

function hdlcDecode(byteSeq) {
let escapedSeq = Array.from(byteSeq)
if(escapedSeq.pop() !== 0x7e) return null
let crc = escapedSeq.pop() | (escapedSeq.pop() << 8)
let l = escapedSeq.length, i, unescapedSeq = []
for(i=0;i<l;i++) {
if(escapedSeq[i] === 0x7d) {
i++
if(escapedSeq[i] === 0x5e || escapedSeq[i] === 0x5d) {
unescapedSeq.push(escapedSeq[i] ^ 0x20)
}
else return null
}
else unescapedSeq.push(escapedSeq[i])
}
return crc16(unescapedSeq) === crc ? Uint8Array.from(unescapedSeq) : null
}

I don’t care about the suboptimal CRC-16-CCITT implementation and untyped array operations internally, because we’re talking about 512 byte frame length at most here.

The actual data transfer is as follows: you send the frame to the endpoint 1, whichever length it is, and you MUST read the following 512 bytes from the same endpoint. No exceptions.

So, as the physical data arrangement becomes clear, let’s take a look at the logical part. And here I have to admit that we have the biggest lack of currently valid information ever. The only source is some obscure and outdated PDF document from Qualcomm themselves officially called “CDMA Dual-Mode Subscriber Station Serial Data - Interface Control Document 80-V1294-1 YP”. All known open-source QCDM implementations are based upon this document as well, so their existence doesn’t help us much. But anyway, let’s go over some of the known bits there which are still valid today.

Note: further command examples will be provided without trailing CRC and 0x7e marker.

Mode switching

Qualcomm documentation defines the 0x29 command to switch terminal modes, and its response is exactly equal to the request. The second byte can be one of the following:

  • 0x0 - Offline Analog;
  • 0x1 - Offline Digital;
  • 0x2 - Reset (reboot);
  • 0x3 - Offline Factory Test;
  • 0x4 - Online (normal mode);
  • 0x5 - Low Power;
  • 0x6 - Power Off.

Switching rules are as follows:

  • From Online mode, you can only switch to Low Power, Offline Analog, Offline Digital or Offline Factory Test mode.
  • From Offline Analog or Offline Digital, you can only switch to Reset mode.
  • From Offline Factory Test mode, you can switch to either Online, or Reset, or Offline Analog and Offline Digital modes.
  • From Low Power mode, you can only switch to Online Mode.
  • From Offline Analog or Offline Digital mode, you can only switch to Reset.
  • Power Off mode can be called from any current mode.

Security code entering

Qualcomm’s PDF defines two kinds of security codes protecting some memory items: SPC (Service Programming Code, 6 digits) and Security Password (8 digits). In most cases for GSM/HSPA/LTE handsets, SPC is equal to 000000 and SP code is equal to 00000000 respectively. Note that the digits here are represented as ASCII sequence. So, the commands to enter these codes - 0x41 and 0x46 respectively - would look as:

1
2
41 30 30 30 30 30 30
46 30 30 30 30 30 30 30 30

For both cases, if the second byte of response (first after the command) is 0x01, then the code is correct and security check is passed.

NVitem access

This was my primary reason why I even started this research. So here’s NVitem read command format (excluding trailing CRC and 0x7e marker): 0x26 [item_number_le] [130 zero bytes]. The response will have the item contents instead of zero bytes and 2 bytes of the operation status. Once again, the status is in the little-endian and the amount of possible statuses is less than 16, so the second byte will always be zero and we just need to look at the first one:

  • 0x0 - request successful;
  • 0x1 and 0xa - internal use;
  • 0x2 - command not recognized;
  • 0x4 - command failed;
  • 0x5 - the item was never written;
  • 0x6 - invalid parameter (probably the item doesn’t exist);
  • 0x8 - item not defined for this target;
  • 0x9 - free memory exhausted.

And now, let’s look at the command to write an NVitem: 0x27 [item_number_le] [128 bytes of payload] 0x00 0x00. Pretty much the same, and the status responses are also the same except two new status bytes are added:

  • 0x3 - NV memory is full;
  • 0x7 - item that you’re trying to write is read-only.

And guess what? That’s exactly what happens when you try rewriting item 550 (IMEI). Even after switching to one of the offline modes and entering both security codes as shown above, you’ll still get that status 7 after, for instance, zeroing the IMEI out with this query:

1
27 26 02 08 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

Moreover, this is where the problems start. Since even if we could override the first IMEI with this query, we don’t know how to switch the context to do the same for the second IMEI. Here’s where another, yet unexplored command feature comes in handy…

Direct EFS file structure manipulation

I encountered this pretty recently although it turned out to be present in the same PDF. Quite interesting, huh? So, all EFS operations are encapsulated within a single 0x59 command code. The second byte of the command denotes the exact operation, which is one of the following:
:

  • 0x0 - create directory;
  • 0x1 - remove directory (must be empty);
  • 0x2 - display directory list;
  • 0x3 - display file list;
  • 0x4 - read file;
  • 0x5 - write file;
  • 0x6 - remove file;
  • 0x7 - get file attributes;
  • 0x8 - set file attributes;
  • 0xa - iterative directory list;
  • 0xb - iterative file list;
  • 0xc - get available space.

After the command and operation code, variable-length data (depending on the code) follows. Not sure why 0x9 isn’t documented here, will probably find it out myself…

And the response has the following format the same 0x59 command, then operation code, then a status byte, then variable-length operation-specific result data. Here’s the list of possible status bytes:

  • 0x00 - operation success;
  • 0x01 - FS device failure;
  • 0x04 - file open;
  • 0x05 - file already exists;
  • 0x06 - file doesn’t exist;
  • 0x07 - directory already exists;
  • 0x08 - directory doesn’t exist;
  • 0x09 - directory not empty;
  • 0x0a - unknown operation;
  • 0x0b - illegal operation;
  • 0x0c - parameter error;
  • 0x0d - bad file name;
  • 0x11 - invalid operation for remote file;
  • 0x16 - out of FS space;
  • 0x17 - block full;
  • 0x18 - out of master directory space;
  • 0x19 - out of block list space.

Other status values between 0x00 and 0x19 are reserved and unused.

File/directory name structure is the ASCIIZ string prepended with its length. Data block structure is a block of maximum 256 bytes, prepended with its 2-byte length (in little endian). Attributes block structure is: 2-byte attribute value (see below), one-byte buffering flag and one-byte cleanup option in case of failure (0 - close file as is, 1 - delete, 2 - truncate, 3 - revert).

I’d happily go through the details but there is one problem: this EFS command set is already obsolete and unused in current Qualcomm systems. Bummer.

Switching to AT command and DLOAD interfaces

Well, this should be simple. To switch to DLOAD, first switch the operating mode to Offline Analog, then issue the 0x3A command. The phone should reboot to EDL mode then. To switch to AT command interface, just issue the 0x44 command from any mode. However, this… just doesn’t work. This 0x44 command is obviously too old, and now there are other ways to do this. Which leads us to…

Subsystem commands

This is where the most interesting stuff usually happens, but this is what we also know little of, except all Subsystem Dispatcher commands have the following structure: 0x4B [subsystem_id_1_byte] [subsystem_cmd_2_bytes] [request_payload]. That’s all I can tell you for now.

No, really. I can’t find a damn leak about how exactly we modify NV items with this thing. All I know for sure is two starting bytes: 4B 13. I know that 32-bit little-endian content length comes somewhere after the initial command bytes.

Let’s take a look at my attempt to read the /nvm/context1/550 file (IMEI2) that I copied and adapted from someone’s logs:

1
4b 13 27 00 09 00 00 00 12 00 00 00 2f 6e 76 6d 2f 63 6f 6e 74 65 78 74 31 2f 35 35 30 00

Here, I can just guess what’s going on: 4b 13 means this is the call to EFS2 subsystem, then a 16-bit subcommand 27 follows (little-endian), then we set the length of how many bytes we’re going to read (32-bit, little-endian), then we set the length of the file name including trailing zero byte (32-bit, little-endian), then we provide a null-terminated file name. However, whichever lengths I try to set, I encounter a single response (excluding CRC and 7e marker):

1
4b 13 27 00 ff ff ff ff 02 00 00 00 2f 6e

Well, the command is valid, otherwise the response would start with 0x13. But tell me please, what do those ff ff ff ff and 02 00 00 00 mean? Is this an error code or a readiness marker to start reading with some other command?

I think the answers will come when I analyze the leaked sources of some radiomodules that came recently. But now, let me introduce something really useful to conclude this post on a positive note…

LTE band unlocking

And here are our beloved NV items 6828 and 6829 that control which LTE bands the handset can connect to. What I know for sure is that we can at least control bands 1 to 64 with this one, but probably some more. The item bytes are allocating the bands as a bitmask like this:

1
[8..1] [16..9] [24..10] [32..25] [40..33] [48..41] [56..49] [64..57] ...

…where the number in brackets are the band numbers corresponding to the bits in each byte, from higher to lower.

So, if we write eight 0xff bytes into the items 6828 and 6829, we can be sure that we unlock all the LTE bands (up to 64) physically supported by the modem. First, we need to go offline and enter the security codes:

1
2
3
29 00 00
41 30 30 30 30 30 30
46 30 30 30 30 30 30 30 30

Then issue the write commands for 6828 (0x1aac) and 6829 (0x1aad):

1
2
3
27 ac 1a ff ff ff ff ff ff ff ff 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

27 ad 1a ff ff ff ff ff ff ff ff 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

And then reboot:

1
29 02 00

Now, we just have to insert the foreign SIM and figure out whether the frequencies we unlocked are physically supported by the radio.

To be continued. Definitely…

_