Tapo C310 is a full-featured weatherproof security camera that you can access from anywhere. Receive instant notifications and check feeds when the motion is detected. Moreover, the automatic siren system will trigger light and sound to frighten away unwanted visitors after the camera detects motion. Day or night, rain or shine, the Tapo camera protects what you love most.source

My “investigation” begins with a TP-Link Tapo C310 IP camera, which is a type of smart camera connected to the Internet and reachable through any web browser and an IP address. From the documents provided by the manufacturer, no additional information was given about the type of hardware architecture (CPU and RAM) or the security of the device. Only the software can reveal clues about what is actually inside the hardware, so all that remains is to analyse the camera’s firmware.

Music provided by COMA - A-Train, press Play and let’s proceed.

Smartphone app

TP-Link provides the Tapo smartphone app (iOSAndroid) but the application itself is not functional without a TP-Link Cloud account. After creating the acount you will be able to set up your camera inside the app and you can access it from anywhere provided you have an Internet connection.

Setting up the camera is a matter of connecting your smartphone to the camera’s WiFi access point (in the form of Tapo_Cam_XXXX, where XXXX are the last four digits of the device’s MAC address) and following the instructions in the Tapo application. The communication between the smartphone and the camera is encrypted, however, the access point is open, meaning there is a timeframe in which the device could be compromised during setup.

The Tapo application requires Internet acccess and perfoms the following requests each time it’s open:

  • n-wap-gw.tplinkcloud.com on TCP port 443 (regional)
  • iac.tplinknbu.com on TCP port 443
  • n-oauth.tplinkcloud.com on TCP port 443
  • n-aps1-wap-gw.tplinkcloud.com on TCP port 443 (regional)
  • api-in-app-marketing-auth.i.tplinknbu.com on TCP port 443
  • aps1-app-server.iot.i.tplinknbu.com on TCP port 443 (regional)
  • download.tplinkcloud.com on TCP port 80
  • aps1-app-tapo-care.i.tplinknbu.com on TCP port 443 (regional)
  • sg-stun.tplinkcloud.com on UDP port 3478 (regional)
  • aps1-security.iot.i.tplinknbu.com on TCP port 443 (regional)
  • aps1-cipc-api.i.tplinknbu.com on TCP port 443 (regional)
  • analytics.tplinkcloud.com on TCP port 443

As you can see, the update checks are done over HTTP and not HTTPS but the firmware is signed anyway so I don’t see a problem here. The regional requests are done to specific servers depending on your current location (aps1 will become euw1 if you’re in Europe).


Video stream

Tip

In firmware version 1.3.0 and below, if you did not set-up a Device Account the default password was TPL075526460603 (as seen from the /bin/cet binary) and anyone on your local network could get unauthorized access to the camera’s RTSP video stream, so it’s not as if TP-Link’s security is stellar.

If you want to view the video stream inside a media player like VLC, you will need to enable Device Account inside the Tapo smartphone app and choose a username and password. After doing that you can open the stream URL inside VLC.

$ vlc rtsp://USERNAME:PASSWORD@CAMERA_IP/stream1

For example, if your Device Account username is bill, password is clinton and camera IP is moni… I mean 192.168.1.69, you can open this stream:

$ vlc rtsp://bill:[email protected]/stream1

You can replace stream1 with stream2 for a low quality video stream.

$ vlc rtsp://bill:[email protected]/stream2

If you’re using the Debian-packaged vlc, be aware of the fact that rtsp support is not enabled. So, use mpv.

$ mpv rtsp://bill:[email protected]/stream1

Warning

Do not mistake the TP-Link Cloud username and password with the Device Account username and password. The Cloud username and password are used for logging into the Tapo smartphone app and the same password can be used to perform API requests to your camera. The Device Account username and password are needed ONLY for viewing the video stream(s) in a third-party app.


Hardware

Manufacturer Code Description Datasheet
Realtek RTL8192EU Wireless LAN 802.11n USB 2.0 Network Adapter link
SigmaStar SSC335 High-Integrated IP Camera SoC Processor, ARM Cortex-A7 800MHz Single Core with 512MB DDR2 link
XMC XM25QH64C 3V 64Mb serial flash memory with dual/quad SPI & QPI link
JWD S16037G 10/100Base SMD Transformer module link

API

To perform API requests to your Tapo camera you will need to use the Tapo Cloud password to generate a security token.

If you want to continue I’ll asume that:

  • You have a TP-Link Tapo C310 camera and you connected it to the mains power source.
  • You opened the Tapo application on your smartphone, created a Tapo Cloud account and you remember the password to it.
  • You added the camera to the Tapo application and you can view video stream from it, meaning all is set-up well.

We’ll start by hashing the password, remember to replace CAMERA_PASSWORD in the line below with your Tapo Cloud password. The username (email address actually) is not required, we’ll be using the generic admin username.

$ python3 -c 'import hashlib; password="CAMERA_PASSWORD"; hashedPassword=hashlib.md5(password.encode("utf8")).hexdigest().upper(); print(hashedPassword)'
BE9A9CBA5264F3A6DB62906AEA447CF5

Next step is to use the hashed password and the admin username to get the stok token that will allow us to do authenticated requests to the camera API. Obviously, replace HASHED_PASSWORD with the string from the previous command (not the actual password, the hashed one) and the IP of your camera (mine is 192.168.1.69).

$ wget -O- --no-check-certificate --quiet --method POST --timeout=0\
	--header 'Content-Type: text/plain'\
	--body-data '{ "method" : "login", "params": { "hashed": True, "password": "HASHED_PASSWORD", "username": "admin"}}'\
	'https://192.168.1.69'

Beautified JSON response:

{
	"error_code": 0,
	"result" : {
		"stok": "21a58ea29ffa45a60b76485e12e10053",
		"user_group": "root"
	}
}

Now that we have the stok token, it’s time to get some basic info about the camera. Replace 192.168.1.69 with your camera IP and use your security token from the step above, in the https://192.168.1.69/stok=21a58ea29ffa45a60b76485e12e10053/ds line:

$ wget -O- --no-check-certificate --quiet --method POST --timeout=0\
	--header 'Content-Type: application/json; charset=UTF-8'\
	--body-data '{'\''method'\'': '\''multipleRequest'\'', '\''params'\'': {'\''requests'\'': [{'\''method'\'': '\''getDeviceInfo'\'', '\''params'\'': {'\''device_info'\'': {'\''name'\'': ['\''basic_info'\'']}}}]}}'\
	'https://192.168.1.69/stok=21a58ea29ffa45a60b76485e12e10053/ds'

Beautified JSON response:

{
	"result": {
		"responses": [{
			"method": "getDeviceInfo",
			"result": {
				"device_info": {
					"basic_info": {
						"ffs": false,
						"device_type": "SMART.IPCAMERA",
						"device_model": "C310",
						"device_name": "C310 1.0",
						"device_info": "C310 1.0 IPC",
						"hw_version": "1.0",
						"sw_version": "1.3.2 Build 220901 Rel.74359n(4555)",
						"device_alias": "camera1",
						"avatar": "Garage",
						"longitude": 0,
						"latitude": 0,
						"has_set_location_info": 0,
						"features": "3",
						"barcode": "",
						"mac": "REDACTED",
						"dev_id": "REDACTED",
						"oem_id": "15483C2A4CF035EA06109A2782F10C2C",
						"hw_desc": "00000000000000000000000000000000",
						"is_cal": true
					}
				}
			},
			"error_code": 0
		}]
	},
	"error_code": 0
}

To check the firmware update status, perform another request (don’t forget to replace the security token and the camera IP):

$ wget -O- --no-check-certificate --quiet --method POST --timeout=0\
	--header 'Content-Type: application/json; charset=UTF-8'\
	--body-data '{'\''method'\'': '\''multipleRequest'\'', '\''params'\'': {'\''requests'\'': [{'\''method'\'': '\''getFirmwareUpdateStatus'\'', '\''params'\'': {'\''cloud_config'\'': {'\''name'\'': '\''upgrade_status'\''}}}]}}'\
	'https://192.168.1.69/stok=21a58ea29ffa45a60b76485e12e10053/ds'

Beautified JSON response:

{
	"result": {
		"responses": [{
			"method": "getFirmwareUpdateStatus",
			"result": {
				"cloud_config": {
					"upgrade_status": {
						"state": "normal",
						"lastUpgradingSuccess": true
					}
				}
			},
			"error_code": 0
		}]
	},
	"error_code": 0
}

To get a list of functions supported by your camera (module_spec function), replace camera IP and security token:

$ wget -O- --no-check-certificate --quiet --method POST --timeout=0\
	--header 'Content-Type: application/json; charset=UTF-8'\
	--body-data '{'\''method'\'': '\''get'\'', '\''function'\'': {'\''name'\'': ['\''module_spec'\'']}}'\
	'https://192.168.1.69/stok=21a58ea29ffa45a60b76485e12e10053/ds'

Beautified JSON response:

{
    "function": {
        "module_spec": {
            ".name": "module_spec",
            ".type": "module-spec",
            "app_version": "1.0.0",
            "led": "1",
            "change_password": "1",
            "local_storage": "1",
            "timing_reboot": "1",
            "ota_upgrade": "1",
            "msg_push": "1",
            "msg_alarm": "1",
            "smart_detection": "1",
            "smart_msg_push_capability": "1",
            "custom_area_compensation": "1",
            "ae_weighting_table_resolution": "5*5",
            "network": ["wifi"],
            "events": ["motion", "tamper"],
            "playback": ["local", "p2p", "relay"],
            "preview": ["local", "p2p", "relay"],
            "video_codec": ["h264"],
            "audio": ["speaker", "microphone"],
            "download": ["video"],
            "record_type": ["timing", "motion"],
            "record_max_slot_cnt": "10",
            "stream_max_sessions": "10",
            "streaming_support_versions": ["1.0"],
            "relay_support_versions": ["1.3"],
            "p2p_support_versions": ["1.1"],
            "device_share": ["preview", "playback", "voice", "cloud_storage"],
            "msg_alarm_list": ["sound", "light"],
            "storage_api_version": "2.2",
            "playback_scale": "1",
            "lens_mask": "1",
            "target_track": "0",
            "wireless_hotspot": "1",
            "wifi_connection_info": "1",
            "greeter": "1.0",
            "linecrossing_detection": "1",
            "intrusion_detection": "1",
            "audioexception_detection": "0",
            "video_detection_digital_sensitivity": "1",
            "client_info": "1",
            "multicast": "0",
            "privacy_mask_api_version": "1.0",
            "ssl_cer_version": "1.0",
            "backlight_coexistence": "1",
            "media_encrypt": "1",
            "multi_user": "0",
            "auth_encrypt": "1",
            "cloud_storage_version": "1.0",
            "ai_enhance_capability": "1",
            "wifi_cascade_connection": "1",
            "custom_auto_mode_exposure_level": "0",
            "verification_change_password": "1",
            "linkage_capability": "1",
            "msg_alarm_separate_list": ["light", "sound"],
            "http_system_state_audio_support": "1"
        }
    },
    "error_code": 0
}

To figure out all the methods, functions and parameters you will need access to the firmware. So, let’s inspect that.

Tip

The default credentials can be found out inside the /usr/bin/logrecordd binary, user is root and the password is slprealtek. This will be useful if you attach a serial console to the board. Looking at online photos I was unable to spot serial pads like in older TP-Link cameras.


Firmware

The firmware itself is the type of software that communicates with and controls the hardware components of the camera; it is the first piece of code that runs on the device, it loads the operating system and provides specific services to run programs, interacting with various hardware components.

Reversing the firmware involves disassembling and understanding the inner workings of the device: this can range from simple analysis (looking at various aspects of the device, such as its file system and various interfaces) to an in-depth look at the firmware itself and uncovering the internal details and algorithms of the firmware.

Versions

It’s hard to find download links for the older firmware versions because the UNIX timestamp is added to the end of the filename, so unless you want to bruteforce it, this is all I have.

Prerequisites

If you’re on macOS:

$ brew install binwalk dtc squashfs

If on a Debian-flavoured Linux:

$ sudo apt install binwalk squashfs-tools device-tree-compiler

Analysis

Start by downloading the latest firmware for the Tapo C310 camera from the manufacturer’s website.

$ wget http://download.tplinkcloud.com/firmware/Tapo_C310v1_en_1.3.2_Build_220901_Rel.74359n_u_1668586937997.bin

A quick file on the firmware did not highlight the type of file and returned a simple data, therefore the file utility was unable to identify the precise type of file. Fortunately, another utility called Binwalk makes much of this work easier by analyzing and extracting the firmware.

$ binwalk Tapo_C310v1_en_1.3.2_Build_220901_Rel.74359n_u_1668586937997.bin

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
2048          0x800           uImage header, header size: 64 bytes, header CRC: 0xC7B58C50, created: 2022-09-01 12:28:57, image size: 60120 bytes, Data Address: 0x0, Entry Point: 0x0, data CRC: 0xBA49C792, OS: Firmware, CPU: ARM, image type: OS Kernel Image, compression type: lzma, image name: "MVX1##I6gd1dfee1CM_UBT1501###XVM"
2112          0x840           xz compressed data
133632        0x20A00         uImage header, header size: 64 bytes, header CRC: 0x84F0E212, created: 2022-09-01 12:39:16, image size: 1585816 bytes, Data Address: 0x20008000, Entry Point: 0x20008000, data CRC: 0xB38164A0, OS: Linux, CPU: ARM, image type: OS Kernel Image, compression type: none, image name: "Linux-4.9.84"
148579        0x24463         xz compressed data
148913        0x245B1         xz compressed data
1722368       0x1A4800        Squashfs filesystem, little endian, version 4.0, compression:xz, size: 5258362 bytes, 863 inodes, blocksize: 65536 bytes, created: 2022-09-01 12:39:23
7015680       0x6B0D00        gzip compressed data, from Unix, last modified: 2022-09-01 12:39:24
7038980       0x6B6804        gzip compressed data, from Unix, last modified: 2022-09-01 12:39:24

And you can recursively extract the firmware file using binwalk:

$ binwalk -Me Tapo_C310v1_en_1.3.2_Build_220901_Rel.74359n_u_1668586937997.bin

Scan Time:     2023-07-21 13:28:48
Target File:   Tapo_C310v1_en_1.3.2_Build_220901_Rel.74359n_u_1668586937997.bin
MD5 Checksum:  eca38a77393bef3e522dab418a3df7b2
Signatures:    411
...

If you want to extract the flattened device tree, first you will need to carve the secondary Linux image (at offset 133632) using dd, use binwalk to extract the contents of the image file and then we can use dd again to carve the FDT from a file inside the Linux image.

$ dd if=Tapo_C310v1_en_1.3.2_Build_220901_Rel.74359n_u_1668586937997.bin of=linux_image bs=1 skip=133632 count=1585816
1585816+0 records in
1585816+0 records out
1585816 bytes transferred in 3.048178 secs (520250 bytes/sec)
$ file linux_image
linux_image: u-boot legacy uImage, Linux-4.9.84, Linux/ARM, OS Kernel Image (Not compressed), 1585816 bytes, Thu Sep  1 12:39:16 2022, Load Address: 0X20008000, Entry Point: 0X20008000, Header CRC: 0X84F0E212, Data CRC: 0XB38164A0
$ binwalk -Me linux_image
Scan Time:     2023-07-21 19:28:03
Target File:   linux_image
MD5 Checksum:  6fdf778179dc62f4f4adcf75ce5677f6
Signatures:    411

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             uImage header, header size: 64 bytes, header CRC: 0x84F0E212, created: 2022-09-01 12:39:16, image size: 1585816 bytes, Data Address: 0x20008000, Entry Point: 0x20008000, data CRC: 0xB38164A0, OS: Linux, CPU: ARM, image type: OS Kernel Image, compression type: none, image name: "Linux-4.9.84"
14947         0x3A63          xz compressed data
15281         0x3BB1          xz compressed data

Scan Time:     2023-07-21 19:28:03
Target File:   _linux_image.extracted/3BB1
MD5 Checksum:  890e9497415f63dec9219d0deff5aa43
Signatures:    411

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
932152        0xE3938         SHA256 hash constants, little endian
2211176       0x21BD68        DES SP2, little endian
2211944       0x21C068        DES SP1, little endian

...

2929435       0x2CB31B        LZMA compressed data, properties: 0xC0, dictionary size: 0 bytes, uncompressed size: 64 bytes
2970448       0x2D5350        Flattened device tree, size: 40469 bytes, version: 17

The FDT is at offset 2970448 in the _linux_image.extracted/3BB1 file and has a size of 40469 bytes, so let’s extract it.

$ dd if=_linux_image.extracted/3BB1 of=device_tree bs=1 skip=2970448 count=40469
40469+0 records in
40469+0 records out
40469 bytes transferred in 0.109888 secs (368275 bytes/sec)
$ file device_tree
device_tree: Device Tree Blob version 17, size=40469, boot CPU=0, string block size=1269, DT structure block size=39144

And now we can analyze the flattened device tree using a tool called device-tree-compiler, a tool that will convert the Device Tree Blob (DTB) to a Device Tree Source (DTS):

$ dtc -I dtb -O dts -o - flattened-device-tree.dts

You can view/download the extracted .dts file here (it’s a text source file). Inside there is a summary of all hardware devices on the board but the most important part is this:

	model = "INFINITY6 SSC009A-S01A QFN88";
	compatible = "sstar,infinity6";

	chosen {
		bootargs = "console=ttyS0,115200n8r androidboot.console=ttyS0 root=/dev/mtdblock2 init=/linuxrc cma=16m";
	};

If you want to browse the whole DTS file be aware of the fact that the information inside is fairly technical and you will need to be familiar with CPUs, clocks, UART devices, NAND flash memory, MMC controllers, etc.

Downgrading

Warning

Do not downgrade the firmware unless you know what you are doing. Never expose a vulnerable device (especially a security camera) to the Internet.

If you want to downgrade the firmware, you can do it in an undocumented way: there is a check_upgrade init service that checks upon the device start-up whether the SD card partition is available and if so, checks if there is a specific file on it (factory_up_boot.bin). If that file exists, the init service starts the firmware upgrade with this file. The check_upgrade file can be found inside the /etc/init.d directory and it’s shown below:

#!/bin/sh /etc/rc.common

START=13

start() {
	if [ ! -d "/tmp/sdcard" ]; then
		mkdir /tmp/sdcard/
	fi

	if [ -b /dev/mmcblk0p1 ]; then
		mount -t vfat /dev/mmcblk0p1 /tmp/sdcard/
	else
		exit 0
	fi

	echo "check firmware upgrade"

	if [ -e /tmp/sdcard/factory_up_boot.bin ]
	then
		echo "start firmware upgrade ..."
		slpupgrade -n "/tmp/sdcard/factory_up_boot.bin"

		while true
		do
			sleep 10
		done
	else
		umount /tmp/sdcard
	fi
}

slpupgrade doesn’t compare version numbers, it just checks the file signature, checksum, camera type and revision, etc.


Others

Linux kernel

Based on OpenWrt Attitude Adjustment 12.09-rc1 running Linux 4.9.84.

root@SLP:~# cat /etc/openwrt_version
12.09-rc1

Open ports

$ nmap -sV -p- 192.168.1.69

Nmap scan report for 192.168.1.69
Host is up (0.0055s latency).
Not shown: 65531 closed tcp ports (conn-refused)
PORT     STATE SERVICE         VERSION
443/tcp  open  ssl/nagios-nsca Nagios NSCA
554/tcp  open  rtsp
2020/tcp open  soap            gSOAP 2.8
8800/tcp open  sunwebadmin?

Telemetry

Telemetry data is POSTed to the https://analytics.tplinkcloud.com API endpoint via the /bin/telemetry and /bin/cloud-service binaries. Telemetry collection is done from within the /bin/create_telemetry_files and /usr/sbin/collect_wifi_info binaries.

Tip

The sane thing to do would be to block all connections to analytics.tplinkcloud.com from a router.


Photos

Click on one of the images for a higher resolution photo.

| | | |

| | | | |