Eoin Carroll, Charles McFarland, Kevin McGrath, and Mark Bereza contributed to this report.
The Internet of Things promises to make our lives easier. Want to remotely turn lights and appliances on and off and monitor them online? A “smart plug,” a Wi-Fi–connected electric outlet, is one simple method. But IoT devices can turn into attack vectors if they are not properly secured.
The McAfee Labs Advanced Threat Research team is committed to uncovering security issues in both software and hardware to help their developers provide safer products for businesses and consumers. We recently investigated a consumer product produced by Belkin. Our research into the Wemo Insight Smart Plug led to the discovery of an unreported buffer overflow in the libUPnPHndlr.so library. This flaw, CVE-2018-6692, allows an attacker to execute remote code. Following our responsible disclosure policy, we reported this research to Belkin on May 21.
Can this vulnerability lead to a useful attack? A smart plug by itself has a low impact. An attacker could turn off the switch or at worst possibly overload the switch. But if the plug is networked with other devices, the potential threat grows. The plug could now be an entry point to a larger attack. Later in this report, we will look at one possible attack.
Exploring the attack surface
Following the manual’s advice, the team used the Wemo phone application to set up the smart plug. We were able to remotely turn the outlet on and off. We then tested the software, including port scanning, monitoring normal network traffic, and reading online research. The Wemo listens on Universal Plug and Play (UPnP) ports TCP 49152 and 49153. The manuals, disassembly images, and the general-purpose programming language (GPL) were all online; they provided information on CPU architecture, the operating system, and applications.
We turned to the hardware and disassembled the device. We identified chips on the main board, found headers for communicating with the device, and pulled the memory off flash. Our online research provided datasheets for each of the chips on the board.
We found universal asynchronous receiver-transmitter (UART) pads on the board and confirmed them with the documentation. We soldered wires to these headers to discover if they were actively transmitting. To test communication with the device, we used an Exodus XI Breakout board, shown below:
After brute-forcing the baud rate, we were able to get debug information via the UART interface. The UART also provided a login prompt; however, neither online research nor simple guessing led us to a working password.
Extraction and firmware analysis
The flash chip discovered on the board was a Maxronix MX25L12835F, which is supported by flashrom, a well-known open-source tool for extracting firmware. Using flashrom and the XI Breakout board, we extracted the firmware from the Wemo device. After we obtained the original firmware image shipped with the device, we updated it using the Wemo mobile application. Once the device was updated, we again extracted the firmware from the device, providing us with a second image. We ran basic sanity checks with the new firmware to ensure our earlier software reconnaissance had not changed.
With the firmware extracted, we analyzed the firmware using binwalk, an open-source binary analysis tool. Binwalk extracted the file system from the firmware for further inspection. Access to the file system allowed us to review system configuration and access binaries.
Finding a vulnerability
Network or remote vulnerabilities are more dangerous than local flaws, so we took a close look at the UPnP ports listening on the local network. During this testing phase our lead analyst was taking a class on Exodus Intelligence Embedded Exploitation. One of the class instructors, Elvis Collado (@b1ack0wl) was developing a UPnP fuzzer and offered to assist our efforts. Using this tool we started fuzzing the open UPnP ports, while monitoring the UART interface on the Wemo. After a short time we saw a crash on the UART interface.
11:37:16.702 stuntsx0x46ac6 STUN client transaction destroyed
sending SIGSEGV to wemoApp for invalid write access to
464d4945 (epc == 2ac1fb58, ra == 2ac1fccc)
Cpu 0
$ 0 : 00000000 00000001 0000006d 464d4945
$ 4 : 31d2e654 31d2e770 00000003 00000001
$ 8 : 0000007c fffffff8 00000007 00000002
$12 : 00000200 00000100 00000807 00000800
$16 : 31d2e6f0 31d2e898 004a1cb8 00000002
$20 : 31d2e638 31d2e6c0 004a1388 31d2e640
$24 : 00000400 2ac1fb30
$28 : 2ac77d40 31d2e600 31d2e648 2ac1fccc
Hi : 00000008
Lo : 00000000
epc : 2ac1fb58 Tainted: P
ra : 2ac1fccc Status: 0100fc13 USER EXL IE
Cause : 8080000c
BadVA : 464d4945
PrId : 0001964c
Modules linked in: softdog rt_rdm rt2860v2_ap(P) raeth
Process wemoApp (pid: 2157, threadinfo=80fa0000, task=802c87f0)
Stack : 2a0000d0 fffffffe 31d2e6f0 31d2e770 31d2e76f 31d2e6f0 31d2e6f0 31d2e770
00000000 31d2e604 00000000 00000000 2ac77d40 00000000 4f464751 4a484d4c
4e444241 47454f49 50464658 45414d42 43445044 464d4945 5552414c 46495048
4b524141 41445a4f 44534e4a 4e4e494c 44434357 494a4855 44515455 44494b45
55584a44 584e4f52 545a5247 51545954 595a4c42 4e594a45 484f5158 46474944
…
Call Trace:
Code: 80a20000 50480004 a0600000 <5440fffa> a0620000 a0600000 10a00006 24840004 24a50001
thready: Destructor freeing name “ChildFDTask”.
Aborted
After repeating and closely observing the experiment several times, we determined that the crash was caused by the following packet:
POST /upnp/control/basicevent1 HTTP/1.1
Host: 192.168.225.183:49154
User-Agent: python-requests/2.9.1
Accept: */*
Connection: keep-alive
SOAPAction: “urn:Belkin:service:basicevent:1#UpdateInsightHomeSettings”
Content-Type: text/xml
Accept-Encoding: gzip, deflate
Content-Length: 3253
<?xml version=”1.0″ ?><s:Envelope s:encodingStyle=”http://schemas.xmlsoap.org/soap/encoding/” xmlns:s=”http://schemas.xmlsoap.org/soap/envelope/”><s:Body><b1ack0wl_ns:UpdateInsightHomeSettingsxmlns:b1ack0wl_ns=”urn:Belkin:service:basicevent:1″><EnergyPerUnitCost>210</EnergyPerUnitCost><Currency>236</Currency><EnergyPerUnitCostVersion>KWWZWIVYBQZKDGSSAAPBCQVQQFAVYZEOEUFIDXXQPDYGESTOD
GIJFERXZNMYAFJQLUZPSIJXFQSPADCRIVHDAJLLPQMPLAVECIQUWLXDLIGPLBKCROGPOCVUI
KTSLIIXULOEBVFKWIERCFGHWHCBBDLWFBKBZXAVGRKTDALDNRPOFQJDXAEOC(…snip…)XHU
OUZPCHUBFGLLWSJBFYFOMCGZZMJIQIUVCDETFBRBZVDVKNBVZFBRSVBSZPAYKZYNQZEQPDV
DWSZNDUPUDCPAVWNFBFBTYMXTBNCWTBJPKORUBHBSCQBPOPOBZNVADMGWRI
</EnergyPerUnitCostVersion></b1ack0wl_ns:UpdateInsightHomeSettings></s:Body></s:Envelope>
For space reasons some of the payload has been removed. (The original data in “EnergyPerUnitCostVersion” was 2,828 characters.) After examining the crash data and the packet, this appears to be a standard buffer overflow, in which data is being overwritten onto the stack. We continued fuzzing, now focused on the “EnergyPerUnitCost” field and found we needed only 32 characters to crash the application.
Although the crash dump provides us with a lot of good information, there is still a lot we do not know. For example, the crash occurs in the “WemoApp” and provides us an offset, but what is the base address of this library? What has been overwritten on the stack? Without access to the application during runtime these questions are difficult to answer. Because we obtained the file system earlier, we could statically analyze the WemoApp binary; but we would still be unable to determine the exact point of the crash easily.
To answer these questions we needed to take one of two paths. We could virtualize the Wemo firmware or binary to continue testing; or if we could determine the root password on the UART interface, there is a chance we could debug on the device itself. Generally, virtualizing firmware is not simple and can sometimes lead to inaccurate test results. It is better to debug on the device. With all the information we found during reconnaissance, it seemed promising that we could bypass the root password. (We did spend some time attempting to virtualize the WemoApp—with no success.)
Bypassing the root password
From the extracted file system, we learned the Wemo runs the embedded Linux system OpenWRT, with the user account information held in either the standard /etc/passwd or /etc/shadow files. We extracted the hash for the root password from /etc/passwd and submitted it to a cracking rig. This method proved ineffective in a reasonable amount of time.
With our ability to read the flash chip we had a good chance to write to the chip. Barring any checksum or validations done on the firmware, we might be able to replace the /etc/passwd file with a known password.
To test this theory, we would have to repack the firmware. Since the GPL for the Wemo is public, we chose to use the same tools used by the developers. Using the GPL, we compiled the same version of squash tools 3.0 with Izma and repackaged the firmware file system with a modified /etc/passwd file. We added padding to ensure the firmware section was the same size as the original. We then used “dd” to insert the new file system segment into the firmware binary. During this process, we discovered that using binwalk to extract the firmware prevented us from correctly repackaging the firmware. We used “dd” with the information provided by binwalk to extract the correct section of the firmware binary for repackaging.
With a new firmware binary in hand, we used the XI Breakout board and flashrom to write the firmware to the flash chip on the board. After rebooting the device, we were able to log in using the new password.
Analyzing the crash
With root access on the Wemo, we could gather more information about the crash during the UPnP fuzzing. First, we needed to compile the tools required to perform more in-depth analysis for this specific architecture. Using the GPL, we compiled gdbserver and gdb for the device. The Wemo had a large amount of installed tools, such as “wget,” making it simple to add files. We downloaded and executed the tools from the /tmp directory.
After a large amount of trying, we failed to get gdb running directly or remotely with the device. So we used gdbserver, in conjunction with Interactive Disassembler Pro, for all debugging. With the debugger connected, we sent the packet causing the crash and saw the exact location of the crash. A segmentation fault occurred at address 0x2AC15B98. From the memory layout from the Linux “proc” directory, we determined his memory address resides in library libUPnPHndlr.so.
2abf3000-2ac4d000 r-xp 00000000 1f:02 82 /rom/lib/libUPnPHndlr.so
Because the crash was caused by a UPnP packet, it was logical to find the crash inside this library . With the base address 0x2abf3000, we calculated the offset for static analysis in IDA to be 0x22b98. At this address, we found the following:
LOAD:00022B70 # =============== S U B R O U T I N E =======================================
LOAD:00022B70
LOAD:00022B70
LOAD:00022B70 .globl TokenParser
LOAD:00022B70 TokenParser: # CODE XREF: ProcessEnergyPerunitCostNotify+84↓p
LOAD:00022B70 # DATA XREF: LOAD:00004210↑o …
LOAD:00022B70 beqz $a1, locret_22BC0
LOAD:00022B74 move $a3, $zero
LOAD:00022B78 move $a3, $zero
LOAD:00022B7C b loc_22BB4
LOAD:00022B80 li $t0, 0x7C # ‘|’
LOAD:00022B84 # —————————————————————————
LOAD:00022B84
LOAD:00022B84 loc_22B84: # CODE XREF: TokenParser+28↓j
LOAD:00022B84 addiu $a1, 1
LOAD:00022B88 addiu $v1, 1
LOAD:00022B8C
LOAD:00022B8C loc_22B8C: # CODE XREF: TokenParser+48↓j
LOAD:00022B8C lb $v0, 0($a1)
LOAD:00022B90 beql $v0, $t0, loc_22BA4
LOAD:00022B94 sb $zero, 0($v1)
LOAD:00022B98 bnezl $v0, loc_22B84
LOAD:00022B9C sb $v0, 0($v1)
LOAD:00022BA0 sb $zero, 0($v1)
LOAD:00022BA4
LOAD:00022BA4 loc_22BA4: # CODE XREF: TokenParser+20↑j
LOAD:00022BA4 beqz $a1, locret_22BC0
LOAD:00022BA8 addiu $a0, 4
LOAD:00022BAC addiu $a1, 1
LOAD:00022BB0 addiu $a3, 1
LOAD:00022BB4
LOAD:00022BB4 loc_22BB4: # CODE XREF: TokenParser+C↑j
LOAD:00022BB4 slt $v0, $a3, $a2
LOAD:00022BB8 bnezl $v0, loc_22B8C
LOAD:00022BBC lw $v1, 0($a0)
LOAD:00022BC0
LOAD:00022BC0 locret_22BC0: # CODE XREF: TokenParser↑j
LOAD:00022BC0 # TokenParser:loc_22BA4↑j
LOAD:00022BC0 jr $ra
LOAD:00022BC4 move $v0, $a3
LOAD:00022BC4 # End of function TokenParser
Because the developers left the binary unstripped, we can name this function TokenParser. The segmentation fault occurs at a branch instruction; however, in MIPS the delay instruction is executed before the branch occurs. Thus the instruction at 0x22B9C is causing the crash. Here the application attempts to load what is at the address stored in $v1 and place it in $v0. Taking a look at the registers, we find the data from our packet in XML tags “EnergyPerUnitCostVersion” is in $v1, leading to an “invalid write access” segmentation fault error.
After statically analyzing the function, it appears to copy data from one section to another, looking three times for a 0x7C or “|” character. If it never finds the “|,” it keeps copying into a statically defined buffer. To fully understand why the overwrite occurs, let’s take a look at the stack as we step through the function:
2EF17630 2AC692F0 MEMORY:2AC692F0
2EF17634 00000000 MEMORY:saved_fp
2EF17638 34333231 MEMORY:34333231 ← previously copied data
2EF1763C 00000035 MEMORY:retaddr+31 ← next byte will be written at 0x2EF1763D
2EF17640 00000000 MEMORY:saved_fp ← zeroed out memory prepared for the copy
2EF17644 00000000 MEMORY:saved_fp
2EF17648 00000000 MEMORY:saved_fp
2EF1764C 00000000 MEMORY:saved_fp
2EF17650 2EF17638 MEMORY:2EF17638 ← start writing at this address; can be overwritten
As the function copies data onto the stack, it eventually copies over the address for the original buffer. Once this address is overwritten, the function attempts to write the next byte at the new value, in this case is an invalid address. This overflow gives an attacker two exploitable vectors: a write-what-where condition allows an attacker to write data to an arbitrary location in memory; by continuing to overwrite data on the stack, an attacker can overwrite the $RA register or return address for the calling function, providing the attacker control of the execution flow.
Writing the exploit
Now that that we understand the vulnerability, can we exploit it? Because this is a standard buffer overflow, we need to answer two questions. How much room is available on the stack, and are there any “bad” bytes that cannot make it onto the stack? To determine the available room, we can examine how much of the payload makes it onto the stack if we repair the address overwritten on the stack with a valid address. We learned only 91 bytes can be written onto the stack.
The next step is to determine if there are any “bad” bytes. After running a few tests, we noticed that only ASCII characters can make it onto the stack. Before the vulnerable code is executed, the packet is parsed by the open-source XML parser “mxml.” This library follows the standard of allowing only ASCII and Unicode characters to exist between tags.
This standard is very problematic for both shellcode and return-oriented programming (ROP) techniques because both memory address and shellcode tend to use mostly nonreadable characters. We could use several techniques to combat room on the stack; however, due to the hard limitation on characters that will pass through the XML sanitization process, it would be best to use functions that are already loaded into memory. One method that does not require extensive shellcode is to use a “return to libc” attack to execute the system command. Because the system call typically takes a string as a parameter, this might pass through the filter. Because the Wemo does not use address space layout randomization, if we use ROP it would be theoretically possible to make a call to system without needing to pass additional shellcode through the XML filter.
We still face a major challenge: Only addresses comprising entirely ASCII characters can pass through the XML filter. This drastically limits the potential for finding usable gadgets. We used IDA to see where libc and system are loaded into memory, and found two implementations: in libuClibc-0.9.33.2.so at address 0x2B0C0FD4; and in libpthread-0.9.33.2.so at address 0x2AD104F4. However, neither of these addresses meet the requirements to pass through the XML filter. Thus even if we could create an ROP chain, we would not be able to send just the address for system in the packet.
Addresses with bad characters are not a new problem for exploit development. One of the most common bypasses is to use addition or subtraction ROP gadgets to create the required address in a register and call that register. Again, however, we face the limitation on which operands can be used for this addition or subtraction equation due to the XML filter.
After studying the memory layout, we discovered that libuClibc-0.9.33.2.so sits at a memory location with addresses that can bypass the XML filter. We were fortunate that this is a large library, providing a decent list of addresses, because it is the only library in such a space. With this discovery, the team created a tool to assist with the creation of this exploit. The tool pulls out all possible ROP gadgets with usable memory addresses and determines if an addition or subtraction equation could call one of the two system calls found in memory, using only the values that will bypass the filter. The address for system in libuClibc-0.9.33.2.so, 0x2B0C0FD4, did not have any usable operands. However, 0x2AD104F4 did. We found several “filter-proof” operands that when added together equaled 0x2AD104F4.
We employed our tool’s output for all possible ROP gadgets that bypass the filter to build an ROP chain, which uses an addition instruction to create the final address for system and stores it in $s0. After the addition, another gadget moves the address for system into $t9 and calls system. This final gadget also moves an address that can be controlled from the stack into the register holding the parameter for the system call. The entire ROP chain consists of only three gadgets, which easily fit in the stack space provided by the buffer overflow.
Piecing everything together
Earlier we discovered two attack techniques that can be used with this vulnerability: a write-what-where, and overwriting the return address on the stack. Each packet sent can use each technique once. To get a parameter to the system call, we must use write-what-where to place the parameter in a writable memory address and pass this address to system. Fortunately, this vulnerable application sets aside a large amount of writable memory that is never used, and in a range accessible to our limited set of addresses that bypass the filter. Unfortunately, the ROP chain that calls system requires the use of write-what-where to handle extra instructions in one of the ROP gadgets. This means that two packets are required to execute the exploit: one to write the parameter for system into memory, and a second to make the call to system. Thus it is important that the first packet exits cleanly and does not crash the program.
One way to execute cleanly is to use three well-placed pipes (“|”) inside the payload to stop writing and exit TokenParser at the appropriate time. It is also important to not overwrite the RA pointer so the program can continue normal execution after the packet is received. Then the second packet is sent containing the ROP chain calling system with the address of the parameter written by the previous packet.
Payload
With the discovery of a valid ROP chain that can call system, we must decide what system should call. Because system executes as root, we can gain complete control of the device. Our research has showed that the device has many Linux commands installed. We leveraged this earlier with wget to copy gdbserver to the device. An attacker could also call wget from system to download and execute any script. We explored further for installed applications and found NetCat, which could allow an attacker to write a script to create a reverse shell. An attacker could download a script using wget, and execute the script containing a NetCat command to create a reverse shell. We tested and proved this is one simple, effective method, opening a reverse shell as root. Attackers could choose many other methods to leverage this exploit and execute code. The following video demonstrates this exploit working with a reverse shell.
To illustrate, the team wrote an attack scenario. After the plug is compromised, it could use the built-in UPnP library to poke a hole in the network router. This hole creates a backdoor channel for an attacker to connect remotely, unnoticed on the network. In the following video, we used a remote shell to control a TCL smart TV connected to the network. The Roku API implementation on the TV uses simple unencrypted HTTP GET/POST requests to issue commands and does not authenticate the machine sending these commands, making remote control trivial. Using the Wemo as a middleman, the attacker can power the TV on and off, install or uninstall applications, and access arbitrary online content. Smart TVs are just one example of using the Wemo to attack another device. With the attacker having established a foothold on the network and able to open arbitrary ports, any machine connected to the network is at risk. Because attacks can be conducted through the Wemo and the port mappings generated using this exploit are not visible from the router’s administration page, the attacker’s footprint remains small and hard to detect.
Conclusion
Discoveries such as CVE-2018-6692 underline the importance of secure coding practices on all devices. IoT devices are frequently overlooked from a security perspective; this may be because many are used for seemingly innocuous purposes such as simple home automation. However, these devices run operating systems and require just as much protection as desktop computers. A vulnerability such as we discovered could become the foothold an attacker needs to enter and compromise an entire business network.
One goal of the McAfee Advanced Threat Research team is to identify and illuminate a broad spectrum of threats in today’s complex and constantly evolving landscape. Through analysis and responsible disclosure, we aim to guide product manufacturers toward a more comprehensive security posture.