devylbr/puppetlab

GitHub: devylbr/puppetlab

一个基于 Go 的恶意软件分析沙箱后端,通过在隔离的 KVM/QEMU 虚拟机中引爆可疑样本并采集行为数据,自动生成评分分析报告。

Stars: 0 | Forks: 0

# PuppetLab 🦠 一个极简但功能完备的 **恶意软件分析沙箱** 后端,使用 Go 编写。你 提交一个文件,它会由 worker 进行“引爆”,然后你会获得一份评分过的 行为报告(进程、网络、文件改动、判定结果)。 - **apiserver** — REST API:上传样本、创建/查看任务、获取报告。 - **worker** — 从 PostgreSQL 的 `SKIP LOCKED` 队列中拉取任务,通过可插拔的执行器进行**引爆**, 对行为进行评分,并生成 JSON 报告。 - **PostgreSQL** — 存储样本、任务队列和报告。 提供两种引爆驱动,通过 `PUPPETLAB_EXECUTOR` 选择: | 驱动 | 作用 | 构建 | |--------|--------------|-------| | `sim` (默认) | 纯 Go 模拟引爆。可在任何地方运行,无需 Hypervisor。 | `go build` | | `libvirt` | 通过 libvirt + QEMU guest agent 进行**真实的 KVM/QEMU** 引爆。 | `go build -tags libvirt` | 目前尚无前端。 ## 目录 - [状态](#status) - [工作原理](#how-it-works) - [项目结构](#project-layout) - [前置条件](#prerequisites) - [快速开始 (Docker)](#quick-start-docker) - [本地运行 (无 Docker)](#run-locally-no-docker) - [配置](#configuration) - [API](#api) - [任务队列 (SKIP LOCKED)](#the-job-queue-skip-locked) - [真实 VM 执行 (libvirt)](#real-vm-execution-libvirt) - [测试](#testing) - [命令参考](#command-reference) ## 状态 | 领域 | 状态 | |------|-------| | API (`/samples`, `/tasks`, `/tasks/{id}`, `/tasks/{id}/report`) | ✅ 正常工作,已通过端到端验证 | | Postgres `SKIP LOCKED` 队列 + 租约 + 清理线程 | ✅ 正常工作,已通过并发 worker 验证 | | `sim` 引爆驱动 | ✅ 正常工作,已通过端到端验证 | | 行为评分 + 解析器 (`ps`/`ss`/`find`,guest-agent 协议) | ✅ 已进行单元测试 | | `libvirt` 驱动 (编排) | ✅ 通过模拟的 guest agent 进行了单元测试 | | `libvirt` 驱动 (针对真实 VM) | ⚠️ 需要 KVM 宿主机和客户机才能编译 (`-tags libvirt`, cgo) 并运行 — 参见 [真实 VM 执行](#real-vm-execution-libvirt) | ## 工作原理 ``` client ──POST /samples──▶ apiserver ──row──▶ Postgres(tasks: queued) client ──POST /tasks────▶ apiserver ▲ │ │ claim (FOR UPDATE SKIP LOCKED) │ ▼ worker ◀─────────┘ detonate sample │ (sim OR libvirt/KVM) ▼ Observations {processes, network, files} │ score -> verdict ▼ Postgres(tasks: completed, report JSONB) client ──GET /tasks/{id}/report─▶ apiserver ──▶ report ``` worker 每次领取一个任务,获取样本字节,将其引爆, 将原始的**观测数据 (Observations)** 转化为评分过的**报告 (Report)**,并进行存储。`sim` 和 `libvirt` 驱动可以在相同的 `Executor` 接口下互换使用。 ## 项目结构 ``` puppetlab/ ├── cmd/ │ ├── apiserver/main.go # HTTP API entrypoint │ └── worker/main.go # queue worker entrypoint ├── internal/ │ ├── api/ # HTTP server + handlers │ │ ├── server.go │ │ └── handlers.go │ ├── config/config.go # env config │ ├── db/ # pgx pool + embedded migration │ │ ├── db.go │ │ └── schema.sql # embedded & applied on startup │ ├── models/models.go # Sample, Task, Report types │ ├── store/store.go # all SQL incl. the SKIP LOCKED claim │ ├── sandbox/ # detonation executors │ │ ├── executor.go # Executor interface + driver factory │ │ ├── sim.go # simulated driver (default) │ │ ├── detonate.go # driver-agnostic orchestration (unit-tested) │ │ ├── qga.go # QEMU guest-agent protocol + output parsers │ │ ├── score.go # behavior -> verdict scoring │ │ ├── libvirt.go # real KVM/QEMU driver (//go:build libvirt) │ │ ├── qga_test.go # parser + scoring tests │ │ └── detonate_test.go # full detonation flow vs a fake guest agent │ └── worker/worker.go # poll loop: fetch sample -> detonate -> report ├── migrations/001_init.sql # standalone copy of the schema ├── Dockerfile # default image (sim driver), builds both binaries ├── Dockerfile.libvirt # worker image with the real libvirt driver ├── docker-compose.yml # postgres + apiserver + worker (sim) └── go.mod / go.sum ``` ## 前置条件 - **Docker + Docker Compose** — 最简单的方式(自带 Postgres)。 - 或者 **Go 1.23+** 以及一个可连通的 **PostgreSQL 13+** 以便在本地运行。 - 对于 `libvirt` 驱动:需要一台装有 `libvirtd`、`libvirt-dev` (cgo) 的 Linux **KVM 宿主机**,以及一台准备好的客户机 VM。完整设置请参阅 [真实 VM 执行](#real-vm-execution-libvirt)。 ## 快速开始 (Docker) ``` docker compose up --build ``` 启动 PostgreSQL、运行在 `:8080` 的 API,以及一个 `sim` worker。扩容 worker 以 查看 `SKIP LOCKED` 队列如何分散负载(同一任务不会被运行两次): ``` docker compose up --build --scale worker=3 ``` 停止: ``` docker compose down # keep the database volume docker compose down -v # wipe the database too ``` ## 本地运行 (无 Docker) 需要 Go 1.23+ 以及一个你可以访问的 PostgreSQL。 ``` createdb puppetlab # or point DATABASE_URL at any existing db export DATABASE_URL="postgres://puppet:puppet@localhost:5432/puppetlab?sslmode=disable" go run ./cmd/apiserver # terminal 1 go run ./cmd/worker # terminal 2 (run several for concurrency) ``` schema 会在启动时自动创建(内嵌的 `schema.sql`,在 advisory lock 下应用,因此 apiserver 和 worker 可以同时运行它)。如果想手动应用,请执行:`psql "$DATABASE_URL" -f migrations/001_init.sql`。 ## 配置 所有配置均通过环境变量进行。 | 变量 | 默认值 | 使用者 | 描述 | |----------|---------|---------|-------------| | `DATABASE_URL` | `postgres://puppet:puppet@localhost:5432/puppetlab?sslmode=disable` | 两者均有 | Postgres 连接字符串 | | `PORT` | `8080` | apiserver | HTTP 监听端口 | | `PUPPETLAB_EXECUTOR` | `sim` | worker | `sim` 或 `libvirt` | | `LIBVIRT_URI` | `qemu:///system` | worker (libvirt) | libvirt 连接 URI | | `VM_DOMAIN` | — | worker (libvirt) | libvirt domain (VM) 名称 | | `VM_SNAPSHOT` | — | worker (libvirt) | 每次运行前要恢复到的快照 | | `GUEST_SAMPLE_PATH` | `/tmp/sample` | worker (libvirt) | 样本写入客户机的路径 | | `WATCH_DIRS` | `/tmp,/root,/home` | worker (libvirt) | 为检测文件改动而进行差异对比的客户机目录 | | `RUN_TIMEOUT_SECONDS` | `60` | worker (libvirt) | 每次引爆的最大墙上时间 | ## API | 方法 | 路径 | 描述 | |--------|------|-------------| | `POST` | `/samples` | 上传样本(multipart `file`,或原始请求体)。返回 SHA-256。 | | `POST` | `/tasks` | 为已存储的样本创建分析任务。返回 `task_id`。 | | `GET` | `/tasks/{id}` | 任务状态:`queued` / `running` / `completed` / `failed`。 | | `GET` | `/tasks/{id}/report` | JSON 报告(完成前返回 `409`)。 | | `GET` | `/healthz` | 存活检测。 | ### 端到端示例 ``` # 1) 上传样本(原始请求体;-F file=@path 也可行) SHA=$(curl -s -X POST "localhost:8080/samples?filename=test.sh" \ --data-binary "echo hello" \ | sed -n 's/.*"sha256":"\([a-f0-9]*\)".*/\1/p') # 2) 创建分析任务 TID=$(curl -s -X POST localhost:8080/tasks \ -H 'Content-Type: application/json' \ -d "{\"sample_sha256\":\"$SHA\"}" \ | sed -n 's/.*"task_id":"\([a-f0-9-]*\)".*/\1/p') # 3) 轮询状态(使用 sim 时,状态从 queued -> running -> completed 大约需要 ~5-10s) curl -s localhost:8080/tasks/$TID # 4) 完成后获取报告 curl -s localhost:8080/tasks/$TID/report | python3 -m json.tool ``` ### 报告结构 ``` { "task_id": "fcfc913e-a353-478c-aa7f-15d30b1ffa2d", "verdict": "malicious", "score": 100, "processes": [ { "pid": 1024, "ppid": 512, "name": "sample", "command_line": "/tmp/sample" }, { "pid": 1101, "ppid": 1024, "name": "powershell", "command_line": "powershell -nop -w hidden -enc SQBFAFgA" } ], "network": [ { "protocol": "UDP", "dest_ip": "8.8.8.8", "dest_port": 53, "domain": "malware-c2.example", "bytes": 88 }, { "protocol": "TCP", "dest_ip": "45.137.21.9", "dest_port": 4444, "bytes": 920 } ], "files": [ { "operation": "create", "path": "/etc/cron.d/persist" }, { "operation": "encrypt", "path": "/home/victim/photos/*.jpg -> *.locked" } ] } ``` 当 `score >= 50` 时 `verdict` 为 `malicious`,否则为 `safe`。分数是 **根据行为得出的** —— 混淆的 PowerShell、C2 端口 (4444/1337/…)、 持久化位置 (cron、启动项、Run 键)、勒索软件式的文件操作等。 使用 `sim` 时,观测数据是合成的;使用 `libvirt` 时,它们来自 真实的 VM。无论哪种方式,评分标准都是一样的。 ## 任务队列 (SKIP LOCKED) worker 使用一条原子语句领取任务,因此多个 worker 可以共享一个队列, 而绝不会争抢同一行记录: ``` UPDATE tasks SET status='running', worker_id=$1, attempts=attempts+1, lease_expires_at = now() + make_interval(secs => $2) WHERE id = ( SELECT id FROM tasks WHERE status='queued' ORDER BY created_at FOR UPDATE SKIP LOCKED LIMIT 1 ) RETURNING id, sample_sha256; ``` 每次领取都会设置一个**租约**。如果 worker 在任务执行期间宕机,任何 worker 的清理线程 都会在租约到期后将任务重新入队(达到 `max_attempts` 后,则标记为 `failed`)。 ## 真实 VM 执行 (libvirt) `libvirt` 驱动会在真实的 KVM/QEMU 客户机中引爆样本,并通过 **QEMU guest agent** —— 一个样本网络无法察觉的带外 virtio 通道 —— 收集行为数据。针对每个任务,它会: 1. 将 domain 恢复到干净的**快照**并确保其正在运行; 2. 等待 `qemu-guest-agent` 响应; 3. 对受监控的目录进行快照(mtime/size)— *执行前*; 4. 将样本复制到客户机中(`guest-file-*`)并执行 `chmod +x`; 5. 启动它(`guest-exec`),在其运行期间轮询 `ss` 以获取出站连接; 6. 捕获 `ps` 并再次对受监控的目录进行快照 — *执行后*; 7. 返回进程 + 网络 + 文件差异 → 评分并生成同样的报告。 以下设置适用于 **Ubuntu 宿主机**。名称与驱动预期的 值(`puppetlab-guest`、快照 `clean`、`qemu:///system`)相匹配。 ### 宿主机设置 (KVM/libvirt) ``` sudo apt update sudo apt install -y qemu-kvm libvirt-daemon-system libvirt-clients \ virtinst libvirt-dev pkg-config cloud-image-utils genisoimage osinfo-db-tools sudo systemctl enable --now libvirtd sudo usermod -aG libvirt,kvm "$USER" # then LOG OUT and back in export LIBVIRT_DEFAULT_URI=qemu:///system # so virsh targets system libvirt # verify kvm-ok # "KVM acceleration can be used" virsh version virsh net-list --all # 'default' present; if inactive: virsh net-start default && virsh net-autostart default ``` ### 创建客户机 VM ``` cd /tmp wget -O noble.img https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img sudo cp noble.img /var/lib/libvirt/images/puppetlab-guest.qcow2 # use the default pool (avoids $HOME perms) sudo qemu-img resize /var/lib/libvirt/images/puppetlab-guest.qcow2 20G # cloud-init:设置密码,安装并启用 guest agent cat > /tmp/user-data <<'EOF' #cloud-config password: puppet chpasswd: { expire: false } ssh_pwauth: true packages: [qemu-guest-agent] runcmd: [[systemctl, enable, --now, qemu-guest-agent]] EOF cat > /tmp/meta-data <<'EOF' instance-id: puppetlab-guest local-hostname: puppetlab-guest EOF cloud-localds /tmp/seed.iso /tmp/user-data /tmp/meta-data sudo mv /tmp/seed.iso /var/lib/libvirt/images/seed.iso # 创建并启动 VM(首次启动需在 NAT 上进行,以便 agent 能够安装) virt-install --connect qemu:///system --name puppetlab-guest \ --memory 2048 --vcpus 2 \ --disk path=/var/lib/libvirt/images/puppetlab-guest.qcow2,format=qcow2,bus=virtio \ --disk path=/var/lib/libvirt/images/seed.iso,device=cdrom \ --os-variant ubuntu24.04 --network network=default,model=virtio \ --channel unix,target_type=virtio,name=org.qemu.guest_agent.0 \ --graphics none --noautoconsole --import # 等待直到 agent 响应(首次启动时 cloud-init 大约需要 ~1-3 分钟) virsh qemu-agent-command puppetlab-guest '{"execute":"guest-ping"}' # -> {"return":{}} ``` ### 为干净的初始状态创建快照 首先分离种子 CD-ROM(内存快照无法包含 cdrom),然后 对**运行中**的状态进行快照,以便恢复时是热状态(agent 已启动): ``` virsh domblklist puppetlab-guest # note the cdrom target (e.g. sda) virsh shutdown puppetlab-guest # wait for 'shut off' (virsh domstate ...) virsh detach-disk puppetlab-guest sda --config virsh start puppetlab-guest until virsh qemu-agent-command puppetlab-guest '{"execute":"guest-ping"}' 2>/dev/null; do sleep 2; done virsh snapshot-create-as puppetlab-guest --name clean --description "clean baseline" --atomic # verify virsh snapshot-list puppetlab-guest # 'clean' listed, state 'running' virsh snapshot-revert puppetlab-guest --snapshot clean ``` ### 构建并运行 libvirt worker ``` go build -tags libvirt -o bin/worker ./cmd/worker # needs libvirt-dev + cgo export PUPPETLAB_EXECUTOR=libvirt LIBVIRT_URI=qemu:///system export VM_DOMAIN=puppetlab-guest VM_SNAPSHOT=clean export GUEST_SAMPLE_PATH=/tmp/sample WATCH_DIRS=/tmp,/root,/home RUN_TIMEOUT_SECONDS=60 export DATABASE_URL="postgres://puppet:puppet@localhost:5432/puppetlab?sslmode=disable" ./bin/worker # log: worker: using "libvirt" executor ``` 或者作为容器运行(挂载宿主机的 libvirt socket): ``` docker build -f Dockerfile.libvirt -t puppetlab-worker-libvirt . docker run --rm \ -e DATABASE_URL=... -e VM_DOMAIN=puppetlab-guest -e VM_SNAPSHOT=clean \ -v /var/run/libvirt:/var/run/libvirt \ puppetlab-worker-libvirt ``` `sim` 和 `libvirt` worker 可以互换,并且可以针对 同一个队列并行运行。通过相同的 [API](#api) 提交样本 —— 它们现在将在 真实的客户机中引爆。 ### 首次引爆测试(无害 — 非恶意软件) 这个“样本”是一个良性脚本(驱动程序会对它执行 `chmod +x` 并运行 `/bin/sh -c '/tmp/sample'`): ``` cat > /tmp/harmless.sh <<'EOF' #!/bin/sh echo "puppetlab harmless test" touch /tmp/puppetlab_was_here # -> file "create" id > /tmp/puppetlab_id.txt # -> file "create" sh -c 'sleep 3' & # -> child process (exec 3<>/dev/tcp/192.168.122.1/53; sleep 4) 2>/dev/null || true # -> network entry EOF SHA=$(curl -s -X POST "localhost:8080/samples?filename=harmless.sh" --data-binary @/tmp/harmless.sh | sed -n 's/.*"sha256":"\([a-f0-9]*\)".*/\1/p') TID=$(curl -s -X POST localhost:8080/tasks -H 'Content-Type: application/json' -d "{\"sample_sha256\":\"$SHA\"}" | sed -n 's/.*"task_id":"\([a-f0-9-]*\)".*/\1/p') curl -s localhost:8080/tasks/$TID/report | python3 -m json.tool ``` **预期结果:** 状态为 `queued → running → completed`;判定结果 `safe`(低分); `files` 包含了那两个 `/tmp/puppetlab_*` 的创建记录;`processes` 显示 `sample`/`sh`。**运行两次** —— 第二次运行必定不会保留 `/tmp/puppetlab_was_here`,从而证明在两次引爆之间执行了快照恢复。 ### 隔离网络(在测试真实恶意软件之前) ``` cat > /tmp/isolated.xml <<'EOF' puppetlab-isolated EOF virsh net-define /tmp/isolated.xml virsh net-start puppetlab-isolated && virsh net-autostart puppetlab-isolated virsh shutdown puppetlab-guest virsh edit puppetlab-guest # change -> 'puppetlab-isolated' virsh start puppetlab-guest until virsh qemu-agent-command puppetlab-guest '{"execute":"guest-ping"}' 2>/dev/null; do sleep 2; done virsh snapshot-delete puppetlab-guest clean virsh snapshot-create-as puppetlab-guest --name clean --atomic # re-baseline on the isolated net ``` 无论网络如何,guest agent 都通过 virtio-serial 工作,因此即使在 完全没有互联网的情况下,引爆依然可以进行。 ### 调试检查清单 | 症状 | 解决方案 | |---------|-----| | `Could not access KVM kernel module: Permission denied` | 将用户添加到 `kvm` 组,重新登录(当前 shell 使用 `newgrp kvm`)。检查 `ls -l /dev/kvm`。 | | `failed to connect socket to '/var/run/libvirt/libvirt-sock'` | `systemctl status libvirtd`;使用 `qemu:///system`;确保在 `libvirt` 组中(重新登录)。 | | `Guest agent is not responding` | 通道是否存在?`virsh dumpxml puppetlab-guest \| grep guest_agent`。仍在启动中 → 等待。客户机中是否安装/运行了 agent?通过 `virsh console` 执行 `systemctl status qemu-guest-agent`。 | | 快照创建失败 (cdrom) | 首先分离种子 cdrom:`virsh detach-disk ... --config`。 | | `internal snapshots are not supported` | 磁盘必须是 **qcow2**(`qemu-img info `),而不是 raw。 | | 恢复缓慢 / 恢复后 agent 死亡 | 快照是在关机状态下拍摄的 —— 请在 **运行中** 且 agent 已启动的状态下重新拍摄。 | | `Could not open '...qcow2': Permission denied` | 将磁盘放在 `/var/lib/libvirt/images` 中;检查 `journalctl -k \| grep -i apparmor`。 | | `go build -tags libvirt` 失败 (`pkg-config: libvirt not found`) | `sudo apt install -y libvirt-dev pkg-config`。 | | `Unknown OS name 'ubuntu24.04'` | `osinfo-query os \| grep -i ubuntu`,使用列出的 id 或 `--os-variant generic`。 | | Worker:`lookup domain` / `lookup snapshot` 错误 | 名称不匹配 —— 检查 `virsh list --all` / `virsh snapshot-list puppetlab-guest`。 | ## 测试 ``` go build ./... # default (sim) build go vet ./... go test ./... # parser, scoring, and full detonation-flow tests gofmt -l . # empty output = formatted ``` `internal/sandbox/detonate_test.go` 针对模拟的 guest agent 演练了整个引爆 编排过程(文件复制 → 执行 → `ss`/`ps`/`find` 解析 → 文件差异对比 → 判定结果),因此无需 Hypervisor 即可覆盖真实 VM 的逻辑。cgo `libvirt.go` 本身只能在安装了 `libvirt-dev` 的宿主机上编译。 ## 命令参考 ``` # --- 构建 / 测试 --- go mod download go build ./... # sim build go build -tags libvirt -o bin/worker ./cmd/worker # real-VM worker (needs libvirt-dev) go test ./... go vet ./... ; gofmt -l . # --- 运行 (Docker) --- docker compose up --build # postgres + api + worker docker compose up --build --scale worker=3 # more workers docker compose logs -f worker docker compose down [-v] # stop [+ wipe db] # --- 运行 (本地) --- go run ./cmd/apiserver go run ./cmd/worker # --- API --- curl -s localhost:8080/healthz curl -s -X POST "localhost:8080/samples?filename=f" --data-binary "echo hi" curl -s -X POST localhost:8080/samples -F "file=@/path/to/file" curl -s -X POST localhost:8080/tasks -H 'Content-Type: application/json' -d '{"sample_sha256":""}' curl -s localhost:8080/tasks/ curl -s localhost:8080/tasks//report | python3 -m json.tool # --- database --- psql "$DATABASE_URL" -f migrations/001_init.sql psql "$DATABASE_URL" -c "SELECT id,status,verdict,score FROM tasks ORDER BY created_at DESC LIMIT 10;" docker compose exec postgres psql -U puppet -d puppetlab # --- VM (libvirt) --- virsh list --all virsh start|shutdown|destroy puppetlab-guest virsh console puppetlab-guest # Ctrl+] to exit virsh snapshot-list puppetlab-guest virsh snapshot-revert puppetlab-guest --snapshot clean virsh qemu-agent-command puppetlab-guest '{"execute":"guest-ping"}' ```
标签:DAST, EVTX分析, Go, IP 地址批量处理, KVM/QEMU, PostgreSQL, Ruby工具, URL发现, 安全检测, 恶意软件分析, 日志审计, 沙箱, 测试用例, 网络安全审计, 虚拟化, 请求拦截