一、概述
Linux 内核把自己的运行时状态挂在 /proc 上。那不是磁盘上的目录——里面绝大多数「文件」是内核在你读取的瞬间现生成的。
这个性质决定了整件事的全部前提:
/proc 里的内容只能在线获取。断电后的磁盘镜像里,/proc 是一个空目录。
跟 Windows 取证里「内存态工件丢失」算是同一类问题。但 Linux 有个明显优势:不做内存镜像,只要在断电前用只读命令把关键项导出成文件,就能拿到大量带时间戳的运行时证据。
而这批证据在镜像里完全无法重建。
所以 /proc 的取证价值几乎全押在一件事上:现场有没有做在线采集,采得全不全。这个判断得在现场做完,事后补不回来。
这一层的位置很特殊:它不属于「拿到镜像之后」的任何环节,它属于「拿到镜像之前」。
上游是现场处置决策。是否断电、是否先做内存采集、是否在断电前导出 /proc,这三个决定必须在现场几秒内做完,依据见远程勘验与在线取证。
它自己要在采集前确认容器环境。在容器内 /proc 是宿主机视角还是容器视角,取决于挂载方式与 cgroup 配置。判错会把宿主机的进程误认成容器的。
下游支撑归属分析。/proc/net/tcp 里的连接要反查进程才能定性,「哪个进程走的」是流量分析的必答项;环境变量里的凭据进入后续分析,但只做离线分析与最小化提取,不做在线使用。
它与内存取证是替代关系而非并列:来不及做完整内存镜像时,逐项导出 /proc 是成本低得多的折中方案。
能提供四类其它地方拿不到的证据:
| 证据类型 |
典型位置 |
为什么磁盘上找不到 |
| 进程完整命令行 |
/proc/<pid>/cmdline |
进程退出即消失;bash_history 只记交互命令,不记脚本参数 |
| 进程环境变量 |
/proc/<pid>/environ |
常含数据库口令、API token、云凭据;不落盘、无法 grep 到 |
| 被删除但仍打开的文件 |
/proc/<pid>/fd/ |
文件已从目录树删除,但 inode 仍被进程持有——经典反取证场景 |
| 真实网络连接 |
/proc/net/tcp、/proc/<pid>/net/tcp |
内存态;日志里可能完全没有 |
具体到每个子项的判读点:
| 子项 |
内容 |
关键判读点 |
/proc/<pid>/cmdline |
NUL 分隔的参数序列 |
可执行文件路径与参数都能取到,与历史文件互补 |
/proc/<pid>/environ |
NUL 分隔的环境变量 |
敏感度高,摘录必须脱敏 |
/proc/<pid>/fd/ |
打开的文件描述符软链 |
指向 (deleted) 的条目即被删除但仍被持有的文件 |
/proc/<pid>/maps、status |
内存映射与状态 |
指向的匿名可执行区是进程注入的线索 |
/proc/net/tcp、tcp6 |
连接与监听状态 |
地址与端口是十六进制小端编码,直接读会得到反的结论 |
/proc/<pid>/net/tcp |
命名空间视图 |
与 /proc/net/tcp 内容可能不同,容器内差异尤其明显 |
/proc/<pid>/cgroup |
容器归属线索 |
判定进程属于哪个容器的基础 |
| 危险项 |
/proc/kcore 等 |
/proc/kcore 相当于内核内存视图,绝不能在原机上读取 |
三个判定要先立起来:十六进制编码的解码方向、命名空间的隔离效果,以及删除状态下文件仍被持有的含义。最后一类是最有价值、也最容易被忽略的。
答得出来的是:案发当时系统上有哪些进程在跑、每个进程带什么参数、谁持有哪些已删除的文件、当时有哪些网络连接及其归属进程,以及某些文件在被删除后是否仍被某个进程占用。
答不出来的,以及几个必须先认清的边界:
离线镜像里的一切。 镜像中的 /proc 是空目录。这不是「没有进程」,而是「这个证据形态不适用于离线分析」——报告里写错这句话,会让一次本可在线完成的调查变成不可能。
历史。 /proc 是当前快照,不记录任何过去。它能回答「现在有什么」,不能回答「一小时前有什么」。
已退出进程的任何信息。 进程一退出,/proc/<pid> 立即消失,没有历史记录可查。
网络连接的目的地内容。 它给出的是连接与进程归属,「传输了什么」需要网络侧材料,见网络流量与 PCAP 取证。
容器内的完整视图。 若 /proc 未按容器隔离,你看到的可能混入了宿主机进程,边界判定不清时不能直接下结论。
敏感凭据的后续使用。 environ 里的 token 与口令只做离线记录与最小化摘录,任何在线使用都不在取证范围内。
要读懂这些,得先有 Linux 进程模型——进程是运行态实体,/proc 是它的文件系统投影而非存储;了解文件描述符与 inode 的引用关系,否则没法解释「文件已删除但 fd 仍指向它」;掌握 socket 状态机与 TCP/IP 基础,否则十六进制解码出来的地址端口方向会搞反;熟悉十六进制与小端序的换算,这是 3.4 解码环节的前提;了解容器与 cgroup 的基本概念,能识别 /proc/<pid>/cgroup 的归属信息。
还有一条纪律:所有操作不得改变系统状态,涉及写盘或触发内核动作的项必须在采集前排除。
二、核心原理
2.1 为什么 /proc 离线读不到
/proc 是一个由内核导出的虚拟文件系统(procfs)。目录项和文件内容都是读取时按需生成的:
读 /proc/1234/cmdline → 内核从当前 task_struct 生成字节流返回
进程退出 → /proc/1234/ 整个目录立即消失
由此可得:
| 场景 |
/proc 是否有内容 |
| 机器运行中 |
有(实时快照) |
| 断电后做 dd 镜像 |
空目录(proc 只是镜像里的一个空壳目录) |
| 机器运行中做在线/内存镜像 |
仍需单独采集——dd 磁盘镜像不包含 /proc 内容 |
采集 DigiForensics-mem.raw 后关机 |
/proc 内容只在内存镜像里,需从内存中恢复(难);最好是在关机前先导出 |
给现场处置的建议:如果委托方可能需要进程/连接证据,在断电前用 3.1 的只读命令把 /proc 关键项导出到独立分区或外接介质。
这个动作成本极低,几秒就完事,但决定了这类证据是否永久可得。一旦断电,这批证据就没了。
2.2 /proc/<pid>/ 关键子项
| 路径 |
内容 |
格式要点 |
取证价值 |
/proc/<pid>/cmdline |
完整命令行 |
★ 参数之间用 NUL(\0)分隔,不是空格 |
★★★★★ 进程实际执行了什么;能证明脚本的真实参数 |
/proc/<pid>/environ |
环境变量 |
NUL 分隔的 KEY=VALUE |
★★★★★ 常含口令/token/云凭据 |
/proc/<pid>/cwd |
当前工作目录 |
软链接(readlink 取目标) |
★★★★ 进程在哪个目录操作文件 |
/proc/<pid>/exe |
可执行文件本体 |
软链接;已被删除时目标带 (deleted) |
★★★★★ (deleted) 是"文件已删但仍在运行"的铁证 |
/proc/<pid>/root |
进程的根目录 |
软链接;容器场景有用 |
★★★ chroot/容器判断 |
/proc/<pid>/fd/ |
打开的文件描述符 |
目录,每项是软链到实际文件 |
★★★★★ 能看到已删除但仍打开的文件 |
/proc/<pid>/maps |
内存映射布局 |
文本,含每段权限和路径 |
★★★★ 定位注入的匿名可执行段、无文件映射的可疑内存 |
/proc/<pid>/status |
进程状态 |
文本键值对 |
★★★ Name/Pid/PPid/Uid/Gid/Threads/State |
/proc/<pid>/stat |
进程统计 |
单行文本,字段多且是第 4 个字段是命令名(含括号) |
★★★ 取启动时间的换算基础 |
/proc/<pid>/statm / smaps |
内存用量/细节 |
文本 |
★★ smaps 可看是否有 RWX 匿名段 |
/proc/<pid>/mountinfo |
该进程看到的挂载 |
文本 |
★★★ 容器场景判断挂载来源 |
/proc/<pid>/cgroup |
cgroup 归属 |
文本 |
★★★★ 容器/系统d 服务归属判定 |
/proc/<pid>/loginuid |
触发登录审计的 uid |
文本 |
★★★ 登录归属(需 pam_loginuid 启用) |
⚠️ cmdline 与 environ 用 NUL 分隔,不是空格也不是换行。 直接 cat 会看到"连成一片"或终端不换行。必须用 tr '\0' ' ' 或 xargs -0 转换。 这是最常见的 /proc 读取错误之一。
⚠️ /proc/<pid>/exe 显示 (deleted) 的含义:文件的目录项已被删除(unlink),但 inode 仍被该进程持有映射——所以程序还在运行,但磁盘上已经没有这个文件了。这正是"删除日志文件后继续写入日志"的实现基础,也是证明"文件曾存在过"的最强证据。
2.3 /proc/net/tcp 的结构与十六进制编码
/proc/net/tcp(tcp6/udp/udp6 同理)是当前网络连接与套接字的快照,第一行是表头:
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 0100007F:1F90 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 12345 ...
1: 0100007F:1F90 0201A8C0:C350 01 00000000:00000000 00:00000000 00000000 0 0 54321 ...
前 4 列是要解析的重点:sl(序号)、local_address(本地地址)、rem_address(对端地址)、st(状态)。
编码规则是本篇的核心知识点,一句话:地址部分是 32 位(IPv4)或 128 位(IPv6)的十六进制,但按 4 字节一组、每组内部字节序反转(小端)存放。
拿 0100007F(IPv4 的 127.0.0.1)走一遍:
十六进制字符串: 01 00 00 7F
每 2 位倒序(字节交换): 7F 00 00 01
按点分十进制读: 127.0.0.1
解码步骤因此是:把 8 位十六进制看作一个小端 32 位整数,再用网络字节序解释。等价于"每 2 位倒序一次"。
端口部分是普通十六进制,没有字节交换:1F90 = 0x1F90 = 8080(十进制)。
本篇要求的核心验证:0100007F:1F90 → 地址 0100007F 倒序为 7F000001 → 127.0.0.1;端口 1F90 → 8080。结果 = 127.0.0.1:8080 ✓
IPv6 的规则:地址是 128 位 = 4 个 32 位字,每个 32 位字各自做一次小端字节交换,再按网络序拼成 16 字节。
/proc/net/tcp6 中的值 |
解码结果 |
00000000000000000000000001000000 |
::1 |
000080FE0000000000000001000000 |
fe80::1 |
B80D012000000000000000001000000 |
2001:db8::1 |
IPv4 与 IPv6 的区分看地址字段的十六进制长度:8 个字符 = IPv4(/proc/net/tcp),32 个字符 = IPv6(/proc/net/tcp6)。两者不会出现在同一个文件里。
状态字段 st 是一字节十六进制(常用):
st |
状态 |
取证含义 |
0A |
LISTEN |
监听中的端口;非预期端口的 LISTEN = 值得深挖 |
01 |
ESTABLISHED |
活跃连接;指向外部 IP 的 ESTABLISHED = 外传/入侵通道 |
06 |
TIME_WAIT |
连接刚关闭(攻击者断开后常见) |
08 |
CLOSE_WAIT |
对端已关闭、本端未关闭(进程未正确收尾,程序异常或恶意常留) |
2.4 /proc 下的网络与其它全局文件
| 路径 |
内容 |
/proc/net/tcp、tcp6、udp、udp6 |
连接与套接字快照(本网络命名空间) |
/proc/net/arp |
ARP 缓存(近期通信过的同网段 IP,局域网取证有用) |
/proc/net/route |
路由表(可判断网关/内网网段) |
/proc/net/dev |
各网卡流量统计(收发字节数,粗略判断外传量) |
/proc/net/unix |
Unix domain socket |
/proc/net/xt_qtaguid/stats |
按 UID 统计流量(部分内核/发行版才有) |
/proc/modules |
已加载内核模块(与第 04 篇的模块排查联动) |
/proc/mounts、/proc/mountinfo |
挂载表(判断 tmpfs、bind mount、容器挂载) |
/proc/uptime |
系统运行时长(秒) ★ |
/proc/stat |
系统启动以来的 CPU 时间累计 |
/proc/uptime 是"这台机器上次什么时候启动的"的快速答案:uptime 秒数 + 当前时间 = 启动时刻。
但断电机器的 /proc 是空的,所以这个方法只在线可用。离线的等价做法是从日志/journal 的首次启动记录推断。
2.5 /proc/kcore 与危险项
/proc/kcore 是内核内存的转储接口,大小约等于物理内存(甚至更大)。它绝不是普通文件。
绝对不要 cat、cp、dd,也不要挂载后去浏览它。在多数系统上尝试读取会导致系统立即挂起或崩溃(它可能触发巨大的不可恢复读),某些配置下还可能造成数据泄露。
它属于内存取证的范畴——需要时用专门的内存采集工具(在断电前从另一个接口获取),不是本篇的操作对象。
同理需要谨慎的还有:/proc/self/mem(需权限,且可致崩溃)、/proc/kallsyms(需 root,部分系统已限制)。
2.6 容器环境下的 /proc 差异
在容器里,/proc 的"视角"是容器的,不是宿主机的。 这是取证时必须先判定的前提。
| 差异点 |
宿主视角 |
容器内视角 |
/proc/<pid> |
所有进程 |
只能看到容器内 PID 命名空间里的进程(容器 PID 1 = 应用自身) |
/proc/net/tcp |
宿主全部连接(默认 net namespace) |
仅该网络命名空间内的连接(--network=host 时才共享宿主) |
/proc/mounts |
宿主完整挂载 |
容器内的挂载(含 overlay) |
/proc/<pid>/root |
/ |
容器自己的根文件系统 ★ |
/proc/1/environ |
systemd 的环境 |
容器的启动命令与环境(容器取证的第一入口) |
容器取证的关键入口(若 /proc 是在容器内采集的):
容器内的 PID 1 命令行 = 容器启动了什么
tr '\0' ' ' < /proc/1/cmdline; echo
tr '\0' '
' < /proc/1/environ | grep -iE 'PASSWORD|SECRET|TOKEN|KEY|DATABASE|DSN'
容器的 cgroup 归属(回到宿主可见的容器 ID)
cat /proc/1/cgroup
容器看到的挂载(含 overlay 层)
cat /proc/1/mounts | grep -E 'overlay|/etc/hosts|/etc/resolv.conf'
判定方法很简单:若 /proc/1/cmdline 显示的不是 systemd/init 而是应用名(nginx、python、java 等),说明这份 /proc 是在容器命名空间内采集的。
此时"看不到某进程"是命名空间隔离的结果,不代表进程不存在。这一点必须在报告中说明,否则会把"命名空间隔离"误判为"无此进程"。
三、操作步骤
3.1 第 1 步:不破坏系统的现场采集(在断电/关机之前执行)
目标:把 /proc 里的关键项导出为普通文件。 全程只读,不 kill 任何进程。
#!/bin/bash
在线 /proc 采集脚本(只读;输出到 /evidence/proc-snapshot)
OUT=/evidence/proc-snapshot
mkdir -p "$OUT"
1) 全局快照
uptime > "$OUT/uptime.txt"
date -u +'%Y-%m-%dT%H:%M:%SZ' > "$OUT/collected-at-utc.txt"
cp /proc/net/tcp "$OUT/net-tcp.txt" 2>/dev/null
cp /proc/net/tcp6 "$OUT/net-tcp6.txt" 2>/dev/null
cp /proc/net/arp "$OUT/net-arp.txt" 2>/dev/null
cp /proc/net/route "$OUT/net-route.txt" 2>/dev/null
cp /proc/modules "$OUT/modules.txt" 2>/dev/null
cp /proc/mounts "$OUT/mounts.txt" 2>/dev/null
2) 逐进程采集关键项(NUL 分隔必须转换!)
for p in /proc/[0-9]*; do
pid=$(basename "$p"); d="$OUT/pid-$pid"; mkdir -p "$d"
tr '\0' ' ' < "$p/cmdline" > "$d/cmdline.txt" 2>/dev/null # ★ NUL→空格
tr '\0' '
' < "$p/environ" > "$d/environ.txt" 2>/dev/null # ★ NUL→换行
readlink "$p/exe" > "$d/exe.txt" 2>/dev/null
readlink "$p/cwd" > "$d/cwd.txt" 2>/dev/null
cp "$p/status" "$d/status.txt" 2>/dev/null
cp "$p/maps" "$d/maps.txt" 2>/dev/null
cp "$p/cgroup" "$d/cgroup.txt" 2>/dev/null
ls -l "$p/fd" > "$d/fd-list.txt" 2>/dev/null # 含 "(deleted)"
done
3) 全盘建立 fd → 目标 索引(已删除文件一眼可见)
grep -l 'deleted' "$OUT"/pid-/fd-list.txt 2>/dev/null
grep -h 'deleted' "$OUT"/pid-/fd-list.txt 2>/dev/null
只读原则:/proc/<pid>/environ 和 /proc/<pid>/fd/ 通常需要 root(environ 受 PTRACE_MODE_READ 限制),用 sudo 读取是必要的,但仍属只读。
绝不要 kill、kill -9、写 /proc/.../mem,也不要尝试 echo > /proc/.../...。
3.2 第 2 步:进程清单与高价值筛选
进程清单(命令行 + exe + 用户)
cd /evidence/proc-snapshot
for d in pid-*; do
pid=${d#pid-}
printf '%-8s %-10s %s
' "$pid" "$(cat $d/exe.txt 2>/dev/null | xargs basename 2>/dev/null)"
"$(cat $d/cmdline.txt 2>/dev/null | cut -c1-90)"
done | sort -n
★ 筛出 exe 指向已删除文件的进程("删了文件还在跑")
grep -l 'deleted' pid-*/exe.txt 2>/dev/null | while read f; do
echo "$f → $(cat "$f") cmd: $(cat "${f%exe.txt}cmdline.txt")"
done
★ 筛出 exe 位于临时/异常路径的进程