ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Python的HIDS源码架构:文件监控、进程扫描与WebShell检测

基于Python的HIDS源码架构:文件监控、进程扫描与WebShell检测 简介基于Python实现的主机入侵检测系统源码面向计算机相关专业毕业设计、课程设计与项目开发场景主要解决主机侧文件监控与进程安全检测两大核心问题适合需要快速理解入侵检测原理并落地演示项目的开发者参考。系统功能覆盖文件监控、多进程目录监控、进程状态检测、提权进程识别以及基于ssdeep相似度匹配的Webshell静态检测并配有日志模块记录运行状态整体方案具有一定工程完整度。资源包共374个文件以342个pyc编译文件和16个py源码文件为主体pyc便于直接运行调用py源码可读可改另含yaml配置、xml工程配置、txt说明、license等辅助文件压缩包仅1.83MB目录结构清晰便于按模块定位代码并二次开发。项目源码经过严格测试可直接展开学习或在此基础上加入更多检测策略已有488人学习下载。1. 为什么一套不太完善的 Python HIDS 源码值得当骨架一台 Ubuntu 20.04 服务器Web 目录被塞了一个经过 base64 混淆的 PHP 一句话木马进程列表里多出来一个用户名为 root 的 python3而你手上的监控脚本只盯着 CPU 和内存。这是我拿到这套基于 Python 的主机入侵检测系统源码后最先想验证的场景。它不大main.py 只负责调度fileMonitoring.py 管文件系统事件processScan.py 做进程快照webshellScan.py 用 ssdeep 相似度查 WebShelllogger.conf 统一把异常写进日志。与其装一个开箱即用但改不动规则的企业级 HIDS不如拿它当毕业设计、课程设计的地基先看懂主机侧检测由哪几个独立模块组成再按自己的数据源扩展。2. 源码骨架与配置层yaml 先行logger 跟上2.1 为什么配置要放 yaml而不是堆在 Python 常量里项目源码的文件分工很直观main.py 是入口fileMonitoring.py、processScan.py、webshellScan.py 是三个检测执行者fileCalculate.py 提供文件哈希能力logger.conf 控制日志输出。源码包里没有直接给出 config.yaml 文件但开发进度里明确了“采用 yaml 格式作为配置文件”所以拿到源码后第一件事不是急着跑报警逻辑而是先补齐配置层。把监控目录、进程轮询间隔、webshell 相似度阈值从 Python 常量里抽出来意味着换一台机器部署时不需要改任何 py 文件这在课程设计答辩现场是个很加分的点。我一般会把 config.yaml 写成下面这个样子字段比源码里默认的更完整方便不同同学按自己的实验环境调整monitor: roots: - /data/www - /data/lib ignore_dirs: - .git - node_modules extensions: - .php - .jsp - .sh - .py debounce_seconds: 1.5 max_workers: 4 process: interval: 5 root_uid: 0 alert_on_new: true alert_on_exit: true webshell: db_dir: ./malware_samples threshold: 80 extensions: - .php - .jsp - .asp logging: config: logger.conf level: INFO这段配置的关键不是字段数量而是把三个检测器的运行参数都收口了。monitor.roots 控制要盯哪些目录ignore_dirs 避免监控到 .git 这类高频变更目录拉高日志量process.interval 是进程快照轮询间隔直接影响 CPU 开销webshell.threshold 是 ssdeep 相似度的报警线后面会专门讲它为什么不能从网上抄一个固定值。配置层最大的价值是让 main.py 启动时只做一次加载然后把相同结构传给不同模块哪个模块出问题就在对应配置块里找原因。2.2 main.py 把三个扫描器串起来的方式main.py 在我的预期里应该是一个很薄的启动调度器而不是把检测逻辑全写在里面。常见的做法是给三个检测器各建一个进程进程之间不共享内存只通过日志文件交换信息。这样做的好处是 psutil 的进程遍历、ssdeep 的文件读入都可能阻塞几十毫秒甚至更久如果放在线程里一个阻塞点会影响另一个检测器的实时性而独立进程崩溃后也不会拖垮整个程序适合演示时快速定位是哪个环节挂的。# main.py 的启动调度思路 import logging from multiprocessing import Process from fileMonitoring import start_file_monitor from processScan import start_process_scanner from webshellScan import start_webshell_scanner def load_yaml(path): import yaml with open(path, r, encodingutf-8) as f: return yaml.safe_load(f) def main(): cfg load_yaml(config.yaml) logging.config.fileConfig(cfg[logging][config]) targets [ Process(targetstart_file_monitor, args(cfg[monitor],), namefile-monitor), Process(targetstart_process_scanner, args(cfg[process],), nameprocess-scanner), Process(targetstart_webshell_scanner, args(cfg[webshell],), namewebshell-scanner), ] for t in targets: t.start() for t in targets: t.join() if __name__ __main__: main()这段代码里每个 Process 的 name 参数很关键它不只是给进程起名字在 ps 看进程列表时能直接区分哪个进程对应哪个检测器。args 传入的是配置块的 dict所以 fileMonitoring 只关心 monitor 字段processScan 只关心 process 字段模块之间的耦合被配置层切开。join() 阻塞在这里是有意为之如果某个子进程异常退出main.py 不会立刻退出方便你手动 kill 主进程后看日志判断谁先挂了。实际开发时还可以在这里加个 Event 或 Queue 做子进程心跳但对毕业设计来说这个骨架已经足够你画架构图了。模块之间的数据流可以先用一张表理清楚写报告时也直接能用模块入口函数主要职责关键依赖fileMonitoring.pystart_file_monitor监控目录创建、删除、修改、移动事件watchdog, fileCalculateprocessScan.pystart_process_scanner快照进程列表diff 出新进程和退出进程psutilwebshellScan.pystart_webshell_scanner扫描脚本文件与样本库做 ssdeep 相似度比对ssdeep, fileCalculatefileCalculate.pysha256对大文件分块计算哈希供文件监控调用hashlib这张表写完你就会发现真正的检测逻辑其实不在 main.py而在三个子模块里。main.py 只是把三个本可以独立运行的脚本包进了一个进程组这就是主机入侵检测系统最基础的形态。2.3 日志模块在检测器里的位置logger.conf 在这个项目里承担着告警输出的责任。检测器不是在控制台 print 一行就完了而是要通过 logging 写到文件否则进程一重启告警就丢了。我一般会把 logger.conf 配成 RotatingFileHandler按大小轮转避免 hids.log 无限增长挤爆磁盘[formatters] keysdefault [formatter_default] format%(asctime)s %(levelname)s %(name)s %(message)s [handlers] keysfile [handler_file] classhandlers.RotatingFileHandler args(hids.log, a, 10485760, 5) formatterdefault [loggers] keysroot [logger_root] handlersfile levelINFO注意 args 里的四个值文件名 hids.log打开模式 a单文件最大 10MB保留 5 个备份。这样日志最多占 60MB 左右不会把贡献磁盘空间当成事故。日志格式里带上 name 字段非常重要因为三个模块都在往同一个 logger 写没有 name 就分不清一条告警来自文件监控还是进程监控排错时只能靠猜。3. 文件监控watchdog 事件处理与多进程目录分摊3.1 watchdog 的事件模型与防抖取舍文件监控选 watchdog 而不是自己写 inotify原因很实际watchdog 在 Linux 上基于 inotify在 macOS 和 Windows 上有各自后端写一遍代码可以在三种环境跑而 inotify 本身只是 Linux 专有。这个项目的进度里提到“完成多进程监控主机文件夹及其子文件夹中的文件采用一个进程监控一个文件夹的方式”这正好是 watchdog 最舒服的使用方式。核心是继承 FileSystemEventHandler覆写 on_created、on_modified、on_deleted、on_moved 四个方法。# fileMonitoring.py 中的事件处理类省略了导入和 logger 初始化 import time from watchdog.events import FileSystemEventHandler class ChangeHandler(FileSystemEventHandler): def __init__(self, dirty_ext(.php, .jsp, .sh)): self.dirty_ext dirty_ext self._recent {} def _debounce(self, path, window1.5): now time.monotonic() if now - self._recent.get(path, 0) window: return True self._recent[path] now return False def on_created(self, event): if event.is_directory or not event.src_path.endswith(self.dirty_ext): return if self._debounce(event.src_path): return logging.warning(file created: %s, event.src_path) def on_modified(self, event): if event.is_directory or not event.src_path.endswith(self.dirty_ext): return if self._debounce(event.src_path): return digest fileCalculate.sha256(event.src_path) logging.warning(file modified: %s sha256%s, event.src_path, digest[:16])事件回调里第一个判断都是 is_directory因为创建目录和创建文件的告警级别完全不同目录事件往往由 git clone、npm install 这类批量操作引发一次能刷几百条直接忽略掉更干净。后缀过滤把范围限制在可执行脚本和 Web 脚本避免 .txt、.log 这些文件频繁写入导致日志爆炸。_debounce 方法是防抖用的某文件正在被编辑器写入时会连续触发 modified如果把每次回调都记日志一个文件保存就能刷二十条记录1.5 秒内的重复事件直接丢掉只在事件稳定后记录一次。这里用 time.monotonic 而不是 time.time原因是 time.time 可能被用户或 NTP 回拨monotonic 保证只增不减比较差值才可靠。3.2 多进程按目录分摊监控负载Watchdog 的 Observer 本身是一个线程在单目录场景下够用。但这个项目的设想是同时监控多个根目录比如 /data/www 和 /data/lib如果用一个 Observer 的 schedule 加两个目录一个目录下的 inotify 事件流量过大时会影响另一个目录的响应。源码进度里的方案是“一个进程监控一个文件夹”这个决策在 Linux 上很实用因为每个进程都有独立的 inotify 实例单个实例的文件描述符上限不会互相挤占。# 多进程文件监控的调度写法 from multiprocessing import Process from watchdog.observers import Observer def start_one_watcher(root, cfg): handler ChangeHandler(tuple(cfg.get(extensions, [.php]))) observer Observer(timeout1) observer.schedule(handler, root, recursiveTrue) observer.start() observer.join() def run(cfg): procs [] for root in cfg[roots]: p Process(targetstart_one_watcher, args(root, cfg), daemonTrue) p.start() procs.append(p) return procsobserver.schedule 里的 recursiveTrue 表示递归监控子目录这相当于 inotify 需要手动为每个子目录添加 watchwatchdog 帮你做了这一层管理。timeout1 是 Observer 内部线程在无事件时的等待秒数调大一点可以减少空轮询但也会稍微延迟事件触发我一般不超过 2 秒。daemonTrue 让子进程随主进程退出而结束不会留下僵尸进程在课程设计演示时不用担心 CtrlC 后残留一堆 python 进程。文件事件里容易被忽略的是 on_moved一次 mv 操作在跨文件系统时会变成 create 和 delete 两个事件只监听单个事件类型会漏掉移动后的路径。我建议在事件处理表里把四个事件都列为必处理项事件方法触发条件建议处理on_created文件或目录创建后缀命中则记录不记录目录on_modified文件内容或属性变更计算哈希并记录防抖后再处理on_deleted文件或目录删除记录删除路径关联基线检查on_moved文件改名或移动同时记录 src_path 和 dest_path3.3 变更后的哈希校验与基线数据文件被修改后只知道路径还不够需要知道内容变成了什么。fileCalculate.py 承担的就是这个职责。对 Web 目录里的 php 文件哈希变化不能简单当成攻击信号因为业务代码本身会更新但对一个被监控的系统目录来说哈希变更往往意味着可疑写入。计算哈希时要注意不能一次性读入整个文件一个几十 MB 的日志文件会把内存瞬间打满# fileCalculate.py 中的 sha256 实现 import hashlib def sha256(path, chunk1024 * 1024): h hashlib.sha256() with open(path, rb) as f: while True: block f.read(chunk) if not block: break h.update(block) return h.hexdigest()chunk 参数控制每次读入的字节数1MB 是个常见的折中点。for 循环里如果 block 为空说明读到了文件末尾break 退出。返回值是完整的 64 位十六进制哈希在日志里取前 16 位足够展示和人工比对完整哈希可以存到 JSON 里作为基线。这里要注意的是哈希计算本身会触发一次文件读取在事件回调里同步算哈希可能阻塞 Observer 线程样本量大的时候应该把hash 计算丢到线程池让文件监控回调立刻返回否则后面的事件排队时间越来越长。4. 进程监控与 webshell 静态检测psutil 快照与 ssdeep 阈值4.1 用 psutil 的 process_iter 做快照差异进程监控最常见的实现是每隔几秒遍历一次进程表和上一次快照做差别比较。这个项目用 psutil 是顺理成章的选择它封装了 /proc 文件系统的细节跨平台都能跑。processScan.py 的核心是两件事抓快照、做 diff。# processScan.py 中的快照与差异比较 import psutil def scrape_process_snapshot(): snapshot {} attrs [pid, name, username, create_time] for p in psutil.process_iter(attrs): try: snapshot[p.info[pid]] { name: p.info[name], username: p.info[username], create_time: p.info[create_time], } except (psutil.NoSuchProcess, psutil.AccessDenied): continue return snapshot def diff_process(old, new): for pid, info in new.items(): if pid not in old: logging.warning(new process: pid%s name%s user%s, pid, info[name], info[username]) for pid, info in old.items(): if pid not in new: logging.warning(process exited: pid%s name%s, pid, info[name])psutil.process_iter 传 attrs 参数是为了只抓需要的字段少开一轮 /proc/ /status 的系统调用遍历几百个进程时速度差距明显。try/except 是必须的因为进程可能在你遍历到一半时退出或者属于其他用户导致你没有权限读取它的 username这两种情况在 psutil 里分别抛 NoSuchProcess 和 AccessDenied忽略掉比让整个扫描器崩掉合理。diff 逻辑本身不复杂但要注意 pid 复用问题一个进程退出后它的 pid 可能被新进程立刻占用所以快照里除了 pid 还要带 create_time否则会把老进程的退出误判成新进程的创建。4.2 提权进程识别不完善在哪源码进度里写了“添加提权进程识别功能暂不完善”这句话在真实环境里意味着误报率会很高。最常见做法是只看 username 字段是不是 root但普通用户通过 sudo 执行命令后psutil 返回的 username 是目标 uid 对应的用户名 root不是启动时的用户。反过来在 Docker 容器里容器内 root 映射到宿主机普通用户username 可能不是 root。所以靠 username 判断提权本质上只是“进程是不是以 root 身份运行”而不是“谁把权限提上来的”。更可靠的方向是读 /proc/ /status 里的 Uid 行它有 real、effective、saved、fs 四组值。当 effective uid 是 0 而 real uid 不是 0 时说明这个进程是通过 setuid 或 sudo 切换了身份这才是真正的提权信号。当前源码没做这一步拿到的源码如果有这部分代码建议往这个方向改而不是在 username 上补规则。4.3 ssdeep 相似度阈值不能拍脑袋webshellScan.py 用的是 ssdeep 做模糊哈希比对它的原理是把文件切分成若干片段每个片段生成一个哈希值再通过对比片段相似度给出 0 到 100 的分数。不同于 sha256 这种加密哈希模糊哈希对修改了几个字符的文件依然能给出较高相似度所以适合检测经过简单混淆的 webshell 变种。# webshellScan.py 的相似度扫描逻辑 import ssdeep from pathlib import Path def scan_directory(scan_dir, db_dir, threshold): samples list(Path(db_dir).glob(*.php)) targets list(Path(scan_dir).rglob(*.php)) for sample in samples: sample_hash ssdeep.hash(sample.read_text(encodingutf-8, errorsignore)) for target in targets: target_hash ssdeep.hash(target.read_text(encodingutf-8, errorsignore)) score ssdeep.compare(sample_hash, target_hash) if score threshold: logging.warning(webshell candidate: %s sample%s score%s, target, sample.name, score)这里有一个容易踩的坑ssdeep 对短文件效果很差小于 4KB 的文件生成哈希的片段太少比较分数基本在 0 到 10 之间一个十几行的一句话木马很可能直接被漏掉。所以 webshell 静态检测只能是辅助不能当成唯一判断依据。threshold 参数调多少需要根据样本分布决定不要盲从网上的经验值阈值范围效果适用场景60-70召回高、误报严重样本库小宁错杀75-85相对平衡一般 Web 目录90 以上误报少、漏报高确认已知家族变体不作为扫描主力我建议先拿 20 个正常 php 文件和 20 个已知 webshell 做一次分数分布统计找到两类文件分数重叠最少的点作为阈值。同时样本库里的文件要定期更新把网上新出现的公开 webshell 加进去否则 ssdeep 只能匹配旧家族。4.4 三个模块的联动判断单个模块都有明显误报面文件监控发现新增 php 可能是正常上传进程监控发现新进程可能是 cron 任务webshell 相似度高可能是自己写的代码。但三个信号在短时间内同时命中比如同一目录下新增了 phpp、新起了 php-fpm 子进程、ssdeep 相似度超过 75那几乎可以确定要人工介入。联动逻辑可以直接写在 main.py 的循环里定时读取三个模块的日志文件做时间窗口关联也可以做成一个独立 alert.py。当前源码没有把联动做完整这恰恰是拿来扩展的好位置。5. 验证与排错watchdog 丢事件与 ssdeep 阈值校准5.1 用最小样本验证三件事拿到源码先不要急着挂到正式目录先建一个测试目录确认文件监控和日志链路是通的。下面这段命令会创建测试 Web 目录并写入一个带请求参数的 php 文件然后等待两秒后查看日志mkdir -p /data/www echo ?php eval($_REQUEST[cmd]); ? /data/www/test.php sleep 2 tail -n 20 ./hids.log如果日志里没有新增文件的记录先检查你是不是真把 /data/www 配进了 config.yaml 的 monitor.roots再看 watchdog 是否以 root 权限运行因为 /data/www 的读权限不够会导致 inotify 事件不触发。用tail -f直接观察日志比反复打开文件更高效。5.2 校准 ssdeep 阈值的做法用下面这段脚本把目标目录和样本库的相似度分数全部排序输出前 15 条就能看个大概python3 - PY import ssdeep from pathlib import Path samples {p.name: ssdeep.hash(p.read_text(errorsignore)) for p in Path(./db).glob(*.php)} scores [] for target in Path(./scan).glob(*.php): th ssdeep.hash(target.read_text(errorsignore)) for name, sh in samples.items(): scores.append((target.name, name, ssdeep.compare(sh, th))) for s in sorted(scores, keylambda x: x[2], reverseTrue)[:15]: print(s) PY这里用 heredoc 临时跑 Python不污染源码。如果正常文件也出现在高分段说明样本库里包含了与业务框架同源的文件需要把样本库调整成真正的 webshell。如果所有分数都低于 70阈值就调低到 60但要在文档里注明误报率会上升。5.3 常见坑inotify 上限与 ssdeep 安装watchdog 收不到事件时先查cat /proc/sys/fs/inotify/max_user_watches默认 8192 对大型目录很容易耗尽。临时调大用sysctl fs.inotify.max_user_watches524288持久化要写进 /etc/sysctl.conf。ssdeep 的 pip 包需要 libfuzzyLinux 系统安装 python 依赖前先执行apt install libfuzzy-dev否则编译会直接报错。Windows 上编译更麻烦测试阶段把 webshellScan 丢到 Linux 容器里跑文件监控和进程监控留在本地是成本最低的拆法。本文还有配套的精品资源点击获取
返回列表