Home Hacking a sat receiver
Post
Abbrechen

Hacking a sat receiver

Nerd-sniped

Recently, we bought a Satellite TV receiver for use at CCCAC — And another one to hack.

A sat receiver in its natural habitat

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.

PCB with chip with 'M' logo, M88CS8001

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+.

Baidu to the rescue

Baidu B2B listing "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.

Memory map in Ghidra

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 :-)