智能合约安全之solidity整数溢出的原理

admin 2022年6月28日09:48:34评论391 views字数 6108阅读20分21秒阅读模式
  • 前言


 整数溢出漏洞在信息安全的各个方面都大量存在,而在solidity编写的智能合约中更是重量级的存在。在使用solidity合约的历史中,出现了多次因整数溢出而出现的严重事故,大多数都是利用溢出凭空取出超大量的代币,并在市场抛售,导致代币价值归零。


solidity中,变量支持的整数类型长度以8递增,从uint8uint256,以及int8int256。在EVM中储存一个数所占的位数是固定,当存储的数字长度超出最大值时会导致进位,使所有1翻转成0


solidity中,溢出分为加法溢出、减法溢出、乘法溢出,其直接原因都是因为运算结果超出了范围。

智能合约安全之solidity整数溢出的原理



  • 分析


1.减法

一个简单的减法溢出:
pragma solidity <=0.6.0;
 
 contract Test {
    
     function sub(uint a, uint b) public pure returns (uint) {
         return a - b;
     }
 }

这里定义了Test合约和sub方法,该方法输入两个uint类型的数并返回他们的差值,将该合约部署到remix上:

智能合约安全之solidity整数溢出的原理


智能合约安全之solidity整数溢出的原理


这里输入a为0,b为1,0减去1后,发生下溢出,结果为115792089237316195423570985008687907853269984665640564039457584007913129639935是一个超大数。


使用命令:
solc ./add.sol --asm
查看该合约生成的字节码,我们只查看与溢出有关的相关操作。

智能合约安全之solidity整数溢出的原理


tag_6中看到与减法在EVM中的字节码为sub

打开go-ethereum源码,与EVM相关的文件存放在core/vm中,其中字节码的实现在instructions.go文件,找到opSub在该文件的第35行:

func opSub(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
     x, y := scope.Stack.pop(), scope.Stack.peek()
     y.Sub(&x, y)
     return nil, nil
 }

从源码中可以知道,sub操作是将栈顶上的两个元素取出后进行Sub运算并压回栈,Sub方法为第三方包http://github.com/holiman/uint256中的函数,该包实现了uint256的表示和运算。


进入到Sub方法的实现:

// Sub sets z to the difference x-y
 func (z *Int) Sub(x, y *Int) *Int {
     var carry uint64
     z[0], carry = bits.Sub64(x[0], y[0], 0)
     z[1], carry = bits.Sub64(x[1], y[1], carry)
     z[2], carry = bits.Sub64(x[2], y[2], carry)
     z[3], _ = bits.Sub64(x[3], y[3], carry)   // 最高位的借位被丢弃
     return z
 }

该方法是Int型的一个成员方法,输入两个Int型,相减,并返回得到的Int型结果,而Int型的定义在第15行:

// Int is represented as an array of 4  uint64, in little-endian order,
 // so that Int[3] is the most significant, and Int[0]  is the least significant
 type Int [4]uint64

这个Int实际上是由4个uint64串联而成的结构,而四个64位就是256位,所以我们可以把Int直接看成uint256,那么这个函数实现的就是两个uint256数的相减。


回到函数,我们发现它使用的是借位减法运算,这种计算既可以说是模仿全加法器的实现,也可说是模仿列竖式计算加减法,让这两个数组上四个uint64数依次相减并使用借位。我们再进入Sub64函数,看一看他的64位相减是如何实现的:

// Sub64 returns the difference of x, y and  borrow: diff = x - y - borrow.
 // The borrow input must be 0 or 1; otherwise the  behavior is undefined.
 // The borrowOut output is guaranteed to be 0 or 1.
 //
 // This function's execution time does not depend on  the inputs.
 func Sub64(x, y, borrow uint64) (diff, borrowOut uint64) {
     diff = x - y - borrow
     // See Sub32 for the bit logic.
     borrowOut = ((^x & y) | (^(x ^ y) & diff)) >> 63
     return
 }

这个函数输入uint64位的x,y和借位,先计算了被减数减减数和借位的值,这个减法没有考虑溢出,如果借位,会发生溢出,其结果diff和发生借位产生的结果相同,borrowOut为是否借位,计算原理就是看上一个减法是否发生溢出。


再回到Sub函数,我们注意到在计算最高位的结果时,该语句将函数返回的借位值丢弃了,而这会造成什么后果呢?对于十进制的减法,我们列一个竖式:

智能合约安全之solidity整数溢出的原理

用0减去1,类比该函数里算法的实现,最低位由0减1,产生借位,并得出改为的值为9。之后,再下一位的0减去上一位借位的1,再次发生借位并算出该位的值是9,以此类推,直到最高位,在发生借位,并算出该位结果为9后,借位的一被丢弃,并导致最终的结果出现错误 。

实验验证:

智能合约安全之solidity整数溢出的原理

我们定义两个Int类型的数num1,num2,其值分别等效为0和1,使用Sub方法将num1减去num2

智能合约安全之solidity整数溢出的原理


结果发生了溢出。这个结果是用四个uint64串联而成的,我们写一个python脚本,将其转化为十进制类型:

num_list = [18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615]
 
 Num = ""
 for num in num_list[::-1]:
     Num += bin(num)[2::]
 print(int(Num, 2))

智能合约安全之solidity整数溢出的原理

最终得到的结果为115792089237316195423570985008687907853269984665640564039457584007913129639935与在remix中测试得到的结果相同。


2.加法

再来看看加法:

pragma solidity <=0.6.0;
 
 contract Test {
    
     function add() pure public returns (uint256 _overflow) {
         uint256  max = 2**256 - 1;
         return max + 1;
     }
 }
uint256的最大值为2**256 - 1,如果此时加一个大于0的值,就会发生溢出。

智能合约安全之solidity整数溢出的原理


智能合约安全之solidity整数溢出的原理

导入remix后,调用add方法,其结果发生上溢。
使用solc编译出字节码后发现加法对应的字节码为add

智能合约安全之solidity整数溢出的原理


在instructions.go文件中查找ADD的实现:
func opAdd(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
     x, y := scope.Stack.pop(), scope.Stack.peek()
     y.Add(&x, y)
     return nil, nil
 }
该函数从栈顶取了两个元素,使用Add算出他们的和之后再压回栈。
进入Add函数的实现:
// Add sets z to the sum x+y
 func (z *Int) Add(x, y *Int) *Int {
     var carry uint64
     z[0], carry = bits.Add64(x[0], y[0], 0)
     z[1], carry = bits.Add64(x[1], y[1], carry)
     z[2], carry = bits.Add64(x[2], y[2], carry)
     z[3], _ = bits.Add64(x[3], y[3], carry)
     return z
 }

和减法一样,256位无符号数的加法也是用四个64位数依次相加得到的,进入到Add64函数:

// Add64 returns the sum with carry of x, y  and carry: sum = x + y + carry.
 // The carry input must be 0 or 1; otherwise the  behavior is undefined.
 // The carryOut output is guaranteed to be 0 or 1.
 //
 // This function's execution time does not depend on  the inputs.
 func Add64(x, y, carry uint64) (sum, carryOut uint64) {
     sum = x + y + carry
     // The sum will overflow if both top bits are set (x &  y) or if one of them
     // is (x | y), and a carry from the lower place happened. If  such a carry
     // happens, the top bit will be 1 + 0 + 1 = 0 (&^ sum).
     carryOut = ((x & y) | ((x | y) &^ sum)) >> 63
     return
 }

函数输入x,y和进位carry,返回xy和进位的值和进位符。在Add函数中,依次从低位向高位累计,同样的,最高位的进位被丢弃:

智能合约安全之solidity整数溢出的原理

实验验证:

智能合约安全之solidity整数溢出的原理


智能合约安全之solidity整数溢出的原理

结果为0,发生了溢出 。

3.乘法

最后再看看乘法:


pragma solidity <=0.6.0;
 
 contract Test {
    
     function mul() pure public returns (uint256  _underflow) {
         uint256 muls = 2**255;
         return muls * 2;
     }
 }

智能合约安全之solidity整数溢出的原理


智能合约安全之solidity整数溢出的原理

导入到remix后,发现2**256 乘2 后变成了0,使用solc获取字节码后发现乘法调用的字节码是mul:

智能合约安全之solidity整数溢出的原理

在instructions.go文件中查找mul的实现:
func opMul(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
     x, y := scope.Stack.pop(), scope.Stack.peek()
     y.Mul(&x, y)
     return nil, nil
 }
该函数从栈顶取出两个元素,调用Mul完成乘法并将结果压回栈中。
进入到Mul的实现:
// Mul sets z to the product x*y
 func (z *Int) Mul(x, y *Int) *Int {
     var (
        res              Int                                        // 存放乘法结果
        carry            uint64                                     // 存放进位
        res1, res2, res3 uint64                                     // 存放一些中间变量
     )
 
     carry, res[0] = bits.Mul64(x[0], y[0])
     carry, res1 = umulHop(carry, x[1], y[0])      
     carry, res2 = umulHop(carry, x[2], y[0])
     res3 = x[3]*y[0] + carry
 
     carry, res[1] = umulHop(res1, x[0], y[1])
     carry, res2 = umulStep(res2, x[1], y[1], carry)
     res3 = res3 + x[2]*y[1] + carry
 
     carry, res[2] = umulHop(res2, x[0], y[2])
     res3 = res3 + x[1]*y[2] + carry
 
     res[3] = res3 + x[0]*y[3]
 
     return z.Set(&res)
 }

这里乘法使用同样丢弃了最高位的进位,umulHop是考虑进位的乘法:

// umulHop computes (hi * 2^64 + lo) = z +  (x * y)
 func umulHop(z, x, y uint64) (hi, lo uint64) {
     hi, lo = bits.Mul64(x, y)
     lo, carry := bits.Add64(lo, z, 0)
     hi, _ = bits.Add64(hi, 0, carry)
     return hi, lo
 }
64位乘法的实现,两个64位无符号的数相乘,结果按照两个64位的数,分成低位和高位返回。
// Mul64 returns the 128-bit product of x  and y: (hi, lo) = x * y
 // with the product bits' upper half returned in hi  and the lower
 // half returned in lo.
 //
 // This function's execution time does not depend on  the inputs.
 func Mul64(x, y uint64) (hi, lo uint64) {
     const mask32 = 1<<32 - 1
     x0 := x & mask32
     x1 := x >> 32
     y0 := y & mask32
     y1 := y >> 32
     w0 := x0 * y0
     t := x1*y0 + w0>>32
     w1 := t & mask32
     w2 := t >> 32
     w1 += x0 * y1
     hi = x1*y1 + w2 + w1>>32
     lo = x * y
     return
 }
实验验证:
使用python脚本获取2**255使用Int的表示法:

智能合约安全之solidity整数溢出的原理


智能合约安全之solidity整数溢出的原理

将这个表示法的2**255导入到go的测试代码中:

智能合约安全之solidity整数溢出的原理


智能合约安全之solidity整数溢出的原理

结果为0,与remix中得到的结果相同。

智能合约安全之solidity整数溢出的原理


  • 总结

以太坊底层使用四个64位整数串联成256位,因为所占位数写死了,所以在超过范围之后依然会发生溢出。而如使用safemath库或使用0.8.0版本以上solc并没有在源头上解决溢出问题,他们都是对结果进行溢出检测,并对发生溢出的结果进行回退。

智能合约安全之solidity整数溢出的原理

原文始发于微信公众号(山石网科安全技术研究院):智能合约安全之solidity整数溢出的原理

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年6月28日09:48:34
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   智能合约安全之solidity整数溢出的原理https://cn-sec.com/archives/1140400.html

发表评论

匿名网友 填写信息