Go 主机安全面试:Linux 文件描述符转移与敏感资源复用检测
主机安全常把“打开敏感文件”和“建立外联”当作检测入口。但进程可以拿到别人已经打开的文件描述符,随后直接读取文件或使用已有 socket,整个过程中未必再次出现对应的 openat 或 connect。
这不是说这些操作绕过了所有安全检查,而是提醒检测研发:资源最初由谁打开,与后来由谁使用,可能不是同一个进程。Go Agent 要把资源使用关系补进攻击链,同时识别正常的服务交接和 IPC,避免看到 fd 转移就告警。
岗位场景
服务 A 打开敏感文件或建立网络连接
-> 进程 B 通过 SCM_RIGHTS 接收 fd,或通过 pidfd_getfd 复制 fd
-> B 使用该 fd 读取、写入或发送数据
-> Go Agent 关联资源、转移方式、调用结果和进程身份
-> 区分正常服务交接、失败尝试和异常资源使用
-> 输出证据等级、攻击链时间线和采集缺口一个典型客户问题是:“告警指向 Web 服务子进程读凭据,但日志里没有它打开凭据文件的记录。”排查时既要考虑采集丢失,也要考虑 fd 继承、复制和转移。本文聚焦后者,不把“没有采到打开事件”直接判为规避审计。
高频面试题
1. fd 是什么?为什么不能用 fd 数字当资源 ID?
简洁答案: fd 是进程描述符表中的索引,类似进程手里的取件号码。同一个数字在不同进程中可以指向不同对象,关闭后也能被复用;fd 指向的 open file description 才保存文件偏移、文件状态标志等信息。
关键知识点:
- “fd 为 7”必须带上进程身份和事件时间才有意义,不能跨进程直接关联。
dup、fork继承和 fd 转移可能让多个描述符指向同一个 open file description,因此可能共享文件偏移和文件状态标志。FD_CLOEXEC是描述符级标志,与底层对象的文件状态标志不同。- 文件路径可能被重命名或删除;
device + inode可以辅助关联,但仍要考虑文件生命周期、inode 复用和命名空间。
Go 落地思路:
- 进程键至少包含
host_id、boot_id、宿主机 PID 和启动时间;容器 PID、namespace ID 作为额外上下文。 - 描述符映射用“进程键 + fd + 分配代次”,看到
close或重新分配时更新;事件缺失时把映射标成不确定。 - 文件对象保存采集时的设备号、inode 和路径快照;socket 保存网络命名空间及可取得的对象标识。不要声称这些字段单独就能证明共享 open file description。
2. SCM_RIGHTS 和 pidfd_getfd 有什么区别?
简洁答案: SCM_RIGHTS 通过 Unix socket 的控制消息传递已打开的 fd,需要发送方配合;pidfd_getfd 通过目标进程的 pidfd 和目标 fd,在调用进程中复制出一个 fd,不需要目标主动发送,但必须通过权限检查。
| 对比项 | SCM_RIGHTS | pidfd_getfd |
|---|---|---|
| 入口 | sendmsg/recvmsg 等消息接口的辅助数据 | 系统调用,参数包括 pidfd、目标 fd 和 flags |
| 是否需要目标配合 | 需要发送方发送 fd | 不需要目标配合 |
| 主要检查边界 | IPC 可达性、对端身份、应用协议与适用的内核安全检查 | PTRACE_MODE_ATTACH_REALCREDS 权限检查 |
| 新描述符从哪里取得 | 接收端控制消息中的 fd 数组 | 成功调用的返回值 |
| 原来的 fd 是否消失 | 不会自动从发送端移除 | 不会从目标进程移除 |
关键知识点:
SCM_RIGHTS传递的是对资源的引用,接收端得到的 fd 数字通常不同于发送端。pidfd_getfd从 Linux 5.6 开始提供,当前 flags 必须为 0;成功返回的描述符带FD_CLOEXEC。- 两者都不是天然恶意行为。服务热重启、特权分离、浏览器沙箱和诊断工具都有合理使用场景。
Go 落地思路:
- 将
transfer_method标成scm_rights、pidfd_getfd或inherit,保留证据来源。 - 成功的
pidfd_getfd事件中,调用方是新持有者,目标进程是原持有者;SCM_RIGHTS的发送方和接收方则分别取自发送、接收证据。 - 保留调用返回值、错误和目标 fd,不用“出现 API”替代“成功取得资源”。
3. 拿到 pidfd 就能任意复制别人的 fd 吗?
简洁答案: 不能。pidfd 提供对进程对象的引用,不是任意访问该进程资源的授权。pidfd_getfd 有独立的 ptrace 访问权限检查,还会受到相关安全策略限制。
关键知识点:
- 同 UID 不保证调用成功,还涉及目标的 dumpable 状态、能力、用户命名空间和 LSM/Yama 等条件。
- 成功获得 pidfd,不代表后续操作都被允许;目标 fd 关闭、进程退出也会导致失败。
- 不能把所有
EPERM都归因于 Yama;seccomp 等也可能拒绝系统调用。 - 返回
EBADF可能是 pidfd 无效,也可能是目标 fd 不再存在,排障要结合参数和前序事件。
Go 落地思路:
- 区分“尝试”“复制成功”“后续使用已观察”三个状态。失败尝试可以作为链路线索,但不能写成凭据已经泄露。
- 建立 pidfd 与目标进程的映射时,要处理 pidfd 自身的继承和转移;只有
pidfd_open事件不足以覆盖全部来源。 - Go 系统调用包装通常以
err暴露失败,内核采集源可能记录负 errno;标准化时保留原始结果,并生成统一状态。
4. 为什么只监控敏感路径打开事件和网络 connect 不够?
简洁答案: 进程获得的可能是已经打开的文件或已经连接的 socket。它可以直接使用该对象,所以“资源使用者没有新的打开或连接事件”并不矛盾。
关键知识点:
- fd 继承、
dup和转移都能改变资源持有关系。 - 文件被删除后,已打开的 fd 仍可能引用它;此时路径快照不是当前路径状态。
- 复制出来的 fd 保留原对象的访问模式;复制只读 fd 不会自动得到写权限。
- 资源后续操作仍可能受其他检查约束,不能笼统称为“绕过所有沙箱”。
Go 落地思路:
- 把“谁最初打开资源”和“谁后来使用资源”保存为两条关系,避免所有活动都归因到最初打开者。
- 对可疑转移后的读取、写入或发送事件进行限时关联,明确是否观察到操作成功。
- 不要让 Agent 为了确认内容而复制并读取客户的敏感 fd:这会产生额外访问,还可能改变共享文件偏移或消费 socket 数据。
5. eBPF、auditd 和 procfs 能直接还原全部 fd 转移吗?
简洁答案: 不能保证。系统调用记录、消息辅助数据、内核文件对象和用户态 fd 分配分别提供不同层次的证据,需要针对采集源验证覆盖范围。
关键知识点:
pidfd_getfd的参数和返回值有利于识别复制动作,但把 pidfd 解析成目标进程仍需要额外信息。- 只看到
sendmsg/recvmsg调用不能证明传了 fd,还需确认控制消息包含SOL_SOCKET/SCM_RIGHTS。 sendmmsg/recvmmsg、兼容 ABI 和短命进程也会影响覆盖,不应宣称挂两个 syscall 就完整了。- procfs 是快照,受进程退出、fd 复用、权限和 namespace 影响,不能代替过去某次转移事件。
- auditd 常规 syscall 参数记录不会自动解码全部辅助数据;eBPF 的对象解析能力也取决于挂载位置和内核版本。
Go 落地思路:
- 对消息辅助数据设置解析字节上限和 fd 数量上限;超限记录
truncated,不把残缺结果当完整消息。 - 系统调用入口、出口分别采集参数与结果,用线程身份关联在途调用,退出后清理;接收端辅助数据应按成功返回后的实际内容解释。
- 上报
can_observe_transfer、can_resolve_object、丢失计数和解析失败计数,让规则知道当前证据能支持什么结论。 - 先通过测试证明关键场景可观测,再决定是否需要更深的内核探针,避免为追求“全覆盖”无边界增加采集成本。
6. Go 自己接收 SCM_RIGHTS 时,要注意哪些边界?
简洁答案: 将业务数据和辅助数据分别接收,验证对端及消息类型,再解析 fd。收到的 fd 是真实资源,所有拒绝和错误路径都必须处理其生命周期。
关键知识点:
net.UnixConn.ReadMsgUnix返回业务数据长度、辅助数据长度和 flags;WriteMsgUnix可以发送这两类数据。- 标准库
syscall的UnixRights、ParseSocketControlMessage、ParseUnixRights可用于理解和验证协议。工程已有x/sys/unix时可沿用;本文不要求新增依赖。 - 检查
MSG_CTRUNC,否则辅助数据可能不完整。Linux 会关闭因接收缓冲区过小等原因未被交付的多余 fd,但应用已收到的 fd 仍由应用负责。 SO_PEERCRED是连接或 socketpair 建立时的对端凭据快照;连接端点若被继承或转交,它不必等于每次实际发送消息的进程。- 如果协议需要逐条消息的发送者凭据,应评估
SO_PASSCRED/SCM_CREDENTIALS,同时保留授权检查,不能只凭 UID 判断业务权限。
Go 落地思路:
- 只解析
oob[:oobn],先确认控制消息的 level/type,再接收 fd 数组;协议约定数量上限和允许的资源类型。 - 解析时登记已收到的全部 fd;任何后续验证失败都关闭已登记资源。辅助数据截断的拒绝路径同样要释放已交付的 fd。
- 用
fstat等元数据验证对象类型,不根据发送方自报的“这是普通文件”直接信任。 - 明确 fd 由哪一层持有和关闭,检查接收 API 的 close-on-exec 保障;底层 Linux
recvmsg可使用MSG_CMSG_CLOEXEC,避免不必要的 exec 继承。
以上是 Agent 自有 IPC 的实现注意事项。被动观察第三方进程时不能假定可以读取其协议,也不需要为了取证主动建立连接或索取 fd。
7. 怎样关联成攻击链,同时降低正常 IPC 的误报?
简洁答案: 先判断转移是否成功,再判断资源敏感性、接收进程来源和后续行为。正常服务交接通常有稳定的服务身份、对端和资源类型;异常链路会出现与角色不匹配的组合。
关键知识点:
- 正常例子:服务升级时,将监听 socket 从旧进程交接给经过验证的新进程。
- 可疑例子:Web 服务派生的临时目录程序成功取得凭据文件 fd,随后出现读取与异常外联证据。
- “监听 socket 交接”和“已连接 socket 复用”是不同场景,规则不能混在一起。
- 时间邻近只能支持关联,不能证明外联内容就是此前读取的凭据;没有内容或更强证据时保留这一边界。
Go 落地思路:
- 先按转移类型和资源类别筛选候选事件,再在有界时间窗内关联进程链、文件使用和网络活动。
- 白名单采用“服务身份 + 经验证的执行文件 + 预期对端 + 资源类别 + 有效期”,避免只信任程序名或放行整个
/tmp。 - 告警输出命中的证据和缺失项,例如“复制成功;观察到读取;出现外联;数据内容未知”,不要自动升级为“已窃取凭据”。
- AI 可以辅助整理链路、解释字段,判定依据仍由可复核的事件和规则提供。
8. Go Agent 怎样控制 CPU、内存和隐私成本?
简洁答案: 优先采集转移的元数据,对候选事件做补充关联;限制缓存容量和解析预算,默认不读取文件内容或消息业务载荷。
关键知识点:
- 文件内容、socket 数据、全部环境变量容易扩大隐私暴露和采集成本,通常不是判断 fd 转移所必需的。
- 扫描所有进程的 fd 表既昂贵又存在竞态,不能把扫描频率当作准确性的保证。
- 单个进程可能持有大量 fd,缓存必须同时限制总量和单进程配额。
- 丢事件和缓存淘汰会影响归因可信度,不能静默删除后继续输出确定结论。
Go 落地思路:
- 保留必要的进程身份、fd、对象元数据、方法、返回值与证据来源,敏感路径按客户策略脱敏。
- 使用有容量上限的队列和 worker,避免每个事件创建 goroutine;复用现有上报与背压机制。
- 对转移映射设置 TTL 和容量上限;删除过期关系时保留必要的丢失统计。
- 监控队列深度、事件延迟、解析失败、缓存淘汰和 fd 数量,以固定回放数据测量 CPU 与内存变化。
9. 客户反馈“没有 open 记录却出现凭据读取”,怎么排查?
简洁答案: 先验证实际读取证据和进程身份,再检查采集覆盖、fd 来源和缓存完整性。最后才决定是正常复用、检测误关联,还是可疑资源使用。
关键知识点:
- PID、fd 和 inode 都可能复用,错误关联会制造看起来完整的攻击链。
- 进程很快退出时,事后 procfs 查询可能已经失去意义。
- 读取调用失败、复制失败和成功行为应分开统计,不能只看事件名称。
- root 权限也不能保证所有策略都允许访问,排查权限时保留具体错误、内核和策略上下文。
Go 落地思路:
- 核对 host、boot、进程启动时间、采集源和事件时间,排除跨进程、跨重启误关联。
- 查找成功的打开、继承、复制或接收事件,确认 fd 当时指向什么对象;来源缺失就标成未知。
- 检查队列丢失、时间窗、缓存淘汰、辅助数据截断和兼容 ABI 覆盖,避免把观测缺口当攻击证据。
- 验证服务升级、沙箱 broker、诊断操作等合法背景,再检查异常进程链和后续资源使用。
- 用脱敏回放样本调整规则并记录版本;不能凭一例误报就把全部 fd 转移行为放行。
学习要点
| 能力 | 必须掌握的知识 | 面试中应讲清的边界 |
|---|---|---|
| Linux 文件模型 | fd、open file description、dup、继承、FD_CLOEXEC | 描述符数字不等于全局资源身份 |
| 资源转移 | SCM_RIGHTS、pidfd_getfd、ptrace 权限检查 | 目标配合与访问授权是不同条件 |
| 事件采集 | 入口/出口、辅助数据、返回值、procfs 快照 | 转移尝试不等于成功使用 |
| Go IPC | ReadMsgUnix、控制消息解析、fd 关闭 | 截断和拒绝路径仍要释放已接收资源 |
| 攻击链与降噪 | 服务身份、资源敏感性、进程链、后续操作 | 时间相关不等于泄露内容已确认 |
| 线上治理 | 有界缓存、覆盖指标、丢失与隐私控制 | 缺失证据必须降低结论确定性 |
小练习与复盘题
仅在隔离的 Linux 测试环境使用自建进程、普通测试文件和本地 socket,不访问客户凭据或业务连接。
- 让两个测试进程通过 Unix socket 传递临时文件 fd,确认两端 fd 数字不需要相同,且接收方无需重新打开文件路径。
- 验证共享文件偏移:文件写入
abcdef后从偏移 0 开始,发送端先读取两字节,再让接收端读取,观察是否从cd开始;对比重新打开文件的情况。 - 设计
pidfd_getfd的成功、权限拒绝、目标 fd 已关闭三类用例,结合测试环境权限解释结果,不为了让用例成功而关闭安全策略。 - 模拟辅助数据缓冲区过小和协议拒绝,验证已收到的 fd 会被清理,持续运行时进程 fd 数量不会单调增长。
- 回放“正常监听 socket 交接”“Web 子进程成功复制敏感文件 fd”“复制失败后正常退出”,写出每类结果的证据等级和抑制依据。
- 人为删除一条转移事件,确认检测结果显示“资源来源未知”,而不是凭后续 fd 数字反推不存在的成功转移。
- 模拟进程退出、同 PID 新进程启动、相同 fd 被重新分配,验证关联不会跨越进程启动时间和描述符生命周期。
- 在固定事件集下记录 Go Agent 的 CPU、内存、队列延迟和缓存淘汰次数,说明预算不足时优先保留哪些证据。
复盘时用四句话回答:谁取得了什么资源?通过哪种方式?哪一步已证实成功?当前还有哪些证据缺口? 这比只列系统调用名称更能体现主机安全研发能力。
参考资料
- Linux man-pages:pidfd_getfd(2):复制语义、权限检查、返回值和错误码。
- Linux man-pages:unix(7):
SCM_RIGHTS、凭据、辅助数据和 fd 生命周期。 - Linux man-pages:recvmsg(2):
MSG_CTRUNC与MSG_CMSG_CLOEXEC。 - Go 官方源码:UnixConn:消息与辅助数据的收发接口。
- Go 官方源码:Unix 控制消息:fd 控制消息编码与解析。
