逆向 Eufy HomeBase 2:固件提取、Wi-Fi 凭证算法还原与 UART 解锁
A story of WiFi secrets, UART lockdowns and a damaged flash memory chip
作者拆焊 flash 存储芯片提取了 Eufy HomeBase 2(固件 3.4.2.2h)固件,逆向还原出 OCEAN_XXXXXX 管理 Wi-Fi 密码算法——基于 I2C 读取的 17 字节序列号与设备序列号做 HMAC SHA256,再经 url-safe base64 编码。
把逆向流程拆为固件提取、算法还原、启动绕过、运行时重建四步,配合可复用的 shell 脚本与重刷写方法,可作为嵌入式设备固件安全分析的入门参考。
亲爱的团猫头鹰契,正如我们在上一篇文章中所宣布的,我们敬爱的 Debug 神已降下旨意,将那充满操作码的神圣光芒洒向他卑微的仆人,我们在这座圣殿里又有一篇新的讲道文。请坐下,聆听我们的一只猫头鹰如何与他的姻亲兄弟一同剖析 Eufy 生态系统的下一个组件:Eufy HomeBase 2。
在祭坛前的祷告,又名免责声明
我们将分析重点放在 Eufy Homebase 2 而不是 Homebase 3 上,原因正如我们在从你的门铃到你的家庭网络中所解释的:它是我所在城市部署的版本,而且也是最容易买到的版本。我们这里分析的固件版本为 3.4.2.2h (2026-06-05 18:45:48)
这是我第一次接触与"硬件"相关的东西。我以前一直在从互联网上下载的固件中做漏洞挖掘,所以请原谅我所犯下的罪过
重要!!! 在继续阅读本文之前,请务必先阅读上一篇文章。
目录
与上一篇文章一样,本文将涉及各种各样的主题;因此,本会的部分成员可能对某些章节没有兴趣,希望直接跳到他们最感兴趣的内容。这正是我们呈上这份神圣贡品的原因:一份目录。
- 0x00 固件提取。描述如何提取固件(以及我们如何损坏了部分闪存)。
- 0x01 识别 Wifi 凭据生成算法。逆向工程用于为 OCEAN_XXXXXX 管理 Wifi 生成凭据的算法。
- 0x02 对 UART 锁定的"橡皮管"破解。刷入一个带后门的 U-Boot 镜像,通过 UART 获取 shell。
- 0x03 为调试而进行的文件系统整理。恢复镜像的功能有限,因此需要一些文件系统的奇技淫巧才能运行主程序。
- 0x04 结语。分享我在 AI 时代对撰写文章缺乏热情的心境。
在本系列的上一章中,我曾提到我们之所以只能提取门铃的固件,是因为 Homebase 的闪存直接焊在了电路板上,并没有暴露出任何可供我们挂接测试钩子以导出固件的引脚。起初我们不想将其拆焊(但正如我稍后将解释的,最终我们不得不这样做),所以我不得不等待以获取额外的硬件。Craig S. Blackie 给我指出了适合 Winbond 25Q256JWEQ (WSON8 8.6mm) 尺寸的弹簧夹。夹子一到,我就搭建了一个可以想象到的最简陋的装置——用橡皮筋固定夹子,然后使用 Raspberry Pi 的 SPI0 接口进行读取,就像我们对门铃所做的那样。
我进行了三次读取,三次的校验和都各不相同,这通常表明存在问题。检查了转储文件后,我很快意识到这些数据是无用的垃圾,完全无法使用。与门铃不同的是,在这种情况下,读取操作所提供的电力还会激活板上与存储器交互的其他组件;因此,为了提取固件,我们不得不拆焊芯片,并在板外进行读取。
我的姐夫 (Óscar Lestón) 是那个勇敢地将闪存芯片拆焊下来的人(我连尝试都不敢):
虽然拆下后内存模块看起来很糟糕,但它几乎没什么损坏……
最后,我们用 Raspberry Pi SPI 0 复制了同样的配置来转储固件。这次三次提取的数据校验和相同,并且包含“有趣的数据”。唯一的问题是……有一个 64 KB 的数据块被擦除/无法读取,很可能是因为我们在拆芯片时把它损坏了。
大部分固件可以使用 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。
首先,我们检查了 Wi-Fi 网络的 ESSID 和密码在固件中的位置。一个简单的 grep 就足以识别出包含 Wi-Fi 名称和凭据的 NVRAM 变量(用于注册门铃的临时 Wi-Fi 和管理 Wi-Fi):
➜ 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
然后,我们尝试了更广泛的搜索,试图找出与 Wi-Fi 设置相关的二进制文件和/或脚本:
➜ 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:
#!/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
对符号做一次快速检查,我们发现了许多与 Wi-Fi 相关的有趣内容,特别是那个诱人的 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 的密码,这与 Wi-Fi 无关。Wi-Fi 部分从 0x0053b690 开始,它检查来自 base_param 的一个参数(我猜是设备类型或型号)与 0x0C 的比较结果。这部分很关键,因为如果该值不等于 0x0C,那么它会使用 MAC 地址(右分支),而如果匹配,则使用 z8GetSN() 返回的内容(左分支)。我们属于 z8GetSN() 的情况。
z8GetSN() 函数通过 i2c 通信,并发送命令 0xC1(i2c 部分在 z8SendData() 中),然后从响应中读取 0x11 个字节。
然后字节被转换成 32 字节字符串,并送入一个名为 mac_func() 的函数,该函数还接收 Homebase 序列号 (T8010...):
此 mac_func 调用一个辅助函数,对前面提到的数据执行 HMAC SHA256,其中“消息”是 z8GetSN 返回的值,“密钥”是序列号:
该结果被一个函数用于将信息编码为文本。
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 来更改:
因此,要复制密码,唯一的未知数据是 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”键后触发执行 /sbin/login.sh 脚本(BusyBox 的 askfirst 操作会显示提示“Please press Enter to activate this console”)。该脚本很简单:
#!/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)还是退出,从而进入循环。
我们没有找到直接启用该变量并绕过锁定的方法。改变启动模式以启用“debug”的方法有多种,但它们都超出了我们的能力范围,因为:
- 其中一种方法基于文件系统中是否存在某个文件。因为要能够放入文件我们需要访问权限,这就成了先有鸡还是先有蛋的问题。
- 另一种方法涉及一个基于 ARP 的复杂魔术协议,以及由固件中不存在的私钥签名的命令。是的,他们在 ARP 上封装自己的协议,并交换魔术签名数据包以远程启用调试。我不知道他们抽了什么风。
在这一点上,我想到如果我们无法绕过它是在生产模式还是调试模式下启动的检查,为什么不彻底消除该检查,直接跳转到 shell?
固件中没有任何加密内容,所以修改它以允许我们通过串行端口直接访问应该是直接的。问题是……你还记得我提到过它“几乎没损坏”吗?好吧,我们在 0x500000(这是映像的加载地址)搞砸了 64kbs。
起初,我以为我们笨拙的双手毁掉了这个项目,使用真实设备配合调试器进行动态分析的设想也即将落空(我们仍然可以选择尝试模拟某些二进制,但拥有原始硬件的访问渠道总是要容易得多)。幸运的是,在进一步分析固件布局之后,我们发现了一个小得多的「恢复镜像(recovery image)」,它在 0xe70000 处仍完好无损。所以,由于「正常」镜像已损坏,引导加载程序只能加载这个恢复镜像,而我们可以对它进行修改以解除 UART 锁定。
「恢复镜像(recovery image)」是一个 带 64 字节头部的 U-Boot 镜像:
| 头部偏移 | 长度 | 字段 | 原始恢复值 |
|---|---|---|---|
0x00 | 4 | Magic | 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 | OS、架构、类型、压缩 | 05 05 02 03 |
0x20 | 32 | 镜像名称区 | 保留,包括供应商使用的字节 |
实际的文件系统被多层不同的压缩层层层包裹(套娃结构),可以在下图中直观看到:
文件系统解包后,我做了最小化的修改,让我们能直接跳转到具有 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'
做完本地测试,验证修改后的恢复镜像已被正确修改并且能够正确解包/加载之后,我们继续在闪存芯片中覆盖它。为了禁用 文档中提到的写保护,按照了下图所示的连接图:
写入新的恢复镜像:
随后我们执行了三次读取以验证该过程是否正确完成。之后,芯片被再次焊回设备电路板上:
芯片焊好后,我再次使用 USB-TTL 将笔记本连接到设备。它先尝试加载正常镜像,但加载失败,随后跳转到了我们植入后门的恢复镜像,加载完成后……我们拿到了 shell!!
0x03 为调试目的对文件系统进行整理
我们终于有了一个 shell;唯一的问题是恢复镜像非常有限,且文件系统没有挂载,因此缺少正确调试系统所需的二进制和库。此外,内核还缺少某些模块——例如负责处理 WiFi 接口的模块,或用于生成 WiFi 密钥的 I2C 接口模块。
默认镜像中可用的命令集非常有限,但至少还有 wget 这样的工具,它允许我们从笔记本向设备传输文件。我搭建了一个简陋的实验环境,用的是我在 2015 年从 Telefónica 拿到的一台旧 TP-Link 路由器。
我用 BusyBox 编译了一个新版本,加入了许多原版中没有的额外命令。使用这个新版 BusyBox,我开始尝试创建一个「staged(分阶段)」文件系统,并将所有路径绑定到这个「staged」文件系统上。我需要执行一些服务,这些服务必须在负责主要服务的大 ELF(homeapp_security,也就是包含 WiFi 逻辑的那个二进制文件)运行之前启动;同时由于内存限制,我还不得不修改一些内核参数,例如 overcommit_ratio。这是一个长达三小时的反复试错过程,直到我最终能够运行主二进制文件并与之交互。
由于这是一个每次使用设备都需要重复的过程(因为恢复镜像中的文件系统不是持久化的,所以每次重启后我都得恢复所有内容),我把 picocom 的日志分享给了 Codex,请它准备一个一键脚本,来重复我之前所做的相同步骤(并去掉那些没用的部分)。说实话,那天晚上玩过 Homebase 之后的第二天早上,我几乎都记不清自己做了什么,所以有了日志真是太好了,可以用来重建并自动化整个过程。永远记录所有日志,它们迟早会救你一命。
homeapp_security 运行之前的所有操作流程图如下:
重建文件系统的两个最重要的脚本如下所示:
#!/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 并将其上传到设备上,以便开始调试 :)
当然,要让二进制文件正常工作还需要做更多的配置修改(例如,我不得不桥接一个 eth 接口来创建 br0,使其处于对等端所需的范围内 —— 长话短说:它使用一个带 MAC 地址的对等表来拒绝或接受通信),但有了调试器一切就容易多了。只需启动进程,检查哪里出错,修正它,或者直接 hook 它,给它提供它所期望的值以继续流程。
0x04 写在最后
随着 LLM 的出现,我已经完全失去了发表技术内容的热情。我从 2007 年起就在网上发表文章,分享我学到或发现的东西,在各种博客和论坛上使用过几十个不同的别名。差不多有 20 年了。而如今,我已经完全不想分享我的经历了。
一方面,如果没人愿意读,那写作也就失去了价值。如今,只有少数人会真的停下来阅读一篇文章;大多数人要么让 AI 给他们做个总结,要么只是随便扫一眼,根本不会留意。以前曾有一个生态系统,人们会花时间检查他们的 RSS 订阅(唉,我老了),阅读他们喜爱的博客或作者的文章。这方面的第一次重大打击来自社交媒体,即时性和「在 Twitter 上刷分」成为了内容创作的主要驱动力。
还有这样一个事实:如今绝大多数发表的文章都是由 AI 直接生成的。即便是那些有趣的研究文章或漏洞分析报告,也是由自动化的"织机"批量产出,使用的语言仿佛被切除了额叶一般,每次我试着阅读都会气得我血压飙升。看起来人们很乐意狼吞虎咽这些毫无营养的垃圾。我想大概是因为这呼应了前面那一点。我用"蹩脚英语"写作(没错,虽然我已经在一家英国公司工作了五年,我的英语依旧糟糕透顶),但哪怕让我选上一百万次,我也宁愿读一篇由真人用"蹩脚英语"写就的文章,也不愿再去看 ChatGPT 那些千篇一律的结构和陈词滥调铺天盖地的重复。
最后一根棺材钉是这样一个事实:在 2026 年,继续为人类读者发布技术内容到底还有没有意义?过去,你会阅读大量资料(甚至将其加入书签以备后用),因为它真的有用。也许你正在琢磨某个产品的固件,而别人早已做过探索;你可以找到那篇文章,沿着他们开辟的道路前进。漏洞分析也是一样:你会阅读别人的工作,这样在遇到类似场景时你就能复现。我说的不只针对具体产品;我说的是方法论:别人是如何解决某个问题的,让你多年后遇到同样的情况时,能够应用当时读到的东西。有了 AI,这套逻辑已经不再成立;世界正在变成一群充当"肉代理"的人,指挥最新的 LLM 去解决任何冒出来的麻烦或局面。
我显然在专业工作中使用 AI,但不会用它来处理我的"业余爱好"项目(比如我发在这个博客上的东西)。我需要把后面这一点说清楚:举个例子,在我做逆向工程遇到一个执行数学运算的函数时(就像上一篇中我曾用它来识别 Reed-Solomon),或者在我想把一个已经手动完成过的任务自动化时(比方说,就在本文中,我就用 AI 根据我执行过的命令——它们都记录在日志里——构建了一个脚本),我会用到它。我非常享受自己的爱好,我不愿让自动化夺走那些我苦思冥想、撞墙无数次的时光,也不愿让它夺走那个灵光乍现、恍然大悟的瞬间所带来的快乐。
大约十年前,我记得读过 Matasano 的 cryptochallenges,从那以后这句话就一直印在我脑海里:用统计方法破解重复密钥 XOR("Vigenere")显然只是一个学术练习,一个"Crypto 101"级别的东西。但"知道如何"破解它的人,远比真正能破解它的人要多。
我想我们现在就活在这样一个世界里:每个人都知道如何让 LLM 去黑点什么,而并非真正知道如何去黑点什么。
和往常一样,欢迎在我们的 twitter @AdeptsOf0xCC 给我们反馈(虽然我把密码弄丢了,已经无法再访问那个账号 :D)。
来源:Hacker News · adepts.of0x.cc