Een verhaal over wifi-geheimen, UART-lockdowns en een beschadigde flashgeheugenchip

Disclaimer

We hebben ons analyse gericht op de Eufy HomeBase 2 in plaats van de HomeBase 3 omdat dit de versie is die ik in mijn stad veelvuldig zag en die ook het gemakkelijkst aan te schaffen was. De firmwareversie die we hier analyseren is 3.4.2.2h (2026-06-05 18:45:48).

Dit is de eerste keer dat ik met hardware heb gewerkt. Voorheen deed ik vulnerability hunting op firmware die ik van internet kon downloaden, dus vergeef me a.u.b. de fouten die ik heb gemaakt.

BELANGRIJK!!! Lees eerst het vorige artikel voordat u verder gaat met dit bericht.

Inhoudsopgave

  • 0x00 Firmware-extractie: Beschrijving van hoe de firmware is geëxtraheerd (en hoe we een deel van het flashgeheugen hebben beschadigd).
  • 0x01 Identificatie van het wifi-credential-generatie-algoritme: Reverse engineering van het algoritme dat wordt gebruikt om de wifi-gegevens voor de OCEAN_XXXXXX beheer-wifi te genereren.
  • 0x02 De UART-lockdown doorbreken: Het flashen van een aangepast U-Boot-image om via UART een shell te verkrijgen.
  • 0x03 Filesystem gardening voor debugdoeleinden: Het recovery-image is beperkt, dus er zijn enkele aanpassingen aan het bestandssysteem nodig om het hoofdprogramma te kunnen draaien.
  • 0x04 Slotwoord: Mijn gebrek aan enthousiasme voor het schrijven van artikelen in het tijdperk van AI.

---

0x00 Firmware-extractie

In het vorige hoofdstuk van deze saga noemde ik dat we alleen de firmware van de deuurbel konden extraheren, omdat het flashgeheugen van de HomeBase direct op de printplaat was gesoldeerd en geen pinnen blootlegde waaraan we test-hooks konden bevestigen. In eerste instantie wilden we het niet lossolderen, dus wachtte ik op aanvullende hardware. Craig S. Blackie wees me op springclips die geschikt waren voor de maat van de Winbond 25Q256JWEQ (WSON8 8.6mm). Zodra de clip arriveerde, maakte ik de meest provisorische opstelling denkbaar, met elastiekjes om de clip te bevestigen, terwijl ik met de SPI0-interface van een Raspberry Pi metingen deed.

Ik voerde drie leesbeurten uit en deze hadden verschillende checksums, wat meestal een indicatie is dat er iets mis is. Na inspectie van de dumps realiseerde ik me dat ze onbruikbaar waren. In tegenstelling tot bij de deuurbel, activeerde de stroomvoorziening voor de leesoperatie in dit geval ook andere componenten op de printplaat die met het geheugen communiceerden. Om de firmware te extraheren, moesten we de chip dus lossolderen en buiten de plaat uitlezen.

Mijn zwager (Óscar Lestón) was de dappere ziel die het flashgeheugen lossoldeerde. Hoewel de geheugenmodule er na verwijdering slecht uitzag, was hij nauwelijks beschadigd.

Vervolgens repliceerden we dezelfde opstelling met de Raspberry Pi SPI 0 om de firmware te dumpen. Deze keer hadden de drie dumps dezelfde checksum en bevatten ze "interessante data". Het enige probleem was dat een blok van 64 KB was gewist of niet kon worden gelezen, zeer waarschijnlijk omdat we het hebben beschadigd tijdens het verwijderen van de chip.

Het grootste deel van de firmware kon worden geëxtraheerd met Binwalk:

/home/psyconauta/research/Eufy/homebase2-firmware/chipoff/STUFF/extractions/homestation2_firmware00.bin
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
DECIMAL                            HEXADECIMAL                        DESCRIPTION
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
83328                              0x14580                            U-Boot version string: 1.1.3 (Jun 14 2023 - 16:18:56)
3022226                            0x2E1D92                           XZ compressed data, total size: 11514432 bytes
14536660                           0xDDCFD4                           XZ compressed data, total size: 2200 bytes
14538862                           0xDDD86E                           XZ compressed data, total size: 2084 bytes
14540948                           0xDDE094                           XZ compressed data, total size: 1868 bytes
14542818                           0xDDE7E2                           XZ compressed data, total size: 168 bytes
14542988                           0xDDE88C                           XZ compressed data, total size: 4192 bytes
14547182                           0xDDF8EE                           XZ compressed data, total size: 2844 bytes
14550028                           0xDE040C                           XZ compressed data, total size: 244 bytes
14550282                           0xDE050A                           XZ compressed data, total size: 1452 bytes
15138816                           0xE70000                           uImage firmware image, header size: 64 bytes, data size: 3429849 bytes, compression: lzma, CPU: MIPS32, OS: Linux, image type: OS Kernel Image, load address: 0x80000000, entry point: 0x802E04F0, creation time: 2023-06-14 12:56:37, image name: "Linux Kernel Image"
21233664                           0x1440000                          JFFS2 filesystem, little endian, nodes: 3426, total size: 12255244 bytes

Sommige delen werden hersteld, andere niet. Omdat er veel XZ-streams waren, kozen we voor het idee om direct 7z te gebruiken om alles in één grote blob te extraheren en daar vervolgens in te zoeken.

➜  extractions mkdir -p 7z_mandanga
➜  extractions 7z e -y -o7z_mandanga homestation2_firmware00.bin
7-Zip 23.01 (x64) : Copyright (c) 1999-2023 Igor Pavlov : 2023-06-20
64-bit locale=es_ES.UTF-8 Threads:16 OPEN_MAX:1024
Scanning the drive for archives:
1 file, 33554432 bytes (32 MiB)
Extracting archive: homestation2_firmware00.bin
ERRORS:
There are data after the end of archive
--
Path = homestation2_firmware00.bin
Type = xz
ERRORS:
There are data after the end of archive
Offset = 3022226
Physical Size = 11514432
Tail Size = 19017774
Method = LZMA2:17
Streams = 358
Blocks = 358
Characteristics = BlockPackSize BlockUnpackSize
ERROR: There are some data after the end of the payload data : homestation2_firmware00
Sub items Errors: 1
Archives with Errors: 1
Open Errors: 1

---

0x01 Identificatie van het wifi-credential-generatie-algoritme

Een van de belangrijkste vragen die we wilden onderzoeken via de firmware was het nieuwe algoritme voor het genereren van inloggegevens voor de OCEAN_XXXXXX beheer-wifi. We merkten dat dit volledig verschilde van wat werd verwacht op basis van een paper uit 2024. We wilden specifiek weten of het algoritme nog steeds zwak was.

Ik gebruikte radare2 om het onderzoek uit te voeren. Eerst zochten we waar de wifi-netwerk ESSID en het wachtwoord te vinden waren in de firmware. Een simpele grep was voldoende om NVRAM-variabelen te identificeren die zowel de wifi-namen als de inloggegevens bevatten:

➜  extractions strings homestation2_firmware00.bin_83358_unknown.raw | grep F1VSXfNQL_ZCd3k -A5 -B5
AuthMode=WPAPSKWPA2PSK;WPAPSKWPA2PSK
EncrypType=AES;AES
RekeyInterval=3600
RekeyMethod=TIME
PMKCachePeriod=10
WPAPSK1=F1VSXfNQL_ZCd3k
WPAPSK2=913ceac3
DefaultKeyID=2;2
Key1Type=0
Key1Str1=
Key2Type=0

➜  extractions strings homestation2_firmware00.bin_83358_unknown.raw | grep OCEAN_E9A5FC -A5 -B5
DDNSPassword=
CountryRegion=0
CountryRegionABand=7
CountryCode=ES
BssidNum=2
SSID1=OCEAN_E9A5FC
SSID2=4350c372
WirelessMode=9
TxRate=0
Channel=0
BasicRate=15

Vervolgens zochten we naar binaire bestanden of scripts die gerelateerd waren aan de wifi-instellingen:

➜  7z_mandanga strings homestation2_firmware00 | rg 'OCEAN_'
%s,<%d>file not exist.return OCEAN_TEST_TF_CARD_NOT_FOUND.
OCEAN_MAGIC
out of memory of OCEAN_TEST_DATA
OCEAN_TF_CARD_FORMAT_SUCCESSED
OCEAN_TF_CARD_FORMAT_FAIL_MOUNT
OCEAN_TF_CARD_FORMAT_FAIL
OCEAN_TF_CARD_FORMAT_IN_USE
ssid_prefix="OCEAN_"
ssid_prefix="OCEAN_"
CONFIG_LEDS_OCEAN_PWM=y
OCEAN_RUN=/mnt/ocean.sh
if [ -f $OCEAN_RUN  ];then
chmod +x $OCEAN_RUN
sh $OCEAN_RUN &

Hieruit bleek dat de ESSID een concatenatie is van "OCEAN_" en het MAC-adres:

➜  7z_mandanga strings homestation2_firmware00 | rg 'ssid_prefix'
ssid_prefix="ocean_factory_"
ssid_prefix="OCEAN_"
WIFI_SSID=$ssid_prefix$SSID_MAC
ssid_prefix="`nvram_get ssid_pre`"
if [ "$ssid_prefix" = "" ];then
ssid_prefix="OCEAN_"
WIFI_SSID=$ssid_prefix$SSID_MAC

Het volledige script onthulde het volgende proces:

#!/bin/sh
work_mode="`cat /var/run/testmode`"
def_mode="`nvram_get 2860 work_mode`"
SSID_MAC=""
nvram_ssid="`nvram_get 2860 SSID1`"
bssid_num=`nvram_get 2860 BssidNum`
eth_state=`apcliDaemon get_eth_state`
repeater_enable=`nvram_get 2860 RepeaterEnable`
wifi_11bgn_enabled=`nvram_get 2860 wifi_11bgn_enabled`
Wireless_Mode=9
if [ "$eth_state" = "0" -a "$repeater_enable" = "1" -a "$wifi_11bgn_enabled" = "1" ]; then
Wireless_Mode=9
get_wifi_mac()
SSID_MAC="`ifconfig ra0 | grep HWaddr | awk '{print $5}' | awk -F ':' '{print $4$5$6}'`"
while [ "$SSID_MAC" = "" ]
sleep 1
SSID_MAC="`ifconfig ra0 | grep HWaddr | awk '{print $5}' | awk -F ':' '{print $4$5$6}'`"
done
echo "get mac in ssid is:$SSID_MAC"
if [ "$bssid_num" = "2" ];then
ifconfig ra1 down>/dev/null 2>&1
if [ "$work_mode" = "1" ];then
ssid_prefix="ocean_factory_"
if [ "$def_mode" = "factory_mode" ];then
ssid_prefix="OCEAN_"
get_wifi_mac
WIFI_SSID=$ssid_prefix$SSID_MAC
echo "set wifi SSID:$WIFI_SSID"
iwpriv ra0 set SSID="$WIFI_SSID"
if [ "$def_mode" = "factory_mode" ];then
if [ "$nvram_ssid" != $WIFI_SSID ];then
nvram_set SSID1 $WIFI_SSID
fi
else
ssid_prefix="`nvram_get ssid_pre`"
if [ "$ssid_prefix" = "" ];then
ssid_prefix="OCEAN_"
get_wifi_mac
WIFI_SSID=$ssid_prefix$SSID_MAC
echo "set wifi SSID:$WIFI_SSID"
if [ "$nvram_ssid" != $WIFI_SSID ];then
nvram_set SSID1 $WIFI_SSID
iwpriv ra0 set SSID="$WIFI_SSID"
if [ "$nvram_ssid" = "MT7628_AP" ];then
sleep 3
/sbin/umount_and_reboot.sh
fi
iwpriv ra0 set HtBw=0
iwpriv ra0 set WirelessMode=$Wireless_Mode
iwpriv ra0 set HtGi=0
iwpriv ra0 set SCSEnable=1
if [ "$bssid_num" = "2" ];then
iwpriv ra1 set HtBw=0
iwpriv ra1 set WirelessMode=$Wireless_Mode
iwpriv ra1 set HtGi=0
iwpriv ra1 set SCSEnable=1
iwpriv ra1 set WmmCapable=1
iwpriv ra1 set ed_chk=0
wpapsk="`nvram_get WPAPSK1`"
if [ "$wpapsk" != "" ];then
iwpriv ra0 set WPAPSK=$wpapsk
iwpriv ra0 set WmmCapable=1
wifi
apcli
iwpriv
home_security
if [ "$eth_state" = "0" -a "$repeater_enable" = "1" ]; then
switch_wan_interface.sh eth2.2 apcli0 0

We stelden vast dat de ESSID wordt gevormd uit "OCEAN_" + MAC-adres en dat het wachtwoord wordt gelezen uit de NVRAM-variabele WPAPSK1. Ik schreef een Python-script om het ELF-binary te extraheren dat deze string gebruikt:

#!/usr/bin/env python3
with open("homestation2_firmware00", "rb") as file:
    data = file.read()
    needle = data.find(b"WPAPSK1")
    magic = data.rfind(b"\x7fELF", 0, needle)
    end = data.find(b"\x7fELF", needle)
    binary = open("elf_wifi_01", "wb")
    binary.write(data[magic:end])
    binary.close()

Het resulterende bestand elfwifi01 is een MIPS32 executable. Met radare2 analyseerden we de functie setwifipwd. De logica werkt als volgt:

  1. Het programma controleert een parameter uit base_param.
  2. Als de waarde overeenkomt met 0x0C, wordt de functie z8GetSN() gebruikt.
  3. z8GetSN() communiceert via I2C (commando 0xC1) en leest 11 bytes uit de respons.
  4. Deze bytes worden omgezet in een string van 32 bytes en samen met het serienummer van de HomeBase (bijv. T8010...) gevoed aan de functie mac_func().
  5. mac_func() voert een HMAC SHA256 uit, waarbij de waarde van z8GetSN() de "message" is en het serienummer de "key".
  6. Het resultaat wordt door zxMemoryEncode omgezet naar Base64 met het volgende alfabet:

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_

  1. Tot slot wordt het wachtwoord bijgewerkt in de NVRAM en toegepast via het externe commando iwpriv ra0 set WPAPSK=%s.

---

0x02 De UART-lockdown doorbreken

UART-toegang tot het apparaat was geblokkeerd. Bij verbinding verscheen er een prompt om op "Enter" te drukken, maar zodra dit gebeurde, stopte het proces en startte het opnieuw, waardoor er een oneindige loop ontstond.

starting pid 5617, tty '/dev/ttyS1': '/sbin/login.sh'
process '/sbin/login.sh' (pid 5617) exited. Scheduling for restart.
Please press Enter to activate this console.
setrlimit(RLIMIT_CORE, &limit);

In de firmware vonden we in het bestand inittab dat de actie askfirst het script /sbin/login.sh aanroept:

#!/bin/sh
DEBUG_LOG="`nvram_get debug_log`"
if [ "$DEBUG_LOG" = "1" -o -f "/mnt/enable_console" ];then
echo "enter debug mode" > /dev/console
/bin/login
fi

Het script controleert of debuglog in de NVRAM op "1" staat of dat het bestand /mnt/enableconsole bestaat. Zo niet, dan wordt de shell niet toegestaan.

Omdat we de normale firmware-image hadden beschadigd (het gat van 64 KB op 0x500000), konden we deze niet simpelweg aanpassen. Echter, we ontdekten een intact "recovery image" op 0xe70000. Dit is een U-Boot image met een 64-byte header.

De structuur van het recovery-image is als volgt:

  • 0x00: Magic (0x27051956)
  • 0x04: Header CRC32
  • 0x0c: Compressed payload length
  • 0x10: RAM load address (0x80000000)
  • 0x14: Entry address (0x802e04f0)
  • 0x18: Payload CRC32

Ik heb het bestandssysteem van dit recovery-image uitgepakt en minimale wijzigingen aangebracht om direct toegang tot de shell met root-rechten te krijgen:

# Aanpassing in het script
old_condition = b'if [ "$DEBUG_LOG" = "1" ];then'
new_condition = b'if [ 1 = 1 ];then' + b' ' * 13

old_login = b'\t/bin/login\n'
new_login = b'\t/bin/sh   \n'

Nadat ik de Write Protection van de flashchip had uitgeschakeld, heb ik het aangepaste recovery-image geflasht en de chip teruggesoldeerd. Bij het opstarten faalt de normale image, springt het apparaat over naar het recovery-image, en we kregen direct een root-shell.

---

0x03 Filesystem gardening voor debugdoeleinden

Hoewel we een shell hebben, is het recovery-image zeer beperkt. Het bestandssysteem is niet gemonteerd en belangrijke libraries en kernelmodules (voor wifi en I2C) ontbreken.

Om dit op te lossen, gebruikte ik wget om bestanden van mijn laptop naar het apparaat over te zetten. Ik compileerde een nieuwe versie van BusyBox met extra commando's en creëerde een "staged" bestandssysteem. Omdat dit proces elke keer opnieuw moet gebeuren na een reboot, heb ik met behulp van AI een automatiseringsscript geschreven op basis van mijn logs.

De twee belangrijkste scripts voor het herstellen van het bestandssysteem zijn homeapp-restore-v5.sh en homeapp-mount-runtime.sh.

homeapp-restore-v5.sh (fragment): Dit script downloadt de benodigde assets (BusyBox, rootfs) via HTTP, verifieert de checksums, mount een tmpfs en bereidt de omgeving voor.

homeapp-mount-runtime.sh (fragment): Dit script voert bind-mounts uit van /dev, /proc, /sys, /etc, /var, /tmp en /media naar het staged bestandssysteem, zodat de binaire bestanden in hun verwachte omgeving kunnen draaien.

Na het uitvoeren van deze scripts kunnen we zien dat de services actief zijn:

# /media/homeapp-debug/busybox-extra netstat -lntup
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:10400           0.0.0.0:*               LISTEN      6016/home_security
tcp        0      0 0.0.0.0:10402           0.0.0.0:*               LISTEN      6016/home_security
tcp        0      0 0.0.0.0:32290           0.0.0.0:*               LISTEN      6016/home_security
tcp        0      0 0.0.0.0:10404           0.0.0.0:*               LISTEN      6016/home_security
tcp        0      0 0.0.0.0:32292           0.0.0.0:*               LISTEN      6016/home_security
tcp        0      0 0.0.0.0:32293           0.0.0.0:*               LISTEN      6016/home_security
tcp        0      0 0.0.0.0:32295           0.0.0.0:*               LISTEN      6016/home_security
tcp        0      0 0.0.0.0:10600           0.0.0.0:*               LISTEN      6016/home_security
tcp        0      0 0.0.0.0:32392           0.0.0.0:*               LISTEN      6016/home_security
tcp        0      0 0.0.0.0:9000            0.0.0.0:*               LISTEN      5974/pushMuxer
tcp        0      0 127.0.0.1:5000          0.0.0.0:*               LISTEN      5963/mips_collector
tcp        0      0 0.0.0.0:554             0.0.0.0:*               LISTEN      5974/pushMuxer
tcp        0      0 0.0.0.0:53              0.0.0.0:*               LISTEN      1405/dnsmasq
tcp        0      0 0.0.0.0:23              0.0.0.0:*               LISTEN      1693/telnetd

Als laatste stap heb ik een compatibele versie van GDB gecompileerd en geüpload, waardoor we nu volledige dynamische analyse kunnen uitvoeren op de HomeBase 2.

---

0x04 Slotwoord

Met de komst van LLM's ben ik mijn enthousiasme voor het publiceren van technische content verloren. Ik publiceer al sinds 2007 artikelen online, maar tegenwoordig voelt het alsof de waarde van het schrijven verdwijnt als niemand het meer echt leest. De meeste mensen vragen een AI om een samenvatting of scannen de tekst vluchtig.

Daarnaast is een groot deel van de gepubliceerde artikelen tegenwoordig door AI gegenereerd, in die "gelobotomiseerde" taal die me elke keer weer irriteert. Ik schrijf in gebroken Engels, maar ik lees duizend keer liever een artikel van een mens in gebroken Engels dan de zoveelste herhaling van stock-frases van ChatGPT.

Maakt het in 2026 nog wel zin om technische content voor mensen te schrijven? Vroeger was het nuttig om methodieken te lezen over hoe iemand een probleem oploste, zodat je dat jaren later kon toepassen. Nu verandert de wereld in een verzameling "meat proxies" die een LLM prompten om een probleem op te lossen.

Ik gebruik AI in mijn professionele werk, maar niet voor mijn hobbyprojecten. Ik geniet van de uren waarin ik mijn hoofd tegen de muur sla om iets uit te vogelen, en van dat "eureka"-moment wanneer het kwartje valt. Automatisering mag me dat plezier niet ontnemen.

Ik herinner me een zin uit de cryptochallenges van Matasano: "Het statistisch breken van een repeating-key XOR ('Vigenere') is overduidelijk een academische oefening, iets van 'Crypto 101'. Maar er zijn meer mensen die 'weten hoe' het moet, dan mensen die het daadwerkelijk kunnen."

Ik denk dat we nu leven in een wereld waar iedereen weet hoe je een LLM moet vragen om iets te hacken, in plaats van dat ze daadwerkelijk weten hoe ze iets moeten hacken.