-
前言
整数溢出漏洞在信息安全的各个方面都大量存在,而在solidity编写的智能合约中更是重量级的存在。在使用solidity合约的历史中,出现了多次因整数溢出而出现的严重事故,大多数都是利用溢出凭空取出超大量的代币,并在市场抛售,导致代币价值归零。
在solidity中,变量支持的整数类型长度以8递增,从uint8到uint256,以及int8到int256。在EVM中储存一个数所占的位数是固定,当存储的数字长度超出最大值时会导致进位,使所有1翻转成0。
在solidity中,溢出分为加法溢出、减法溢出、乘法溢出,其直接原因都是因为运算结果超出了范围。
-
分析
1.减法
contract Test { function sub(uint a, uint b) public pure returns (uint) { return a - b; } } |
这里定义了Test合约和sub方法,该方法输入两个uint类型的数并返回他们的差值,将该合约部署到remix上:
这里输入a为0,b为1,0减去1后,发生下溢出,结果为115792089237316195423570985008687907853269984665640564039457584007913129639935是一个超大数。
tag_6中看到与减法在EVM中的字节码为sub
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方法的实现:
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行:
// 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位相减是如何实现的:
// 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函数,我们注意到在计算最高位的结果时,该语句将函数返回的借位值丢弃了,而这会造成什么后果呢?对于十进制的减法,我们列一个竖式:
用0减去1,类比该函数里算法的实现,最低位由0减1,产生借位,并得出改为的值为9。之后,再下一位的0减去上一位借位的1,再次发生借位并算出该位的值是9,以此类推,直到最高位,在发生借位,并算出该位结果为9后,借位的一被丢弃,并导致最终的结果出现错误 。
实验验证:
我们定义两个Int类型的数num1,num2,其值分别等效为0和1,使用Sub方法将num1减去num2
结果发生了溢出。这个结果是用四个uint64串联而成的,我们写一个python脚本,将其转化为十进制类型:
Num = "" for num in num_list[::-1]: Num += bin(num)[2::] print(int(Num, 2)) |
2.加法
再来看看加法:
contract Test { function add() pure public returns (uint256 _overflow) { uint256 max = 2**256 - 1; return max + 1; } } |
x, y := scope.Stack.pop(), scope.Stack.peek() y.Add(&x, y) return nil, nil } |
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函数:
// 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函数中,依次从低位向高位累计,同样的,最高位的进位被丢弃:
最后再看看乘法:
contract Test { function mul() pure public returns (uint256 _underflow) { uint256 muls = 2**255; return muls * 2; } } |
x, y := scope.Stack.pop(), scope.Stack.peek() y.Mul(&x, y) return nil, nil } |
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是考虑进位的乘法:
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 } |
// 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 } |
-
总结
原文始发于微信公众号(山石网科安全技术研究院):智能合约安全之solidity整数溢出的原理
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论