- 网络安全
【免费下载链接】sliver
Adversary Emulation Framework
本篇技术指南围绕随 Sliver 植入体源码一并 vendored 的第三方依赖 go-clr 变更日志 展开,完整梳理该库四个已发布版本(v1.0.0 ~ v1.0.3)的每一次变更背景,并结合仓库内 go-clr 源码(接口封装、STDOUT/STDERR 重定向、HRESULT 处理)与 Sliver 植入体在taskrunner中的实际集成调用链,说明这些版本演进如何影响 Windows 端进程内 .NET 程序集执行能力。读完本文,你将掌握 go-clr 各版本核心修复的底层原理,以及 Sliver 如何通过LoadCLR/LoadAssembly/InvokeAssembly组合复用它执行内存中的 .NET 程序集。
一、go-clr 是什么,为什么它出现在 Sliver 仓库中
go-clr 是一个用纯 Go 编写的 PoC 级库,目标是在 Go 进程内托管 CLR(Common Language Runtime),从而直接从磁盘加载 .NET DLL,或从内存加载 .NET 程序集(assembly)并执行。它的实现方式是不依赖 cgo,而是通过封装 Windows 系统调用(syscall)与大量unsafe.Pointer操作,从内存中按 COM 接口 vtable 布局装载结构体,逐一调用ICLRMetaHost、ICLRRuntimeInfo、ICORRuntimeHost等托管宿主接口。
该库以 vendored 依赖的形式被固定在 Sliver 仓库中(目录 implant/vendor/github.com/Ne0nd0g/go-clr),并在植入体侧的 dotnet_windows.go 中被显式导入使用,其角色是支撑 Windows 植入体执行execute-assembly类任务。其自述文档(README.md)也坦诚说明:这是实验性 PoC,大量使用unsafe,并非生产级稳定代码,但作为攻防对抗(Adversary Emulation)框架的进程内 .NET 执行组件,其价值在于完全用 Go 复刻了 CLR 托管的关键路径。
二、版本 1.0.0(2021-04-08):从 fork 出发的初始标签版
变更日志对本版本的描述是"Initial tagged version from forked"——即从上游 fork 后打出的第一个正式标签。
2.1 新增:STDOUT/STDERR 重定向缓冲
本版本最重要的新增特性被明确记录为"Added buffer to collect redirected STDOUT/STDERR"(新增缓冲区以收集重定向后的 STDOUT/STDERR)。这一能力在今天的源码中依然保留,构成了 io.go 的核心:
- 包级声明了
Stdout、Stderr两个bytes.Buffer(见 io.go),以及用于串行化读写的mutex; RedirectStdoutStderr()通过os.Pipe()分别为 STDOUT/STDERR 创建新的读写文件句柄,再用windows.SetStdHandle将STD_OUTPUT_HANDLE与STD_ERROR_HANDLE指向新句柄(见 io.go);- 随后启动
BufferStdout()/BufferStderr()两个 goroutine,以 4KB 缓冲区循环读取管道内容,并调用bytes.TrimRight(buf, "\x00")剔除空字节后写入Stdout/Stderr缓冲(见 io.go)。
之所以必须重定向,原因在代码注释中写得很清楚:CLR 执行托管程序集时运行在 Go 进程之外,无法用普通的 Go 方式捕获输出,因此只能把进程级句柄换掉再收回来。这一点正是 C2 框架需要"拿到程序集输出并回传"的关键机制,Sliver 正是这一设计的直接受益者。
三、版本 1.0.1(2021-04-08):标签与导入路径的修正
变更日志用一句带过:"Learned how to correctly use tags and updated import"(学会正确使用标签,并更新了导入路径)。
从仓库现状可以印证这次修正的结果:
- 项目模块从
github.com/ropnop/go-clr迁至github.com/Ne0nd0g/go-clr(README 中的安装示例使用go get github.com/ropnop/go-clr,而 Sliver 的导入语句已是clr "github.com/Ne0nd0g/go-clr",见 dotnet_windows.go); - 模块元数据在 implant/go-mod 与 implant/go-sum 中记录为
github.com/Ne0nd0g/go-clr; - 该版本同日发布,与 v1.0.0 在同一天,说明这是对初版标签的快速修正,而不是功能性迭代。
对使用者而言,这个版本变化意味着导入路径从原作者 fork 的仓库切换到了维护者的新仓库,Sliver 仓库内记录的正是修正后的版本路径。
四、版本 1.0.2(2022-04-02):目标程序集加载修复与依赖升级
本版本有两项变更:
4.1 修复:正确加载目标程序集
变更日志记录合并了来自 @audibleblink 的 Pull 2,"fixed an error when attempting to load correctly targeted assemblies"(修复了尝试加载正确目标程序集时出现的错误)。
结合源码可以理解这项修复对应的执行路径。程序集从字节数组加载的完整链路位于 go-clr.go 的ExecuteByteArray中:
- 通过
CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost)创建 metahost; - 调用
GetInstalledRuntimes枚举已安装运行时,并按目标版本筛选(如"v4"); - 取得
ICLRRuntimeInfo后调用IsLoadable()校验可加载性; - 取得
ICORRuntimeHost并获取默认 AppDomain; - 将原始字节构造为 SafeArray(
CreateSafeArray),调用appDomain.Load_3(safeArrayPtr)从内存加载程序集; - 通过
assembly.GetEntryPoint()取入口方法,再依据方法签名是否接收参数来决定是否用PrepareParameters构造参数 SafeArray,最后methodInfo.Invoke_3执行。
任何一步的 HRESULT 非零都会返回错误——v1.0.2 的修复正是针对"加载到的程序集并非调用方所期望的目标程序集"这类寻址/加载问题,确保Load_3与后续入口点解析能命中正确的程序集映像。
4.2 变更:升级支撑 Go 模块
同步更新了所依赖的 Go 模块版本。仓库内 hresult.go 依赖golang.org/x/sys/windows(io.go 中大量使用其句柄与 DLL 函数),这类基础设施模块的升级通常伴随接口签名或常量定义的同步更新,也是该版本"Change"而非"Fix"的原因。
五、版本 1.0.3(2022-11-10):错误处理与无控制台输出两大修复
这是变更日志中记录的最新一个版本,包含一次行为变更和一次缺陷修复,也是 Sliver 仓库 vendored 到的版本。
5.1 行为变更:返回错误而非直接退出程序
合并自 @mec07 的 Pull 3,"return errors instead of exiting the program"(返回错误而非直接退出程序)。这一变更把库的失败模式从"进程级os.Exit"改为"函数级返回 error",对库的宿主(尤其是 C2 植入体)至关重要——如果 .NET 程序集执行失败就令整个植入体进程退出,后果是灾难性的。
从当前源码看,这一设计已贯穿所有对外 API:
ExecuteDLLFromDisk在ExecuteInDefaultAppDomain返回非零值时,会返回"the ICLRRuntimeHost::ExecuteInDefaultAppDomain method returned a non-zero return value: %d"错误(见 go-clr.go);LoadCLR对运行时枚举失败返回包装错误(见 go-clr.go);InvokeAssembly在执行出错时仍继续读取重定向缓冲区并返回 stdout/stderr,注释明确"Don't return because there could be data on STDOUT/STDERR"(见 go-clr.go)。
5.2 缺陷修复:无控制台应用无法返回输出
对应 Issue 4 ——"Application's Without A Console Will Not Return Output"(无控制台的应用程序不会返回输出)。
该问题的根因与修复逻辑完全保留在 io.go 中:
kernel32 := windows.NewLazySystemDLL("kernel32.dll") getConsoleWindow := kernel32.NewProc("GetConsoleWindow") // Ensure the process has a console because if it doesn't there will be no output to capture hConsole, _, _ := getConsoleWindow.Call() if hConsole == 0 { // https://learn.microsoft.com/en-us/windows/console/allocconsole allocConsole := kernel32.NewProc("AllocConsole") ret, _, err := allocConsole.Call() if ret == 0 { return fmt.Errorf("there was an error calling kernel32!AllocConsole with return code %d: %s", ret, err) } // Get a handle to the newly created/allocated console hConsole, _, _ = getConsoleWindow.Call() // Hide the console window user32 := windows.NewLazySystemDLL("user32.dll") showWindow := user32.NewProc("ShowWindow") ret, _, err = showWindow.Call(hConsole, windows.SW_HIDE) ... }修复逻辑分三步:先用GetConsoleWindow探测当前进程是否有控制台;若没有(hConsole == 0,典型场景即 C2 植入体这种无控制台的 GUI/服务进程),则调用AllocConsole分配一个隐藏控制台,再用ShowWindow(..., SW_HIDE)将其隐藏,最后才执行SetStdHandle重定向。这一步顺序非常关键:重定向到管道的前提是进程存在可写的标准输出设备,没有控制台时重定向无从谈起。
代码注释还记录了一个容易踩的坑:被重定向的 STDOUT 写入端一旦被关闭,后续的Invoke_3会触发COR_E_TARGETINVOCATION(0x80131604,定义见 hresult.go),因此缓冲读取采用"永不关闭写入端、依赖 goroutine 持续读管道"的模式。
六、Sliver 侧的真实消费方式:从版本能力到实战链路
go-clr 的 API 在 Sliver 植入体中被组织成"单例 CLR + 程序集缓存"的使用模式,见 dotnet_windows.go:
CLRInstance结构体持有一个*clr.ICORRuntimeHost与sync.Mutex,通过GetRuntimeHost懒加载:首次调用时执行clr.LoadCLR(runtime)并在失败时尽力clr.RedirectStdoutStderr()(见 dotnet_windows.go);LoadAssembly(data, assemblyArgs, runtime)先按sha256.Sum256计算程序集哈希并查询assemblies缓存;未命中才调用clr.LoadAssembly(rtHost, data)加载,随后统一走clr.InvokeAssembly(methodInfo, assemblyArgs)执行(见 dotnet_windows.go);- 一个细节处理:当参数为
[""]时,会改写为[" "],因为空字符串切片会让 CLR 加载器去匹配无参方法,导致Main(String[] args)无法命中(见 dotnet_windows.go)。
再往上,task_windows.go 的InProcExecuteAssembly在调用LoadAssembly前还会视开关执行 AMSI 补丁(patchAmsi)与 ETW 补丁(patchEtw),形成完整的"进程内 execute-assembly"链路。可以说,go-clr 各版本积累的错误返回、无控制台输出捕获、目标程序集加载修复,最终都转化为 Sliver Windows 植入体执行 .NET 程序集时的稳定性和可观测性。
七、版本采纳与升级建议
当前 Sliver 仓库 vendored 的 go-clr 即为变更日志中的最新版v1.0.3(2022-11-10),可通过 modules.txt 与 go-sum 核实其精确依赖版本。结合各版本变更点,可以得出以下采纳要点:
| 版本 | 日期 | 核心变更 | 对 Sliver 类宿主的意义 |
|---|---|---|---|
| v1.0.0 | 2021-04-08 | fork 初始标签;新增 STDOUT/STDERR 重定向缓冲 | 奠定进程内输出捕获基础 |
| v1.0.1 | 2021-04-08 | 修正标签与导入路径 | 导入路径固定为github.com/Ne0nd0g/go-clr |
| v1.0.2 | 2022-04-02 | 修复目标程序集加载;升级 Go 模块 | 提升从内存加载程序集的命中准确性 |
| v1.0.3 | 2022-11-10 | 错误返回替代进程退出;修复无控制台输出丢失 | 植入体健壮性与输出完整性的关键版本 |
在将 go-clr 集成进长驻进程(如 C2 植入体)时,应从 v1.0.3 起步:它保证 .NET 执行失败不会拖垮宿主进程,且为无控制台进程补齐了AllocConsole+ 隐藏窗口的兜底路径。同时需要注意其 PoC 定位——代码大量依赖unsafe与 COM vtable 手动布局(如ICLRRuntimeInfoVtbl的逐方法指针定义,见 iclrruntimeinfo.go),在引入生产环境前应充分评估其稳定性边界,并在植入体侧(如 Sliver 所做的那样)以互斥锁、程序集缓存与独立的 stdout/stderr 缓冲来隔离其不确定性。
- 网络安全
【免费下载链接】sliver
Adversary Emulation Framework
相关推荐
从 v1.0.0 到 v1.0.3:Sliver 内置 go-clr 库的版本演进与 .NET 内存执行实现解析
从 v1.0.0 到 v1.0.3:Sliver 内置 go clr 库的版本演进与 .NET 内存执行实现解析 本文以 Sliver 仓库中内置的 githu
网络安全Podman 中的 go-zfs 库演进全解析:从 v1.0.0 到 v3.0.0 的 ZFS 管理能力变迁
Podman 中的 go zfs 库演进全解析:从 v1.0.0 到 v3.0.0 的 ZFS 管理能力变迁 go zfs( github.com/mistif
容器运行时云原生CLI深入 go-clr:在 Go 进程中托管 .NET CLR 并执行 DLL 与内存程序集的技术剖析
深入 go clr:在 Go 进程中托管 .NET CLR 并执行 DLL 与内存程序集的技术剖析 本文基于 Sliver 仓库内 vendored 的 git
网络安全
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考