Opus Spreadtrum

It’s been like an eternity since I last wrote anything here. I won’t go into the details of why, but maybe blogging just isn’t for me. Probably because my hobby is the area where code speaks for itself. However, sometimes an interesting case emerges where you just can’t simply show the code, you feel the need to explain the whole story behind it. This story is about dumping Unisoc firmware. This story is about the project I had to complete in order to finally readback the flash memory of Philips Xenium E111 whose codes were a mystery for me and whose firmware was nowhere to be found. And this story is something I want to share.

If you remember MTreader, you remember how beautiful and elegant things are in feature phone MediaTeks (MT626x) when it comes to flash memory readback: you just enter the boot ROM, manipulate necessary registers to remap the flash memory onto virtual address space, et voilà, all your dump is available as 32-bit little-endian chunks you can directly read from the device with no hassle! And.. I was never so mistaken when I thought Unisoc chipsets were similar to that in any way. Oh no, they are a total mess. But first, let’s start with the basics. From now on, I’m going to only refer to the SC6531E/F/M as the primary research target.

Stage 1: the boot ROM and the protocol

The boot ROM is the part that, as the name suggests, is always there. It handles the hardware power-on sequence and starts the first bootloader. It, however, has a capability to enter the command mode that opens a special USB port (VID 0x1782, PID 0x4d00) with one IN and one OUT endpoints. To enter this mode, you need to power the device on (by connecting the USB cable) while holding a “bootkey” - a device-specific key that signals the chipset to trigger the boot ROM command mode. On the no-name Prestigio PFP1184DUO that I mostly used for testing, the bootkey is 9. On Philips Xenium E111, the bootkey is D-pad Center. And so on. It can also be Call key, Soft Left, Soft Right, #, 7 (the most popular options along with 9 and Center) or anything else except End. So it’s up to you to find the bootkey with trial and error.

Once we’re connected to the boot ROM command mode port, the next thing we should know is how to talk to it. Well, here’s how it should be and how it’s implemented at least in SC6531E/F. Remember HDLC protocol I mentioned in my Qualcomm-related posts? Unisoc uses quite a strange modification of it. Here’s how a typical HDLC frame looks in Unisoc:

1
7e [data packet] [2 byte CRC] 7e

The first difference from the Qualcomm implementation is that 0x7e bytes occur both at the beginning and the end of the HDLC frame. That sounds even more logical than in the Qualcomms. The second difference is that there are two types of CRCs depending on whether we’ve already booted into the FDL or not (more on that later). And the third, most strange difference is the escaping behavior. No, the rules are the same as for Qualcomm (0x7d0x7d 0x5d, 0x7e0x7d 0x5e) but the data we’re operating on when escaping the outgoing frame and unescaping the incoming frame are different from the standard approach.

Let me explain. Here’s the sequence we need to go through when shaping the HDLC frame to send ot to the device:

  1. Calculate the CRC on the raw data packet.
  2. Escape the data packet.
  3. Concatenate 0x7e, escaped data packet, two CRC bytes and 0x7e.

Now, how do we get the raw data packet from the HDLC frame received from the device? The reverse sequence, as everyone would assume, would look like this:

  1. Remove the first and the last 0x7e markers.
  2. Read the last two bytes as CRC and treat the rest as the data packet.
  3. Unescape the data packet.
  4. Calculate the CRC of the unescaped data packet and validate against the value we have previously read.
  5. If the values don’t match, report the error, otherwise return the unescaped packet.

However, this, as you may have already guessed, didn’t work. This is because, as it turned out, CRC also requires escaping in the frames sent to and unescaping in the frames received from the device! So, the correct sequence is as follows:

  1. Remove the first and the last 0x7e markers.
  2. Unescape the whole remaining sequence.
  3. Read the last two bytes as CRC and treat the rest as the data packet.
  4. Calculate the CRC of the data packet and validate against the value we have previously read.
  5. If the values don’t match, report the error, otherwise return the unescaped packet.

OK, but how do we calculate the packet CRC? As I already told, there can be two checksum types in the Unisoc’s HDLC frames - the one used in the boot ROM (some variant of XMODEM CRC) and the one used in FDLs (a simple 16-bit checksum). This is the case when it’s much simpler to just show the code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def crc16_xmodem(data: bytes): # xmodem used in boot mode 
crc = 0
data = bytearray(data)
msb = crc >> 8
lsb = crc & 255
for c in data:
x = (0xFF & c) ^ msb
x ^= (x >> 4)
msb = (lsb ^ (x >> 3) ^ (x << 4)) & 255
lsb = (x ^ (x << 5)) & 255
return (msb << 8) + lsb

def crc16_fdl(data: bytes): # used in FDL1/2 mode
crc = 0
data = bytearray(data)
l = len(data)
for i in range(0,l,2):
if i+1 == l:
crc += data[i]
else:
crc += (data[i]<<8)|data[i+1]
crc = (crc >> 16) + (crc & 0xffff)
crc += (crc >> 16)
return ~crc & 0xffff

Now that we know how to deal with this weird HDLC version, let’s talk about the commands and their responses. Not counting the HDLC frame, the generic outline of the data packet looks like this. All values are big-endian. 16b and 32b denote 16-bit and 32-bit values respectively.

Sync command (BSL_CMD_CHECK_BAUD): 0x7e

Any other command: [command_16b] [param_len_in_bytes_16b] [[param1_32b] [param2_32b]...]

Some commands don’t expect a response, but most do. If there is a response, it looks like this (after HDLC decoding): [response_code_16b] [response_length_16b] [response_data]

Some commands (like the one we’re going to use for flash reading) can return a special success code, but most of them return the generic BSL_REP_ACK success code (0x80). We can use this fact to check everything went well.

So, let’s try the boot ROM handshake. In SC6531E/F/M, this is the following command sequence (without HDLC encoding/decoding, responses shown as response_code, data):

  1. 0x7e -> 0x80, SPRD3
  2. 0x00 -> 0x80

In some other chipset variants, you may need to first loop-send 0x7e command “as is” without escaping, but this is subject to additional research.

Stage 2: FDL1 and FDL2

Now we know how to talk to the boot ROM. From the publicly available sources, it turned out to provide no direct memory interface similar to what we encountered in the MediaTeks. This is why we need additional pieces of code called FDLs (firmware downloaders, I guess) that use the same command protocol but allow us to do the things we need. Why several FDLs? Because in SC6531E/F/M, for instance, boot ROM only allows us to load a binary less than 16384 bytes in size. Hence this chainload.

Both the boot ROM and FDL1 provide three commands to load a binary into the specified memory address and one to execute the loaded binary under that address:

  1. BSL_CMD_START_DATA (0x1) - prepare to send the data into memory. Parameters: [addr_32b] [length_32b]
  2. BSL_CMD_MIDST_DATA (0x2) - transfer the data chunk to the device. Parameters: data buffer (max 1024 bytes recommended).
  3. BSL_CMD_END_DATA (0x3) - end the data transfer. Parameters: none.
  4. BSL_CMD_EXEC_DATA (0x4) - run the executable loaded into the memory. Parameters: [addr_32b]

So, now we can, using these commands in the boot ROM command interface, read the FDL1 binary, split it into chunks, send them to the device chunk by chunk, finalize the transfer and run the FDL1 code. The only remaining question is which address to send this binary to. Unfortunately, this is a need-to-know information that can, however, be retrieved from the XML descriptions of some firmware in the .pac format. And for SC6531E/F we know that it expects FDL1 at the address 0x40004000 and FDL2 at the address 0x14000000. So we just reuse that.

And how do we verify that FDL1 is up and running? Well, this is what had stalled me for like half a year as I gave up after desperately trying to get the communication working correctly. But the solution is as simple as a brick: we just reconnect to the device at this point. Then we do the same handshake as with boot ROM but just remember we must switch to the second CRC algorithm (crc16_fdl) from now on. If everything is done right, then, instead of SPRD3, we receive Spreadtrum Boot Block version 1.2 or whatever FDL1 response is set up during the handshake as a response to the sync command. This means we can proceed with uploading FDL2 to the second address and starting it up using the same procedure as described above.

Once we have uploaded FDL2 though, we must not reconnect to the device. Instead, we just verify it’s running by sending the BSL_CMD_CHANGE_BAUD (0x9) command with a single integer parameter set to our baudrate (921600, 0x000e1000) and validate the successful response code. And this is where the fun begins.

Stage 3: Flash reading interface

In case you have forgotten, we still are trying to read the flash memory contents from the phone. Everything described above is also required to do any other kind of operation (and I will probably research other things like flashing or NV item manipulation in this mode as well someday) but we have finally reached the point when FDL2 is already running on the device and we have working communication with it. So, what’s next?

Next, here is another portion of weird news for ya: regardless of whether it’s a smartphone or not, all FDL2 loaders available to the public (i.e. not as a part of InfinityBox CM2SCR or other prorietary Faildows-only BS) only operate with the concept of partitions. So, every operation for flash reading (as well as writing, I reckon) requires a partition ID (or partition name but this is not our case). Where to get it for non-smartphones? Well, there are two sources of truth to explore: XML files found in the .pac firmware and the disassembly of the FDL2 binary itself. In case of SC6531E/F/M, we’re looking for the partition ID marked as PS (“Protocol station”, whatever that means) with the ID 0x80000003. Turns out this is the ID that allows us to read any flash location on the phone. Just what we needed.

Of course, we also need the command itself. And this was another source of confusion to me since most source code I encountered was either using the commands to read partitions in a new “smartphony” fashion (and these didn’t seem to be even implemented in the FDL2 of interest) or used the correct command in a wrong way. Finally I figured out what was wrong and here it is in all its glory, 3-parameter BSL_CMD_READ_FLASH (0x6) command that accepts base address (in fact, it’s the partition ID I’ve written about), length of the data to read (I’d stick to 4096 bytes at a time for a good reason) and the offset where to start reading from. There is another caveat: since the response is going to be larger than a usual read buffer size, we send the command as the request and read back from the port until the end of the frame is encountered. Then we decode the whole received frame as usual and write the decoded chunk if everything went smoothly. To read other chunks we just manipulate the offset we pass to the command until we read all the partition data we want to. And that’s it!

After reading whatever data you needed, it’s always nice to signal the device to reset to the normal mode by sending the BSL_CMD_NORMAL_RESET (0x5) command that accepts no parameters but should return the 0x80 response code as usual.

Stage 4: Going beyond and writing to flash memory

(followup from 2021-12-20)

The next day after publishing this post first, I suddenly started the research on how to reuse the same FDLs for flashing. I even downloaded 989-page PDF specification for SC6531E from some obscure Chinese source, and also studied dloader source code with no apparent success, until it hit me that once again, everything is different here but in fact it’s even simpler than for reading. And it doesn’t even involve any partition IDs or labels or whatever.

Yes, it turned out that as soon as FDL2 is ready, our good old friend, BSL_CMD_START_DATA command, as well as its BSL_CMD_MIDST_DATA and BSL_CMD_END_DATA companions, is fully capable of writing any flash area, as long as we know the base address where flash resides. This is where that 989-page document came in handy where it said (on pages 95 to 96) that SC6531E can either map flash memory onto 0x0 or 0x10000000. Since zero mapping didn’t work, only the second option was left, and guess what… It worked! So, to write flash area, we just need to send BSL_CMD_START_DATA with the address above or equal to 0x10000000 and below [0x10000000 + size_of_your_flash_memory], and that’s it.

Another caveat, however, can await you after running the finalizer command, BSL_CMD_END_DATA: instead of BSL_REP_ACK (0x80), you may receive BSL_FLASH_CFG_ERROR (0xa4) for some unknown/unobvious reason, and you also won’t be able to send BSL_CMD_NORMAL_RESET afterwards. All this, however, can be safely ignored in case of SC6531E, as I verified the test targets to send and receive the whole flash range with no data errors. For SC6531F though, matters are much more complicated, so this post section is still subject to updates in the nearest future.

Conclusion

All my efforts to find and apply all this information resulted in the creation of UniFlash utility that’s freely available for everyone to use and study. It’s written in Python 3 and only requires PyUSB as a dependency. Dumped image extraction and repacking is another story but for now I just recommend to look at ilyazx’s bzpwork tool for that purpose.

As for the Xenium E111 though… I finally created its firmware dump, decoded it with bzpwork and, surprisingly, couldn’t find a way to access what I wanted, although I had seen it in the dump itself. What does this mean? This means another journey begins, and who knows where it may lead me. Adieu!

_