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)

Linux version 2.6.28.9-oslinuxR7.5 (root@lxdevenv) (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):

#!/usr/bin/python import socket pkt = "NOTIFY * HTTP/1.1 \r

" + \ "HOST: 239.255.255.250:1900 \r

" + \ "USN:uuid:schemas:device:" + \ "A" * 512 + ":end \r

\r

" s = socket . socket ( socket . AF_INET , socket . SOCK_DGRAM ) s . sendto ( pkt , ( '239.255.255.250' , 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.

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: 00004 D4C lw $ra , 0x158 ( $sp ) .text: 00004 D50 lw $s3 , 0x154 ( $sp ) .text: 00004 D54 lw $s2 , 0x150 ( $sp ) .text: 00004 D58 lw $s1 , 0x14C ( $sp ) .text: 00004 D5C lw $s0 , 0x148 ( $sp ) .text: 00004 D60 jr $ra .text: 00004 D64 addiu $sp , 0x160 .text: 00004 D64 # 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: 00004 DB4 jalr $t9 ; unique_service_name .text: 00004 DB8 move $a1 , $s0 .text: 00004 DBC lw $gp , 0x28 + saved_gp ( $sp ) .text: 00004 DC0 move $a0 , $s1 ; arg0 .text: 00004 DC4 la $t9 , 0x49C0 .text: 00004 DC8 or $at , $zero .text: 00004 DCC jalr $t9 ; ssdp_request_type1 .text: 00004 DD0 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 ; }

This port gives a lot of technical information on the system :

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

private

We can also extractpublic [0] RSA key to decrypt firmware updates with pflupg-tool

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