必须以 root 权限运行程序才能读取 /etc/shadow,推荐使用线程安全的 getspnam_r 接口解析密码哈希字段 sp_pwdp,并用 crypt() 进行校验;注意算法兼容性与平台差异。

Shadow文件权限和读取限制必须绕过
普通用户无法直接读取 /etc/shadow,因为它的权限是 000(即 ----------),只有 root 可读。C++ 程序若以非 root 身份运行,fopen("/etc/shadow", "r") 会返回 NULL,errno 为 EACCES。这不是代码写得不对,而是系统强制隔离。真正能解析的前提只有一个:进程已获得 root 权限(例如通过 sudo 启动,或 setuid 二进制,但后者有严重安全风险,不建议)。
- 不要尝试用
fopen+ 错误处理“优雅降级”——没权限就是没权限,降级也没 shadow 数据可降 - 避免在代码里硬编码
/etc/shadow路径;可用getspnam_r接口,它内部自动处理路径和权限检查 - 某些容器或最小化系统(如 Alpine)可能压根不生成
/etc/shadow,而是用/etc/passwd存盐值,需先确认文件存在且格式合规
用 getspnam_r 而不是 getspnam
getspnam 是线程不安全的老接口,返回指向静态缓冲区的 struct spwd*,多线程下极易被覆盖。现代 C++ 项目必须用可重入版本 getspnam_r,它要求调用方提供缓冲区和长度,并显式处理 ERANGE 错误。
- 缓冲区大小不能拍脑袋定:至少预留
1024字节,shadow 行可能含长 Base64 编码的 bcrypt 哈希(如$2y$10$...) - 返回值为
0表示成功;非零值需检查是否等于ERANGE,若是则 realloc 缓冲区后重试 - 注意
struct spwd中sp_pwdp字段才是密码哈希字符串,sp_namp是用户名,别和passwd结构体字段混淆
密码哈希字段解析没有标准 API,只能按分隔符切
/etc/shadow 是冒号分隔的纯文本,但 C++ 标准库不提供原生解析器。sp_pwdp 的值形如 $salt$hash 或 $salt$hash,其中 $ 是算法标识符,后面跟着 salt 和 hash。你无法靠 crypt(3) 反向解密,只能提取字段用于比对或审计。
- 用
std::string::find_first_of(':')定位第 2 个冒号(因第 1 段是用户名,第 2 段才是sp_pwdp) - 避免用
std::stringstream+getline按:拆分——某字段为空(如密码过期字段)会导致错位,应严格按位置计数 - 注意换行符:
getspnam_r返回的sp_pwdp不带尾随换行,但手动读文件时需用std::getline并 trim\n
解析结果不能直接用于密码校验
拿到 sp_pwdp 字符串只是第一步。C++ 本身不提供 crypt(3) 的跨平台封装,Linux 下需链接 -lcrypt,macOS 需用 CommonCrypto 替代,Windows 则基本不可行。更关键的是:crypt() 只接受明文密码 + 完整哈希串(如 $6$salt$...)作为 salt 参数,不是单独传 salt。
立即学习“C++免费学习笔记(深入)”;
- 不要自己实现 SHA-256 或 bcrypt 解密逻辑——既无必要也不安全
- 如果目标是验证用户输入密码,应调用
crypt(input_pass.c_str(), spwd->sp_pwdp),比较返回值是否等于spwd->sp_pwdp - 注意:glibc 的
crypt()不支持 Argon2,遇到$argon2i$开头的哈希会直接失败,需额外集成 libargon2
真正的难点不在“怎么读”,而在“读完之后能不能安全、兼容地用”。哪怕 root 权限和 getspnam_r 都搞定了,哈希算法演进(从 DES 到 yescrypt)、平台差异、以及 crypt() 在不同 libc 上的行为分歧,才是让解析逻辑稳定运行三年以上的障碍。


















