使用Binary Ninja进行漏洞建模:从Heartbleed案例看静态分析与定理证明
漏洞建模与Binary Ninja - Trail of Bits博客
使用Binary Ninja进行漏洞建模
Josh Watson
2018年4月4日
binary-ninja, program-analysis, reversing, static-analysis
这是关于Binary Ninja中间语言(BNIL)系列文章的第三部分。您可以在此处阅读第一部分,在此处阅读第二部分。
在我的上一篇文章中,我演示了如何利用低级中间语言(LLIL)编写架构无关的插件来反虚拟化C++虚函数。自那时起,Binary Ninja添加了许多新功能;特别是中级中间语言(MLIL)和静态单赋值(SSA)形式[1]。在本文中,我将讨论这两者,并演示它们的一个有趣用途:自动化漏洞发现。
许多静态分析器可以对源代码执行漏洞发现,但如果您只有二进制文件呢?我们如何建模漏洞然后检查二进制文件是否易受攻击?简短答案:使用Binary Ninja的MLIL和SSA形式。它们使得构建和求解方程组变得容易,通过定理证明器将二进制文件像炼金术一样转化为漏洞!
让我们以大家曾经最喜欢的热门漏洞Heartbleed为例,逐步了解这个过程。
像2014年一样黑客:让我们找到Heartbleed!
对于那些可能不记得或不熟悉Heartbleed漏洞的人,让我们快速回顾一下。Heartbleed是OpenSSL 1.0.1 – 1.0.1f中的一个远程信息泄露漏洞,允许攻击者向任何使用TLS的服务发送特制的TLS心跳消息。该消息会欺骗服务响应最多64KB的未初始化数据,其中可能包含私密信息,如私有加密密钥或个人数据。这是可能的,因为OpenSSL使用攻击者消息中的字段作为malloc和memcpy调用的size参数,而没有首先验证给定大小是否小于或等于要读取的数据大小。以下是OpenSSL 1.0.1f中易受攻击的代码片段,来自tls1_process_heartbeat:
/* 首先读取类型和有效载荷长度 */
hbtype = *p++;
n2s(p, payload);
pl = p;
/* 跳过一些内容... */
if (hbtype == TLS1_HB_REQUEST)
{
unsigned char *buffer, *bp;
int r;
/* 为响应分配内存,大小为1字节消息类型,
* 加上2字节有效载荷长度,加上有效载荷,加上填充
*/
buffer = OPENSSL_malloc(1 + 2 + payload + padding);
bp = buffer;
/* 输入响应类型、长度并复制有效载荷 */
*bp++ = TLS1_HB_RESPONSE;
s2n(payload, bp);
memcpy(bp, pl, payload);
bp += payload;
/* 随机填充 */
RAND_pseudo_bytes(bp, padding);
r = ssl3_write_bytes(s, TLS1_RT_HEARTBEAT, buffer, 3 + payload + padding);
查看代码,我们可以看到size参数(payload)直接来自用户控制的TLS心跳消息,从网络字节顺序转换为主机字节顺序(n2s),然后传递给OPENSSL_malloc和memcpy,没有验证。在这种情况下,当payload的值大于pl处的数据时,memcpy将从pl开始的缓冲区溢出,并开始读取紧随其后的数据,泄露不应泄露的数据。1.0.1g中的修复非常简单:
hbtype = *p++;
n2s(p, payload);
if (1 + 2 + payload + 16 > s->s3->rrec.length)
return 0; /* 根据RFC 6520第4节静默丢弃 */
pl = p;
这个新检查确保memcpy不会溢出到不同的数据中。
早在2014年,Andrew就写了一篇关于编写clang分析器插件来查找像Heartbleed这样的漏洞的博客文章。然而,clang分析器插件运行在源代码上;如果我们没有源代码,如何在二进制文件中找到相同的漏洞?一种方法:通过将MLIL变量表示为一组约束并用定理证明器求解它们来构建漏洞模型!
用z3将二进制代码建模为方程
定理证明器允许我们构建一个方程组并:
- 验证这些方程是否相互矛盾。
- 找到使方程成立的值。
例如,如果我们有以下方程:
x + y = 8
2x + 3 = 7
定理证明器可以告诉我们:a) 这些方程存在解,意味着它们不相互矛盾,以及b) 这些方程的解是x = 2和y = 6。
对于本练习,我将使用Microsoft Research的Z3定理证明器。使用z3 Python库,上述示例将如下所示:
>>> from z3 import *
>>> x = Int('x')
>>> y = Int('y')
>>> s = Solver()
>>> s.add(x + y == 8)
>>> s.add(2*x + 3 == 7)
>>> s.check()
sat
>>> s.model()
[x = 2, y = 6]
Z3告诉我们方程可以满足,并提供值来求解它们。我们可以将这种技术应用于建模漏洞。事实证明,汇编指令可以被建模为代数语句。以下面的汇编片段为例:
lea eax, [ebx+8]
cmp eax, 0x20
jle allocate
int3
allocate:
push eax
call malloc
ret
当我们将此汇编提升到Binary Ninja的LLIL时,我们得到以下图:
图1. LLIL使得识别有符号比较条件变得容易。
在这段代码中,eax取ebx的值,然后加8。如果该值高于0x20,则引发中断。但是,如果该值小于或等于0x20,则该值传递给malloc。我们可以使用LLIL的输出将其建模为一组方程,如果不可能发生整数溢出,则这些方程应该是不可满足的(例如,永远不应该存在ebx的值使得ebx大于0x20但eax小于或等于0x20),这将看起来像这样:
eax = ebx + 8
ebx > 0x20
eax <= 0x20
如果我们将这些方程插入Z3会发生什么?不完全是我们所希望的。
>>> eax = Int('eax')
>>> ebx = Int('ebx')
>>> s = Solver()
>>> s.add(eax == ebx + 8)
>>> s.add(ebx > 0x20)
>>> s.add(eax <= 0x20)
>>> s.check()
unsat
应该存在整数溢出,但我们的方程是unsat。这是因为Int类型(或z3术语中的"sort")表示所有整数集合中的一个数字,其范围为-∞到+∞,因此不可能发生溢出。相反,我们必须使用BitVec sort,将每个变量表示为32位向量:
>>> eax = BitVec('eax', 32)
>>> ebx = BitVec('ebx', 32)
>>> s = Solver()
>>> s.add(eax == ebx + 8)
>>> s.add(ebx > 0x20)
>>> s.add(eax <= 0x20)
>>> s.check()
sat
这就是我们预期的结果!有了这个结果,Z3告诉我们eax可能溢出并调用malloc,其值出乎意料。再多几行,我们甚至可以看到满足这些方程的可能值:
>>> s.model()
[ebx = 2147483640, eax = 2147483648]
>>> hex(2147483640)
'0x7ffffff8'
这对于寄存器非常有效,寄存器可以轻松表示为离散的32位变量。为了表示内存访问,我们还需要Z3的Array sort,它可以模拟内存区域。然而,栈变量驻留在内存中,并且更难用约束求解器建模。相反,如果我们可以将栈变量视为模型中的寄存器呢?我们可以使用Binary Ninja的中级中间语言轻松做到这一点。
中级中间语言
正如LLIL抽象了本机反汇编一样,中级中间语言(MLIL)在LLIL之上添加了另一层抽象。LLIL抽象了标志和NOP指令,而MLIL抽象了栈,将栈访问和寄存器访问都呈现为变量。此外,在将LLIL映射到MLIL的过程中,识别并消除了未被引用的内存存储。这些过程可以在下面的示例中观察到。注意如何没有栈访问(即push或pop指令),并且var_8根本没有出现在MLIL中。
图2a. x86中的示例函数。
图2b. 示例函数的LLIL。
图2c. 示例函数的MLIL。
您可能在MLIL中注意到的另一个特征是变量被类型化。Binary Ninja最初启发式地推断这些类型,但用户以后可以通过手动分配覆盖这些类型。类型通过函数传播,并在确定函数签名时帮助通知分析。
MLIL结构
在结构上,MLIL和LLIL非常相似;两者都是表达式树,并共享许多相同的操作表达式类型(有关IL树结构的更多信息,请参阅我的第一篇博客文章)。然而,存在几个明显的差异。显然,MLIL没有类似于LLIL_PUSH和LLIL_POP的操作,因为栈已被抽象。基于寄存器的操作LLIL_REG、LLIL_SET_REG等被替换为MLIL_VAR、MLIL_SET_VAR等。除此之外,由于类型化,MLIL还具有结构的概念;MLIL_VAR_FIELD和MLIL_STORE_STRUCT表达式描述了这些操作。
图3. MLIL中的类型可以生成非常清晰的代码。
有些操作对LLIL和MLIL是通用的,尽管它们的操作数不同。LLIL_CALL操作有一个操作数:dest,调用的目标。相比之下,MLIL_CALL操作还指定了标识哪些变量接收返回值的输出操作数,以及保存描述函数调用参数的MLIL表达式列表的params操作数。用户特定的调用约定,或基于变量过程间使用的自动化分析确定的调用约定,决定了这些参数和返回值。这允许Binary Ninja识别诸如ebx寄存器在x86 PIC二进制文件中用作全局数据指针的情况,或使用自定义调用约定的情况。
将所有这一切结合在一起,MLIL非常接近反编译的代码。这也使得MLIL非常适合转换为Z3,因为它通过Binary Ninja的API抽象了寄存器和栈变量。
MLIL和API
在Binary Ninja API中使用MLIL与使用LLIL类似,尽管存在一些显著差异。与LLIL一样,函数的MLIL可以直接通过Function类的medium_level_il属性访问,但没有相应的MLIL方法来获取get_low_level_il_at。为了直接访问特定指令的MLIL,用户必须首先查询LLIL。LowLevelILInstruction类现在有一个medium_level_il属性,用于检索其MediumLevelILInstruction形式。作为一行Python代码,这将看起来像current_function.get_low_level_il_at(address).medium_level_il。重要的是要记住,这有时可以是None,因为LLIL指令可以在MLIL中完全优化掉。
MediumLevelILInstruction类引入了LowLevelILInstruction类中不可用的新便利属性。vars_read和vars_written属性使得查询指令使用的变量列表变得简单,而无需解析操作数。如果我们重新访问我第一篇博客文章中的旧指令lea eax, [edx+ecx*4],等效的MLIL指令将看起来与LLIL相似。事实上,乍一看似乎是相同的。
>>> current_function.medium_level_il[0]
<il: eax = ecx + (edx << 2)>
但是,如果我们仔细看,我们可以看到区别:
>>> current_function.medium_level_il[0].dest
<var int32_t* eax>
与LLIL不同,在LLIL中dest将是表示寄存器eax的ILRegister对象,这里的dest操作数是一个类型化的Variable对象,表示名为eax的变量作为int32_t指针。
还为MLIL引入了几个其他新属性和方法。如果我们想提取此表达式读取的变量,这将非常简单:
>>> current_function.medium_level_il[0].vars_read
[<var int32_t* ecx>, <var int32_t edx>]
branch_dependence属性返回基本块的条件分支,当只有真或假分支支配时,这些分支支配指令的基本块。这对于确定指令明确依赖哪些决策很有用。
两个属性使用数据流分析来计算MLIL表达式的值:value可以高效地计算常量值,而possible_values使用计算成本更高的路径敏感数据流分析来计算指令可以导致的值范围和不相交集合。
图5. 路径敏感数据流分析识别可以到达特定指令的所有具体数据值。
有了这些功能,我们可以建模寄存器、栈变量和内存,但还有一个我们需要解决的障碍:变量通常被重新分配依赖于先前赋值值的值。例如,如果我们正在迭代指令并遇到如下内容:
mov eax, ebx
lea eax, [ecx+eax*4]
在创建我们的方程时,我们如何建模这种重新分配?我们不能只是将其建模为:
eax = ebx
eax = ecx + (eax * 4)
这可能导致各种不可满足性,因为约束纯粹表达关于方程组中变量的数学真理,并且根本没有时间元素。由于约束求解没有时间概念,我们需要找到某种方法来弥合这一差距,转换程序以有效消除时间概念。此外,我们需要能够高效地确定先前值eax的来源。拼图的最后一块是通过Binary Ninja API可用的另一个功能:SSA形式。
静态单赋值(SSA)形式
与中级中间语言的发布同时,Binary Ninja还为BNIL系列中的所有表示引入了静态单赋值(SSA)形式。SSA形式是一种程序表示,其中每个变量被定义一次且仅一次。如果变量被赋予新值,则定义该变量的新"版本"。一个简单的例子如下:
a = 1
b = 2
a = a + b
在SSA形式中变为:
a1 = 1
b1 = 2
a2 = a1 + b1
SSA形式引入的另一个概念是phi函数(或Φ)。当变量的值依赖于程序控制流所采取的路径时,例如if语句或循环,Φ函数表示该变量可能采用的所有可能值。该变量的新版本被定义为该函数的结果。下面是一个更复杂(和具体)的例子,使用Φ函数:
def f(a):
if a > 20:
a = a * 2
else:
a = a + 5
return a
在SSA形式中变为:
def f(a0):
if a0 > 20:
a1 = a0 * 2
else:
a2 = a0 + 5
a3 = Φ(a1, a2)
return a3
SSA使得在整个程序生命周期内显式跟踪变量的所有定义和使用变得容易,这正是我们在Z3中建模变量赋值所需要的。
Binary Ninja中的SSA形式
IL的SSA形式可以在Binary Ninja中查看,但默认不可用。为了查看它,您必须首先在首选项中选中"启用插件开发调试模式"框。如下所示的SSA形式并不是为了视觉消费而设计的,因为它比正常的IL图更难阅读。相反,它主要旨在与API一起使用。
图6. MLIL函数(左)及其对应的SSA形式(右)。
任何中间语言的SSA形式都可以通过API中的ssa_form属性访问。此属性存在于函数(例如LowLevelILFunction和MediumLevelILFunction)和指令(例如LowLevelILInstruction和MediumLevelILInstruction)对象中。在这种形式中,诸如MLIL_SET_VAR和MLIL_VAR的操作被替换为新操作MLIL_SET_VAR_SSA和MLIL_VAR_SSA。这些操作使用SSAVariable操作数而不是Variable操作数。SSAVariable对象是其对应Variable的包装器,但添加了它在SSA形式中表示的Variable版本的信息。回到我们之前的重新分配示例,MLIL SSA形式将输出以下内容:
eax#1 = ebx#0
eax#2 = ecx#0 + (eax#1 << 2)
这解决了重用变量标识符的问题,但仍然存在定位变量使用和定义的问题。为此,我们可以分别使用MediumLevelILFunction.get_ssa_var_uses和MediumLevelILFunction.get_ssa_var_definition(这些方法也是LowLevelILFunction类的成员)。
现在我们的工具包已经完成,让我们深入实际在Binary Ninja中建模一个真实世界的漏洞!
示例脚本:查找Heartbleed
我们的方法将与Andrew的以及Coverity在其关于该主题的文章中使用的方法非常相似。字节交换操作是数据来自网络且用户控制的一个很好的指标,因此我们将使用Z3建模memcpy操作并确定size参数是否是字节交换的值。
图7. OpenSSL 1.0.1f的tls1_process_heartbeat中易受攻击的memcpy的size参数的反向静态切片,在MLIL中
步骤1:找到我们的"sinks"
执行典型的源到sink的污点跟踪将耗时且昂贵,如前述文章所示。让我们反过来做;识别所有对memcpy函数的代码引用并检查它们。
memcpy_refs = [
(ref.function, ref.address)
for ref in bv.get_code_refs(bv.symbols['_memcpy'].address)
]
dangerous_calls = []
for function, addr in memcpy_refs:
call_instr = function.get_low_level_il_at(addr).medium_level_il
if check_memcpy(call_instr.ssa_form):
dangerous_calls.append((addr, call_instr.address))
步骤2:消除我们知道不易受攻击的sinks
在check_memcpy中,我们可以快速消除Binary Ninja的数据流可以自行计算的任何size参数[2],使用MediumLevelILInstruction.possible_values属性。我们将建模剩下的任何内容。
def check_memcpy(memcpy_call):
size_param = memcpy_call.params[2]
if size_param.operation != MediumLevelILOperation.MLIL_VAR_SSA:
return False
possible_sizes = size_param.possible_values
# 数据流不会组合来自移位字节的多个可能值,
# 所以我们关心的任何值在此时都是未确定的。
# 这将来可能会改变?
if possible_sizes.type != RegisterValueType.UndeterminedValue:
return False
model = ByteSwapModeler(size_param, bv.address_size)
return model.is_byte_swap()
步骤3:跟踪size依赖的变量
使用size参数作为起点,我们使用所谓的静态反向切片来向后跟踪代码,并跟踪size参数依赖的所有变量。
var_def = self.function.get_ssa_var_definition(self.var.src)
# 访问我们的变量直接依赖的语句
self.to_visit.append(var_def)
while self.to_visit:
idx = self.to_visit.pop()
if idx is not None:
self.visit(self.function[idx])
visit方法接受一个MediumLevelILInstruction对象,并根据指令操作字段的值分派不同的方法。回顾BNIL是一种基于树的语言,访问者方法将递归调用指令操作数上的visit,直到到达树的终止节点。此时,它将为Z3模型生成一个变量或常量,该变量或常量将通过递归调用者传播回来,非常类似于第2部分中的vtable-navigator插件。
MLIL_ADD的访问者相当简单,递归生成其操作数,然后返回两者的和:
def visit_MLIL_ADD(self, expr):
left = self.visit(expr.left)
right = self.visit(expr.right)
if None not in (left, right):
return left + right
步骤4:识别可能是字节交换一部分的变量
MLIL_VAR_SSA,描述SSAVariable的操作,是MLIL指令树的终止节点。当我们遇到一个新的SSA变量时,我们识别负责此变量定义的指令,并将其添加到我们向后切片时要访问的指令集中。然后,我们生成一个Z3变量来表示我们模型中的此SSAVariable。最后,我们查询Binary Ninja的范围值分析,以查看此变量是否被约束为单个字节(即,在0 – 0xff范围内,从8的倍数的偏移开始)。如果是,我们继续在模型中约束此变量到该值范围。
def visit_MLIL_VAR_SSA(self, expr):
if expr.src not in self.visited:
var_def = expr.function.get_ssa_var_definition(expr.src)
if var_def is not None:
self.to_visit.append(var_def)
src = create_BitVec(expr.src, expr.size)
value_range = identify_byte(expr, self.function)
if value_range is not None:
self.solver.add(
Or(
src == 0,
And(src >= value_range.start, src <= value_range.end)
)
)
self.byte_vars.add(expr.src)
return src
我们访问的MLIL指令的父操作通常是MLIL_SET_VAR_SSA或MLIL_SET_VAR_PHI。在visit_MLIL_SET_VAR_SSA中,我们可以像往常一样递归生成src操作数的模型,但MLIL_SET_VAR_PHI操作的src操作数是SSAVariable对象的列表,表示Φ函数的每个参数。我们将这些变量的定义站点添加到我们要访问的指令集中,然后为我们的模型编写一个表达式,说明dest == src0 || dest == src1 || … || dest == srcn:
phi_values = []
for var in expr.src:
if var not in self.visited:
var_def = self.function.get_ssa_var_definition(var)
self.to_visit.append(var_def)
src = create_BitVec(var, var.var.type.width)
# ...
phi_values.append(src)
if phi_values:
phi_expr = reduce(
lambda i, j: Or(i, j), [dest == s for s in phi_values]
)
self.solver.add(phi_expr)
在visit_MLIL_SET_VAR_SSA和visit_MLIL_SET_VAR_PHI中,我们跟踪被约束为单个字节的变量,以及它们被约束为哪个字节:
# 如果此值永远不能大于一个字节,
# 那么它必须是我们交换中的一个字节。
# 将其添加到列表中以便以后检查。
if src is not None and not isinstance(src, (int, long)):
value_range = identify_byte(expr.src, self.function)
if value_range is not None:
self.solver.add(
Or(
src == 0,
And(src >= value_range.start, src <= value_range.end)
)
)
self.byte_vars.add(*expr.src.vars_read)
if self.byte_values.get(
(value_range.start, value_range.end)
) is None:
self.byte_values[
(value_range.start, value_range.end)
] = simplify(Extract(
int(math.floor(math.log(value_range.end, 2))),
int(math.floor(math.log(value_range.start, 2))),
src
)
)
最后,一旦我们访问了变量的定义指令,我们将其标记为已访问,以便不会再次添加到to_visit中。
步骤5:识别size参数上的约束
一旦我们切片了size参数并定位了用于字节交换的潜在字节,我们需要确保在导致memcpy的执行路径上没有约束会限制size的值。memcpy的MediumLevelILInstruction对象的branch_dependence属性标识了到达指令所需的强制控制流决策,以及必须采用哪个分支(真/假)。我们检查每个分支决策检查的变量,以及这些变量的依赖关系。如果基于我们确定为交换中的任何字节做出了决策,我们将假设此size参数被约束并中止其分析。
for i, branch in self.var.branch_dependence.iteritems():
for vr in self.function[i].vars_read:
if vr in self.byte_vars:
raise ModelIsConstrained()
vr_def = self.function.get_ssa_var_definition(vr)
if vr_def is None:
continue
for vr_vr in self.function[vr_def].vars_read:
if vr_vr in self.byte_vars:
raise ModelIsConstrained
步骤6:求解模型
如果size不受约束,并且我们发现size参数依赖于只是字节的变量,我们需要向我们的Z3求解器添加一个最终方程。为了识别字节交换,我们需要确保即使我们的size参数不受约束,size仍然仅由我们先前标识的字节显式构造。此外,我们还希望确保size参数的反向等于标识的字节反向。如果我们只是向模型添加一个方程来实现这些属性,它将不起作用。定理检查器只关心是否有任何值满足方程,而不是所有值,所以这 presents了一个问题。
我们可以通过否定最终方程来克服这个问题。通过告诉定理求解器我们想要确保没有值满足否定并寻找unsat结果,我们可以找到满足原始(非否定)方程的所有值的size参数。所以如果我们在添加此方程后我们的模型是不可满足的,那么我们已经找到了一个字节交换的size参数。这可能是一个错误!
self.solver.add(
Not(
And(
var == ZeroExt(
var.size() - len(ordering)*8,
Concat(*ordering)
),
reverse_var == ZeroExt(
reverse_var.size() - reversed_ordering.size(),
reversed_ordering
)
)
)
)
if self.solver.check() == unsat:
return True
步骤7:查找错误
我在两个版本的OpenSSL上测试了我的脚本:首先是易受攻击的1.0.1f,然后是修复了漏洞的1.0.1g。我使用命令./Configure darwin-i386-cc在macOS上编译了两个版本,以获得32位x86版本。当脚本在1.0.1f上运行时,我们得到以下结果:
图8. find_heartbleed.py成功识别1.0.1f中的两个易受攻击的Heartbleed函数。
如果我们在打补丁的版本1.0.1g上运行脚本:
图9. 易受攻击的函数在打补丁的1.0.1g中不再被识别!
正如我们所看到的,移除Heartbleed漏洞的打补丁版本不再被我们的模型标记!
结论
我现在已经介绍了Heartbleed缺陷如何导致OpenSSL中的重大信息泄露错误,以及Binary Ninja的中级中间语言和SSA形式如何无缝转换为像Z3这样的约束求解器。将所有内容结合在一起,我演示了如何准确地在二进制文件中建模诸如Heartbleed之类的漏洞。您可以在此处找到完整的脚本。
当然,这样的静态模型只能走这么远。对于更复杂的程序建模,例如过程间分析和循环,探索使用符号执行引擎(如我们的开源工具Manticore)进行约束求解。
既然您知道如何利用IL进行漏洞分析,请加入Binary Ninja Slack并与社区分享您自己的工具,如果您有兴趣了解更多关于BNIL、SSA形式和其他好东西的信息。
最后,不要错过2018年Infiltrate的Binary Ninja研讨会。我将与Vector 35团队一起闲逛并帮助回答问题!
[1] 在第2部分之后,Jordan告诉我Rusty说过,“Josh真的可以使用SSA形式。”既然SSA形式现在可用,我在这里添加了文章脚本的重构和更简洁版本!
[2] 目前这仅成立,因为Binary Ninja的数据流不计算不同值范围的并集,例如使用按位或将两个字节连接起来,就像在字节交换中发生的那样。我相信这是为了速度而做的设计权衡。如果Vector 35曾经实现完整的代数求解器,这可能会改变,并且将需要新的启发式方法。
如果您喜欢这篇文章,请分享:
Twitter
LinkedIn
GitHub
Mastodon
Hacker News
页面内容
像2014年一样黑客:让我们找到Heartbleed!
用z3将二进制代码建模为方程
中级中间语言
MLIL结构
MLIL和API
静态单赋值(SSA)形式
Binary Ninja中的SSA形式
示例脚本:查找Heartbleed
步骤1:找到我们的"sinks"
步骤2:消除我们知道不易受攻击的sinks
步骤3:跟踪size依赖的变量
步骤4:识别可能是字节交换一部分的变量
步骤5:识别size参数上的约束
步骤6:求解模型
步骤7:查找错误
结论
最近的帖子
非传统创新者奖学金
劫持您的PajaMAS中的多代理系统
我们构建了MCP一直需要的安全层
利用被遗弃硬件中的零日漏洞
Inside EthCC[8]:成为智能合约审计员
©️ 2025 Trail of Bits.
使用Hugo和Mainroad主题生成。
更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)
公众号二维码