作者:古河@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),这是一个非常重要的检测,目的是确保两点:

  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官方发布补丁修复漏洞


源链接

Hacking more

...