Reverse-engineering TP-Link KC100

Geistless
6 min readAug 6, 2020

--

This week I got my hands on two pieces of the TP-Link KC100 Spot camera. It is a very nice product from TP-Link with some very cool features:

  • 130° wide-view angle;
  • Night-vision up to 6 meters;
  • Very easy setup through the Kasa Smart App (available for iOS and for Android);
  • Easy wall mount;
  • etc.

But it has a very big drawback — as all other IP cameras — it phones home. Normally I trust TP-Link and its products and I cannot blame them for spying on me. But what I am always scared of is some malicious person breaking into their systems and stealing / eavesdropping on my data.

Therefore I decided to block access to the Internet for both MAC addresses inside my router. The problem this comes with, is when outside of the home network, the cameras are shown as offline in the Kasa Smart App, even when I am logged to my network via VPN.

The way how the Kasa Smart App works is, that upon launch it sends a UDP broadcast packet on port 9999 with an encrypted content.

UDP broadcast packet on port 9999

Every TP-Link device on your network, which receives this packet answers to your smartphone with information about itself. That’s how the app knows which devices are online.

Now, when you’re outside your network and you’re connected over VPN over Layer 3 (e.g. tun, not tap) then the broadcast is not forwarded. So no devices answer back to you, thus the app doesn’t know which devices you have.

What’s inside the encrypted UDP packets? Following the information in this very well written article, I was able to decrypt the data in the UDP packets. Here is what the Kasa Smart App sends:

{"system":{"get_sysinfo":{}}}

And here is what one of the cameras answers:

{"system":{"get_sysinfo":{"err_code":0,"system":{"sw_ver":"2.3.0 Build 20200415 rel.61333","hw_ver":"2.0","model":"KC100(UN)","hwId":"DEADBEAFDEADBEAFDEADBEAFDEADBEAF","oemId":"CAFECAFECAFECAFECAFECAFECAFECAFE","deviceId":"CAFEDEADBEAFCAFEDEADBEAFCAFEDEADBEAFCAFE","dev_name":"Kasa Spot","c_opt":[0,1],"type":"IOT.IPCAMERA","alias":"My Kasa Spot Name","mic_mac":"DEADBEAFCAFE","mac":"00:11:22:33:44:55","longitude":01.234567,"latitude":09.876543,"rssi":-52,"system_time":1596600000,"led_status":"on","updating":false,"status":"configured","resolution":"1080P","camera_switch":"on","bind_status":true,"last_activity_timestamp":1596611111}}}}

Basically speaking the 9999 port interface is a configuration interface, where you can send JSON-structured commands and receive JSON-structured responses. Review the article linked above for more information and commands how this works.

The I fired up my Kali Linux and performed an nmap on the device:

nmap results

The ports 10443, 18443 and 19443 all seem like HTTPS (usually 443) ports. That’s why I fired the browser and opened the first one:

Trying port 10443 in the browser

So they really speak the HTTP protocol, but there is no “default” page. What was more important to notice, was the yellow exclamation mark in the address bar. This meant, the certificate was not CA signed one, but a self-signed. This meant the application did not search for a specific certificate.

Then I decided to try with the infamous dirb command:

# dirb https://192.168.10.40:10443/ /usr/share/wordlists/dirb/big.txt

-----------------
DIRB v2.22
By The Dark Raver
-----------------

START_TIME: Thu Aug 6 13:49:43 2020
URL_BASE: https://192.168.10.40:10443/
WORDLIST_FILES: /usr/share/wordlists/dirb/big.txt

-----------------

GENERATED WORDS: 20458

---- Scanning URL: https://192.168.10.40:10443/ ----

-----------------
END_TIME: Thu Aug 6 14:51:03 2020
DOWNLOADED: 20458 - FOUND: 0

Well, not good.

Enter: disassembly of the Kasa Smart App for Android. I downloaded the app from the Google Play Store and used jadx to disassemble it. After spending another one hour analyzing the code I found the following part:

Part of the Kasa Smart App disassembled code

In the code, the path “/https/stream/mixed” was stated for one of our ports: 19443. So I tried this one, and:

Trying another URL

Voila! Now we only needed to find the username and password.

Since the App was not checking the certificate, we could’ve either started mitmproxy, or (the solution I decided) fire a simple HTTPS server and forward all the requests with it.

Since exactly at this time I started learning rust and my learning app was a simple HTTPS server, I decided to test it live. The handler function for each request will print us all the request’s data, so we can then see what kind of authorization they’re using.

But before getting the request, we first have to trick the Kasa Smart App to send a request to it. For this I used arpspoof. So here are some commands I executed on my Kali machine:

Preparing the HTTPS server

And then started the spoofing process:

Activating arpspoof

What this basically does is to tell my smarphone on IP address 192.168.10.37, that the IP address 192.168.10.40, where the camera should be, is located at the MAC address of my Kali machine. Thus my smartphone will try to search for the camera on the Kali machine.

Not long after starting this, my Kasa Smart App lost connection to the camera stream and I got output from my HTTPS server:

Request from Kasa Smart App

Now we can see that it is using basic authorization and the string is base64 encoded. After encoding it, I found out how it’s built. Here is a simple pseudo-code for the algorithm:

Pseudo-code algorithm for basic authorization

The two first lines have to be replaced with the account data, that you’ve used to login into the Kasa Smart App / setup the camera.

Now it was time to try it out!

First curl try

OK, this is a good result — authentication works, but there is no “Content-length” header, according to curl. Why is this?

If we check the response headers, we see the one saying:

Content-Type: multipart/x-mixed-replace;boundary=data-boundary--

This shows, that the content length is actually infinite and data will be constantly sent to us, delimiting the different parts of the video and audio with the delimiter:

--data-boundary--

Now let’s modify this a bit: just add the option “--ignore-content-length” to curl. Then let it run for a couple of seconds and stop it via Ctrl+C. Open the file called fl.test and look its contents:

Structure of the downloaded data

Great, we have an MJPEG stream of audio and video, which we can easily convert to mp4 with ffmpeg:

# curl -vv -k -u admin:YWRtaW4= --ignore-content-length "https://192.168.10.40:19443/https/stream/mixed?video=h264&audio=g711&resolution=hd&deviceId= CAFEDEADBEAFCAFEDEADBEAFCAFEDEADBEAFCAFE" --output - | ffmpeg -y -i - test.mp4

The newly created file called “test.mp4” can be now played with your favorite multimedia player.

From this point on, you can start an RTSP server, or do h264 m3u streaming, etc.

All the paths are open.

--

--

Geistless

Developing software since I was 7 years old. Privacy and security advocate, in my spare time I reverse-engineer software and hardware, to learn how they work.