Posted in

【绝密流程】Mac Go环境配置的「军工级」审计清单(含SHA256校验、证书链验证、二进制签名比对、沙箱行为日志采集)

第一章:Mac Go环境配置的「军工级」审计总览

在 macOS 平台上构建 Go 开发环境,绝非仅执行 brew install go 即可高枕无忧。真正的「军工级」审计要求对工具链完整性、二进制可信源、环境隔离性、路径策略及安全边界进行逐层验证与加固。

核心审计维度

  • 来源可信性:必须从官方 go.dev/dl 下载 .pkg.tar.gz,禁用第三方镜像(如国内代理)用于首次安装,避免供应链投毒风险;
  • 签名与校验:下载后务必核验 SHA256 哈希值与 GPG 签名(Go 官方发布密钥已预置在 https://go.dev/dl/KEYS);
  • 路径最小权限原则GOROOT 应设为 /usr/local/go(需 sudo chown -R root:wheel /usr/local/go),禁止写入用户主目录;
  • 模块代理与校验:启用 GOPROXY=direct + GOSUMDB=sum.golang.org 组合,强制校验所有依赖模块的 go.sum,禁用 GOSUMDB=off

验证与加固指令

# 1. 下载并校验 macOS ARM64 官方包(以 go1.23.0 为例)
curl -O https://go.dev/dl/go1.23.0.darwin-arm64.tar.gz
curl -O https://go.dev/dl/go1.23.0.darwin-arm64.tar.gz.sha256
shasum -a 256 go1.23.0.darwin-arm64.tar.gz | diff - go1.23.0.darwin-arm64.tar.gz.sha256

# 2. 安装后立即审计环境变量(推荐写入 ~/.zprofile,非 ~/.zshrc)
echo 'export GOROOT=/usr/local/go' >> ~/.zprofile
echo 'export GOPATH=$HOME/go' >> ~/.zprofile
echo 'export PATH=$GOROOT/bin:$GOPATH/bin:$PATH' >> ~/.zprofile
source ~/.zprofile

# 3. 强制启用模块校验与零代理策略
go env -w GOPROXY=direct
go env -w GOSUMDB=sum.golang.org
go env -w GO111MODULE=on

关键配置项审计表

变量 推荐值 审计意义
GOBIN 空(继承 PATH 避免意外覆盖系统二进制
GOCACHE $HOME/Library/Caches/go-build 符合 macOS 文件系统规范
GODEBUG 无默认值 生产环境严禁设置 gocacheverify=1 以外调试项

所有操作完成后,运行 go version && go env GOROOT GOPATH GOPROXY GOSUMDB 输出应完全符合上述策略,且 go test std 无校验失败或网络代理日志。

第二章:可信源获取与完整性校验体系构建

2.1 Go官方发布包的SHA256多层校验流程(含交叉验证脚本实践)

Go 官方发布包提供三层 SHA256 校验保障:

  • go<version>.src.tar.gz.sha256(源码包摘要)
  • go<version>.linux-amd64.tar.gz.sha256(二进制包摘要)
  • checksums.txt(含全部文件哈希及签名,由 golang.org/dl 签发)

校验流程逻辑

# 下载并验证 checksums.txt 的 GPG 签名(可信锚点)
curl -O https://go.dev/dl/checksums.txt
curl -O https://go.dev/dl/checksums.txt.sig
gpg --verify checksums.txt.sig checksums.txt

# 提取目标包哈希并比对
grep "go1.22.5.linux-amd64.tar.gz" checksums.txt | cut -d' ' -f1
sha256sum go1.22.5.linux-amd64.tar.gz | cut -d' ' -f1

逻辑分析:首行通过 GPG 验证 checksums.txt 完整性与来源真实性;后续 grep 提取权威哈希值,cut 截取首字段(32 字节 SHA256),避免空格/换行干扰。该流程实现「签名→摘要→文件」三级信任链。

交叉验证关键字段对照表

文件来源 哈希类型 是否带签名 用途
*.sha256 SHA256 快速单包校验
checksums.txt SHA256 是(GPG) 全量清单+防篡改锚点
graph TD
    A[下载 .tar.gz] --> B[计算本地 SHA256]
    C[获取 checksums.txt] --> D[GPG 验证签名]
    D --> E[解析目标包哈希]
    B --> F[比对 E 与本地值]
    F -->|一致| G[校验通过]

2.2 镜像源安全评估模型:golang.org vs. goproxy.cn vs. 自建私有代理的证书链拓扑分析

证书链提取与验证脚本

# 提取目标域名完整证书链(含中间CA)
openssl s_client -connect goproxy.cn:443 -showcerts -servername goproxy.cn 2>/dev/null | \
  sed -n '/BEGIN CERTIFICATE/,/END CERTIFICATE/p' > goproxy_chain.pem

该命令通过 TLS 握手获取服务端发送的全部证书(含 leaf + intermediates),-servername 启用 SNI,确保获取正确虚拟主机证书;输出经 sed 过滤后保留 PEM 格式证书块,为后续拓扑分析提供原始输入。

三方证书链关键特征对比

指标 golang.org goproxy.cn 自建私有代理(Nginx+Let’s Encrypt)
根证书信任锚 ISRG Root X1 DST Root CA X3 ISRG Root X1
中间证书层级数 2(leaf → R3 → X1) 2(leaf → X3) 2(leaf → R3 → X1)
OCSP Stapling 支持 ❌(需手动配置)

信任链拓扑差异

graph TD
  A[golang.org leaf] --> B[Let's Encrypt R3]
  B --> C[ISRG Root X1]
  D[goproxy.cn leaf] --> E[DST Root CA X3]
  E --> F[Legacy OS trust store]
  G[Private proxy leaf] --> H[Let's Encrypt R3]
  H --> C

自建代理若未启用 OCSP Stapling,客户端需主动发起 OCSP 查询,引入额外网络延迟与隐私泄露风险。

2.3 macOS平台二进制签名解析:codesign -dvvv 与 dwarfdump -uuid 的联合取证方法

在 macOS 安全审计中,签名完整性与二进制身份需交叉验证。单一工具易被绕过,而 codesign -dvvvdwarfdump -uuid 联合使用可构建可信链。

签名元数据深度提取

codesign -dvvv /Applications/TextEdit.app
# 输出含 TeamIdentifier、Authority、CDHash、CMS digest 等字段
# -d: 显示签名信息;-v: 验证并输出详细路径;-v: 再次启用冗余日志(即 -vvv)

该命令揭示签名时间戳、证书链及嵌入式资源规则(如 entitlements.plist),但不校验二进制原始内容一致性。

UUID 一致性比对

dwarfdump --uuid /Applications/TextEdit.app/Contents/MacOS/TextEdit
# 输出:UUID: 1A2B3C4D-5E6F-7890-ABCD-EF1234567890 (x86_64)

DWARF UUID 来自 Mach-O 的 LC_UUID load command,反映编译时唯一标识,不可伪造。

工具 来源层 可篡改性 关键用途
codesign -dvvv 签名段(__CODE_SIGNATURE) 需重签名才变 验证签名有效性与权限
dwarfdump -uuid Mach-O 头部(LC_UUID) 编译期固化 校验二进制未被替换

联合取证逻辑

graph TD
    A[获取 codesign 输出的 CDHash] --> B[提取 Mach-O 的 LC_UUID]
    B --> C{二者是否匹配已知构建记录?}
    C -->|是| D[签名与二进制身份一致]
    C -->|否| E[存在重打包或中间人篡改]

2.4 Go SDK压缩包内嵌文件粒度校验:tar –list + sha256sum -c 的自动化比对流水线

核心校验流程设计

采用双阶段验证:先提取归档结构,再逐文件校验哈希。避免解压全量内容,兼顾效率与精度。

自动化校验脚本

# 生成文件列表及对应sha256(含路径前缀剥离)
tar -tzf go-sdk-v1.12.0.tar.gz | \
  grep -v '/$' | \
  xargs -I{} sh -c 'echo "./{}  $(tar -xOzf go-sdk-v1.12.0.tar.gz {} | sha256sum | cut -d" " -f1)"' > manifest.sha256

# 执行标准校验
sha256sum -c manifest.sha256 --quiet

tar -tzf 列出压缩包内所有非目录项;xargs -I{} 实现每行路径的独立哈希计算;--quiet 抑制成功输出,仅报错。

校验关键参数对照表

参数 作用 安全影响
-t 列出归档内容(不提取) 防止恶意文件触发解压逻辑
--quiet 仅输出失败项 减少日志噪声,便于CI断言

流程图示意

graph TD
  A[读取tar.gz] --> B[tar -tzf 提取路径列表]
  B --> C[逐路径提取+sha256sum]
  C --> D[生成manifest.sha256]
  D --> E[sha256sum -c 验证]

2.5 校验结果审计日志生成与不可篡改存证(基于SQLite WAL模式+FSync强制落盘)

日志写入原子性保障

启用 WAL(Write-Ahead Logging)模式,避免锁表竞争:

PRAGMA journal_mode = WAL;
PRAGMA synchronous = FULL; -- 等价于 SQLITE_SYNC_FULL,强制 fsync 到磁盘
PRAGMA wal_autocheckpoint = 1000; -- 每1000页自动检查点,平衡性能与持久性

PRAGMA synchronous = FULL 确保每次 INSERT 后调用 fsync(),即使断电也不会丢失已提交事务;WAL 模式使读写并发,且日志文件 *.wal 本身即为线性、只追加的审计凭证。

不可篡改性设计要点

  • 所有校验记录含时间戳、哈希摘要(SHA-256)、操作者ID、原始数据指纹
  • 表结构强制 NOT NULL + CHECK(length(hash) == 64) 约束
字段 类型 约束 说明
id INTEGER PK AUTOINCREMENT 逻辑递增,非时间序
ts TEXT NOT NULL, ISO8601 UTC 时间戳
digest TEXT NOT NULL, CHECK(…) 原始结果哈希
log_entry TEXT NOT NULL JSON 格式完整记录

数据同步机制

def commit_with_fsync(conn):
    conn.execute("INSERT INTO audit_log (...) VALUES (...)")
    conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")  # 主动截断已刷盘 WAL
    conn.commit()  # 触发底层 fsync

wal_checkpoint(TRUNCATE) 清理已持久化 WAL 页,避免日志膨胀;commit()synchronous=FULL 下隐式执行 fsync(),确保日志物理落盘。

第三章:运行时信任锚点加固与证书链验证

3.1 macOS Keychain中Go依赖证书的信任策略配置(system vs. login keychain权限边界)

Go 工具链(如 go getgo mod download)在 TLS 握手时依赖系统根证书库,macOS 上由 Keychain Services 提供信任锚点。默认行为优先查询 login keychain(用户级),但部分自签名或企业 CA 证书需注入 system keychain 才对所有用户及后台进程生效。

权限边界差异

  • login keychain:受用户登录会话保护,可被 security add-trusted-cert -k login.keychain-db 修改;
  • system keychain:需 sudo 权限,路径为 /System/Library/Keychains/SystemRootCertificates.keychain(只读)或 /Library/Keychains/System.keychain(可写)。

信任策略配置示例

# 将自建CA证书设为“始终信任”并注入 system keychain
sudo security add-trusted-cert \
  -d -r trustRoot \          # -d: 添加到系统钥匙串;-r trustRoot: 显式设为根信任
  -k /Library/Keychains/System.keychain \
  internal-ca.crt

逻辑分析-d 确保证书加入指定钥匙串而非默认 login;-r trustRoot 覆盖 GUI 中手动设置的“使用系统默认”策略,强制信任层级;省略 -p ssl 则默认启用所有服务策略(包括 TLS)。

钥匙串类型 访问权限 Go 进程可见性 典型用途
login 用户会话内可写 ✅(当前用户 shell) 开发调试、个人代理 CA
system sudo 可写 ✅(所有用户/daemon) 组织级 TLS 中间证书
graph TD
  A[Go HTTP Client] --> B{TLS握手}
  B --> C[SecTrustEvaluate]
  C --> D[Keychain Search Order]
  D --> E[login keychain]
  D --> F[system keychain]
  D --> G[system roots]
  E -.→|无匹配则降级| F
  F -.→|无匹配则降级| G

3.2 go get阶段TLS握手深度审计:openssl s_client + Wireshark TLS解密实战

当执行 go get 时,Go 工具链会向模块代理(如 proxy.golang.org)发起 HTTPS 请求,全程依赖 TLS 1.2/1.3 握手。要审计其安全性,需捕获并解密真实握手流量。

使用 openssl s_client 主动探查

openssl s_client -connect proxy.golang.org:443 -tls1_2 -debug -msg
  • -tls1_2 强制使用 TLS 1.2,排除协议协商干扰;
  • -debug 输出原始 TLS 记录字节;
  • -msg 打印明文解析后的握手消息(ClientHello/ServerHello/Certificate等),验证 SNI、ALPN(h2)、ECDHE 参数及证书链完整性。

Wireshark 解密关键路径

环境变量 作用
SSLKEYLOGFILE Go 进程启动前设置,输出 Client Random + Master Secret

启用后,Wireshark 可通过 Edit → Preferences → Protocols → TLS → (Pre)-Master-Secret log filename 加载该文件,实现全握手解密。

TLS 握手核心流程

graph TD
    A[go get 发起请求] --> B[ClientHello: SNI=proxy.golang.org, ALPN=h2]
    B --> C[ServerHello + Certificate + CertificateVerify]
    C --> D[EncryptedExtensions + Finished]
    D --> E[HTTP/2 GET /golang.org/x/net/@v/list]

3.3 自签名CA注入与go mod verify的协同验证机制(含GOSUMDB=off场景下的替代方案)

Go 模块校验依赖双重保障:go mod verify 校验模块哈希一致性,而 GOSUMDB 提供权威签名背书。当使用自签名 CA 时,需将其注入 Go 的信任链:

# 将自签名CA证书注入系统及Go信任库
sudo cp my-ca.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates
# 同时告知Go使用该CA(仅限Go 1.21+)
export GOCERTIFICATEAUTHORITY=my-ca.crt

此命令将 CA 注入操作系统信任库,并通过 GOCERTIFICATEAUTHORITY 显式指定 Go 使用的根证书路径,确保 go mod download/verify 在 TLS 握手及 sumdb 查询中均能完成可信验证。

GOSUMDB=off 时,校验退化为本地 go.sum 文件比对,此时必须辅以以下措施:

  • ✅ 强制启用 GOINSECURE 仅限私有模块域名(如 *.internal.example.com
  • ✅ 定期执行 go mod verify && go list -m all 检测哈希漂移
  • ✅ 使用 cosigngo.sum 签名并存证
场景 校验主体 信任锚点 风险等级
默认(GOSUMDB=proxy.golang.org) sumdb 签名 + go.sum Google 签名密钥
GOSUMDB=off + 自签名CA go.sum 本地哈希 人工维护的 checksum 清单 中高
graph TD
    A[go get] --> B{GOSUMDB set?}
    B -->|Yes| C[Query sumdb via TLS → 验证签名]
    B -->|No| D[Compare against go.sum only]
    C --> E[CA 验证 TLS 证书 → 自签名CA必须可信]
    D --> F[依赖 go.sum 完整性 → 需额外完整性保护]

第四章:沙箱化执行与行为可观测性建设

4.1 使用sandbox-exec构建最小权限Go构建沙箱(profile语法详解与syscall白名单设计)

sandbox-exec 是 macOS 原生的细粒度沙箱工具,通过 .sb 配置文件定义进程可执行的系统调用与资源访问边界。

profile 语法核心结构

(version 1)
(deny default)
(allow sysctl-read)
(allow file-read* (subpath "/usr/lib"))
(allow process-exec (literal "/usr/bin/gcc"))
  • (deny default) 禁用所有未显式允许的操作;
  • file-read* 是通配宏,匹配 file-read-metadatafile-read-data 等;
  • (literal "/usr/bin/gcc") 强制路径精确匹配,防止符号链接绕过。

syscall 白名单设计原则

  • 仅放行 Go 构建链必需调用:open, read, write, mmap, brk, clone, wait4, exit
  • 显式拒绝 socket, connect, fork, ptrace 等高风险 syscall;
  • 使用 sysctl-read 仅限读取 hw.ncpu 等构建感知参数。
syscall 允许理由 风险规避点
mmap Go linker 内存映射 限制 MAP_JIT=0
clone runtime 启动 goroutine 禁用 CLONE_NEWNS
wait4 编译器子进程同步 不允许 WUNTRACED
graph TD
    A[Go 构建进程] --> B[sandbox-exec 加载 .sb profile]
    B --> C{syscall 拦截引擎}
    C -->|白名单内| D[执行]
    C -->|未授权| E[EPERM 终止]

4.2 Go编译/测试过程的系统调用捕获:dtrace + sysdig双引擎行为日志采集方案

为精准刻画 Go 工具链行为,需在进程粒度捕获 go buildgo test 全生命周期系统调用。

双引擎协同采集策略

  • dtrace(macOS/BSD):低开销跟踪用户态进程 exec, openat, mmap
  • sysdig(Linux):支持容器上下文,通过 syscall.openat, proc.exec 过滤 Go 构建进程

示例采集命令

# Linux 环境下捕获 go test 行为(仅 openat/mmap/write)
sudo sysdig -p "%evt.time,%proc.name,%evt.type,%fd.name" \
  "proc.name=go and (evt.type=openat or evt.type=mmap or evt.type=write)" \
  -c topprocs_syscall -n 10

-p 定制输出字段;proc.name=go 限定目标进程;-c topprocs_syscall 聚合高频系统调用。该命令可定位测试阶段对 *_test.gogo.mod 的实际读取路径。

采集字段语义对照表

字段 含义 Go 场景示例
%evt.time 微秒级时间戳 定位编译卡点
%fd.name 文件描述符对应路径 /usr/local/go/src/fmt/
%evt.type 系统调用类型 openat, statx, clone
graph TD
  A[go test ./...] --> B{dtrace/sysdig 触发}
  B --> C[捕获 execve → fork → mmap → write]
  C --> D[结构化日志写入 ring buffer]
  D --> E[实时解析:Go 模块加载路径/测试覆盖率文件写入]

4.3 go run/go test的进程树隔离与资源约束:launchd.plist配置与cgroups-lite模拟实践

Go 工具链默认不提供进程沙箱,但可通过操作系统层机制实现轻量级隔离。

launchd.plist 示例(macOS)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>local.go.test.isolated</string>
  <key>ProgramArguments</key>
  <array>
    <string>go</string>
    <string>test</string>
    <string>-v</string>
    <string>./...</string>
  </array>
  <key>ProcessType</key>
  <string>Interactive</string>
  <key>HardResourceLimits</key>
  <dict>
    <key>NumberOfFiles</key>
    <integer>256</integer>
    <key>NumberOfProcesses</key>
    <integer>32</integer>
  </dict>
</dict>
</plist>

该配置通过 HardResourceLimits 限制文件描述符与子进程数,由 launchd 在 fork 时注入 RLIMIT_NOFILE/RLIMIT_NPROC,实现内核级资源硬限。

cgroups-lite 模拟(Linux)

# 创建 memory-limited scope
sudo mkdir -p /sys/fs/cgroup/memory/go-test-scope
echo $$ | sudo tee /sys/fs/cgroup/memory/go-test-scope/cgroup.procs
echo "50M" | sudo tee /sys/fs/cgroup/memory/go-test-scope/memory.limit_in_bytes
go test ./... 2>/dev/null
机制 隔离维度 启动开销 跨平台支持
launchd.plist 进程树 + RLIMIT macOS only
cgroups v1 内存/CPU/PID Linux only
graph TD
  A[go run/test] --> B{OS Platform}
  B -->|macOS| C[launchd + rlimit]
  B -->|Linux| D[cgroups v1/v2]
  C --> E[Kernel-enforced caps]
  D --> E

4.4 沙箱行为日志的结构化解析与异常模式识别(基于jq + awk的实时告警管道)

沙箱日志通常为嵌套JSON流式输出,需在无状态管道中完成解析、过滤与告警触发。

核心处理链路

tail -n0 -F /var/log/sandbox/activity.log \
  | jq -r 'select(.event_type == "process_spawn" and .privilege_level == "high") | [.timestamp, .pid, .cmdline, .parent_pid] | @tsv' \
  | awk -F'\t' '$4 ~ /^[0-9]+$/ && $2 != $4 { print "ALERT: suspicious child process", $2, "spawned by", $4, "at", $1 }'

jq 提取高权限进程启动事件并转为TSV;awk 过滤父子PID不一致(规避合法继承),避免误报。-r启用原始输出,@tsv确保字段安全转义。

常见异常模式表

模式类型 触发条件 告警等级
隐蔽父进程 parent_pid == 1 或非数字 HIGH
命令行混淆 cmdline ~ /base64|xxd|iconv/ MEDIUM

实时管道拓扑

graph TD
  A[tail -F] --> B[jq filter & format]
  B --> C[awk stateless detection]
  C --> D[syslog or webhook]

第五章:结语:从合规配置到持续可信演进

在金融行业某头部券商的零信任架构升级项目中,团队最初聚焦于满足《证券期货业网络安全等级保护基本要求》(JR/T 0072—2021)中关于“身份鉴别”与“访问控制”的条款,完成了基于硬件令牌+动态口令的双因素认证配置,并通过了等保三级测评。然而上线三个月后,安全运营中心连续捕获到三起异常行为:同一高权限运维账号在非工作时段从境外IP发起SSH连接,且会话中执行了未授权的kubectl get secrets -n kube-system命令——该操作虽未违反初始策略(因策略仅校验身份与IP白名单),却暴露出静态策略对横向移动行为的失察。

这促使团队启动“可信演进”机制,将合规基线转化为动态能力引擎:

策略生命周期闭环管理

采用GitOps模式管理所有访问策略,策略变更需经CI/CD流水线自动触发三项验证:

  • 与CMDB资产标签的拓扑一致性检查(如:禁止数据库管理员访问前端CDN节点)
  • 基于历史审计日志的基线偏离度分析(调用Prometheus+Grafana API计算策略命中率波动阈值)
  • 沙箱环境中的策略冲突仿真(使用Open Policy Agent Rego脚本执行策略组合验证)

实时可信度评分驱动决策

为每个主体(用户、设备、服务)构建多维可信画像,关键维度如下表所示:

维度 数据源 更新频率 评分权重
设备健康度 EDR终端进程签名完整性 实时 30%
行为一致性 UEBA模型输出的LSTM异常分值 5分钟 40%
环境风险 Shodan API返回的公网暴露面评级 每日 20%
权限时效性 IAM系统中RBAC角色最后使用时间 每小时 10%

当某开发人员的可信度评分低于65分(阈值动态调整)时,系统自动将其访问权限降级至只读沙箱环境,并向其直属主管推送包含原始审计日志片段的工单(含时间戳、源IP ASN信息、命令哈希值)。

自适应策略编排实践

在2023年某次勒索软件攻击事件响应中,平台检测到内网多个Windows终端出现SMB协议异常连接簇(目标端口445,连接间隔

graph LR
A[EDR告警触发] --> B{可信度评分<50?}
B -- 是 --> C[自动隔离终端并冻结关联AD账户]
B -- 否 --> D[启用增强监控:抓取全流量PCAP+内存dump]
C --> E[调用SOAR剧本:同步更新防火墙微隔离策略]
D --> F[将样本送入本地沙箱集群进行行为分析]

某次生产环境部署中,CI/CD流水线因镜像层哈希不匹配被拦截,经溯源发现是开发人员误将测试环境的.env文件打包进生产镜像。平台不仅阻断发布,还自动生成修复建议:

  • 提取Dockerfile中COPY . /app指令对应路径的.gitignore规则
  • 调用Trivy扫描结果定位敏感文件类型(如*.pem.aws/credentials
  • 推送补丁PR至GitLab,附带修复前后镜像大小对比(原镜像892MB → 修复后317MB)

这种演进不是配置的叠加,而是将每一次安全事件转化为策略参数的校准刻度。当某次渗透测试中红队成功利用OAuth隐式流漏洞获取用户Token时,平台在2小时内完成三项改进:在API网关层注入PKCE强制校验、为所有前端应用添加Content-Security-Policy: frame-ancestors 'none'头、将OAuth客户端ID加入威胁情报IOC库并启用实时DNS阻断。

合规配置是起点而非终点,而持续可信演进的本质在于让系统具备从噪声中识别信号、从信号中提炼规则、从规则中生成防御的能力。

从入门到进阶,系统梳理 Go 高级特性与工程实践。

发表回复

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