1. 问题场景:当嵌入式设备“锁”在门外时
作为一名嵌入式开发工程师,或者负责设备运维的技术人员,你很可能遇到过这种尴尬又紧急的情况:一台正在运行Linux的嵌入式设备,比如工控机、路由器、智能网关或者某个定制化的硬件盒子,你需要登录进去进行调试或维护,但root密码却没人记得了。可能是设备出厂后默认密码被改过但未记录,也可能是前任同事留下的“遗产”,总之,你现在被挡在了系统大门之外。
这不仅仅是“忘记密码”那么简单。与个人电脑不同,嵌入式设备往往没有显示器、键盘和鼠标(即没有“控制台”),通常只能通过串口或网络(SSH)访问。当密码未知时,这些访问通道就都失效了。更棘手的是,许多嵌入式系统为了安全或空间考虑,默认不提供像桌面Linux那样的“单用户模式”或“恢复模式”的便捷入口。直接对存储介质(如eMMC、SD卡、NAND Flash)进行物理操作,又需要专业工具和知识,风险很高。
因此,掌握在不知道root密码的情况下,合法、安全地重置密码并进入系统,是一项非常实用的应急技能。它不涉及任何系统漏洞利用,而是基于Linux系统启动流程和文件系统权限的基本原理,通过介入启动过程来临时获得修改关键文件(如密码文件)的权限。下面,我将以最常见的嵌入式Linux环境为例,手把手带你走通几种主流方法,并深入讲解其背后的原理与避坑要点。
2. 核心思路:介入Linux启动流程
在深入具体操作前,我们必须理解其核心原理。Linux系统启动后,最终会由init进程(可能是systemd,sysvinit,busybox init等)来启动各项服务并准备登录环境。/etc/shadow文件存储了加密后的用户密码,而root用户的密码就存放在其中。在系统正常运行时,这个文件受到严格的权限保护,普通方式无法直接修改。
我们的突破口在启动流程。如果能在内核加载完毕、但init进程尚未完全按照既定配置执行之前,获得一个具有root权限的交互式Shell,那么我们就可以在这个“黄金时间窗口”内,直接修改/etc/shadow文件。
对于嵌入式Linux,主要有三种介入方式,其选择取决于你的物理访问权限和设备的启动配置:
- 通过串口终端(Serial Console)修改内核启动参数:这是最经典、最可靠的方法,前提是你的设备有可用的串口,并且Bootloader(如U-Boot)支持交互式命令。
- 通过外部存储介质(SD/USB)引导一个临时系统:当无法通过串口访问Bootloader,或者Bootloader被锁定时,可以考虑从SD卡或U盘启动一个小的Linux系统(如BusyBox),然后挂载设备上原有的根文件系统进行修改。
- 在系统运行时利用Initramfs:有些系统在启动初期会使用一个初始内存文件系统(Initramfs),如果这个Initramfs内置了Shell并且允许交互,我们可以在启动时中断其流程。
接下来,我们将重点讲解前两种最实用的方法,并详细分析第三种方法的适用场景与风险。
3. 方法一:通过U-Boot串口终端修改启动参数
这是嵌入式领域最标准、最推荐的解决方案。U-Boot是嵌入式设备上最流行的开源Bootloader。它负责初始化硬件,加载内核和设备树,并传递启动参数。我们的目标就是在U-Boot的交互式命令行中,临时修改传递给内核的启动参数。
3.1 前期准备与连接
首先,你需要准备以下硬件和软件:
- 硬件:目标嵌入式设备、USB转TTL串口模块(如CH340、CP2102、FT232等)、杜邦线(母对母)。
- 软件:串口终端软件,如Windows下的Putty、MobaXterm,macOS/Linux下的
screen、minicom或picocom。 - 关键信息:目标设备的串口引脚定义(通常是TX、RX、GND),以及串口波特率(常见的有115200、9600、57600等)。这些信息可以从设备原理图、硬件手册或主板丝印上找到。
连接步骤:
- 断电:确保目标设备完全断电。
- 连接串口线:将USB转TTL模块的GND连接到设备的GND引脚。将模块的TX引脚连接到设备的RX引脚,模块的RX引脚连接到设备的TX引脚。(注意:TX接RX,RX接TX,交叉连接)。
- 连接电脑:将USB转TTL模块插入电脑。
- 打开终端软件:在电脑上打开串口终端软件,选择正确的串口端口(如COM3、/dev/ttyUSB0),设置波特率(通常115200)、数据位(8)、停止位(1)、无校验位、无流控。
- 上电:给目标设备上电,并立刻在终端软件中连续按回车键或指定的中断键(如U-Boot通常是任意键,有些设备是空格键或
Ctrl+C)。
如果成功,你将看到U-Boot的启动提示符,通常是=>或U-Boot>。
注意:有些生产设备可能会在U-Boot中设置密码或禁用命令行交互。如果按中断键无效,直接进入了内核启动,说明此路可能不通,需要尝试其他方法。
3.2 中断启动与修改内核命令行的艺术
成功进入U-Boot命令行后,我们通常使用printenv命令查看当前环境变量。其中,bootargs或bootcmd变量包含了传递给内核的启动参数。
我们的核心策略是:让内核在启动后,不执行默认的init程序,而是直接执行一个Shell。这通过修改bootargs中的init参数来实现。
标准操作流程:
查看当前启动命令:
=> printenv bootargs bootargs=console=ttyS0,115200 root=/dev/mmcblk0p2 rw rootwait这个例子显示,内核启动参数设置了控制台和根文件系统位置,但没有指定
init,系统会使用默认的/sbin/init。临时修改启动参数: 我们使用
setenv命令来修改bootargs,添加init=/bin/sh或init=/bin/bash。这告诉内核,启动后不要运行正常的init系统,直接运行一个Shell。=> setenv bootargs 'console=ttyS0,115200 root=/dev/mmcblk0p2 rw rootwait init=/bin/sh'关键点:这里使用了单引号将整个参数字符串括起来,确保它被作为一个整体传递。根文件系统设备(如
/dev/mmcblk0p2)需要根据你的实际情况修改,可以通过printenv查看原设置或使用mmc list等命令查看存储设备分区。启动内核: 修改后,使用
boot或run某个具体的启动命令(如run bootcmd)来启动内核。=> boot
3.3 进入单用户Shell与密码重置实操
如果一切顺利,内核启动后,你会直接获得一个#提示符的Shell,并且拥有root权限。此时,根文件系统通常是以**只读(ro)**方式挂载的,这是第一个坑。
重新挂载根文件系统为可写:
# mount -o remount,rw /执行这条命令,将
/文件系统重新挂载为读写模式。如果遇到错误,可能需要指定文件系统类型,如mount -o remount,rw -t ext4 /。使用
passwd命令修改密码:# passwd root然后输入两遍新密码。这是最标准、最安全的方式,因为它会自动处理
/etc/shadow文件的加密和更新。(备选)直接编辑shadow文件: 如果
passwd命令不可用(比如BusyBox精简版未包含),我们可以手动编辑/etc/shadow文件。首先,删除root用户的密码哈希值,使其密码为空。# vi /etc/shadow找到以
root:开头的一行,将其第二个字段(两个冒号之间的加密密码)删除。例如,将:root:$6$salt$hashedpassword:19215:0:99999:7:::修改为:
root::19215:0:99999:7:::保存退出。这样,root用户就可以无需密码登录了。注意:这是一个临时应急措施,进入系统后务必立即用
passwd命令设置新密码。同步与重启: 修改完成后,执行
sync命令确保数据写入存储。然后重启设备。# sync # reboot -f
3.4 潜在问题与深度排查
问题1:
init=/bin/sh启动后,提示can‘t run ’/bin/sh‘: No such file or directory- 原因:根文件系统里可能没有
/bin/sh,或者它链接到的BusyBox不在那个路径。嵌入式系统常用BusyBox,其Shell可能在/bin/busybox或/bin/ash。 - 解决:尝试
init=/bin/bash、init=/bin/ash或init=/bin/busybox sh。你可以先不修改启动参数,让系统正常启动一次(虽然进不去),通过串口日志观察系统调用的init路径是什么。
- 原因:根文件系统里可能没有
问题2:根文件系统挂载失败,提示
VFS: Unable to mount root fs- 原因:
root=参数指定的设备节点不对,或者文件系统类型内核不支持。 - 解决:在U-Boot中,使用
mmc list、scsi scan、usb start等命令查看可用的存储设备。然后尝试不同的设备节点,如/dev/mmcblk0p1、/dev/mmcblk1p2、/dev/sda1等。必要时,可能还需要在bootargs中指定文件系统类型,如rootfstype=ext4。
- 原因:
问题3:系统仍然启动了正常的init进程
- 原因:有些内核或Bootloader会忽略命令行中的
init=参数,或者系统使用了initramfs,其内部的脚本硬编码了init路径。 - 解决:尝试更强大的参数组合。例如,在
init=/bin/sh后面加上single或-s参数(单用户模式),或者直接rd.break(针对systemd + initramfs)。例如:init=/bin/sh single。这需要你对系统的init体系有一定了解。
- 原因:有些内核或Bootloader会忽略命令行中的
4. 方法二:从外部介质启动临时系统
当串口方法行不通时(比如Bootloader被锁定,或者设备根本没有引出串口),我们可以考虑“借壳生蛋”——从SD卡或U盘启动一个独立的、小型的Linux系统,然后用这个系统挂载嵌入式设备内部存储上的根文件系统,直接修改其/etc/shadow文件。
4.1 制作可启动的救援介质
你需要一台x86的Linux主机(或虚拟机)来制作启动介质。
- 准备镜像:下载一个轻量级的、支持你目标设备架构的Linux镜像。对于ARM设备,Raspberry Pi OS Lite(适用于树莓派类)或Armbian(适用于各种ARM开发板)的镜像非常合适。更通用的选择是BusyBox制作的极小根文件系统镜像,或者Buildroot构建的定制镜像。
- 识别设备:将SD卡或U盘插入主机。使用
lsblk或fdisk -l命令确认其设备名,例如/dev/sdb。务必确认无误,否则可能格式化错误磁盘! - 写入镜像:使用
dd命令写入。假设镜像文件为rescue.img,SD卡为/dev/sdb:
写入完成后,SD卡上应该有两个分区:一个启动分区(FAT32,包含内核和DTB),一个根文件系统分区(通常是ext4)。sudo dd if=rescue.img of=/dev/sdb bs=4M status=progress conv=fsync
4.2 在临时系统中定位并修改目标系统
将制作好的SD卡插入目标设备,并配置设备从SD卡启动(可能需要修改跳线帽或进入Bootloader选择启动顺序)。启动后,你将进入临时系统。
挂载目标系统的根文件系统:
- 首先,使用
fdisk -l或lsblk找到嵌入式设备内置的存储(如eMMC、NAND)。它可能显示为/dev/mmcblk0(SD/eMMC控制器)或/dev/mtdblock0(MTD设备)。 - 查看分区:
fdisk -l /dev/mmcblk0。通常,根文件系统在第二个或第三个分区(如/dev/mmcblk0p2)。 - 创建挂载点并挂载:
mkdir /mnt/original_root mount /dev/mmcblk0p2 /mnt/original_root- 如果挂载失败,提示文件系统错误,可以尝试指定文件系统类型:
mount -t ext4 /dev/mmcblk0p2 /mnt/original_root。
- 首先,使用
chroot或直接编辑:- 方法A(推荐,更干净):使用
chroot切换到目标系统的环境,然后使用其自带的passwd命令。
# 挂载必要的虚拟文件系统 mount -t proc proc /mnt/original_root/proc mount -t sysfs sys /mnt/original_root/sys mount -o bind /dev /mnt/original_root/dev # 执行chroot chroot /mnt/original_root /bin/sh # 现在你已经在目标系统的上下文里了 passwd root # 修改密码后退出chroot exit- 方法B(直接编辑):如果不支持
chroot或想快速操作,可以直接编辑shadow文件。
vi /mnt/original_root/etc/shadow同样,清空root用户的密码字段(删除两个冒号间的哈希值)。
- 方法A(推荐,更干净):使用
清理与重启:
# 卸载文件系统 umount /mnt/original_root/proc umount /mnt/original_root/sys umount /mnt/original_root/dev umount /mnt/original_root sync # 关机或重启,并移除SD卡,让设备从内部存储启动 reboot
4.3 此方法的局限性与应对策略
- 架构兼容性:你制作的救援镜像必须与目标设备的CPU架构(ARMv7, ARMv8/AArch64, MIPS等)完全一致。用x86的镜像启动ARM设备是绝对不可能的。
- 驱动兼容性:临时系统的内核需要包含目标设备存储控制器(如eMMC/SD控制器、USB控制器)的驱动,否则无法识别内置磁盘。
- 文件系统兼容性:临时系统需要支持目标根文件系统的类型(ext4, squashfs, jffs2, ubifs等)。
- 应对策略:最稳妥的方式是,直接使用该嵌入式设备原厂或社区提供的系统镜像来制作救援盘。例如,对于树莓派,就用树莓派官方的Lite镜像;对于某款开发板,就用其SDK里生成的系统镜像。这样能最大程度保证兼容性。
5. 方法三:利用Initramfs进行突破
Initramfs(Initial RAM File System)是一个临时的根文件系统,它在内核启动后、挂载真实根文件系统之前被加载到内存中。它的任务是准备好挂载真实根文件系统所需的环境(比如加载特殊的驱动、解密磁盘等)。如果这个Initramfs内置了Shell并且允许交互,我们就有机会。
5.1 识别与中断Initramfs流程
当系统从串口启动时,观察内核日志。如果你看到类似 “Running /init” 或 “Welcome to Buildroot” 后,出现一个 “/ #” 或 “(initramfs)” 的提示符,并且停顿了几秒,说明系统进入了Initramfs的Shell。如果没有自动进入,可以在内核启动早期(出现Initramfs相关日志时)快速按下Ctrl+C或回车键,有时可以中断其脚本执行,掉入Shell。
5.2 在Initramfs中操作
一旦进入Initramfs的Shell,你通常已经拥有root权限,并且真实的根文件系统设备可能已经被识别但尚未挂载。
- 检查存储设备:使用
ls /dev/mmcblk*或ls /dev/sd*或cat /proc/mtd查看块设备或MTD设备。 - 手动挂载根分区:
mkdir /new_root mount /dev/mmcblk0p2 /new_root - 执行切换:现代Initramfs使用
pivot_root或switch_root来切换到真实根文件系统。但我们可以“欺骗”系统。- 一种方法是直接
chroot到/new_root,然后修改密码,操作同方法二。
mount -t proc proc /new_root/proc mount -t sysfs sys /new_root/sys mount -o bind /dev /new_root/dev chroot /new_root /bin/sh passwd root exit- 另一种方法是,直接覆盖Initramfs的
/init脚本,让它执行我们想要的命令(比如直接启动Shell而不挂载真正的根)。但这需要你对Initramfs的构造比较了解,风险较高。
- 一种方法是直接
5.3 风险与注意事项
- 并非所有系统都有:很多嵌入式系统为了精简,编译内核时没有启用Initramfs,或者使用了极其精简的、无交互Shell的Initramfs。
- 行为不可预测:不同发行版、不同构建系统(Yocto, Buildroot)生成的Initramfs,其内部脚本和行为差异很大。强行中断可能会破坏启动流程,导致无法正常挂载根文件系统。
- 临时性:在Initramfs中所做的任何修改,如果只存在于内存中,重启后会失效。你必须确保修改的是已经挂载上来的、真实的根文件系统分区,并且执行了
sync。
6. 安全考量与后续加固
通过上述任何一种方法重置密码后,你都能重新获得系统的控制权。但这本身是一个巨大的安全漏洞,因此,在解决问题后,必须进行安全加固。
- 立即修改密码:如果你是通过清空shadow文件字段进入的,进入系统后第一件事就是使用
passwd命令为root设置一个强密码。 - 评估访问控制:
- 串口访问:考虑在U-Boot中设置密码(
setenv bootdelay 0只能防君子,真正需要修改U-Boot源码编译或设置环境变量密码)。对于生产设备,物理上封堵或禁用调试串口是常见做法。 - Bootloader锁定:有些芯片提供安全启动(Secure Boot)功能,可以签名校验内核和Bootloader,防止被篡改。但这需要一套完整的密钥管理体系。
- 文件系统加密:如果数据敏感,可以考虑对根文件系统进行加密(如LUKS)。这样即使通过外部系统挂载,没有密码也无法读取数据。但这会增加启动复杂性,并需要安全存储密钥。
- 串口访问:考虑在U-Boot中设置密码(
- 审计与监控:确保系统日志(
/var/log/auth.log,/var/log/secure)记录所有登录尝试。可以考虑部署入侵检测系统。 - 最小权限原则:避免日常使用root账户。创建具有sudo权限的普通用户进行运维。
7. 总结与最佳实践选择
面对嵌入式Linux设备忘记root密码的困境,选择哪种方法取决于你的物理访问权限、设备配置和技术储备。
- 首选方案(强烈推荐):串口 + U-Boot修改启动参数。只要设备留有串口且Bootloader未锁,这就是最直接、最干净、侵入性最小的方法。它不需要额外的硬件(除了串口线),也不修改存储介质上的任何原始数据(除了密码文件),风险最低。
- 备选方案:从外部SD/USB启动临时系统。当串口不可用时,这是最通用的解决方案。它的成功率很高,但前提是你能制作一个与目标设备兼容的启动镜像。建议平时就为重要的设备准备好一个“救援镜像”SD卡,以备不时之需。
- 探索性方案:利用Initramfs。这个方法可遇不可求,取决于系统构建时的配置。可以作为前两种方法都失败后的尝试,但不要抱太大希望。
在实际操作中,务必保持谨慎。尤其是在使用dd命令或挂载操作时,反复确认设备名(如/dev/sdb,/dev/mmcblk0),避免误操作导致数据丢失。整个过程的核心思想是理解Linux的启动链条,并在链条的适当环节进行干预。掌握这项技能,不仅能解决“忘记密码”的问题,更是深入理解嵌入式Linux系统启动和调试的绝佳实践。