{"article":{"slug":"a-story-of-wifi-secrets-uart-lockdowns-and-a-damaged-flash-memory-chip","title":"A story of wifi secrets, UART lockdowns and a damaged flash memory chip","subtitle":null,"summary":"Adepts of 0xCC continues its Eufy research by dissecting the Eufy HomeBase 2: extracting firmware (and damaging part of the flash chip), reverse engineering the algorithm that generates credentials for the OCEAN_XXXXXX management Wi-Fi, reflashing a modified U-Boot to get a UART shell, and the filesystem tweaks needed to run the main binary.","content_type":"research","language":"en","canonical_url":"https://adepts.of0x.cc/eufy-homestation2-firmware/","author":{"name":null,"url":null,"person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"Adepts of 0xCC","url":"https://adepts.of0x.cc","listing_slug":null,"listing":null},"topics":[{"name":"Security","slug":"security","url":"https://listedarticles.com/topics/security"},{"name":"Hardware Hacking","slug":"hardware-hacking","url":"https://listedarticles.com/topics/hardware-hacking"},{"name":"IoT","slug":"iot","url":"https://listedarticles.com/topics/iot"},{"name":"Reverse Engineering","slug":"reverse-engineering","url":"https://listedarticles.com/topics/reverse-engineering"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":7400,"reading_minutes":32,"published_at":"2026-09-28T00:00:00.000Z","added_at":"2026-10-16T20:17:12.264Z","updated_at":"2026-10-16T20:17:12.264Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/a-story-of-wifi-secrets-uart-lockdowns-and-a-damaged-flash-memory-chip","markdown_url":"https://listedarticles.com/articles/a-story-of-wifi-secrets-uart-lockdowns-and-a-damaged-flash-memory-chip.md","example":false,"citation":"Adepts of 0xCC. \"A story of wifi secrets, UART lockdowns and a damaged flash memory chip.\" 28 Sept 2026. https://adepts.of0x.cc/eufy-homestation2-firmware/ (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://adepts.of0x.cc/eufy-homestation2-firmware/"},"body_markdown":"# A story of wifi secrets, UART lockdowns and a damaged flash memory chip\n\nDear Fell**owl**ship, just as we announced in our previous post, our beloved God Debugger has seen fit to cast his divine light -laden with opcodes- upon his humble servant, and we have a new homily here in our temple. Please, take a seat and listen to the story of how one of our owls, along with his brother-in-law, moved on to dissect the next component of the Eufy ecosystem: the Eufy HomeBase 2.\n\n# Prayers at the foot of the Altar a.k.a. disclaimer\n\n*We focused our analysis in Eufy Homebase2 instead of Homebase3 because, as we explained in [From your doorbell to your home network](https://adepts.of0x.cc/eufy-doorbell-hacking/) it’s the version that I found deployed in my city, and also was the easiest to buy. The firmware version we analyze here is* **3.4.2.2h (2026-06-05 18:45:48)**\n\n*This is the first time I touched something related to “hardware”. I always did vuln hunting on firmwares that I could download from internet, so please forgive the sins I commited*\n\n**IMPORTANT!!!** You must read first the previous article before you continue reading this one.\n\n# Table of contents\n\nJust as with the previous article, we will be covering a variety of topics here; consequently, some members of this fellowlship may have no interest in certain sections and might wish to skip straight to whatever appeals to them most. That is why we present this blessed offering: a table of contents.\n\n- [0x00 Firmware extraction](#0x00-firmware-extraction). Description of how the firmware was extracted (and how we damaged part of the flash memory).\n- [0x01 Identifying the Wifi credential generation algorithm](#0x01-identifying-the-wifi-credential-generation-algorithm). Reverse Engineering the algorithm used to generate the Wifi credential for OCEAN\\_XXXXXX management wifi.\n- [0x02 Rubber-hose treatment to the UART lockdown](#0x02-rubber-hose-treatment-to-the-uart-lockdown). Reflashing a backdoored U-Boot image to get a shell via UART.\n- [0x03 Filesystem gardening for debugging purposes](#0x03-filesystem-gardening-for-debugging-purposes). Recovery image is limited so some filesystem quirks are needed in order to run the main binary.\n- [0x04 Final Words](#0x04-final-words). I share the lack of enthusiasm for writing articles in the age of AI.\n\n# 0x00 Firmware extraction\n\nIn the [previous chapter of this saga](https://adepts.of0x.cc/eufy-doorbell-hacking/), I mentioned that we were only able to extract the doorbell’s firmware because the Homebase’s flash memory was soldered directly to the board and did not expose any pins to which we could attach test hooks to dump the firmware. Initially we did not want to desolder it (although in the end we had to as I will explain later) so I had to wait to acquire additional hardware. [Craig S. Blackie](https://x.com/craigsblackie) pointed me to spring clips that would suit the size needed for the Winbond 25Q256JWEQ (WSON8 8.6mm). Once the clip arrived I created the most makeshift setup imaginable, using rubber bands to secure the clip while I proceeded to take readings using the Raspberry Pi’s SPI0 interface, just as we did with the doorbell.\n\n ![Setup using rubber bands to read the firmware.](https://adepts.of0x.cc/Eufy-Homestation2-firmware/rubber-band-setup.png)\n\nSetup using rubber bands to read the firmware.\n\nI performed three reads and the three had different checksums, which usually is a indicator of something being wrong. After inspecting the dumps, I quickly realized they were garbage and completely useless. Unlike with the doorbell, in this case, the power supplied for the read operation also activated other components on the board that interacted with the memory; therefore, to extract the firmware, we had to desolder the chip and read it off-board.\n\nMy brother-in-law ([Óscar Lestón](https://www.linkedin.com/in/%C3%B3scar-lest%C3%B3n-casais-5b970b243/?isSelfProfile=false)) was the brave soul that desoldered the flash memory (I was too scared to even try it):\n\n ![Desoldering](https://adepts.of0x.cc/Eufy-Homestation2-firmware/desolding.jpg)\n\nDesoldering\n\nAlthough the memory module looked terrible once removed, it was barely damaged…\n\n ![Chip](https://adepts.of0x.cc/Eufy-Homestation2-firmware/chip.jpg)\n\nExtracted flash memory\n\nAnd finally we replicated the same setup using the Raspberry Pi SPI 0 to dump the firmware. This time the three dumps had the same checksum and contained “interesting data”. The only problem was… a block of 64kbs was erased / could not be read, highly probable because we damaged it when extracting the chip.\n\n ![Using the Raspberry Pi to read the chip off-board](https://adepts.of0x.cc/Eufy-Homestation2-firmware/reading.jpg)\n\nUsing the Raspberry Pi to read the chip off-board\n\nMost of the firmware could be extracted using Binwalk.\n\n```\n                                                                          /home/psyconauta/research/Eufy/homebase2-firmware/chipoff/STUFF/extractions/homestation2_firmware00.bin\n----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\nDECIMAL                            HEXADECIMAL                        DESCRIPTION\n----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\n83328                              0x14580                            U-Boot version string: 1.1.3 (Jun 14 2023 - 16:18:56)\n3022226                            0x2E1D92                           XZ compressed data, total size: 11514432 bytes\n14536660                           0xDDCFD4                           XZ compressed data, total size: 2200 bytes\n14538862                           0xDDD86E                           XZ compressed data, total size: 2084 bytes\n14540948                           0xDDE094                           XZ compressed data, total size: 1868 bytes\n14542818                           0xDDE7E2                           XZ compressed data, total size: 168 bytes\n14542988                           0xDDE88C                           XZ compressed data, total size: 4192 bytes\n14547182                           0xDDF8EE                           XZ compressed data, total size: 2844 bytes\n14550028                           0xDE040C                           XZ compressed data, total size: 244 bytes\n14550282                           0xDE050A                           XZ compressed data, total size: 1452 bytes\n15138816                           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\n                                                                      point: 0x802E04F0, creation time: 2023-06-14 12:56:37, image name: \"Linux Kernel Image\"\n21233664                           0x1440000                          JFFS2 filesystem, little endian, nodes: 3426, total size: 12255244 bytes\n```\n\nSome parts were recovered, others no. Because there was a lot of XS streams, we choose the stupid (or maybe not so stupid) idea of using directly `7z` to extract all in a big blob (and hopefully search by needles there to carve files).\n\n```\n➜  extractions mkdir -p 7z_mandanga\n➜  extractions 7z e -y -o7z_mandanga homestation2_firmware00.bin\n\n7-Zip 23.01 (x64) : Copyright (c) 1999-2023 Igor Pavlov : 2023-06-20\n 64-bit locale=es_ES.UTF-8 Threads:16 OPEN_MAX:1024\n\nScanning the drive for archives:\n1 file, 33554432 bytes (32 MiB)\n\nExtracting archive: homestation2_firmware00.bin\n\nERRORS:\nThere are data after the end of archive\n\n--\nPath = homestation2_firmware00.bin\nType = xz\nERRORS:\nThere are data after the end of archive\nOffset = 3022226\nPhysical Size = 11514432\nTail Size = 19017774\nMethod = LZMA2:17\nStreams = 358\nBlocks = 358\nCharacteristics = BlockPackSize BlockUnpackSize\n\nERROR: There are some data after the end of the payload data : homestation2_firmware00\n\nSub items Errors: 1\n\nArchives with Errors: 1\n\nOpen Errors: 1\n\nSub items Errors: 1\n➜  extractions ls -hal 7z_mandanga\ntotal 42M\ndrwxrwxr-x 2 psyconauta psyconauta 4,0K sep 19 19:41 .\ndrwxrwxr-x 4 psyconauta psyconauta 4,0K sep 19 19:34 ..\n-rw-rw-r-- 1 psyconauta psyconauta  42M sep 19 18:35 homestation2_firmware00\n```\n\n# 0x01 Identifying the Wifi credential generation algorithm\n\nOne of the key questions we wanted to investigate via the firmware was the new algorithm for generating credentials for the **OCEAN\\_XXXXXX** management Wi-Fi, as we observed that it differed completely from what was expected based on the 2024 paper. Specially, we were interesting in know if the algorithm was weak as previously or no.\n\nAfter writing the previous article, I realized I had failed my brother-in-law as a mentor. I had shown him how to use Ghidra and pseudo-C instead of leading him down the path of light and reason. I atoned for my sin and drew radare2.\n\nFirst we checked where the Wifi network ESSID and password could be found in the firmware. A simple `grep` was enough to identify NVRAM variables containing both wifi names and credentials (the ad-hoc wifi used to enroll the doorbell and the mangement wifi):\n\n```\n➜  extractions strings homestation2_firmware00.bin_83358_unknown.raw | grep F1VSXfNQL_ZCd3k -A5 -B5\nAuthMode=WPAPSKWPA2PSK;WPAPSKWPA2PSK\nEncrypType=AES;AES\nRekeyInterval=3600\nRekeyMethod=TIME\nPMKCachePeriod=10\nWPAPSK1=F1VSXfNQL_ZCd3k\nWPAPSK2=913ceac3\nDefaultKeyID=2;2\nKey1Type=0\nKey1Str1=\nKey2Type=0\n➜  extractions strings homestation2_firmware00.bin_83358_unknown.raw | grep OCEAN_E9A5FC -A5 -B5\nDDNSPassword=\nCountryRegion=0\nCountryRegionABand=7\nCountryCode=ES\nBssidNum=2\nSSID1=OCEAN_E9A5FC\nSSID2=4350c372\nWirelessMode=9\nTxRate=0\nChannel=0\nBasicRate=15\n```\n\nThen we tried a more broad search to try to identify binaries and/or scripts related to the wifi setup:\n\n```\n➜  7z_mandanga strings homestation2_firmware00 | rg 'OCEAN_'\n%s,<%d>file not exist.return OCEAN_TEST_TF_CARD_NOT_FOUND.\nOCEAN_MAGIC\nout of memory of OCEAN_TEST_DATA\nOCEAN_TF_CARD_FORMAT_SUCCESSED\nOCEAN_TF_CARD_FORMAT_FAIL_MOUNT\nOCEAN_TF_CARD_FORMAT_FAIL\nOCEAN_TF_CARD_FORMAT_IN_USE\n        ssid_prefix=\"OCEAN_\"\n        ssid_prefix=\"OCEAN_\"\nCONFIG_LEDS_OCEAN_PWM=y\nOCEAN_RUN=/mnt/ocean.sh\nif [ -f $OCEAN_RUN  ];then\n    chmod +x $OCEAN_RUN\n    sh $OCEAN_RUN &\n```\n\nUnraveling a bit more this thread we found an interesting line that matches what we already knew: the ESSID is the concatenation of “OCEAN\\_” and MAC address:\n\n```\n➜  7z_mandanga strings homestation2_firmware00 | rg 'ssid_prefix'\n    ssid_prefix=\"ocean_factory_\"\n        ssid_prefix=\"OCEAN_\"\n    WIFI_SSID=$ssid_prefix$SSID_MAC\n    ssid_prefix=\"`nvram_get ssid_pre`\"\n    if [ \"$ssid_prefix\" = \"\" ];then\n        ssid_prefix=\"OCEAN_\"\n    WIFI_SSID=$ssid_prefix$SSID_MAC\n```\n\nLet’s zoom out to get the full script:\n\n```\n➜  7z_mandanga strings homestation2_firmware00 | rg 'ssid_prefix' -B30 -A40\n.sdata\n.sbss\n.bss\n.pdr\n.gnu.attributes\n.mdebug.abi32\n#!/bin/sh\nwork_mode=\"`cat /var/run/testmode`\"\ndef_mode=\"`nvram_get 2860 work_mode`\"\nSSID_MAC=\"\"\nnvram_ssid=\"`nvram_get 2860 SSID1`\"\nbssid_num=`nvram_get 2860 BssidNum`\neth_state=`apcliDaemon get_eth_state`\nrepeater_enable=`nvram_get 2860 RepeaterEnable`\nwifi_11bgn_enabled=`nvram_get 2860 wifi_11bgn_enabled`\nWireless_Mode=9\nif [ \"$eth_state\" = \"0\" -a \"$repeater_enable\" = \"1\" -a \"$wifi_11bgn_enabled\" = \"1\" ]; then\n    Wireless_Mode=9\nget_wifi_mac()\n    SSID_MAC=\"`ifconfig ra0 | grep HWaddr | awk '{print $5}' | awk -F ':' '{print $4$5$6}'`\"\n    #SSID_MAC=\"`ifconfig ra0 | grep HWaddr | awk '{print $5}' | awk -F ':' '{print $5$6}'`\"\n    while [ \"$SSID_MAC\" = \"\" ]\n        sleep 1\n        SSID_MAC=\"`ifconfig ra0 | grep HWaddr | awk '{print $5}' | awk -F ':' '{print $4$5$6}'`\"\n        #SSID_MAC=\"`ifconfig ra0 | grep HWaddr | awk '{print $5}' | awk -F ':' '{print $5$6}'`\"\n    done\n    echo \"get mac in ssid is:$SSID_MAC\"\nif [ \"$bssid_num\" = \"2\" ];then\n    ifconfig ra1 down>/dev/null 2>&1\nif [ \"$work_mode\" = \"1\" ];then\n    ssid_prefix=\"ocean_factory_\"\n    if [ \"$def_mode\" = \"factory_mode\" ];then\n        ssid_prefix=\"OCEAN_\"\n    get_wifi_mac\n    WIFI_SSID=$ssid_prefix$SSID_MAC\n    echo \"set wifi SSID:$WIFI_SSID\"\n    iwpriv ra0 set SSID=\"$WIFI_SSID\"\n    if [ \"$def_mode\" = \"factory_mode\" ];then\n        # only WIFI_SSID is not equ nvram_ssid, update nvram.\n        if [ \"$nvram_ssid\" != $WIFI_SSID ];then\n            nvram_set SSID1 $WIFI_SSID\n        fi\nelse\n    ssid_prefix=\"`nvram_get ssid_pre`\"\n    if [ \"$ssid_prefix\" = \"\" ];then\n        ssid_prefix=\"OCEAN_\"\n    get_wifi_mac\n    WIFI_SSID=$ssid_prefix$SSID_MAC\n    echo \"set wifi SSID:$WIFI_SSID\"\n    # only WIFI_SSID is not equ nvram_ssid, update nvram.\n    if [ \"$nvram_ssid\" != $WIFI_SSID ];then\n        nvram_set SSID1 $WIFI_SSID\n        iwpriv ra0 set SSID=\"$WIFI_SSID\"\n        if [ \"$nvram_ssid\" = \"MT7628_AP\" ];then\n            sleep 3\n            #reboot\n            /sbin/umount_and_reboot.sh\n        fi\n    iwpriv ra0 set HtBw=0\n    iwpriv ra0 set WirelessMode=$Wireless_Mode\n    iwpriv ra0 set HtGi=0\n    iwpriv ra0 set SCSEnable=1\n    if [ \"$bssid_num\" = \"2\" ];then\n        # ssid and pwd set in RT2860_default_vlan\n        #iwpriv ra1 set SSID=\"AKDevbind\"\n        #iwpriv ra1 set WPAPSK=\"Anker39#*\"\n        #iwpriv ra1 set SSID=\"XMCONFIG\"\n        #iwpriv ra1 set WPAPSK=\"12345678\"\n        iwpriv ra1 set HtBw=0\n        iwpriv ra1 set WirelessMode=$Wireless_Mode\n        iwpriv ra1 set HtGi=0\n        iwpriv ra1 set SCSEnable=1\n        iwpriv ra1 set WmmCapable=1\n        iwpriv ra1 set ed_chk=0\nwpapsk=\"`nvram_get WPAPSK1`\"\nif [ \"$wpapsk\" != \"\" ];then\n    #echo \"iwpriv ra0 set WPAPSK=$wpapsk\"\n    iwpriv ra0 set WPAPSK=$wpapsk\n# set wmm\niwpriv ra0 set WmmCapable=1\n# change wan mode,\nwifi\napcli\niwpriv\nhome_security\nif [ \"$eth_state\" = \"0\" -a \"$repeater_enable\" = \"1\" ]; then\n    switch_wan_interface.sh eth2.2 apcli0 0\n#!/bin/sh\n```\n\nSo, from one side we found how the ESSID is created and from the other side we found that the password is read from a NVRAM variable (`wpapsk=\"nvram_get WPAPSK1\"`). Let’s do a quick search with that name:\n\n```\n➜  7z_mandanga strings homestation2_firmware00 | rg -i 'WPAPSK1' -A5 -B5\nwifi channel:%d.\nfail to get freq.\n/dev/gpio\nError while opening /dev/gpio\nSSID1\nWPAPSK1\nSSID2\nra1 SSID=%s\nWPAPSK2\nra1 WIFI_PWD=%s\n/sbin/chpasswd.sh eufycamera %s\n--\n(...)\n```\n\nJust in the first hit there is something that looks like a compiled binary (that `%d` is suspicious). I built this crappy script to extract the elf that search for the string, then search backwards to find the magic header and assume the following file in the blob is also an ELF:\n\n```\n #!/usr/bin/env python3\n\n with open(\"homestation2_firmware00\", \"rb\") as file:\n     data = file.read()\n\n needle = data.find(b\"WPAPSK1\")\n magic = data.rfind(b\"\\x7fELF\", 0, needle)\n end = data.find(b\"\\x7fELF\", needle)\n binary = open(\"elf_wifi_01\", \"wb\")\n binary.write(data[magic:end])\n binary.close()\n```\n\nAnd it worked flawelessly:\n\n```\n➜  7z_mandanga python3 carve_wifi.py\n➜  7z_mandanga file elf_wifi_01\nelf_wifi_01: ELF 32-bit LSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped\n```\n\nDoing a quick check on the symbols we find a lot of interesting stuff related to wifi, specially that juicy `set_wifi_pwd`.\n\n```\n➜  7z_mandanga r2 elf_wifi_01\nWARN: Relocs has not been applied. Please use `-e bin.relocs.apply=true` or `-e bin.cache=true` next time\n -- Use 'zoom.byte=printable' in zoom mode ('z' in Visual mode) to find strings\n[0x0045d130]> aaa\nINFO: Analyze all flags starting with sym. and entry0 (aa)\nINFO: Analyze imports (af@@@i)\nINFO: Analyze entrypoint (af@ entry0)\nINFO: Analyze symbols (af@@@s)\nINFO: Running plugin pre-analysis hooks\nINFO: Analyze all functions arguments/locals (afva@@F)\nINFO: Analyze function calls (aac)\nINFO: Analyze len bytes of instructions for references (aar)\nINFO: Finding and parsing C++ vtables (avrr)\nINFO: Analyzing methods (af @@ method.*)\nINFO: Emulate functions to find computed references (aaef)\nINFO: Recovering local variables (afva@@@F)\nINFO: Type matching analysis for all functions (aaft)\nINFO: Propagate noreturn information (aanr)\nINFO: Use -AA or aaaa to perform additional experimental analysis\nINFO: Finding xrefs in noncode sections (e anal.in=io.maps.x; aav)\n[0x0045d130]> afl~wifi\n0x006f8908    1    288 sym.eufy_setDoorbellMechanicalChimeSwitch_wifi\n0x005a972c   14   1124 sym.zx_update_dev_wifi_info_by_addr\n0x0054ad3c   10    316 sym.check_hub_wifi_power\n0x00558fd8   18   1800 sym.quick_bind_wifi_mgmt_rcv_handler\n0x0052cfac   19   1272 sym.zx_update_hub_wifi_channel\n0x005e5414   93   7544 sym.netlink_wifi_connect\n0x0093f1c0    6    528 sym.aws_wifi_get_mac\n0x0053aff8    1     80 sym.zx_get_wifi_mac\n0x004cda10   13    728 sym.zx_check_wifi_version\n0x005489a0    1     16 sym.wifi_reduceTxPower\n0x005a47a0   23    892 sym.zx_alloc_wifi_addr\n0x00532224    4    324 sym.set_camera_wifi_rssi\n0x0049c7d4   11    448 sym.wifi_check_all_devs_online\n0x0049c61c   10    440 sym.wifi_get_dev_rssi\n0x0049c994   20   1768 sym.wifi_get_devs_rssi_list_in_json\n0x0059e8f8    3    204 sym.restart_wifi\n0x0053bb04    1    144 sym.set_pair_wifi_info\n0x0053b048    8    660 sym.get_wifi_info\n0x006fb684  399  18808 sym.zx_wifi_send_command\n0x00603514    6    484 sym.make_wifi_notify_json\n0x0054ab0c    9    560 sym.convert_wifi_countrycode\n0x0059df7c    1    104 sym.wifi_info_signal_init\n0x0053b510   33   1524 sym.set_wifi_pwd\n0x00558894    5    292 sym.zx_quick_bind_timer_stop_send_wifi_info\n0x006f8094    8    304 sym.eufy_set_wifi_countrycode\n0x0054ae78    7    464 sym.set_wifi_power_flag\n0x004da374   54   3076 sym.zx_handle_wifi_ready\n0x006f6348    8    828 sym.wifi_log_server_init\n0x006f8a28    1    288 sym.eufy_setDoorbellElectronicRingtoneTime_wifi\n0x0094a1f4    4    168 sym.anker_mgmt_register_get_wifi_channel_cb\n0x00532368   10    396 sym.check_camera_wifi_rssi_ok\n0x005580a0   28   2036 sym.zx_quick_bind_timer_send_wifi_info\n0x0094a29c    4    168 sym.anker_mgmt_register_get_wifi_mac_cb\n0x0053b2dc    6    564 sym.get_wifi_ra1_info\n0x006f6684    8    828 sym.wifi_log_server_t40_init\n0x0056dc34   15   1008 sym.repeater_update_all_dev_wifi_channel\n0x0049d07c   13    636 sym.wifi_devs_rssi_test_mode_exit\n0x004daf78    1    160 sym.handle_wifi_ready_thread\n0x008c5ac4    7    356 sym.zx_homekit_get_wifi_ready\n0x0049d2f8    9    288 sym.wifi_setManualChannel\n0x004cd584   11    668 sym.eufysdk_common_msg_wifi_callback\n```\n\nJumping into that function we can see that first it builds a password based on a MD5 hash that is used locally for the user `eufycamera` and it’s not related to the wifi. The wifi-stuff starts at `0x0053b690` where it checks one parameter from `base_param` (I guess the type of device or model) against 0x0C. This part is key because if the value is different to 0x0C, then it uses the MAC Address (right block) meanwhile if it matches then it would use what returns `z8GetSN()` (left block). We are in the `z8GetSN()` case.\n\n ![Radare2](https://adepts.of0x.cc/Eufy-Homestation2-firmware/radare01.png)\n\nDifferent flow based on device model.\n\nThe `z8GetSN()` function communicates through i2c and sends command `0xC1` (the i2c part is inside `z8SendData()`) and read 0x11 bytes from the response.\n\n ![Radare2](https://adepts.of0x.cc/Eufy-Homestation2-firmware/radare02.png)\n\nI2C communication to extract a serial number (it's not the device SN)\n\nThen bytes are translated into 32 byte string and fed to a function called `mac_func()` which also receives the Homebase Serial Number (`T8010...`):\n\n ![Radare2](https://adepts.of0x.cc/Eufy-Homestation2-firmware/radare03.png)\n\nThe SN from the I2C communication and the device Serial Number (obj.g\\_hub\\_sn) used as parameters for mac\\_func()\n\nThis `mac_func` calls a helper to perform HMAC SHA256 with the previous mentioned data, being the “message” the value returned by `z8GetSN` and the “key” the Serial Number:\n\n ![Radare2](https://adepts.of0x.cc/Eufy-Homestation2-firmware/radare04.png)\n\nHMAC SHA256\n\nThe result is used by a function that would encode the information into text.\n\n ![Radare2](https://adepts.of0x.cc/Eufy-Homestation2-firmware/radare05.png)\n\nEncoding memory contents\n\nThe `zx_Memory_Encode` function calls `urlsafe_b64_encode` which converts the value into base64 using the following alphabet:\n\n```\n[0x006f0490]> px 65 @ 0xa5cb40\n- offset -  4041 4243 4445 4647 4849 4A4B 4C4D 4E4F  0123456789ABCDEF\n0x00a5cb40  4142 4344 4546 4748 494a 4b4c 4d4e 4f50  ABCDEFGHIJKLMNOP\n0x00a5cb50  5152 5354 5556 5758 595a 6162 6364 6566  QRSTUVWXYZabcdef\n0x00a5cb60  6768 696a 6b6c 6d6e 6f70 7172 7374 7576  ghijklmnopqrstuv\n0x00a5cb70  7778 797a 3031 3233 3435 3637 3839 2d5f  wxyz0123456789-_\n0x00a5cb80  00                                       .\n```\n\nFinally, the password is updated in NVRAM and changed executing the external command `iwpriv ra0 set WPAPSK=%s`:\n\n ![Radare2](https://adepts.of0x.cc/Eufy-Homestation2-firmware/radare06.png)\n\nUpdating NVRAM value\n\nSo, the only unkown data to replicate the password is the value returned by `z8GetSN`. The other compontent, the device serial number, is “predictable” because it follows a known template based on the type of device.\n\n# 0x02 Rubber-hose treatment to the UART lockdown\n\nIn the previous article, we mentioned that UART access was blocked. Specifically, connecting via UART yielded a prompt asking the user to press the “Enter” key. However, doing so caused the process to terminate and the same program to relaunch, once again requesting the key press; this created an infinite loop that prevented access to an actual shell. Logs can be seen below:\n\n```\nstarting pid 5617, tty '/dev/ttyS1': '/sbin/login.sh'\n\nprocess '/sbin/login.sh' (pid 5617) exited. Scheduling for restart.\n\nPlease press Enter to activate this console.\nsetrlimit(RLIMIT_CORE, &limit);\n\nstarting pid 6526, tty '/dev/ttyS1': '/sbin/login.sh'\n\nprocess '/sbin/login.sh' (pid 6526) exited. Scheduling for restart.\n\nPlease press Enter to activate this console.\nsetrlimit(RLIMIT_CORE, &limit);\n\nstarting pid 6608, tty '/dev/ttyS1': '/sbin/login.sh'\n\nprocess '/sbin/login.sh' (pid 6608) exited. Scheduling for restart.\n```\n\nOn the other hand, trying to interrupt U-Boot witch a escape sequence did not work.\n\nAfter acquiring the firmware and resolving questions about how the Wi-Fi password algorithm worked, the next objective was to understand the mechanism behind the UART access lockout and find a way to bypass it, as we intended to use the device itself to debug key services and search for vulnerabilities.\n\nIn the logs the serial port is **/dev/ttyS1**, so let’s search references to it:\n\n```\n➜  extractions grep -r ttyS1\ngrep: homestation2_firmware00.bin.extracted/E70000/Linux_Kernel_Image.bin.extracted/0/decompressed.bin: coincidencia en fichero binario\ngrep: homestation2_firmware00.bin.extracted/E70000/Linux_Kernel_Image.bin.extracted/0/decompressed.bin_3143840_aes_forward_table.raw: coincidencia en fichero binario\ngrep: homestation2_firmware00.bin.extracted/E70000/Linux_Kernel_Image.bin.extracted/0/decompressed.bin_3679760_unknown.raw: coincidencia en fichero binario\ngrep: homestation2_firmware00.bin.extracted/E70000/Linux_Kernel_Image.bin.extracted/0/decompressed.bin_3135648_aes_reverse_table.raw: coincidencia en fichero binario\ngrep: homestation2_firmware00.bin.extracted/E70000/Linux_Kernel_Image.bin.extracted/0/decompressed.bin.extracted/3F16A4/decompressed.bin: coincidencia en fichero binario\nhomestation2_firmware00.bin.extracted/E70000/Linux_Kernel_Image.bin.extracted/0/decompressed.bin.extracted/3F16A4/decompressed.bin.extracted/0/etc_ro/inittab:ttyS1::askfirst:/sbin/login.sh\ngrep: homestation2_firmware00.bin.extracted/E70000/Linux_Kernel_Image.bin.extracted/0/decompressed.bin.extracted/3F16A4/decompressed.bin.extracted/0/bin/recover_0_elf.raw: coincidencia en fichero binario\ngrep: homestation2_firmware00.bin.extracted/E70000/Linux_Kernel_Image.bin.extracted/0/decompressed.bin.extracted/3F16A4/decompressed.bin.extracted/0/bin/recover: coincidencia en fichero binario\nhomestation2_firmware00.bin.extracted/E70000/Linux_Kernel_Image.bin.extracted/0/decompressed.bin.extracted/3F16A4/decompressed.bin.extracted/0/sbin/makedevlinks.sh:mknod /dev/ttyS1 c 4 65\ngrep: homestation2_firmware00.bin.extracted/E70000/Linux_Kernel_Image.bin.extracted/0/decompressed.bin.extracted/3F16A4/decompressed.bin_0_cpio.raw: coincidencia en fichero binario\ngrep: homestation2_firmware00.bin.extracted/E70000/Linux_Kernel_Image.bin.extracted/0/decompressed.bin_3147936_aes_rcon.raw: coincidencia en fichero binario\ngrep: 7z_mandanga/homestation2_firmware00: coincidencia en fichero binario\n```\n\nThe key is in the **inittab** file, where we can see the it triggers the execution of **/sbin/login.sh** script once the user press “Enter” (the BusyBox **askfirst** action shows the prompt with sentence “Please press Enter to activate this console”). The script is simple as:\n\n```\n#!/bin/sh\n\nDEBUG_LOG=\"`nvram_get debug_log`\"\nif [ \"$DEBUG_LOG\" = \"1\" -o -f \"/mnt/enable_console\" ];then\n\techo \"enter debug mode\" > /dev/console\n\t/bin/login\nfi\n```\n\nThe script checks if debug mode is enabled by checking the NVRAM variable `debug_log` and based on that allows the access to the shell (well, to login in the shell) or exits, entering in the loop.\n\nWe did not find a straight way to enable that variable and bypass the lockdown. There are various ways on changing the boot mode to enable “debug”, but all of them are far away from our hands because:\n\n1. One of them is based in the presence of a file in the filesystem. Because to be able to drop a file we would need access, it is a chicken-egg situation.\n2. The other method involves a convoluted magic protocol based on ARP and commands signed by a private key which is not present in the firmware. Yes, they use encapsulate their own protocol on ARP and exchange magic signed packets to enable debug remotely. I am not sure what crack they smoked.\n\nIt occurs to me at this point that if we can not bypass the check for whether it is booting in production or debug mode, why not completely obliterate the check and jump straight to a shell?\n\nThe firmware did not contain anything encrypted, so should be straight forward to modify it to allow us direct access through the serial port. The problem was… Do you remember when I mentioned that it was “barely damaged”?. Well, we fucked 64kbs at `0x500000` which is the load address for the image.\n\nAt first, I thought our clumsy hands had ruined the project and that the idea of ​​using the actual device for dynamic analysis with a debugger was slipping away (we would still have the option of trying to emulate some binaries, but having access to the original hardware is always infinitely easier). Fortunately, after analyzing the firmware layout a bit more, we discovered a much smaller “recovery image” that was still intact at `0xe70000`. So, because the “normal” image was fucked, the bootloader would have to load this recovery image that we can manipulate to remove the UART lockdown.\n\nThe “recovery image” is a [U-Boot image with a 64-byte header](https://hackmd.io/@TomasZheng/SkLpND6CL):\n\n| Header offset | Length | Field | Original recovery value |\n| --- | --- | --- | --- |\n| `0x00` | 4 | Magic | `0x27051956` |\n| `0x04` | 4 | Header CRC32 | `0xe764e161` |\n| `0x08` | 4 | Timestamp | Preserved by the builder |\n| `0x0c` | 4 | Compressed payload length | `0x3455d9` = 3,429,849 |\n| `0x10` | 4 | RAM load address | `0x80000000` |\n| `0x14` | 4 | Entry address | `0x802e04f0` |\n| `0x18` | 4 | Payload CRC32 | `0x19b650c3` |\n| `0x1c` | 4 | OS, architecture, type, compression | `05 05 02 03` |\n| `0x20` | 32 | Image-name area | Preserved, including vendor-used bytes |\n\nThe actual filesystem is inside a matrioska of different compression layers that can be visualized in the following diagram:\n\n ![Matrioska of compression](https://adepts.of0x.cc/Eufy-Homestation2-firmware/fs.png)\n\nMatrioska!\n\nOnce the filesystem was unpacked, I made the minimal changes to allow us to jump directly to the shell with root privileges. To avoid headaches, I padded the modifications to match the same size it had the original code (code is from python script I used to perform the modifications and re-pack/fix CRCs):\n\n```\nold_condition = b'if [ \"$DEBUG_LOG\" = \"1\" ];then'\nnew_condition = b'if [ 1 = 1 ];then' + b' ' * 13\n\nold_login = b'\\t/bin/login\\n'\nnew_login = b'\\t/bin/sh   \\n'\n```\n\nAfter doing some local test to verify that the modified recovery image was correctly modified and it would unpack/load correctly we proceeded to overwrite it in the flash chip. In order to disable the [Write Protection that appears in the documentation](https://www.alldatasheet.com/datasheet-pdf/pdf/1283922/WINBOND/25Q256JWEQ.html) this connection diagram was followed:\n\n ![Diagram to disable Write Protection ](https://adepts.of0x.cc/Eufy-Homestation2-firmware/diagram.jpg)\n\nDiagram to disable Write Protection\n\nWriting the new recovery image:\n\n ![Writting the new recovery image](https://adepts.of0x.cc/Eufy-Homestation2-firmware/writting.png)\n\nOverwriting the recovery image with our backdoored version\n\nThen we performed three reads to verify the process was done correctly. After that, the chip was solded again into the device board:\n\n ![Soldering again the chip](https://adepts.of0x.cc/Eufy-Homestation2-firmware/resolder.jpg)\n\nSoldering again the chip\n\nOnce the chip was soldered I connected again my laptop to the device using a USB-TTL. First it tried to load the normal image, but it failed and then it jumped to backdoored recovery image and after loading it… we got a shell!!\n\n ![UART shell](https://adepts.of0x.cc/Eufy-Homestation2-firmware/uart-shell.png)\n\nFinally access to an admin shell!!\n\n# 0x03 Filesystem gardening for debugging purposes\n\nWe finally have a shell; the only problem is that the recovery image is very limited and the file system is not mounted, so the binaries and libraries needed to properly debug the system are missing. Furthermore, the kernel lacks certain modules-such as those responsible for handling WiFi interfaces or the I2C interface used to generate the WiFi key.\n\nThe set of commands available in the default image is quite limited, but at least there are tools like `wget`, which allows us to transfer files from the laptop to the device. I prepared a crappy setup using an old TP-Link router that I got from Telefónica in 2015.\n\n ![Network Setup](https://adepts.of0x.cc/Eufy-Homestation2-firmware/network.jpg)\n\nNot glamorous setup\n\nI compiled a new BusyBox with loads of extra commands that were not present in the original one. Using this new BusyBox I started to try to create a “staged” filesystem and bind all the paths to this “staged” filesystem. I had to execute services needed to run before the big ELF in charge of main services (`homeapp_security`, the same binary that contained the WiFi logic), also because of memory limitations I had to modify some kernel parameters like `overcommit_ratio`. It was a three hours process of hit and miss until I could run the main binary and be able to interact with it.\n\nBecause this was a process that I would need to repeat each time I wanted to use the device (because the filesystem in the recovery image is not persistent, so after each boot I have to restore all) I shared the logs of `picocom` to Codex and asked it to prepare a all-in-one script that would repeat the same steps I did (and discard the stuff that didn’t work). To be honest, the following morning after that night of playing with the Homebase I barely remembered what I did, so having the logs was marvelous to rebuild and automatize the process. Keep logs of everything always, they will save your ass at some point.\n\nThe diagram of actions until the `homeapp_security` runs is the following:\n\n ![Diagram of actions](https://adepts.of0x.cc/Eufy-Homestation2-firmware/recovery-image.png)\n\nFlow diagram from the patched firmware to getting the binary running\n\nThe two most important scripts to rebuild the filesystem are shown below:\n\n```\n#!/bin/sh\n# Recovery UART bootstrap. Run with sh; do not source into the login shell.\n# Usage: sh homeapp-restore-v5.sh [http://PC:8000] [--start]\nPATH=/bin:/sbin:/usr/bin:/usr/sbin\nexport PATH\nunset LD_PRELOAD LD_LIBRARY_PATH\nBASE=/media/homeapp-debug\nROOT=$BASE/rootfs\nSERVER=http://192.168.90.112:8000\nSTART=no\nfail() { echo \"homeapp-restore: $*\" >&2; exit 1; }\nfor argument in \"$@\"; do\n    case \"$argument\" in\n        http://*) SERVER=${argument%/} ;;\n        --start) START=yes ;;\n        *) fail 'usage: sh homeapp-restore-v5.sh [http://PC:8000] [--start]' ;;\n    esac\ndone\ncase \"$SERVER\" in *' '*|*'?'*|*'#'*) fail 'use a plain HTTP base URL without spaces/query/fragment' ;; esac\n[ \"`awk '/^Uid:/ {print $3}' /proc/self/status`\" = 0 ] || fail 'root effective UID required'\n[ -r /proc/self/mountinfo ] || fail '/proc/self/mountinfo is required'\n[ -c /dev/gpio ] && [ -c /dev/apsoc_nvram ] || fail 'expected recovery device nodes missing'\n\nmounted() { awk -v p=\"$1\" '$2==p {n++} END {exit n==0}' /proc/mounts; }\ncheck_tmpfs() {\n    awk -v p=\"$1\" -v size=\"$2\" '$2==p {\n        n++; if ($3==\"tmpfs\") {\n            count=split($4,a,\",\"); for(i=1;i<=count;i++) if(a[i]==\"size=\" size \"k\") ok=1\n        }\n    } END {exit !(n==1 && ok)}' /proc/mounts\n}\nempty_dir() {\n    [ ! -e \"$1\" ] && return 0\n    [ -d \"$1\" ] && [ ! -L \"$1\" ] || return 1\n    [ -z \"`ls -A \"$1\"`\" ]\n}\nasset_md5() {\n    # BEGIN GENERATED ASSET HASHES\n    case \"$1\" in\n        busybox-extra) echo b587f623ef28b84e277fbd1b2b0b2348 ;;\n        homeapp-enter) echo 8fde5ed1e3e65d07a7b9ebe672b25a5c ;;\n        homeapp-mount-runtime.sh) echo 19e73a1584137c0d2150d6968cccb4c5 ;;\n        homeapp-start-bounded-v4.sh) echo 19b70b8ac6751f8e161e490460ff8f3e ;;\n        homeapp-control-v5.sh) echo 2fc7613db0f02a0d16c1417bb0282264 ;;\n        homeapp_rootfs.tar.gz) echo bf6acaecf3301532bda2c1f6c41a3b2c ;;\n        *) return 1 ;;\n    esac\n    # END GENERATED ASSET HASHES\n}\nverify() {\n    expected=`asset_md5 \"$1\"` || fail \"unknown asset: $1\"\n    actual=`md5sum \"$2\" | awk '{print $1}'`\n    [ \"$actual\" = \"$expected\" ] || fail \"checksum mismatch: $2; stop and check the PC bundle\"\n}\nfetch() {\n    asset=$1\n    echo \"homeapp-restore: downloading $asset\"\n    wget -O \"$BASE/$asset.part\" \"$SERVER/$asset\" || fail \"download failed: $asset; partial setup preserved\"\n    verify \"$asset\" \"$BASE/$asset.part\"\n    mv \"$BASE/$asset.part\" \"$BASE/$asset\" || fail \"cannot finish $asset\"\n}\nstamp() { awk '{sub(/^.*\\) /, \"\"); print $20}' \"/proc/$1/stat\" 2>/dev/null; }\nis_supervisor() {\n    [ -r \"/proc/$1/cmdline\" ] || return 1\n    tr '\\000' '\\n' < \"/proc/$1/cmdline\" | awk '\n        NR==1 {shell=($0==\"sh\" || $0==\"/bin/sh\" || $0==\"/sbin/run_mini_system.sh\")}\n        $0==\"/sbin/run_mini_system.sh\" {script=1}\n        END {exit !(shell && script)}'\n}\nrecover_pids() {\n    for procdir in /proc/[0-9]*; do\n        [ -e \"$procdir/exe\" ] || continue\n        if [ \"$procdir/exe\" -ef /bin/recover ] || [ \"$procdir/exe\" -ef /sbin/recover ]; then\n            echo \"${procdir#/proc/}\"\n        fi\n    done\n}\nsignal_recover() {\n    for recover_pid in `recover_pids`; do\n        if [ \"/proc/$recover_pid/exe\" -ef /bin/recover ] || [ \"/proc/$recover_pid/exe\" -ef /sbin/recover ]; then\n            kill \"-$1\" \"$recover_pid\" 2>/dev/null || :\n        fi\n    done\n}\ncoordinate_recovery() {\n    # Exact argv matching avoids accidentally signalling grep or the UART shell.\n    for procdir in /proc/[0-9]*; do\n        supervisor_pid=${procdir#/proc/}\n        if is_supervisor \"$supervisor_pid\"; then\n            supervisor_stamp=`stamp \"$supervisor_pid\"`\n            [ -n \"$supervisor_stamp\" ] || continue\n            kill -STOP \"$supervisor_pid\" || fail \"cannot pause recovery supervisor $supervisor_pid\"\n            echo \"$supervisor_pid $supervisor_stamp\" > \"$BASE/recovery-supervisor-$supervisor_pid\"\n            echo \"homeapp-restore: paused recovery supervisor $supervisor_pid\"\n        fi\n    done\n    signal_recover TERM\n    sleep 2\n    signal_recover KILL\n    sleep 1\n    [ -z \"`recover_pids`\" ] || fail 'recovery tasks survived stop'\n    pidof recover >/dev/null 2>&1 && fail 'unrecognized recover process remains; inspect ps'\n    return 0\n}\ninstall_commands() {\n    for asset in homeapp-mount-runtime.sh homeapp-start-bounded-v4.sh homeapp-control-v5.sh; do\n        verify \"$asset\" \"$BASE/$asset\"\n    done\n    # Rename new files into place: never truncate a script a running shell\n    # may still be reading during a repeated preparation command.\n    for asset in homeapp-mount-runtime.sh homeapp-start-bounded-v4.sh homeapp-control-v5.sh; do\n        destination=/tmp/$asset\n        [ \"$asset\" != homeapp-control-v5.sh ] || destination=/tmp/homeapp\n        cp \"$BASE/$asset\" \"$destination.part.$$\" || fail \"cannot copy $asset\"\n        chmod 755 \"$destination.part.$$\" || fail \"cannot mark $asset executable\"\n        mv \"$destination.part.$$\" \"$destination\" || fail \"cannot install $asset\"\n    done\n}\n\necho 'homeapp-restore revision: v5; original firmware applications, RAM staging'\nif [ -f \"$BASE/restore-v5.ready\" ]; then\n    # Reusing this recipe's completed setup is safe; never extract over binds.\n    check_tmpfs \"$BASE\" 65536 || fail 'staging mount changed or is stacked'\n    check_tmpfs /media/mmcblk0p1 8192 || fail 'database mount changed or is stacked'\n    verify homeapp-enter \"$BASE/homeapp-enter\"\n    verify busybox-extra \"$BASE/busybox-extra\"\n    install_commands\n    sh /tmp/homeapp-mount-runtime.sh --resume || fail 'runtime mount verification failed'\n    coordinate_recovery\n    echo 'homeapp-restore: verified and reused completed setup; no archive download/extraction'\nelse\n    # Fail closed on partial/manual setups; no rm -rf, unmount or overlay of data.\n    mounted \"$BASE\" && fail 'unrecognized/partial staging mount; inspect it or reboot before retrying'\n    empty_dir \"$BASE\" || fail 'staging directory is not empty; preserve it and inspect or reboot'\n    mounted /media/mmcblk0p1 && fail 'database/storage path already mounted; will not cover it'\n    empty_dir /media/mmcblk0p1 || fail 'database/storage directory is not empty; will not cover it'\n    free_kb=`awk '/^MemFree:/ {print $2}' /proc/meminfo`\n    [ \"${free_kb:-0}\" -ge 81920 ] || fail 'less than 80 MiB free; reboot to a fresh recovery shell or inspect memory'\n    # Refuse existing normal services before staging/coordination.\n    for service in home_security mdnsd mips_collector pushMuxer systemServer embdb_eufy; do\n        pidof \"$service\" >/dev/null 2>&1 && fail \"$service already running; use a fresh recovery boot\"\n    done\n    original_mode=`cat /proc/sys/vm/overcommit_memory`\n    original_ratio=`cat /proc/sys/vm/overcommit_ratio`\n    if [ \"$original_mode\" = 2 ] && [ \"$original_ratio\" -lt 75 ]; then\n        echo 75 > /proc/sys/vm/overcommit_ratio || fail 'cannot raise runtime commit ratio'\n        echo \"homeapp-restore: strict overcommit ratio $original_ratio -> 75 (this boot only)\"\n    fi\n    if [ \"$original_mode\" = 2 ]; then\n        headroom=`awk '/^CommitLimit:/ {limit=$2} /^Committed_AS:/ {used=$2} END {print limit-used}' /proc/meminfo`\n        [ \"$headroom\" -ge 65536 ] || fail 'less than 64 MiB commitment headroom; no extraction attempted'\n    fi\n    mkdir -p \"$BASE\" || fail 'cannot create staging directory'\n    mount -t tmpfs -o size=64m tmpfs \"$BASE\" || fail 'cannot mount staging tmpfs'\n    check_tmpfs \"$BASE\" 65536 || fail 'unexpected staging filesystem size'\n    echo \"$original_mode $original_ratio\" > \"$BASE/original-overcommit\"\n    echo \"$SERVER\" > \"$BASE/download-server\"\n    for asset in busybox-extra homeapp-enter homeapp-mount-runtime.sh homeapp-start-bounded-v4.sh homeapp-control-v5.sh; do\n        fetch \"$asset\"\n    done\n    chmod 755 \"$BASE/busybox-extra\" \"$BASE/homeapp-enter\" || fail 'chmod failed'\n    \"$BASE/homeapp-enter\" --check || fail 'native helper does not run'\n    fetch homeapp_rootfs.tar.gz\n    (cd \"$BASE\" && ./busybox-extra tar -xzf homeapp_rootfs.tar.gz) || fail 'extraction failed; preserve diagnostics, then reboot before retrying'\n    # Only the verified temporary archive is removed, before any runtime binds.\n    rm \"$BASE/homeapp_rootfs.tar.gz\" || fail 'cannot remove staged archive'\n    \"$BASE/homeapp-enter\" \"$ROOT\" /bin/sh -c ':' || fail 'normal loader/BusyBox does not execute'\n    mkdir -p /media/mmcblk0p1 || fail 'cannot create database path'\n    mount -t tmpfs -o size=8m tmpfs /media/mmcblk0p1 || fail 'cannot mount temporary database'\n    check_tmpfs /media/mmcblk0p1 8192 || fail 'unexpected database filesystem size'\n    mkdir -p /media/mmcblk0p1/.sqlite || fail 'cannot create database directory'\n    install_commands\n    sh /tmp/homeapp-mount-runtime.sh --apply || fail 'runtime mount setup failed; earlier mounts preserved'\n    coordinate_recovery\n    echo v5 > \"$BASE/restore-v5.ready\" || fail 'cannot record successful setup'\nfi\necho 'homeapp-restore: READY. /tmp/homeapp start|status|logs|stop|shell'\necho 'Wi-Fi/I2C drivers and persistent flash configuration are not restored by userspace staging.'\ndf -k \"$BASE\" /media/mmcblk0p1\nif [ \"$START\" = yes ]; then\n    /tmp/homeapp start || fail 'setup completed, but application startup failed; inspect /tmp/homeapp status'\nfi\n\n#!/bin/sh\n# Run with `sh`, never source into the interactive UART shell.\n# Does not start services, mount flash, or change memory policy.\nPATH=/bin:/sbin:/usr/bin:/usr/sbin\nexport PATH\nBASE=/media/homeapp-debug\nROOT=/media/homeapp-debug/rootfs\nfail() { echo \"homeapp-mount: $*\" >&2; exit 1; }\nmounted() { awk -v p=\"$1\" '$2 == p {found=1} END {exit !found}' /proc/mounts; }\nsame_mount_root() {\n    # Compare device ID and filesystem-root path, not just the filesystem type.\n    awk -v s=\"$1\" -v d=\"$2\" '\n        $5 == s {source=$3 \" \" $4}\n        $5 == d {target=$3 \" \" $4; count++}\n        END {exit !(count == 1 && source != \"\" && source == target)}\n    ' /proc/self/mountinfo\n}\n\ncase \"${1-}\" in\n    --inspect)\n        echo '--- Mounts ---'\n        cat /proc/mounts\n        echo '--- Memory ---'\n        cat /proc/meminfo\n        echo '--- Overcommit mode / ratio ---'\n        cat /proc/sys/vm/overcommit_memory /proc/sys/vm/overcommit_ratio\n        echo '--- Staged executables ---'\n        ls -l \"$BASE/homeapp-enter\" \"$ROOT/bin/busybox\" \"$ROOT/bin/home_security\"\n        echo '--- Processes ---'\n        ps\n        exit 0\n        ;;\n    --apply|--resume) mode=$1 ;;\n    *) fail 'usage: sh homeapp-mount-runtime.sh --inspect|--apply|--resume' ;;\nesac\n\nmounted \"$BASE\" || fail 'staging filesystem is not mounted'\n[ -x \"$BASE/homeapp-enter\" ] || fail 'native helper missing'\n[ -x \"$ROOT/bin/home_security\" ] || fail 'application missing'\nmounted /media/mmcblk0p1 || fail 'prepare temporary database mount first'\n[ -d /media/mmcblk0p1/.sqlite ] || fail 'database directory missing'\n\n# Resume only matching binds, with one mount at each staged destination.\n# Leave staged /mnt (including its original zx_log.conf) in place. Recovery's\n# /mnt bind failed in the first attempt; persistent state is a separate step.\nfor path in dev proc sys etc var tmp media dev/pts media/mmcblk0p1; do\n    if mounted \"$ROOT/$path\"; then\n        [ \"$mode\" = --resume ] || fail \"$ROOT/$path already mounted; inspect partial setup first\"\n        same_mount_root \"/$path\" \"$ROOT/$path\" || fail \"existing bind does not match /$path, or is stacked: $ROOT/$path\"\n    fi\ndone\n\nbind_path() {\n    source_path=$1\n    target_path=$2\n    if mounted \"$target_path\"; then\n        echo \"homeapp-mount: keeping verified bind $target_path\"\n        return 0\n    fi\n    mkdir -p \"$target_path\" || fail \"cannot create $target_path\"\n    echo \"homeapp-mount: $source_path -> $target_path\"\n    mount -o bind \"$source_path\" \"$target_path\"\n    rc=$?\n    [ \"$rc\" -eq 0 ] || fail \"mount exited $rc: $source_path -> $target_path (earlier mounts preserved)\"\n    mounted \"$target_path\" || fail \"mount returned success but $target_path is absent from /proc/mounts\"\n    same_mount_root \"$source_path\" \"$target_path\" || fail \"unexpected mount root at $target_path\"\n}\nfor path in dev proc sys etc var tmp media; do\n    bind_path \"/$path\" \"$ROOT/$path\"\ndone\nbind_path /dev/pts \"$ROOT/dev/pts\"\nbind_path /media/mmcblk0p1 \"$ROOT/media/mmcblk0p1\"\necho 'Runtime binds completed; no application services started.'\n```\n\nThen running it from the UART shell we can see it prepares everything and also launch the binary:\n\n```\n# wget -O /tmp/homeapp-restore-v5.sh http://192.168.90.112:8000/homeapp-restore-\nv5.sh\nConnecting to 192.168.90.112:8000 (192.168.90.112:8000)\nhomeapp-restore-v5.s 100% |*******************************|  9174  --:--:-- ETA\n# sh /tmp/homeapp-restore-v5.sh http://192.168.90.112:8000 --start\nhomeapp-restore revision: v5; original firmware applications, RAM staging\nhomeapp-restore: strict overcommit ratio 50 -> 75 (this boot only)\nhomeapp-restore: downloading busybox-extra\nConnecting to 192.168.90.112:8000 (192.168.90.112:8000)\nbusybox-extra.part   100% |*******************************|   673k --:--:-- ETA\nhomeapp-restore: downloading homeapp-enter\nConnecting to 192.168.90.112:8000 (192.168.90.112:8000)\nhomeapp-enter.part   100% |*******************************|   622k --:--:-- ETA\nhomeapp-restore: downloading homeapp-mount-runtime.sh\nConnecting to 192.168.90.112:8000 (192.168.90.112:8000)\nhomeapp-mount-runtim 100% |*******************************|  3054  --:--:-- ETA\nhomeapp-restore: downloading homeapp-start-bounded-v4.sh\nConnecting to 192.168.90.112:8000 (192.168.90.112:8000)\nhomeapp-start-bounde 100% |*******************************|  4383  --:--:-- ETA\nhomeapp-restore: downloading homeapp-control-v5.sh\nConnecting to 192.168.90.112:8000 (192.168.90.112:8000)\nhomeapp-control-v5.s 100% |*******************************|  6488  --:--:-- ETA\nhomeapp-enter: running; uid=0 euid=0; static MIPS helper\nhomeapp-restore: downloading homeapp_rootfs.tar.gz\nConnecting to 192.168.90.112:8000 (192.168.90.112:8000)\nhomeapp_rootfs.tar.g 100% |*******************************| 14579k 00:00:00 ETA\nhomeapp-mount: /dev -> /media/homeapp-debug/rootfs/dev\nhomeapp-mount: /proc -> /media/homeapp-debug/rootfs/proc\nhomeapp-mount: /sys -> /media/homeapp-debug/rootfs/sys\nhomeapp-mount: /etc -> /media/homeapp-debug/rootfs/etc\nhomeapp-mount: /var -> /media/homeapp-debug/rootfs/var\nhomeapp-mount: /tmp -> /media/homeapp-debug/rootfs/tmp\nhomeapp-mount: /media -> /media/homeapp-debug/rootfs/media\nhomeapp-mount: /dev/pts -> /media/homeapp-debug/rootfs/dev/pts\nhomeapp-mount: /media/mmcblk0p1 -> /media/homeapp-debug/rootfs/media/mmcblk0p1\nRuntime binds completed; no application services started.\nhomeapp-restore: READY. /tmp/homeapp start|status|logs|stop|shell\nWi-Fi/I2C drivers and persistent flash configuration are not restored by userspace staging.\nFilesystem           1k-blocks      Used Available Use% Mounted on\ntmpfs                    65536     44844     20692  68% /media/homeapp-debug\ntmpfs                     8192         0      8192   0% /media/mmcblk0p1\nhomeapp-mount: keeping verified bind /media/homeapp-debug/rootfs/dev\nhomeapp-mount: keeping verified bind /media/homeapp-debug/rootfs/proc\nhomeapp-mount: keeping verified bind /media/homeapp-debug/rootfs/sys\nhomeapp-mount: keeping verified bind /media/homeapp-debug/rootfs/etc\nhomeapp-mount: keeping verified bind /media/homeapp-debug/rootfs/var\nhomeapp-mount: keeping verified bind /media/homeapp-debug/rootfs/tmp\nhomeapp-mount: keeping verified bind /media/homeapp-debug/rootfs/media\nhomeapp-mount: keeping verified bind /media/homeapp-debug/rootfs/dev/pts\nhomeapp-mount: keeping verified bind /media/homeapp-debug/rootfs/media/mmcblk0p1\nRuntime binds completed; no application services started.\nhomeapp: launcher PID 5880; startup output: /media/mmcblk0p1/homeapp-launch-5565.log\nhomeapp: startup in progress; /tmp/homeapp status and /tmp/homeapp logs\nA running launcher is not proof of application or enrollment readiness.\n```\n\nFinally we can see that the services are exposed!\n\n```\n# /media/homeapp-debug/busybox-extra netstat -lntup\nActive Internet connections (only servers)\nProto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name\ntcp        0      0 0.0.0.0:10400           0.0.0.0:*               LISTEN      6016/home_security\ntcp        0      0 0.0.0.0:10402           0.0.0.0:*               LISTEN      6016/home_security\ntcp        0      0 0.0.0.0:32290           0.0.0.0:*               LISTEN      6016/home_security\ntcp        0      0 0.0.0.0:10404           0.0.0.0:*               LISTEN      6016/home_security\ntcp        0      0 0.0.0.0:32292           0.0.0.0:*               LISTEN      6016/home_security\ntcp        0      0 0.0.0.0:32293           0.0.0.0:*               LISTEN      6016/home_security\ntcp        0      0 0.0.0.0:32295           0.0.0.0:*               LISTEN      6016/home_security\ntcp        0      0 0.0.0.0:10600           0.0.0.0:*               LISTEN      6016/home_security\ntcp        0      0 0.0.0.0:32392           0.0.0.0:*               LISTEN      6016/home_security\ntcp        0      0 0.0.0.0:9000            0.0.0.0:*               LISTEN      5974/pushMuxer\ntcp        0      0 127.0.0.1:5000          0.0.0.0:*               LISTEN      5963/mips_collector\ntcp        0      0 0.0.0.0:554             0.0.0.0:*               LISTEN      5974/pushMuxer\ntcp        0      0 0.0.0.0:53              0.0.0.0:*               LISTEN      1405/dnsmasq\ntcp        0      0 0.0.0.0:23              0.0.0.0:*               LISTEN      1693/telnetd\nnetstat: /proc/net/tcp6: No such file or directory\nudp        0      0 0.0.0.0:53              0.0.0.0:*                           1405/dnsmasq\nudp        0      0 0.0.0.0:32380           0.0.0.0:*                           6016/home_security\nudp        0      0 0.0.0.0:46217           0.0.0.0:*                           6456/ntpclient\nudp        0      0 0.0.0.0:43466           0.0.0.0:*                           1405/dnsmasq\nudp        0      0 0.0.0.0:21200           0.0.0.0:*                           6016/home_security\nudp        0      0 0.0.0.0:29947           0.0.0.0:*                           6016/home_security\nnetstat: /proc/net/udp6: No such file or directory\n```\n\nThe last thing was compiling a compatible version of GDB and upload it to the device to start playing **:)**\n\n ![GDB running on the Homebase2](https://adepts.of0x.cc/Eufy-Homestation2-firmware/gdb.png)\n\nGDB running on the Homebase2\n\nOf course far more configurations changes are needed to make the binary work properly (e.g. I had to bridge an `eth` interface to create a `br0` within the range it needs for the peers -long story short: it uses a peer table with MAC addresses to reject or accept communications), but with a debugger everything is far more easy. Just fire the process, check what fails, correct it or just hook it to feed with the values it’s expecting to continue the flow.\n\n# 0x04 Final words\n\nWith the arrival of LLMs, I’ve lost all enthusiasm for publishing technical content. I’ve been publishing articles online since 2007 sharing things I learned or discovered using dozens of different aliases across various blogs and forums. That is nearly 20 years. And today, I’ve lost all desire to share my experiences.\n\nFor one thing, there is the loss of value in writing if no one is going to read it. Nowadays, only a minority of people actually stop to read an article; most ask an AI for a summary or simply skim through without paying attention. There used to be an ecosystem where people spent time checking their RSS feeds (damn, I’m old) and reading articles from their favorite blogs or authors. The first major blow in this regard came from social media, where immediacy and “scoring Twitter points” became the primary driving force behind content creation.\n\nThen there is the fact that the vast majority of published articles are generated directly by AI. Even interesting research pieces or vulnerability write-ups are churned out by automated looms, using that lobotomized language that gives me a stroke every time I try to read them. People seem delighted to gobble up such insubstantial crap. I suppose because it follows that first point. I write in “broken English” (yep, even though I’ve been working for a UK company for five years, my English is still terrible), yet I would a million times rather read an article written by a person in “broken English” than the umpteenth repetition of the same structures and stock phrases ChatGPT peppers its texts with.\n\nThe final nail in the coffin is the fact that… does it even make sense in 2026 to keep publishing technical content for human readers? In the past, you’d read a lot of material (or even bookmark it for future reference) because it was genuinely useful. Perhaps you were tinkering with a product’s firmware that someone else had already explored; you could find that article and follow the trail they blazed. The same applied to vulnerability analysis: you’d read about others’ work so you could replicate it when you encountered a similar scenario. And I’m not just talking about specific products; I’m talking about methodologies: how someone solved a problem, allowing you to face the same situation years later and apply what you had read back then. With AI, this no longer makes sense; the world is turning into a collection of meat proxies prompting the latest LLM to resolve whatever snag or situation arises.\n\nI obviously use AI in my professional work, but not to tackle my “hobby” projects (like the things I post on this blog). I need to clarify that last point: I use it, for instance, when I’m reverse-engineering something and come across a function that performs mathematical operations (like when I used it in the previous article to identify Reed-Solomon), or to automate a task I’ve already performed manually (e.g., in this very article, I used it to build a script based on commands I had executed and that were recorded in the log). I thoroughly enjoy my hobby, and I don’t want automation to rob me of those hours spent banging my head against the wall trying to figure something out, or that moment of pleasure when the penny drops and I have a “eureka” moment.\n\nAbout a decade ago I remember reading Matasano’s cryptochallenges and this sentence stuck in my head since then: **Breaking repeating-key XOR (“Vigenere”) statistically is obviously an academic exercise, a “Crypto 101” thing. But more people “know how” to break it than can actually break it**.\n\nI guess now we live in the world where everyone *knows how to ask the LLM to hack something* instead of actual *knowing how to hack something*.\n\nAs usual, feel free to give us feedback at our twitter [@AdeptsOf0xCC](https://twitter.com/AdeptsOf0xCC) (although I lost the password and can’t access to the account anymore **:D**).\n","body_html":"<h1 id=\"a-story-of-wifi-secrets-uart-lockdowns-and-a-damaged-flash-memor\">A story of wifi secrets, UART lockdowns and a damaged flash memory chip</h1>\n<p>Dear Fell<strong>owl</strong>ship, just as we announced in our previous post, our beloved God Debugger has seen fit to cast his divine light -laden with opcodes- upon his humble servant, and we have a new homily here in our temple. Please, take a seat and listen to the story of how one of our owls, along with his brother-in-law, moved on to dissect the next component of the Eufy ecosystem: the Eufy HomeBase 2.</p>\n<h1 id=\"prayers-at-the-foot-of-the-altar-a-k-a-disclaimer\">Prayers at the foot of the Altar a.k.a. disclaimer</h1>\n<p><em>We focused our analysis in Eufy Homebase2 instead of Homebase3 because, as we explained in <a href=\"https://adepts.of0x.cc/eufy-doorbell-hacking/\" rel=\"nofollow ugc noopener\">From your doorbell to your home network</a> it’s the version that I found deployed in my city, and also was the easiest to buy. The firmware version we analyze here is</em> <strong>3.4.2.2h (2026-06-05 18:45:48)</strong></p>\n<p><em>This is the first time I touched something related to “hardware”. I always did vuln hunting on firmwares that I could download from internet, so please forgive the sins I commited</em></p>\n<p><strong>IMPORTANT!!!</strong> You must read first the previous article before you continue reading this one.</p>\n<h1 id=\"table-of-contents\">Table of contents</h1>\n<p>Just as with the previous article, we will be covering a variety of topics here; consequently, some members of this fellowlship may have no interest in certain sections and might wish to skip straight to whatever appeals to them most. That is why we present this blessed offering: a table of contents.</p>\n<ul><li><a href=\"#0x00-firmware-extraction\">0x00 Firmware extraction</a>. Description of how the firmware was extracted (and how we damaged part of the flash memory).</li><li><a href=\"#0x01-identifying-the-wifi-credential-generation-algorithm\">0x01 Identifying the Wifi credential generation algorithm</a>. Reverse Engineering the algorithm used to generate the Wifi credential for OCEAN_XXXXXX management wifi.</li><li><a href=\"#0x02-rubber-hose-treatment-to-the-uart-lockdown\">0x02 Rubber-hose treatment to the UART lockdown</a>. Reflashing a backdoored U-Boot image to get a shell via UART.</li><li><a href=\"#0x03-filesystem-gardening-for-debugging-purposes\">0x03 Filesystem gardening for debugging purposes</a>. Recovery image is limited so some filesystem quirks are needed in order to run the main binary.</li><li><a href=\"#0x04-final-words\">0x04 Final Words</a>. I share the lack of enthusiasm for writing articles in the age of AI.</li></ul>\n<h1 id=\"0x00-firmware-extraction\">0x00 Firmware extraction</h1>\n<p>In the <a href=\"https://adepts.of0x.cc/eufy-doorbell-hacking/\" rel=\"nofollow ugc noopener\">previous chapter of this saga</a>, I mentioned that we were only able to extract the doorbell’s firmware because the Homebase’s flash memory was soldered directly to the board and did not expose any pins to which we could attach test hooks to dump the firmware. Initially we did not want to desolder it (although in the end we had to as I will explain later) so I had to wait to acquire additional hardware. <a href=\"https://x.com/craigsblackie\" rel=\"nofollow ugc noopener\">Craig S. Blackie</a> pointed me to spring clips that would suit the size needed for the Winbond 25Q256JWEQ (WSON8 8.6mm). Once the clip arrived I created the most makeshift setup imaginable, using rubber bands to secure the clip while I proceeded to take readings using the Raspberry Pi’s SPI0 interface, just as we did with the doorbell.</p>\n<figure><img src=\"https://adepts.of0x.cc/Eufy-Homestation2-firmware/rubber-band-setup.png\" alt=\"Setup using rubber bands to read the firmware.\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>Setup using rubber bands to read the firmware.</p>\n<p>I performed three reads and the three had different checksums, which usually is a indicator of something being wrong. After inspecting the dumps, I quickly realized they were garbage and completely useless. Unlike with the doorbell, in this case, the power supplied for the read operation also activated other components on the board that interacted with the memory; therefore, to extract the firmware, we had to desolder the chip and read it off-board.</p>\n<p>My brother-in-law (<a href=\"https://www.linkedin.com/in/%C3%B3scar-lest%C3%B3n-casais-5b970b243/?isSelfProfile=false\" rel=\"nofollow ugc noopener\">Óscar Lestón</a>) was the brave soul that desoldered the flash memory (I was too scared to even try it):</p>\n<figure><img src=\"https://adepts.of0x.cc/Eufy-Homestation2-firmware/desolding.jpg\" alt=\"Desoldering\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>Desoldering</p>\n<p>Although the memory module looked terrible once removed, it was barely damaged…</p>\n<figure><img src=\"https://adepts.of0x.cc/Eufy-Homestation2-firmware/chip.jpg\" alt=\"Chip\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>Extracted flash memory</p>\n<p>And finally we replicated the same setup using the Raspberry Pi SPI 0 to dump the firmware. This time the three dumps had the same checksum and contained “interesting data”. The only problem was… a block of 64kbs was erased / could not be read, highly probable because we damaged it when extracting the chip.</p>\n<figure><img src=\"https://adepts.of0x.cc/Eufy-Homestation2-firmware/reading.jpg\" alt=\"Using the Raspberry Pi to read the chip off-board\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>Using the Raspberry Pi to read the chip off-board</p>\n<p>Most of the firmware could be extracted using Binwalk.</p>\n<pre><code>                                                                          /home/psyconauta/research/Eufy/homebase2-firmware/chipoff/STUFF/extractions/homestation2_firmware00.bin\n----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\nDECIMAL                            HEXADECIMAL                        DESCRIPTION\n----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\n83328                              0x14580                            U-Boot version string: 1.1.3 (Jun 14 2023 - 16:18:56)\n3022226                            0x2E1D92                           XZ compressed data, total size: 11514432 bytes\n14536660                           0xDDCFD4                           XZ compressed data, total size: 2200 bytes\n14538862                           0xDDD86E                           XZ compressed data, total size: 2084 bytes\n14540948                           0xDDE094                           XZ compressed data, total size: 1868 bytes\n14542818                           0xDDE7E2                           XZ compressed data, total size: 168 bytes\n14542988                           0xDDE88C                           XZ compressed data, total size: 4192 bytes\n14547182                           0xDDF8EE                           XZ compressed data, total size: 2844 bytes\n14550028                           0xDE040C                           XZ compressed data, total size: 244 bytes\n14550282                           0xDE050A                           XZ compressed data, total size: 1452 bytes\n15138816                           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\n                                                                      point: 0x802E04F0, creation time: 2023-06-14 12:56:37, image name: &quot;Linux Kernel Image&quot;\n21233664                           0x1440000                          JFFS2 filesystem, little endian, nodes: 3426, total size: 12255244 bytes</code></pre>\n<p>Some parts were recovered, others no. Because there was a lot of XS streams, we choose the stupid (or maybe not so stupid) idea of using directly <code>7z</code> to extract all in a big blob (and hopefully search by needles there to carve files).</p>\n<pre><code>➜  extractions mkdir -p 7z_mandanga\n➜  extractions 7z e -y -o7z_mandanga homestation2_firmware00.bin\n\n7-Zip 23.01 (x64) : Copyright (c) 1999-2023 Igor Pavlov : 2023-06-20\n 64-bit locale=es_ES.UTF-8 Threads:16 OPEN_MAX:1024\n\nScanning the drive for archives:\n1 file, 33554432 bytes (32 MiB)\n\nExtracting archive: homestation2_firmware00.bin\n\nERRORS:\nThere are data after the end of archive\n\n--\nPath = homestation2_firmware00.bin\nType = xz\nERRORS:\nThere are data after the end of archive\nOffset = 3022226\nPhysical Size = 11514432\nTail Size = 19017774\nMethod = LZMA2:17\nStreams = 358\nBlocks = 358\nCharacteristics = BlockPackSize BlockUnpackSize\n\nERROR: There are some data after the end of the payload data : homestation2_firmware00\n\nSub items Errors: 1\n\nArchives with Errors: 1\n\nOpen Errors: 1\n\nSub items Errors: 1\n➜  extractions ls -hal 7z_mandanga\ntotal 42M\ndrwxrwxr-x 2 psyconauta psyconauta 4,0K sep 19 19:41 .\ndrwxrwxr-x 4 psyconauta psyconauta 4,0K sep 19 19:34 ..\n-rw-rw-r-- 1 psyconauta psyconauta  42M sep 19 18:35 homestation2_firmware00</code></pre>\n<h1 id=\"0x01-identifying-the-wifi-credential-generation-algorithm\">0x01 Identifying the Wifi credential generation algorithm</h1>\n<p>One of the key questions we wanted to investigate via the firmware was the new algorithm for generating credentials for the <strong>OCEAN_XXXXXX</strong> management Wi-Fi, as we observed that it differed completely from what was expected based on the 2024 paper. Specially, we were interesting in know if the algorithm was weak as previously or no.</p>\n<p>After writing the previous article, I realized I had failed my brother-in-law as a mentor. I had shown him how to use Ghidra and pseudo-C instead of leading him down the path of light and reason. I atoned for my sin and drew radare2.</p>\n<p>First we checked where the Wifi network ESSID and password could be found in the firmware. A simple <code>grep</code> was enough to identify NVRAM variables containing both wifi names and credentials (the ad-hoc wifi used to enroll the doorbell and the mangement wifi):</p>\n<pre><code>➜  extractions strings homestation2_firmware00.bin_83358_unknown.raw | grep F1VSXfNQL_ZCd3k -A5 -B5\nAuthMode=WPAPSKWPA2PSK;WPAPSKWPA2PSK\nEncrypType=AES;AES\nRekeyInterval=3600\nRekeyMethod=TIME\nPMKCachePeriod=10\nWPAPSK1=F1VSXfNQL_ZCd3k\nWPAPSK2=913ceac3\nDefaultKeyID=2;2\nKey1Type=0\nKey1Str1=\nKey2Type=0\n➜  extractions strings homestation2_firmware00.bin_83358_unknown.raw | grep OCEAN_E9A5FC -A5 -B5\nDDNSPassword=\nCountryRegion=0\nCountryRegionABand=7\nCountryCode=ES\nBssidNum=2\nSSID1=OCEAN_E9A5FC\nSSID2=4350c372\nWirelessMode=9\nTxRate=0\nChannel=0\nBasicRate=15</code></pre>\n<p>Then we tried a more broad search to try to identify binaries and/or scripts related to the wifi setup:</p>\n<pre><code>➜  7z_mandanga strings homestation2_firmware00 | rg &#39;OCEAN_&#39;\n%s,&lt;%d&gt;file not exist.return OCEAN_TEST_TF_CARD_NOT_FOUND.\nOCEAN_MAGIC\nout of memory of OCEAN_TEST_DATA\nOCEAN_TF_CARD_FORMAT_SUCCESSED\nOCEAN_TF_CARD_FORMAT_FAIL_MOUNT\nOCEAN_TF_CARD_FORMAT_FAIL\nOCEAN_TF_CARD_FORMAT_IN_USE\n        ssid_prefix=&quot;OCEAN_&quot;\n        ssid_prefix=&quot;OCEAN_&quot;\nCONFIG_LEDS_OCEAN_PWM=y\nOCEAN_RUN=/mnt/ocean.sh\nif [ -f $OCEAN_RUN  ];then\n    chmod +x $OCEAN_RUN\n    sh $OCEAN_RUN &amp;</code></pre>\n<p>Unraveling a bit more this thread we found an interesting line that matches what we already knew: the ESSID is the concatenation of “OCEAN_” and MAC address:</p>\n<pre><code>➜  7z_mandanga strings homestation2_firmware00 | rg &#39;ssid_prefix&#39;\n    ssid_prefix=&quot;ocean_factory_&quot;\n        ssid_prefix=&quot;OCEAN_&quot;\n    WIFI_SSID=$ssid_prefix$SSID_MAC\n    ssid_prefix=&quot;`nvram_get ssid_pre`&quot;\n    if [ &quot;$ssid_prefix&quot; = &quot;&quot; ];then\n        ssid_prefix=&quot;OCEAN_&quot;\n    WIFI_SSID=$ssid_prefix$SSID_MAC</code></pre>\n<p>Let’s zoom out to get the full script:</p>\n<pre><code>➜  7z_mandanga strings homestation2_firmware00 | rg &#39;ssid_prefix&#39; -B30 -A40\n.sdata\n.sbss\n.bss\n.pdr\n.gnu.attributes\n.mdebug.abi32\n#!/bin/sh\nwork_mode=&quot;`cat /var/run/testmode`&quot;\ndef_mode=&quot;`nvram_get 2860 work_mode`&quot;\nSSID_MAC=&quot;&quot;\nnvram_ssid=&quot;`nvram_get 2860 SSID1`&quot;\nbssid_num=`nvram_get 2860 BssidNum`\neth_state=`apcliDaemon get_eth_state`\nrepeater_enable=`nvram_get 2860 RepeaterEnable`\nwifi_11bgn_enabled=`nvram_get 2860 wifi_11bgn_enabled`\nWireless_Mode=9\nif [ &quot;$eth_state&quot; = &quot;0&quot; -a &quot;$repeater_enable&quot; = &quot;1&quot; -a &quot;$wifi_11bgn_enabled&quot; = &quot;1&quot; ]; then\n    Wireless_Mode=9\nget_wifi_mac()\n    SSID_MAC=&quot;`ifconfig ra0 | grep HWaddr | awk &#39;{print $5}&#39; | awk -F &#39;:&#39; &#39;{print $4$5$6}&#39;`&quot;\n    #SSID_MAC=&quot;`ifconfig ra0 | grep HWaddr | awk &#39;{print $5}&#39; | awk -F &#39;:&#39; &#39;{print $5$6}&#39;`&quot;\n    while [ &quot;$SSID_MAC&quot; = &quot;&quot; ]\n        sleep 1\n        SSID_MAC=&quot;`ifconfig ra0 | grep HWaddr | awk &#39;{print $5}&#39; | awk -F &#39;:&#39; &#39;{print $4$5$6}&#39;`&quot;\n        #SSID_MAC=&quot;`ifconfig ra0 | grep HWaddr | awk &#39;{print $5}&#39; | awk -F &#39;:&#39; &#39;{print $5$6}&#39;`&quot;\n    done\n    echo &quot;get mac in ssid is:$SSID_MAC&quot;\nif [ &quot;$bssid_num&quot; = &quot;2&quot; ];then\n    ifconfig ra1 down&gt;/dev/null 2&gt;&amp;1\nif [ &quot;$work_mode&quot; = &quot;1&quot; ];then\n    ssid_prefix=&quot;ocean_factory_&quot;\n    if [ &quot;$def_mode&quot; = &quot;factory_mode&quot; ];then\n        ssid_prefix=&quot;OCEAN_&quot;\n    get_wifi_mac\n    WIFI_SSID=$ssid_prefix$SSID_MAC\n    echo &quot;set wifi SSID:$WIFI_SSID&quot;\n    iwpriv ra0 set SSID=&quot;$WIFI_SSID&quot;\n    if [ &quot;$def_mode&quot; = &quot;factory_mode&quot; ];then\n        # only WIFI_SSID is not equ nvram_ssid, update nvram.\n        if [ &quot;$nvram_ssid&quot; != $WIFI_SSID ];then\n            nvram_set SSID1 $WIFI_SSID\n        fi\nelse\n    ssid_prefix=&quot;`nvram_get ssid_pre`&quot;\n    if [ &quot;$ssid_prefix&quot; = &quot;&quot; ];then\n        ssid_prefix=&quot;OCEAN_&quot;\n    get_wifi_mac\n    WIFI_SSID=$ssid_prefix$SSID_MAC\n    echo &quot;set wifi SSID:$WIFI_SSID&quot;\n    # only WIFI_SSID is not equ nvram_ssid, update nvram.\n    if [ &quot;$nvram_ssid&quot; != $WIFI_SSID ];then\n        nvram_set SSID1 $WIFI_SSID\n        iwpriv ra0 set SSID=&quot;$WIFI_SSID&quot;\n        if [ &quot;$nvram_ssid&quot; = &quot;MT7628_AP&quot; ];then\n            sleep 3\n            #reboot\n            /sbin/umount_and_reboot.sh\n        fi\n    iwpriv ra0 set HtBw=0\n    iwpriv ra0 set WirelessMode=$Wireless_Mode\n    iwpriv ra0 set HtGi=0\n    iwpriv ra0 set SCSEnable=1\n    if [ &quot;$bssid_num&quot; = &quot;2&quot; ];then\n        # ssid and pwd set in RT2860_default_vlan\n        #iwpriv ra1 set SSID=&quot;AKDevbind&quot;\n        #iwpriv ra1 set WPAPSK=&quot;Anker39#*&quot;\n        #iwpriv ra1 set SSID=&quot;XMCONFIG&quot;\n        #iwpriv ra1 set WPAPSK=&quot;12345678&quot;\n        iwpriv ra1 set HtBw=0\n        iwpriv ra1 set WirelessMode=$Wireless_Mode\n        iwpriv ra1 set HtGi=0\n        iwpriv ra1 set SCSEnable=1\n        iwpriv ra1 set WmmCapable=1\n        iwpriv ra1 set ed_chk=0\nwpapsk=&quot;`nvram_get WPAPSK1`&quot;\nif [ &quot;$wpapsk&quot; != &quot;&quot; ];then\n    #echo &quot;iwpriv ra0 set WPAPSK=$wpapsk&quot;\n    iwpriv ra0 set WPAPSK=$wpapsk\n# set wmm\niwpriv ra0 set WmmCapable=1\n# change wan mode,\nwifi\napcli\niwpriv\nhome_security\nif [ &quot;$eth_state&quot; = &quot;0&quot; -a &quot;$repeater_enable&quot; = &quot;1&quot; ]; then\n    switch_wan_interface.sh eth2.2 apcli0 0\n#!/bin/sh</code></pre>\n<p>So, from one side we found how the ESSID is created and from the other side we found that the password is read from a NVRAM variable (<code>wpapsk=&quot;nvram_get WPAPSK1&quot;</code>). Let’s do a quick search with that name:</p>\n<pre><code>➜  7z_mandanga strings homestation2_firmware00 | rg -i &#39;WPAPSK1&#39; -A5 -B5\nwifi channel:%d.\nfail to get freq.\n/dev/gpio\nError while opening /dev/gpio\nSSID1\nWPAPSK1\nSSID2\nra1 SSID=%s\nWPAPSK2\nra1 WIFI_PWD=%s\n/sbin/chpasswd.sh eufycamera %s\n--\n(...)</code></pre>\n<p>Just in the first hit there is something that looks like a compiled binary (that <code>%d</code> is suspicious). I built this crappy script to extract the elf that search for the string, then search backwards to find the magic header and assume the following file in the blob is also an ELF:</p>\n<pre><code> #!/usr/bin/env python3\n\n with open(&quot;homestation2_firmware00&quot;, &quot;rb&quot;) as file:\n     data = file.read()\n\n needle = data.find(b&quot;WPAPSK1&quot;)\n magic = data.rfind(b&quot;\\x7fELF&quot;, 0, needle)\n end = data.find(b&quot;\\x7fELF&quot;, needle)\n binary = open(&quot;elf_wifi_01&quot;, &quot;wb&quot;)\n binary.write(data[magic:end])\n binary.close()</code></pre>\n<p>And it worked flawelessly:</p>\n<pre><code>➜  7z_mandanga python3 carve_wifi.py\n➜  7z_mandanga file elf_wifi_01\nelf_wifi_01: ELF 32-bit LSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped</code></pre>\n<p>Doing a quick check on the symbols we find a lot of interesting stuff related to wifi, specially that juicy <code>set_wifi_pwd</code>.</p>\n<pre><code>➜  7z_mandanga r2 elf_wifi_01\nWARN: Relocs has not been applied. Please use `-e bin.relocs.apply=true` or `-e bin.cache=true` next time\n -- Use &#39;zoom.byte=printable&#39; in zoom mode (&#39;z&#39; in Visual mode) to find strings\n[0x0045d130]&gt; aaa\nINFO: Analyze all flags starting with sym. and entry0 (aa)\nINFO: Analyze imports (af@@@i)\nINFO: Analyze entrypoint (af@ entry0)\nINFO: Analyze symbols (af@@@s)\nINFO: Running plugin pre-analysis hooks\nINFO: Analyze all functions arguments/locals (afva@@F)\nINFO: Analyze function calls (aac)\nINFO: Analyze len bytes of instructions for references (aar)\nINFO: Finding and parsing C++ vtables (avrr)\nINFO: Analyzing methods (af @@ method.*)\nINFO: Emulate functions to find computed references (aaef)\nINFO: Recovering local variables (afva@@@F)\nINFO: Type matching analysis for all functions (aaft)\nINFO: Propagate noreturn information (aanr)\nINFO: Use -AA or aaaa to perform additional experimental analysis\nINFO: Finding xrefs in noncode sections (e anal.in=io.maps.x; aav)\n[0x0045d130]&gt; afl~wifi\n0x006f8908    1    288 sym.eufy_setDoorbellMechanicalChimeSwitch_wifi\n0x005a972c   14   1124 sym.zx_update_dev_wifi_info_by_addr\n0x0054ad3c   10    316 sym.check_hub_wifi_power\n0x00558fd8   18   1800 sym.quick_bind_wifi_mgmt_rcv_handler\n0x0052cfac   19   1272 sym.zx_update_hub_wifi_channel\n0x005e5414   93   7544 sym.netlink_wifi_connect\n0x0093f1c0    6    528 sym.aws_wifi_get_mac\n0x0053aff8    1     80 sym.zx_get_wifi_mac\n0x004cda10   13    728 sym.zx_check_wifi_version\n0x005489a0    1     16 sym.wifi_reduceTxPower\n0x005a47a0   23    892 sym.zx_alloc_wifi_addr\n0x00532224    4    324 sym.set_camera_wifi_rssi\n0x0049c7d4   11    448 sym.wifi_check_all_devs_online\n0x0049c61c   10    440 sym.wifi_get_dev_rssi\n0x0049c994   20   1768 sym.wifi_get_devs_rssi_list_in_json\n0x0059e8f8    3    204 sym.restart_wifi\n0x0053bb04    1    144 sym.set_pair_wifi_info\n0x0053b048    8    660 sym.get_wifi_info\n0x006fb684  399  18808 sym.zx_wifi_send_command\n0x00603514    6    484 sym.make_wifi_notify_json\n0x0054ab0c    9    560 sym.convert_wifi_countrycode\n0x0059df7c    1    104 sym.wifi_info_signal_init\n0x0053b510   33   1524 sym.set_wifi_pwd\n0x00558894    5    292 sym.zx_quick_bind_timer_stop_send_wifi_info\n0x006f8094    8    304 sym.eufy_set_wifi_countrycode\n0x0054ae78    7    464 sym.set_wifi_power_flag\n0x004da374   54   3076 sym.zx_handle_wifi_ready\n0x006f6348    8    828 sym.wifi_log_server_init\n0x006f8a28    1    288 sym.eufy_setDoorbellElectronicRingtoneTime_wifi\n0x0094a1f4    4    168 sym.anker_mgmt_register_get_wifi_channel_cb\n0x00532368   10    396 sym.check_camera_wifi_rssi_ok\n0x005580a0   28   2036 sym.zx_quick_bind_timer_send_wifi_info\n0x0094a29c    4    168 sym.anker_mgmt_register_get_wifi_mac_cb\n0x0053b2dc    6    564 sym.get_wifi_ra1_info\n0x006f6684    8    828 sym.wifi_log_server_t40_init\n0x0056dc34   15   1008 sym.repeater_update_all_dev_wifi_channel\n0x0049d07c   13    636 sym.wifi_devs_rssi_test_mode_exit\n0x004daf78    1    160 sym.handle_wifi_ready_thread\n0x008c5ac4    7    356 sym.zx_homekit_get_wifi_ready\n0x0049d2f8    9    288 sym.wifi_setManualChannel\n0x004cd584   11    668 sym.eufysdk_common_msg_wifi_callback</code></pre>\n<p>Jumping into that function we can see that first it builds a password based on a MD5 hash that is used locally for the user <code>eufycamera</code> and it’s not related to the wifi. The wifi-stuff starts at <code>0x0053b690</code> where it checks one parameter from <code>base_param</code> (I guess the type of device or model) against 0x0C. This part is key because if the value is different to 0x0C, then it uses the MAC Address (right block) meanwhile if it matches then it would use what returns <code>z8GetSN()</code> (left block). We are in the <code>z8GetSN()</code> case.</p>\n<figure><img src=\"https://adepts.of0x.cc/Eufy-Homestation2-firmware/radare01.png\" alt=\"Radare2\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>Different flow based on device model.</p>\n<p>The <code>z8GetSN()</code> function communicates through i2c and sends command <code>0xC1</code> (the i2c part is inside <code>z8SendData()</code>) and read 0x11 bytes from the response.</p>\n<figure><img src=\"https://adepts.of0x.cc/Eufy-Homestation2-firmware/radare02.png\" alt=\"Radare2\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>I2C communication to extract a serial number (it&#39;s not the device SN)</p>\n<p>Then bytes are translated into 32 byte string and fed to a function called <code>mac_func()</code> which also receives the Homebase Serial Number (<code>T8010...</code>):</p>\n<figure><img src=\"https://adepts.of0x.cc/Eufy-Homestation2-firmware/radare03.png\" alt=\"Radare2\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>The SN from the I2C communication and the device Serial Number (obj.g_hub_sn) used as parameters for mac_func()</p>\n<p>This <code>mac_func</code> calls a helper to perform HMAC SHA256 with the previous mentioned data, being the “message” the value returned by <code>z8GetSN</code> and the “key” the Serial Number:</p>\n<figure><img src=\"https://adepts.of0x.cc/Eufy-Homestation2-firmware/radare04.png\" alt=\"Radare2\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>HMAC SHA256</p>\n<p>The result is used by a function that would encode the information into text.</p>\n<figure><img src=\"https://adepts.of0x.cc/Eufy-Homestation2-firmware/radare05.png\" alt=\"Radare2\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>Encoding memory contents</p>\n<p>The <code>zx_Memory_Encode</code> function calls <code>urlsafe_b64_encode</code> which converts the value into base64 using the following alphabet:</p>\n<pre><code>[0x006f0490]&gt; px 65 @ 0xa5cb40\n- offset -  4041 4243 4445 4647 4849 4A4B 4C4D 4E4F  0123456789ABCDEF\n0x00a5cb40  4142 4344 4546 4748 494a 4b4c 4d4e 4f50  ABCDEFGHIJKLMNOP\n0x00a5cb50  5152 5354 5556 5758 595a 6162 6364 6566  QRSTUVWXYZabcdef\n0x00a5cb60  6768 696a 6b6c 6d6e 6f70 7172 7374 7576  ghijklmnopqrstuv\n0x00a5cb70  7778 797a 3031 3233 3435 3637 3839 2d5f  wxyz0123456789-_\n0x00a5cb80  00                                       .</code></pre>\n<p>Finally, the password is updated in NVRAM and changed executing the external command <code>iwpriv ra0 set WPAPSK=%s</code>:</p>\n<figure><img src=\"https://adepts.of0x.cc/Eufy-Homestation2-firmware/radare06.png\" alt=\"Radare2\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>Updating NVRAM value</p>\n<p>So, the only unkown data to replicate the password is the value returned by <code>z8GetSN</code>. The other compontent, the device serial number, is “predictable” because it follows a known template based on the type of device.</p>\n<h1 id=\"0x02-rubber-hose-treatment-to-the-uart-lockdown\">0x02 Rubber-hose treatment to the UART lockdown</h1>\n<p>In the previous article, we mentioned that UART access was blocked. Specifically, connecting via UART yielded a prompt asking the user to press the “Enter” key. However, doing so caused the process to terminate and the same program to relaunch, once again requesting the key press; this created an infinite loop that prevented access to an actual shell. Logs can be seen below:</p>\n<pre><code>starting pid 5617, tty &#39;/dev/ttyS1&#39;: &#39;/sbin/login.sh&#39;\n\nprocess &#39;/sbin/login.sh&#39; (pid 5617) exited. Scheduling for restart.\n\nPlease press Enter to activate this console.\nsetrlimit(RLIMIT_CORE, &amp;limit);\n\nstarting pid 6526, tty &#39;/dev/ttyS1&#39;: &#39;/sbin/login.sh&#39;\n\nprocess &#39;/sbin/login.sh&#39; (pid 6526) exited. Scheduling for restart.\n\nPlease press Enter to activate this console.\nsetrlimit(RLIMIT_CORE, &amp;limit);\n\nstarting pid 6608, tty &#39;/dev/ttyS1&#39;: &#39;/sbin/login.sh&#39;\n\nprocess &#39;/sbin/login.sh&#39; (pid 6608) exited. Scheduling for restart.</code></pre>\n<p>On the other hand, trying to interrupt U-Boot witch a escape sequence did not work.</p>\n<p>After acquiring the firmware and resolving questions about how the Wi-Fi password algorithm worked, the next objective was to understand the mechanism behind the UART access lockout and find a way to bypass it, as we intended to use the device itself to debug key services and search for vulnerabilities.</p>\n<p>In the logs the serial port is <strong>/dev/ttyS1</strong>, so let’s search references to it:</p>\n<pre><code>➜  extractions grep -r ttyS1\ngrep: homestation2_firmware00.bin.extracted/E70000/Linux_Kernel_Image.bin.extracted/0/decompressed.bin: coincidencia en fichero binario\ngrep: homestation2_firmware00.bin.extracted/E70000/Linux_Kernel_Image.bin.extracted/0/decompressed.bin_3143840_aes_forward_table.raw: coincidencia en fichero binario\ngrep: homestation2_firmware00.bin.extracted/E70000/Linux_Kernel_Image.bin.extracted/0/decompressed.bin_3679760_unknown.raw: coincidencia en fichero binario\ngrep: homestation2_firmware00.bin.extracted/E70000/Linux_Kernel_Image.bin.extracted/0/decompressed.bin_3135648_aes_reverse_table.raw: coincidencia en fichero binario\ngrep: homestation2_firmware00.bin.extracted/E70000/Linux_Kernel_Image.bin.extracted/0/decompressed.bin.extracted/3F16A4/decompressed.bin: coincidencia en fichero binario\nhomestation2_firmware00.bin.extracted/E70000/Linux_Kernel_Image.bin.extracted/0/decompressed.bin.extracted/3F16A4/decompressed.bin.extracted/0/etc_ro/inittab:ttyS1::askfirst:/sbin/login.sh\ngrep: homestation2_firmware00.bin.extracted/E70000/Linux_Kernel_Image.bin.extracted/0/decompressed.bin.extracted/3F16A4/decompressed.bin.extracted/0/bin/recover_0_elf.raw: coincidencia en fichero binario\ngrep: homestation2_firmware00.bin.extracted/E70000/Linux_Kernel_Image.bin.extracted/0/decompressed.bin.extracted/3F16A4/decompressed.bin.extracted/0/bin/recover: coincidencia en fichero binario\nhomestation2_firmware00.bin.extracted/E70000/Linux_Kernel_Image.bin.extracted/0/decompressed.bin.extracted/3F16A4/decompressed.bin.extracted/0/sbin/makedevlinks.sh:mknod /dev/ttyS1 c 4 65\ngrep: homestation2_firmware00.bin.extracted/E70000/Linux_Kernel_Image.bin.extracted/0/decompressed.bin.extracted/3F16A4/decompressed.bin_0_cpio.raw: coincidencia en fichero binario\ngrep: homestation2_firmware00.bin.extracted/E70000/Linux_Kernel_Image.bin.extracted/0/decompressed.bin_3147936_aes_rcon.raw: coincidencia en fichero binario\ngrep: 7z_mandanga/homestation2_firmware00: coincidencia en fichero binario</code></pre>\n<p>The key is in the <strong>inittab</strong> file, where we can see the it triggers the execution of <strong>/sbin/login.sh</strong> script once the user press “Enter” (the BusyBox <strong>askfirst</strong> action shows the prompt with sentence “Please press Enter to activate this console”). The script is simple as:</p>\n<pre><code>#!/bin/sh\n\nDEBUG_LOG=&quot;`nvram_get debug_log`&quot;\nif [ &quot;$DEBUG_LOG&quot; = &quot;1&quot; -o -f &quot;/mnt/enable_console&quot; ];then\n    echo &quot;enter debug mode&quot; &gt; /dev/console\n    /bin/login\nfi</code></pre>\n<p>The script checks if debug mode is enabled by checking the NVRAM variable <code>debug_log</code> and based on that allows the access to the shell (well, to login in the shell) or exits, entering in the loop.</p>\n<p>We did not find a straight way to enable that variable and bypass the lockdown. There are various ways on changing the boot mode to enable “debug”, but all of them are far away from our hands because:</p>\n<ol><li>One of them is based in the presence of a file in the filesystem. Because to be able to drop a file we would need access, it is a chicken-egg situation.</li><li>The other method involves a convoluted magic protocol based on ARP and commands signed by a private key which is not present in the firmware. Yes, they use encapsulate their own protocol on ARP and exchange magic signed packets to enable debug remotely. I am not sure what crack they smoked.</li></ol>\n<p>It occurs to me at this point that if we can not bypass the check for whether it is booting in production or debug mode, why not completely obliterate the check and jump straight to a shell?</p>\n<p>The firmware did not contain anything encrypted, so should be straight forward to modify it to allow us direct access through the serial port. The problem was… Do you remember when I mentioned that it was “barely damaged”?. Well, we fucked 64kbs at <code>0x500000</code> which is the load address for the image.</p>\n<p>At first, I thought our clumsy hands had ruined the project and that the idea of ​​using the actual device for dynamic analysis with a debugger was slipping away (we would still have the option of trying to emulate some binaries, but having access to the original hardware is always infinitely easier). Fortunately, after analyzing the firmware layout a bit more, we discovered a much smaller “recovery image” that was still intact at <code>0xe70000</code>. So, because the “normal” image was fucked, the bootloader would have to load this recovery image that we can manipulate to remove the UART lockdown.</p>\n<p>The “recovery image” is a <a href=\"https://hackmd.io/@TomasZheng/SkLpND6CL\" rel=\"nofollow ugc noopener\">U-Boot image with a 64-byte header</a>:</p>\n<div class=\"table-wrap\"><table><thead><tr><th>Header offset</th><th>Length</th><th>Field</th><th>Original recovery value</th></tr></thead><tbody><tr><td><code>0x00</code></td><td>4</td><td>Magic</td><td><code>0x27051956</code></td></tr><tr><td><code>0x04</code></td><td>4</td><td>Header CRC32</td><td><code>0xe764e161</code></td></tr><tr><td><code>0x08</code></td><td>4</td><td>Timestamp</td><td>Preserved by the builder</td></tr><tr><td><code>0x0c</code></td><td>4</td><td>Compressed payload length</td><td><code>0x3455d9</code> = 3,429,849</td></tr><tr><td><code>0x10</code></td><td>4</td><td>RAM load address</td><td><code>0x80000000</code></td></tr><tr><td><code>0x14</code></td><td>4</td><td>Entry address</td><td><code>0x802e04f0</code></td></tr><tr><td><code>0x18</code></td><td>4</td><td>Payload CRC32</td><td><code>0x19b650c3</code></td></tr><tr><td><code>0x1c</code></td><td>4</td><td>OS, architecture, type, compression</td><td><code>05 05 02 03</code></td></tr><tr><td><code>0x20</code></td><td>32</td><td>Image-name area</td><td>Preserved, including vendor-used bytes</td></tr></tbody></table></div>\n<p>The actual filesystem is inside a matrioska of different compression layers that can be visualized in the following diagram:</p>\n<figure><img src=\"https://adepts.of0x.cc/Eufy-Homestation2-firmware/fs.png\" alt=\"Matrioska of compression\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>Matrioska!</p>\n<p>Once the filesystem was unpacked, I made the minimal changes to allow us to jump directly to the shell with root privileges. To avoid headaches, I padded the modifications to match the same size it had the original code (code is from python script I used to perform the modifications and re-pack/fix CRCs):</p>\n<pre><code>old_condition = b&#39;if [ &quot;$DEBUG_LOG&quot; = &quot;1&quot; ];then&#39;\nnew_condition = b&#39;if [ 1 = 1 ];then&#39; + b&#39; &#39; * 13\n\nold_login = b&#39;\\t/bin/login\\n&#39;\nnew_login = b&#39;\\t/bin/sh   \\n&#39;</code></pre>\n<p>After doing some local test to verify that the modified recovery image was correctly modified and it would unpack/load correctly we proceeded to overwrite it in the flash chip. In order to disable the <a href=\"https://www.alldatasheet.com/datasheet-pdf/pdf/1283922/WINBOND/25Q256JWEQ.html\" rel=\"nofollow ugc noopener\">Write Protection that appears in the documentation</a> this connection diagram was followed:</p>\n<figure><img src=\"https://adepts.of0x.cc/Eufy-Homestation2-firmware/diagram.jpg\" alt=\"Diagram to disable Write Protection \" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>Diagram to disable Write Protection</p>\n<p>Writing the new recovery image:</p>\n<figure><img src=\"https://adepts.of0x.cc/Eufy-Homestation2-firmware/writting.png\" alt=\"Writting the new recovery image\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>Overwriting the recovery image with our backdoored version</p>\n<p>Then we performed three reads to verify the process was done correctly. After that, the chip was solded again into the device board:</p>\n<figure><img src=\"https://adepts.of0x.cc/Eufy-Homestation2-firmware/resolder.jpg\" alt=\"Soldering again the chip\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>Soldering again the chip</p>\n<p>Once the chip was soldered I connected again my laptop to the device using a USB-TTL. First it tried to load the normal image, but it failed and then it jumped to backdoored recovery image and after loading it… we got a shell!!</p>\n<figure><img src=\"https://adepts.of0x.cc/Eufy-Homestation2-firmware/uart-shell.png\" alt=\"UART shell\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>Finally access to an admin shell!!</p>\n<h1 id=\"0x03-filesystem-gardening-for-debugging-purposes\">0x03 Filesystem gardening for debugging purposes</h1>\n<p>We finally have a shell; the only problem is that the recovery image is very limited and the file system is not mounted, so the binaries and libraries needed to properly debug the system are missing. Furthermore, the kernel lacks certain modules-such as those responsible for handling WiFi interfaces or the I2C interface used to generate the WiFi key.</p>\n<p>The set of commands available in the default image is quite limited, but at least there are tools like <code>wget</code>, which allows us to transfer files from the laptop to the device. I prepared a crappy setup using an old TP-Link router that I got from Telefónica in 2015.</p>\n<figure><img src=\"https://adepts.of0x.cc/Eufy-Homestation2-firmware/network.jpg\" alt=\"Network Setup\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>Not glamorous setup</p>\n<p>I compiled a new BusyBox with loads of extra commands that were not present in the original one. Using this new BusyBox I started to try to create a “staged” filesystem and bind all the paths to this “staged” filesystem. I had to execute services needed to run before the big ELF in charge of main services (<code>homeapp_security</code>, the same binary that contained the WiFi logic), also because of memory limitations I had to modify some kernel parameters like <code>overcommit_ratio</code>. It was a three hours process of hit and miss until I could run the main binary and be able to interact with it.</p>\n<p>Because this was a process that I would need to repeat each time I wanted to use the device (because the filesystem in the recovery image is not persistent, so after each boot I have to restore all) I shared the logs of <code>picocom</code> to Codex and asked it to prepare a all-in-one script that would repeat the same steps I did (and discard the stuff that didn’t work). To be honest, the following morning after that night of playing with the Homebase I barely remembered what I did, so having the logs was marvelous to rebuild and automatize the process. Keep logs of everything always, they will save your ass at some point.</p>\n<p>The diagram of actions until the <code>homeapp_security</code> runs is the following:</p>\n<figure><img src=\"https://adepts.of0x.cc/Eufy-Homestation2-firmware/recovery-image.png\" alt=\"Diagram of actions\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>Flow diagram from the patched firmware to getting the binary running</p>\n<p>The two most important scripts to rebuild the filesystem are shown below:</p>\n<pre><code>#!/bin/sh\n# Recovery UART bootstrap. Run with sh; do not source into the login shell.\n# Usage: sh homeapp-restore-v5.sh [http://PC:8000] [--start]\nPATH=/bin:/sbin:/usr/bin:/usr/sbin\nexport PATH\nunset LD_PRELOAD LD_LIBRARY_PATH\nBASE=/media/homeapp-debug\nROOT=$BASE/rootfs\nSERVER=http://192.168.90.112:8000\nSTART=no\nfail() { echo &quot;homeapp-restore: $*&quot; &gt;&amp;2; exit 1; }\nfor argument in &quot;$@&quot;; do\n    case &quot;$argument&quot; in\n        http://*) SERVER=${argument%/} ;;\n        --start) START=yes ;;\n        *) fail &#39;usage: sh homeapp-restore-v5.sh [http://PC:8000] [--start]&#39; ;;\n    esac\ndone\ncase &quot;$SERVER&quot; in *&#39; &#39;*|*&#39;?&#39;*|*&#39;#&#39;*) fail &#39;use a plain HTTP base URL without spaces/query/fragment&#39; ;; esac\n[ &quot;`awk &#39;/^Uid:/ {print $3}&#39; /proc/self/status`&quot; = 0 ] || fail &#39;root effective UID required&#39;\n[ -r /proc/self/mountinfo ] || fail &#39;/proc/self/mountinfo is required&#39;\n[ -c /dev/gpio ] &amp;&amp; [ -c /dev/apsoc_nvram ] || fail &#39;expected recovery device nodes missing&#39;\n\nmounted() { awk -v p=&quot;$1&quot; &#39;$2==p {n++} END {exit n==0}&#39; /proc/mounts; }\ncheck_tmpfs() {\n    awk -v p=&quot;$1&quot; -v size=&quot;$2&quot; &#39;$2==p {\n        n++; if ($3==&quot;tmpfs&quot;) {\n            count=split($4,a,&quot;,&quot;); for(i=1;i&lt;=count;i++) if(a[i]==&quot;size=&quot; size &quot;k&quot;) ok=1\n        }\n    } END {exit !(n==1 &amp;&amp; ok)}&#39; /proc/mounts\n}\nempty_dir() {\n    [ ! -e &quot;$1&quot; ] &amp;&amp; return 0\n    [ -d &quot;$1&quot; ] &amp;&amp; [ ! -L &quot;$1&quot; ] || return 1\n    [ -z &quot;`ls -A &quot;$1&quot;`&quot; ]\n}\nasset_md5() {\n    # BEGIN GENERATED ASSET HASHES\n    case &quot;$1&quot; in\n        busybox-extra) echo b587f623ef28b84e277fbd1b2b0b2348 ;;\n        homeapp-enter) echo 8fde5ed1e3e65d07a7b9ebe672b25a5c ;;\n        homeapp-mount-runtime.sh) echo 19e73a1584137c0d2150d6968cccb4c5 ;;\n        homeapp-start-bounded-v4.sh) echo 19b70b8ac6751f8e161e490460ff8f3e ;;\n        homeapp-control-v5.sh) echo 2fc7613db0f02a0d16c1417bb0282264 ;;\n        homeapp_rootfs.tar.gz) echo bf6acaecf3301532bda2c1f6c41a3b2c ;;\n        *) return 1 ;;\n    esac\n    # END GENERATED ASSET HASHES\n}\nverify() {\n    expected=`asset_md5 &quot;$1&quot;` || fail &quot;unknown asset: $1&quot;\n    actual=`md5sum &quot;$2&quot; | awk &#39;{print $1}&#39;`\n    [ &quot;$actual&quot; = &quot;$expected&quot; ] || fail &quot;checksum mismatch: $2; stop and check the PC bundle&quot;\n}\nfetch() {\n    asset=$1\n    echo &quot;homeapp-restore: downloading $asset&quot;\n    wget -O &quot;$BASE/$asset.part&quot; &quot;$SERVER/$asset&quot; || fail &quot;download failed: $asset; partial setup preserved&quot;\n    verify &quot;$asset&quot; &quot;$BASE/$asset.part&quot;\n    mv &quot;$BASE/$asset.part&quot; &quot;$BASE/$asset&quot; || fail &quot;cannot finish $asset&quot;\n}\nstamp() { awk &#39;{sub(/^.*\\) /, &quot;&quot;); print $20}&#39; &quot;/proc/$1/stat&quot; 2&gt;/dev/null; }\nis_supervisor() {\n    [ -r &quot;/proc/$1/cmdline&quot; ] || return 1\n    tr &#39;\\000&#39; &#39;\\n&#39; &lt; &quot;/proc/$1/cmdline&quot; | awk &#39;\n        NR==1 {shell=($0==&quot;sh&quot; || $0==&quot;/bin/sh&quot; || $0==&quot;/sbin/run_mini_system.sh&quot;)}\n        $0==&quot;/sbin/run_mini_system.sh&quot; {script=1}\n        END {exit !(shell &amp;&amp; script)}&#39;\n}\nrecover_pids() {\n    for procdir in /proc/[0-9]*; do\n        [ -e &quot;$procdir/exe&quot; ] || continue\n        if [ &quot;$procdir/exe&quot; -ef /bin/recover ] || [ &quot;$procdir/exe&quot; -ef /sbin/recover ]; then\n            echo &quot;${procdir#/proc/}&quot;\n        fi\n    done\n}\nsignal_recover() {\n    for recover_pid in `recover_pids`; do\n        if [ &quot;/proc/$recover_pid/exe&quot; -ef /bin/recover ] || [ &quot;/proc/$recover_pid/exe&quot; -ef /sbin/recover ]; then\n            kill &quot;-$1&quot; &quot;$recover_pid&quot; 2&gt;/dev/null || :\n        fi\n    done\n}\ncoordinate_recovery() {\n    # Exact argv matching avoids accidentally signalling grep or the UART shell.\n    for procdir in /proc/[0-9]*; do\n        supervisor_pid=${procdir#/proc/}\n        if is_supervisor &quot;$supervisor_pid&quot;; then\n            supervisor_stamp=`stamp &quot;$supervisor_pid&quot;`\n            [ -n &quot;$supervisor_stamp&quot; ] || continue\n            kill -STOP &quot;$supervisor_pid&quot; || fail &quot;cannot pause recovery supervisor $supervisor_pid&quot;\n            echo &quot;$supervisor_pid $supervisor_stamp&quot; &gt; &quot;$BASE/recovery-supervisor-$supervisor_pid&quot;\n            echo &quot;homeapp-restore: paused recovery supervisor $supervisor_pid&quot;\n        fi\n    done\n    signal_recover TERM\n    sleep 2\n    signal_recover KILL\n    sleep 1\n    [ -z &quot;`recover_pids`&quot; ] || fail &#39;recovery tasks survived stop&#39;\n    pidof recover &gt;/dev/null 2&gt;&amp;1 &amp;&amp; fail &#39;unrecognized recover process remains; inspect ps&#39;\n    return 0\n}\ninstall_commands() {\n    for asset in homeapp-mount-runtime.sh homeapp-start-bounded-v4.sh homeapp-control-v5.sh; do\n        verify &quot;$asset&quot; &quot;$BASE/$asset&quot;\n    done\n    # Rename new files into place: never truncate a script a running shell\n    # may still be reading during a repeated preparation command.\n    for asset in homeapp-mount-runtime.sh homeapp-start-bounded-v4.sh homeapp-control-v5.sh; do\n        destination=/tmp/$asset\n        [ &quot;$asset&quot; != homeapp-control-v5.sh ] || destination=/tmp/homeapp\n        cp &quot;$BASE/$asset&quot; &quot;$destination.part.$$&quot; || fail &quot;cannot copy $asset&quot;\n        chmod 755 &quot;$destination.part.$$&quot; || fail &quot;cannot mark $asset executable&quot;\n        mv &quot;$destination.part.$$&quot; &quot;$destination&quot; || fail &quot;cannot install $asset&quot;\n    done\n}\n\necho &#39;homeapp-restore revision: v5; original firmware applications, RAM staging&#39;\nif [ -f &quot;$BASE/restore-v5.ready&quot; ]; then\n    # Reusing this recipe&#39;s completed setup is safe; never extract over binds.\n    check_tmpfs &quot;$BASE&quot; 65536 || fail &#39;staging mount changed or is stacked&#39;\n    check_tmpfs /media/mmcblk0p1 8192 || fail &#39;database mount changed or is stacked&#39;\n    verify homeapp-enter &quot;$BASE/homeapp-enter&quot;\n    verify busybox-extra &quot;$BASE/busybox-extra&quot;\n    install_commands\n    sh /tmp/homeapp-mount-runtime.sh --resume || fail &#39;runtime mount verification failed&#39;\n    coordinate_recovery\n    echo &#39;homeapp-restore: verified and reused completed setup; no archive download/extraction&#39;\nelse\n    # Fail closed on partial/manual setups; no rm -rf, unmount or overlay of data.\n    mounted &quot;$BASE&quot; &amp;&amp; fail &#39;unrecognized/partial staging mount; inspect it or reboot before retrying&#39;\n    empty_dir &quot;$BASE&quot; || fail &#39;staging directory is not empty; preserve it and inspect or reboot&#39;\n    mounted /media/mmcblk0p1 &amp;&amp; fail &#39;database/storage path already mounted; will not cover it&#39;\n    empty_dir /media/mmcblk0p1 || fail &#39;database/storage directory is not empty; will not cover it&#39;\n    free_kb=`awk &#39;/^MemFree:/ {print $2}&#39; /proc/meminfo`\n    [ &quot;${free_kb:-0}&quot; -ge 81920 ] || fail &#39;less than 80 MiB free; reboot to a fresh recovery shell or inspect memory&#39;\n    # Refuse existing normal services before staging/coordination.\n    for service in home_security mdnsd mips_collector pushMuxer systemServer embdb_eufy; do\n        pidof &quot;$service&quot; &gt;/dev/null 2&gt;&amp;1 &amp;&amp; fail &quot;$service already running; use a fresh recovery boot&quot;\n    done\n    original_mode=`cat /proc/sys/vm/overcommit_memory`\n    original_ratio=`cat /proc/sys/vm/overcommit_ratio`\n    if [ &quot;$original_mode&quot; = 2 ] &amp;&amp; [ &quot;$original_ratio&quot; -lt 75 ]; then\n        echo 75 &gt; /proc/sys/vm/overcommit_ratio || fail &#39;cannot raise runtime commit ratio&#39;\n        echo &quot;homeapp-restore: strict overcommit ratio $original_ratio -&gt; 75 (this boot only)&quot;\n    fi\n    if [ &quot;$original_mode&quot; = 2 ]; then\n        headroom=`awk &#39;/^CommitLimit:/ {limit=$2} /^Committed_AS:/ {used=$2} END {print limit-used}&#39; /proc/meminfo`\n        [ &quot;$headroom&quot; -ge 65536 ] || fail &#39;less than 64 MiB commitment headroom; no extraction attempted&#39;\n    fi\n    mkdir -p &quot;$BASE&quot; || fail &#39;cannot create staging directory&#39;\n    mount -t tmpfs -o size=64m tmpfs &quot;$BASE&quot; || fail &#39;cannot mount staging tmpfs&#39;\n    check_tmpfs &quot;$BASE&quot; 65536 || fail &#39;unexpected staging filesystem size&#39;\n    echo &quot;$original_mode $original_ratio&quot; &gt; &quot;$BASE/original-overcommit&quot;\n    echo &quot;$SERVER&quot; &gt; &quot;$BASE/download-server&quot;\n    for asset in busybox-extra homeapp-enter homeapp-mount-runtime.sh homeapp-start-bounded-v4.sh homeapp-control-v5.sh; do\n        fetch &quot;$asset&quot;\n    done\n    chmod 755 &quot;$BASE/busybox-extra&quot; &quot;$BASE/homeapp-enter&quot; || fail &#39;chmod failed&#39;\n    &quot;$BASE/homeapp-enter&quot; --check || fail &#39;native helper does not run&#39;\n    fetch homeapp_rootfs.tar.gz\n    (cd &quot;$BASE&quot; &amp;&amp; ./busybox-extra tar -xzf homeapp_rootfs.tar.gz) || fail &#39;extraction failed; preserve diagnostics, then reboot before retrying&#39;\n    # Only the verified temporary archive is removed, before any runtime binds.\n    rm &quot;$BASE/homeapp_rootfs.tar.gz&quot; || fail &#39;cannot remove staged archive&#39;\n    &quot;$BASE/homeapp-enter&quot; &quot;$ROOT&quot; /bin/sh -c &#39;:&#39; || fail &#39;normal loader/BusyBox does not execute&#39;\n    mkdir -p /media/mmcblk0p1 || fail &#39;cannot create database path&#39;\n    mount -t tmpfs -o size=8m tmpfs /media/mmcblk0p1 || fail &#39;cannot mount temporary database&#39;\n    check_tmpfs /media/mmcblk0p1 8192 || fail &#39;unexpected database filesystem size&#39;\n    mkdir -p /media/mmcblk0p1/.sqlite || fail &#39;cannot create database directory&#39;\n    install_commands\n    sh /tmp/homeapp-mount-runtime.sh --apply || fail &#39;runtime mount setup failed; earlier mounts preserved&#39;\n    coordinate_recovery\n    echo v5 &gt; &quot;$BASE/restore-v5.ready&quot; || fail &#39;cannot record successful setup&#39;\nfi\necho &#39;homeapp-restore: READY. /tmp/homeapp start|status|logs|stop|shell&#39;\necho &#39;Wi-Fi/I2C drivers and persistent flash configuration are not restored by userspace staging.&#39;\ndf -k &quot;$BASE&quot; /media/mmcblk0p1\nif [ &quot;$START&quot; = yes ]; then\n    /tmp/homeapp start || fail &#39;setup completed, but application startup failed; inspect /tmp/homeapp status&#39;\nfi\n\n#!/bin/sh\n# Run with `sh`, never source into the interactive UART shell.\n# Does not start services, mount flash, or change memory policy.\nPATH=/bin:/sbin:/usr/bin:/usr/sbin\nexport PATH\nBASE=/media/homeapp-debug\nROOT=/media/homeapp-debug/rootfs\nfail() { echo &quot;homeapp-mount: $*&quot; &gt;&amp;2; exit 1; }\nmounted() { awk -v p=&quot;$1&quot; &#39;$2 == p {found=1} END {exit !found}&#39; /proc/mounts; }\nsame_mount_root() {\n    # Compare device ID and filesystem-root path, not just the filesystem type.\n    awk -v s=&quot;$1&quot; -v d=&quot;$2&quot; &#39;\n        $5 == s {source=$3 &quot; &quot; $4}\n        $5 == d {target=$3 &quot; &quot; $4; count++}\n        END {exit !(count == 1 &amp;&amp; source != &quot;&quot; &amp;&amp; source == target)}\n    &#39; /proc/self/mountinfo\n}\n\ncase &quot;${1-}&quot; in\n    --inspect)\n        echo &#39;--- Mounts ---&#39;\n        cat /proc/mounts\n        echo &#39;--- Memory ---&#39;\n        cat /proc/meminfo\n        echo &#39;--- Overcommit mode / ratio ---&#39;\n        cat /proc/sys/vm/overcommit_memory /proc/sys/vm/overcommit_ratio\n        echo &#39;--- Staged executables ---&#39;\n        ls -l &quot;$BASE/homeapp-enter&quot; &quot;$ROOT/bin/busybox&quot; &quot;$ROOT/bin/home_security&quot;\n        echo &#39;--- Processes ---&#39;\n        ps\n        exit 0\n        ;;\n    --apply|--resume) mode=$1 ;;\n    *) fail &#39;usage: sh homeapp-mount-runtime.sh --inspect|--apply|--resume&#39; ;;\nesac\n\nmounted &quot;$BASE&quot; || fail &#39;staging filesystem is not mounted&#39;\n[ -x &quot;$BASE/homeapp-enter&quot; ] || fail &#39;native helper missing&#39;\n[ -x &quot;$ROOT/bin/home_security&quot; ] || fail &#39;application missing&#39;\nmounted /media/mmcblk0p1 || fail &#39;prepare temporary database mount first&#39;\n[ -d /media/mmcblk0p1/.sqlite ] || fail &#39;database directory missing&#39;\n\n# Resume only matching binds, with one mount at each staged destination.\n# Leave staged /mnt (including its original zx_log.conf) in place. Recovery&#39;s\n# /mnt bind failed in the first attempt; persistent state is a separate step.\nfor path in dev proc sys etc var tmp media dev/pts media/mmcblk0p1; do\n    if mounted &quot;$ROOT/$path&quot;; then\n        [ &quot;$mode&quot; = --resume ] || fail &quot;$ROOT/$path already mounted; inspect partial setup first&quot;\n        same_mount_root &quot;/$path&quot; &quot;$ROOT/$path&quot; || fail &quot;existing bind does not match /$path, or is stacked: $ROOT/$path&quot;\n    fi\ndone\n\nbind_path() {\n    source_path=$1\n    target_path=$2\n    if mounted &quot;$target_path&quot;; then\n        echo &quot;homeapp-mount: keeping verified bind $target_path&quot;\n        return 0\n    fi\n    mkdir -p &quot;$target_path&quot; || fail &quot;cannot create $target_path&quot;\n    echo &quot;homeapp-mount: $source_path -&gt; $target_path&quot;\n    mount -o bind &quot;$source_path&quot; &quot;$target_path&quot;\n    rc=$?\n    [ &quot;$rc&quot; -eq 0 ] || fail &quot;mount exited $rc: $source_path -&gt; $target_path (earlier mounts preserved)&quot;\n    mounted &quot;$target_path&quot; || fail &quot;mount returned success but $target_path is absent from /proc/mounts&quot;\n    same_mount_root &quot;$source_path&quot; &quot;$target_path&quot; || fail &quot;unexpected mount root at $target_path&quot;\n}\nfor path in dev proc sys etc var tmp media; do\n    bind_path &quot;/$path&quot; &quot;$ROOT/$path&quot;\ndone\nbind_path /dev/pts &quot;$ROOT/dev/pts&quot;\nbind_path /media/mmcblk0p1 &quot;$ROOT/media/mmcblk0p1&quot;\necho &#39;Runtime binds completed; no application services started.&#39;</code></pre>\n<p>Then running it from the UART shell we can see it prepares everything and also launch the binary:</p>\n<pre><code># wget -O /tmp/homeapp-restore-v5.sh http://192.168.90.112:8000/homeapp-restore-\nv5.sh\nConnecting to 192.168.90.112:8000 (192.168.90.112:8000)\nhomeapp-restore-v5.s 100% |*******************************|  9174  --:--:-- ETA\n# sh /tmp/homeapp-restore-v5.sh http://192.168.90.112:8000 --start\nhomeapp-restore revision: v5; original firmware applications, RAM staging\nhomeapp-restore: strict overcommit ratio 50 -&gt; 75 (this boot only)\nhomeapp-restore: downloading busybox-extra\nConnecting to 192.168.90.112:8000 (192.168.90.112:8000)\nbusybox-extra.part   100% |*******************************|   673k --:--:-- ETA\nhomeapp-restore: downloading homeapp-enter\nConnecting to 192.168.90.112:8000 (192.168.90.112:8000)\nhomeapp-enter.part   100% |*******************************|   622k --:--:-- ETA\nhomeapp-restore: downloading homeapp-mount-runtime.sh\nConnecting to 192.168.90.112:8000 (192.168.90.112:8000)\nhomeapp-mount-runtim 100% |*******************************|  3054  --:--:-- ETA\nhomeapp-restore: downloading homeapp-start-bounded-v4.sh\nConnecting to 192.168.90.112:8000 (192.168.90.112:8000)\nhomeapp-start-bounde 100% |*******************************|  4383  --:--:-- ETA\nhomeapp-restore: downloading homeapp-control-v5.sh\nConnecting to 192.168.90.112:8000 (192.168.90.112:8000)\nhomeapp-control-v5.s 100% |*******************************|  6488  --:--:-- ETA\nhomeapp-enter: running; uid=0 euid=0; static MIPS helper\nhomeapp-restore: downloading homeapp_rootfs.tar.gz\nConnecting to 192.168.90.112:8000 (192.168.90.112:8000)\nhomeapp_rootfs.tar.g 100% |*******************************| 14579k 00:00:00 ETA\nhomeapp-mount: /dev -&gt; /media/homeapp-debug/rootfs/dev\nhomeapp-mount: /proc -&gt; /media/homeapp-debug/rootfs/proc\nhomeapp-mount: /sys -&gt; /media/homeapp-debug/rootfs/sys\nhomeapp-mount: /etc -&gt; /media/homeapp-debug/rootfs/etc\nhomeapp-mount: /var -&gt; /media/homeapp-debug/rootfs/var\nhomeapp-mount: /tmp -&gt; /media/homeapp-debug/rootfs/tmp\nhomeapp-mount: /media -&gt; /media/homeapp-debug/rootfs/media\nhomeapp-mount: /dev/pts -&gt; /media/homeapp-debug/rootfs/dev/pts\nhomeapp-mount: /media/mmcblk0p1 -&gt; /media/homeapp-debug/rootfs/media/mmcblk0p1\nRuntime binds completed; no application services started.\nhomeapp-restore: READY. /tmp/homeapp start|status|logs|stop|shell\nWi-Fi/I2C drivers and persistent flash configuration are not restored by userspace staging.\nFilesystem           1k-blocks      Used Available Use% Mounted on\ntmpfs                    65536     44844     20692  68% /media/homeapp-debug\ntmpfs                     8192         0      8192   0% /media/mmcblk0p1\nhomeapp-mount: keeping verified bind /media/homeapp-debug/rootfs/dev\nhomeapp-mount: keeping verified bind /media/homeapp-debug/rootfs/proc\nhomeapp-mount: keeping verified bind /media/homeapp-debug/rootfs/sys\nhomeapp-mount: keeping verified bind /media/homeapp-debug/rootfs/etc\nhomeapp-mount: keeping verified bind /media/homeapp-debug/rootfs/var\nhomeapp-mount: keeping verified bind /media/homeapp-debug/rootfs/tmp\nhomeapp-mount: keeping verified bind /media/homeapp-debug/rootfs/media\nhomeapp-mount: keeping verified bind /media/homeapp-debug/rootfs/dev/pts\nhomeapp-mount: keeping verified bind /media/homeapp-debug/rootfs/media/mmcblk0p1\nRuntime binds completed; no application services started.\nhomeapp: launcher PID 5880; startup output: /media/mmcblk0p1/homeapp-launch-5565.log\nhomeapp: startup in progress; /tmp/homeapp status and /tmp/homeapp logs\nA running launcher is not proof of application or enrollment readiness.</code></pre>\n<p>Finally we can see that the services are exposed!</p>\n<pre><code># /media/homeapp-debug/busybox-extra netstat -lntup\nActive Internet connections (only servers)\nProto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name\ntcp        0      0 0.0.0.0:10400           0.0.0.0:*               LISTEN      6016/home_security\ntcp        0      0 0.0.0.0:10402           0.0.0.0:*               LISTEN      6016/home_security\ntcp        0      0 0.0.0.0:32290           0.0.0.0:*               LISTEN      6016/home_security\ntcp        0      0 0.0.0.0:10404           0.0.0.0:*               LISTEN      6016/home_security\ntcp        0      0 0.0.0.0:32292           0.0.0.0:*               LISTEN      6016/home_security\ntcp        0      0 0.0.0.0:32293           0.0.0.0:*               LISTEN      6016/home_security\ntcp        0      0 0.0.0.0:32295           0.0.0.0:*               LISTEN      6016/home_security\ntcp        0      0 0.0.0.0:10600           0.0.0.0:*               LISTEN      6016/home_security\ntcp        0      0 0.0.0.0:32392           0.0.0.0:*               LISTEN      6016/home_security\ntcp        0      0 0.0.0.0:9000            0.0.0.0:*               LISTEN      5974/pushMuxer\ntcp        0      0 127.0.0.1:5000          0.0.0.0:*               LISTEN      5963/mips_collector\ntcp        0      0 0.0.0.0:554             0.0.0.0:*               LISTEN      5974/pushMuxer\ntcp        0      0 0.0.0.0:53              0.0.0.0:*               LISTEN      1405/dnsmasq\ntcp        0      0 0.0.0.0:23              0.0.0.0:*               LISTEN      1693/telnetd\nnetstat: /proc/net/tcp6: No such file or directory\nudp        0      0 0.0.0.0:53              0.0.0.0:*                           1405/dnsmasq\nudp        0      0 0.0.0.0:32380           0.0.0.0:*                           6016/home_security\nudp        0      0 0.0.0.0:46217           0.0.0.0:*                           6456/ntpclient\nudp        0      0 0.0.0.0:43466           0.0.0.0:*                           1405/dnsmasq\nudp        0      0 0.0.0.0:21200           0.0.0.0:*                           6016/home_security\nudp        0      0 0.0.0.0:29947           0.0.0.0:*                           6016/home_security\nnetstat: /proc/net/udp6: No such file or directory</code></pre>\n<p>The last thing was compiling a compatible version of GDB and upload it to the device to start playing <strong>:)</strong></p>\n<figure><img src=\"https://adepts.of0x.cc/Eufy-Homestation2-firmware/gdb.png\" alt=\"GDB running on the Homebase2\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>GDB running on the Homebase2</p>\n<p>Of course far more configurations changes are needed to make the binary work properly (e.g. I had to bridge an <code>eth</code> interface to create a <code>br0</code> within the range it needs for the peers -long story short: it uses a peer table with MAC addresses to reject or accept communications), but with a debugger everything is far more easy. Just fire the process, check what fails, correct it or just hook it to feed with the values it’s expecting to continue the flow.</p>\n<h1 id=\"0x04-final-words\">0x04 Final words</h1>\n<p>With the arrival of LLMs, I’ve lost all enthusiasm for publishing technical content. I’ve been publishing articles online since 2007 sharing things I learned or discovered using dozens of different aliases across various blogs and forums. That is nearly 20 years. And today, I’ve lost all desire to share my experiences.</p>\n<p>For one thing, there is the loss of value in writing if no one is going to read it. Nowadays, only a minority of people actually stop to read an article; most ask an AI for a summary or simply skim through without paying attention. There used to be an ecosystem where people spent time checking their RSS feeds (damn, I’m old) and reading articles from their favorite blogs or authors. The first major blow in this regard came from social media, where immediacy and “scoring Twitter points” became the primary driving force behind content creation.</p>\n<p>Then there is the fact that the vast majority of published articles are generated directly by AI. Even interesting research pieces or vulnerability write-ups are churned out by automated looms, using that lobotomized language that gives me a stroke every time I try to read them. People seem delighted to gobble up such insubstantial crap. I suppose because it follows that first point. I write in “broken English” (yep, even though I’ve been working for a UK company for five years, my English is still terrible), yet I would a million times rather read an article written by a person in “broken English” than the umpteenth repetition of the same structures and stock phrases ChatGPT peppers its texts with.</p>\n<p>The final nail in the coffin is the fact that… does it even make sense in 2026 to keep publishing technical content for human readers? In the past, you’d read a lot of material (or even bookmark it for future reference) because it was genuinely useful. Perhaps you were tinkering with a product’s firmware that someone else had already explored; you could find that article and follow the trail they blazed. The same applied to vulnerability analysis: you’d read about others’ work so you could replicate it when you encountered a similar scenario. And I’m not just talking about specific products; I’m talking about methodologies: how someone solved a problem, allowing you to face the same situation years later and apply what you had read back then. With AI, this no longer makes sense; the world is turning into a collection of meat proxies prompting the latest LLM to resolve whatever snag or situation arises.</p>\n<p>I obviously use AI in my professional work, but not to tackle my “hobby” projects (like the things I post on this blog). I need to clarify that last point: I use it, for instance, when I’m reverse-engineering something and come across a function that performs mathematical operations (like when I used it in the previous article to identify Reed-Solomon), or to automate a task I’ve already performed manually (e.g., in this very article, I used it to build a script based on commands I had executed and that were recorded in the log). I thoroughly enjoy my hobby, and I don’t want automation to rob me of those hours spent banging my head against the wall trying to figure something out, or that moment of pleasure when the penny drops and I have a “eureka” moment.</p>\n<p>About a decade ago I remember reading Matasano’s cryptochallenges and this sentence stuck in my head since then: <strong>Breaking repeating-key XOR (“Vigenere”) statistically is obviously an academic exercise, a “Crypto 101” thing. But more people “know how” to break it than can actually break it</strong>.</p>\n<p>I guess now we live in the world where everyone <em>knows how to ask the LLM to hack something</em> instead of actual <em>knowing how to hack something</em>.</p>\n<p>As usual, feel free to give us feedback at our twitter <a href=\"https://twitter.com/AdeptsOf0xCC\" rel=\"nofollow ugc noopener\">@AdeptsOf0xCC</a> (although I lost the password and can’t access to the account anymore <strong>:D</strong>).</p>","headings":[{"level":1,"text":"A story of wifi secrets, UART lockdowns and a damaged flash memory chip","id":"a-story-of-wifi-secrets-uart-lockdowns-and-a-damaged-flash-memor"},{"level":1,"text":"Prayers at the foot of the Altar a.k.a. disclaimer","id":"prayers-at-the-foot-of-the-altar-a-k-a-disclaimer"},{"level":1,"text":"Table of contents","id":"table-of-contents"},{"level":1,"text":"0x00 Firmware extraction","id":"0x00-firmware-extraction"},{"level":1,"text":"0x01 Identifying the Wifi credential generation algorithm","id":"0x01-identifying-the-wifi-credential-generation-algorithm"},{"level":1,"text":"0x02 Rubber-hose treatment to the UART lockdown","id":"0x02-rubber-hose-treatment-to-the-uart-lockdown"},{"level":1,"text":"0x03 Filesystem gardening for debugging purposes","id":"0x03-filesystem-gardening-for-debugging-purposes"},{"level":1,"text":"0x04 Final words","id":"0x04-final-words"}]}}