The TP-Link Tapo C200 is a 1080p IP camera that can tilt and pan. The camera supports RTSP out of the box without needing to hack or modify the stock firmware.

Serial console[edit | edit source]

Based on the information from https://drmnsamoliu.github.io/shell.html

Serial console can be accessed by wiring a USB UART to the test pads on the main board. Note that the TX/RX appears to be at 3.3v levels, so take care to use an appropriate UART device or a logic level shifter.

Pin Function
TP9 to UART RX (via 3.3v level shifter)
TP10 to UART TX (via 3.3v level shifter)
TP11 GND

You should see some boot messages when the device is powered on. Enter slp when you see the second Autobooting in 1 seconds message to enter the U-Boot menu.

U-Boot 2014.01-v1.2 (Aug 25 2021 - 12:36:20)

Board: IPCAM RTS3903 CPU: 500M :rx5281 prid=0xdc02
force spi nor mode
DRAM:  64 MiB @ 1066 MHz
Skipping flash_init
Flash: 0 Bytes
SF: Unsupported flash IDs: manuf 20, jedec 4017, ext_jedec 0000
flash status is 0, 2, 0
SF: Detected unknown with page size 256 Bytes, erase size 64 KiB, total 8 MiB
Using default environment

Autobooting in 1 seconds
set watchdog, resetting...

U-Boot 2014.01-v1.2 (Jun 27 2022 - 09:04:38)

Board: IPCAM RTS3903 CPU: 500M :rx5281 prid=0xdc02
force spi nor mode
DRAM:  64 MiB @ 1066 MHz
manuf 20, jedec 4017, ext_jedec 0000
flash status is 0, 2, 0
SF: Detected unknown with page size 256 Bytes, erase size 64 KiB, total 8 MiB
Flash: 0 Bytes
manuf 20, jedec 4017, ext_jedec 8204
flash status is 0, 2, 0
SF: Detected unknown with page size 256 Bytes, erase size 64 KiB, total 8 MiB
Using default environment

In:    serial
Out:   serial
Err:   serial
Net:   Realtek PCIe GBE Family Controller mcfg = 0024
new_ethaddr = 00:00:00:00:00:00
r8168#0
VF: validateLocalFirmware: copying flash to 0x82000000
manuf 20, jedec 4017, ext_jedec 0000
flash status is 0, 2, 0
SF: Detected unknown with page size 256 Bytes, erase size 64 KiB, total 8 MiB
SF: 8388608 bytes @ 0x0 Read: OK
VF: validateLocalFirmware: ret=0(82fb7de0)
VF: validateLocalFirmware: validate local firmware...
TP Header at 82070000
Autobooting in 1 seconds  <-- At this point, type 'slp'
rlxboot#

On the latest firmware as of November 2022, these are the settings that are set on my unit. Note the address to the kernel is at 0x70000 rather than 0x60000 as documented by the Tapo C200 research docs.

rlxboot# printenv
addmisc=setenv bootargs ${bootargs}console=ttyS0,${baudrate}panic=1
baudrate=57600
bootaddr=(0xBC000000 + 0x120000)
bootargs=console=ttyS1,57600 root=/dev/mtdblock6 spdev=/dev/mtdblock7 rts-quadspi.channels=dual
bootcmd=sf probe;sf read 0x82000000 0x70000 0x300000;bootm 0x82000000 
bootdelay=1
bootfile=/vmlinux.img
ethact=r8168#0
ethaddr=00:00:00:00:00:00
load=tftp 80500000 ${u-boot}
loadaddr=0x80100000
stderr=serial
stdin=serial
stdout=serial

Environment size: 477/131068 bytes

If you let the camera boot normally, you will eventually be allowed to log in after hitting the enter key. Unfortunately, the root password is currently unknown (we talk about this in a section below). Instead, we'll have to boot into single user mode to bypass this password.

To boot into single user mode, tack on init=/bin/sh to the bootargs variable and run the bootcmd commands:

# setenv bootargs ${bootargs} init=/bin/sh
# sf probe
# sf read 0x82000000 0x70000 0x300000
# bootm 0x82000000

Continuing the boot process[edit | edit source]

Because we're running the shell as init, nothing is mounted at this stage. After some exploring, it turns out that the system's based on OpenWRT 12. The init scripts that /sbin/init runs to mount our filesystems are actually in /etc/preinit. Calling /etc/preinit is sufficient in getting our system in a more usable state.

# /etc/preinit
## Set the root password since it's set to something unknown
# passwd
## Continue with the boot process
# exec /sbin/init

After letting the system boot, you should now be able to log in as root using the password you assigned.

You can start telnetd by running: telnetd -l /bin/sh -F

Exploring the system[edit | edit source]

The system is running OpenWRT 12.09-rc1 with kernel Linux SLP 3.10.27 #2 PREEMPT Mon Jun 27 09:12:59 CST 2022 rlx GNU/Linux

root@SLP:~# cat /proc/cpuinfo
system type             : RLX Linux for IPCam Platform
machine                 : Unknown
processor               : 0
cpu model               : Taroko V0.2  FPU V0.1
BogoMIPS                : 497.66
tlb_entries             : 64
mips16 implemented      : yes

root@SLP:~# free -m
             total         used         free       shared      buffers
Mem:         40524        37556         2968            0         2236
-/+ buffers:              35320         5204
Swap:            0            0            0

Process list[edit | edit source]

After getting telnet to run, this is what the process list looks like.

root@SLP:~# ps -w
  PID USER       VSZ STAT COMMAND
    1 root      2324 S    init
    2 root         0 SW   [kthreadd]
    3 root         0 SW   [ksoftirqd/0]
    4 root         0 SW   [kworker/0:0]
    5 root         0 SW<  [kworker/0:0H]
    6 root         0 SW   [kworker/u2:0]
    7 root         0 SW   [rcu_preempt]
    8 root         0 SW   [rcu_bh]
    9 root         0 SW   [rcu_sched]
   10 root         0 SW<  [khelper]
   11 root         0 SW<  [writeback]
   12 root         0 SW<  [bioset]
   13 root         0 SW<  [kblockd]
   14 root         0 SW   [khubd]
   15 root         0 SW   [kworker/0:1]
   16 root         0 SW   [kswapd0]
   17 root         0 SW   [fsnotify_mark]
   18 root         0 SW<  [crypto]
   46 root         0 SW   [kworker/u2:2]
   47 root         0 SW<  [deferwq]
   50 root         0 SW<  [kworker/0:1H]
  277 root      2324 S    -ash
  292 root         0 DW   [reset_thread]
  303 root         0 SW<  [cryptodev_queue]
  316 root       864 S    /sbin/hotplug2 --override --persistent --set-rules-file /etc/hotplug2.rules --set-coldplug-cmd /sbin/udev
  332 root       888 S    /sbin/ubusd
  359 root      8036 S    tp_manage
  394 root      3420 S    /usr/bin/ledd
  396 root      3220 S    /usr/sbin/netlinkd
  399 root      5464 S <  /usr/bin/system_state_audio
  473 root      1636 S    /sbin/netifd
  474 root      1516 S    /usr/sbin/connModed
  479 root      1528 S    /usr/sbin/connModed
  485 root     10124 S    /usr/sbin/wlan-manager
  609 root         0 SW   [RTW_CMD_THREAD]
  651 root      1200 S    wpa_supplicant -B -Dwext -iwlan0 -P/tmp/supplicant_pid -C/var/run/wpa_supplicant -bbr-wan
  677 root     13516 S    /usr/bin/dsd
  705 root      4412 S    /bin/cloud-brd -c /var/etc/cloud_brd_conf
  800 root     14972 S    /bin/cloud-client
  804 root     13764 S    /bin/cloud-service
 1072 root      3876 S    /usr/sbin/uhttpd -f -h /www -T 180 -A 0 -n 8 -R -r C200 -C /tmp/uhttpd.crt -K /tmp/uhttpd.key -s 443
 1082 root      5880 S    /usr/bin/rtspd
 1084 root      5988 S    /usr/bin/relayd
 1092 root      7196 S    /usr/bin/p2pd
 1096 root     11144 S    /bin/dn_switch
 1098 root      4200 S    /bin/storage_manager
 1138 root     39736 R    /bin/cet
 1192 root     30360 S    /bin/vda
 1198 root      3804 S    /bin/wtd
 1206 root     11344 S    /bin/nvid
 1258 root      2324 S    udhcpc -p /var/run/static-dhcpc.pid -s /lib/netifd/static-dhcp.script -f -t 0 -i br-wan -r 10.1.3.247 -H
 1344 root      2328 S    /usr/sbin/ntpd -n -p time.nist.gov -p 128.138.140.44 -p 192.36.144.22 -p time-a.nist.gov -p time-b.nist.g
 1359 root      3840 S    /usr/bin/motord
 3887 root      2320 S    telnetd -l /bin/sh -F
 3900 root      2324 S    /bin/sh
 3918 root      2320 R    ps -w

Network daemons[edit | edit source]

root@SLP:~# netstat -lnp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 0.0.0.0:8800            0.0.0.0:*               LISTEN      1138/cet
tcp        0      0 127.0.0.1:929           0.0.0.0:*               LISTEN      1092/p2pd
tcp        0      0 0.0.0.0:20002           0.0.0.0:*               LISTEN      359/tp_manage
tcp        0      0 0.0.0.0:2020            0.0.0.0:*               LISTEN      1206/nvid
tcp        0      0 0.0.0.0:554             0.0.0.0:*               LISTEN      1138/cet
tcp        0      0 0.0.0.0:23              0.0.0.0:*               LISTEN      3887/telnetd
tcp        0      0 127.0.0.1:921           0.0.0.0:*               LISTEN      1084/relayd
tcp        0      0 127.0.0.1:922           0.0.0.0:*               LISTEN      1082/rtspd
tcp        0      0 0.0.0.0:443             0.0.0.0:*               LISTEN      1072/uhttpd
udp        0      0 0.0.0.0:20002           0.0.0.0:*                           359/tp_manage
udp        0      0 0.0.0.0:3702            0.0.0.0:*                           1206/nvid

Filesystems[edit | edit source]

root@SLP:~# df -h
Filesystem                Size      Used Available Use% Mounted on
/dev/root                 2.5M      2.5M         0 100% /rom
tmpfs                    19.8M   1012.0K     18.8M   5% /tmp
tmpfs                   512.0K         0    512.0K   0% /dev
rwroot                   19.8M     24.0K     19.8M   0% /overlay
/dev/mtdblock7            3.5M      3.5M         0 100% /rom/sp_rom
overlayfs:/overlay        3.5M      3.5M         0 100% /rom/mnt
overlayfs:/overlay       19.8M     24.0K     19.8M   0% /

Root password[edit | edit source]

The root password on this firmware version has changed and is no longer 'slprealtek' as discovered by kubik369 and documented on the Tapo C200 research site. The root hash is now set to:

root:$1$IjsWXCH9$z2Woc0uNFN4a7bZM7SGCF.:0:0:root:/root:/bin/ash

RTSP[edit | edit source]

RTSP works out of the box after you enable it with the Tapo app. You must set a username and password.

There are two streams that are enabled with this option.

  • Full resolution - rtsp://username:password@$IP/stream1
  • 360p resolution - rtsp://username:password@$IP/stream2

Hack[edit | edit source]

Tapo C200 with an Arduino attached
Tapo C200 with an Arduino attached

There are a few approaches that could be taken to hack the device so that there is persistent telnet access across reboots. The one that DrmnSamoliu used was an exploit in the art partition (I think he went with this approach because modifying the system partitions would cause UBoot to stop booting as it does checks at startup).

Getting persistent telnet access[edit | edit source]

C200 with Arduino Installed
C200 with Arduino Installed

The approach I went with to avoid needing to flash anything was to tack on a Arduino to the serial port which takes over the startup sequence from UBoot. The Arduino will make the camera enter single user mode, reset the root password, enable telnet from any IP address, and also attempt to run a script from the SD card on startup. This should hopefully also survive firmware upgrades unless they decide to change how UBoot behaves as the changes are applied each time the camera boots.

Here's the Arduino sketch I eventually came up with.

int boot_stage = 0;
bool boot_done = false;
unsigned long last_act = 0;

void setup() {
  // initialize serial:
  Serial.begin(57600);
  Serial.setTimeout(250);

  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, LOW);
}

void loop() {
   if (Serial.available() <= 0) {
     // no serial activity before boot is complete? Hmmm..
     if ( ! boot_done && millis() - last_act > 10000ul) {
       Serial.println("");
       last_act = millis();
     }
   
     return;
   }

  // work on the current line
	String input = Serial.readString();
  input.trim();

  // empty? Do nothing
  if (input.length() == 0) {
    return;
  }

  // got something? Try to work with it.
  digitalWrite(LED_BUILTIN, HIGH);
  last_act = millis();

  if (input.indexOf("Autobooting in 1 seconds") >= 0) {
    boot_stage++;

    //if (boot_stage >= 2) {
      delay(100);
      Serial.println("slp");
      delay(200);
    //}
  }
  
  else if (input.indexOf("rlxboot#") >= 0) {
    delay(250);
    Serial.println("setenv bootargs ${bootargs} init=/bin/sh");
    delay(250);
    // Clear buffer and get prompt
    while (Serial.available() > 0) Serial.read();
    Serial.println("printenv bootcmd");
    
    while (true) {
	    input = Serial.readString();
      if (input.indexOf("bootcmd=") >= 0) {
        delay(100);
        serial_write_string(input.substring(input.indexOf("=") + 1));
        break;
      }
    }

    delay(1000);
    // Clear buffer
    while (Serial.available() > 0) Serial.read();
  }

  else if (input.indexOf("/ #") >= 0) {
    delay(250);
    Serial.println("/etc/preinit");
    delay(2500);
    Serial.println("echo -en password\\\\npassword | passwd");
    delay(250);
    Serial.println("sed -i 's/-b 127.0.0.1//g' /etc/init.d/telnet");
    delay(250);
    Serial.println(F("echo IyEvYmluL3NoIC9ldGMvcmMuY29tbW9uCgpTVEFSVD00MQoKc3RhcnQoKSB7CnsKCXdoaWxlIHRydWUgOyBkbwoJCXNkX3Jlc3VsdD0kKG1vdW50IHwgZ3JlcCAibW1jYmxrMCIpCgkJaWYgW1sgIiRzZF9yZXN1bHQiWCA9PSAiIlggXV07IHRoZW4KCQkJIyBubyBTRCBjYXJkIHlldD8KCQkJZWNobyAiV2FpdGluZyBmb3IgU0QgY2FyZC4iCgkJCXNsZWVwIDUKCQllbHNlCgkJCWJyZWFrCgkJZmkKCWRvbmUKCglbIC1mIC90bXAvbW50L2hhcmRkaXNrXzEvc3RhcnQuc2ggXSAmJiAuIC90bXAvbW50L2hhcmRkaXNrXzEvc3RhcnQuc2gKfSYKfQoK | base64 -d > /etc/init.d/hack"));
    delay(250);
    Serial.println("chmod 755 /etc/init.d/hack");
    delay(250);
    Serial.println("ln -s /etc/init.d/hack /etc/rc.d/S41hack");
    delay(250);
    Serial.println("exec /sbin/init");

    boot_done = true;
    boot_stage = 1;
  }

  digitalWrite(LED_BUILTIN, LOW);
}

void serial_write_string(String data) {
  for (int i = 0; i < data.length(); i++) {
    Serial.print(data[i]);
  }
  Serial.println("");
}

I've created a startup.sh script on the SD card which will also stop the Cloud services as I plan to run this without internet access. That script is given below.

#!/bin/bash
# Reset the root password, in case you don't want 'password' as the default.
echo -e "changeme\nchangeme" | passwd root

# Kill cloud services
killall wtd cloud-client cloud-brd cloud-service

Interesting info[edit | edit source]

GPIO LEDs[edit | edit source]

The red and green LEDs are controllable via the GPIO devices at:

  • /sys/devices/platform/leds-gpio/leds/led-red/brightness
  • /sys/devices/platform/leds-gpio/leds/led-green/brightness

Third account[edit | edit source]

The account to access the RTSP stream is controlled by the third account that's defined in uci.

user_management.third_account=third_account
user_management.third_account.username=camadmin
user_management.third_account.passwd=5F4DCC3B5AA765D61D8327DEB882CF99

Here, we have a 'camadmin' account with a password 'password' in MD5.

Settings[edit | edit source]

Description Key
Enable SD Card recording record_plan.chn1_channel.enabled=on / off
Video recording resolution video.main.resolution=1920*1080

video.main.resolution=1280*720

video.main.resolution=640*360

Motion detection / alert motion_detection.motion_det.enabled=on
Camera alarm msg_alarm.chn1_msg_alarm_info.enabled=on / off

msg_alarm.chn1_msg_alarm_info.light_type=0 / 1 (enable light)

msg_alarm.chn1_msg_alarm_info.alarm_type=0 / 1 (0 siren, 1 tone)

msg_alarm.chn1_msg_alarm_info.alarm_mode=sound / light / light sound

Flip (invert) image image.switch.flip_type=off / center
Loop recording harddisk_manage.harddisk.loop=on / off (on will result in SD card being filled with empty files)
Privacy mode lens_mask.lens_mask_info.enabled=off / on
Distortion correction image.switch.ldc=off / on
OSD text OSD.label_info_1.enabled=off / on

OSD.label_info_1.text=custom text

OSD date OSD.date.enabled=on / off

OSD.week.x_coor=6000 (?)

OSD.week.y_coor=500 (?)

Applying changes could possibly be made by restarting cet.

# /etc/init.d/cet terminate
# /etc/init.d/cet resume

Tapo API[edit | edit source]

There has been some work at reverse engineering the Tapo app API. Here are two main projects I've found:

The API itself is quite simple and involves POSTing JSON payloads. Most options you see with uci can be set with it.

Get a token[edit | edit source]

You need to obtain a stok (security token?) before being able to issue commands.

POST to the camera with the following json payload containing the MD5 hash of your password.

{"method":"login","params":{"hashed":true,"username":"camadmin","password":"5F4DCC3B5AA765D61D8327DEB882CF99"}}

This should return your token if successful.

{"error_code": 0, "result" : { "stok": "85c34addb51fb200f9a9108417b9678c", "user_group": "third_account"}}

Sending commands[edit | edit source]

Commands are POSTed to the camera with the token as part of the URI: https://camera-ip/stok=$TOKEN$/ds

Things you can do are listed below.

What Payload
Camera movement. Set your X/Y coord values as positive or negative steps. {"method":"do","motor":{"move":{"y_coord":"0","x_coord":"-1"}}}
Set the OSD data {"method":"set","OSD":{"date":{"enabled":"on","x_coor":6800,"y_coor":9400},"week":{"enabled":"off"},"font":{"color":"white","color_type":"auto","display":"ntnb","size":"auto"},"label_info_1":{"enabled":"off","text":"Howdy","x_coor":0,"y_coor":450}}}
Infrared / Night mode {"method":"set","image":{"common":{"inf_type":"on"}}}

Using CURL[edit | edit source]

Perhaps something like this

#!/bin/bash

function stok() {
        curl -s -k -X POST https://10.1.3.247 --data '{"method":"login","params":{"hashed":true,"username":"camadmin","password":"5F4DCC3B5AA765D61D8327DEB882CF99"}}' | grep -ohE '[0-9a-z]{32}'
}

Stok=$(stok)

curl -k -X POST https://10.1.3.247/stok=$Stok/ds --data '{"method":"do","motor":{"move":{"y_coord":"0","x_coord":"-1"}}}'
curl -k -X POST https://10.1.3.247/stok=$Stok/ds --data '{"method":"set","OSD":{"date":{"enabled":"on","x_coor":6800,"y_coor":9400},"week":{"enabled":"off"},"font":{"color":"white","color_type":"auto","display":"ntnb","size":"auto"},"label_info_1":{"enabled":"off","text":"Howdy","x_coor":0,"y_coor":450}}}'

See also[edit | edit source]