关键词:task_struct、mm_struct、VMA、slab、btf2json、bash 历史、TTY 钩子、rootkit、ART、dalvik、Frida
难度:高级
前置知识:Linux 内核内存管理、进程结构、Android 运行时(ART)、Volatility 3 Linux 插件
一、概述
Linux 内存取证在技术上比 Windows 更开放——没有卷级加密,/proc 直接暴露大量运行时信息。
但困难不在采集,而在符号。
Volatility 3 分析 Linux 需要与目标内核精确匹配的 ISF,这件事无法自动完成。Windows 侧可以按 PDB GUID 从微软符号服务器自动下载,Linux 做不到。各发行版与自定义内核都会改动内核,同一个版本号的二进制也未必相同。符号必须自行构建(dwarf2json 或 btf2json)。这一步没做,后面所有插件都跑不起来。
Android 是另一个独立子问题:内核是 Linux,用户态却是 ART 而非标准 libc 栈。匿名可执行映射、ART 堆、注入框架痕迹都有自己的判据,采集门槛(root、SELinux)也完全不同。
这一步落在分析定性环节,是 memory 板块里非 Windows 平台的落点。采集已经由内存采集方法完成,要解决的是符号怎么备、镜像怎么读、结果怎么判。它与 Windows 侧是互补而非替代——判断一台 Linux/Android 机器上发生了什么,通常要拿 Windows 侧那套判据(父子关系、命令行、连接归属)加上 Linux/Android 特有的判据(TTY 挂钩、命名空间、ART 堆)。
可用的对象与关键点:
| 对象 |
形态 |
关键点 |
| 符号来源 |
BTF(btf2json,用 vmlinuz)/ DWARF(dwarf2json,需调试包) |
必须与目标内核精确匹配 |
| 内核结构 |
task_struct、files_struct、vm_area_struct |
插件的解析基础 |
| 命令历史 |
linux.bash 的内存副本 |
磁盘文件被删也不影响 |
| 环境变量 |
linux.envars |
常含 API Key 与数据库口令 |
| TTY 输入 |
TTY 驱动层输入缓冲 |
键盘记录器必须挂钩 TTY 层 |
| 命名空间 |
/proc/<pid>/ns/* 的链接目标 |
容器逃逸与横向移动的判据 |
| Android 侧 |
ART 堆、匿名可执行映射、/data/app 私有目录 |
用户态不是标准 libc 栈 |
第三到第六行是 Linux 特有的优势项,其中 linux.bash 的价值最高:磁盘上的历史被删了,内存里的那份还在。
据此能回答的是这些:目标 Linux 上跑着哪些进程、执行过哪些命令、进程环境变量里有什么、是否有 rootkit 迹象(未挂载的文件系统、隐藏进程、TTY 钩子)、容器有没有命名空间穿越痕迹、Android 上有没有注入框架或监控应用的运行时痕迹。
回答不了的是这些。符号不匹配时不是"结论少一点",是全部结论作废。对已锁定或已重启的 Android 设备,明文在内存里就是密文块,这一项无解。内存里取不到的(已换出页、已释放内存),回落盘侧的工件地图重找一遍。
往下读之前,先确认你已经掌握:Linux 内核符号与 BTF/DWARF 的基本概念、/proc 虚拟文件系统的结构、Android 的 root 与 SELinux 机制、以及 Volatility 的基本操作。符号构建是这一主题最大的前置工作,建议先看Windows 内存 Profile 机制理解符号层在整条链路中的位置,再看proc 虚拟文件系统取证打牢 /proc 结构基础。
二、核心原理
2.1 核心结构与插件对应
物理内存
├── 内核动态内存
│ ├── slab / SLUB ← task_struct、mm_struct 从此分配
│ │ └── kmem_cache:每种对象类型一个池 ← rootkit 常用来藏数据
│ └── 内核模块区、内核栈、kmsg 环形日志
└── 用户空间(每进程独立)
├── task_struct ← 进程在内核中的"身份证"
├── mm_struct ← 地址空间描述符
└── VMA 链表 ← 文本段/数据段/堆/栈/匿名映射 ← 注入常在匿名映射
| 结构 |
作用 |
插件 |
task_struct |
PID、状态、地址空间指针、父子关系 |
linux.pslist、psaux、pstree |
| VMA |
地址空间中一段连续区域 |
linux.proc.Maps、linux.malware.malfind |
slab / kmem_cache |
内核对象分配器 |
linux.malware.check_modules、hidden_modules |
kmsg |
内核环形日志(dmesg 数据源) |
linux.kmsg |
/proc/kcore 的意义:它把物理内存按 section 暴露为 ELF 视图,section 划分对应物理地址区间。
这正是 LiME 用 format=lime 记录地址范围的原因——分析工具需要"文件偏移 → 物理地址"的映射才能正确解析结构。
2.2 Linux 特有工件
| 工件 |
内容 |
独特性 |
插件 |
| bash 历史 |
历史缓冲区,含时间戳 |
磁盘文件删了也能从内存取回 |
linux.bash |
| 环境变量 |
进程环境 |
常直接带密钥与内网地址 |
linux.envars |
| TTY 事件 |
终端输入的按键 |
Windows 无对等物 |
linux.malware.tty_check |
| 打开的文件 |
文件描述符指向 |
能看到已 unlink 但仍打开的句柄 |
linux.lsof |
| 内核日志 |
dmesg 缓冲内容 |
模块加载、异常 |
linux.kmsg |
| 挂载点 |
挂载表,含网络文件系统 |
反映运行期的挂载关系 |
linux.mountinfo |
linux.bash 的价值被严重低估。/root/.bash_history 被清空是入侵案的常见反取证手段,但 bash 进程内存中的历史缓冲区仍保留着这些命令——包括清除命令本身。
注意:bash 记录时间戳的前提是 HISTFILE 设置了时间格式(如 %F),否则不记录时间。所以有输出不代表每条都有时间戳。
2.3 rootkit 检测的三个层次
| 层次 |
手法 |
检测插件 |
局限 |
| 一、用户态 |
替换/劫持 ls、ps、ss、netstat、top |
linux.lsof(看实际加载了什么) |
需与发行版哈希基线比对 |
| 二、内核模块 |
加载隐藏 .ko,钩子函数 |
check_modules、hidden_modules |
若 sysfs 被篡改则失效 |
| 三、内核级 |
改系统调用表/IDT,不加载模块 |
check_syscall、check_idt、tty_check |
隐蔽性最高 |
check_modules 与 hidden_modules 的区别:前者比对"内存中的模块信息"与"sysfs 中可读到的信息"——若 rootkit 篡改 /sys/module 隐藏了模块,两者会不一致。后者专门寻找"在内存里有完整结构、但未出现在模块列表"的模块。
TTY 检查是 Linux 独有的检测面。键盘记录器必须挂钩 TTY 层的输入处理函数——这是它的工作原理决定的。tty_check 正是检查这些函数指针是否指向预期模块之外的地方。
2.4 符号获取:必须自建
Linux 内核的调试信息(.debug 段或 BTF)不在任何中心服务器上,随发行版 -dbgsym 包分发或需自行编译。
| 工具 |
语言 |
输入 |
依赖 |
适用 |
btf2json |
Rust(独立工具) |
--btf <BTF 文件> + --map <符号表> |
不需要完整调试内核 |
现代内核推荐 |
dwarf2json |
Go |
--elf <vmlinux> |
完整 DWARF |
有发行版 dbgsym 包时 |
⚠️ 别把两者混用:--btf / --map 是** btf2json 的参数**;dwarf2json 只有 --elf / --elf-symbols / --elf-types / --system-map,给它传 --btf 会直接报错。
关键差异:Windows 是"事后联网取",Linux 是**"事前在目标机上准备"**。
且 Linux 符号文件名不影响匹配——内核 banner 编码在 ISF 内部,框架靠它匹配;这一点与 Windows(靠 GUID)不同。
2.5 Android 特有:ART 运行时与注入迹象
/proc/<pid>/maps 关键映射判读:
| 映射名 |
判读 |
[anon:dalvik-main space (region space)] |
正常,ART 主堆,每个 Android 进程都有 |
[anon:dalvik-LinearAlloc] / [anon:dalvik-indirect ref table] |
正常 |
[anon:dalvik-jit-code-cache] |
正常但需注意:可执行,是合法 JIT 代码 |
[anon:scudo:primary] |
正常(内存分配器) |
/data/local/tmp/*.so |
⚠️ 高度可疑,调试框架常驻位置 |
/data/data/<pkg>/files/*.so |
可疑(除非该应用正常行为) |
| 匿名可执行页(无对应文件) |
⚠️ 强注入信号 |
判读核心:dalvik-* 映射不是注入迹象——每个 Android 进程都有。把 dalvik-main space 报成"隐藏代码"是常见误判。
真正的注入迹象:① 匿名可执行映射且无对应文件;② /data/local/tmp/ 下的 .so;③ 内存中的注入框架字符串(如 frida-agent、gum-js-loop,见第七章);④ 应用主类加载路径与包名不符。在涉案设备上发现 Frida 痕迹,意味着要么设备被用于攻击,要么分析环境被污染——两种情况都要写入报告。
2.6 iOS 内存取证的现实
iOS 的结论先行:现代 iOS 上在线内存采集不可行。
| 障碍 |
说明 |
| 必须越狱 |
未越狱设备无法获得内核级访问 |
| 公开方案已失效 |
该领域曾出现的公开方案在现代 iOS 上已失效 |
idevicesyslog 不是内存 |
它只是系统日志(等价于 log stream) |
| Apple 的立场 |
不向第三方开放内核内存访问,仅企业 MDM 有正式通道 |
易混淆概念澄清:很多资料把 idevicesyslog、Crash 日志、ipsw 提取列为"iOS 内存取证",这些都不是。
前者是日志,中间是崩溃转储(仅崩溃瞬间的少量内容),后者是固件镜像。真正的内存镜像在 iOS 上没有可行的合法采集路径。
实务建议:iOS 案件的内存维度应作为"证据局限"明确写入报告,而不是勉强用日志冒充内存证据。
三、操作步骤
3.1 第 1 步:符号准备(Linux 侧的前置工作)
# 1) 确认目标内核版本(采集时就应记录)
uname -r # → 5.10.134-android12-9
# 2) 收集符号生成所需的输入(在目标机或同构机器上)
ls -la /boot/vmlinuz-$(uname -r) /boot/System.map-$(uname -r)
# 3) 检查内核是否含 BTF(决定用哪条路线)
# 现代内核可直接从 /sys/kernel/btf/vmlinux 取
ls -la /sys/kernel/btf/vmlinux 2>/dev/null || \
objcopy -O binary --only-section=.BTF /boot/vmlinuz-$(uname -r) /tmp/vmlinux.btf
路线 A(推荐):btf2json——只需 vmlinuz 的 BTF + System.map,不需要完整调试内核;路线 B:dwarf2json——需要带 DWARF 的 vmlinux。 完整构建命令见第七章。
确认符号可用:
python3 vol.py -f DigiForensics-mem.raw -s /opt/isf linux.pslist
# 正常输出应含 init、内核线程、用户进程;报符号不匹配即 ISF 无效
# 看框架实际加载了哪个符号文件
python3 vol.py -f DigiForensics-mem.raw -s /opt/isf isfinfo
3.2 第 2 步:进程、痕迹、网络与 rootkit
V="python3 vol.py -f DigiForensics-mem.raw -s /opt/isf"
$V linux.pslist # 进程列表
$V linux.pstree # 父子关系
$V linux.psaux # 含命令行、环境、父进程
$V linux.proc.Maps # 内存映射(识别匿名可执行区)
$V linux.bash # bash 历史(含时间戳)
$V linux.envars # 环境变量(常含密钥)
$V linux.kmsg # 内核日志
$V linux.lsof # 打开的文件
# 注意:Linux 没有 netscan 插件
$V linux.sockstat # 套接字统计(当前活动)
$V linux.sockscan # 套接字扫描(内核池扫描)
$V linux.malware.tty_check
进程摘链检测(与 Windows 同思路,见第 05 篇):
$V linux.pslist | awk '{print $2}' | sort -u > /tmp/pslist_pids.txt
$V linux.psscan | awk '{print $2}' | sort -u > /tmp/psscan_pids.txt
comm -13 /tmp/pslist_pids.txt /tmp/psscan_pids.txt # psscan 有、pslist 无
与 Windows 的重要差异:linux.sockscan 也是扫描内核池得到,同样存在 stale data 问题。判读原则与第 06 篇一致——扫描结果必须交叉验证。
⚠️ 命名规范:linux.malware.* 是规范名称。顶层 linux.check_modules、linux.tty_check、linux.hidden_modules 等旧名虽仍可用,但已标记弃用并设定了移除日期,脚本里应固定用规范名。
3.3 第 3 步:Android 采集与初筛
完整采集脚本:
cat > /work/android-acquire.sh <<'SCRIPT'
#!/bin/bash
# Android 内存采集(需 root)
set -e
OUT="${1:-/sdcard/DigiForensics-mem.raw}"
LOG="/sdcard/acq-log.txt"
echo "=== Android 内存采集 $(date -Iseconds) ===" > "$LOG"
getprop ro.product.model >> "$LOG" 2>&1
getprop ro.build.version.release >> "$LOG" 2>&1
getprop ro.build.fingerprint >> "$LOG" 2>&1
uname -a >> "$LOG" 2>&1
getenforce >> "$LOG" 2>&1 # SELinux 状态(关键)
grep -E 'MemTotal|MemAvailable' /proc/meminfo >> "$LOG" 2>&1
ps -A -o PID,PPID,USER,NAME >> "$LOG" 2>&1
/data/local/tmp/avml "$OUT" >> "$LOG" 2>&1
ls -l "$OUT" >> "$LOG" 2>&1
echo "=== 完成 $(date -Iseconds) ===" >> "$LOG"
cat "$LOG"
SCRIPT
adb push /work/android-acquire.sh /data/local/tmp/
adb shell su -c "setenforce 0" # 放行(记录此状态变更!)
adb shell su -c "chmod 755 /data/local/tmp/android-acquire.sh"
adb shell su -c "/data/local/tmp/android-acquire.sh /sdcard/DigiForensics-mem.raw"
adb pull /sdcard/DigiForensics-mem.raw
adb shell su -c "rm -f /sdcard/DigiForensics-mem.raw /data/local/tmp/avml /data/local/tmp/android-acquire.sh"
Android 初筛脚本(> ⚠️ 手机镜像同样动辄数 GB,strings 一次读全会 OOM——复用第 06 篇的 scan_img 分块函数,或对单进程用 windows.memmap 等价方式先切出可疑进程内存):
cat > /work/android-triage.sh <<'SCRIPT'
#!/bin/bash
# Android 内存镜像初筛(依赖外部定义的 scan_img <正则> <输出> [最小串长])
IMG="${1:-DigiForensics-mem.raw}"
scan_img 'frida-agent|gum-js-loop|gum-js-bridge|re\.frida' /tmp/t1.txt
scan_img '/data/local/tmp/.*\.so' /tmp/t2.txt 8
scan_img 'password=|passwd=|jdbc:|mysql://|postgres(ql)?://' /tmp/t3.txt
scan_img 'api[_-]?key|access[_-]?token|Bearer ' /tmp/t4.txt
scan_img 'AIza[0-9A-Za-z_-]{35}|AKIA[0-9A-Z]{16}' /tmp/t5.txt
scan_img 'magisk|supersu|busybox|xposed|substrate|cydia' /tmp/t6.txt
cat /tmp/t1.txt /tmp/t2.txt /tmp/t5.txt | head -40 # 注入框架 / 临时 .so / root 框架
grep -hE 'AIza|AKIA' /tmp/t5.txt # 云凭据(仅记录,不外传)
SCRIPT
chmod +x /work/android-triage.sh
/work/android-triage.sh DigiForensics-mem.raw
3.4 第 4 步:ART 映射判读
$V linux.proc.Maps > /work/maps.txt
# 正常的 ART 映射(每个进程都有,不是异常)
grep -E 'dalvik-main space|dalvik-LinearAlloc|dalvik-indirect ref table' /work/maps.txt | head
# 可疑:临时目录的库
grep -E '/data/local/tmp/' /work/maps.txt
# 匿名可执行映射(注入的强信号)——先排除 ART 合法映射
grep -E '\[anon' /work/maps.txt | grep -viE 'dalvik|scudo|jit-code' | head -20
四、常见陷阱
Linux 与 Android 的困难不在采集而在解释:Windows 侧能按 PDB GUID 自动取到符号,Linux 侧必须事前自建;Android 侧满屏 dalvik-* 映射,正常与异常在形态上高度相似。
本章按符号、Linux 工件与 rootkit、stale data、Android 判读、阴性表述五组展开。
4.1 符号:Linux 侧的硬前置
四条陷阱全在这一组。共同点是:它们都发生在符号还没备好的时候,而此时插件往往不会报"错",只是给出一片看起来正常的空结果。
陷阱 1:Linux 符号不准备就直接分析
现象:拿到 Linux 内存镜像直接跑 linux.pslist,报符号不匹配或输出为空,转而认为镜像不可用。
为什么会误判:1 把 Linux 的困难点说得很直接——技术上比 Windows 更开放,但困难在别处:符号。Volatility 3 分析 Linux 需要与目标内核精确匹配的 ISF,而 2.1 的结构表里 task_struct、mm_struct、VMA 链表的解析全依赖这些结构定义。没有匹配符号,框架连 task_struct 的字段偏移都不知道,输出为空是必然的。
误判代价:把"没有解释器"当成"数据不可用"。
案例第 1 步整个花在符号构建上(dwarf2json 从带 DWARF 的 vmlinux 生成 ISF),第 2 步才拿到正常的进程列表。跳过这一步,64 GB 的服务器镜像就白采了。
正确做法:符号是分析的前置工作,不是分析的步骤。
3.1 是完整流程:确认 uname -r → 收集 /boot/vmlinux-$(uname -r) 与 System.map-$(uname -r) → 检查是否有 BTF(ls -la /sys/kernel/btf/vmlinux)→ 生成 ISF → 用 linux.pslist + isfinfo 验证。
验证标准要具体:输出里应能看到 swapper/0/0、kthreadd 这类内核线程,以及至少一个用户进程。