Ethereum智能合约静态分析(下)

Ethereum智能合约静态分析(上)

https://blog.csdn.net/weixin_43405220/article/details/100180472

第五章 常见的智能合约漏洞以及检测方法

在本章中,我们首先会介绍智能合约中常见的漏洞,之后会分析检测这些漏洞的方法。

5.1 智能合约中常见的漏洞

5.1.1 整数溢出漏洞

我们以8位无符号整数为例分析溢出产生的原因,如下图所示,最大的8位无符号整数是255,如果此时再加1就会变为0。
在这里插入图片描述
Solidity语言支持从uint8uint256uint256的取值范围是0到2256-1。如果某个uint256变量的值为2256-1,那么这个变量再加1就会发生溢出,同时该变量的值变为0。

pragma solidity >=0.4.22 <0.6.0;
contract Test {
    function overflow() public pure returns (uint256 _overflow) {
        uint256 max = 2**256-1;
        return max + 1;
    }
}

上面的合约代码中,变量max的值为2256-1,是uint256所能表示的最大整数,如果再加1就会产生溢出,max的值变为0。

5.1.2 重入漏洞

当智能合约向另一个智能合约转账时,后者的fallback函数会被调用。如果fallback函数中存在恶意代码,那么恶意代码会被执行,这就是重入漏洞产生的前提。那么重入漏洞在什么情况下会发生呢,下面我们以一个存在重入漏洞的智能合约为例进行分析。

pragma solidity >=0.4.22 <0.6.0;
contract Bank {
    address owner;
    mapping (address => uint256) balances;
    constructor() public payable{ 
        owner = msg.sender; 
    }
    function deposit() public payable { 
        balances[msg.sender] += msg.value;
    }
    function withdraw(address receiver, uint256 amount) public{
        require(balances[msg.sender] > amount);
        require(address(this).balance > amount);
        // 使用 call.value()()进行ether转币时,没有Gas限制
        receiver.call.value(amount)();
        balances[msg.sender] -= amount;
    }
    function balanceOf(address addr) public view returns (uint256) { 
        return balances[addr]; 
    }
}
contract Attack {
    address owner;
    address victim;
    constructor() public payable { 
        owner = msg.sender;
    }
    function setVictim(address target) public{
        victim = target;
    }
    function step1(uint256 amount) public  payable{
        if (address(this).balance > amount) {
            victim.call.value(amount)(bytes4(keccak256("deposit()")));
        }
    }
    function step2(uint256 amount) public{
        victim.call(bytes4(keccak256("withdraw(address,uint256)")), this,amount);
    }
    // selfdestruct, send all balance to owner
    function stopAttack() public{
        selfdestruct(owner);
    }
    function startAttack(uint256 amount) public{
        step1(amount);
        step2(amount / 2);
    }
    function () public payable {
        if (msg.sender == victim) {
            // 再次尝试调用Bank合约的withdraw函数,递归转币
            victim.call(bytes4(keccak256("withdraw(address,uint256)")), this,msg.value);
        }
    }
}

在上面的代码中,智能合约Bank是存在重入漏洞的合约,其内部的withdraw()方法使用了call方法进行转账,使用该方法转账时没有gas限制。 智能合约Attack是个恶意合约,用来对存在重入的智能合约Bank进行攻击。攻击流程如下:

  • Attack先给Bank转币
  • Bank在其内部的账本balances中记录Attack转币的信息
  • Attack要求Bank退币
  • Bank先退币再修改账本balances

问题就出在Bank是先退币再去修改账本balances。因为Bank退币的时候,会触发Attack的fallback函数,而Attack的fallback函数中会再次执行退币操作,如此递归下去,Bank没有机会进行修改账本的操作,最后导致Attack会多次收到退币。

5.2 漏洞的检测方法

5.2.1 整数溢出漏洞的检测

过约束求解可以很容易的发现智能合约中的整数溢出漏洞,下面我们就通过一个具体的例子一步步的分析。

首先对5.1.1节中的智能合约进行反编译,得到的部分反编译代码如下:

00048: JUMPDEST
00049: PUSH1 0x00
0004b: DUP1
0004c: PUSH32 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
0006d: SWAP1
0006e: POP
0006f: PUSH1 0x01
00071: DUP2
00072: ADD
00073: SWAP2
00074: POP
00075: POP
00076: SWAP1
00077: JUMP

这段反编译后的代码对应的是智能合约中的overflow函数,第00072行的ADD指令对应的是函数中max + 1这行代码。ADD指令会把栈顶的两个值出栈,相加后把结果压入栈顶。下面我们就通过一段伪代码来演示如何检测整数溢出漏洞:

def checkOverflow():
    first = stack.pop(0)
    second = stack.pop(0)
    first = BitVecVal(first, 256)
    second = BitVecVal(second, 256)
    computed = first + second
    solver.add(UGT(first, computed))
    if check_sat(solver) == sat:
        print "have overflow"

我们先把栈顶的两个值出栈,然后使用z3中BitVecVal()函数的把这两个值转变成位向量常量,接着计算两个位向量常量相加的结果,最后构建表达式UGT(first, computed)来判断加数是否大于相加的结果,如果该表达式有解则说明会发生整数溢出。

5.2.2 重入漏洞的检测

在分析重入漏洞之前,我们先来总结在智能合约中用于转账的方法:

  • address.transfer(amount): 当发送失败时会抛出异常,只会传递2300Gas供调用,可以防止重入漏洞
  • address.send(amount): 当发送失败时会返回false,只会传递2300Gas供调用,可以防止重入漏洞
  • address.gas(gas_value).call.value(amount)(): 当发送失败时会返回false,传递所有可用Gas进行调用(可通过 gas(gas_value) 进行限制),不能有效防止重入

通过以上对比不难发现,transfer(amount)send(amount)限制Gas最多为2300,使用这两个方法转账可以有效地防止重入漏洞。call.value(amount)()默认不限制Gas的使用,这就会很容易导致重入漏洞的产生。既然call指令是产生重入漏洞的原因所在,那么接下来我们就详细分析这条指令。
call指令有七个参数,每个参数的含义如下所示:

call(gas, address, value, in, insize, out, outsize)

  • 第一个参数是指定的gas限制,如果不指定该参数,默认不限制。
  • 第二个参数是接收转账的地址
  • 第三个参数是转账的金额
  • 第四个参数是输入给call指令的数据在memory中的起始地址
  • 第五个参数是输入的数据的长度
  • 第六个参数是call指令输出的数据在memory中的起始地址
  • 第七个参数是call指令输出的数据的长度

通过以上的分析,总结下来我们可以从以下两个维度去检测重入漏洞:

  • 判断call指令第一个参数的值,如果没有设置gas限制,那么就有产生重入漏洞的风险
  • 检查call指令之后,是否还有其他的操作。

第二个维度中提到的call指令之后是否还有其他操作,是如何可以检测到重入漏洞的呢?
接下来我们就详细分析下。在5.1.2节中的智能合约Bank是存在重入漏洞的,根本原因就是使用call指令进行转账没有设置Gas限制,同时在withdraw方法中先退币再去修改账本balances,关键代码如下:

receiver.call.value(amount)();
balances[msg.sender] -= amount;

执行call指令的时候,会触发Attack中的fallback函数,而Attack的fallback函数中会再次执行退币操作,如此递归下去,导致Bank无法执行接下来的修改账本balances的操作。此时如果我们对代码做出如下调整,先修改账本balances,之后再去调用call指令,虽然也还会触发Attack中的fallback函数,Attack的fallback函数中也还会再次执行退币操作,但是每次退币操作都是先修改账本balances,所以Attack只能得到自己之前存放在Bank中的币,重入漏洞不会发生。

balances[msg.sender] -= amount;
receiver.call.value(amount)();

总结

本文的第一章介绍了智能合约编译环境的搭建以及编译器的使用,第二章讲解了常用的汇编指令并且对反编译后的代码进行了逐行的分析。前两章都是基本的准备工作,从第三章开始,我们使用之前的反编译代码,构建了完整的控制流图。第四章中我们介绍了z3的用法以及如何把控制流图中的基本块中的指令用z3转换成数学表达式。第五章中我们通过整数溢出和重入漏洞的案例,详细分析了如何在约束求解的过程中检测智能合约中的漏洞。

原文链接:http://blogs.360.cn/post/staticAnalysis_of_smartContract.html

猜你喜欢

转载自blog.csdn.net/weixin_43405220/article/details/100553931