4、进程数据提取:进程创建与终止事件、进程树构建、命令行参数提取、进程哈希计算
进程,是操作系统里最活跃的实体。你想想看,每一次点击、每一次命令执行,背后都是进程在干活。但对我们做安全的人来说,进程也是攻击者最常用的“马甲”。恶意代码总要运行起来才能搞破坏,对吧?所以,把进程数据提取干净、分析透彻,是EDR取证的核心基本功。
我个人习惯,拿到一份终端日志,第一件事就是看进程列表。不是随便扫一眼,而是要把进程的“前世今生”都扒出来。这一章,我们就来聊聊怎么干这活。
4.1 进程创建与终止事件
进程的一生,无非就是“生”和“死”。但这两个瞬间,恰恰是证据最集中的地方。
进程创建事件,EDR通常会在操作系统内核层面挂钩子。比如Windows上,会用PsSetCreateProcessNotifyRoutine来注册回调。一旦有新进程诞生,EDR就能抓到以下关键信息:
- 进程ID(PID):唯一标识,但会复用,要注意时间戳。
- 父进程ID(PPID):谁生了它?这是溯源的关键。
- 映像路径:可执行文件在磁盘上的位置。
- 命令行:启动时带了什么参数。
- 时间戳:创建时间(精确到微秒最好)。
- 用户SID:以哪个用户身份运行的。
- 会话ID:属于哪个登录会话。
重点:进程创建事件是“白名单”思路的基石。你只要知道正常系统里有哪些进程会启动,异常的就很容易露馅。
进程终止事件,很多人觉得不重要——进程都死了,还查什么?其实不然。我在项目中遇到过好几次,恶意进程执行完就自毁,但终止事件里会留下退出码和运行时长。有些攻击者会利用“自杀”来逃避检测,但终止事件告诉我们:它曾经来过,而且走得可能并不安详。
终止事件通常包含:
- 进程ID
- 退出码(0通常表示正常退出,非0可能有异常)
- 终止时间
- CPU总时间、内存峰值(侧面反映它干了多少活)
小技巧:把进程创建和终止事件配对,可以算出进程的“存活时间”。如果一个进程只活了0.1秒就结束了,嗯,这很可疑——正常程序不会这么“短命”。
4.2 进程树构建
单个进程的数据,就像拼图里的一块。你得把它们拼起来,才能看清全貌。进程树,就是那张拼图。
进程树的核心逻辑很简单:通过PPID把进程串起来。根节点通常是systemd(Linux)或winlogon.exe(Windows),然后一层层往下长。
我一般会这样构建:
- 收集所有进程创建事件,建立
PID -> 进程信息的字典。 - 遍历字典,对每个进程,找到它的PPID对应的父进程。
- 把子进程挂到父进程的“孩子列表”里。
- 从根节点开始,递归打印或可视化。
这里有个坑,我吃过亏——进程ID复用。操作系统会回收PID,如果日志跨度时间长,同一个PID可能对应不同的进程。所以,构建进程树时,一定要用(PID, 创建时间戳)作为唯一键,而不是只用PID。
注意:有些恶意软件会“借壳生蛋”,比如通过explorer.exe启动,但父进程其实是cmd.exe。这时候,只看PPID可能会被误导。我建议结合命令行参数和用户行为一起分析。
下面是我用Python写的一个简单进程树构建示例:
def build_process_tree(events):
# events: list of dict, each with pid, ppid, cmdline, timestamp
tree = {}
for ev in events:
pid = ev['pid']
ppid = ev['ppid']
node = {
'pid': pid,
'ppid': ppid,
'cmdline': ev['cmdline'],
'children': []
}
tree[pid] = node
# 挂载子节点
root = None
for pid, node in tree.items():
ppid = node['ppid']
if ppid == 0 or ppid not in tree:
root = node
else:
tree[ppid]['children'].append(node)
return root
def print_tree(node, indent=0):
print(' ' * indent + f"PID:{node['pid']} Cmd:{node['cmdline']}")
for child in node['children']:
print_tree(child, indent + 2)
你看,代码其实不复杂。但真正落地时,要考虑事件乱序、丢失、重复等问题。我一般会先按时间戳排序,再用滑动窗口去重。
4.3 命令行参数提取
命令行参数,是攻击者最常“做手脚”的地方。你想想看,一个正常的notepad.exe,命令行就是notepad.exe。但如果变成notepad.exe /c "powershell -enc ...",那就有问题了。
提取命令行参数,有几种方式:
- Windows:通过
WMI的Win32_Process类,或者ETW(Event Tracing for Windows)的Microsoft-Windows-Kernel-Process提供程序。ETW能拿到完整的命令行,包括空格和引号。 - Linux:读取
/proc/[pid]/cmdline文件,注意里面用\0分隔参数。也可以用auditd的SYSCALL事件来捕获execve的参数。 - macOS:通过
sysctl或proc_infoAPI获取。
关键点:命令行参数里经常藏着IP地址、域名、Base64编码的Payload、甚至是明文密码。我建议提取后,立刻做一次简单的敏感信息扫描。
我曾经处理过一个案例,攻击者把恶意DLL的路径藏在命令行参数里,用rundll32.exe加载。命令行长这样:
rundll32.exe C:\Users\Public\cache.dll,EntryPoint
如果不提取命令行,只看进程名rundll32.exe,你根本发现不了异常。所以,命令行参数提取,是EDR数据提取里绝对不能省的一步。
4.4 进程哈希计算
哈希,说白了就是给文件办个“身份证”。MD5、SHA1、SHA256,都是常用的算法。进程哈希计算,就是计算可执行文件的哈希值,然后去威胁情报平台查一查——这个文件是白名单里的,还是已知恶意软件?
计算哈希,有两点要注意:
- 算哪个文件? 当然是进程映像路径指向的那个文件。但要注意,有些进程会自解压或动态生成代码,这时候映像路径可能只是个“壳”。我建议同时计算内存中进程的哈希(通过读取进程内存),但那个开销比较大,一般只在深度调查时做。
- 什么时候算? 最好在进程创建时就算,因为文件可能很快被删除或修改。有些恶意软件会“下载-执行-自删除”,如果你等进程跑完了再去算,文件已经没了。
下面是一个用Python计算文件哈希的示例:
import hashlib
def calculate_file_hash(filepath, algorithm='sha256'):
h = hashlib.new(algorithm)
try:
with open(filepath, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b''):
h.update(chunk)
return h.hexdigest()
except FileNotFoundError:
return None
except PermissionError:
return None # 权限不够,常见于系统进程
建议:不要只算一种哈希。我一般会同时算SHA256和SHA1。SHA256用于精确匹配,SHA1用于兼容老旧的情报系统。MD5虽然快,但已经不安全了,不建议作为唯一依据。
哈希计算的结果,通常会跟威胁情报联动。比如,把SHA256丢给VirusTotal,看看有多少家引擎报毒。但要注意,哈希匹配不是万能的。攻击者稍微改一个字节,哈希就变了。所以,哈希只是线索之一,不能作为定罪的全部证据。
知识体系总览
为了让你更直观地理解这一章的内容,我画了一张图。它展示了进程数据提取的四个核心模块,以及它们之间的关系。
这张图里,四个模块是并列的,但实际分析时,我通常会先看进程创建事件,然后构建进程树,再提取命令行参数,最后算哈希。这个顺序,能帮你最快地发现异常。
好了,这一章的内容就到这里。进程数据提取,是EDR分析的地基。地基打不牢,后面盖什么楼都容易塌。希望你能把这些方法用到实际工作中,多动手,多踩坑,慢慢就有感觉了。