EOS 官方 API 中 Asset 结构体的乘法运算溢出漏洞描述

综述

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),这是一个非常重要的检测,目的是确保两点:
1.乘法结果没有导致符号改变(如两个正整数相乘,结果变成了负数)
2.乘法结果没有溢出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乘法时,将会面临所有可能类型的溢出,包括:

  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开发者重视合约中的溢出问题,在编写代码时提高安全意识,避免造成不必要的损失。

时间线

2018-7-26: 360 Vulcan团队在代码审计中发现asset中乘法运算的溢出问题 2018-7-27: 通过Hackerone平台将漏洞提交给EOS官方 2018-8-7: EOS官方发布补丁修复漏洞

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值