Today we’ll be jailbreaking the Netgear NTV300 set top box…with a TV remote.
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:
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:
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:
In fact, system() and popen() are used generously throughout the code. These are particularly interesting:
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:
So what happens if we tell the NTV300 to connect to an SSID named “`reboot`”?
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:
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:
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:
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:
We can now change the DHCP settings back to dynamic, connect the NTV300 to our access point and telnet in (username root, no password):
Rooted with nothing but the remote control it came with. That’s all folks.