【链安科技】EOS资产Asset乘法运算溢出漏洞

【链安科技】EOS资产Asset乘法运算溢出漏洞_第1张图片

综述

asset是EOS官方头文件中提供的用来代表货币资产(如官方货币EOS或自己发布的其它货币单位)的一个结构体。在使用asset进行乘法运算(operator *=)时,由于官方代码的bug,导致其中的溢出检测无效化。造成的结果是,如果开发者在智能合约中使用了asset乘法运算,则存在发生溢出的风险。

漏洞细节

问题代码存在于contracts/eosiolib/asset.hpp:

【链安科技】EOS资产Asset乘法运算溢出漏洞_第2张图片
image

可以看到,这里官方代码一共有3处检查,用来防范溢出的发生。不幸的是,这三处检查没有一处能真正起到作用。

首先我们来看检查(2)和(3),比较明显,它们是用来检查乘法的结果是否在合法取值范围[-max_amouont, max_amount]之内。这里的问题是他们错误地被放置在了amouont *= a这句代码之前,正确的做法是将它们放到amouont *= a之后,因为它的目的是检测运算结果的合法性。正确的代码顺序应该是这样:

【链安科技】EOS资产Asset乘法运算溢出漏洞_第3张图片
image

下面来看检测(1),这是一个非常重要的检测,目的是确保两点:

1.乘法结果没有导致符号改变(如两个正整数相乘,结果变成了负数)

2.乘法结果没有溢出64位符号数(如两个非零正整数数相乘,结果比其中任意一个都小)

image

这里的问题非常隐晦,直接看C++源代码其实看不出什么问题。但是我们要知道,EOS的智能合约最终是编译成webassembly字节码文件来执行的,让我们来看看编译后的字节码长什么样子:

【链安科技】EOS资产Asset乘法运算溢出漏洞_第4张图片
image

上述字节码对应于源码中的:

image

这个结果让我们非常吃惊,应为很明显,生成的字节码代表的含义是:

image

相当于说这个assert的条件变成了永远是true,这里面的溢出检测就这样凭空消失了!!!

根据我们的经验,会发生这样的问题,很可能是编译器优化导致的。于是我们查看了一下官方提供的编译脚本(eosiocpp):

image

可以看到它是调用clang进行编译的,并且默认开启了编译器优化,优化级别是O3,比较激进的一个级别。

我们尝试关闭编译器优化(使用-O0),然后重新编译相同的代码,这次得到的对应字节码如下:

【链安科技】EOS资产Asset乘法运算溢出漏洞_第5张图片
image

可以看到这次生成的字节码中完整保留了溢出检测的逻辑,至此我们可以确定这个问题是编译器优化造成的。

为什么编译器优化会导致这样的后果呢?这是因为在下面的语句中,amount和a的类型都是有符号整数:

image

在C/C++标准中,有符号整数的溢出属于“未定义行为(undefined behavior)”。当出现未定义行为时,程序的行为是不确定的。所以当一些编译器(包括gcc,clang)做优化时,不会去考虑出现未定义行为的情况(因为一旦出现未定义行为,整个程序就处于为定义状态了,所以程序员需要自己在代码中去避免未定义行为)。简单来讲,在这个例子里面,clang在做优化时不会去考虑以下乘法出现溢出的情况:

image

那么在不考虑上面乘法溢出的前提下,下面的表达式将永远为true:

image

于是一旦打开编译器优化,整个表达式就直接被优化掉了。

漏洞的危害

由于asset乘法中所有的三处检测通通无效,当合约中使用asset乘法时,将会面临所有可能类型的溢出,包括:

  1. a > 0, b > 0, a * b < 0

  2. a > 0, b > 0, a * b < a

  3. a * b > max_amount

  4. a * b < -max_amount

响应建议

对于EOS开发者,如果您的智能合约中使用到了asset的乘法操作,我们建议您更新对应的代码并重新编译您的合约。因为像asset这样的工具代码是静态编译进合约中的,必须重新编译才能解决其中的安全隐患。

同时,我们也建议各位EOS开发者重视合约中的溢出问题,在编写代码时提高安全意识,避免造成不必要的损失。

本文转载自《Asset乘法运算溢出漏洞》,已获得原作者授权

你可能感兴趣的:(【链安科技】EOS资产Asset乘法运算溢出漏洞)