第一章:Go二进制逆向分析导论
Go语言编译生成的二进制文件具有独特性:静态链接、自带运行时、符号表丰富但结构特殊,且默认不剥离调试信息(如-ldflags="-s -w"未显式启用时)。这使得Go二进制既比C程序更“自包含”,又在反汇编与函数识别上面临新挑战——例如goroutine调度器、defer链、panic处理路径均深度内嵌于标准运行时中。
Go二进制的关键特征
- 无外部libc依赖:默认静态链接,
ldd ./binary输出通常为not a dynamic executable; - 内建符号表:
.gopclntab段存储函数元数据(入口地址、行号映射、参数大小),可被go tool objdump或delve解析; - 字符串常量集中管理:大量字符串通过
runtime.rodata段统一维护,常成为定位关键逻辑的锚点; - 函数名保留完整包路径:如
main.main、net/http.(*Server).Serve,极大提升语义可读性。
快速识别Go二进制的方法
执行以下命令验证目标是否为Go构建:
# 检查ELF interpreter(Go使用musl或glibc,但关键在段名)
readelf -S ./target | grep -E '\.gosymtab|\.gopclntab|\.go.buildinfo'
# 提取Go版本与构建信息(若未strip)
strings ./target | grep -E 'go1\.[0-9]{1,2}|buildid|GOROOT'
常用分析工具链对比
| 工具 | 优势 | 局限 |
|---|---|---|
go tool objdump |
原生支持Go符号解码,显示源码行号 | 仅限已安装Go环境的目标 |
Ghidra + GoLoader |
自动恢复函数签名与类型信息 | 需手动加载go-loader.py插件 |
Radare2 |
支持.gopclntab解析(aaa; afg @@=) |
函数图谱需二次标注 |
初学者建议从strings ./binary | grep "func"入手,结合nm -C ./binary | grep "T main\|T runtime\."快速定位主干函数,再以go tool objdump -s main.main ./binary逐指令跟踪入口逻辑。
第二章:goroutine调度器与栈布局深度解析
2.1 G结构体内存布局与关键字段定位(理论+IDA插件自动标注实践)
G结构体是Go运行时调度的核心数据结构,其内存布局直接影响goroutine的创建、切换与GC行为。在runtime2.go中定义的g结构体,首字段stack为stack类型(含lo/hi),紧随其后的是_goid(goroutine ID)、m(绑定的M指针)及sched(保存寄存器上下文的gobuf)。
关键字段偏移推导
g.sched.pc:位于g + 0x90(amd64,Go 1.22)g.m:位于g + 0x158g.status:单字节字段,位于g + 0x160
IDA插件自动标注示例(Python API)
# 自动识别G结构体并标注sched.pc字段
g_struct = ida_struct.get_struc_id("g")
pc_field = ida_struct.get_member_by_name(g_struct, "sched.pc")
ida_struct.set_member_cmt(pc_field, "Saved RIP on goroutine switch", True)
该脚本利用IDA结构体解析能力,在反编译视图中为
g.sched.pc添加注释,避免手动追踪gogo/gopreempt_m等汇编跳转点。字段偏移需结合runtime.g源码与go tool compile -S生成的汇编交叉验证。
| 字段名 | 偏移(x86_64) | 类型 | 作用 |
|---|---|---|---|
stack.lo |
0x0 | uintptr | 栈底地址 |
sched.pc |
0x90 | uintptr | 下次恢复执行的指令地址 |
m |
0x158 | *m | 绑定的OS线程 |
graph TD
A[goroutine创建] --> B[g结构体分配]
B --> C[初始化sched.pc=fn.addr]
C --> D[首次调度:gogo跳转]
D --> E[抢占时:保存PC到g.sched.pc]
2.2 M与P结构体关联机制及跨线程栈切换痕迹识别(理论+GDB动态追踪实践)
M(Machine)代表OS线程,P(Processor)代表调度器上下文;二者通过 m->p 和 p->m 双向指针绑定,构成Goroutine调度的核心枢纽。
数据同步机制
Golang运行时通过原子操作维护 m->p 的绑定状态:
// runtime/proc.go 伪代码示意
if atomic.LoadPtr(&mp->p) == nil &&
atomic.CompareAndSwapPtr(&mp->p, nil, pp) {
atomic.Storeuintptr(&pp->m, uintptr(unsafe.Pointer(mp)));
}
mp->p 为 *p 类型指针,pp->m 存储当前绑定的M地址;该操作确保M-P绑定的排他性与可见性。
GDB动态识别栈切换痕迹
在 schedule() 函数断点处,执行:
(gdb) p/x $rsp # 查看当前栈顶
(gdb) p/x ((m*)$rdi)->g0->stack.hi # 检查g0栈边界
(gdb) p ((m*)$rdi)->p->status # 验证P状态是否 _Pidle/_Prunning
| 字段 | 类型 | 含义 |
|---|---|---|
m->p |
*p |
当前绑定的P结构体地址 |
p->m |
*m |
绑定的M结构体地址 |
p->status |
uint32 |
P状态(_Pidle/_Prunning等) |
graph TD
A[M线程进入syscall] –> B{m->p != nil?}
B –>|是| C[保存m->p->m = nil; m->oldp = p]
B –>|否| D[直接休眠]
C –> E[P被其他M抢占或复用]
2.3 goroutine栈帧结构与SP/PC寄存器映射关系还原(理论+objdump反汇编交叉验证实践)
Go 运行时为每个 goroutine 分配独立栈(初始 2KB,按需增长),其栈帧遵循类 ABI 规范但非标准 C ABI。关键在于:SP(栈顶指针)指向当前帧最低地址(向下增长),而 PC 并不直接存储于栈中,而是由 gobuf.pc 字段保存在 G 结构体中。
栈帧布局示意(x86-64)
| 偏移 | 内容 | 说明 |
|---|---|---|
| +0 | 返回地址 | 调用者 call 指令下一条指令地址 |
| +8 | 保存的 BP | 若启用帧指针(-gcflags="-l"禁用) |
| +16 | 局部变量/参数 | 按编译器分配顺序压栈 |
objdump 验证片段
0000000000456789 <main.add>:
456789: 48 83 ec 18 sub $0x18,%rsp # SP -= 24 → 新栈帧起始
45678d: 48 89 7c 24 08 mov %rdi,0x8(%rsp) # 参数 a 入栈偏移 +8
456792: 48 89 74 24 10 mov %rsi,0x10(%rsp) # 参数 b 入栈偏移 +16
▶ 逻辑分析:sub $0x18,%rsp 直接修改 SP,确立当前 goroutine 栈帧边界;%rdi/%rsi 是调用约定传入的参数,被显式存入相对于 SP 的固定偏移——这印证了 Go 编译器通过 SP 基址+偏移访问栈内数据,而非依赖 BP。
SP/PC 映射还原要点
SP值来自g.sched.sp,在 goroutine 切换时由调度器保存/恢复;PC值来自g.sched.pc,对应下一条待执行指令地址,与SP构成完整执行上下文快照;runtime.gentraceback通过遍历g.stack区域,结合pcsp表(由 linker 生成)反查每条 PC 对应的 SP 偏移,实现栈回溯。
2.4 defer/panic触发时的栈展开路径与g0栈回溯技巧(理论+核心转储内存扫描实践)
当 panic 触发时,Go 运行时从当前 goroutine 的栈帧开始逐层调用 defer 链,并同步展开栈(stack unwinding);若主 goroutine panic 未被 recover,最终会切换至 g0 执行致命终止逻辑。
栈展开关键路径
runtime.gopanic→runtime.deferproc(注册)→runtime.deferreturn(执行)→runtime.fatalpanicg0此时承载系统级调用栈,其g0.sched.sp指向最后保存的用户栈指针
g0 栈回溯核心技巧
# 从 core dump 提取 g0 栈顶地址(假设已知 runtime.g0 地址)
(gdb) p/x *(struct g*)0x7ffff7f8a000
# 查看 g0.sched.sp 和 g0.stack
(gdb) x/16xg $g0->sched.sp-128
该命令从
g0.sched.sp向下扫描 128 字节,定位可能残留的runtime.gopanic或runtime.mcall返回地址。需结合go tool objdump -s runtime.gopanic对照符号偏移。
常见栈帧特征(内存扫描锚点)
| 地址内容类型 | 典型值示例 | 用途 |
|---|---|---|
| 函数返回地址 | 0x000000000042a1c0 |
匹配 runtime.* 符号 |
| defer 记录指针 | 0xc000012340 |
指向 runtime._defer 结构 |
| panic 结构体指针 | 0xc000098760 |
runtime._panic 头部 |
graph TD
A[panic 被调用] --> B[runtime.gopanic]
B --> C[遍历 defer 链执行]
C --> D{recover 拦截?}
D -->|否| E[切换至 g0]
E --> F[runtime.fatalpanic]
F --> G[扫描 g0.stack.hi ~ sched.sp 内存]
2.5 调度器唤醒点识别与runtime.mcall/routine.gogo调用链重建(理论+符号混淆下模式匹配实践)
Go 运行时调度器在 Goroutine 切换时依赖 runtime.mcall(进入系统栈)与 runtime.gogo(恢复用户栈)构成核心上下文切换原语。符号混淆后,二者常被剥离或重命名,但保留可识别的汇编模式。
关键指令模式特征
mcall:CALL指令跳转至栈切换函数,紧随MOVQ SP, (R14)类保存现场操作gogo: 包含MOVQ (R14), SP及后续RET,标志栈恢复起点
混淆环境下的匹配策略
- 使用
objdump -d提取.text段所有CALL/JMP目标地址 - 对每个目标函数扫描是否存在
SP与R14寄存器对称存取序列
// 示例:混淆后 gogo 片段(amd64)
movq 0x8(%r14), %rsp // 恢复 SP → 核心标识
movq 0x10(%r14), %rbp
ret
逻辑分析:
%r14在 Go 中固定为g结构体指针;0x8(%r14)偏移对应g.sched.sp字段。该模式在任意符号名下均稳定存在,是调用链重建的锚点。
| 工具 | 用途 | 是否支持模式回溯 |
|---|---|---|
| delve | 动态断点 + 寄存器观察 | ✅ |
| readelf + awk | 静态节扫描 + 指令匹配 | ✅ |
| Ghidra Script | 自动标注 mcall/gogo 候选 | ✅ |
graph TD
A[识别 CALL 指令目标] --> B{目标函数含 R14/SP 对称操作?}
B -->|是| C[标记为 gogo 候选]
B -->|否| D[跳过]
C --> E[反向追溯调用者中 mcall 模式]
第三章:Go接口类型(iface/eface)逆向解构
3.1 iface与eface底层结构体定义与ABI对齐规则推导(理论+go:build -gcflags=”-S”汇编对照实践)
Go 运行时中,iface(接口)与 eface(空接口)是两类核心动态类型载体,其内存布局直接受 ABI 对齐约束。
接口结构体定义
// src/runtime/runtime2.go(简化)
type iface struct {
tab *itab // 类型-方法表指针
data unsafe.Pointer // 指向实际数据(已装箱)
}
type eface struct {
_type *_type // 具体类型描述符
data unsafe.Pointer // 指向值副本
}
tab 和 _type 均为指针,64位平台下占8字节;data 同理。二者均为 16 字节对齐结构(含填充),确保 CPU 高效访存。
ABI 对齐验证(关键推导)
| 字段 | 类型 | 大小(bytes) | 偏移(bytes) | 对齐要求 |
|---|---|---|---|---|
| tab / _type | itab / _type | 8 | 0 | 8 |
| data | unsafe.Pointer | 8 | 8 | 8 |
| 总大小 | — | 16 | — | 16(因末字段对齐=8,结构体对齐=max(8,8)=8 → 但 runtime 强制 16 字节对齐以适配 SIMD/寄存器批量操作) |
汇编对照实践
go build -gcflags="-S" main.go 2>&1 | grep -A5 "TINTERFACE"
输出中可见 MOVQ AX, (SP) 等连续 16 字节搬运指令,印证双指针对齐打包行为。
graph TD A[源码 iface/eface 定义] –> B[编译器插入 padding] B –> C[ABI 要求 16B 对齐] C –> D[汇编中 SP 偏移呈 16 的倍数]
3.2 接口方法表(itab)动态生成时机与符号剥离后恢复策略(理论+PLT/GOT劫持模拟重构实践)
Go 运行时在首次调用接口方法时,惰性生成 itab(interface table),其地址由 runtime.getitab 动态计算并缓存。符号剥离(strip -s)后,.rela.plt 与 .got.plt 中的重定位项虽消失,但 PLT stub 结构和 GOT 项偏移仍物理存在。
itab 生成关键路径
- 首次
iface.assert触发getitab(interfacetype, _type, canfail) - 若未命中全局
itabTable哈希桶,则分配新itab并原子写入 - 方法指针数组通过
(*_type).uncommon().methods动态填充
GOT 覆盖模拟(x86-64)
# 假设 got_entry = 0x4a12b0,指向原 runtime.getitab
mov rax, 0x401f00 # 恶意伪造 itab 构造函数地址
mov QWORD PTR [0x4a12b0], rax
此汇编将 GOT 中
runtime.getitab的解析目标篡改为可控桩函数,后续itab分配将被劫持——需确保伪造itab结构体满足itab.inter/itab._type/itab.fun[0]对齐约束。
| 字段 | 偏移 | 说明 |
|---|---|---|
inter |
0 | *interfacetype(只读) |
_type |
8 | *_type(类型元数据) |
fun[0] |
32 | 第一个方法实际地址 |
graph TD
A[接口调用] --> B{itab 缓存命中?}
B -- 否 --> C[调用 runtime.getitab]
C --> D[查 itabTable 哈希桶]
D -- 未找到 --> E[分配新 itab + 填充方法表]
D -- 找到 --> F[返回缓存 itab]
E --> G[写入全局 itabTable]
3.3 空接口值传递引发的堆分配痕迹与类型指针提取技巧(理论+heap profile逆向映射实践)
空接口 interface{} 在值传递时,若底层类型未内联(如大结构体或非栈友好类型),会触发隐式堆分配——Go 编译器将数据复制到堆并存储指向它的指针。
堆分配观测示例
func passEmptyInterface(v [128]int) interface{} {
return v // 触发 heap-alloc: size > 128B 且非逃逸分析友好
}
分析:
[128]int占 1024 字节,超出默认栈帧安全阈值;编译器插入runtime.newobject调用,该分配在pprof -alloc_space中可见为runtime.mallocgc栈帧。
类型信息逆向提取关键路径
| 步骤 | 工具/方法 | 作用 |
|---|---|---|
| 1 | go tool pprof -http=:8080 mem.pprof |
定位高分配 site |
| 2 | runtime/debug.ReadGCStats + runtime.MemStats |
关联分配量与调用栈 |
| 3 | unsafe.Pointer(&iface.itab) |
从 interface{} 值中提取 itab 指针,进而解析 *runtime._type |
核心技巧:从 iface 结构反推类型
type iface struct {
tab *itab // → itab._type → _type.name
data unsafe.Pointer
}
利用
unsafe提取tab->_type->string可实现运行时类型名还原,配合 heap profile 实现“分配源→具体类型→业务逻辑”的逆向映射。
第四章:defer链表结构与执行逻辑还原
4.1 _defer结构体在栈/堆中的生命周期与字段偏移稳定性分析(理论+Go版本差异对比实践)
Go 运行时将 defer 调用封装为 _defer 结构体,其内存归属(栈/堆)由逃逸分析与 defer 数量共同决定:单个非循环 defer 通常栈分配;多个或闭包捕获变量时触发堆分配。
内存布局稳定性关键字段
// Go 1.17+ runtime/panic.go(简化)
type _defer struct {
siz int32 // defer 参数总大小(含函数指针、参数)
fn uintptr // 延迟函数地址
_link *_defer // 链表指针(栈上指向下一个 defer,堆上用于 GC 可达性)
sp uintptr // 关联的栈帧指针(决定生命周期终点)
}
sp字段决定_defer是否随当前 goroutine 栈帧销毁而失效;_link在栈分配时复用栈空间,堆分配时由 GC 管理。Go 1.18 起siz改为int32(原uintptr),字段偏移发生变更。
Go 版本差异对照表
| 字段 | Go 1.17 | Go 1.18+ | 影响 |
|---|---|---|---|
siz |
uintptr |
int32 |
偏移减小 4/8 字节,影响 unsafe.Offsetof 稳定性 |
_link |
栈/堆统一 _defer* |
栈上仍复用,堆上引入 heapBits 标记 |
GC 扫描逻辑强化 |
生命周期决策流程
graph TD
A[编译期 defer 数量 & 逃逸分析] --> B{≤1 且无逃逸?}
B -->|是| C[栈分配:_defer 布局于当前栈帧]
B -->|否| D[堆分配:runtime.newdefer 分配 + GC 跟踪]
C --> E[随函数返回自动回收]
D --> F[依赖 _defer._link 链表与 GC 标记清除]
4.2 defer链表构建顺序与函数返回前的链表遍历指令模式识别(理论+LLVM IR反推汇编特征实践)
Go 编译器在函数入口插入 runtime.deferproc 调用,按逆序将 defer 记录压入 goroutine 的 _defer 链表头:
func example() {
defer fmt.Println("first") // 地址A → next = nil
defer fmt.Println("second") // 地址B → next = A
} // 返回前链表:B → A
逻辑分析:
deferproc(fn, argp)将新_defer结构体分配在栈/堆,并通过d.link = g._defer; g._defer = d实现头插。参数fn是 defer 函数指针,argp指向其参数帧起始地址。
defer 遍历触发点识别(LLVM IR 特征)
在优化后 LLVM IR 中,call @runtime.deferreturn 出现在所有 ret 指令前,且被 br 控制流包围:
| IR 特征 | 含义 |
|---|---|
call void @runtime.deferreturn(i32 %s) |
%s 为 defer 栈帧序号 |
br label %defer_check |
统一出口跳转锚点 |
执行时序流程
graph TD
A[函数执行] --> B[defer语句:头插链表]
B --> C[ret指令前:调用 deferreturn]
C --> D[deferreturn:pop + call]
D --> E[链表为空?→ ret]
4.3 panic恢复场景下defer链表截断与recover调用点定位(理论+信号上下文寄存器快照分析实践)
当 panic 触发时,运行时会从当前 goroutine 的 defer 链表逆序执行,直至遇到 recover() 调用或链表耗尽。关键在于:recover() 仅在 defer 函数中且 panic 正在传播时有效,此时运行时会截断 defer 链表,丢弃 recover() 之后注册的 defer。
寄存器快照揭示调用点
在 Linux x86-64 下,SIGUSR1 信号 handler 中可捕获 rip(指令指针)与 rbp(帧基址),精确定位 recover() 所在栈帧:
// 信号上下文寄存器快照(gdb extract)
(gdb) info registers rip rbp rsp
rip 0x49a2c0 // 指向 runtime.gopanic+0x2e0
rbp 0xc00003df58 // 当前 defer 栈帧基址
rsp 0xc00003df00
分析:
rbp指向deferProc栈帧,结合runtime._defer结构体偏移(fn=24,link=16),可遍历链表并比对fn地址是否落在runtime.gorecover符号范围内。
defer 截断行为验证
| 现象 | 表现 |
|---|---|
recover() 成功 |
d.link 被置为 nil,后续 defer 不执行 |
非 defer 中调用 recover() |
返回 nil,链表无截断 |
func f() {
defer fmt.Println("A") // 注册顺序:A → B → C
defer func() {
recover() // ✅ 截断点:B 之后的 C 被跳过
fmt.Println("B")
}()
defer fmt.Println("C")
panic("boom")
}
逻辑:
runtime.deferproc将 defer 节点压入 goroutine 的_defer链表头;runtime.panicwrap在发现recover()后,将当前节点d.link = nil,终止遍历。
graph TD A[panic 开始] –> B[遍历 defer 链表] B –> C{遇到 recover?} C –>|是| D[截断链表 d.link = nil] C –>|否| E[执行 defer.fn] D –> F[恢复栈,继续执行]
4.4 多层嵌套defer与闭包捕获变量的栈帧关联性还原(理论+调试信息缺失下的内存访问模式挖掘实践)
当 defer 链深度 ≥3 且闭包捕获局部变量时,Go 运行时会将变量抬升至堆或在栈帧中保留额外偏移槽位。调试符号缺失时,需通过 runtime.gopclntab 反向解析 PC→SP 偏移,并结合 goroutine.stack 中的寄存器快照推断闭包捕获点。
栈帧偏移推断关键路径
- 解析
defer链中每个*_defer结构体的fn和sp字段 - 比对相邻 defer 的
sp差值,定位闭包环境指针(fn.env)所在栈地址 - 用
readmem在该地址读取 8 字节,解引用为捕获变量实际地址
示例:三重 defer 中闭包变量生命周期探测
func nested() {
x := 42
defer func() { _ = x }() // 捕获 x
defer func() { _ = x + 1 }()
defer func() { println(x) }()
}
分析:
x被抬升为栈上固定偏移(如[rbp-0x18]),三个 defer 的fn.env均指向同一栈基址+固定偏移。runtime/debug.ReadBuildInfo()不可见,但unsafe.Pointer(uintptr(&x)-0x18)可复现运行时取值路径。
| 推断依据 | 内存表现 | 工具链支持 |
|---|---|---|
defer.sp 差值 |
栈帧收缩步长(通常 0x20~0x40) | dlv dump stack |
fn.env 解引用值 |
指向 [rbp-0x18] 等静态偏移 |
objdump -d |
graph TD
A[goroutine 执行 nested] --> B[分配栈帧,x 存于 rbp-0x18]
B --> C[创建第1个 defer:env=rbp]
C --> D[创建第2个 defer:env=rbp]
D --> E[创建第3个 defer:env=rbp]
E --> F[defer 执行时,所有 env+偏移 → 同一 x 地址]
第五章:CTF实战案例复盘与工具链演进
真实赛题复盘:2023 HITB CTF Web方向“Token Vault”漏洞链
在HITB Dubai线下赛中,一道Web题要求攻击者绕过JWT签名验证并读取管理员私钥。初始阶段选手普遍尝试HS256密钥爆破失败,后通过响应头X-Powered-By: Express/4.18.2 + jwt-simple@0.5.6定位到旧版jwt-simple库——该库存在算法混淆漏洞(CVE-2015-9235),允许将HS256签名的token强制转为none算法解析。参赛队伍使用自研脚本jwt-alg-switch.py完成自动重签,成功注入伪造管理员身份。关键payload如下:
import jwt
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiZ2VzdCIsImV4cCI6MTcxNzQwMjYwMH0.xxxx"
header = {"alg": "none", "typ": "JWT"}
payload = {"user": "admin", "exp": 1717402600}
forged = jwt.encode(payload, "", algorithm=None, headers=header)
工具链协同演进路径
现代CTF解题已从单点工具转向流水线协作。下表对比了2020–2024年主流战队的工具链迭代:
| 年份 | 核心调试器 | 自动化框架 | 协作中枢 | 关键改进 |
|---|---|---|---|---|
| 2020 | GDB + pwndbg | Bash脚本集 | Slack频道 | 手动同步exploit状态 |
| 2022 | GEF + Pwntools 4.x | ROPgadget + ropper | GitHub Actions CI | 自动化ROP链生成与测试 |
| 2024 | Pwndbg + QEMU-user-static + Ghidra RPC | CTFd-Plugin + custom LLM agent | Discord + Obsidian知识图谱 | 实时反编译补丁比对+符号执行辅助约束求解 |
静态分析工具链的深度整合
在DEF CON Quals 2024的Pwn题“KernelKraft”中,团队采用三阶静态分析流程:
- 使用
binja-ctf-plugin提取内核模块的syscall表与ioctl handler地址; - 调用
angr构建系统调用图谱,识别未校验copy_from_user的边界路径; - 将CFG导出为DOT格式,经
graphviz渲染后人工标注竞态窗口,最终定位ioctl(0x1337)中kfree()与copy_to_user()的UAF窗口。
flowchart LR
A[BinaryNinja提取符号] --> B[angr构建CFG]
B --> C{是否存在未验证指针引用?}
C -->|Yes| D[生成约束条件]
C -->|No| E[跳过该路径]
D --> F[Ghidra插件注入注释]
动态取证工具的战场适配
面对近年CTF中高频出现的eBPF沙箱逃逸题,传统strace已失效。某战队开发bpf-tracer工具,直接hook bpf_prog_load()系统调用,实时捕获BPF字节码并自动分类指令模式。在PlaidCTF 2024的“eBPF Jail”题中,该工具检测到ldabs指令异常访问skb->data偏移量0xfffffff8,结合bpftool prog dump xlated反汇编确认其触发了内核sk_skb_is_valid_access()绕过逻辑。
多模态漏洞挖掘工作流
当逆向固件题引入AI模型组件时,工具链需融合传统二进制分析与ML推理追踪。例如在2024 Riscure CTF中,某IoT设备固件包含TensorFlow Lite模型,团队使用tflite-inspect提取算子图,再以ghidra-tflite-loader加载模型权重至Ghidra内存空间,最终发现CONV_2D算子输入缓冲区未做尺寸校验,构造超长padding触发栈溢出。整个过程依赖radare2的r2pipe与Python tflite.Model API双向通信实现自动化参数提取。
