Nerd-sniped
Recently, we bought a Satellite TV receiver for use at CCCAC — And another one to hack.
This little box (PremiumX FTA 521S) has antenna inputs, HDMI and SCART for video output, two USB ports, a few buttons, and a RS232 port.
After plying the heat spreader off the SoC, an unfamiliar logo was revealed.
Entering the chip‘s part number, M88CS8001, into duckduckgo didn‘t reveal much, mostly just IC dealers (like hkinventory) and Sat TV forums.
The website of Ascent Communication Technology has a relatively detailed list of technical features for a different device based on M88CS8001, and it mentions a high‑performance dual MIPS CPU.
The crucial clue, as to who the mysterious „M“ company is, came from a Baidu search: A listing on Baidu B2B listed the chip as MONTAGELZ 混频器 M88CS8001-S030 QFN 19+.
Montage LZ indeed turns out to be the manufacturer of this SoC, and even mentions it on its website, albeit with little technical information.
Firmware analysis
The firmware can either be downloaded or read from the firmware flash with a SOIC-8 clip and flashrom.
A quick binwalk -A
confirms we‘re dealing with a MIPS CPU:
1
2
3
4
5
6
7
8
9
$ binwalk -A dump.bin
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
2176 0x880 MIPSEL instructions, function epilogue
2472 0x9A8 MIPSEL instructions, function epilogue
3336 0xD08 MIPSEL instructions, function epilogue
4876 0x130C MIPSEL instructions, function epilogue
...
Partition table
At offset 0x10000
(aka. at the 64 KiB mark), there‘s an interesting structure:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
00010000 2a 5e 5f 5e 2a 44 4d 28 5e 6f 5e 29 00 00 3f 00 |*^_^*DM(^o^)..?.|
00010010 00 00 00 04 00 00 40 00 15 00 30 00 fc 00 01 00 |......@...0.....|
00010020 00 04 00 00 56 72 04 00 5e f3 fd 3f 30 30 30 30 |....Vr..^..?0000|
00010030 30 30 30 31 61 76 5f 63 70 75 00 00 e6 07 01 06 |0001av_cpu......|
00010040 0a 2a 38 00 00 00 00 00 00 00 00 00 89 00 01 00 |.*8.............|
00010050 56 76 04 00 54 53 03 00 4e 43 52 43 30 30 30 30 |Vv..TS..NCRC0000|
00010060 30 30 30 31 69 6d 67 00 00 00 00 00 e6 07 01 06 |0001img.........|
00010070 0a 29 29 00 00 00 00 00 00 00 00 00 88 00 01 00 |.)).............|
00010080 aa c9 07 00 6c 42 21 00 4e 43 52 43 30 30 30 30 |....lB!.NCRC0000|
00010090 30 30 30 31 64 65 6d 6f 00 00 00 00 e6 07 01 06 |0001demo........|
000100a0 0a 2a 38 00 00 00 00 00 00 00 00 00 af 00 01 00 |.*8.............|
000100b0 16 0c 29 00 48 00 00 00 4e 43 52 43 30 30 30 30 |..).H...NCRC0000|
000100c0 30 30 30 31 69 72 31 00 00 00 00 00 e6 07 01 06 |0001ir1.........|
...
After a bit of fiddling around, I figured out some of the more important fields in this structure and wrote a little parser:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
nr. flags offset size crc32 name
0 00400000 400 47256 3ffdf35e av_cpu
CRC is correct!
1 00000000 47656 35354 4352434e img
2 00000000 7c9aa 21426c 4352434e demo
3 00000000 290c16 48 4352434e ir1
4 00000000 290c5e 39 4352434e ir2
5 00000000 290c97 9 4352434e fp
6 00000000 290ca0 10 4352434e sdram
7 00000000 290cb0 60 4352434e misc
8 00000000 290d10 3bc52 4352434e resource
9 00000000 2cc962 54fe 4352434e logo
10 00000000 2d1e60 1510 4352434e cas
11 00000000 2d3370 c7c6 4352434e radio
12 00000000 2dfb36 b2e0 4352434e radio2
13 00000000 2eae16 945 4352434e music_lo
14 00000000 2eb75b 6078 4352434e ssdata
15 00000000 2f17d3 9956 4352434e preset
16 00000000 2fb129 300 4352434e preset3
17 00000000 340000 80000 4352434e iwtable
18 00000000 3c0000 10000 4352434e iwview
19 00000000 3d0000 10000 4352434e ucaskey
I‘m calling it a partition table.
Ghidra
The first 64 KiB (offset 0x0
- 0x10000
) contain a bootloader, which is uncompressed, but the av_cpu, img, and demo partitions are compressed with
LZMA,
which binwalk -e
will gladly unpack:
1
2
3
4
5
6
7
8
9
$ file fw/_*.bin.extracted/*
fw/_av_cpu.bin.extracted/10: data
fw/_av_cpu.bin.extracted/10.7z: LZMA compressed data, non-streamed, size 670576
fw/_demo.bin.extracted/0: data
fw/_demo.bin.extracted/0.7z: LZMA compressed data, non-streamed, size 5675288
fw/_img.bin.extracted/0: data
fw/_img.bin.extracted/0.7z: LZMA compressed data, non-streamed, size 649701
fw/_resource.bin.extracted/0: data
fw/_resource.bin.extracted/0.7z: LZMA compressed data, non-streamed, size 808792
After some fumbling around with Ghidra, I finally loaded the code as follows:
partition | load address |
---|---|
bootloader | 0x9e000000 (raw from offset 0 to 0x10000) |
demo |
0x80008000 (decompressed) |
img |
0x80200000 (decompressed) |
av_cpu |
0x83e10000 (decompressed) |
The bootloader made it fairly straightforward to find the UART output routine, but not much else.
The demo
partition contains both a full-fledged operating system and the main
application of the sat receiver.
Fortunately there are enough strings that I was able to identify many drivers, and after two days in Ghidra I had a rough memory map.
Getting code exec
At the Kimiko Festival, I finally tried to get code exec on the device. To that end, I wrote a little program that would print a character on the serial port, in an endless loop:
1
2
3
4
5
li t1, 'A'
lui t0, 0xbf54
loop: sh t1, 0x100(t0)
b loop
nop
Unfortunately, my mips-linux-gnu-as
was quite unhappy to read what I had just
written:
1
2
3
4
start.s: Assembler messages:
start.s:1: Error: invalid operands `li t1,65'
start.s:2: Error: invalid operands `lui t0,0xbf54'
start.s:3: Error: invalid operands `sh t1,0x100(t0)'
I couldn‘t figure out why that was the case, so I opened the MIPS32 architecture manual and assembled these instructions by hand:
1
2
3
4
5
.word 0b001000 << 26 | 0 << 21 | 9 << 16 | 0xbf54 # li t1, 'A'
.word 0b001111 << 26 | 0 << 21 | 8 << 16 | 0xbf54 # lui t0, 0xbf54
loop: .word 0b101001 << 26 | 8 << 21 | 9 << 16 | 0x0100 # sh t1, 0x100(t0)
b loop
nop
With that, and a bit of objcopy
, I had a flat file with my code in it.
I compressed it (lzma -F alone start.bin
) and injected it into my flash
image. My first idea was to hijack av_cpu
, but it didn‘t quite work as expected
(instead resulting in a crash dump from the main application).
Instead of investigating this crash any further, I switched to injecting my code
into the demo
partition. The result wasn‘t right either, as execution seemed
to stop right at the time when my code should have run.
Binwalk offered a clue:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Scan Time: 2022-06-16 00:15:39
Target File: /.../fw/_demo.bin.extracted/0.7z
MD5 Checksum: ea0913a8f44a590cc792a44bfc369c32
Signatures: 411
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 5675288 bytes
Scan Time: 2022-06-16 00:15:39
Target File: /.../start.lzma
MD5 Checksum: 9cfa0594f9484a6ef23cbb4e9e9b729b
Signatures: 411
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: -1 bytes
The lzma
command (from XZ) doesn‘t set the uncompressed size field!
It even (almost) says so in the manpage:
Streamed vs. non-streamed .lzma files
The uncompressed size of the file can be stored in the .lzma header. LZMA Utils does that when compressing regular files. The alternative is to mark that uncompressed size is unknown and use end-of-payload marker to indicate where the decompressor should stop. LZMA Utils uses this method when uncompressed size isn‘t known, which is the case, for example, in pipes.
I tried to find an option to change this behavior, but couldn‘t, so I resorted to patching the size field manually, from:
1
2
3
00000000 5d 00 00 80 00 ff ff ff ff ff ff ff ff 00 2a 2f |].............*/|
00000010 bd 22 07 c0 db 2e 6c fb 52 4d f9 ed 41 a4 32 c4 |."....l.RM..A.2.|
00000020 1f cb af 6a 7f ff f7 b7 08 00 |...j......|
to:
1
2
3
00000000 5d 00 00 80 00 20 00 00 00 00 00 00 00 00 2a 2f |].... ........*/|
00000010 bd 22 07 c0 db 2e 6c fb 52 4d f9 ed 41 a4 32 c4 |."....l.RM..A.2.|
00000020 1f cb af 6a 7f ff f7 b7 08 00 |...j......|
The result was, to my surprise and joy, a success!
1
2
3
4
5
6
7
8
9
10
11
12
[...]
*****************************************
** Board: mips CPU: sym - MIPS 24KEc
** SOC name : 0x8080
** PACKET type : SIP_68S_DDR2
*****************************************
DRAM: 20 MiB
phy_clk = 405, clk=50
R_SPIN_CH0_BAUD: 40000009
[0x1c 0x3016].TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT
...
Yay!!
Wait, why T instead of A?
The answer is in my hand-assembled MIPS code. I made a copy-paste error and
loaded 0xbf54
instead of A (0x41
). The UART doesn‘t care about the upper
bits (0xbf00
), but the lower bits, 0x54
, are indeed T.
Holding it wrong
After I asked on twitter why GNU as didn‘t want to assemble my code, I was
pointed at regdef.h
, which defines the register names on MIPS. After
including it (and changing the filename from start.s
to start.S
to enable
the C preprocessor), I could finally write normal MIPS code, as I had
originally intended.
Conclusion
It‘s a computer!
Now that I can run my own code on it, I might try porting Linux or something :-)