跳到正文
Hacker News· m0thra·· 6 天前AI 评分22

Eufy HomeBase 2 固件逆向:WiFi 密钥算法、UART 解锁与损坏的闪存芯片

A story of WiFi secrets, UART lockdowns and a damaged flash memory chip

AI 导读

安全研究者对 Eufy HomeBase 2(固件版本 3.4.2.2h)展开硬件级逆向,拆焊 Winbond 25Q256JWEQ 闪存芯片后离板读取固件,过程中损坏了一个 64KB 块。研究还原了 OCEAN_XXXXXX 管理 WiFi 的凭据生成算法,并通过重刷植入后门的 U-Boot 镜像经 UART 获取 shell。

正文 · AI 翻译

亲爱的Fellowlship,正如我们在上一篇文章中宣布的那样,我们敬爱的God Debugger认为应当将他那充满操作码的神圣之光洒向他卑微的仆人,于是我们的神殿里又有了一篇新的布道。请坐下,聆听我们的其中一只猫头鹰,连同他的小舅子,如何继续剖析Eufy生态系统的下一个组件:Eufy HomeBase 2。

祭坛脚下的祈祷,又名免责声明

我们将分析重点放在Eufy Homebase2而非Homebase3,是因为正如我们在从你的门铃到你的家庭网络中所解释的,这是我在我所在城市发现部署的版本,也是最容易买到的版本。我们在此分析的固件版本是 3.4.2.2h (2026-06-05 18:45:48)

这是我第一次接触与“硬件”相关的东西。我一直都是在可以从互联网下载的固件上进行漏洞挖掘,所以请原谅我犯下的罪过

重要!!! 在继续阅读本文之前,你必须先阅读上一篇文章。

目录

与上一篇文章一样,我们在这里将涵盖各种主题;因此,这个fellowlship中的一些成员可能对某些部分不感兴趣,可能想直接跳到最吸引他们的内容。这就是为什么我们呈上这份神圣的供品:一份目录。

在这个系列的上一章中,我提到我们之所以只能提取门铃的固件,是因为Homebase的闪存直接焊接在板上,没有暴露任何可以连接测试钩来转储固件的引脚。起初我们不想把它拆焊下来(尽管最终我们不得不这么做,正如我稍后会解释的那样),所以我不得不等待购买额外的硬件。Craig S. Blackie向我推荐了适合Winbond 25Q256JWEQ(WSON8 8.6mm)所需尺寸的弹簧夹。夹子到货后,我搭建了你能想象到的最临时凑合的装置,用橡皮筋固定夹子,然后像我们对门铃所做的那样,使用树莓派的SPI0接口进行读取。

Setup using rubber bands to read the firmware.
使用橡皮筋固定以读取固件的装置。

我进行了三次读取,三次的校验和都不同,这通常表明出了问题。检查转储内容后,我很快意识到它们是垃圾数据,完全无用。与门铃不同,在这种情况下,读取操作所用的电源也激活了板上与存储器交互的其他组件;因此,为了提取固件,我们不得不将芯片拆焊下来并在板外读取。

我的姐夫(Óscar Lestón)是那位勇敢的人,他拆焊了闪存芯片(我甚至都不敢尝试):

Desoldering
拆焊

尽管内存模块拆下后看起来惨不忍睹,但它几乎没怎么损坏……

Chip
提取出的闪存芯片

最后我们使用 Raspberry Pi SPI 0 复制了相同的设置来转储固件。这一次,三次转储的校验和相同,并且包含“有趣的数据”。唯一的问题是……有一个 64kbs 的块被擦除/无法读取,极有可能是因为我们在提取芯片时损坏了它。

Using the Raspberry Pi to read the chip off-board
使用 Raspberry Pi 在板外读取芯片

大部分固件可以使用 Binwalk 提取。


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

有些部分被恢复了,有些没有。因为有很多 XS 流,我们选择了这个愚蠢(或者也许没那么愚蠢)的想法,直接使用 7z 将所有内容提取为一个大块(并希望在那里通过搜索特征来切分文件)。

➜  extractions mkdir -p 7z_mandanga  
➜  extractions 7z e -y -o7z_mandanga homestation2_firmware00.bin 

7-Zip 23.01 (x64) : Copyright (c) 1999-2023 Igor Pavlov : 2023-06-20
 64-bit locale=es_ES.UTF-8 Threads:16 OPEN_MAX:1024

Scanning the drive for archives:
1 file, 33554432 bytes (32 MiB)

Extracting archive: homestation2_firmware00.bin
         
ERRORS:
There are data after the end of archive

--
Path = homestation2_firmware00.bin
Type = xz
ERRORS:
There are data after the end of archive
Offset = 3022226
Physical Size = 11514432
Tail Size = 19017774
Method = LZMA2:17
Streams = 358
Blocks = 358
Characteristics = BlockPackSize BlockUnpackSize

ERROR: There are some data after the end of the payload data : homestation2_firmware00

Sub items Errors: 1

Archives with Errors: 1

Open Errors: 1

Sub items Errors: 1
➜  extractions ls -hal 7z_mandanga 
total 42M
drwxrwxr-x 2 psyconauta psyconauta 4,0K sep 19 19:41 .
drwxrwxr-x 4 psyconauta psyconauta 4,0K sep 19 19:34 ..
-rw-rw-r-- 1 psyconauta psyconauta  42M sep 19 18:35 homestation2_firmware00

0x01 识别 Wifi 凭据生成算法

我们想通过固件调查的关键问题之一是为 OCEAN_XXXXXX 管理 Wi-Fi 生成凭据的新算法,因为我们观察到它与基于 2024 年论文的预期完全不同。特别是,我们感兴趣的是想知道该算法是否像以前一样弱。

在写完上一篇文章后,我意识到我作为导师辜负了我的姐夫。我向他展示了如何使用 Ghidra 和伪 C,而不是引导他走向光明与理性的道路。我为我的罪过赎罪,并画出了 radare2。

首先我们检查了 Wifi 网络 ESSID 和密码在固件中的位置。一个简单的 grep 就足以识别包含 wifi 名称和凭据的 NVRAM 变量(用于注册门铃的 ad-hoc wifi 和管理 wifi):

➜  extractions strings homestation2_firmware00.bin_83358_unknown.raw | grep F1VSXfNQL_ZCd3k -A5 -B5  
AuthMode=WPAPSKWPA2PSK;WPAPSKWPA2PSK
EncrypType=AES;AES
RekeyInterval=3600
RekeyMethod=TIME
PMKCachePeriod=10
WPAPSK1=F1VSXfNQL_ZCd3k
WPAPSK2=913ceac3
DefaultKeyID=2;2
Key1Type=0
Key1Str1=
Key2Type=0
➜  extractions strings homestation2_firmware00.bin_83358_unknown.raw | grep OCEAN_E9A5FC -A5 -B5   
DDNSPassword=
CountryRegion=0
CountryRegionABand=7
CountryCode=ES
BssidNum=2
SSID1=OCEAN_E9A5FC
SSID2=4350c372
WirelessMode=9
TxRate=0
Channel=0
BasicRate=15

然后我们尝试了更广泛的搜索,试图识别与 wifi 设置相关的二进制文件和/或脚本:

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

进一步梳理这条线索,我们发现了一行有趣的代码,与我们已知的情况相符:ESSID 是“OCEAN_”和 MAC 地址的拼接:

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

让我们缩小范围以获取完整脚本:

➜  7z_mandanga strings homestation2_firmware00 | rg 'ssid_prefix' -B30 -A40
.sdata
.sbss
.bss
.pdr
.gnu.attributes
.mdebug.abi32
#!/bin/sh
work_mode="`cat /var/run/testmode`"
def_mode="`nvram_get 2860 work_mode`"
SSID_MAC=""
nvram_ssid="`nvram_get 2860 SSID1`"
bssid_num=`nvram_get 2860 BssidNum`
eth_state=`apcliDaemon get_eth_state`
repeater_enable=`nvram_get 2860 RepeaterEnable`
wifi_11bgn_enabled=`nvram_get 2860 wifi_11bgn_enabled`
Wireless_Mode=9
if [ "$eth_state" = "0" -a "$repeater_enable" = "1" -a "$wifi_11bgn_enabled" = "1" ]; then
    Wireless_Mode=9
get_wifi_mac()
    SSID_MAC="`ifconfig ra0 | grep HWaddr | awk '{print $5}' | awk -F ':' '{print $4$5$6}'`"
    #SSID_MAC="`ifconfig ra0 | grep HWaddr | awk '{print $5}' | awk -F ':' '{print $5$6}'`" 
    while [ "$SSID_MAC" = "" ]
        sleep 1
        SSID_MAC="`ifconfig ra0 | grep HWaddr | awk '{print $5}' | awk -F ':' '{print $4$5$6}'`"
        #SSID_MAC="`ifconfig ra0 | grep HWaddr | awk '{print $5}' | awk -F ':' '{print $5$6}'`"
    done
    echo "get mac in ssid is:$SSID_MAC"
if [ "$bssid_num" = "2" ];then
    ifconfig ra1 down>/dev/null 2>&1
if [ "$work_mode" = "1" ];then
    ssid_prefix="ocean_factory_"
    if [ "$def_mode" = "factory_mode" ];then
        ssid_prefix="OCEAN_"
    get_wifi_mac
    WIFI_SSID=$ssid_prefix$SSID_MAC
    echo "set wifi SSID:$WIFI_SSID"
    iwpriv ra0 set SSID="$WIFI_SSID"
    if [ "$def_mode" = "factory_mode" ];then
        # only WIFI_SSID is not equ nvram_ssid, update nvram.
        if [ "$nvram_ssid" != $WIFI_SSID ];then
            nvram_set SSID1 $WIFI_SSID
        fi
else
    ssid_prefix="`nvram_get ssid_pre`"
    if [ "$ssid_prefix" = "" ];then
        ssid_prefix="OCEAN_"
    get_wifi_mac
    WIFI_SSID=$ssid_prefix$SSID_MAC
    echo "set wifi SSID:$WIFI_SSID"
    # only WIFI_SSID is not equ nvram_ssid, update nvram.
    if [ "$nvram_ssid" != $WIFI_SSID ];then
        nvram_set SSID1 $WIFI_SSID
        iwpriv ra0 set SSID="$WIFI_SSID"
        if [ "$nvram_ssid" = "MT7628_AP" ];then
            sleep 3
            #reboot
            /sbin/umount_and_reboot.sh
        fi
    iwpriv ra0 set HtBw=0
    iwpriv ra0 set WirelessMode=$Wireless_Mode
    iwpriv ra0 set HtGi=0
    iwpriv ra0 set SCSEnable=1
    if [ "$bssid_num" = "2" ];then
        # ssid and pwd set in RT2860_default_vlan
        #iwpriv ra1 set SSID="AKDevbind"
        #iwpriv ra1 set WPAPSK="Anker39#*"
        #iwpriv ra1 set SSID="XMCONFIG"
        #iwpriv ra1 set WPAPSK="12345678"
        iwpriv ra1 set HtBw=0
        iwpriv ra1 set WirelessMode=$Wireless_Mode
        iwpriv ra1 set HtGi=0
        iwpriv ra1 set SCSEnable=1
        iwpriv ra1 set WmmCapable=1
        iwpriv ra1 set ed_chk=0
wpapsk="`nvram_get WPAPSK1`"
if [ "$wpapsk" != "" ];then
    #echo "iwpriv ra0 set WPAPSK=$wpapsk"
    iwpriv ra0 set WPAPSK=$wpapsk
# set wmm
iwpriv ra0 set WmmCapable=1
# change wan mode, 
wifi
apcli
iwpriv
home_security
if [ "$eth_state" = "0" -a "$repeater_enable" = "1" ]; then
    switch_wan_interface.sh eth2.2 apcli0 0
#!/bin/sh

所以,一方面我们发现了 ESSID 是如何创建的,另一方面我们发现密码是从一个 NVRAM 变量(wpapsk="nvram_get WPAPSK1")中读取的。让我们用那个名字快速搜索一下:

➜  7z_mandanga strings homestation2_firmware00 | rg -i 'WPAPSK1' -A5 -B5  
wifi channel:%d.
fail to get freq.
/dev/gpio
Error while opening /dev/gpio
SSID1
WPAPSK1
SSID2
ra1 SSID=%s
WPAPSK2
ra1 WIFI_PWD=%s
/sbin/chpasswd.sh eufycamera %s
--
(...)

就在第一个命中中,有一些看起来像是编译过的二进制文件(那个 %d 很可疑)。我编写了这个蹩脚的脚本来提取搜索该字符串的 elf,然后向后搜索以找到魔数头,并假设 blob 中的下一个文件也是 ELF:

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

它完美地工作了:

➜  7z_mandanga python3 carve_wifi.py
➜  7z_mandanga file elf_wifi_01     
elf_wifi_01: ELF 32-bit LSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped

快速检查符号后,我们发现了许多与 wifi 相关的有趣内容,特别是那个诱人的 set_wifi_pwd。

➜  7z_mandanga r2 elf_wifi_01
WARN: Relocs has not been applied. Please use `-e bin.relocs.apply=true` or `-e bin.cache=true` next time
 -- Use 'zoom.byte=printable' in zoom mode ('z' in Visual mode) to find strings
[0x0045d130]> aaa
INFO: Analyze all flags starting with sym. and entry0 (aa)
INFO: Analyze imports (af@@@i)
INFO: Analyze entrypoint (af@ entry0)
INFO: Analyze symbols (af@@@s)
INFO: Running plugin pre-analysis hooks
INFO: Analyze all functions arguments/locals (afva@@F)
INFO: Analyze function calls (aac)
INFO: Analyze len bytes of instructions for references (aar)
INFO: Finding and parsing C++ vtables (avrr)
INFO: Analyzing methods (af @@ method.*)
INFO: Emulate functions to find computed references (aaef)
INFO: Recovering local variables (afva@@@F)
INFO: Type matching analysis for all functions (aaft)
INFO: Propagate noreturn information (aanr)
INFO: Use -AA or aaaa to perform additional experimental analysis
INFO: Finding xrefs in noncode sections (e anal.in=io.maps.x; aav)
[0x0045d130]> afl~wifi
0x006f8908    1    288 sym.eufy_setDoorbellMechanicalChimeSwitch_wifi
0x005a972c   14   1124 sym.zx_update_dev_wifi_info_by_addr
0x0054ad3c   10    316 sym.check_hub_wifi_power
0x00558fd8   18   1800 sym.quick_bind_wifi_mgmt_rcv_handler
0x0052cfac   19   1272 sym.zx_update_hub_wifi_channel
0x005e5414   93   7544 sym.netlink_wifi_connect
0x0093f1c0    6    528 sym.aws_wifi_get_mac
0x0053aff8    1     80 sym.zx_get_wifi_mac
0x004cda10   13    728 sym.zx_check_wifi_version
0x005489a0    1     16 sym.wifi_reduceTxPower
0x005a47a0   23    892 sym.zx_alloc_wifi_addr
0x00532224    4    324 sym.set_camera_wifi_rssi
0x0049c7d4   11    448 sym.wifi_check_all_devs_online
0x0049c61c   10    440 sym.wifi_get_dev_rssi
0x0049c994   20   1768 sym.wifi_get_devs_rssi_list_in_json
0x0059e8f8    3    204 sym.restart_wifi
0x0053bb04    1    144 sym.set_pair_wifi_info
0x0053b048    8    660 sym.get_wifi_info
0x006fb684  399  18808 sym.zx_wifi_send_command
0x00603514    6    484 sym.make_wifi_notify_json
0x0054ab0c    9    560 sym.convert_wifi_countrycode
0x0059df7c    1    104 sym.wifi_info_signal_init
0x0053b510   33   1524 sym.set_wifi_pwd
0x00558894    5    292 sym.zx_quick_bind_timer_stop_send_wifi_info
0x006f8094    8    304 sym.eufy_set_wifi_countrycode
0x0054ae78    7    464 sym.set_wifi_power_flag
0x004da374   54   3076 sym.zx_handle_wifi_ready
0x006f6348    8    828 sym.wifi_log_server_init
0x006f8a28    1    288 sym.eufy_setDoorbellElectronicRingtoneTime_wifi
0x0094a1f4    4    168 sym.anker_mgmt_register_get_wifi_channel_cb
0x00532368   10    396 sym.check_camera_wifi_rssi_ok
0x005580a0   28   2036 sym.zx_quick_bind_timer_send_wifi_info
0x0094a29c    4    168 sym.anker_mgmt_register_get_wifi_mac_cb
0x0053b2dc    6    564 sym.get_wifi_ra1_info
0x006f6684    8    828 sym.wifi_log_server_t40_init
0x0056dc34   15   1008 sym.repeater_update_all_dev_wifi_channel
0x0049d07c   13    636 sym.wifi_devs_rssi_test_mode_exit
0x004daf78    1    160 sym.handle_wifi_ready_thread
0x008c5ac4    7    356 sym.zx_homekit_get_wifi_ready
0x0049d2f8    9    288 sym.wifi_setManualChannel
0x004cd584   11    668 sym.eufysdk_common_msg_wifi_callback

跳入该函数,我们可以看到它首先基于 MD5 哈希构建一个密码,该密码在本地用于用户 eufycamera,与 wifi 无关。wifi 相关的内容从 0x0053b690 开始,在那里它检查来自 base_param 的一个参数(我猜是设备类型或型号)是否等于 0x0C。这部分是关键,因为如果该值不等于 0x0C,那么它使用 MAC 地址(右侧块),而如果匹配,则使用 z8GetSN() 返回的内容(左侧块)。我们处于 z8GetSN() 的情况。

Radare2
基于设备型号的不同流程。

z8GetSN() 函数通过 i2c 通信并发送命令 0xC1(i2c 部分在 z8SendData() 内部),并从响应中读取 0x11 字节。

Radare2
通过 I2C 通信提取序列号(不是设备 SN)

然后字节被转换为 32 字节字符串,并传递给一个名为 mac_func() 的函数,该函数还接收 Homebase 序列号(T8010...):

Radare2
来自 I2C 通信的 SN 和设备序列号(obj.g_hub_sn)被用作 mac_func() 的参数

这个 mac_func 调用一个辅助函数,使用前面提到的数据执行 HMAC SHA256,其中“消息”是 z8GetSN 返回的值,“密钥”是序列号:

Radare2
HMAC SHA256

结果被一个函数使用,该函数将信息编码为文本。

Radare2
编码内存内容

zx_Memory_Encode 函数调用 urlsafe_b64_encode,后者使用以下字母表将值转换为 base64:

[0x006f0490]> px 65 @ 0xa5cb40
- offset -  4041 4243 4445 4647 4849 4A4B 4C4D 4E4F  0123456789ABCDEF
0x00a5cb40  4142 4344 4546 4748 494a 4b4c 4d4e 4f50  ABCDEFGHIJKLMNOP
0x00a5cb50  5152 5354 5556 5758 595a 6162 6364 6566  QRSTUVWXYZabcdef
0x00a5cb60  6768 696a 6b6c 6d6e 6f70 7172 7374 7576  ghijklmnopqrstuv
0x00a5cb70  7778 797a 3031 3233 3435 3637 3839 2d5f  wxyz0123456789-_
0x00a5cb80  00                                       .

最后,密码在 NVRAM 中更新,并通过执行外部命令 iwpriv ra0 set WPAPSK=%s 来更改:

Radare2
更新 NVRAM 值

因此,复制密码的唯一未知数据是 z8GetSN 返回的值。另一个组成部分,即设备序列号,是“可预测的”,因为它遵循基于设备类型的已知模板。

0x02 对 UART 锁定的橡胶管攻击

在上一篇文章中,我们提到 UART 访问被阻止了。具体来说,通过 UART 连接会得到一个提示,要求用户按“Enter”键。然而,这样做会导致进程终止,同一个程序重新启动,再次要求按键;这创建了一个无限循环,阻止了访问真正的 shell。日志如下所示:

starting pid 5617, tty '/dev/ttyS1': '/sbin/login.sh'

process '/sbin/login.sh' (pid 5617) exited. Scheduling for restart.

Please press Enter to activate this console. 
setrlimit(RLIMIT_CORE, &limit); 

starting pid 6526, tty '/dev/ttyS1': '/sbin/login.sh'

process '/sbin/login.sh' (pid 6526) exited. Scheduling for restart.

Please press Enter to activate this console. 
setrlimit(RLIMIT_CORE, &limit); 

starting pid 6608, tty '/dev/ttyS1': '/sbin/login.sh'



process '/sbin/login.sh' (pid 6608) exited. Scheduling for restart.

另一方面,尝试用转义序列中断 U-Boot 没有成功。

在获取固件并解决了关于 Wi-Fi 密码算法如何工作的问题后,下一个目标是理解 UART 访问锁定背后的机制并找到绕过它的方法,因为我们打算使用设备本身来调试关键服务并搜索漏洞。

在日志中,串口是 /dev/ttyS1,所以让我们搜索对它的引用:

➜  extractions grep -r ttyS1 
grep: homestation2_firmware00.bin.extracted/E70000/Linux_Kernel_Image.bin.extracted/0/decompressed.bin: coincidencia en fichero binario
grep: homestation2_firmware00.bin.extracted/E70000/Linux_Kernel_Image.bin.extracted/0/decompressed.bin_3143840_aes_forward_table.raw: coincidencia en fichero binario
grep: homestation2_firmware00.bin.extracted/E70000/Linux_Kernel_Image.bin.extracted/0/decompressed.bin_3679760_unknown.raw: coincidencia en fichero binario
grep: homestation2_firmware00.bin.extracted/E70000/Linux_Kernel_Image.bin.extracted/0/decompressed.bin_3135648_aes_reverse_table.raw: coincidencia en fichero binario
grep: homestation2_firmware00.bin.extracted/E70000/Linux_Kernel_Image.bin.extracted/0/decompressed.bin.extracted/3F16A4/decompressed.bin: coincidencia en fichero binario
homestation2_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
grep: 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
grep: 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
homestation2_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
grep: homestation2_firmware00.bin.extracted/E70000/Linux_Kernel_Image.bin.extracted/0/decompressed.bin.extracted/3F16A4/decompressed.bin_0_cpio.raw: coincidencia en fichero binario
grep: homestation2_firmware00.bin.extracted/E70000/Linux_Kernel_Image.bin.extracted/0/decompressed.bin_3147936_aes_rcon.raw: coincidencia en fichero binario
grep: 7z_mandanga/homestation2_firmware00: coincidencia en fichero binario

关键在 inittab 文件中,我们可以看到一旦用户按下“Enter”(BusyBox 的 askfirst 操作会显示提示“Please press Enter to activate this console”),它就会触发执行 /sbin/login.sh 脚本。该脚本很简单,如下:

#!/bin/sh

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

该脚本通过检查 NVRAM 变量 debug_log 来检查是否启用了调试模式,并基于此允许访问 shell(好吧,是登录到 shell)或退出,进入循环。

我们没有找到直接启用该变量并绕过锁定的方法。有多种方法可以更改启动模式以启用“调试”,但所有这些方法都遥不可及,因为:

  1. 其中一种基于文件系统中存在某个文件。因为要能够放置文件,我们需要访问权限,这是一个先有鸡还是先有蛋的情况。
  2. 另一种方法涉及一个复杂的魔法协议,基于 ARP 和由固件中不存在的私钥签名的命令。是的,他们在 ARP 上封装了自己的协议,并交换魔法签名数据包以远程启用调试。我不确定他们抽了什么烟。

此时我想到,如果我们无法绕过检查它是在生产模式还是调试模式下启动,为什么不完全删除检查并直接跳转到 shell?

固件不包含任何加密内容,所以修改它以允许我们通过串口直接访问应该是直截了当的。问题是……你还记得我提到它“几乎没损坏”吗?好吧,我们在 0x500000 处搞坏了 64kbs,那是镜像的加载地址。

起初,我以为是我们笨拙的双手毁了这个项目,用调试器对实际设备进行动态分析的想法也正在溜走(我们仍然可以选择尝试模拟一些二进制文件,但能访问原始硬件总是要容易得多)。幸运的是,在进一步分析固件布局后,我们发现了一个小得多的“恢复镜像”,它在0xe70000处仍然完好无损。所以,由于“正常”镜像已经损坏,引导加载程序将不得不加载这个我们可以操纵的恢复镜像,以移除 UART 锁定。

“恢复镜像”是一个带有 64 字节头的 U-Boot 镜像:

头部偏移 长度 字段 原始恢复值
0x00 4 魔数 0x27051956
0x04 4 头部 CRC32 0xe764e161
0x08 4 时间戳 由构建器保留
0x0c 4 压缩负载长度 0x3455d9 = 3,429,849
0x10 4 RAM 加载地址 0x80000000
0x14 4 入口地址 0x802e04f0
0x18 4 负载 CRC32 0x19b650c3
0x1c 4 操作系统、架构、类型、压缩 05 05 02 03
0x20 32 镜像名称区域 保留,包括供应商使用的字节

实际文件系统位于不同压缩层的套娃之中,如下图所示:

Matrioska of compression
套娃!

文件系统解包后,我做了最小的修改,以便能够直接以 root 权限跳转到 shell。为了避免麻烦,我对修改进行了填充,使其与原始代码的大小相同(代码来自我用来执行修改并重新打包/修复 CRC 的 python 脚本):

old_condition = b'if [ "$DEBUG_LOG" = "1" ];then'
new_condition = b'if [ 1 = 1 ];then' + b' ' * 13

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

在完成一些本地测试以验证修改后的恢复镜像已正确修改并且能够正确解包/加载后,我们继续将其覆盖到闪存芯片中。为了禁用文档中出现的写保护,遵循了以下连接图:

Diagram to disable Write Protection
禁用写保护的连接图

写入新的恢复镜像:

Writting the new recovery image
用我们的后门版本覆盖恢复镜像

然后我们执行了三次读取以验证过程是否正确完成。之后,芯片被再次焊接到设备板上:

Soldering again the chip
再次焊接芯片

芯片焊接完成后,我再次使用 USB-TTL 将笔记本电脑连接到设备。首先它尝试加载正常镜像,但失败了,然后它跳转到后门恢复镜像,加载后……我们获得了一个 shell!!

UART shell
终于访问到管理员 shell!!

0x03 用于调试目的的文件系统园艺

我们终于有了一个 shell;唯一的问题是恢复镜像非常有限,并且文件系统没有挂载,因此缺少正确调试系统所需的二进制文件和库。此外,内核缺少某些模块——例如负责处理 WiFi 接口或用于生成 WiFi 密钥的 I2C 接口的模块。

默认镜像中可用的命令集相当有限,但至少有一些工具,比如wget,它允许我们将文件从笔记本电脑传输到设备。我用一个 2015 年从 Telefónica 获得的旧 TP-Link 路由器准备了一个简陋的设置。

Network Setup
不华丽的设置

我编译了一个新的 BusyBox,其中包含大量原版中没有的额外命令。使用这个新的 BusyBox,我开始尝试创建一个“分阶段”文件系统,并将所有路径绑定到这个“分阶段”文件系统上。我必须先执行那些在主服务所依赖的大 ELF(homeapp_security,也就是包含 WiFi 逻辑的同一个二进制文件)之前需要运行的服务,同时由于内存限制,我还得修改一些内核参数,比如 overcommit_ratio。这是一个反复试错的三小时过程,直到我能够运行主二进制文件并与之交互。

因为每次我想使用这个设备时都需要重复这个过程(因为恢复镜像中的文件系统不是持久的,所以每次启动后我都必须恢复所有内容),我把 picocom 的日志分享给了 Codex,并让它准备一个一体化的脚本,能够重复我所做的相同步骤(并丢弃那些不起作用的部分)。说实话,在折腾 Homebase 的那个夜晚之后的第二天早上,我几乎不记得自己做了什么,所以有日志对于重建和自动化这个过程来说太棒了。永远记录一切日志,它们总会在某个时候救你一命。

直到 homeapp_security 运行之前的操作流程图如下:

Diagram of actions
从修补后的固件到让二进制文件运行的流程图

重建文件系统最重要的两个脚本如下所示:

#!/bin/sh
# Recovery UART bootstrap. Run with sh; do not source into the login shell.
# Usage: sh homeapp-restore-v5.sh [http://PC:8000] [--start]
PATH=/bin:/sbin:/usr/bin:/usr/sbin
export PATH
unset LD_PRELOAD LD_LIBRARY_PATH
BASE=/media/homeapp-debug
ROOT=$BASE/rootfs
SERVER=http://192.168.90.112:8000
START=no
fail() { echo "homeapp-restore: $*" >&2; exit 1; }
for argument in "$@"; do
    case "$argument" in
        http://*) SERVER=${argument%/} ;;
        --start) START=yes ;;
        *) fail 'usage: sh homeapp-restore-v5.sh [http://PC:8000] [--start]' ;;
    esac
done
case "$SERVER" in *' '*|*'?'*|*'#'*) fail 'use a plain HTTP base URL without spaces/query/fragment' ;; esac
[ "`awk '/^Uid:/ {print $3}' /proc/self/status`" = 0 ] || fail 'root effective UID required'
[ -r /proc/self/mountinfo ] || fail '/proc/self/mountinfo is required'
[ -c /dev/gpio ] && [ -c /dev/apsoc_nvram ] || fail 'expected recovery device nodes missing'

mounted() { awk -v p="$1" '$2==p {n++} END {exit n==0}' /proc/mounts; }
check_tmpfs() {
    awk -v p="$1" -v size="$2" '$2==p {
        n++; if ($3=="tmpfs") {
            count=split($4,a,","); for(i=1;i<=count;i++) if(a[i]=="size=" size "k") ok=1
        }
    } END {exit !(n==1 && ok)}' /proc/mounts
}
empty_dir() {
    [ ! -e "$1" ] && return 0
    [ -d "$1" ] && [ ! -L "$1" ] || return 1
    [ -z "`ls -A "$1"`" ]
}
asset_md5() {
    # BEGIN GENERATED ASSET HASHES
    case "$1" in
        busybox-extra) echo b587f623ef28b84e277fbd1b2b0b2348 ;;
        homeapp-enter) echo 8fde5ed1e3e65d07a7b9ebe672b25a5c ;;
        homeapp-mount-runtime.sh) echo 19e73a1584137c0d2150d6968cccb4c5 ;;
        homeapp-start-bounded-v4.sh) echo 19b70b8ac6751f8e161e490460ff8f3e ;;
        homeapp-control-v5.sh) echo 2fc7613db0f02a0d16c1417bb0282264 ;;
        homeapp_rootfs.tar.gz) echo bf6acaecf3301532bda2c1f6c41a3b2c ;;
        *) return 1 ;;
    esac
    # END GENERATED ASSET HASHES
}
verify() {
    expected=`asset_md5 "$1"` || fail "unknown asset: $1"
    actual=`md5sum "$2" | awk '{print $1}'`
    [ "$actual" = "$expected" ] || fail "checksum mismatch: $2; stop and check the PC bundle"
}
fetch() {
    asset=$1
    echo "homeapp-restore: downloading $asset"
    wget -O "$BASE/$asset.part" "$SERVER/$asset" || fail "download failed: $asset; partial setup preserved"
    verify "$asset" "$BASE/$asset.part"
    mv "$BASE/$asset.part" "$BASE/$asset" || fail "cannot finish $asset"
}
stamp() { awk '{sub(/^.*\) /, ""); print $20}' "/proc/$1/stat" 2>/dev/null; }
is_supervisor() {
    [ -r "/proc/$1/cmdline" ] || return 1
    tr '\000' '\n' < "/proc/$1/cmdline" | awk '
        NR==1 {shell=($0=="sh" || $0=="/bin/sh" || $0=="/sbin/run_mini_system.sh")}
        $0=="/sbin/run_mini_system.sh" {script=1}
        END {exit !(shell && script)}'
}
recover_pids() {
    for procdir in /proc/[0-9]*; do
        [ -e "$procdir/exe" ] || continue
        if [ "$procdir/exe" -ef /bin/recover ] || [ "$procdir/exe" -ef /sbin/recover ]; then
            echo "${procdir#/proc/}"
        fi
    done
}
signal_recover() {
    for recover_pid in `recover_pids`; do
        if [ "/proc/$recover_pid/exe" -ef /bin/recover ] || [ "/proc/$recover_pid/exe" -ef /sbin/recover ]; then
            kill "-$1" "$recover_pid" 2>/dev/null || :
        fi
    done
}
coordinate_recovery() {
    # Exact argv matching avoids accidentally signalling grep or the UART shell.
    for procdir in /proc/[0-9]*; do
        supervisor_pid=${procdir#/proc/}
        if is_supervisor "$supervisor_pid"; then
            supervisor_stamp=`stamp "$supervisor_pid"`
            [ -n "$supervisor_stamp" ] || continue
            kill -STOP "$supervisor_pid" || fail "cannot pause recovery supervisor $supervisor_pid"
            echo "$supervisor_pid $supervisor_stamp" > "$BASE/recovery-supervisor-$supervisor_pid"
            echo "homeapp-restore: paused recovery supervisor $supervisor_pid"
        fi
    done
    signal_recover TERM
    sleep 2
    signal_recover KILL
    sleep 1
    [ -z "`recover_pids`" ] || fail 'recovery tasks survived stop'
    pidof recover >/dev/null 2>&1 && fail 'unrecognized recover process remains; inspect ps'
    return 0
}
install_commands() {
    for asset in homeapp-mount-runtime.sh homeapp-start-bounded-v4.sh homeapp-control-v5.sh; do
        verify "$asset" "$BASE/$asset"
    done
    # Rename new files into place: never truncate a script a running shell
    # may still be reading during a repeated preparation command.
    for asset in homeapp-mount-runtime.sh homeapp-start-bounded-v4.sh homeapp-control-v5.sh; do
        destination=/tmp/$asset
        [ "$asset" != homeapp-control-v5.sh ] || destination=/tmp/homeapp
        cp "$BASE/$asset" "$destination.part.$$" || fail "cannot copy $asset"
        chmod 755 "$destination.part.$$" || fail "cannot mark $asset executable"
        mv "$destination.part.$$" "$destination" || fail "cannot install $asset"
    done
}

echo 'homeapp-restore revision: v5; original firmware applications, RAM staging'
if [ -f "$BASE/restore-v5.ready" ]; then
    # Reusing this recipe's completed setup is safe; never extract over binds.
    check_tmpfs "$BASE" 65536 || fail 'staging mount changed or is stacked'
    check_tmpfs /media/mmcblk0p1 8192 || fail 'database mount changed or is stacked'
    verify homeapp-enter "$BASE/homeapp-enter"
    verify busybox-extra "$BASE/busybox-extra"
    install_commands
    sh /tmp/homeapp-mount-runtime.sh --resume || fail 'runtime mount verification failed'
    coordinate_recovery
    echo 'homeapp-restore: verified and reused completed setup; no archive download/extraction'
else
    # Fail closed on partial/manual setups; no rm -rf, unmount or overlay of data.
    mounted "$BASE" && fail 'unrecognized/partial staging mount; inspect it or reboot before retrying'
    empty_dir "$BASE" || fail 'staging directory is not empty; preserve it and inspect or reboot'
    mounted /media/mmcblk0p1 && fail 'database/storage path already mounted; will not cover it'
    empty_dir /media/mmcblk0p1 || fail 'database/storage directory is not empty; will not cover it'
    free_kb=`awk '/^MemFree:/ {print $2}' /proc/meminfo`
    [ "${free_kb:-0}" -ge 81920 ] || fail 'less than 80 MiB free; reboot to a fresh recovery shell or inspect memory'
    # Refuse existing normal services before staging/coordination.
    for service in home_security mdnsd mips_collector pushMuxer systemServer embdb_eufy; do
        pidof "$service" >/dev/null 2>&1 && fail "$service already running; use a fresh recovery boot"
    done
    original_mode=`cat /proc/sys/vm/overcommit_memory`
    original_ratio=`cat /proc/sys/vm/overcommit_ratio`
    if [ "$original_mode" = 2 ] && [ "$original_ratio" -lt 75 ]; then
        echo 75 > /proc/sys/vm/overcommit_ratio || fail 'cannot raise runtime commit ratio'
        echo "homeapp-restore: strict overcommit ratio $original_ratio -> 75 (this boot only)"
    fi
    if [ "$original_mode" = 2 ]; then
        headroom=`awk '/^CommitLimit:/ {limit=$2} /^Committed_AS:/ {used=$2} END {print limit-used}' /proc/meminfo`
        [ "$headroom" -ge 65536 ] || fail 'less than 64 MiB commitment headroom; no extraction attempted'
    fi
    mkdir -p "$BASE" || fail 'cannot create staging directory'
    mount -t tmpfs -o size=64m tmpfs "$BASE" || fail 'cannot mount staging tmpfs'
    check_tmpfs "$BASE" 65536 || fail 'unexpected staging filesystem size'
    echo "$original_mode $original_ratio" > "$BASE/original-overcommit"
    echo "$SERVER" > "$BASE/download-server"
    for asset in busybox-extra homeapp-enter homeapp-mount-runtime.sh homeapp-start-bounded-v4.sh homeapp-control-v5.sh; do
        fetch "$asset"
    done
    chmod 755 "$BASE/busybox-extra" "$BASE/homeapp-enter" || fail 'chmod failed'
    "$BASE/homeapp-enter" --check || fail 'native helper does not run'
    fetch homeapp_rootfs.tar.gz
    (cd "$BASE" && ./busybox-extra tar -xzf homeapp_rootfs.tar.gz) || fail 'extraction failed; preserve diagnostics, then reboot before retrying'
    # Only the verified temporary archive is removed, before any runtime binds.
    rm "$BASE/homeapp_rootfs.tar.gz" || fail 'cannot remove staged archive'
    "$BASE/homeapp-enter" "$ROOT" /bin/sh -c ':' || fail 'normal loader/BusyBox does not execute'
    mkdir -p /media/mmcblk0p1 || fail 'cannot create database path'
    mount -t tmpfs -o size=8m tmpfs /media/mmcblk0p1 || fail 'cannot mount temporary database'
    check_tmpfs /media/mmcblk0p1 8192 || fail 'unexpected database filesystem size'
    mkdir -p /media/mmcblk0p1/.sqlite || fail 'cannot create database directory'
    install_commands
    sh /tmp/homeapp-mount-runtime.sh --apply || fail 'runtime mount setup failed; earlier mounts preserved'
    coordinate_recovery
    echo v5 > "$BASE/restore-v5.ready" || fail 'cannot record successful setup'
fi
echo 'homeapp-restore: READY. /tmp/homeapp start|status|logs|stop|shell'
echo 'Wi-Fi/I2C drivers and persistent flash configuration are not restored by userspace staging.'
df -k "$BASE" /media/mmcblk0p1
if [ "$START" = yes ]; then
    /tmp/homeapp start || fail 'setup completed, but application startup failed; inspect /tmp/homeapp status'
fi



#!/bin/sh
# Run with `sh`, never source into the interactive UART shell.
# Does not start services, mount flash, or change memory policy.
PATH=/bin:/sbin:/usr/bin:/usr/sbin
export PATH
BASE=/media/homeapp-debug
ROOT=/media/homeapp-debug/rootfs
fail() { echo "homeapp-mount: $*" >&2; exit 1; }
mounted() { awk -v p="$1" '$2 == p {found=1} END {exit !found}' /proc/mounts; }
same_mount_root() {
    # Compare device ID and filesystem-root path, not just the filesystem type.
    awk -v s="$1" -v d="$2" '
        $5 == s {source=$3 " " $4}
        $5 == d {target=$3 " " $4; count++}
        END {exit !(count == 1 && source != "" && source == target)}
    ' /proc/self/mountinfo
}

case "${1-}" in
    --inspect)
        echo '--- Mounts ---'
        cat /proc/mounts
        echo '--- Memory ---'
        cat /proc/meminfo
        echo '--- Overcommit mode / ratio ---'
        cat /proc/sys/vm/overcommit_memory /proc/sys/vm/overcommit_ratio
        echo '--- Staged executables ---'
        ls -l "$BASE/homeapp-enter" "$ROOT/bin/busybox" "$ROOT/bin/home_security"
        echo '--- Processes ---'
        ps
        exit 0
        ;;
    --apply|--resume) mode=$1 ;;
    *) fail 'usage: sh homeapp-mount-runtime.sh --inspect|--apply|--resume' ;;
esac

mounted "$BASE" || fail 'staging filesystem is not mounted'
[ -x "$BASE/homeapp-enter" ] || fail 'native helper missing'
[ -x "$ROOT/bin/home_security" ] || fail 'application missing'
mounted /media/mmcblk0p1 || fail 'prepare temporary database mount first'
[ -d /media/mmcblk0p1/.sqlite ] || fail 'database directory missing'

# Resume only matching binds, with one mount at each staged destination.
# Leave staged /mnt (including its original zx_log.conf) in place. Recovery's
# /mnt bind failed in the first attempt; persistent state is a separate step.
for path in dev proc sys etc var tmp media dev/pts media/mmcblk0p1; do
    if mounted "$ROOT/$path"; then
        [ "$mode" = --resume ] || fail "$ROOT/$path already mounted; inspect partial setup first"
        same_mount_root "/$path" "$ROOT/$path" || fail "existing bind does not match /$path, or is stacked: $ROOT/$path"
    fi
done

bind_path() {
    source_path=$1
    target_path=$2
    if mounted "$target_path"; then
        echo "homeapp-mount: keeping verified bind $target_path"
        return 0
    fi
    mkdir -p "$target_path" || fail "cannot create $target_path"
    echo "homeapp-mount: $source_path -> $target_path"
    mount -o bind "$source_path" "$target_path"
    rc=$?
    [ "$rc" -eq 0 ] || fail "mount exited $rc: $source_path -> $target_path (earlier mounts preserved)"
    mounted "$target_path" || fail "mount returned success but $target_path is absent from /proc/mounts"
    same_mount_root "$source_path" "$target_path" || fail "unexpected mount root at $target_path"
}
for path in dev proc sys etc var tmp media; do
    bind_path "/$path" "$ROOT/$path"
done
bind_path /dev/pts "$ROOT/dev/pts"
bind_path /media/mmcblk0p1 "$ROOT/media/mmcblk0p1"
echo 'Runtime binds completed; no application services started.'

然后从 UART shell 运行它,我们可以看到它准备好了一切并启动了二进制文件:

# wget -O /tmp/homeapp-restore-v5.sh http://192.168.90.112:8000/homeapp-restore-
v5.sh
Connecting to 192.168.90.112:8000 (192.168.90.112:8000)
homeapp-restore-v5.s 100% |*******************************|  9174  --:--:-- ETA
# sh /tmp/homeapp-restore-v5.sh http://192.168.90.112:8000 --start
homeapp-restore revision: v5; original firmware applications, RAM staging
homeapp-restore: strict overcommit ratio 50 -> 75 (this boot only)
homeapp-restore: downloading busybox-extra
Connecting to 192.168.90.112:8000 (192.168.90.112:8000)
busybox-extra.part   100% |*******************************|   673k --:--:-- ETA
homeapp-restore: downloading homeapp-enter
Connecting to 192.168.90.112:8000 (192.168.90.112:8000)
homeapp-enter.part   100% |*******************************|   622k --:--:-- ETA
homeapp-restore: downloading homeapp-mount-runtime.sh
Connecting to 192.168.90.112:8000 (192.168.90.112:8000)
homeapp-mount-runtim 100% |*******************************|  3054  --:--:-- ETA
homeapp-restore: downloading homeapp-start-bounded-v4.sh
Connecting to 192.168.90.112:8000 (192.168.90.112:8000)
homeapp-start-bounde 100% |*******************************|  4383  --:--:-- ETA
homeapp-restore: downloading homeapp-control-v5.sh
Connecting to 192.168.90.112:8000 (192.168.90.112:8000)
homeapp-control-v5.s 100% |*******************************|  6488  --:--:-- ETA
homeapp-enter: running; uid=0 euid=0; static MIPS helper
homeapp-restore: downloading homeapp_rootfs.tar.gz
Connecting to 192.168.90.112:8000 (192.168.90.112:8000)
homeapp_rootfs.tar.g 100% |*******************************| 14579k 00:00:00 ETA
homeapp-mount: /dev -> /media/homeapp-debug/rootfs/dev
homeapp-mount: /proc -> /media/homeapp-debug/rootfs/proc
homeapp-mount: /sys -> /media/homeapp-debug/rootfs/sys
homeapp-mount: /etc -> /media/homeapp-debug/rootfs/etc
homeapp-mount: /var -> /media/homeapp-debug/rootfs/var
homeapp-mount: /tmp -> /media/homeapp-debug/rootfs/tmp
homeapp-mount: /media -> /media/homeapp-debug/rootfs/media
homeapp-mount: /dev/pts -> /media/homeapp-debug/rootfs/dev/pts
homeapp-mount: /media/mmcblk0p1 -> /media/homeapp-debug/rootfs/media/mmcblk0p1
Runtime binds completed; no application services started.
homeapp-restore: READY. /tmp/homeapp start|status|logs|stop|shell
Wi-Fi/I2C drivers and persistent flash configuration are not restored by userspace staging.
Filesystem           1k-blocks      Used Available Use% Mounted on
tmpfs                    65536     44844     20692  68% /media/homeapp-debug
tmpfs                     8192         0      8192   0% /media/mmcblk0p1
homeapp-mount: keeping verified bind /media/homeapp-debug/rootfs/dev
homeapp-mount: keeping verified bind /media/homeapp-debug/rootfs/proc
homeapp-mount: keeping verified bind /media/homeapp-debug/rootfs/sys
homeapp-mount: keeping verified bind /media/homeapp-debug/rootfs/etc
homeapp-mount: keeping verified bind /media/homeapp-debug/rootfs/var
homeapp-mount: keeping verified bind /media/homeapp-debug/rootfs/tmp
homeapp-mount: keeping verified bind /media/homeapp-debug/rootfs/media
homeapp-mount: keeping verified bind /media/homeapp-debug/rootfs/dev/pts
homeapp-mount: keeping verified bind /media/homeapp-debug/rootfs/media/mmcblk0p1
Runtime binds completed; no application services started.
homeapp: launcher PID 5880; startup output: /media/mmcblk0p1/homeapp-launch-5565.log
homeapp: startup in progress; /tmp/homeapp status and /tmp/homeapp logs
A running launcher is not proof of application or enrollment readiness.

最后我们可以看到服务已经暴露出来了!

# /media/homeapp-debug/busybox-extra netstat -lntup
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 0.0.0.0:10400           0.0.0.0:*               LISTEN      6016/home_security
tcp        0      0 0.0.0.0:10402           0.0.0.0:*               LISTEN      6016/home_security
tcp        0      0 0.0.0.0:32290           0.0.0.0:*               LISTEN      6016/home_security
tcp        0      0 0.0.0.0:10404           0.0.0.0:*               LISTEN      6016/home_security
tcp        0      0 0.0.0.0:32292           0.0.0.0:*               LISTEN      6016/home_security
tcp        0      0 0.0.0.0:32293           0.0.0.0:*               LISTEN      6016/home_security
tcp        0      0 0.0.0.0:32295           0.0.0.0:*               LISTEN      6016/home_security
tcp        0      0 0.0.0.0:10600           0.0.0.0:*               LISTEN      6016/home_security
tcp        0      0 0.0.0.0:32392           0.0.0.0:*               LISTEN      6016/home_security
tcp        0      0 0.0.0.0:9000            0.0.0.0:*               LISTEN      5974/pushMuxer
tcp        0      0 127.0.0.1:5000          0.0.0.0:*               LISTEN      5963/mips_collector
tcp        0      0 0.0.0.0:554             0.0.0.0:*               LISTEN      5974/pushMuxer
tcp        0      0 0.0.0.0:53              0.0.0.0:*               LISTEN      1405/dnsmasq
tcp        0      0 0.0.0.0:23              0.0.0.0:*               LISTEN      1693/telnetd
netstat: /proc/net/tcp6: No such file or directory
udp        0      0 0.0.0.0:53              0.0.0.0:*                           1405/dnsmasq
udp        0      0 0.0.0.0:32380           0.0.0.0:*                           6016/home_security
udp        0      0 0.0.0.0:46217           0.0.0.0:*                           6456/ntpclient
udp        0      0 0.0.0.0:43466           0.0.0.0:*                           1405/dnsmasq
udp        0      0 0.0.0.0:21200           0.0.0.0:*                           6016/home_security
udp        0      0 0.0.0.0:29947           0.0.0.0:*                           6016/home_security
netstat: /proc/net/udp6: No such file or directory

最后一件事是编译一个兼容版本的 GDB 并上传到设备上,开始玩起来 :)

GDB running on the Homebase2
GDB 在 Homebase2 上运行

当然,要让二进制文件正常工作还需要进行更多的配置更改(例如,我必须桥接一个 eth 接口,以在其对等节点所需的范围内创建一个 br0——长话短说:它使用一个带有 MAC 地址的对等节点表来拒绝或接受通信),但有了调试器,一切都容易得多。只需启动进程,检查哪里失败,修正它,或者直接挂钩它以提供它期望的值来继续流程。

0x04 最后的话

随着 LLM 的出现,我已经失去了发布技术内容的所有热情。自 2007 年以来,我一直在网上发表文章,使用数十个不同的化名在各种博客和论坛上分享我学到或发现的东西。这将近 20 年了。而今天,我已经失去了分享经验的所有欲望。

一方面,如果没有人会读,写作就失去了价值。如今,只有少数人会真正停下来读一篇文章;大多数人会向 AI 要一个摘要,或者只是心不在焉地浏览一下。曾经有一个生态系统,人们会花时间查看他们的 RSS 订阅(该死,我老了)并阅读他们最喜欢的博客或作者的文章。这方面的第一次重大打击来自社交媒体,即时性和“在 Twitter 上得分”成为内容创作的主要驱动力。

还有一个事实是,绝大多数发表的文章都是直接由 AI 生成的。即便是那些有趣的研究文章或漏洞分析,也是由自动化织机批量产出的,用的那种被切除脑叶的语言,每次我试图阅读时都让我中风。人们似乎很乐意吞下这种空洞的垃圾。我想是因为这符合第一点。我用“蹩脚英语”写作(是的,尽管我已经在一家英国公司工作了五年,我的英语仍然很糟糕),但我宁愿读一篇由人用“蹩脚英语”写的文章一百万次,也不愿读 ChatGPT 在其文本中反复重复的相同结构和套话。

最后一根稻草是……在 2026 年,继续为人类读者发布技术内容还有意义吗?过去,你会阅读大量材料(甚至将其收藏以备将来参考),因为它确实有用。也许你正在摆弄某个产品的固件,而别人已经探索过了;你可以找到那篇文章,沿着他们开辟的道路前进。漏洞分析也是如此:你会阅读别人的工作,以便在遇到类似场景时能够复现。我说的不仅仅是特定产品;我说的是方法论:某人如何解决问题,让你在多年后面对同样的情况时能够应用当时读到的东西。有了 AI,这不再有意义;世界正在变成一堆肉代理,提示最新的 LLM 来解决出现的任何问题或情况。

我显然在专业工作中使用 AI,但不是为了处理我的“爱好”项目(比如我发布在这个博客上的东西)。我需要澄清最后一点:例如,当我在逆向工程某样东西并遇到一个执行数学运算的函数时,我会使用它(比如在上一篇文章中我用它来识别 Reed-Solomon),或者自动化一个我已经手动执行过的任务(例如,在这篇文章中,我用它基于我执行过并记录在日志中的命令构建了一个脚本)。我非常享受我的爱好,我不想让自动化夺走那些我撞墙试图弄明白某件事的时光,或者当事情豁然开朗、我有了“尤里卡”时刻的那种愉悦感。

大约十年前,我记得读过 Matasano 的 cryptochallenges,从那以后这句话就一直印在我脑海里:从统计上破解重复密钥 XOR(“Vigenere”)显然是一个学术练习,一个“Crypto 101”的东西。但“知道如何”破解它的人比实际能破解它的人更多。

我想现在我们生活的世界里,每个人都知道如何让 LLM 去黑某个东西,而不是真正知道如何黑某个东西。

像往常一样,欢迎在我们的推特 @AdeptsOf0xCC 上给我们反馈(虽然我丢了密码,再也无法访问那个账号了 :D)。

来源:Hacker News · adepts.of0x.cc