Testing the 'Netatmo Welcome' Smart Camera – Hardware Hacking

Netatmo Welcome is a smart camera, which is capable of recognizing faces, streaming recordings into the cloud or alerting the owner in case of a burglary. As part of ongoing research into the Internet of Things security, we continued our analysis of the camera and did some hardware hacking. We were able to get a root shell on the camera and can now deploy our Linux or android images on the camera. We also found out that the IPsec mode means a constant VPN connection, which the manufacturer uses to send commands to the camera but can/could be used to access each camera remotely. With knowledge of how the password protection is working, everyone can get access. All you need is a USB to RS232 converter.

Contents

  1. Dismantling and getting a serial port connection
  2. Exploiting the camera using the uboot shell
  3. Analyzing the console login script
  4. Getting/Flashing firmware updates
  5. Enable local SSH/disable SSH access for Netatmo
  6. Conclusion

1. Dismantling and getting a serial port connection

We found teardowns for the camera on the internet and identified a serial port connection on the mainboard! The camera can be dismantlement easily, you only need to have the necessary Torx screwdriver and some flair. Then we soldered a three pin wire to the ports for a constant connection and connected it to a serial port to USB converter you can get for some bucks at most online hardware stores (USB to RS232 TTL UART PL2303HX adapter). The pin assignment is labeled on the plate and the serial port connection is set to default values (115200 8N1, with hardware flow control set to 1 and software flow control set to 0) so we could easily connect to it using the minicom or screen (screen /dev/ttyUSB0 115200,cs8,ixon) tool.

Cables connected to the serial ports on the mainboard of the camera.
Cables connected to the serial ports on the mainboard of the camera.

2. Exploiting the camera using the uboot shell

What you see while connected to the serial port connection is the detailed boot and event log and an error message on pressing the return key, that the password is wrong following the echoed firmware version.

Incorrect password.
Firmware version: 73
NSC[70:ee:50:2e:33:09] password:

We could terminate the password prompt with CTRL+D to restart the service, revealing the name console_login.sh. While bruteforcing might be possible with firmware version 73 (the version our camera was delivered with), we encountered some protections on this end when updated to version 199. We will go into these changes more detailed in the next chapter.

We first tried to use a technique called pin2pwn but quickly found out that an uboot console can be safely gained by pressing and holding the 's' key while booting the device. Once in the uboot shell we gathered information on which commands are available:

? - alias for 'help'
base - print or set address offset
boot - boot default, i.e., run 'bootcmd'
boota - boota - boot android bootimg from memory

bootd - boot default, i.e., run 'bootcmd'
bootelf - Boot from an ELF image in memory
bootm - boot application image from memory
bootvx - Boot vxWorks from an ELF image
cmp - memory compare
cp - memory copy
crc32 - checksum calculation
efex - run to efex
env - environment handling commands
exit - exit script
false - do nothing, unsuccessfully
fastboot_test- do a sprite test
**fatdown** - download data to a dos filesystem
fatinfo - print information about filesystem
**fatload** - load binary file from a dos filesystem
fatls - list files in a directory (default /)
go - start application at address 'addr'
help - print command description/usage
key_test- Test the key value

logo - show default logo
loop - infinite loop on address range
mass_test- do a usb mass test
md - memory display
memcpy_test- do a memcpy test
mm - memory modify (auto-incrementing address)
**mmc** - MMC sub system
mmcinfo - display MMC info
mtest - simple RAM read/write test
mw - memory write (fill)
nm - memory modify (constant address)
pburn - do a burn test
printenv- print environment variables
recovery- sunxi recovery function
reset - Perform RESET of the CPU
run - run commands in an environment variable
save_userdata- save user data
saveenv - save environment variables to persistent storage
setenv - set environment variables
showvar - print local hushshell variables
shutdown- shutdown the system
sprite_test- do a sprite test
standby - run to boot standby
sunxi_bmp_info - manipulate BMP image data
sunxi_bmp_show - manipulate BMP image data
sunxi_boot_signature- sunxi_boot_signature sub-system
**sunxi_flash** - sunxi_flash sub-system
test - minimal test like /bin/sh
timer_test - do a timer and int test
timer_test1 - do a timer and int test
true - do nothing, successfully
version - print monitor, compiler and linker version

the locations from our bootlog:

--------fastboot partitions--------
-total partitions:10-
-name- -start- -size-
bootloader : 1000000 1000000
env : 2000000 20000
boot : 2020000 2000000
system : 4020000 28000000
data : 2c020000 40000000
misc : 6c020000 1000000
recovery : 6d020000 2000000
cache : 6f020000 1000000
databk : 70020000 38000000
UDISK : a8020000 0
-----------------------------------

and what environment variables are set:

#printenv:

baudrate=115200
boot_fastboot=fastboot
boot_normal=**sunxi_flash read 40007800 boot;boota 40007800**
boot_recovery=sunxi_flash read 40007800 recovery;boota 40007800
bootargs=console=ttyS0,115200 root=/dev/nanddinit=/init loglevel=8 partitions=${partitions}
bootcmd=run setargs_nand boot_normal
bootdelay=3
console=ttyS0,115200
init=/init
loglevel=8
mmc_root=/dev/mmcblk0p7
nand_root=/dev/nandd
setargs_mmc=setenv bootargs console=${console} root=${mmc_root}init=${init} loglevel=${loglevel} partitions=${partitions}
setargs_nand=setenv bootargs console=${console} root=${nand_root}init=${init} loglevel=${loglevel} partitions=${partitions}
stderr=serial
stdin=serial
stdout=serial

Environment size: 699/131068 bytes

The interesting part is that we have access to the SD card in uboot using the fat commands (fatload, fatdown, fatls...) and also know how the boot image is loaded to ram and where it is located. We used this by first downloading the used boot image, then extracting it to disable the login script, compress it again and loading it to RAM to then finally boot it.

sunxi_flash read 40007800 boot fatdown mmc 0:1 40007800 boot.img

The boot image was extracted using a tool named bootimgtool.

bootimgtool -x boot.img

Then the ramdisk was extracted using gunzip and cpio:

gunzip -c ../ramdisk.img | cpio -i

Knowing the name of the password prompt service, we simply searched after console_login.sh in the init.rc file and removed the service definition.

#service console /system/bin/console_login.sh
# class core
# console
# disabled
# user shell
# group log

And finally everything was put together again and copied to the memory card, by executing the following commands in the ramdisk directory:

find . | cpio -o -H newc | gzip > ../newRamdisk.img bootimgtool -r newRamdisk.img -c bootNew.img

In uboot the SD card is recognized/found using the mmc command and fatload is used to put the new image to ram. Finally, we use the boota command, the same way it is used for normal boot, to start the system using our new image.

fatload mmc 0:1 40007800 new3.img boota 40007800

When booting is done, we have direct access to the system without having to add a user password. Privilege escalation is as easy as typing 'su' to get root privileges.

3. Analyzing the console login script

So this was a surprise, the login script checks for the secret we already found in our first blog post about the Netatmo Welcome camera. By knowing this, direct shell access is straightforward now! Particular funny is the comment at the first line of the script 'A simple "login" equivalent to replace the open serial console on production, to slow-down reverse-engineering. If you can read this, it didn't work.'. Well, it seems it did not work for us.

#!/system/bin/sh

# A simple "login" equivalent to replace the open serial console on production,
# to slow-down reverse-engineering.
# **If you can read this, it didn't work.**

if [ -e /system/etc/netatmo-production.txt ]; then
  PRODFLAG=`cat /system/etc/netatmo-production.txt`
  if [ "$PRODFLAG" -eq 0 ]; then
    exec /system/bin/sh
  fi
  # prod=1 or file missing : ask for password
fi

DBLIBTOOL=/system/bin/dblibtool
# md5 of the md5 of rsa
BIGSECRET="4ca9f3a5ce53a81a12af0f6c22a5775d"
NBTRY=0

while true; do
  MAC=`${DBLIBTOOL} -get 1`
  SECRET=`${DBLIBTOOL} -get 3`

  echo -n "Firmware version: "
  cat /system/etc/netatmo-version.txt

  VALIDSECRET=1
  if [ -z "$MAC" -o -z "$SECRET" ]; then
    echo "NSC[] : dblib not configured yet"
    VALIDSECRET=0
  fi

  if [ ${#SECRET} -lt 6 ]; then
    echo "NSC[$MAC] dblib secret invalid"
    VALIDSECRET=0
  fi

  # Only need to type first 6 characters of SECRET
  SHORTSECRET=`echo -n "$SECRET" | /system/bin/busybox cut -c 1-6`

  echo -n "NSC[$MAC] password: "

  # "read -s" is not implemented, do it old-fashion
  busybox stty -echo
  read password
  busybox stty echo

  if [ $VALIDSECRET -eq 1 ]; then
    if [ "**$password" == "$SHORTSECRET" -o "$password" == "$SECRET"** ]; then
      echo "Password accepted."
      exec /system/bin/sh
    fi
  fi

  # In case of a corrupted dblib, be able to login with a master password
  password_hash=`echo -n "$password" | /system/bin/busybox md5sum - | /system/bin/busybox cut -d' ' -f 1`
  if [ **"$password_hash" == "$BIGSECRET"** ]; then
    echo "Master password accepted."
    exec /system/bin/sh
  fi

  echo "Incorrect password."
done

The login script contained new code at the end with 2 second sleep time and was disabled after 3 attempts. While in the first version 6 characters for short secret were enough, later there were 16 characters requested. The script was disabled by forcing an endless loop, that could be simply circumvented by pressing control+c to terminate and restart the login script, which was fixed by a trap '' INT command.

# Only need to type first 16 characters of SECRET
SHORTSECRET=`echo -n "$SECRET" | /system/bin/busybox cut -c 1-16`
sleep 1
echo "Incorrect password."
sleep 1
NBTRY=$((NBTRY+1))
if [ $NBTRY -gt 3 ]; then
  echo "Login disabled."
  while true; do
    sleep 10
    echo "."
  done
fi

We made a simple change to the script for persistence by changing the comparison to unequal and added '-c su' for convenience reasons:

if [ "$password_hash" **!=** "$BIGSECRET" ]; then
  echo "Master password accepted."
  exec /system/bin/sh **-c 'su'**
fi

4. Getting/Flashing firmware updates

We analyzed the firmware update script and found the correct API call for firmware request updates.

curl --silent --data 'mac=70:ee:de:ad:be:ef&secret=$SECRET&fw=73&hw_version=246' https://apicom.netatmo.net/api/getcamerainfo
{
  "body": {
    "timezone": "Europe/Berlin",
    "share_info": false,
    "firmware_info": {
      "fw_url": "https://fw-556112.c.cdn77.org/nsc-v250-hk-fix-faceconvert-prod.zip"
    },
    "home_id": "5bd724ed2d3e046ad38bd32f"
  },
  "status": "ok",
  "time_exec": 0.007094144821167,
  "time_server": 1540826441
}

Giving us a static still valid URL to download the, at the moment of writing, newest firmware image as ZIP-file.

You can download the latest (13.10.2022) firmware image here:
https://n3tfw.blob.core.windows.net/nacamera-clients/nsc-v5012000-41480bb7baf7d3852020a1f23e1ca616.zip

The ZIP files are typical Android update ZIPs/JAR files, with a certificate (CERT.RSA) and a list of SHA1 sums for each file (MANIFEST.MF). The list itself is signed with the certificate. Changing a file inside the ZIP will result in an integrity check failure. The private certificate allowed to sign these zips is not owned by us, so we have to change the recovery checking these certificates! We used the tools/ubiquitous Android debug keys, from this XDA post:
https://forum.xda-developers.com/nook-touch/general/solution-customize-update-factory-image-t3027759

In short: First get a copy of your recovery image, then use scp to get it to your local machine and extract it the same way we did before:
dd of=/def/block/by-name/recovery if=recovery.img b=1024 bootimgtool -x recovery.img gunzip -c ../ramdisk.img | cpio -i

Next you have to replace/add the default key to the /res/keys file in your extracted ramdisk. Then pack, scp and flash your new recovery:

find . | cpio -o -H newc | gzip > ../newRamdisk.img
bootimgtool -r newRamdisk.img -c recoveryNew.img
scp ...
dd of=recoveryNew.img if=/def/block/by-name/recovery b=1024

You can then change the downloaded firmware to your liking, but don't forget to use the signapk tool with the default android key to sign it afterwards!
java -jar signapk.jar -w testkey.x509.pem testkey.pk8 nsc-v250-softscheck2.zip nsc-v250-softscheck2.signed.zip

You can flash the .zip by copying it to the device and using the following commands taken from the Netatmo update script (don't forget to adjust the path to your file):

mkdir -p /cache/recovery
echo "boot-recovery --update_package=$DIR/ota.zip" >; /cache/recovery/command
/system/bin/netatmo_reboot.sh 3 recovery

5. Enable local SSH/disable SSH access for Netatmo

It is much more convenient to access the camera via SSH. To do so, add the line "ListenAddress 0.0.0.0" in /data/ssh/sshd_config. You might want to block Netatmo from accessing your device via SSH, by deleting the line containing "ListenAddress 10.255.145.118".

In newer firmware versions, this alone won't do the trick as traffic to port 22 is still blocked by the firewall. Rename or remove /system/bin/iptables_ssh.sh and delete lines 13 and 19 in /system/bin/check_ssh.sh (shown below) to remove iptables rules, which block the SSH port. After rebooting, you should be able to connect via SSH.

#!/bin/sh

IS_PROD=$(cat "/etc/netatmo-production.txt")
if [ "$IS_PROD" -eq 0 ] ; then
  exit
fi
# default to 0 if file does not exist
SSH_ALLOWED_DELAY=$(cat "/data/netatmo/var/ssh-timeout.txt" || echo 0)
if [ "$SSH_ALLOWED_DELAY" -lt "$(date +%s)" ]; then
  # Check if there is an already set rule to prevent adding more than one rule
  if [ "$(iptables -L INPUT --line-numbers | grep ssh | busybox awk '{print $1}')" -eq 0 ]; then
    # Disable ssh port
    iptables -A INPUT -j REJECT -p tcp --dport 22
  fi

  # Do the same with ipv6
  if [ "$(ip6tables -L INPUT --line-numbers | grep ssh | busybox awk '{print $1}')" -eq 0 ]; then
    # Disable ssh port
    ip6tables -A INPUT -j REJECT -p tcp --dport 22
  fi
fi

For convenience, you can just use the RSA Key that's already on the device (/data/ssh/authorized_keys). Fun fact: In the firmware version 250 this private key seemed to belong to fpotter, which is very likely Fred Potter, the CEO of Netatmo.

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDDj/6pdOoky0qsAiKZxfyMoUwiBpFkrAUBUA2ZaSCZIobARH3ANYYWaJoKN8+mjo6UpShK9GVSgBnRaLV0vwkr+QP1anTBX3s9zRhm2vDA5nfwnogWq/MZ4VaVONl4aLnpw29bBYuomhgU1Oxexzp6dP//XmbqUjLXH9ND1fR6LBkHkhpWNWa27O7UhpEs+fkuZlMuEasUNntgoyU5818950uxrtK3rHz1HN3UdxknREsy5VMPQgchS23kNr+w2lirqDWz0q8YpGqU89YCuQlNKeAPOdlw7APBqi719LxlusCwjnqi/x+B3mpGWLdukNT327EBAP8cwaIrwicRhTcF fpotter@dell-fpotter

6. Conclusion

Once we got into the bootloader shell, things were pretty easy. If you have a serial port connection, setting a password is not enough to secure your device. We recommend to further secure the boot process, so that only images signed by Netatmo can be booted. An attacker that is able to boot into his own image, can completely own the device.

Read about the other vulnerability we found here.

Read about other interesting topics on our blog.