第一章:Go语言反编译攻防概述
Go语言因其静态链接、二进制自包含及默认关闭调试信息等特性,常被误认为“天然抗反编译”。然而,其编译产物(ELF/PE/Mach-O)仍携带丰富的符号表、类型元数据(如runtime._type结构)、函数名、字符串字面量及调用关系线索,为逆向分析提供了坚实基础。攻击者可利用这些信息还原控制流、识别关键逻辑(如License校验、加密密钥提取),而防御方则需理解哪些信息可剥离、哪些行为易暴露——攻防本质是信息可见性与运行时行为隐蔽性的持续博弈。
Go二进制的典型信息残留
- 函数符号:
main.main、crypto/aes.(*Cipher).Encrypt等未裁剪时完整保留; - 字符串常量:硬编码密钥、API URL、错误提示均以明文形式存在于
.rodata段; - 类型反射信息:通过
go tool objdump -s "runtime\.newobject"可定位类型描述符,进而推导结构体字段布局; - Goroutine调度痕迹:
runtime.g0、runtime.m0等全局变量地址泄露运行时栈基址。
基础反编译流程示例
使用go tool objdump提取函数汇编并定位关键逻辑:
# 1. 导出所有符号(含隐藏符号)
go tool nm -n ./target_binary | grep " T " | head -10
# 2. 反汇编main.main函数(-s指定符号名,-S输出带源码行号的汇编)
go tool objdump -s "main\.main" ./target_binary
# 3. 搜索敏感字符串(如JWT签名算法标识)
strings ./target_binary | grep -i "HS256\|RSA\|AES"
上述命令直接作用于Go原生二进制,无需第三方工具链,结果可快速映射到Go源码语义层。
关键防御维度对比
| 防御措施 | 是否影响调试 | 是否破坏符号表 | 对性能影响 | 实际效果 |
|---|---|---|---|---|
go build -ldflags="-s -w" |
是 | 是 | 无 | 移除符号表和调试信息,但字符串/类型信息仍存 |
| UPX压缩 | 否 | 否 | 极低 | 仅增加分析门槛,无法阻止静态解包 |
| 控制流扁平化 | 否 | 否 | 中高 | 扰乱CFG,但需专用Go插件(如goflow)支持 |
Go反编译并非黑箱操作,而是依赖对运行时机制与二进制格式的深度理解。攻防双方的技术焦点始终围绕“信息熵的增减”展开:一方竭力降低二进制的信息密度,另一方则持续挖掘隐式语义线索。
第二章:Go二进制结构深度解析与静态逆向实战
2.1 Go运行时符号表(pclntab)的定位与解码原理
Go二进制中,pclntab(Program Counter Line Table)是运行时实现栈回溯、panic打印、反射调试的核心元数据区,位于.gopclntab段。
符号表物理定位
Go 1.16+ 使用 runtime.pclntable 全局变量指向该表起始地址;可通过 runtime.findfunc(pc) 定位函数元信息。
解码关键结构
// pclntab头部格式(简化)
type pclnHeader struct {
magic uint32 // 0xfffffffa(小端)
offPcsp uint32 // pc→spdelta偏移表
offPcfile uint32 // pc→源文件名偏移表
offPcline uint32 // pc→行号偏移表
nfiles uint32 // 源文件数
}
magic 字段用于校验有效性;offPcfile 等字段提供各子表相对于pclntab基址的偏移量,支持随机访问。
查找流程
graph TD
A[输入PC地址] --> B[二分查找 func tab]
B --> C[获取 funcInfo 结构]
C --> D[查 pcfile 表得文件索引]
D --> E[查 pcline 表得行号]
| 字段 | 含义 | 解码方式 |
|---|---|---|
pcsp |
栈帧大小映射 | 基于PC的单调递增序列 |
pcfile |
源文件名字符串索引 | 查filetab数组 |
pcline |
行号映射 | 差分编码,节省空间 |
2.2 Go函数元信息(funcdata、stackmap)提取与调用图重建
Go 运行时依赖 funcdata 和 stackmap 实现栈帧遍历、垃圾回收与 panic 恢复。这些元信息嵌入在可执行文件的 .text 段中,由编译器在 SSA 后端生成。
funcdata 结构解析
每个函数符号关联多个 funcdata 条目(如 FUNCDATA_InlTree, FUNCDATA_ArgsSize),通过 runtime.funcInfo 可定位:
// 示例:从 PC 获取 funcInfo(需 runtime 包权限)
f := findfunc(pc)
if f.valid() {
argsSize := *(*int32)(unsafe.Pointer(f.args))
// argsSize: 函数参数总字节数(含隐式 receiver)
}
findfunc(pc)通过二分查找.pclntab表;f.args指向FUNCDATA_ArgsSize数据区,类型为int32。
stackmap 作用
描述栈上每个 slot 是否为指针,用于 GC 扫描。格式为位图 + 偏移数组。
| 字段 | 类型 | 说明 |
|---|---|---|
nptr |
uint32 | 栈中指针数量 |
nbit |
uint32 | 位图字节数 |
bitvector |
[]byte | 每 bit 表示 1 个 uintptr |
调用图重建流程
graph TD
A[读取 .pclntab] --> B[解析 funcInfos]
B --> C[提取所有 funcdata/stackmap]
C --> D[构建函数节点与 call 指令边]
D --> E[拓扑排序生成调用图]
2.3 Go字符串与反射类型信息的内存布局还原与字符串常量提取
Go 运行时将字符串常量固化在只读数据段(.rodata),其底层由 string 结构体(struct{ptr *byte, len int})描述,而反射类型信息(reflect.Type)则通过 runtime._type 结构体关联符号表。
字符串结构体内存视图
// string 在 runtime 中的真实定义(简化)
type stringStruct struct {
str *byte // 指向 .rodata 中的字节序列
len int // 长度(不包含终止符)
}
str 字段直接映射到 ELF 的只读段偏移,len 由编译器静态计算并写入二进制;二者共同构成零拷贝可读视图。
反射类型与字符串常量的绑定关系
| 字段 | 类型 | 说明 |
|---|---|---|
commonType.name |
*string |
指向类型名字符串的指针 |
commonType.pkgPath |
*string |
指向包路径字符串的指针 |
提取流程(mermaid)
graph TD
A[解析 ELF .rodata 段] --> B[定位 runtime._type 符号]
B --> C[读取 nameOff/pkgPathOff 偏移]
C --> D[从 .rodata + offset 提取 UTF-8 字节流]
D --> E[构造 Go string 值]
2.4 Go接口与方法集在汇编层的实现特征识别与虚函数表重构
Go 接口在运行时无传统 C++ 虚函数表(vtable),而是通过 iface 和 eface 两种结构体承载方法集信息。
接口底层结构示意
// runtime/runtime2.go(简化)
type iface struct {
tab *itab // 接口表指针
data unsafe.Pointer // 实例数据指针
}
type itab struct {
inter *interfacetype // 接口类型描述
_type *_type // 动态类型
link *itab
hash uint32
fun [1]uintptr // 方法地址数组(变长)
}
fun 字段是方法地址连续存储区,索引即方法集顺序;inter 与 _type 共同哈希定位唯一 itab,避免重复生成。
方法调用的汇编特征
CALL AX指令后紧跟MOV AX, [RAX+0x8]类型加载,表明间接跳转;itab.fun[0]对应接口首个方法,偏移由GOARCH决定(如 amd64 为 8 字节对齐)。
| 字段 | 作用 | 汇编可见性 |
|---|---|---|
tab |
指向方法查找入口 | LEA RAX, [RDX+0x0] |
fun[0] |
第一个方法实际地址 | CALL QWORD PTR [RAX] |
hash |
itab 缓存键 | CMP EAX, DWORD PTR [RAX+0x10] |
graph TD
A[接口变量赋值] --> B[运行时查表:inter+_type→itab]
B --> C{itab已缓存?}
C -->|是| D[直接取fun[n]地址]
C -->|否| E[动态生成itab并注册到hash表]
D --> F[CALL fun[n]]
2.5 Go Goroutine调度器痕迹分析与协程上下文恢复实践
Go 运行时通过 g(Goroutine 结构体)、m(OS 线程)、p(Processor)三元组协同调度。g 中的 sched 字段保存了寄存器现场(如 sp, pc, lr),是上下文恢复的关键。
Goroutine 调度痕迹捕获
可通过 runtime.ReadMemStats 与 debug.ReadGCStats 辅助观测,但更直接的是利用 GODEBUG=schedtrace=1000 启动时输出调度器快照。
上下文恢复核心代码
// 从 g.sched 恢复寄存器现场(简化示意,实际由汇编实现)
func gogo(buf *gobuf) {
// sp ← buf.sp, pc ← buf.pc, 保存当前 g 到 g0,切换至目标 g
// 注:buf.pc 指向 goroutine 的函数入口或挂起点(如 runtime.goexit + offset)
// buf.sp 是该 goroutine 栈顶指针,确保栈帧连续
}
此函数由 asm_amd64.s 中的 gogo 汇编例程执行,不返回——它直接跳转至目标 pc,完成协程上下文切换。
关键字段对照表
| 字段 | 类型 | 作用 |
|---|---|---|
sched.sp |
uintptr | 切换前栈顶地址 |
sched.pc |
uintptr | 下一条待执行指令地址 |
sched.g |
*g | 关联的 Goroutine 实例 |
graph TD
A[goroutine 阻塞] --> B[save g.sched: sp/pc]
B --> C[转入 runq 或 waitq]
D[scheduler 选择 g] --> E[load g.sched]
E --> F[gogo: jmp to pc with sp]
第三章:动态调试与运行时行为捕获技术
3.1 Delve深度定制调试:断点注入、寄存器状态快照与栈帧回溯
Delve 不仅支持基础断点,更可通过 dlv CLI 或 rpc2 接口实现运行时动态断点注入:
# 在函数入口动态注入条件断点(仅当 err != nil 时触发)
dlv attach 12345 --headless --api-version=2 \
-c 'break main.processRequest if err != nil'
该命令向 PID 12345 的进程注入条件断点;
--api-version=2启用 RPCv2 协议以支持寄存器快照;if子句由 Delve 在目标进程上下文中求值。
寄存器快照捕获
执行 registers -a 可导出完整 CPU 寄存器状态,含 RSP/RBP/PC 等关键字段,用于分析栈对齐异常。
栈帧回溯增强
Delve 默认 stack 命令仅显示符号化帧;启用 --full 参数可还原未优化的内联调用链与 SP 偏移:
| 字段 | 说明 |
|---|---|
Frame PC |
当前帧指令指针地址 |
SP Offset |
相对于当前栈顶的偏移量 |
Call Site |
调用方源码位置(含行号) |
graph TD
A[触发断点] --> B[暂停 goroutine]
B --> C[快照寄存器 & 内存页]
C --> D[解析 DWARF 信息]
D --> E[重构完整调用栈]
3.2 Go runtime hook技术:拦截gc、malloc、goroutine创建等关键路径
Go runtime 提供了有限但强大的内部钩子机制,允许在关键路径(如 mallocgc、gcStart、newproc1)插入自定义逻辑,常用于可观测性、内存审计与调度干预。
核心可挂钩点概览
runtime.SetFinalizer配合对象生命周期监听(间接 hook)runtime.ReadMemStats+ 定期轮询触发 GC 前后行为- 修改
runtime/proc.go中goexit0或newproc1(需修改源码并重编译)
关键路径 hook 示例(需 patch runtime)
// 在 src/runtime/proc.go 的 newproc1 开头插入:
func newproc1(fn *funcval, argp unsafe.Pointer, narg int32, callergp *g, callerpc uintptr) {
// 自定义 hook:记录 goroutine 创建上下文
if hookGoroutineCreate != nil {
hookGoroutineCreate(fn, callerpc)
}
// ... 原有逻辑
}
此 patch 需重新构建 Go 工具链;
hookGoroutineCreate可捕获函数指针与调用栈地址,用于追踪协程来源。参数fn指向闭包函数元数据,callerpc是调用方指令地址,可用于符号化解析。
运行时 hook 能力对比表
| Hook 点 | 是否需 patch | 可观测粒度 | 稳定性 |
|---|---|---|---|
GC 启动 (gcStart) |
是 | 全局 GC 周期 | 低(版本敏感) |
mallocgc |
是 | 单次堆分配 | 极低 |
runtime.GC() 回调 |
否 | 手动触发时机 | 高 |
graph TD
A[应用启动] --> B[注册 runtime hook 函数指针]
B --> C{是否 patch runtime?}
C -->|是| D[重编译 Go 工具链]
C -->|否| E[利用 SetFinalizer / MemStats 轮询模拟]
D --> F[拦截 mallocgc / newproc1 / gcStart]
E --> G[弱实时性,但无需修改源码]
3.3 基于eBPF的用户态Go程序行为观测与敏感API调用审计
Go程序因goroutine调度、内联优化及符号剥离,传统ptrace或LD_PRELOAD难以稳定捕获os.Open、net.Dial、crypto/tls.(*Conn).Handshake等敏感API调用。eBPF提供无侵入、高性能的用户态函数跟踪能力。
核心技术路径
- 利用
uprobe/uretprobe在Go运行时符号(如runtime.syscall、syscall.Syscall)或Go标准库导出符号(需-gcflags="-l"保留符号)处插桩 - 通过
bpf_get_current_comm()与bpf_get_current_pid_tgid()关联进程上下文 - 使用
bpf_perf_event_output()将调用栈、参数、时间戳推送至用户态ring buffer
Go特化适配挑战
- Go 1.20+ 默认启用
-buildmode=pie,需解析/proc/[pid]/maps动态定位.text基址 runtime.cgoCall绕过常规调用链,需额外挂载cgo_callers符号
// uprobe_go_open.c:监控 os.Open 的 syscall.Syscall 入口
SEC("uprobe/syscall_open")
int trace_syscall_open(struct pt_regs *ctx) {
u64 pid = bpf_get_current_pid_tgid();
char comm[16];
bpf_get_current_comm(&comm, sizeof(comm));
if (bpf_strncmp(comm, sizeof(comm), "myapp") != 0) return 0;
// 获取第1个参数:文件路径指针(需user-space解引用)
void *path_ptr = (void *)PT_REGS_PARM1(ctx);
bpf_printk("PID %d: open(%p)", pid >> 32, path_ptr);
return 0;
}
逻辑分析:该eBPF程序在
syscall.Syscall入口触发,通过PT_REGS_PARM1提取open系统调用首参(路径地址)。因Go字符串内存布局特殊,实际路径内容需在用户态借助process_vm_readv从目标进程读取;bpf_printk仅作调试,生产环境应改用perf event输出。
| 观测维度 | eBPF方案优势 | 传统方案局限 |
|---|---|---|
| 函数覆盖率 | 可覆盖内联/CGO/运行时调度路径 | LD_PRELOAD无法劫持内联调用 |
| 性能开销 | ptrace导致百倍延迟下降 | |
| Go版本兼容性 | 依赖符号存在性,需适配1.18+ ABI变化 | 静态链接二进制完全失效 |
graph TD
A[Go程序启动] --> B{eBPF加载}
B --> C[uprobe挂载 runtime.syscall]
B --> D[uretprobe挂载 net.Dial]
C --> E[捕获open/read/write参数]
D --> F[提取dstAddr+TLS标志]
E & F --> G[用户态聚合:进程/线程/调用栈/时间]
第四章:混淆对抗与主动防御工程实践
4.1 Go符号剥离、字符串加密与控制流扁平化的逆向绕过策略
Go二进制常通过-ldflags="-s -w"剥离符号,配合自定义字符串解密函数与控制流扁平化(CFG Flattening)显著提升逆向门槛。
字符串动态还原示例
// 解密函数(XOR+偏移)
func decrypt(s string, key byte) string {
b := []byte(s)
for i := range b {
b[i] = b[i]^key + byte(i%7)
}
return string(b)
}
该函数以单字节key为种子,逐字节异或后叠加循环偏移。逆向时可在runtime.makeslice调用后下断点,捕获解密完成的明文字符串。
绕过控制流扁平化关键路径
- 定位
switch { case x: ... }主导的调度器块 - 提取
case分支对应的phi节点跳转表 - 使用
ghidra脚本批量重写goto跳转逻辑
| 方法 | 适用场景 | 工具支持 |
|---|---|---|
| 符号恢复 | go:build未禁用debug |
delve+goread |
| 字符串dump | 运行时内存扫描 | GDB+gef |
| CFG反扁平化 | 静态分析重构 | Ghidra+de4go |
graph TD
A[加载剥离二进制] --> B[内存中定位decrypt调用]
B --> C[Hook解密入口获取明文]
C --> D[重建函数调用图]
4.2 利用go:linkname与内联汇编构造反调试/反dump检测模块
核心原理
go:linkname 指令可绕过 Go 符号封装,直接绑定运行时私有函数;结合 //go:nosplit 与内联 x86-64 汇编,实现无栈依赖的底层检测。
关键检测手段
ptrace(PTRACE_TRACEME)系统调用失败 → 进程已被调试__libc_start_main地址异常偏移 → ELF 被内存 dump 修改rdtsc时间戳突变 → 动态插桩断点触发
示例:检测 ptrace 调试器
//go:linkname ptrace syscall.ptrace
func ptrace(request, pid, addr, data uintptr) (err error)
//go:nosplit
func isBeingDebugged() bool {
// 内联汇编触发 ptrace(PTRACE_TRACEME)
asm volatile (
"movq $100, %%rax\n\t" // SYS_ptrace
"movq $0, %%rdi\n\t" // PTRACE_TRACEME
"movq $0, %%rsi\n\t" // pid = 0 (self)
"movq $0, %%rdx\n\t" // addr = 0
"movq $0, %%r10\n\t" // data = 0
"syscall\n\t"
"testq %%rax, %%rax\n\t"
"jns 1f\n\t" // 若rax ≥ 0,未被调试
"movq $1, %0\n\t" // 否则置标志
"1:\n\t"
: "=r"(ret)
:
: "rax", "rdi", "rsi", "rdx", "r10", "r11", "rcx"
)
return ret != 0
}
逻辑分析:该汇编块直接调用
SYS_ptrace,若当前进程已被ptrace附加,则ptrace(PTRACE_TRACEME)必然失败(返回-EPERM),rax为负值,据此判定调试状态。寄存器列表明确清除污染,确保无 GC 干扰。
检测能力对比表
| 方法 | 实时性 | 抗 dump | 依赖 libc |
|---|---|---|---|
ptrace 自检 |
⭐⭐⭐⭐ | ⭐⭐ | 否 |
/proc/self/status |
⭐⭐ | ⭐⭐⭐⭐ | 是 |
rdtsc 时序差 |
⭐⭐⭐ | ⭐ | 否 |
4.3 Go module签名验证与二进制完整性校验机制落地实现
Go 1.21+ 原生支持 go mod verify 与 GOSUMDB 协同的双层校验:模块源码哈希一致性 + 签名链可信锚定。
核心验证流程
# 启用严格签名验证(禁用无签名回退)
export GOSUMDB=sum.golang.org+local:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
go mod download rsc.io/quote@v1.5.2
此命令触发三阶段校验:① 查询 sum.golang.org 获取该版本的
*.zipSHA256 及其由 Google 签发的sig;② 本地解压后计算实际 ZIP 哈希;③ 使用公钥验证签名有效性。任一失败即中止构建。
验证策略对比
| 策略 | 是否校验签名 | 是否校验哈希 | 适用场景 |
|---|---|---|---|
sum.golang.org |
✅ | ✅ | 生产环境默认 |
off |
❌ | ❌ | 离线调试 |
sum.golang.org+insecure |
✅ | ✅ | 允许自签名证书 |
自定义签名服务集成
// 在 go.sum 中嵌入自签名模块记录示例
rsc.io/quote v1.5.2 h1:5bQzY...= // go:sumdb sig:sha256:... key:mycorp-key-v1
该行表明:模块哈希经 mycorp-key-v1 私钥签名,客户端需预置对应公钥至 $GOCACHE/sumdb/public/ 才能完成链式信任验证。
4.4 基于LLVM IR的Go编译期插桩与运行时指纹动态混淆方案
Go 默认不暴露中间表示层,但通过 gcflags="-l -m" 配合 llgo 或自定义 go tool compile 后端可导出 LLVM IR。插桩点集中于函数入口/出口、接口调用及 runtime·stack 调用处。
插桩触发机制
- 检测
//go:instrument注解函数 - 自动注入
@obf_fingerprint内联汇编桩点 - 运行时通过
mmap分配只执行页加载混淆密钥表
混淆密钥调度流程
graph TD
A[编译期:LLVM Pass] --> B[识别call @runtime·getcallerpc]
B --> C[插入%fp_seed = call @gen_seed_i64]
C --> D[重写call指令为call @obf_call_wrapper]
运行时指纹生成(伪代码)
// 在 _cgo_init 后注册指纹钩子
func init() {
registerFpHook(func() uint64 {
var r rtm.Registers
getRegisters(&r) // 获取RIP/RSP等寄存器快照
return mixHash(r.RIP, r.RSP, nanotime()) // 动态熵源
})
}
该函数在每次 obf_call_wrapper 入口被调用,其返回值经 AES-ECB 加密后作为本次调用链的唯一指纹密钥,用于异或混淆后续栈帧地址。
| 阶段 | 输入 | 输出 |
|---|---|---|
| 编译期插桩 | Go AST + LLVM IR | 带 %fp_seed 的 bitcode |
| 运行时混淆 | nanotime()+寄存器 |
每次调用唯一密钥流 |
第五章:反编译攻防的伦理边界与工业级红线守则
开源组件的“合法逆向”实践边界
某金融级SDK(v2.3.1)在未声明许可证类型的情况下被嵌入多个第三方App。安全团队通过JADX反编译确认其含Apache-2.0兼容的加密工具类,但发现其LicenseValidator.class中硬编码了非标准签名密钥校验逻辑。依据《GPLv3第6条》及中国《计算机软件保护条例》第十七条,仅对公开接口行为进行动态分析与协议逆向属合理使用;但提取并复用该密钥校验算法至自有产品,则触发著作权侵权风险。实际处置中,团队采用Frida Hook替代静态提取,全程保留调用栈日志与操作录屏,形成可审计的技术验证链。
企业级反编译沙箱的合规配置清单
| 配置项 | 工业级强制值 | 违规示例 | 审计依据 |
|---|---|---|---|
| 内存扫描深度 | ≤3层对象引用链 | 全堆转储+序列化反序列化遍历 | ISO/IEC 27001 A.8.2.3 |
| 符号表处理 | 自动剥离调试符号 & 方法名混淆映射表 | 保留原始包名/类名/行号信息 | 《GB/T 35273-2020》第6.3条 |
| 输出产物 | 仅生成AST抽象语法树PDF | 导出完整Java源码文件 | PCI DSS v4.0 Requirement 6.5.7 |
红线触发的实时熔断机制
某车联网OTA升级包经Apktool解包后,发现com.xxxx.firmware.core模块存在JNI层调用/dev/block/mmcblk0p15的裸设备读取指令。此时企业自研的反编译平台立即触发三级熔断:
- 自动终止dex2jar进程
- 清空内存页中所有
/dev/block/*路径字符串 - 向SOC平台推送
SEC-REDLINE-007事件(含SHA256+时间戳+操作员工号)
该机制已在2023年某车企渗透测试中拦截3起越权固件解析行为,全部符合UNECE R155法规附件5第4.2款对“车载系统完整性验证”的强制要求。
flowchart LR
A[反编译请求] --> B{是否含硬件访问API?}
B -->|是| C[启动熔断引擎]
B -->|否| D[执行AST转换]
C --> E[清除敏感内存]
C --> F[生成审计事件]
C --> G[锁定操作会话]
D --> H[输出语法树PDF]
法务协同的漏洞披露流程
当反编译发现某医疗IoT设备固件中存在CVE-2022-36027变种(OpenSSL ASN.1解析绕过),团队未直接公开POC。而是:
① 使用objdump -d libcrypto.so | grep -A5 "call.*0x1000"定位汇编级触发点
② 将反编译所得函数签名、寄存器状态快照、崩溃core dump上传至企业区块链存证平台(哈希上链)
③ 通过CNVD绿色通道提交,附带openssl version -a与readelf -d firmware.bin原始输出
该流程使厂商在72小时内发布补丁,规避了《医疗器械网络安全注册审查指导原则》中“未授权披露导致临床风险”的监管追责。
混淆强度与合规性的量化关系
ProGuard配置中-obfuscationdictionary若使用常见英文单词表(如/usr/share/dict/words),其熵值低于3.2 bits/char,将导致反编译后代码可读性提升47%(基于2023年BlackHat Arsenal工具集实测)。工业场景必须采用密码学安全随机生成的混淆词典(openssl rand -hex 1024 | fold -w8 | sort -u > dict.txt),且需在SARIF报告中标注混淆熵值——此项已纳入华为鸿蒙生态兼容性认证V5.1的强制检测项。
