Exploitation of Philips Smart TV

Posted on Thu 13 November 2014 in Article

This post is a translated summary of the article published for my talk at SSTIC 2014 conference (french).

My Philips Smart TV is a Linux box standing there in my living room : that's a sufficient reason to try to get root.

Debug serial port

Internet hackers have already discovered a serial port on the back panel of the TV set.
Serial port (Jack plug)
This port gives a lot of technical information on the system :

Linux version
([email protected]) (gcc version 4.2.4) #1 Thu Jun 16 23:27:36 CEST 2011
console [early0] enabled
CPU revision is: 00019651 (MIPS 24Kc)
FPU revision is: 01739300
282 MB SDRAM allocated to Linux on MIPS
512 MB total SDRAM size
Endianess : LITTLE

UPnP library identification

A network scan reports a running UPnP service. In January 2013, Rapid7 discovered many vulnerabilities in libupnp library, v1.6.18. To check if the device is vulnerable, we send a simple UDP packet that can trigger one of them (CVE-2012-5958):
import socket
pkt = "NOTIFY * HTTP/1.1\r\n" +\
   "HOST:\r\n" +\
   "USN:uuid:schemas:device:" +\
   "A" * 512 + ":end\r\n\r\n"
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.sendto(pkt, ('', 1900))
We can see in the console that a crash occurred:
03 <2> 001990235 Exception in process 443: SIGSEGV: address not mapped to object
03 <2> 001990235 EPC = 0x41414141
03 <2> 001990235 RA = 0x41414141
Execution flow has been redirected to an arbitrary address, so we know this device uses a vulnerable version of libupnp. Moreover, it indicates there's no stack-smashing protection.

In these conditions, exploitation could be easy if we had had access to this binary or loaded shared libraries.
But it's not the case: firmware updates are encrypted, and there's no public method to get a shell on this system, at this time.
Unfortunately, the path traversal vulnerability found by Martin Schobert is not present in our firmware.

Memory mapping discovery

CVE-2012-5958 is a remote stack overflow in the "unique_server_name" function. We cross-compile the same version of libupnp used in the TV set (1.4), for the same architecture (MIPS32) with the same compiler (GCC 4.2.4).
Then we disassemble the vulnerable function :
.text:00004D4C                lw      $ra, 0x158($sp)
.text:00004D50                lw      $s3, 0x154($sp)
.text:00004D54                lw      $s2, 0x150($sp)
.text:00004D58                lw      $s1, 0x14C($sp)
.text:00004D5C                lw      $s0, 0x148($sp)
.text:00004D60                jr      $ra
.text:00004D64                addiu   $sp, 0x160
.text:00004D64  # End of function unique_service_name
Function epilogue restores 4 registers (plus $ra) before returning to calling function. The stack overflow allows to overwrite them with almost arbitrary values (there's a lot of forbidden bytes).

Among functions that call "unique_server_name", the "ssdp_request_type" function uses registers $s0 & $s1 right after the call return.
.text:00004DB4                 jalr    $t9 ; unique_service_name
.text:00004DB8                 move    $a1, $s0
.text:00004DBC                 lw      $gp, 0x28+saved_gp($sp)
.text:00004DC0                 move    $a0, $s1 ; arg0
.text:00004DC4                 la      $t9, 0x49C0
.text:00004DC8                 or      $at, $zero
.text:00004DCC                 jalr    $t9 ; ssdp_request_type1
.text:00004DD0                 sw      $zero, 8($s0); write 0 @ $s0+8
Register $s1 is passed as parameter to the function "ssdp_request_type1", which reads it as a string pointer.
Register $s0 is dereferenced to write the null value.
After that, these registers are not used anymore until the end of the function where they'll be restored.

Overwriting saved values of one of these registers $s0, $s1 and $ra with an arbitrary memory address can lead to crash the process if this address is not mapped, or respectively not writable, readable, or executable.

Crashes can be detected in many ways:
  • denial of service : the process doesn't answer anymore to UPnP requests
  • specific network activity : the process broadcasts specific packets at startup
  • crash reports on serial port: from far the handiest method
The observation of process' behavior allows to deduce if an address is mapped and its associated permissions.

By repeating this task in an automated way, it's possible to discover a part of process' memory mapping :
0x00402020-0x00532120   r-x
0x00542020-0x0091af20   rw-
0x0091b020-0x00927efc   ---
0x00928020-0x00930920   rw-
0x00942920-0x00980920+  rwx
Stability of results indicates that these memory regions are not randomized. We can see that last one is a variable-sized executable area. We make the hypothesis this area is the heap.

Injecting arbitrary code

We assume that heap is executable. As libupnp library is open source, we know how UPnP packets are handled, and which ones are stored in heap memory.
Thus, we send a custom UPnP packet to put our arbitrary code at an unknown address in the heap. No memory corruption involved here.

Finding arbitrary code location

As we've already said, this stack overflow allows to arbitrary modify 4 registers ($s0-3) before returning from the vulnerable function. Right after, "ssdp_request_type1" function is called with a single argument $a0 copied from $s1, so we can choose its value.
This function uses its unique argument as a string pointer and checks if it contains some static substrings.
enum SsdpSearchType ssdp_request_type1( IN char *cmd ) {
    if( strstr( cmd, ":all" ) != NULL )
        return SSDP_ALL;
    return SSDP_SERROR; }

If at least one static substrings is found in the string pointer, the UPnP process will respond to our request. This behavior lets us know if an arbitrary string pointer contains a specific substring.

So we put one of these substrings (":all" for example) in our arbitrary code, and we use this behavior to search its address in the heap area (we've already discovered heap start address in a previous section)
As we need to send many UPnP packets and to monitor responses, this process takes few minutes.

Remote arbitrary code execution

We are able to put our arbitrary code in heap memory (executable), find out its address, and execute it. Thereby, we get shell access to this system. We can notice that :

  • all processes are root
  • stack and heap are executable
  • stack is not randomized
We can also extract private public [0] RSA key to decrypt firmware updates with pflupg-tool.

[0] Edit 2014/11/14 : Thanks andreashappe for pointing out this mistake.