作者:古河@360 Vulcan Team
作者博客:奇虎360技术博客
asset是EOS官方头文件中提供的用来代表货币资产(如官方货币EOS或自己发布的其它货币单位)的一个结构体。在使用asset进行乘法运算(operator *=
)时,由于官方代码的bug,导致其中的溢出检测无效化。造成的结果是,如果开发者在智能合约中使用了asset乘法运算,则存在发生溢出的风险。
问题代码存在于contracts/eosiolib/asset.hpp:
asset& operator*=( int64_t a ) {
eosio_assert( a == 0 || (amount * a) / a == amount, "multiplication overflow
or underflow" ); <== (1)
eosio_assert( -max_amount <= amount, "multiplication underflow" ); <= (2)
eosio_assert( amount <= max_amount, "multiplication overflow" ); <= (3)
amount *= a;
return *this;
}
可以看到,这里官方代码一共有3处检查,用来防范溢出的发生。不幸的是,这三处检查没有一处能真正起到作用。
首先我们来看检查(2)和(3),比较明显,它们是用来检查乘法的结果是否在合法取值范围[-max_amouont, max_amount]
之内。这里的问题是他们错误地被放置在了amouont *= a
这句代码之前,正确的做法是将它们放到amouont *= a
之后,因为它的目的是检测运算结果的合法性。正确的代码顺序应该是这样:
amount *= a;
eosio_assert( -max_amount <= amount, "multiplication underflow" ); <= (2)
eosio_assert( amount <= max_amount, "multiplication overflow" ); <= (3)
下面来看检测(1),这是一个非常重要的检测,目的是确保两点:
乘法结果没有导致符号改变(如两个正整数相乘,结果变成了负数)
乘法结果没有溢出64位符号数(如两个非零正整数数相乘,结果比其中任意一个都小)
eosio_assert( a == 0 || (amount * a) / a == amount, "multiplication
overflow or underflow" ); <== (1)
这里的问题非常隐晦,直接看C++源代码其实看不出什么问题。但是我们要知道,EOS的智能合约最终是编译成webassembly字节码文件来执行的,让我们来看看编译后的字节码长什么样子:
(call $eosio_assert
(i32.const 1) // always true
(i32.const 224) // "multiplication overflow or underflow\00")
)
上述字节码对应于源码中的:
eosio_assert( a == 0 || (amount * a) / a == amount, "multiplication
overflow or underflow" ); <== (1)
这个结果让我们非常吃惊,应为很明显,生成的字节码代表的含义是:
eosio_assert(1, "multiplication overflow or underflow" );
相当于说这个assert的条件变成了永远是true,这里面的溢出检测就这样凭空消失了!!!
根据我们的经验,会发生这样的问题,很可能是编译器优化导致的。于是我们查看了一下官方提供的编译脚本(eosiocpp):
($PRINT_CMDS; /root/opt/wasm/bin/clang -emit-llvm -O3 --std=c++14 --target
=wasm32 -nostdinc \
可以看到它是调用clang进行编译的,并且默认开启了编译器优化,优化级别是O3,比较激进的一个级别。
我们尝试关闭编译器优化(使用-O0),然后重新编译相同的代码,这次得到的对应字节码如下:
(block $label$0
(block $label$1
(br_if $label$1
(i64.eqz
(get_local $1)
)
)
(set_local $3
(i64.eq
(i64.div_s
(i64.mul
(tee_local $1
(i64.load
(get_local $0)
)
)
(tee_local $2
(i64.load
(get_local $4)
)
)
)
(get_local $2)
)
(get_local $1)
)
)
(br $label$0)
)
(set_local $3
(i32.const 1)
)
)
(call $eosio_assert
(get_local $3) // condition based on "a == 0 || (amount * a) / a == amount"
(i32.const 192) // "multiplication overflow or underflow\00")
可以看到这次生成的字节码中完整保留了溢出检测的逻辑,至此我们可以确定这个问题是编译器优化造成的。
为什么编译器优化会导致这样的后果呢?这是因为在下面的语句中,amount和a的类型都是有符号整数:
eosio_assert( a == 0 || (amount * a) / a == amount, "multiplication
overflow or underflow" );
在C/C++标准中,有符号整数的溢出属于“未定义行为(undefined behavior)”。当出现未定义行为时,程序的行为是不确定的。所以当一些编译器(包括gcc,clang)做优化时,不会去考虑出现未定义行为的情况(因为一旦出现未定义行为,整个程序就处于为定义状态了,所以程序员需要自己在代码中去避免未定义行为)。简单来讲,在这个例子里面,clang在做优化时不会去考虑以下乘法出现溢出的情况:
(amount * a)
那么在不考虑上面乘法溢出的前提下,下面的表达式将永远为true:
a == 0 || (amount * a) / a == amount
于是一旦打开编译器优化,整个表达式就直接被优化掉了。
8月7日EOS官方发布了这个漏洞的补丁:
https://github.com/EOSIO/eos/commit/b7b34e5b794e323cdc306ca2764973e1ee0d168f
由于asset乘法中所有的三处检测通通无效,当合约中使用asset乘法时,将会面临所有可能类型的溢出,包括:
对于EOS开发者,如果您的智能合约中使用到了asset的乘法操作,我们建议您更新对应的代码并重新编译您的合约。因为像asset这样的工具代码是静态编译进合约中的,必须重新编译才能解决其中的安全隐患。
同时,我们也建议各位EOS开发者重视合约中的溢出问题,在编写代码时提高安全意识,避免造成不必要的损失。
2018-7-26: 360 Vulcan团队在代码审计中发现asset中乘法运算的溢出问题
2018-7-27: 通过Hackerone平台将漏洞提交给EOS官方
2018-8-7: EOS官方发布补丁修复漏洞