第一章:Golang TLS变量逆向的典型失败场景与根本归因
在对Go二进制程序进行TLS(Transport Layer Security)相关逻辑逆向时,研究者常遭遇看似可解却实际无法还原原始配置的困境。根本原因并非反编译工具能力不足,而是Go运行时对crypto/tls包的深度优化与静态链接特性共同导致关键变量语义丢失。
TLS配置结构体的零值混淆
Go编译器会将未显式初始化的tls.Config字段(如MinVersion、CipherSuites)默认置为零值,并在运行时动态补全。逆向时若仅依赖静态分析,易将0x0误判为“未设置”,而实际上MinVersion = 0在运行时被映射为tls.VersionTLS10。验证方式如下:
# 使用objdump提取.data段中疑似tls.Config的地址范围
objdump -s -j .data ./binary | grep -A 20 "00 00 00 00 00 00 00 00"
# 结合runtime.debug.ReadBuildInfo()获取编译时TLS版本约束
go tool nm ./binary | grep -i "tls\|crypto"
静态链接导致的符号剥离
Go默认静态链接所有标准库,crypto/tls中大量函数(如(*Config).serverHelloInfo)无导出符号,且内联优化使调用链断裂。常见失败表现为:IDA Pro无法识别tls.Conn虚表结构,Hopper显示大量sub_XXXXX跳转而无函数名。
运行时反射注入的加密参数
部分程序通过reflect.Value.Set()动态写入Config.Certificates或Config.ClientCAs,这些操作在二进制中表现为runtime.convT2E+reflect.Value.Set调用序列,但证书PEM数据本身被拆解为多个常量字符串并经XOR混淆——静态扫描无法关联其最终拼接逻辑。
| 失败现象 | 根本原因 | 可验证线索 |
|---|---|---|
CipherSuites为空数组 |
编译时未指定,运行时由defaultCipherSuites()填充 |
检查crypto/tls/cipher_suites.go调用引用 |
InsecureSkipVerify=true不可见 |
字段值被内联为常量0x1,但结构体偏移被重排 |
使用dlv attach在(*Config).Clone断点观察内存布局 |
逆向者需结合动态调试(如delve)捕获tls.Config实例化后的完整内存镜像,而非依赖静态反编译结果。
第二章:IDA 7.7环境下Golang TLS结构的静态解析框架
2.1 Go运行时TLS布局理论:_tls_SlotOffsets与g结构体偏移映射原理
Go运行时通过静态TLS槽位(_tls_SlotOffsets)实现goroutine本地存储的快速访问,其本质是将g结构体字段地址编译期固化为相对于TLS基址的固定偏移。
TLS槽位与g字段的映射机制
_tls_SlotOffsets是一个由编译器生成的全局数组,每个元素对应g中一个关键字段(如goid、m、sched.pc)在TLS中的字节偏移- 运行时通过
getg()获取当前g指针,实际是读取TLS[0](即_tls_SlotOffsets[0]指向的g地址)
核心代码解析
// runtime/asm_amd64.s(简化示意)
TEXT runtime·getg(SB), NOSPLIT, $0
MOVQ TLS, AX // 加载TLS基址(IA32 GS base 或 x86-64 FS base)
MOVQ 0(AX), AX // 读取 _tls_SlotOffsets[0] → 即当前 g 地址
RET
该指令链依赖CPU TLS寄存器(FS/GS)直接寻址,避免函数调用开销。0(AX)中的即_tls_SlotOffsets[0]的编译期常量偏移值。
| 槽位索引 | 对应g字段 | 偏移(x86-64) | 用途 |
|---|---|---|---|
| 0 | g |
0 | goroutine指针 |
| 1 | m |
8 | 关联M结构体 |
| 2 | goid |
16 | 协程ID |
graph TD
A[TLS基址] --> B[_tls_SlotOffsets[0]]
B --> C[g结构体首地址]
C --> D[g.m]
C --> E[g.goid]
2.2 IDA 7.7符号加载与go:build信息提取实战:定位runtime·tls_SlotOffsets符号
Go 1.21+ 二进制中,runtime.tls_SlotOffsets 是 TLS 插槽偏移表的关键符号,但默认不导出。IDA 7.7 需结合 go:build 注释与 .note.go.buildid 段提取构建元数据。
提取 go:build 注释
# 在 IDA Python 控制台执行
for seg in [".rodata", "__rodata"]:
ea = ida_segment.get_segm_by_name(seg)
if ea:
for i in range(0, ida_segment.get_segm_size(ea), 4):
s = ida_bytes.get_strlit_contents(ea.start_ea + i, 64, ida_nalt.STRTYPE_C)
if s and b"go:build" in s:
print(f"Found build comment at {hex(ea.start_ea + i)}: {s[:32]}")
该脚本遍历只读段扫描 C 字符串,匹配 go:build 前缀;参数 64 限制最大长度防越界,STRTYPE_C 确保按 null-terminated 解析。
符号定位关键路径
- 步骤1:定位
runtime·getg函数(必含TLS访问指令) - 步骤2:向上回溯至
runtime·checkASM调用链 - 步骤3:在
.data.rel.ro中搜索 8-byte 对齐的int64数组(即tls_SlotOffsets)
| 字段 | 含义 | 典型值 |
|---|---|---|
len |
插槽数量 | 4–16 |
elem[0] |
g 结构体 TLS 偏移 | 0x0 |
elem[1] |
m 结构体 TLS 偏移 | 0x8 |
graph TD
A[加载PE/ELF] --> B[解析.note.go.buildid]
B --> C[识别Go版本≥1.21]
C --> D[启用TLS符号启发式扫描]
D --> E[定位tls_SlotOffsets]
2.3 基于IDAPython的TLS slot自动枚举脚本:解析runtime·getg()返回值偏移链
TLS slot 枚举需逆向定位 runtime.getg() 返回的 goroutine 结构体中 m 字段的偏移,该字段指向当前 M 结构体,其 tls 数组即为关键入口。
核心逻辑:从 getg() 指令流反推偏移
IDA 中定位 runtime.getg 函数,通常以 MOV RAX, [RSP+...] 或 LEA RAX, [RIP+offset] 形式返回 g 结构体指针。真实偏移需结合 Go 版本符号表或 DWARF 调试信息校准。
IDAPython 自动化步骤
- 解析
getg函数首条有效指令(跳过 NOP/INT3) - 提取内存操作数中的立即数偏移(如
[rsp+0x8]→0x8) - 验证该偏移是否指向
g.m.tls[0](典型为g + 0x10 → m → m.tls[0])
def find_getg_offset():
ea = idaapi.get_name_ea(0, "runtime.getg")
if ea == idaapi.BADADDR: return None
for i in range(5):
inst = idc.GetDisasm(ea)
if "mov" in inst.lower() and "[" in inst and "+" in inst:
# 匹配: mov rax, [rsp+0x8]
off_match = re.search(r"\+0x([0-9a-fA-F]+)\]", inst)
if off_match: return int(off_match.group(1), 16)
ea = idc.next_head(ea)
return None
该脚本提取
getg的栈偏移,作为g结构体基址起点;后续需叠加g.m.tls字段在g中的固定偏移(Go 1.20+ 为0x10),最终定位 TLS slot 0 地址。
| Go 版本 | g → m 偏移 | m.tls 偏移 | 总偏移(g→tls[0]) |
|---|---|---|---|
| 1.19 | 0x8 | 0x10 | 0x18 |
| 1.21 | 0x10 | 0x18 | 0x28 |
graph TD
A[getg returns g*] --> B[g + g_m_offset → *m]
B --> C[m + m_tls_offset → tls[0]]
C --> D[tls[0] = current M's TLS base]
2.4 IDA交叉引用追踪技巧:从syscall.Syscall到goroutine-local状态存储路径还原
syscall.Syscall入口识别
在Go二进制中,syscall.Syscall常被内联为SYSCALL指令(x86-64下为0f 05)。IDA中可通过交叉引用(Xrefs)定位其调用点:
mov rax, 0x10 ; sys_read
mov rdi, [rbp-0x8] ; fd → goroutine关联fd
mov rsi, rbx ; buf → 可能指向g.stack0或g.panic
mov rdx, 0x1000
syscall ; ← 此处设断点并回溯调用链
该指令前寄存器值常携带goroutine上下文线索:rdi可能源自runtime.guintptr,rsi若指向栈低地址,则大概率属当前g结构体。
goroutine-local状态定位路径
通过Xref向上追溯至runtime.systemstack调用,可定位g指针传递路径:
| 步骤 | IDA操作 | 关键线索 |
|---|---|---|
| 1 | Jump → Xrefs to → syscall.Syscall |
找到最近的runtime.entersyscall调用 |
| 2 | 追踪g参数传递(如mov rax, [rbp+var_10]) |
var_10通常为runtime.g指针 |
| 3 | 查看g结构体偏移(如+0x100为g._panic) |
结合go:linkname符号辅助确认 |
数据同步机制
g结构体中m字段(g.m)指向绑定线程,而g.m.curg反向指向自身——构成闭环引用。IDA中启用Structures → g视图并应用Go 1.20+布局定义,可自动解析g.panic、g._defer等字段。
graph TD
A[syscall.Syscall] --> B[runtime.entersyscall]
B --> C[save g.ptr to m.curg]
C --> D[g.m.p <--> g.m.curg]
D --> E[g.stackguard0 → stack boundary check]
2.5 TLS变量重命名与结构体重建:在IDA中同步还原g->m->curg->tls字段层级
数据同步机制
IDA Pro 中需手动对 Go 运行时 TLS 指针链 g->m->curg->tls 进行符号映射。TLS 在不同架构下偏移不同(如 amd64 为 0x80,arm64 为 0x90),需结合 runtime·getg() 反汇编结果校准。
结构体字段重建步骤
- 定位
runtime.g结构体起始地址(通常由gs寄存器或CGO调用链推导) - 根据 Go 版本(如 1.21+)确认
g.m偏移(0x130),再定位m.curg(0x80) - 最终计算
curg.tls偏移并应用Edit → Structs → Rename field
关键偏移对照表(Go 1.21, amd64)
| 字段路径 | 偏移(hex) | 类型 |
|---|---|---|
g.m |
0x130 |
*runtime.m |
m.curg |
0x80 |
*runtime.g |
curg.tls |
0x80 |
[4]*uintptr |
# IDA Python 脚本:批量重命名 tls 字段
g_struct = get_struc_id("runtime_g")
if g_struct != BADADDR:
m_off = 0x130
curg_off = m_off + 0x80 # m.curg
tls_off = curg_off + 0x80 # curg.tls
set_member_name(g_struct, tls_off, "tls") # 同步命名
该脚本通过链式偏移计算精准定位
tls字段,避免硬编码错误;set_member_name确保结构体视图与交叉引用实时同步,支撑后续tls[0](GID)、tls[1](MID)等语义分析。
第三章:Ghidra辅助验证与双引擎差异比对方法论
3.1 Ghidra中Go ELF/DWARF符号恢复:修复缺失的runtime.tls_SlotOffsets全局变量
Go 1.21+ 编译的二进制默认剥离 .debug_* 段,导致 runtime.tls_SlotOffsets(TLS 插槽偏移数组)在 Ghidra 中不可见,影响 TLS 访问语义还原。
核心定位策略
- 通过
__tls_get_addr@plt调用上下文反向追踪lea rax, [rip + offset]指令 - 匹配 Go 运行时 TLS 初始化模式:
mov qword ptr [rbp - 0x8], rax→call runtime.settls
符号重建代码示例
# 在 Ghidra Python 脚本中注入符号
addr = currentProgram.getAddressFactory().getAddress("004a7b80") # 实际偏移需动态计算
dt = getDataTypesService().getDataType("/go/tlsSlotOffsets")
createData(addr, dt)
004a7b80是典型.data.rel.ro段内tls_SlotOffsets起始地址;/go/tlsSlotOffsets需预先定义为int64[16]类型,对应 Go 的[]uintptr。
关键字段映射表
| 字段索引 | Go TLS 插槽含义 | Ghidra 命名建议 |
|---|---|---|
| 0 | g (goroutine) |
tls_g_slot |
| 1 | m (OS thread) |
tls_m_slot |
graph TD
A[识别 tls_get_addr 调用] --> B[提取 RIP-relative LEA 地址]
B --> C[验证地址位于 .data.rel.ro]
C --> D[创建 int64[16] 数据类型]
D --> E[应用符号名 runtime.tls_SlotOffsets]
3.2 IDA与Ghidra TLS slot offset输出比对:自动化diff脚本检测反编译一致性偏差
TLS slot offset 是 Windows 程序中识别线程局部存储关键位置的核心指标。IDA Pro 与 Ghidra 在解析 .tls 节或 IMAGE_TLS_DIRECTORY 时,因符号解析策略与基址推导逻辑差异,常导致同一二进制文件输出不一致的 slot 偏移(如 0x18 vs 0x20)。
数据同步机制
需统一提取两工具输出中的 TLS Slot Offset 字段(通常位于反编译注释或结构体字段名中),格式化为 <func_name>:<offset_hex> 键值对。
自动化比对脚本核心逻辑
# tls_diff.py:逐行匹配函数级TLS偏移,忽略注释与空格
import re
pattern = r"//\s*TLS\s+slot\s+offset:\s*(0x[0-9a-fA-F]+)|\.tls_slot_(\w+):\s*(0x[0-9a-fA-F]+)"
# 匹配两种常见输出格式:IDA 注释式 / Ghidra 结构体字段式
该正则同时捕获 // TLS slot offset: 0x18(IDA)与 .tls_slot_main: 0x20(Ghidra),re.IGNORECASE 确保大小写鲁棒性;0x[0-9a-fA-F]+ 保证十六进制偏移精准提取。
| 工具 | 输出示例 | 解析字段 |
|---|---|---|
| IDA | // TLS slot offset: 0x18 |
注释行提取 |
| Ghidra | tls_slot_init: 0x20 |
结构体成员名+值 |
graph TD
A[读取IDA输出] --> B[正则提取slot偏移]
C[读取Ghidra输出] --> B
B --> D[按函数名归一化键]
D --> E[diff对比并高亮差异]
3.3 双引擎联合调试:利用Ghidra PDB导入+IDA远程调试器验证goroutine切换时TLS寄存器行为
在 Go 运行时调度中,gs_base(x86-64 下对应 GS 段基址)被用作 TLS 寄存器,指向当前 goroutine 的 g 结构体。跨 goroutine 切换时,runtime.switchtoM 会动态更新 GS 值。
Ghidra 中导入 PDB 定位 TLS 符号
# Ghidra Python 脚本:从 PDB 加载 g_struct 并标记 gs_base 引用
currentProgram.getSymbolTable().getSymbols("runtime.g").next().getAddress()
# 注:需先执行 File → Import → PDB(启用 "Load symbols and types")
该脚本依赖 PDB 提供的类型信息定位 g 结构体布局;gs_base 在 runtime·save_g 中被写入 %gs:0x0,Ghidra 可据此反向标注 TLS 访问点。
IDA 远程调试捕获切换瞬间
| 断点位置 | 触发条件 | GS 值变化 |
|---|---|---|
runtime.mcall |
协程让出 CPU 前 | 指向旧 g |
runtime.gogo |
新 goroutine 恢复执行 | 更新为新 g 地址 |
TLS 切换流程可视化
graph TD
A[goroutine A 执行] -->|mcall| B[保存 A.g 到 M.g0]
B --> C[切换 GS 指向 g0]
C --> D[gogo 跳转至 goroutine B]
D --> E[加载 B.g 到 GS]
第四章:10分钟精准定位goroutine-local状态的工程化流程
4.1 快速识别目标goroutine:通过runtime·mstart→runtime·newproc1→runtime·gogo调用链锚定
在 Go 运行时调度分析中,runtime.mstart 是 M 启动的入口,它最终触发 runtime.newproc1 创建新 goroutine,并由 runtime.gogo 切换至其栈执行。
调用链关键节点语义
mstart():M 初始化后进入调度循环,调用schedule()→execute()→gogo()newproc1():分配g结构体,设置g.sched.pc指向用户函数,g.sched.sp指向栈顶gogo():汇编实现,直接跳转到g.sched.pc,完成上下文切换
核心调度跳转逻辑(x86-64)
// runtime/asm_amd64.s 中 gogo 的核心片段
MOVQ g_sched+g_sptr(g), SI // 加载目标 g 的 sched 结构地址
MOVQ 0(SI), BP // BP = sched.pc(目标函数入口)
MOVQ 8(SI), SP // SP = sched.sp(目标栈顶)
JMP BP // 跳转执行
g.sched.pc存储用户函数地址(如main.main),g.sched.sp是该 goroutine 独立栈的初始栈指针。gogo不返回,彻底移交控制权。
调度链路可视化
graph TD
A[runtime.mstart] --> B[runtime.schedule]
B --> C[runtime.execute]
C --> D[runtime.newproc1]
D --> E[runtime.gogo]
E --> F[用户函数入口]
| 函数 | 触发时机 | 关键副作用 |
|---|---|---|
mstart |
M 启动时 | 进入调度循环 |
newproc1 |
go f() 调用时 |
初始化 g.sched.{pc,sp,g} |
gogo |
协程首次/恢复执行前 | 寄存器重载 + JMP 到 g.sched.pc |
4.2 TLS slot动态绑定分析:结合IDAGraph插件可视化g.m.tls[0]至用户变量的内存投影路径
TLS slot并非静态映射,而是通过TlsSetValue/TlsGetValue在运行时与线程局部存储索引动态关联。IDAGraph插件可将g.m.tls[0](全局TLS数组首项)作为根节点,递归追踪其指向的_tls_index、_tls_array及最终用户变量(如thread_local int counter)。
数据同步机制
TLS访问需经两层间接:
g.m.tls[0]→PVOID tls_array[TLS_MAX_COUNT]tls_array[slot_id]→ 用户变量地址(如&counter)
// IDA Python脚本片段:提取TLS slot绑定关系
slot_idx = idaapi.get_qword(g_m_tls_0 + 0x0) // 获取slot索引
tls_array = idaapi.get_qword(tls_array_base + slot_idx * 8)
user_var_addr = idaapi.get_qword(tls_array) // 实际变量地址
该脚本从g.m.tls[0]出发,按Windows TLS ABI规范计算偏移,还原出变量在当前线程中的真实内存位置。
可视化路径示例
| 节点类型 | 地址 | 说明 |
|---|---|---|
g.m.tls[0] |
0x7FF…1000 | 全局TLS槽位指针数组 |
tls_array[3] |
0x7FF…2000 | 线程私有数据块基址 |
counter |
0x7FF…2018 | 用户定义thread_local变量 |
graph TD
A[g.m.tls[0]] --> B[tls_array_base]
B --> C[tls_array[slot_id]]
C --> D[user_var_address]
4.3 Go 1.21+ TLS优化适配:处理MOVQ GS:[0x0], RAX等新式TLS访问模式的IDA签名识别
Go 1.21 起默认启用 GOEXPERIMENT=fieldtrack 与更激进的 TLS 内联优化,将传统 CALL runtime.getg 替换为直接 GS 段基址偏移访问(如 MOVQ GS:[0x0], RAX),导致 IDA Pro 原有 Go 标准签名(如 getg 调用序列)大面积失效。
新旧 TLS 访问对比
| 特征 | Go ≤1.20 | Go 1.21+ |
|---|---|---|
| TLS 获取方式 | CALL runtime.getg |
MOVQ GS:[0x0], RAX |
| 指令长度 | 5 字节(near call) | 4 字节(REX.W + MODRM) |
| IDA 签名匹配 | ✅ 匹配 E8 ?? ?? ?? ?? |
❌ 原签名完全遗漏 |
IDA 签名增强示例
// 新增 TLS base load signature (x86_64)
// Pattern: MOVQ GS:[0x0], RAX → 65 48 8B 04 25 00 00 00 00
// Note: 65 = GS prefix, 48 = REX.W, 8B = MOVQ r64, m64, 04 25 = SIB with disp32
该字节序列精准捕获 Go 运行时对 G 结构体的零偏移直读,是识别 Go 1.21+ 协程上下文的关键锚点。
识别流程
graph TD
A[扫描 .text 段] --> B{匹配 MOVQ GS:[0x0], RAX?}
B -->|Yes| C[提取 RAX 目标寄存器]
B -->|No| D[回退至 getg 调用匹配]
C --> E[关联后续 G.ptr 字段访问]
4.4 实战案例复现:逆向某云原生组件中goroutine-local context.Context泄漏点
问题现象定位
通过 pprof 持续采样发现 goroutine 数量线性增长,且 runtime.ReadMemStats().NumGC 无显著变化——指向 context 生命周期失控。
关键代码片段
func startWorker(ctx context.Context, id int) {
// ❌ 错误:将传入ctx直接绑定至goroutine生命周期
go func() {
select {
case <-time.After(5 * time.Second):
process(ctx) // ctx未随goroutine退出而cancel
}
}()
}
ctx被闭包捕获但未受超时/取消约束;time.After不触发 context cancel,导致ctx及其携带的value(如 trace span、logger)长期驻留堆。
泄漏链路分析
graph TD
A[HTTP Handler] --> B[context.WithTimeout]
B --> C[startWorker]
C --> D[goroutine 持有ctx]
D --> E[ctx.Value 存储span/logger]
E --> F[GC 无法回收]
修复方案对比
| 方案 | 是否阻断泄漏 | 额外开销 | 适用场景 |
|---|---|---|---|
ctx = context.WithCancel(parent) + 显式 defer cancel |
✅ | 极低 | 确定生命周期 |
ctx, _ = context.WithTimeout(ctx, 3s) |
✅ | 中等 | IO-bound 任务 |
直接使用 context.Background() |
⚠️(丢失trace) | 无 | 无上下文依赖逻辑 |
第五章:总结与高阶逆向能力迁移建议
从Windows驱动逆向到Linux内核模块分析的路径映射
某安全团队在分析一款勒索软件的内核级加密驱动(cryptdrv.sys)后,成功将PE/COFF符号解析、IRP调度链追踪、内核对象句柄劫持等技能迁移至Linux平台。他们复用IDA Pro中自定义的ntoskrnl函数签名库,结合sigmake工具生成vmlinux符号匹配模板,将Windows下的KeWaitForSingleObject调用模式映射为Linux中wait_event_interruptible()的等待状态机识别逻辑。该迁移使内核模块malmod.ko的初始化函数定位时间从8小时压缩至47分钟。
动态插桩技术的跨架构适配策略
在ARM64移动设备上逆向某金融App的Native层混淆代码时,团队将x86_64下成熟的Intel Pin指令级插桩经验转化为Frida+Unicorn混合方案:
- 使用
frida-trace -i "sub_12345"捕获目标函数入口 - 在
onEnter回调中注入Unicorn引擎模拟关键AES密钥派生逻辑 - 将原x86汇编中的
rdtsc时间戳检测替换为clock_gettime(CLOCK_MONOTONIC, &ts)调用拦截
该组合方案在未root的Android 13设备上稳定运行,日志吞吐量达2.3万条/秒。
二进制协议逆向成果的横向复用表
| 原始场景 | 协议特征 | 迁移目标 | 复用组件 | 验证耗时 |
|---|---|---|---|---|
| 工业PLC固件 | Modbus TCP变种(0x83异常码加密) | 智能家居网关固件 | 自研modbus_decoder.py + TLS握手字段提取器 |
3.2小时 |
| 游戏客户端 | 自定义RPC序列化(TLV+ZSTD压缩) | 医疗IoT设备通信模块 | zstd_decompressor.js + TLV长度字段校验规则 |
1.7小时 |
| 车载T-Box | CAN帧ID动态映射表(EEPROM存储) | 无人机飞控固件 | EEPROM内存布局解析器 + ID冲突检测算法 | 6.5小时 |
混淆控制流图的标准化重构流程
针对OLLVM 5.0的flattening混淆,团队建立统一重构流水线:
- 使用
angr提取所有switch跳转表基地址 - 通过
radare2的aaa命令生成基础块关系图 - 执行
cfg_accurate插件识别phi节点与支配边界 - 导出
.dot文件后用以下mermaid脚本生成可交互拓扑:
flowchart TD
A[Entry Block] --> B{Flattening Dispatcher}
B --> C[Case Handler 0x1A]
B --> D[Case Handler 0x2F]
C --> E[Decrypted Payload]
D --> E
E --> F[Exit Stub]
逆向知识资产的版本化管理实践
所有自研脚本、IDB数据库、YARA规则均纳入Git LFS管理,采用语义化版本控制:
reversing_tools/v2.4.1/ida_scripts/pe_loader.py支持Windows 11 22H2新加载器行为yara_rules/iot_firmware/2024Q3/esp32_crypto.yar新增对ESP-IDF v5.1.2的AES-NI指令特征匹配ghidra_scripts/struct_recover.py通过getDataTypeManager().findDataTypes("struct.*")自动同步结构体定义
某次对路由器固件的紧急响应中,直接检出v2.3.0分支的mips_gadget_finder.py脚本,在37分钟内定位到CVE-2023-27209利用链中的addiu $sp,$sp,-0x1c栈溢出gadget。
