2. 固件中硬编码凭据的自动化扫描
扫描目标与凭据类型
固件解包后,根文件系统通常位于 squashfs-root/ 目录。硬编码凭据常出现在以下位置:
/etc/passwd、/etc/shadow:系统账户与口令哈希/etc/config/、/etc/*.conf:服务配置中的明文密码/usr/bin/、/usr/sbin/:二进制中嵌入的 API Key、Token/www/、/web/:Web 前端 JS 中的密钥/etc/init.d/:启动脚本中的硬编码凭据
扫描脚本设计
脚本采用“文件遍历 → 正则匹配 → 上下文提取 → 风险分级”四阶段处理。核心匹配规则如下:
| 凭据类型 | 正则模式 | 风险等级 |
|---|---|---|
| 明文密码 | (password|passwd|pwd)\s*[=:]\s*["']?([^"'\s]{4,}) | 高 |
| API Key | (api[_-]?key|apikey)\s*[=:]\s*["']?([A-Za-z0-9]{16,}) | 高 |
| Token | (token|secret)\s*[=:]\s*["']?([A-Za-z0-9_\-]{12,}) | 中 |
| 私钥文件 | -----BEGIN.*PRIVATE KEY----- | 严重 |
| Shadow 哈希 | ^[^:]+:\$[0-9]\$[^:]+: | 严重 |
完整扫描脚本
#!/usr/bin/env python3
import os, re, json, sys
RULES = [
("CRITICAL", "私钥文件", re.compile(rb"-----BEGIN.*PRIVATE KEY-----")),
("CRITICAL", "Shadow哈希", re.compile(rb"^[^:]+:\$[0-9]\$[^:]+:", re.M)),
("HIGH", "明文密码", re.compile(rb"(?:password|passwd|pwd)\s*[=:]\s*[\"']?([^\"'\s]{4,})", re.I)),
("HIGH", "API Key", re.compile(rb"(?:api[_-]?key|apikey)\s*[=:]\s*[\"']?([A-Za-z0-9]{16,})", re.I)),
("MEDIUM", "Token/Secret", re.compile(rb"(?:token|secret)\s*[=:]\s*[\"']?([A-Za-z0-9_\-]{12,})", re.I)),
]
SKIP_EXT = {".png", ".jpg", ".gif", ".ico", ".woff", ".ttf", ".so", ".ko"}
MAX_SIZE = 2 * 1024 * 1024 # 跳过大于2MB的文件
def scan_file(path):
findings = []
try:
if os.path.getsize(path) > MAX_SIZE:
return findings
with open(path, "rb") as f:
data = f.read()
except (OSError, PermissionError):
return findings
for level, label, pattern in RULES:
for m in pattern.finditer(data):
# 提取匹配行上下文
start = data.rfind(b"\n", 0, m.start()) + 1
end = data.find(b"\n", m.end())
if end == -1:
end = len(data)
line = data[start:end].decode("utf-8", errors="replace").strip()
findings.append({
"level": level,
"type": label,
"file": path,
"line": line[:200],
"offset": m.start()
})
return findings
def scan_root(root_dir):
all_findings = []
for dirpath, dirnames, filenames in os.walk(root_dir):
for fn in filenames:
ext = os.path.splitext(fn)[1].lower()
if ext in SKIP_EXT:
continue
fp = os.path.join(dirpath, fn)
all_findings.extend(scan_file(fp))
return all_findings
def main():
if len(sys.argv) < 2:
print("用法: python3 scan_creds.py <squashfs-root目录>")
sys.exit(1)
root = sys.argv[1]
findings = scan_root(root)
# 按风险等级排序输出
order = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2}
findings.sort(key=lambda x: order.get(x["level"], 9))
report = {"total": len(findings), "findings": findings}
print(json.dumps(report, indent=2, ensure_ascii=False))
# 摘要统计
from collections import Counter
c = Counter(f["level"] for f in findings)
print(f"\n=== 风险摘要 ===", file=sys.stderr)
for lv in ["CRITICAL", "HIGH", "MEDIUM"]:
print(f" {lv}: {c.get(lv, 0)} 条", file=sys.stderr)
if __name__ == "__main__":
main()
运行与结果判读
对已解包的固件根目录执行:
python3 scan_creds.py ./squashfs-root/ > creds_report.json
输出 JSON 中每条记录包含风险等级、凭据类型、文件路径、匹配行内容和字节偏移。判读要点:
CRITICAL级发现(私钥、shadow 哈希)应立即标记为阻断项,说明固件存在可直接利用的凭据泄露。HIGH级明文密码和 API Key 需人工确认是否为默认凭据或调试凭据,若是则同样属于高危。- 同一凭据在多个文件中重复出现时,应合并统计并追溯其被哪些服务引用。
- 若扫描结果为空,需检查是否因文件权限、压缩格式或编码问题导致漏扫,可先用
grep -r "password" ./squashfs-root/交叉验证。
对二进制文件中的凭据,可先用
strings 提取可打印字符串再送入正则匹配,避免直接读取大文件导致内存问题。脚本中 MAX_SIZE 和 SKIP_EXT 可根据固件实际情况调整。验证与交叉检查
脚本输出后,用以下命令交叉验证是否存在漏扫:
# 快速grep验证
grep -rn "password\|passwd\|api_key\|secret" ./squashfs-root/etc/ 2>/dev/null | head -20
# 检查shadow文件是否被正确识别
cat ./squashfs-root/etc/shadow 2>/dev/null | head -5
# 统计各类型发现数量
python3 -c "
import json
d = json.load(open('creds_report.json'))
from collections import Counter
print(Counter(f['type'] for f in d['findings']))
"
扫描结果中的匹配行可能包含误报(如代码注释中的示例密码、文档中的占位符)。建议对每条 HIGH 及以上发现人工确认其是否为真实可用凭据,再纳入最终风险清单。