Jailbreaking the NeoTV – /dev/ttyS0

Today we’ll be jailbreaking the Netgear NTV300 set top box…with a TV remote.

The Netgear NeoTV 300

Negear’s NeoTV set top boxes are designed to compete with the popular Roku, and can stream video from all the usual sources (Netflix, HuluPlus, Youtube, etc). The NTV300 is one of the least expensive NeoTV models, and while a GPL release is available, it contains only copies of the various standard open source utilities used by the NTV300. All the interesting bits – such as Netflix streaming, or the ability to build a custom firmware image – are not included.

Inside the NTV300 we find a Mediatek ARM SoC, a 128MB NAND flash chip and 256MB of RAM:

Inside the NTV300

The four pin header in the top right corner of the PCB is a serial port (115200 baud 8N1), and while it provides access to the U-Boot boot loader, it does not provide a root shell. After the system boots, it displays copious debug messages and allows for rudimentary control over the NTV300’s user interface (i.e., pressing the right arrow key on the keyboard while in the serial terminal is the same as pressing the right arrow key on the remote control). Various attempts to send BREAK and SIGINT signals have no affect; we’ll have to dig a little deeper into this one.

Luckily, the firmware updates for the NTV300 aren’t encrypted. A binwalk scan of the firmware update image reveals a few firmware headers and two SquashFS images:

DECIMAL         HEX             DESCRIPTION
-------------------------------------------------------------------------------------------------------
63944           0xF9C8          Mediatek bootloader
111840          0x1B4E0         Mediatek bootloader
128133          0x1F485         LZMA compressed data, properties: 0x80, dictionary size: 1073741824 bytes, uncompressed size: 196608 bytes
293660          0x47B1C         JFFS2 filesystem data little endian, JFFS node length: 8195
410769          0x64491         LZMA compressed data, properties: 0x02, dictionary size: 8388608 bytes, uncompressed size: 1073741824 bytes
410793          0x644A9         LZMA compressed data, properties: 0x02, dictionary size: 8388608 bytes, uncompressed size: 1073741824 bytes
410817          0x644C1         LZMA compressed data, properties: 0x02, dictionary size: 8388608 bytes, uncompressed size: 1073741824 bytes
428064          0x68820         uImage header, header size: 64 bytes, header CRC: 0x2023172F, created: Tue Oct 16 04:37:00 2012, image size: 1896744 bytes, Data Address: 0xDA00000, Entry Point: 0xDA00000, data CRC: 0xFD61E493, OS: Linux, CPU: ARM, image type: OS Kernel Image, compression type: none, image name:
429156          0x68C64         LZMA compressed data, properties: 0x87, dictionary size: 250216448 bytes, uncompressed size: 14786800 bytes
445513          0x6CC49         gzip compressed data, from Unix, last modified: Sun Oct 14 23:00:19 2012, max compression
4182784         0x3FD300        Squashfs filesystem, little endian, version 4.0, compression: gzip, size: 76854395 bytes, 905 inodes, blocksize: 131072 bytes, created: Tue Oct 16 23:34:59 2012
30793205        0x1D5DDF5       PNG image, 133 x 133, 8-bit/color RGBA, non-interlaced
70987253        0x43B2DF5       JFFS2 filesystem data little endian, JFFS node length: 102880
72970663        0x45971A7       PNG image, 240 x 204, 8-bit/color RGBA, non-interlaced
73055216        0x45ABBF0       PNG image, 240 x 204, 8-bit/color RGBA, non-interlaced
73172060        0x45C845C       PNG image, 240 x 204, 8-bit/color RGBA, non-interlaced
73261506        0x45DE1C2       PNG image, 240 x 204, 8-bit/color RGBA, non-interlaced
73386095        0x45FC86F       PNG image, 240 x 204, 8-bit/color RGBA, non-interlaced
73436271        0x4608C6F       PNG image, 240 x 204, 8-bit/color RGBA, non-interlaced
78240759        0x4A9DBF7       PNG image, 780 x 870, 8-bit/color RGBA, non-interlaced
81538240        0x4DC2CC0       Squashfs filesystem, little endian, version 4.0, compression: gzip, size: 17109954 bytes, 326 inodes, blocksize: 131072 bytes, created: Thu Oct  4 01:54:51 2012
98651328        0x5E14CC0       PNG image, 1280 x 720, 8-bit/color RGB, non-interlaced
98675264        0x5E1AA40       PNG image, 720 x 480, 8-bit/color RGB, non-interlaced

While the firmware update does not appear to contain a complete file system, most of the interesting stuff appears to be in the first SquashFS image. The /usr/local/bin/ntv300ui binary is particularly interesting as it is responsible for providing the NTV300’s user interface, including the handling of user input from both the remote control and the serial console.

Although the ntv300ui binary has been stripped, there are plenty of debug printfs that reveal the original function names:

Printf’s reveal original function names

A quick IDAPython script takes care of renaming most of these functions:

import re

funcs = []
regex = re.compile('^[a-zA-Z_]*$')

for xref in XrefsTo(LocByName("printf")):
        ea = xref.frm
        found = False
        real_name = None

        for i in range(0, 10):
                ea -= 4
                if GetMnem(ea) == "LDR":
                        opnd = GetOpnd(ea, 0)
                        if opnd == "R1":
                                r1_string = GetString(LocByName(GetOpnd(ea, 1)[1:]))
                                if r1_string is not None and regex.match(r1_string) and len(r1_string) > 3:
                                        real_name = r1_string
                                else:
                                        real_name = None
                        elif opnd in ["R0", "R2", "R3"]:
                                r3_string = GetString(LocByName(GetOpnd(ea, 1)[1:]))
                                
                                if r3_string is not None and '%s' in r3_string: 
                                        found = True
                                        break
                                else:
                                        found = False
        if found and real_name is not None:
                name = GetFunctionName(xref.frm)
                if name not in funcs:
                        funcs.append(name)
                        print real_name
                        MakeName(LocByName(name), real_name)

print "Renamed %d functions!" % len(funcs)

With functions properly named, reversing can begin in ernest, and the code in ntv300ui isn’t exactly confidence inspiring. It looks like Netgear hired some Unix admins and told them to write an application in C; for example, here is how they re-implemented libc’s stat() function:

How not to stat a file

In fact, system() and popen() are used generously throughout the code. These are particularly interesting:

System calls to iwpriv

Popen calls to iwpriv

System call to wpa_cli

The SSID and encryption key values are used as part of system() and popen() calls. So where do the SSID and network key values come from? You guessed it, the user:

User controlled data!

So what happens if we tell the NTV300 to connect to an SSID named “`reboot`”?

Command injection via SSID

Connecting to `reboot`

Rebooting!

Sweet! Since we are already connected to the serial port, it would be nice if we could spawn a shell for ourselves on the serial terminal. Let’s try:

Connecting to “;/bin/sh #

Shell successfully spawned on the serial terminal

While this provides us with a minimalist shell, it is not very user friendly. There is no command echoing, and a ton of debug output is intermixed with the command output. Let’s see if we can find an easier way to get a shell – preferably one that doesn’t involve taking the device apart.

Examining the file system on the live device, there are plenty of files and directories that were not included in the firmware update file. Checking out some of the start up scripts, we find this juicy piece of code in /root/rc.user:

if [ -f /mnt/ubi_boot/mfg_test/enable ]; then
                echo "[WNC RD] Maufacturing Mode"
                #chmod +x /mnt/ubi_boot/mfg_test/*.sh

                #if [ ! -f /mnt/ubi_boot/mfg_test/disable_app_player ]; then
                #       #(sleep 3; /usr/local/mfg_test/play_power_on.sh) &
                #       /usr/local/mfg_test/play_power_on.sh &
                #fi

                echo "[WNC RD] Set ip forward"
                echo 1 > /proc/sys/net/ipv4/ip_forward

                #chmod +x /mnt/ubi_boot/mfg_test/reset
                /usr/local/mfg_test/reset &

                echo "[WNC RD] Set Ethernet Fixed IP: [192.168.0.100]"
                #ifconfig eth0 192.168.0.100 netmask 255.255.255.0 up
                echo -n 0 > /mnt/ubi_boot/settings/NetworkInterface
                echo -n 1 > /mnt/ubi_boot/settings/IpMode
                echo -n 192.168.0.100 > /mnt/ubi_boot/settings/IpAddress
                echo -n 255.255.255.0 > /mnt/ubi_boot/settings/SubNetMask
                echo -n 0.0.0.0 > /mnt/ubi_boot/settings/Gateway
                echo -n 0.0.0.0 > /mnt/ubi_boot/settings/PrimaryDNS
                echo -n 0.0.0.0 > /mnt/ubi_boot/settings/SecondaryDNS

                sync

                echo "[WNC RD] enable telnetd"
                inetd -d &
                (sleep 5; ifconfig eth0 192.168.0.100 netmask 255.255.255.0 up; ping -c 1 192.168.0.10; ping -c 1 192.168.0.11) &

        else
                echo "[WNC RD] Normal mode"

                # XBMC Server
                if [ -f /usr/local/bin/xbeventd -a -e /mnt/fifo ]; then
                        /usr/local/bin/xbeventd /mnt/fifo &
                fi

                if [ -f /usr/local/bin/xbhttpd ]; then
                        /usr/local/bin/xbhttpd
                fi

                if [ -f /usr/local/bin/xbmdns ]; then
                        /usr/local/bin/xbmdns &
                fi

        fi

It checks to see if the /mnt/ubi_boot/mfg_test/enable file exists, and if so, it fires up a telnet service (among other things). However, the mfg_test directory doesn’t exist at all on the production system:

Directory listing of /mnt/ubi_boot/

But with the SSID command injection vulnerability, we can easily create it. The commands to create the file are too long to fit into the restricted 32-character SSID input field, so we’ll echo them piecemeal into a shell script and then execute that script:

cd /mnt/ubi_boot

mkdir mfg_test

cd mfg_test

echo >> enable

/bin/sh /tmp/a

Finally, we power cycle the box. If successful, the NTV300’s IP address should have been set statically by the /root/rc.user script upon reboot. Let’s check:

Static IP settings

We can now change the DHCP settings back to dynamic, connect the NTV300 to our access point and telnet in (username root, no password):

Root telnet shell

Rooted with nothing but the remote control it came with. That’s all folks.

Bookmark the permalink.

Comments are closed.