“SILENTTRINITY”这个词我在好几年前第一次看到时第一反应是“又一个 C2 框架”。真正上手用了一个星期后我改变了对它的看法——它最大的亮点不是通信方式多花哨而是模块扩展机制用 Boo 语言写后渗透模块。你不需要重新编译整个框架不用去啃 C# 底层只要放一个.boo文件进目录几秒钟后这个新功能就能在当前任务会话里被调用。这篇文章就是我沉淀了这段时间的经验之后整理出来的从环境搭建、Boo 语法、模块结构到一个完整的信息侦察模块实现全部走一遍。适合正在做红队工具建设、渗透测试流程自动化或者刚接触 SILENTTRINITY 但不知道模块怎么写的同学。1. 整体设计与开发思路1.1 SILENTTRINITY 的模块化设计先看官方源码结构你会发现模块就是服务端和客户端之间的“翻译层”。框架本身是一个标准的 C2 架构服务端负责控制、生成 payload、维护会话客户端植入目标机器的那部分负责执行任务并回传结果。传统的 C2 框架里如果你想加一个新功能往往要改服务端逻辑、改客户端逻辑、重新编译一个完全不同的 agent维护成本非常重。SILENTTRINITY 的做法是把“任务逻辑”单独抽象成一个模块。任务从服务端下发时实际上发给客户端的是一段经过编译的 Boo 脚本引用信息客户端会匹配到对应的模块类实例化然后调用它的Execute方法。你写的模块只需要关心“怎么执行、返回什么结果”至于通信协议、加解密、载荷交换这些全都有框架处理。这样的好处就是你写的新模块能立刻被所有在线客户端使用。不需要为不同架构的 payload 各维护一份模块代码因为模块是在服务端这边定义、编译、注入到客户端的。这个设计思路对做红队自动化特别友好因为我可以在一个项目里积累大量模块按需启用而不是维护一整套重复的工具链。1.2 模块类型与应用场景从功能性质上来分后渗透模块大致可以分成这几类信息侦察、文件操作、进程管理、权限操作、横向移动辅助、持久化。一般来说凡是“单次操作、返回结果”的功能都适合做成 SILENTTRINITY 模块。比如你想获取目标系统信息那就是一个典型的侦察模块调用 .NET 的System.Environment拿版本、拿用户、拿机器名然后拼一个字符串返回。复杂一点的像横向移动会涉及很多前置条件和参数处理但只要拆解清楚同样可以做进模块里。我在实际做模块规划时有一个经验先写最简单的一类也就是“无参数或只有一个参数”的侦察模块。这样能快速验证框架的加载流程、数据回传流程和理解上下文对象的工作方式。等你有了感觉再去做带多个参数、带交互过程的复杂模块难度就不是线性上升而是模块化思考带来的友好扩展了。1.3 模块开发的完整流程一个模块从想法到落地遵循下面的闭环确认接口阅读你当前版本的 SILENTTRINITY 源码找到模块基类、命名空间和方法签名。搭建 Boo 环境不用真的单独装一个 Boo 解释器但准备一个简单的 Boo 脚本调试工具能让语法检查更快。编写模块代码把功能拆成几个 .NET API 调用处理好参数和异常。放置文件并加载把.boo文件放到模块目录框架会自动发现它。执行测试通过控制台或监听器调用该模块观察回显和日志。迭代根据日志修正问题优化输出格式和异常处理。很多新手会跳过第一步上来就套用网上旧的示例结果在新版本框架上加载报错然后一头雾水。接口版本对不上的问题可能是你遇到的第一个大坑所以后面我会专门留一节讲模块加载时的排查。2. 环境准备与 Boo 语言快速入门2.1 搭建开发环境SILENTTRINITY 是基于 .NET 环境构建的所以你的开发机器需要对应版本的 .NET SDK。我建议从项目主页获取最新 release 或直接 clone 源码按照 README 的说明编译服务端和客户端组件。这个过程里你可能需要装一些 NuGet 依赖如果网络状态不好可能会卡在下载这一步解决办法是配置镜像源或者提前把依赖缓存好。编译好之后重点看源码目录下的 Modules 文件夹。你新增的每个.boo文件都应该放在这个目录里框架启动时会扫描这个目录把模块注册到列表。你要留意文件名不能有中文和特殊符号尽量用大小写敏感的英文命名因为 Boo 编译时对文件名和类名的对应关系比较敏感。另外我建议准备一个单独的文本编辑器加 Boo 语法高亮插件或者直接用支持 Boo 语言扩展的编辑器。虽然 Boo 语法比较轻量但好的高亮能帮你提前发现缩进和括号问题。这个语言对缩进有要求虽然不是像 Python 那样完全以缩进界定代码块但整体风格非常接近 Python 和 C# 的混合体。2.2 Boo 语法速览Boo 是一个运行在 .NET 平台上的动态语言语法流畅对 C# 开发者很友好对 Python 开发者也不会陌生。下面几个语法点是你写 SILENTTRINITY 模块时最常用的。变量定义# 显式类型 name as string zhangsan count as int 10 # 使用 def 定义变量 def value 3.14方法定义def Add(a as int, b as int) as int: return a b类和继承class MyModule(AbstractModule): Name as string: get: return my_module调用 .NET APIimport System.Diagnostics process Process.Start(cmd.exe, /c whoami) output process.StandardOutput.ReadToEnd() return output列表和字典items [] items.Add(a) users {admin: 1, guest: 0} for key in users: print($key ${users[key]})你可能会发现 Boo 的def关键字非常有用它既能定义变量也能定义方法。这个“鸭子类型”特性让你可以在不确定返回类型时直接使用def然后在运行时获得 NET 对象。有一个坑需要提前提醒Boo 对字符串插值用$而不是 C# 的插值写法并且字典的遍历顺序不像 Python 3.7 以后那样保持插入顺序所以如果你用字典收集数据再输出要注意顺序问题。2.3 编写第一个 Hello World 模块在你深刻理解所有框架 API 之前先把一个最小的模块跑通比什么都重要。下面这个模块只做一件事返回一段文本。namespace MyModules import System import SilentTrinity.Core class HelloWorld(AbstractModule): Name as string: get: return hello_world Description as string: get: return 返回一句问候语 Author as string: get: return author [Params] _message as string Hello from Boo! def Execute(self, context as AbstractContext) as string: return _message把这段代码保存为hello_world.boo放在 Modules 目录下。启动服务端后在控制台执行use hello_world然后运行你会看到返回的问候语。如果这里失败了排查方向就是文件是否放到了正确目录。类名和文件名是否匹配。import SilentTrinity.Core是不是当前源码命名空间的准确名称。基类名是否正确比如有的版本叫AbstractModule有的版本叫ModuleBase。这个试错过程非常培养直觉因为你会慢慢掌握“模块编译错误信息”的解读方式。3. 实战开发从零实现一个系统信息侦察模块3.1 模块需求分析Helloworld 太玩具了我们要来一个真正能用的系统信息侦察模块。这个模块的作用是执行之后帮我们快速拿到目标机器的操作系统版本、当前用户名、计算机名、环境变量列表、运行进程数量、网络连接数量等基础信息。我把模块设计成支持一个可选参数mode默认是全量输出。如果你只想看进程列表就可以在调用时传process只想看网络连接就传network。这样做的目的是让模块在常规信息收集时足够快不需要每次把所有数据都拖回来也能避免结果太长导致回传不稳定。输出格式我会选择易读的纯文本每行一个字段。因为后渗透场景里结果通常会被人工审阅纯文本比 JSON 更容易初筛。如果你自己有二次分析工具也可以把代码里的字符串拼接改成 JSON 序列化。3.2 核心代码实现下面是核心模块的完整示例请根据你当前源码实际接口微调namespace MyModules import System import System.Diagnostics import System.Net.NetworkInformation import SilentTrinity.Core class SysInfo(AbstractModule): Name as string: get: return sysinfo Description as string: get: return 获取系统基本信息、进程和网络连接概况 Author as string: get: return author [Params] _mode as string all def Execute(self, context as AbstractContext) as string: output try: if _mode all or _mode os: output self.GetOsInfo() if _mode all or _mode network: output self.GetNetworkInfo() if _mode all or _mode process: output self.GetProcessInfo() except ex as Exception: return ERROR: ${ex.ToString()} return output def GetOsInfo(self) as string: line OS: System.Environment.OSVersion.ToString() \n line MachineName: System.Environment.MachineName \n line UserName: System.Environment.UserName \n line SystemDirectory: System.Environment.SystemDirectory \n line Version: System.Environment.Version.ToString() \n return line def GetProcessInfo(self) as string: output [Processes]\n processes Process.GetProcesses() output Total: ${processes.Length}\n for p in processes: output ${p.Id}: ${p.ProcessName}\n return output def GetNetworkInfo(self) as string: output [Network]\n interfaces NetworkInterface.GetAllNetworkInterfaces() output Interfaces: ${interfaces.Length}\n for ni as NetworkInterface in interfaces: output ${ni.Name} (${ni.NetworkInterfaceType})\n return output这个模块有几点设计值得注意Params特性或者属性命名方式在不同版本里可能不一样你要去源码里确认。我习惯在类里定义一个带默认值的字段然后通过特性把它变成可传入参数。Execute里面我用了大量字符串拼接。在 Boo 里字符串插值非常方便但多层嵌套时要注意括号和引号。GetProcessInfo里直接遍历全部进程当目标机器进程数量非常多时输出会很长所以在实际部署时我会加一个数量上限比如只要前 50 个进程防止结果回传超时。NetworkInterface.GetAllNetworkInterfaces()在权限不足时可能抛出异常但因为我外面包了try...except异常会转成字符串回传不会导致客户端直接崩溃。3.3 与上下文对象的交互模块执行的时候context对象承载了非常重要的运行时信息。你需要搞清楚它提供哪些属性和方法比如获取当前会话标识、获取已传入的参数、引用当前目标机器的基本状态等。不同版本的 SILENTTRINITY 对上下文对象的名字和接口定义有差异但思路是一致的上下文是模块和框架之间的“会话管家”。模块自己不应该直接发起网络连接、直接传递控制指令这些都应该由框架完成。你只需要“本地执行、返回结果”。如果你在模块里想做一些更复杂的操作比如上传文件内容那也是把文件作为一个二进制数组返回由框架负责传输。回传结果有个小技巧如果你希望结果在服务端能被漂亮地打印出来记得在输出末尾保留一个换行符或者干脆只用\n而不是\r\n避免在某些终端环境里出现多余空行。这个问题我当时调试了很久才发现是换行符差异导致的显示错乱。3.4 模块测试与验证写完了代码把它命名为sysinfo.boo放进模块目录。重新启动服务端然后在控制台输入list modules如果列表中出现了sysinfo说明加载成功。接着选择一个处于活跃状态的会话或者监听器运行use sysinfo set mode all execute正常情况下你会看到返回的系统信息文本。如果出现编译错误控制台或日志文件会有异常信息最常见的错误包括命名空间找不到、方法签名不匹配、类型转换失败。我之前在测试时遇到过一个很隐蔽的问题Process.GetProcesses()返回的是进程对象数组但我在 Boo 里对数组调用Length属性某些版本会识别为.Count结果编译报错。解决办法就是统一使用集合的Count属性或者先复制到一个列表再统计。这个坑在文档里很难第一时间发现所以建议大家多写几行很小的测试代码逐步验证某个 API 是否可用。4. 常见问题与排查技巧4.1 模块加载失败的定位模块加载失败是新手最容易遇到的问题。常见原因有以下几种现象可能原因排查方向列表中看不到模块文件路径不对或扩展名错误确认文件是.boo且处于正确的 Modules 目录出现编译错误基类名或命名空间不对对比当前源码中的模块接口定义模块加载成功但执行报错方法签名冲突或参数特性错误检查Params特性、方法名、返回类型执行时没有任何输出异常被吞掉或结果为空字符串在Execute中增加try...except并返回具体的异常信息模块加载的日志通常在框架的日志模块里你可以通过控制台日志或文件日志看到 Boo 编译错误的具体行号。Boole 错误提示一般比较明确比如“unexpected token”说明有括号或缩进问题“method not found”说明类的接口定义不对。另外要注意的是模块文件不会热加载。我见过很多人在服务端运行状态下复制模块进去然后发现列表没更新以为路径错了其实只需要重启服务端或者让框架重新扫描模块目录。4.2 执行时的运行时异常运行时的异常五花八门但归结起来主要就是权限问题、参数问题和 API 可用性问题。权限问题最常见的是目标机器上当前进程权限不够。比如你想枚举某些高权限服务的可执行文件可能会遇到拒绝访问。这种情况下我通常会在代码里捕获异常然后返回“Permission denied”而不是让整个模块崩溃。这类错误信息在模块开发阶段是宝贝因为你能快速知道目标环境下什么能做什么不能做。参数问题则是因为用户在使用模块时传入了空值或类型不匹配的值。你可以在Execute开头就对参数做校验比如if _mode is null or _mode : return Usage: modeall|os|process|networkAPI 可用性问题主要集中在不同版本 .NET 里接口被移除或改名。这时最快的排查方法是在本地写一个小的 Boo 脚本直接调用这个 API看能不能正常运行。如果本地能运行那问题多半出在模块上下文或权限环境里。4.3 数据回传与传输稳定性当模块返回的数据量比较大的时候比如遍历了上千个进程SILENTTRINITY 的回传链路可能会遇到截断或超时。这类问题的表面现象是控制台只看到一部分结果或者模块执行看起来卡了很久。经验上你会从几个方向解决限制输出大小。模块内部做截断只返回最关键的 N 条数据。增加超时控制。在读取进程列表前设置一个合理的最大耗时避免模块执行时间过长导致告警或者会话掉线。结果分段返回。如果真的需要完整大数据可以拆成多次小批量比如每次返回 500 行客户端约定好分批上传。另外编码问题也经常出现。默认情况下目标机器的代码页和中国开发机的 UTF-8 可能不一致导致中文乱码。我通常会在模块里统一使用 ASCII 可打印字符或把输出转成 Base64 后再交给框架处理这样能最大程度避免编码带来的脏数据。4.4 安全使用与模块库维护写后渗透模块天然和攻击面相关所以我必须很明确地提一句所有操作只应该在你有权限授权的环境中进行。做红队演练、安全测试、培训验证一切都以合同授权和合规要求为前提。不要尝试把这个框架和模块用于任何未经授权的场景这不仅是对自己负责也是保护整个安全研究环境。在此基础上我建议你对自己的模块库做版本管理和分类管理。可以按功能前缀命名比如collect_、exec_、persist_方便在大量模块中快速检索。每次修改模块都记录一下变更原因尤其是 API 适配方面的调整。等模块数量多了这套管理习惯能帮你省下大量时间。还有一个小技巧保留一个“最小模板模块”每次写新模块从它拷贝而不是从旧模块里改。这样可以避免把旧模块里的参数残留和依赖项带到新模块里减少调试成本。最后一点个人体会如果你已经折腾完上面的流程大概率能感受到 Boo 模块体系真正厉害的地方它把开发重心从“理解编译链路”转移到了“表达功能意图”。你不需要知道 C# 工程是怎么把客户端喂给主机的只需要清楚 .NET 平台上哪个 API 能帮你完成想要的动作。这就为那些熟悉操作逻辑但不熟悉底层 C2 实现的同学提供了一个非常顺滑的切入点。我最初上手的时候被模块基类名称和一些参数特性的细节卡了整整一个下午。那种看着源码但不确定该继承哪个类、该覆写哪个方法的状态确实很磨人。但等你摸索出第一个能跑的模块后面就像打开了一个工具箱。我现在写一个简单的侦察模块从设计到验证十分钟左右完成而且很多代码模式是复用的。希望这篇教程能让你少走我走过的弯路顺利进入 SILENTTRINITY 模块开发的大门。