Sliver 服务端数据模型全解析:server/db/models 的 ORM 架构与持久化设计
Sliver 服务端数据模型全解析:server/db/models 的 ORM 架构与持久化设计
本文以
server/db/models/README.md为骨架,系统梳理 Sliver(Adversary Emulation Framework)服务端状态持久化的全部 ORM 模型:从 beacon、canary、certificates 到 crackstations 等 18 个模型文件各自负责的数据域、字段设计、关联关系与 GORM 钩子约定,并深入server/db、server/configs/database.go等配套源码,讲清这些模型如何被自动迁移到 SQLite / PostgreSQL / MySQL,以及测试如何验证 protobuf 双向转换与索引设计。读完本文,你将能够独立定位任意一个服务端状态对应的表与字段,理解 Sliver 数据层的组织方式和扩展新模型的标准流程。
Overview:models 包在 Sliver 中的定位
根据 server/db/models/README.md 的定义,server/db/models 是 Database models and ORM definitions for server state——即服务端全部状态的数据库模型与 ORM 定义层。它负责:
- 定义数据模式(schema)与表结构;
- 描述模型之间的关联关系(一对一、一对多、外键);
- 提供查询辅助方法(如
ToProtobuf()/FromProtobuf()转换、状态判定方法); - 通过 GORM 钩子(
BeforeCreate)在写入前自动填充 UUID、时间戳等字段。
README 指出该子系统中的关键例程(Key routines)覆盖 beacon、canary、certificates 和 crackstations 四大块,它们是 C2 通信、DNS 蜜罐、TLS 证书体系与离线密码破解这四条核心业务线的数据基座。除 README 列出的 18 个模型文件外,目录中还包含 README 未列出的支撑文件:uuid.go(UUID 持久化类型)、legacy_slice_serializer.go(旧版切片序列化兼容)、wgip_reservations.go(WireGuard 隧道 IP 分配)、ai.go(AI 会话存储)以及若干 *_test.go 测试文件。
目录导览:18 个模型文件各司其职
下表完整对应 README 的文件清单,并补充各文件的职责:
| 文件 | 职责(README 原文) | 核心模型 |
|---|---|---|
| beacon.go | 定义 beacon ORM 模型及其关联关系 | Beacon、BeaconTask |
| canary.go | 存储 DNS canary 记录与状态标志 | DNSCanary |
| certificate_authorities.go | 存储服务端证书颁发机构 PEM 数据 | CertificateAuthority |
| certificates.go | 持久化监听器与操作员的证书元数据 | Certificate |
| crackstations.go | 建模破解节点与基准测试数据 | Crackstation、CrackJob、CrackTask、CrackCommand 等 |
| credentials.go | 保存捕获的凭证条目与标签 | Credential |
| host.go | 表示发现的主机及主机专属元数据 | Host、IOC、ExtensionData |
| http-c2.go | 存储 HTTP C2 配置数据 | HttpC2Config 及 6 个子模型 |
| implant.go | 跟踪植入体构建、配置与产物 | ImplantBuild、ImplantConfig、ImplantProfile |
| jobs.go | 记录长期运行的服务器任务与状态 | ListenerJob 及 5 种监听器 |
| keyex.go | 持久化植入体的密钥交换状态 | KeyExHistory |
| keyvalue.go | 通用键值存储(杂项设置) | KeyValue |
| loot.go | 描述战利品工件与文件位置 | Loot |
| monitor.go | 存储监控任务配置 | MonitoringProvider |
| operator.go | 表示操作员、权限与 MFA 数据 | Operator |
| resource_id.go | 分配人类可读的资源标识符 | ResourceID |
| website.go | 捕获托管网站配置与资产 | Website、WebContent |
| wgkeys.go | 存储 WireGuard 对端密钥与元数据 | WGKeys、WGPeer 等 |
贯穿所有模型的基础设施
自定义 UUID 类型
几乎每个模型的主键都是 UUID 类型(gorm:"primaryKey;->;<-:create;type:uuid;")。该类型定义在 uuid.go:
type UUID uuid.UUID // 基于 Go 标准库 uuid 的命名类型
注释明确了引入它的原因:标准库类型未实现 database/sql 的 Scanner 与 driver.Valuer 接口,因此 models 包用命名类型补齐持久化边界,同时保留 Sliver 既有数据库的规范字符串表示。关键能力:
NewUUID():生成随机 v4 UUID;NilUUID():全零 UUID,用于判断"未赋值";ParseUUID()/ParseUUIDOrNil():解析文本 UUID,兼容紧凑形式、{...}花括号包裹与urn:uuid:URN 前缀(见ParseUUID中len==34/len==41的分支处理);Scan()/Value():支持从UUID、uuid.UUID、16 字节原始数据、字符串四种来源读取,写入时统一输出规范小写字符串。
UUIDFromBytes 对长度有严格要求:必须是 16 字节,否则返回错误 "uuid: UUID must be exactly 16 bytes long"。
BeforeCreate 钩子约定
全部模型统一实现了 BeforeCreate(tx *gorm.DB) error 钩子,模式高度一致:写入前自动生成 UUID、打上 CreatedAt = time.Now() 时间戳。例如 certificate_authorities.go 中的 CertificateAuthority.BeforeCreate:
func (c *CertificateAuthority) BeforeCreate(tx *gorm.DB) (err error) {
c.ID = NewUUID()
c.CreatedAt = time.Now()
return nil
}
这意味着业务代码创建记录时无需手动指定主键与创建时间,GORM 会在 INSERT 前自动填充,保证所有表的 ID 均为随机 UUID、创建时间均由数据库模型层统一维护。字段标签 gorm:"->;<-:create;" 进一步声明这些字段只读(->)、仅在创建时可写(<-:create)。
旧版切片序列化兼容层
legacy_slice_serializer.go 注册了一个名为 legacyjsonslice 的 GORM 自定义序列化器(schema.RegisterSerializer),专门处理 CrackCommand 中的历史列表字段(Hashes、OutfileFormat、CPUAffinity 等,标签如 gorm:"type:text;serializer:legacyjsonslice")。其价值在于:既能按 JSON 数组读写,又能兼容历史上单元素标量存储与 PostgreSQL 花括号数组语法({1,3}),从而保证旧库升级(AutoMigrate 改列类型)时数据不丢失——这一点被 crackstations_test.go 中的 TestCrackCommandLegacyScalarSlicesSurviveMigration 明确覆盖。
数据库初始化与自动迁移
models 包本身只定义结构,真正把结构变成表的是 server/db/sql.go 中的 newDBClient()。它根据 configs.GetDatabaseConfig() 返回的方言选择驱动,支持三种后端:
| 方言 | 常量 | 驱动 |
|---|---|---|
| SQLite | configs.Sqlite |
glebarez/sqlite(纯 Go,免 CGO) |
| PostgreSQL | configs.Postgres |
gorm.io/driver/postgres |
| MySQL | configs.MySQL |
gorm.io/driver/mysql |
初始化时,代码逐模型调用 AutoMigrate(而非一次性传入全部模型),注释明确说明原因:if one fails, subsequent models will not be created——单个模型迁移失败不影响后续模型建表。完整迁移清单包含约 50 个模型,从 AIConversation、Beacon、DNSCanary、Crackstation、CertificateAuthority、Host、ImplantProfile、ListenerJob、MonitoringProvider 等,与 README 的文件清单一一对应。迁移完成后还会对连接池做配置:SetMaxIdleConns、SetMaxOpenConns、连接最大复用时长 1 小时。
数据库连接参数来自 server/configs/database.go,配置文件为应用根目录下 configs/database.yaml(旧版 database.json 会被自动迁移)。核心配置项:
dialect: sqlite3 # sqlite3 | postgresql | mysql
database: sliver
username: ""
password: ""
host: "127.0.0.1"
port: 5432
params: {} # 连接参数(URL 编码)
pragmas: {} # 仅 SQLite 生效
max_idle_conns: 10 # 默认值见 getDefaultDatabaseConfig
max_open_conns: 100
log_level: warn
SQLite 默认启用一组 pragma 以兼顾并发与耐久性:journal_mode(WAL)、busy_timeout(5000)、synchronous(NORMAL)、temp_store(MEMORY),并附加 cache=shared;用户若显式提供 _pragma 参数则不会覆盖其自定义值。
信标与任务模型:beacon.go
beacon.go 是 C2 异步通信(Beacon 模式)的数据核心,包含两个模型:
Beacon——代表一台运行 beacon 植入体的主机,字段覆盖完整的主机画像与通信状态:
- 主机信息:
Hostname、Username、UID/GID、OS、Arch、Locale、Integrity(BeforeCreate默认置为"-")、Capabilities(uint64 能力位图); - 通信信息:
Transport、RemoteAddress、ActiveC2、ProxyURL、Version; - 心跳信息:
LastCheckin、ReconnectInterval、Interval、Jitter、NextCheckin; - 关联:
ImplantBuildID指向植入体构建,Tasks []BeaconTask一对多挂载任务队列。
BeaconTask——beacon 的任务执行单元,是"异步下发-回传"机制的持久化载体:
type BeaconTask struct {
ID UUID `gorm:"primaryKey;->;<-:create;type:uuid;"`
EnvelopeID int64 `gorm:"uniqueIndex"` // 任务信封唯一编号
BeaconID UUID `gorm:"type:uuid;"`
CreatedAt time.Time `gorm:"->;<-:create;"`
State string // pending / sent / completed / canceled
SentAt int64
CompletedAt int64
Description string
Request []byte // *sliverpb.Envelope(序列化后的请求)
Response []byte // *sliverpb.Envelope(序列化后的响应)
}
状态常量定义在文件顶部:PENDING、SENT、COMPLETED、CANCELED。BeforeCreate 钩子用 crypto/rand 生成 8 字节随机数填充 EnvelopeID。Beacon.Task() 方法把 *sliverpb.Envelope 用 proto.Marshal 序列化后包装成 PENDING 状态的 BeaconTask,是 RPC 层下派任务的入口。ToProtobuf(content bool) 的布尔参数控制是否携带大体积的 Request/Response 二进制内容,列表场景可传 false 避免拉取全部数据。测试 beacon_test.go 验证了 Capabilities 字段在模型与 protobuf 间的传递。
监听任务模型:jobs.go
jobs.go 持久化服务器上长期运行的各类监听器任务。顶层模型 ListenerJob 以 JobID uint32(唯一)标识任务编号,Type 字段标明监听器类型,并同时内嵌五类监听器配置结构:
HTTPListener:Domain、Host、Port、Secure、Website、内嵌Cert/Key([]byte)、Acme、EnforceOtp、LongPollTimeout、LongPollJitter、RandomizeJarm、Staging——覆盖 HTTP(S) 监听、ACME 证书、长轮询、JARM 指纹随机化等特性;MTLSListener:Host、Port;DNSListener:Domains []DnsDomain(一对多)、Canaries、Host、Port、EnforceOtp;WGListener:Host、Port、NPort、KeyPort、TunIP;MultiplayerListener:多人模式监听,含WireGuard与WireGuardOptIn两个字段——注释说明WireGuardOptIn用于区分"用户显式选择"与"WireGuard 曾短暂作为默认值自动创建的行",ToProtobuf()中WireGuard: j.WireGuard && j.WireGuardOptIn保证了只有显式选择才会生效。
ListenerJobFromProtobuf() 根据 Type 常量(constants.HttpStr、MtlsStr、DnsStr、WGStr、MultiplayerModeStr 等)把 protobuf 请求还原为对应监听器模型。
植入体构建与配置:implant.go
implant.go 是植入体生命周期(构建→配置→运行)的持久化中心,也是整个 models 包字段最丰富的文件之一。
ImplantBuild——一次植入体构建产物。除 Name(唯一)、三个校验和字段(MD5、SHA1、SHA256)与 Burned 标志(是否已在威胁情报平台曝光)外,还按传输通道分组保存了一整套密钥材料:
- ECC:
PeerPublicKey、PeerPublicKeyDigest、PeerPrivateKey、PeerPublicKeySignature、AgeServerPublicKey、MinisignServerPublicKey; - MTLS:
MtlsCACert、MtlsCert、MtlsKey; - WireGuard:
WGImplantPrivKey、WGServerPubKey; Stage:是否为分阶段(staged)构建。
ImplantConfig——植入体构建配置,字段按功能域划分:Go 目标(GOOS/GOARCH/TemplateName)、beacon 行为(IsBeacon、BeaconInterval、BeaconJitter)、规避选项(Evasion、ObfuscateSymbols、SGNEnabled、ShellcodeEncoder)、传输开关(IncludeMTLS/IncludeWG/IncludeHTTP/IncludeDNS/IncludeTCP/IncludeNamePipe)、运行限制(LimitDomainJoined、LimitHostname、LimitUsername、LimitDatetime、LimitFileExists、LimitLocale)、输出格式(Format、IsSharedLib、IsService、IsShellcode)、Donut shellcode 选项(DonutEntropy 等 8 项)以及 C2 []ImplantC2 与 CanaryDomains []CanaryDomain 两个关联列表。HttpC2ConfigName 字段把配置与 HTTP C2 模型串接起来。
配套子模型:
ImplantC2:C2 地址条目(Priority、URL、Options),IsC2Enabled()辅助函数按 URL scheme 判断某类传输是否启用;CanaryDomain:canary 域名,属于ImplantConfig;ImplantProfile:可复用配置模板,Name唯一,内嵌一个*ImplantConfig;EncoderAsset:仅记录被嵌入植入体的资产名(不保存实际数据)。
ImplantConfigFromProtobuf 在还原时会对 C2 URL 做 url.Parse 校验并记录告警日志,无效 URL 会被跳过(见 copyC2List)。
HTTP C2 配置模型:http-c2.go
http-c2.go 存储 HTTP(S) C2 信道的外观配置,是"流量伪装"能力的数据基础。顶层 HttpC2Config(Name 唯一)拆分为服务端与植入体两侧配置:
HttpC2ServerConfig:RandomVersionHeaders(随机版本头)、Headers []HttpC2Header、Cookies []HttpC2Cookie。
HttpC2ImplantConfig:包含伪装与路径生成的全部参数:
UserAgent、ChromeBaseVersion(默认 106)、MacOSVersion(默认10_15_7)、NonceQueryArgChars、NonceQueryLength、NonceMode;ExtraURLParameters、Headers;- 生成参数:
MinFileGen/MaxFileGen、MinPathGen/MaxPathGen、MinPathLength/MaxPathLength; Extensions(GORM 不支持原生字符串数组,用逗号分隔存 text,转换时strings.Split/strings.Join);PathSegments []HttpC2PathSegment。
六个子模型 HttpC2Header、HttpC2Cookie、HttpC2URLParameter、HttpC2PathSegment 均通过外键回指父配置。其中 HttpC2URLParameter 的注释明确 Name 至少 3 字符、Probability 取值 0–100;HttpC2PathSegment 的 SegmentType 注释标注取值含义为 Poll / Session / Close。
文件末尾的 RandomizeImplantConfig() 是植入体生成时使用的核心入口:它从父配置中按 Min/Max 范围随机抽样路径段(RandomPathSegments 内部按 IsFile 分拣出文件与路径两类后分别抽样,randomSample 保证空输入不 panic),并按目标 OS/Arch 用 GenerateUserAgent 生成对应平台的 Chrome UA 字符串(Windows/Linux/macOS 分支,macOS 使用 Macintosh; Intel Mac OS X 模板)。相关边界行为由 http-c2_test.go 中的 TestRandomSampleEmptyValues 与 TestRandomPathSegmentsWithNoSegments 覆盖。
侦察与情报模型
canary.go:DNS Canary
DNSCanary(canary.go)记录 DNS canary 域名及其触发状态:ImplantName、Domain、Triggered(是否被访问)、FirstTrigger/LatestTrigger、Count(触发次数)。ToProtobuf() 用 time.RFC1123 格式化时间戳,反向转换 DNSCanaryFromProtobuf 则解析同一格式——保持跨端时间字符串的可互换性。
host.go:主机与 IOC
Host(host.go)表示被发现的受害主机:HostUUID(唯一)、Hostname、OSVersion、Locale,并通过 foreignKey:HostID;references:HostUUID 外键声明关联两个列表——IOCs []IOC(上传到远程系统的可疑文件:Path、FileHash)与 ExtensionData []ExtensionData(扩展输出:Name、Output)。ToProtobuf() 会把 ExtensionData 展平成以 Name 为键的 map,便于客户端按扩展名检索输出。
凭证与战利品模型
Credential(credentials.go)保存捕获的凭证:OriginHostUUID(来源主机)、Collection(集合标签)、Username、Plaintext、Hash(注释指向 hashcat 的 example_hashes 作为哈希类型参考)、HashType(int32)、IsCracked。HashType 与 protobuf 中的 clientpb.HashType 直接对应,为破解子系统提供类型映射。
Loot(loot.go)描述战利品工件:FileType、Name、Size、OriginHostID。注意它只存元数据与来源,实际文件内容由服务器文件系统管理,ToProtobuf() 输出 clientpb.Loot 供客户端展示。
操作员与多人模式模型
Operator(operator.go)表示有权连接服务器的操作员账号:
Token:注意存储的是令牌的 SHA256 哈希(gorm:"uniqueIndex",注释明示 "This is the SHA256 of the token"),而非明文;- 三级权限位:
PermissionAll(访问全部 gRPC API)、PermissionBuilder(构建器 API)、PermissionCrackstation(破解站 API),默认均为false; - WireGuard 支持:
WGPubKey、WGTunIP。
同文件的 GenerateOperatorToken() 使用 crypto/rand 读取 32 字节生成 64 位十六进制令牌,失败直接 panic(安全随机数不可降级)。
WireGuard 密钥模型(wgkeys.go)包含三个模型:WGKeys(服务器私钥/公钥)、MultiplayerWGKeys(多人模式专用)、WGPeer(对端:含 TunIP)。三者共用 initWGKeysModel 填充 ID 与时间戳。wgip_reservations.go 中的 WGIPReservation 则负责跨所有消费者统一分配 WireGuard 隧道 IP(TunIP 唯一索引、OwnerType 取值 operator/peer、OwnerID),避免 IP 冲突。
证书与 CA 模型
CertificateAuthority(certificate_authorities.go)与 Certificate(certificates.go)结构几乎一致,均含 CommonName、CAType、KeyType(仅叶子证书)、CertificatePEM、PrivateKeyPEM。区别在于用途:CA 模型专门存储服务端证书颁发机构的 PEM 数据(注释强调 Stored separately from leaf certificates),叶子证书模型则持久化各监听器与操作员的证书元数据。二者分离存储,符合"CA 私钥与业务证书私钥隔离"的安全实践。
密码破解子系统:crackstations.go
crackstations.go 是 models 包中关联结构最复杂的文件,完整建模了分布式 hashcat 破解流水线,也是 README 点名的关键例程之一:
Crackstation:破解节点(ID 即节点名),记录OperatorName、HashcatVersion、BenchmarkHashcatVersion、BenchmarkSchemaVersion,关联Tasks []CrackTask与Benchmarks []Benchmark;Benchmark:性能基准(HashType、PerSecondRate每秒速率);CrackFile/CrackFileChunk:破解文件(字典、规则等)的分块传输模型,MaxN(chunkSize)按分块大小计算总块数(math.Ceil),Sha2_256校验完整性;CrackJob:一个任务 = 父命令,其 keyspace 可能被拆成多个CrackTask分发到多个破解站。状态判定由Status()方法集中实现:按CancelledAt/PausedAt/CompletedAt/Err以及子任务状态推导出CANCELLED/PAUSED/IN_PROGRESS/FAILED/COMPLETED;字段RecoveredBytes注释说明它用于限制流式结果批次的累积恢复负载,属于内部队列记账、不暴露给操作员;CrackTask:分发到具体破解站的任务分片,含租约机制字段(LeaseExpiresAt、LastHeartbeatAt、LeaseToken、Attempt重试次数)、输出字段(Stdout/Stderr及截断标志与总字节数)、分片范围(ShardSkip/ShardLimit);CrackResult:恢复出的 hash/明文对,Fingerprint为服务器生成的幂等键(唯一索引,不通过 RPC 暴露);CrackJobCredential:记录任务选中的凭证行(CrackJobID+CredentialID联合唯一);CrackCommand:完整封存 hashcat 命令的全部参数——从AttackMode、HashType、Hashes到后端选择(BackendIgnoreCUDA/BackendIgnoreHip/BackendIgnoreMetal/BackendIgnoreOpenCL)、性能调优(WorkloadProfile、KernelAccel、KernelLoops、KernelThreads)、规则与字符集(CustomCharset1-8、RuleLeft/RuleRight、RulesFile)、brain 客户端等,甚至保留了一批*V7后缀字段(如HashMode、HccapxMessagePairV7)以兼容 hashcat v7 参数变体。列表字段用legacyjsonslice序列化器持久化。
索引设计上,CrackJob.CompletedAt+CreatedAt 组成 idx_crack_jobs_top 复合索引(CompletedAt 优先)服务"top 轮询",CrackTask 的 CrackJobID+State 与 LeaseExpiresAt+State 分别服务任务查询与租约回收,CrackCommand.CrackJobID 索引服务命令回溯。crackstations_test.go 中的 TestCrackTopPollingIndexes 用 PRAGMA index_info 直接断言了这三个索引的列顺序。
网站托管模型:website.go
website.go 支持 C2 服务器托管诱饵网站。Website(Name 唯一)一对多关联 WebContent;WebContent 保存 Path、Size、ContentType、OriginalFile、Sha256,实际内容不落库——Website.ToProtobuf(webContentDir) 从磁盘按 webcontent.ID.String() 拼路径读取文件字节,读不到则跳过,体现了"数据库存元数据、文件系统存内容"的设计。
杂项存储模型
KeyExHistory(keyex.go):密钥交换历史,主键直接是Sha256字符串,用于防重放/去重;KeyValue(keyvalue.go):通用键值存储,Key唯一,承载各类杂项设置;MonitoringProvider(monitor.go):监控服务提供商配置(Type注释为 vt 或 xforce,APIKey、APIPassword);ResourceID(resource_id.go):分配人类可读资源标识,Type取值encoder或stager,Value为请求中引用的素数编号。
AI 会话模型:ai.go
README 未列出但实际存在于目录中的 ai.go 为 Sliver 的 AI 辅助能力提供持久化:AIConversation(会话)记录操作员、Provider、Model、ThinkingLevel、SystemPrompt、上下文窗口用量(输入/输出/总 token、窗口大小及是否估算)、会话目标(TargetSessionID/TargetBeaconID),一对多关联 AIConversationMessage(消息,含角色、序号、工具调用四元组 ToolCallID/ToolName/ToolArguments/ToolResult、可见性与状态等)。消息的 IncludeInContext 在未显式设置时由可见性(AI_MESSAGE_VISIBILITY_CONTEXT)推导,控制哪些消息参与上下文窗口计费与提交。
如何验证:测试覆盖哪些保证
models 包的测试文件从三个维度锁定了数据层行为:
- protobuf 双向转换:crackstations_test.go 的
TestCrackCommandExtendedFieldsProtobufRoundTrip用 60+ 字段的完整样例验证FromProtobuf(ToProtobuf(x)) == x;TestCrackTaskProtobufRoundTrip验证任务字段(含二进制 Stdout/Stderr)往返一致;TestCrackTaskZeroTimestampsRemainUnset保证零值时间戳不误编码; - SQLite 实际落库往返:
TestCrackCommandSQLiteRepeatedFieldsRoundTrip用glebarez/sqlite打开临时库真实执行AutoMigrate→Create→First,验证切片字段持久化;迁移测试模拟旧列类型升级与 PostgreSQL 花括号数组的过渡表示; - 索引与边界:
TestCrackTopPollingIndexes检查轮询索引列序;http-c2_test.go 覆盖随机抽样的空输入边界;beacon_test.go 校验字段透传。
小结
server/db/models 是 Sliver 服务端状态的单一事实源(single source of truth):从 C2 信标、监听任务、植入体构建到破解流水线、证书体系、WireGuard 密钥与 AI 会话,全部围绕"GORM 模型 + UUID 主键 + BeforeCreate 钩子 + protobuf 转换方法"这一套统一约定组织。理解这 18+ 个模型文件及其关联结构,就等于掌握了 Sliver 服务端数据层的完整地图;在此基础上,通过 server/db/sql.go 的 AutoMigrate 清单可以看清全量表集合,通过 *_test.go 可以确认每个模型对外承诺的持久化与转换行为。