https://medium.com/numen-cyber-labs/the-story-of-a-high-vulnerability-in-move-reference-safety-verify-module-2340f3d8c642
0x0 前言
前段时间,我们发现了一个关键漏洞 在 Aptos Movevm 中。经过一番深入研究,我们又发现了一个严重的漏洞,也是一个整数溢出,但这一次非常有趣。
我们知道 Move 语言在执行字节码之前会验证代码单元。在代码单元中验证它分为 4 个步骤。此错误发生在 reference_safety。我们将在下面更详细地介绍它。
该模块定义了用于验证程序主体的引用安全性的传递函数。检查包括(但不限于)验证没有悬挂引用、对可变引用的访问是安全的以及对全局存储引用的访问是安全的。
这里是验证入口点,它会调用analyze_function。
在analyze_function中,每个基本块都会对函数进行校验,那么基本块是什么?
在里面编译器构造 ,基本块是一个直线代码序列,除了入口没有分支,除了出口没有分支。
在Move语言中,我们如何识别一个基本块?
在Move语言中,基本块是通过遍历字节码,找到所有的分支指令和循环指令来确定的。你可以看到下面的核心代码:
下面是 Move 语言的代码块示例。我们可以看到有 3 个基本块。分支由指令确定:BrTrue、Branch、Ret。
0x1 移动中的参考安全
Move 支持两种类型的引用:不变的 — 用 & 定义(例如 &T)和可变的 — &mut(例如 &mut T)。您可以使用不可变 (&) 引用从结构中读取数据并使用可变 (&mut) 来修改它们。通过使用正确类型的引用,您可以帮助维护安全性,并且您应该知道此方法是更改值还是仅读取。
下面是官方 Move 教程中的示例:
在上面的例子中,我们可以看到 mut_ref_t 是值 t 的可变引用。
因此,Move引用安全模块试图确认引用是否有效,它以函数为单位,扫描函数中的基本块,通过字节码指令校验判断所有引用操作是否合法。
下图显示了它验证引用安全性的例程。
这里的状态是 AbstractState,它包含借用图和局部变量,它们都用于保护引用。
借用图 是表示局部引用之间关系的图。
从上图我们可以看出,有一个前状态 其中包括本地和借用图(L,BG),然后执行基本块将生成一个后状态 与 (L',BG'),然后将合并之前和之后的状态以更新块状态并将该块的后置条件传播到后续块。这就像节点海 V8涡轮风扇发动机的优化。
下面的代码就是上图对应的主循环。首先执行块代码(执行指令不成功会返回AnalysisError),然后尝试合并前状态 和后状态 通过确定是否连接结果 是否改变。如果改变了,当前块包含一个边缘点(即有循环),它会跳回到循环的开始,在下一轮仍然会执行这个块,直到后状态 等于前状态 或因某些错误而中止。
我们如何判断joinResult是变化还是不变?
通过上面的代码,我们可以通过判断locals和borrow关系是否发生变化来判断join结果是否发生了变化。这里的 join_ 函数用于更新本地和借用图状态。
结合下面的join_代码,第6行是初始化一个新的locals Map,第9行如果all值为None,则迭代locals中的所有索引,执行block之前和之后不插入到新的当地人地图。如果state之前有value,post state为None,我们需要释放brow_graph id,也就是消除value的借用关系。反之亦然。特别是,当两个值都存在并且相同时,像第 30-33 行一样将它们插入到新映射中,然后在第 38 行合并 borrow_graph。
从上面我们可以看到 self.iter_locals() 是本地人的数量。请注意,这个局部变量不仅包括函数的真实局部变量,还包括参数。
0x2 漏洞
到这里我们就把所有和漏洞相关的代码都划过了,你找到了吗?
如果您找不到漏洞,那也没关系。我将在下面详细介绍漏洞触发过程。
首先在 blew 代码中如果参数 length 添加局部长度大于 256。似乎没有问题吧?
但是这个函数将返回项目类型为 u8 的 Iterator。
所以在函数 join_() 中是 function_view.parameters().len() 和 function_view.locals().len() 组合值大于 256。
在代码中,对于 self.iter_locals() 中的本地 ,局部变量的类型为 u8。 256次迭代后,会造成溢出。溢出后local的值为8。
其实Move有校验local号的套路,可惜在check bounds模块只校验locals,不包括参数的长度。
开发人员似乎知道这里需要检查参数 + 本地值。但是,代码在检查边界模块中验证局部计数,它不包括参数的长度。
0x3 将溢出移动到 DoS
我们知道有一个主循环扫描代码块并在调用 execute_block 函数并加入状态后。如果移动代码存在,循环将跳转到再次开始执行的块。
所以如果我们做一个循环代码块,利用溢出来改变块的状态,它使得AbstractState对象中的新locals映射与之前不同,然后用函数execute_block函数再次执行块,我们知道这个函数分析代码字节码并授予本地访问权限,因此如果新的 AbstractState 本地映射中不存在参考值偏移量,则会导致 DoS。
在审核代码后,我发现通过使用 MoveLoc/CopyLoc/FreeRef 操作码,我们可以实现这个目标。
这里让我们看看文件路径中execute_block函数调用的copy_loc函数:
移动/语言/移动字节码验证器/src/reference_safety/abstract_state.rs
在第287行,代码尝试通过LocalIndex作为参数获取本地值,如果LocalIndex不存在,会导致panic,想象一下,当节点执行这段恶意代码时,会导致整个节点崩溃。
0x4PoC
这是 PoC,您可以在 git commit 中复制它:add615b64390ea36e377e2a575f8cb91c9466844
这是崩溃日志:
线程“regression_tests::reference_analysis::PoC”在“调用`Option::unwrap()` 时惊慌失措”在“无”上value', language/move-bytecode-verifier/src/reference_safety/abstract_state.rs:287:39
注意:使用 `RUST_BACKTRACE=1` 运行显示回溯的环境变量
DoS触发步骤:
我们可以看到代码块是一个无条件分支,每次执行完最后一条指令branch(0),都会跳回第一条指令,所以会多次调用execute_block和join函数。
1.第一次这里我们设置参数为SignatureIndex(0),locals为SignatureIndex(0)会导致num_locals为132*2=264。所以在调用
领先的新本地人长度为 264–256=8
2、第二次执行execute_block函数时,执行第一条指令copy_local(57),57是需要入栈的locals的offset,但是这次locals只有8的长度,offset 57不存在,因此这将导致 get(57).unwrap() 函数不返回任何内容并引起恐慌。
0x5 摘要
这就是关于这个漏洞的全部故事。从中我们了解到:
首先,这个漏洞表明没有绝对安全的代码。 Move语言在代码执行之前确实做了很好的静态校验,但是就像这个漏洞一样,可以通过溢出漏洞完全绕过之前的边界校验。
其次,代码审计很重要,因为程序员有时会疏忽大意。作为Move语言安全的领导者,我们将继续深挖Move的安全问题。
第三点,对于Move语言,我们建议语言设计者增加更多的校验码,以防止意外情况的发生。
目前Move语言主要是在验证阶段进行一系列的安全检查,但我认为这还不够。一旦验证被绕过,在运行阶段没有过多的安全加固,就会导致进一步的危害被加深,引发更严重的问题。最后,我们还发现了Move语言的另一个漏洞,我们会尽快向大家披露。