ARTICLE DETAIL

资讯详情

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

打造终端虚拟宠物TamaFi:Go+SQLite实现安全研究工作流可视化

打造终端虚拟宠物TamaFi:Go+SQLite实现安全研究工作流可视化 1. 为什么“黑客”需要一只虚拟宠物1.1 安全研究者的工作场景比你想的更孤独我平时有一半工作时间泡在终端里跑 CTF 的题目、翻日志、写自动化脚本、盯着某个漏洞利用链调来调去。外人觉得这工作很酷实际上绝大多数时候是枯燥的一个人对着黑底白字的终端一坐就是四五个小时。更折磨人的是安全研究这种任务几乎没有“进度条”你做了很多努力但成果可能是一天下来什么都没有或者只在某个不起眼的 commit 里留下一行注释。这种环境下人很容易疲劳、烦躁甚至自我怀疑。我试过用 TODO 清单、番茄钟来管理自己但效果都不持久。清单只是冷冰冰的文本番茄钟也只是倒计时它们提供不了情绪价值也不会在你连续工作五小时后给你一点正向反馈。这时候我脑子里冒出一个想法能不能把我那台终端变成一个“活的东西”让命令行里有一个看得见、摸得着的伙伴天天看着我干活喂它吃“任务积分”看它进化。于是就有了 TamaFi。1.2 TamaFi 的定位不是玩具是终端里的“进度条陪伴者”TamaFi 这个名字前半部分致敬经典的电子宠物“拓麻歌子Tamagotchi”后半部分的 Fi 代表“Fitness/Finance/Find”之类的含义随意解读主要是想强调它是一套有养成、有成长、有反馈的系统。它是一个跑在终端里的虚拟宠物你用真实的工作行为去喂养它它用状态变化、进化、收集品来反馈你的付出。举个例子你完成一个 CTF 靶场的挑战TamaFi 会给你加任务积分宠物会从蛋里孵出来慢慢从幼体变成成体你今天只是接了十个紧急工单完全没有研究时间宠物就会因为“饥饿”而状态下降。这种设计的本质是把抽象、长期、不确定的安全学习目标变成一个看得见、可交互、有情绪的对象。有人会问这不就是养电子鸡吗对表面上是。但关键差别在于传统电子鸡要求你频繁点按操作而 TamaFi 的设计原则是“不断开当前工作流”。它不要求你停下手中的事情去喂食而是自动感应你的终端行为再折算成养分和成长值。它是工作流的副产品而不是额外负担。这一点决定了它能不能被真正长期用下来。2. 整体设计思路与架构拆解2.1 核心需求一个不打断工作的陪伴型终端工具动手写代码之前我先给自己定了五条设计原则后续所有功能都是围绕这五条来的低侵入不改变你现有的命令习惯不强制你打开额外面板。所有交互发生在终端本身最多在右下角有一行状态提示。离线优先虚拟宠物是本地的数据必须存在本地。我的终端里有很多敏感操作决不能让行为数据默认上传到某台云端服务器。可扩展积分规则、任务类型、进化路径都要能通过配置文件调整。安全研究每个人方向不同有人天天搞 Web有人搞二进制有人搞取证规则必须能定制。省资源终端里已有的东西够多了TamaFi 的内存占用必须控制在可忽略的范围。我给自己定的目标是常驻进程不超过 30 MB。隐私透明它“看”命令但只记录必要信息默认不存储命令输出。命令名可以做脱敏和哈希处理敏感命令直接跳过。这个后面细讲。基于这五条我决定不做 Electron 套壳应用也不做成 SaaS 网站。最合适的形态是一个轻量级守护进程加上终端 Hook再配一个可选的本地 Web 面板用来查看宠物状态。你看它的时候终端就是它的家你不看它它安静地在后台记录和计算。2.2 技术选型为什么用 Go SQLite WebSocket技术栈方面我做了个对比最终选择了 Go 来做主进程。语言/方案优势劣势我的结论Python生态丰富写脚本快能快速验证想法打包和跨平台分发麻烦常驻内存偏高适合原型验证不适合最终常驻Go交叉编译容易单文件分发内存占用极低并发模型简单生态相对少一些UI 库不如 Web 丰富最终选择Rust性能极高内存控制最精细学习曲线陡开发速度慢技术可行但个人效率太低Node.js终端生态很活跃WebSocket 天然友好内存占用一般分发时要带运行时备选方案最后我用了 Go 作为守护进程SQLite 作为本地存储WebSocket 用来向本地 Web 面板推送状态。这里解释一下为什么一定要 SQLite而不是直接写 JSON 文件JSON 文件在并发写入时容易损坏而且“事件流水”这类数据以后要按日期、任务类型做聚合查询用 JSON 文件做聚合需要全量读取效率很差。SQLite 是单文件数据库免安装、免配置一个文件拿走就能备份完美符合“离线优先”的要求。WebSocket 用在哪里它不用于跨网络通信而是让本机的浏览器面板可以实时看到宠物的一举一动。比如你在终端里完成了一条任务命令宠物立刻吃了一口“经验因子”网页端的宠物画面同步动起来。这种实时反馈很影响体验如果用轮询接口一秒一次也能凑合但体感明显差一个档次。2.3 三大核心子系统状态机、积分引擎、命令钩子TamaFi 拆开来看是三个子系统状态机负责宠物的“生命表现”。我参照经典电子宠物设计了一个有限状态机状态包括蛋、幼体、成体、完全体、休眠。每个状态由饥饿、心情、能量三个维度驱动。如果长时间不干活宠物会从正常变成饥饿再变成休眠一旦你重新投入工作并积累积分它会逐步恢复。积分引擎负责把“行为”变成“成长”。安全研究里不同的行为价值不同完成一个 HTB 靶场、完成一个 TryHackMe 房间、跑完一次信息收集、写了一个自动化脚本这些都应该有不同的积分权重。积分引擎需要支持权重配置、时间衰减、连击加成。比如连续三天都有产出成长速度增加 20%断了一天后连击重置。这套机制能有效鼓励持续性投入。命令钩子负责感知用户行为。它通过在 Shell 的提示符钩子里拦截用户执行过的命令把命令名去敏之后传给守护进程守护进程再判断这条命令属于哪种“活动类型”折算成积分。这里有个容易踩坑的地方命令钩子不能拦截输出内容否则会把敏感日志、密钥、Token 全部录进去。所以我只在钩子里读取fc -ln -1之类的历史命令记录并且过滤掉包含关键字的命令。三个子系统互相配合构成了 TamaFi 的核心循环你执行命令 - 命令钩子感知 - 积分引擎折算 - 状态机更新 - 宠物表现变化 - 你获得反馈 - 你继续执行命令。3. 实操过程与核心环节实现3.1 初始化项目与数据模型设计先说明一下我是在 macOS 和 Linux 上做的开发测试Windows 环境下用 WSL 也能跑。项目的初始化很简单mkdir tamafi cd tamafi go mod init github.com/yourname/tamafi数据模型是这件事的地基必须先想清楚。我建了三张核心表pets、task_events、inventory。CREATE TABLE IF NOT EXISTS pets ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, state TEXT NOT NULL DEFAULT egg, hunger INTEGER NOT NULL DEFAULT 50, mood INTEGER NOT NULL DEFAULT 50, energy INTEGER NOT NULL DEFAULT 50, growth REAL NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS task_events ( id INTEGER PRIMARY KEY, task_type TEXT NOT NULL, command_hash TEXT NOT NULL, weight REAL NOT NULL DEFAULT 1.0, streak INTEGER NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS inventory ( id INTEGER PRIMARY KEY, item_name TEXT NOT NULL, acquired_at DATETIME DEFAULT CURRENT_TIMESTAMP );pets表里的hunger、mood、energy我用了 0-100 的整数区间初始全部是 50。为什么不设成 100因为如果一开始全满你很难看到“因工作而下降、因休息而恢复”的动态过程反馈会失真。growth字段是浮点累计值用来判断宠物是否进入下一阶段。task_events记录每一次被认定的“有效工作事件”。注意我存的是command_hash而不是命令原文目的很简单我不想在数据库里留下完整的敏感命令历史。命令原文只是用来推导任务类型和权重推导完之后只保留哈希值。对你自己的环境你可以选择保存完整命令但从隐私优先的角度我推荐哈希。inventory是宠物身上的“装备”和“徽章”。比如连续七天完成任务解锁一个“熬夜冠军”徽章完成一百次信息收集类命令解锁一个“侦察兵”挂件。这些物品有收集价值也给了安全研究者多一个坚持的理由。3.2 宠物状态机与成长算法实现状态机的工作逻辑其实不复杂但实现的时候容易变成一团乱麻原因在于“状态”和“维度”之间不是简单的一一对应。我最终采用事件驱动的方式实现type Pet struct { ID int32 Name string State string Hunger int Mood int Energy int Growth float64 } func (p *Pet) ApplyEvent(ev Event) { switch ev.Type { case task_completed: w : ev.Weight // 基础成长 p.Growth w // 饥饿度下降做任务消耗精力 p.Hunger clamp(p.Hunger - int(3*w), 0, 100) // 心情上升完成任务本身会带来满足感 p.Mood clamp(p.Mood int(2*w), 0, 100) // 能量下降干活久了自然会累 p.Energy clamp(p.Energy - int(1*w), 0, 100) case rest: p.Energy clamp(p.Energy30, 0, 100) p.Mood clamp(p.Mood10, 0, 100) p.Hunger clamp(p.Hunger5, 0, 100) } p.MaybeTransition() }这里clamp是简单的边界限制函数避免数值越界。MaybeTransition根据Growth的值判断状态迁移func (p *Pet) MaybeTransition() { switch { case p.State egg p.Growth 10: p.State baby case p.State baby p.Growth 50: p.State teen case p.State teen p.Growth 200: p.State adult case p.Hunger 100 || p.Energy 0: p.State sleep default: if p.State sleep p.Hunger 60 p.Energy 20 { p.State baby } } }这个算法的关键点是成长阈值必须考虑到“时间衰减”否则你一个月突击猛干宠物进化到完全体然后你半年不碰它它依然长那样这不符合电子宠物的逻辑。我加了一个简单的衰减器每天零点定时任务如果task_events里当天没有记录Growth会缓慢下降。数值很温和一天只降 2%目的是让人长期保持手感而不是靠一次爆发永久毕业。积分权重这部分的默认配置我写在了一个 JSON 文件里你可以按自己的研究方向改{ task_weights: { ctf: 8.0, recon: 4.0, exploit: 10.0, scripting: 3.0, documentation: 2.0, routine: 1.0 }, streak_bonus: 0.2, decay_rate: 0.02, rest_min_interval: 30m }我的经验是权重不要搞得太复杂六个类型已经足够覆盖九成场景。权重差异决定了行为的“含金量”写一个完整漏洞利用脚本比单纯跑 nmap 扫描更难、更值钱所以exploit类型的权重设计为最高分。这种设计反过来会鼓励你去做更有挑战的事而不是沉迷于刷简单命令。3.3 终端命令钩子让虚拟宠物“看见”你的工作这是整个项目里最核心、也最容易翻车的部分。要让宠物感知你的行为我选择了 Shell 的预执行钩子机制。以 zsh 为例在.zshrc里加function tamafi_precmd() { cmd$(fc -ln -1 2/dev/null | sed s/^[[:space:]]*//) echo $cmd | tamafi-agent --check $cmd /dev/null 21 } precmd_functions(tamafi_precmd)fc -ln -1的作用是取出最后一条最近执行过的历史命令不显示命令编号也不读取命令输出。注意这个函数是在“命令执行完毕之后”触发的所以如果你中途按下 CtrlC它依然会记录这样其实是合理的你确实尝试了这条命令。Bash 用户则用PROMPT_COMMANDPROMPT_COMMANDcmd$(fc -ln -1 2/dev/null | sed s/^[[:space:]]*//); tamafi-agent --check $cmd /dev/null 21 tamafi-agent是一个轻量的 Go 程序它的核心逻辑是接收--check参数传入的命令字符串。先判断是否在“忽略名单”里。比如命令包含rm -rf、.env、password、token等敏感关键词直接跳过不做任何记录。再用关键词匹配判断任务类型。例如命令里包含nmap、masscan、subfinder判定为recon包含python3但带ctf、writeup、payload之类的判定为ctf包含htb、thm、tryhackme判定为ctf。把匹配结果写入task_events并调用状态机更新宠物。这里要解释一个设计细节为什么不让 Go agent 自己去终端里读取历史文件因为不同终端的 history 存储路径和格式差异太大直接读文件容易误读、串行化、还容易碰到锁冲突。用 Shell 自己的历史机制把命令“喂”给 agent是最省事、跨终端兼容性最好的方案。命令钩子还要注意“误触发”问题。我最初把curl直接归类为routine结果发现几乎任何工具都会调用 curl导致积分通胀严重宠物成长快得离谱。后来我改成即使识别到curl也必须有明确关键词比如api/、webhook、request才计分。宁可漏掉不可滥计这是积分系统保持可信度的关键。3.4 Web 面板与实时状态展示命令行里看宠物状态是基本功但我还是额外做了一个本地 Web 面板。原因很简单终端里只能看到一行hunger50 energy60之类的文本没有“养宠物”的实感。Web 面板可以放一张 SVG 宠物图加上动画和背景变化反馈情绪更丰富。Go 标准库自带net/http做一个小服务不需要额外引入框架http.HandleFunc(/api/status, func(w http.ResponseWriter, r *http.Request) { p : loadPet() json.NewEncoder(w).Encode(p) }) http.HandleFunc(/ws, func(w http.ResponseWriter, r *http.Request) { conn, _ : upgrader.Upgrade(w, r, nil) for { pet : loadPet() conn.WriteJSON(pet) time.Sleep(2 * time.Second) } })这个/ws接口是给网页端用的每两秒推一次数据浏览器端实时更新。两秒的间隔是一个折中一秒太频繁会让 CPU 白白做无用功五秒太慢宠物吃东西的反应看起来会卡顿。另外我把这个服务的监听地址默认绑定在127.0.0.1:7654只允许本机访问因为状态数据里可能包含任务习惯不能暴露到局域网。最后是通知系统。我加了一个可选的桌面通知当宠物进化时用notify-send推送一条“TamaFi 进化啦”的消息。这个功能在深度投入干活的时候特别有用相当于一个低干扰的里程碑提醒。它不会弹窗打扰你的思路只在你完成某个阶段性目标时才出现。4. 常见问题与排查技巧实录4.1 命令钩子失灵的三种典型场景我在给朋友试用 TamaFi 的过程中遇到最多的问题就是“为什么我敲了命令宠物完全没反应”。第一种典型场景是别名冲突。如果你给nmap设了一个别名比如alias nmapsudo nmap那么在 zsh 的precmd钩子里fc -ln -1拿到的可能是sudo nmap -sV ...而不是nmap -sV ...这时候如果关键词规则里只写了nmap实际上也能匹配到因为子串匹配依然存在。真正的问题是某些别名把命令改得面目全非比如你设了alias httpx~/tools/httpx -silent然后平时敲的是httpx可钩子里拿到的是完整的~/tools/httpx -silent这时候匹配逻辑必须支持路径后缀识别。我的解决方式是先做 basename 提取再输入匹配器。第二种典型场景是子 Shell 与 tmux 会话。我在 tmux 里开了多个窗格时每个小程序都是独立 shell只有当前聚焦的那个窗格会触发自己的precmd。你在另一个窗格里跑长任务主窗格的宠物感知不到。这不是 bug而是 shell 机制本来的行为。如果你希望所有窗格都被记录可以考虑在 tmux 的 hook 里统一调用 agent但这样复杂度会成倍上升。我的建议是不要强求用它来感知“主要的干活窗格”就够了。第三种典型场景是环境变量没有继承。有些用户通过 launchd 或 systemd 把tamafi-agent跑成了后台服务但 Shell 钩子调用tamafi-agent时PATH 环境变量里找不到它的路径。解决方案是配置绝对路径不要偷懒写tamafi-agent。如果程序路径是/usr/local/bin/tamafi-agent那就直接写全路径别依赖 PATH。4.2 宠物数值乱跳与成长失速的处理第一次完整跑通时我遇到一个让人哭笑不得的问题宠物成长快得像开了挂两天就从蛋变成完全体。排查过程对我很有启发。问题出在两个地方。第一是积分事件重复记录。我的precmd钩子本来应该在每次执行命令后触发一次但 zsh 的precmd_functions在某些主题框架里会被执行两次导致同一条命令被 agent 记录了两遍。我加了一个“两秒内相同命令哈希去重”的缓存机制才解决。第二是时间衰减功能实现错了。我当时是用每个事件的时间戳去判断“是否是新的一天”但如果有批量脚本在半夜回放历史命令就会触发大量连续事件导致衰减逻辑来不及生效。我后来改成只看当天零点以后的task_events数量超过 30 条的部分不再计分。这样做还有一个额外好处防止拿历史记录“刷”宠物。数值乱跳还有一个常见原因是时钟回拨。比如你手动改了系统时间或者有 NTP 校正状态机里的趋势计算会用负的增量导致宠物数值突然变化。对于这种问题稳健的做法是存储上次更新的 Unix 时间戳并且强制要求增量大于等于 0。如果发现时间倒退就忽略本轮更新。4.3 隐私顾虑与资源占用的避坑心得这是个重点。很多安全研究者对“命令被记录”有着天然的戒心这完全可以理解。我的做法是默认只记录命令哈希。在task_events表里不保留明文命令只保留 SHA-256 前 16 位。查询时只能看到“某种任务类型”看不到具体命令。忽略名单优先匹配。只要命令里出现预设的高危关键词直接丢弃不落库不参与评分。我把token、secret、shadow、.ssh都放进了默认忽略名单。彻底关闭模式。如果你在做高度敏感的工作可以临时运行tamafi-agent --pause之后所有钩子检查直接返回不做任何记录。资源占用方面Go 编译的单二进制常驻内存稳定在 18-25 MBSQLite 数据库文件在连续使用一个月后大概是 1 MB 量级。这个水平在我的容忍范围内。唯一要注意的是避免在钩子里启动重量级调用我试过在precmd里直接调用 agent 每次启动要花 50ms会明显拖慢终端响应后来改成常驻服务模式agent 变成 unix socket 客户端启动速度降到 1ms 以下。4.4 从“养电子鸡”到“养成工作流”的进阶玩法最后分享几个我用下来觉得最有价值的扩展方向。一是自定义任务权重。默认配置对 Web 安全研究者比较友好但如果你是做二进制漏洞的方向可以把scripting的权重调高把ctf的分类拆成pwn和web两个子类。这样宠物成长的方向就和你的实际技能树一致游戏化反馈才真正有意义。二是加入专注模式。我给 TamaFi 加了一个“研究模式”启动一个 25 分钟计时器期间只记录探索类命令不响应任何外部通知。计时结束后如果这段时间里确实有有效事件宠物额外获得一颗“专注宝石”宝石可以用来兑换宠物外观。这种设计让“充分专注”变成一种可积累的资产。三是团队排行榜。如果你和朋友组队参加 CTF可以让 TamaFi 导出一个匿名的积分摘要 JSON团队成员之间互相看成长曲线。不过要注意这个功能默认关闭因为随便开放数据接口会破坏隐私承诺。我只在小组内部用而且只会展示任务类型分布不展示任何命令细节。写在最后我用 TamaFi 三个月后的真实感受说实话最初做这个小东西的时候我只是觉得“给终端加个电子宠物很有趣”完全没有预料到它真的能改变我的工作习惯。过去我经常干到一半就觉得烦躁忍不住切去看新闻、刷帖子一刷就是半小时。有了 TamaFi 之后我看到宠物因为长时间断档而掉状态会有一种说不出的愧疚感。这种愧疚感不是来自什么严厉的监督者而是来自一个陪伴你努力的“小东西”。它让我更愿意把中断时间缩短也更愿意在完成一件枯燥任务后才去休息。我个人觉得这类工具最大的价值不在于“督促你工作”而在于“让你的付出变得可见”。安全研究是一个结果高度不确定的领域你很难每天都说清楚自己学到了什么。但有只宠物会在你身边把你投入的时间一点一点变成成长值变成新的形态变成一件件小徽章。它不能替你解决难题却能在你埋头折腾一整天后用一次安静的进化告诉你今天没有白过。
返回列表