ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

内存取证入门实战:OtterCTF 1-5题完整复盘与Volatility使用指南

内存取证入门实战:OtterCTF 1-5题完整复盘与Volatility使用指南

内存取证这个方向,入门时最大的错觉是:拿到一个几百MB甚至几GB的内存镜像,总想着能有一个按钮直接把“问题”刷出来。实际上,前半小时往往都在跟工具、格式和一堆莫名其妙的偏移量搏斗。OtterCTF的1到5题,是我个人认为很适合系统过一遍的入门序列,它把“镜像识别、进程分析、网络行为、内存注入、文件恢复”这几个取证核心场景串了起来。这篇复盘就按我做题时的完整流程,把每一步为什么这么做、工具怎么选、坑点在哪里全部摊开,给同样在内存取证上踩坑的人一份能直接抄作业的参考。

1. 开局准备:镜像类型、工具版本与Profile识别思路

1.1 先别急着跑插件,把镜像“身份”确认了

拿到 OtterCTF.vmem 这类镜像文件,第一件事绝对不是直接跑pslist,而是确认镜像本身是什么格式、什么系统版本。VMware 的.vmem本质上是把客户机物理内存原样导出的 raw 文件,里面会有大量零页和碎片,但内存取证工具根本不关心这些碎片,它关心的是怎么定位到系统的符号和结构体。

我用 Volatility 2.6 的习惯是先跑一条imageinfo:

vol.py -f OtterCTF.vmem imageinfo

这条命令会扫描镜像里的 KDBG 结构,给出建议的系统版本 Profile。OtterCTF 这套题跑出来的建议 profile 基本指向 Win7SP1x64。这一步很关键,因为后面所有插件都需要在正确的 Profile 前提下才能正确解析,选错直接导致命令报错或者输出一堆无意义的数据。

如果你用的是 Volatility 3,对应命令是:

python3 vol.py -f OtterCTF.vmem windows.info

Vol3 的好处是它不要求你手动指定 Profile,会自动下载需要的符号表并识别系统信息,对新手友好很多。但实际做题时,Vol2 仍然是主力,因为它的插件生态更全,文档和历史 writeup 基本都以 Vol2 命令为准。

1.2 两代 Volatility 的选型与交叉验证

有人问我到底该学 Vol2 还是 Vol3,我的建议是:两个都要装,主力用 Vol2,遇到解析异常或者想快速确认结果时用 Vol3 交叉验证。

Vol2 的痛点很明显:它依赖 Python2,在现在的主流系统上跑起来需要额外处理环境,很多新机器默认已经没有 Python2。所以建议直接用一个独立的虚拟环境或者容器来跑 Vol2,避免污染系统 Python3 环境。Vol3 则原生支持 Python3,符号表下载也更现代化,但部分老插件(比如针对 Win7 的某些注册表插件)在 Vol3 里的实现还在完善中。

做题的时候还有个细节:imageinfo给出的 “Suggested Profile(s)” 往往有好几个,比如 Win7SP1x64 和 Win7SP1x86 同时出现。别想太多,按第一个建议的来。如果跑一个简单插件时出现明显乱码或报错,再回过来尝试其他候选 Profile。这就像开车先看仪表盘确认车的型号,型号错了后面的所有操作都白搭。

这一阶段同时要把常用工具的副本准备好:除了 Volatility,还有strings、grep、xxd、file、7zip、Python3 环境以及后续可能用到的离线分析工具。CTF 赛场上时间紧张,别把时间浪费在临时装依赖上。

2. 第1题:字符串深翻与系统基础信息,最不起眼但最管用的入口

2.1 从海量字符串里“捡”第一滴血

第1题往往不会直接问“恶意进程是谁”,而是先让你确认一些系统基础信息,比如操作系统、用户名、机器名甚至某个密码。这种题听起来简单,但很多人一上来就习惯性跑pslist,反而绕了远路。最直接的做法是先用strings翻。

我的习惯是先做一轮字符串提取,带上最少 8 个字符的长度过滤:

strings -n 8 OtterCTF.vmem > otter_strings.txt

镜像文件有好几百 MB,提取出来的字符串文件可能到几百 MB,所以建议直接落盘而不是在终端里看。之后用 grep 逐步缩小范围:

grep -a -i -E "flag|password|passphrase|ctf|key" otter_strings.txt | head -50

注意一定要加-a参数,让 grep 把二进制内容当作文本处理,否则很多包含不可见字符的文本会被跳过。搜索时别只搜英文大小写,CTF 里的 flag 经常隐藏在 Base64、URL 编码或者 ASCII 范围内,所以先看一眼整体内容很重要。

2.2 用户账户、机器名与环境变量

字符串搜索是“面”,系统信息的精确提取则需要用 Volatility 的注册表和用户枚举插件。第1题如果问的是用户名或用户密码,通常可以用:

vol.py -f OtterCTF.vmem --profile=Win7SP1x64 hashdump

hashdump会把 SAM 数据库里的用户哈希导出来,这里面能拿到用户名列表。如果题目只要求某个账户对应的密码原文,那就得结合题目给的提示去撞哈希或者查常见弱密码。

机器名则可以用注册表printkey插件提取:

vol.py -f OtterCTF.vmem --profile=Win7SP1x64 printkey -K "ControlSet001\Control\ComputerName\ComputerName"

这里printkey的-K参数后面跟的是注册表键的完整路径。Vol2 的printkey可以直接用反斜杠路径,比 Vol3 的写法直观很多。除了机器名,envars插件可以看出系统环境变量:

vol.py -f OtterCTF.vmem --profile=Win7SP1x64 envars

环境变量里经常会暴露当前用户名、临时目录、进程启动路径等信息,对后续分析定位很有用。

2.3 为什么第一题先看字符串,而不是直接查进程

原因在于题目设计逻辑:OtterCTF 的内存取证实训 1 到 5 题是递进式的,第1题目的就是让你熟悉镜像、确认身份信息,用一个相对简单的入口把你带进来。先做系统信息确认还有一个实际价值:你可以拿这些信息去验证后续工具输出是否准确。比如你在进程列表里看到一个进程的启动路径含有某个用户名,就能和前面的用户信息对上,分析起来更有底。

实测下来,第1题最大的坑是:字符串搜索太贪心。不加-n限制的话,strings会把大量短小无意义的字符片段也丢给你,像是内存里的随机噪声。这类噪声会让 grep 结果爆炸,但真正的 flag 往往就在更长的字符串里。-n 8是我试过比较平衡的阈值,如果搜不到,再把阈值降到 6 或 4。

3. 第2题:从进程树看异常,进程枚举不能只看 pslist

3.1 pslist 只看到“还活着”的进程

很多初学者做第2题时,会习惯性先跑:

vol.py -f OtterCTF.vmem --profile=Win7SP1x64 pslist

pslist的输出很好看,进程名、PID、父进程、启动时间一应俱全。但它的原理是基于内核的进程活动链表(ActiveProcessLinks)来遍历的,这意味着恶意程序如果做了断链隐藏(unlink),pslist就会漏掉它。在内存取证中,进程枚举至少要跑两套不同机制的插件做交叉验证。

我自己的做题顺序是:pstree->pslist->psxview。pstree可以一眼看出进程之间的父子关系,pslist提供具体细节,psxview则从多个内核数据结构交叉比对进程对象,能暴露某些被隐藏或伪装的进程。

vol.py -f OtterCTF.vmem --profile=Win7SP1x64 pstree

3.2 从父子关系找“孤僻”的可疑进程

真正的关键不是列出所有进程,而是发现“不该出现”或“关系奇怪”的进程。Windows 系统里,explorer.exe通常只作为桌面外壳存在,它的子进程一般是用户手动启动的软件;services.exe的子进程一般是系统服务。如果一个看起来很像系统服务的进程挂在explorer.exe底下,或者某个进程的父进程是空的、父进程 PID 指向一个早已退出的进程,那你就要盯上它。

第2题给我印象最深的地方是:让我见识到“进程名可以伪装,但路径和父子关系很难伪装”。比如一个进程叫svchost.exe,看起来正常,但如果通过dlllist -p <PID>看它加载的模块路径来自用户临时目录,或者通过cmdline看到它的启动参数带了一串不正常的路径,那它就非常可疑。

vol.py -f OtterCTF.vmem --profile=Win7SP1x64 dlllist -p 1234 vol.py -f OtterCTF.vmem --profile=Win7SP1x64 cmdline -p 1234

第2题一般会问类似“哪个进程是恶意的”“恶意进程的 PID 是多少”这类问题。做题时别急着下结论,先把这个可疑进程的 PID、父进程、启动时间、命令行参数记录下来,形成一张简表,后面第3题要接着用。

3.3 psxview 的交叉验证价值

psxview输出的信息量很大,列名涉及 PsActiveProcessHead、PspCidTable、CSRSS 等不同内核结构。简单理解:不同内核结构记录同一进程时,正常进程应该在所有列表中都能找到;如果某个列表里查无此进程,说明它可能在这个维度上做了隐藏。做题时看psxview输出,重点关注哪一行存在False或空白,那往往就是突破口。

不过别以为所有False都代表恶意,有些系统进程本身在某些列表中不存在,属于正常现象。要结合具体进程名和上下文去判断,这也是为什么先跑pstree、先了解正常进程有哪些很重要。

4. 第3题:网络连接 + 命令行参数,把外联行为钉死

4.1 netscan 找出已经建立的连接

到了第3题,你基本上已经从进程列表里锁定了一个或多个可疑进程,接下来就是确认它到底在干什么。内存取证的经典手法是查网络连接。Vol2 里面最常用的是:

vol.py -f OtterCTF.vmem --profile=Win7SP1x64 netscan

netscan扫描内核网络结构,可以找到 TCP 连接、UDP 连接、监听端口等。输出里多关注ESTABLISHED状态的外部 IP 和端口。如果某个进程和外部 IP 保持连接,这个外联行为本身就是强证据。

有些镜像对netscan支持不好时,可以退回老式的两个插件:

vol.py -f OtterCTF.vmem --profile=Win7SP1x64 sockets vol.py -f OtterCTF.vmem --profile=Win7SP1x64 connections

connections只显示 TCP 连接,sockets显示全部套接字。在较新的 Windows 版本上,这两个老插件可能已经无法正常工作,所以我建议优先netscan。

拿到外联 IP 或端口后,第3题通常就直接有答案了。如果题目给了一个 IP 让你反查进程,那就反过来:看哪个进程的 PID 出现在该连接记录里,再回cmdline验证。

4.2 cmdline 和 envars 让进程的“动机”暴露

网络连接只能说明“它在通信”,但为什么通信还要看进程命令行。对可疑进程执行:

vol.py -f OtterCTF.vmem --profile=Win7SP1x64 cmdline -p <PID>

如果这是一个挖矿病毒,命令行里大概率能直接看到矿池地址、钱包地址、组件参数;如果是个反弹 shell,命令行里可能会包含 bash、-e、/bin/sh之类的字样。这些线索结合起来,就可以把“异常进程”定性为“恶意行为”。

envars在某些场景下也很关键,因为它能看到进程的环境变量,包括临时目录、用户配置文件路径、可能的 DLL 搜索路径。有些恶意软件会通过环境变量修改 DLL 加载路径实现劫持,这在envars的输出中可能留下痕迹。

做第3题时最需要注意的坑是:别把所有外部 IP 都当成恶意。正常系统里的浏览器、更新服务、杀毒软件都有外联行为。你要做的是“关联分析”——先把第2题锁定的可疑进程 PID 拿好,再看网络记录里哪些连接属于该 PID,只看它就好,其他连接先放一边。

第3题的取证逻辑很像刑警办案:进程列表是“一个人现在在哪”,网络连接是“他正在给谁打电话”,命令行参数是“他在说什么”。三条线索合在一起,一个完整的动态行为画像就出来了。

5. 第4题:内存里的注入现场,malfind 与导出分析

5.1 malfind 找到具备执行权限的注入区域

第4题开始进入真正“硬核”的进程内存分析阶段。很多恶意软件不会直接释放一个文件,而是把自己陷入正常进程的地址空间里运行。这种注入行为,仅靠第2、3题的网络和命令行是看不出来的,必须直接审计进程内存。

Vol2 的malfind插件专门检测具有“可写 + 可执行”权限的内存区域(PAGE_EXECUTE_READWRITE),并尝试解析该区域开头的指令内容。执行:

vol.py -f OtterCTF.vmem --profile=Win7SP1x64 malfind -p <PID>

输出里如果能看到不正常的 MZ 头(DOS 可执行文件头),那基本可以断定该进程被注入了代码。这类注入区域的内存权限组合非常典型,正常程序很少会自行申请“可写可执行”的内存页,因为这是安全编码规范里的大忌。

第4题往往就会问你:“这段注入内容的起始地址是什么”“注入区域里包含了什么”。所以要会读malfind输出的偏移量和内容摘要。如果输出里包含MZ字样或一长串看起来像 shellcode 的十六进制,那基本就锁定答案了。

5.2 procdump 与 memdump 导出后离线深挖

malfind解决“注入在哪”的问题,但要回答“注入内容是什么”,最好把进程内存导出成文件再分析。Vol2 提供两个导出插件:

vol.py -f OtterCTF.vmem --profile=Win7SP1x64 procdump -p <PID> -D ./output/ vol.py -f OtterCTF.vmem --profile=Win7SP1x64 memdump -p <PID> -D ./output/

procdump导出的是可执行映象,适合直接看 PE 文件本身;memdump导出的是完整进程地址空间,里面包含堆、栈、模块,信息更全。做题时一般先procdump,不够再上memdump。

导出的文件继续做字符串分析:

strings -el ./output/<PID>.dmp > dmp_strings.txt grep -a -i -E "flag|ctf|key|http" dmp_strings.txt

这里-el参数非常关键,它代表提取 UTF-16 编码的字符串。Windows 内部很多字符串(尤其是 flag、密码)都是宽字符存储的,只用默认strings会漏掉一半内容。我第一次做题就是忘了这个参数,导致在进程转储里搜什么都没有,后来加上-el立刻就能搜到线索。

另外一个隐蔽的坑:memdump得到的文件可能非常大,几十 MB 到几百 MB 都有可能,所以输出目录要预留足够空间。磁盘写满会导致导出文件残缺,后续分析全是白搭。

第4题的完整方法论可以总结成一句话:先用malfind找可疑内存区,再用procdump或memdump导出,最后用双编码字符串搜索和十六进制文件头识别来判断内容性质。这一套组合拳下来,绝大多数注入类题目都能拿下来。

6. 第5题:文件对象与注册表,取证的最后一道收口

6.1 filescan + dumpfiles:从内存里“抢救”文件

第5题的侧重点开始从进程转移到文件和注册表。前面的题你可能已经锁定了恶意进程和网络行为,但这个恶意软件是怎么进来的、它的本体文件在哪、它有没有启动项,这些就需要文件系统痕迹来回答。

内存中虽然没有完整的磁盘文件系统,但内核的文件缓存、文件对象和内存映射会留下大量痕迹。Vol2 的filescan能扫描内存中的_FILE_OBJECT结构,从而枚举曾经被打开的文件路径:

vol.py -f OtterCTF.vmem --profile=Win7SP1x64 filescan | grep -i -E "\.exe|\.dll|\.zip|\.txt|\.flag|tmp"

输出结果里会有大量格式化好的记录,每一行都带有一个文件对象偏移。找到可疑文件后,用dumpfiles按偏移量导出:

vol.py -f OtterCTF.vmem --profile=Win7SP1x64 dumpfiles -Q 0x000000000000 -D ./output/

注意-Q后面跟的是文件对象偏移量,只要是filescan输出里对应文件行的前那个 0x 长地址就可以。dumpfiles会导出文件内容,导出后的文件名一般是file.None.0x...dat之类,需要根据文件头部信息判断这是什么类型。看到MZ就是 PE 可执行文件,看到PK就是 zip 压缩包,看到7z就是 7z 压缩包。

6.2 注册表痕迹:hivelist、printkey 与自启动项

文件对象能告诉你“有什么文件在内存里待过”,但没法告诉你“谁让它启动的”。这时候注册表分析就派上用场了。

先用hivelist找到注册表 hive 在内存中的位置:

vol.py -f OtterCTF.vmem --profile=Win7SP1x64 hivelist

拿到ntuser.dat、SYSTEM、SOFTWARE等 hive 的虚拟地址后,再配合printkey遍历指定键值。最常检查的路径是自启动项:

vol.py -f OtterCTF.vmem --profile=Win7SP1x64 printkey -K "Software\Microsoft\Windows\CurrentVersion\Run"

第5题如果把注册表作为考点,答案经常就藏在 Run 键、RunOnce 或者某些服务的 ImagePath 里。恶意软件为了开机自启动,往往会在这些位置写入自己对应的路径。这个键值和前面filescan导出的文件路径一对比,整个攻击链就闭合了。

除此之外,shimcache也是一个常考的点。它记录系统启动时加载过的可执行文件,即使文件后来被删除,这条痕迹一般也还在:

vol.py -f OtterCTF.vmem --profile=Win7SP1x64 shimcache

第5题做下来,你会意识到一件事:内存取证不是单独看某一个层面,文件、进程、网络、注册表都是互相咬合的证据碎片。前面几题分别练单项能力,第5题就是把它们串起来。

6.3 关于文件导出的扩展技巧

dumpfiles导出的文件有时候内容不完整,这是因为内存中的文件对象可能只缓存了部分内容。遇到这种情况,可以尝试用foremost或binwalk对整块导出区域做文件雕刻,或者回到原始镜像里用更大的偏移范围去提取。

另外,如果题目给了你一个“回收到回收站”的文件,filescan的输出里路径通常会包含$Recycle.Bin,这个线索要特别敏感。回收站里的文件也是内存中真实存在过的文件对象,同样可以导出。第5题经常出现这种让人“以为丢了其实没丢”的情况。

7. 复盘:做题顺序、命令速查与踩坑清单

7.1 我个人的标准做题顺序

现在很多新人在内存取证时最大的问题是“不知道下一步干什么”。我自己跑完 OtterCTF 1 到 5 后,把做题顺序固定成了一套流水线:

  1. 先file和imageinfo,确认镜像格式、系统 Profile。
  2. 跑一轮pstree和pslist,建立正常进程基线。
  3. 用cmdline查看可疑进程启动参数,用netscan关联网络外联。
  4. 用malfind检查可疑进程的注入区域,memdump导出后深挖字符串。
  5. 用filescan找文件对象,dumpfiles导出关键文件。
  6. 用hivelist+printkey查注册表启动项和用户痕迹。

这套顺序不是死规矩,但能确保你不会遗漏重大线索。每一题的考点基本都能落在其中某一步上。

7.2 踩过的坑和记住的经验

列几个真实会踩的坑,每个我都试过一次:

Volatility2 的 Python2 环境和依赖问题最烦人,建议直接做成容器或独立目录,别和系统 Python3 混在一起。Volatility3 虽然不用手动指定 Profile,但某些老插件在 Win7 镜像上解析效果不如 Vol2,两把武器都要带。

imageinfo建议的 Profile 不是百分百可靠。如果紧接着跑pslist报 “Cannot find KDBG” 或输出一堆乱码,不要慌,换相邻 Profile 再试,或者直接用 Vol3 交叉验证一下。

strings一定别只跑纯 ASCII。Windows 下很多信息都是 UTF-16LE 编码,用默认strings会漏掉。建议至少跑两遍:

strings -n 8 image > ascii.txt strings -el -n 8 image > utf16.txt

两个文件都留着,搜索时都要搜。这个习惯救了我很多次。

导出文件要预留磁盘空间。一个几 GB 的镜像做memdump,很容易生成 GB 级文件。如果磁盘不够,导出中途报错,之前的所有步骤都前功尽弃。做题之前先df -h看一下空间,这是最基本的自我检查。

最后是心态层面的经验:不要以为命令输出越复杂越好,也不要被海量信息吓住。内存取证本质上就是“从噪音中找信号”。OtterCTF 1 到 5 这套题的编排很友好,它逼着你一步步来,每一步只用关注一个侧面。当你把第1题的系统信息、第2题的异常进程、第3题的外联地址、第4题的注入内容、第5题的文件和注册表痕迹全部串联起来,你就会发现,原来一段恶意行为在内存里留下的脚印,远比你想象的要多得多。这个串联能力,比多背几个插件命令重要得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进