区块链合约安全系列:如何认识及预防公链合约中的自毁攻击

Posted BSN研习社

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了区块链合约安全系列:如何认识及预防公链合约中的自毁攻击相关的知识,希望对你有一定的参考价值。

id:BSN_2021 公众号:BSN 研习社 作者:红枣科技张雪良

背景:由于公链环境下所有的信息都是共享的,智能合约相当于是完全透明化,任何人都可以调用,外加一些利益的驱动,导致引发了很多hacker的攻击。其中self destruct攻击也是常见的攻击方式之一。

目标:将目标合约瘫痪掉,无法做正常的业务,从而认识以及预防自毁攻击漏洞。

对象:适用于用Solidity语言开发的智能合约,例如BSN中的武汉链(基于ETH)和泰安链(基于 fisco bcos)上运行的智能合约。

前言

在进入正题之前,我先带大家从基础知识点开始一点点深入到怎么攻击以及预防。好,废话不多话,先看下selfdestruct的官方解释:

selfdestruct(address payable recipient)

Destroy the current contract, sending its funds to the given Address and end execution

理解起来也很简单,就是说合约销毁的时候,可以把ether转到指定的地址。

常见的用法 : 当我们的合约有漏洞或者业务变更的变化时,需要把它销毁掉,以避免造成更多的影响。此时可以在合约里提供一个销毁方法;

示例如下:

pragma solidity >=0.7.0 <0.9.0;
contract Destructible 
    address payable owner;
    constructor() 
       owner = payable(msg.sender); 
    
   
     // 销毁合约
    function destroy() public 
        if (msg.sender == owner)
           selfdestruct(owner);
        
    

当需要销毁时,合约的owner可以调用destory()方法进行合约销毁。

那接下来,我们正式进入主题,如何使用self-destory进行攻击。

攻击演示

1. 合约示例

演示需要用到的两个合约,一个模拟业务合约,一个为攻击合约。

业务合约: 一个简单的游戏,每个用户每次可以存放1ether到合约里,等到第7次存放的用户将成为赢家,可以把7ether提到自己账户里。

攻击合约: 编写了一个合约销毁的方法,即本合约销毁时会发送ether到指定的合约。

攻击逻辑: 调用攻击合约的attack方法,使得上述的业务合约余额超过7。

示例如下:

pragma solidity >=0.7.0 <0.9.0;

// 业务合约
contract EtherGame 
    uint public targetAmount = 7 ether;
    address public winner;
    // 充值ether
    function deposit() public payable 
        require(msg.value == 1 ether, "You can only send 1 Ether");

        uint balance = address(this).balance;
        require(balance <= targetAmount, "Game is over");

        if (balance == targetAmount) 
            winner = msg.sender;
        
    
    // 提取ether
    function claimReward() public 
        require(msg.sender == winner, "Not winner");
        winner = address(0);
        (bool sent, ) = msg.sender.callvalue: address(this).balance("");
        require(sent, "Failed to send Ether");
    
    // 查询当前余额
    function balanceOf() public view returns (uint)
        return address(this).balance;
    

// 攻击合约
contract Attack 

    EtherGame etherGame;
    constructor(EtherGame _etherGame) 
        etherGame = EtherGame(_etherGame);
    
    
    // 合约销毁和发送ether
    function attack() public payable 
        // 发送ether到指定的业务合约
        selfdestruct(payable(address(etherGame)));
    


2. 合约部署

老规矩,我们使用remix进行部署测试。

部署成功后,截图如下:

3. 正常业务操作

准备两个账户A和B,分别为100ether。 具体操作流程为:A调用5次存放5ether,B调用2次存放2ether,B将成为winner,然后提取7ether。

两个账户合计调用7次后,查询余额以及winner信息,截图如下:

B账户提取ether,结果截图如下:

上面的图中可以看到,B账户成功的提取了合约里的7 ether。

4. 攻击操作

我们还是用上面的两个账户账户A和B。 具体操作流程为:A调用5次存放5ether,B调用攻击合约的attack方法并发送3ether。

上图可以发现业务合约当前余额为8 ether。

上图可以看到攻击合约的owner变为0x0地址。

到此,业务合约已被攻击,即业务无法正常进行,不能存放以及提取。

下面,我们进行测试depoit和claimReward的方法调用,结果信息截图如下:

解决方案

最后,给大家推荐一个常用的方案:将全局address(this).balance改为变量统计进入deposit逻辑的ether数量。

最终代码如下所示:


pragma solidity >=0.7.0 <0.9.0;

// 业务合约
contract EtherGame 
    uint public targetAmount = 7 ether;
    address public winner;
    uint public balance;// 记录ether数量
    // 充值ether
    function deposit() public payable 
        require(msg.value == 1 ether, "You can only send 1 Ether");

        balance += msg.value;
        require(balance <= targetAmount, "Game is over");

        if (balance == targetAmount) 
            winner = msg.sender;
        
    
    // 提取ether
    function claimReward() public 
        require(msg.sender == winner, "Not winner");
        winner = address(0);
        (bool sent, ) = msg.sender.callvalue: balance("");
        require(sent, "Failed to send Ether");
        
    
    // 查询当前余额
    function balanceOf() public view returns (uint)
        return address(this).balance;
    


今天的讲解到此结束,感谢大家的阅读,如果你有其他的想法或者建议,欢迎一块交流。

本文由 mdnice 多平台发布

Flow区块链NFT开发及部署教程Cadence合约

Flow区块链是著名的加密猫团队特别针对NFT应用开发的新的公链,其目的是解决以太坊在NFT开发应用上存在的诸多问题,目前已经得到NBA、UFC等多个大牌厂商的支持。在这个教程中我们将学习如何创建Cadence智能合约并在Flow区块链上发行NFT。

区块链开发教程链接: 以太坊 | 比特币 | EOS | Tendermint | Hyperledger Fabric | Omni/USDT | Ripple | Tron

1、Flow开发环境安装

首先我们需要安装Flow CLI,详细的安装说明可以查看Flow官方文档, 在这里我们仅列出几种操作系统下的安装命令:

  • macOS
brew install flow-cli
  • Linux
sh -ci “$(curl -fsSL https://storage.googleapis.com/flow-cli/install.sh)"
  • Windows
iex “& { $(irm ‘https://storage.googleapis.com/flow-cli/install.ps1') }”

我们需要将文件保存到IPFS,为此我们使用Pinata。你可以注册一个免费账号,然后点击这里获取API访问密钥。在下一教程中我们将使用Pinata API。

我们还需要安装NodeJS和一个支持Flow智能合约代码语法高亮的文本编辑器,Flow的智能合约使用Cadence语言编写。Visual Studio Code提供Cadence高亮扩展

下面让我们创建项目目录:

mkdir pinata-party

进入该目录,然后执行如下命令初始化一个新的Flow项目:

cd pinata-party
flow project init

现在使用你喜欢的代码编辑器(比如加载了Cadence扩展的Visual Studio Code)打开项目文件。

可以看到flow.json,我们接下来就会用到。首先创建一个cadence文件夹,并在该文件夹内创建contracts文件夹,最后在contracts文件夹内创建合约文件PinataPartyContract.cdc。

在我们继续之前,需要指出的是,下面我们的操作将在Flow区块链仿真器上完成,要将项目部署到测试链或主链,只需要更新flow.json中的配置参数即可。现在让我们在flow.json中设置仿真环境以便接下来进行智能合约代码的编写:

"contracts": {
     "PinataPartyContract": "./cadence/contracts/PinataPartyContract.cdc"
}

接下来更新flow.json中的deployments对象:

"deployments": {
     "emulator": {
          "emulator-account": ["PinataPartyContract"]
     }
}

上面的设置用来告诉Flow CLI使用仿真器来部署合约,在配置中同时也声明了账号以及我们接下来要开发的合约。

2、编写Cadence智能合约

Flow提供了一个创建NFT智能合约的出色的教程,这是一个很好的参考,但是Flow自己同时指出,他们还没有解决NFT元数据的问题。Flow团队希望在链上存储元数据。这是一个好主意,他们肯定会给出一个合理的解决方案。然而我们现在就希望铸造一些带元数据的通证,我们也希望这些元数据关联到NFT。元数据只是一个部分,我们也希望指向通证最终代表的媒体。

如果你熟悉以太坊区块链上的NFT,你可能知道这些NFT支持的资产存储在传统数据库和云主机上。在大多数情况下这没太大问题。我们之前讨论过可寻址内容的天才思路,以及在传统云平台上存储区块链附加数据的问题,这指向了两个要点:

  • 资产应当可验证
  • 应当易于转移维护责任

IPFS解决了这些问题,Pinata则提供了一个简单的接入层方面我们将内容长期锁定在IPFS上。这就是我们希望的解决方案,对吗?我们希望确保可以证明NFT的所有权,提供NFT的数据,并确保我们可以控制IPFS上的底层资产。

综合这些想法,现在我们来编写智能合约,该合约将负责铸造NFT、关联元数据并确保元数据指向保存在IPFS上的底层资产。

打开PinataPartyContract.cdc文件,输入如下代码:

pub contract PinataPartyContract {
  pub resource NFT {
    pub let id: UInt64
    init(initID: UInt64) {
      self.id = initID
    }
  }
}

第一步是定义我们的合约,并在合约内创建一个资源。资源存储在用户账号中,对其的访问需要满足访问控制条件。NFT必须是唯一标识的,id属性就是用来标识我们的通证。

接下来我们需要创建一个资源接口以便定义可以为其他人可用的能力,例如合约主之外的其他人。

pub resource interface NFTReceiver {
  pub fun deposit(token: @NFT, metadata: {String : String})
  pub fun getIDs(): [UInt64]
  pub fun idExists(id: UInt64): Bool
  pub fun getMetadata(id: UInt64) : {String : String}
}

将上述代码放在NFT资源代码下面。这个NFTReceiver接口定义了具备访问权限的人可以调用以下方法:

  • deposit
  • getIDs
  • idExists
  • getMetadata

接下来我们需要实现通证集,可以将其视为保存用户全部的NFT的钱包。

pub resource Collection: NFTReceiver {
    pub var ownedNFTs: @{UInt64: NFT}
    pub var metadataObjs: {UInt64: { String : String }}

    init () {
        self.ownedNFTs <- {}
        self.metadataObjs = {}
    }

    pub fun withdraw(withdrawID: UInt64): @NFT {
        let token <- self.ownedNFTs.remove(key: withdrawID)!

        return <-token
    }

    pub fun deposit(token: @NFT, metadata: {String : String}) {
        self.metadataObjs[token.id] = metadata
        self.ownedNFTs[token.id] <-! token
    }

    pub fun idExists(id: UInt64): Bool {
        return self.ownedNFTs[id] != nil
    }

    pub fun getIDs(): [UInt64] {
        return self.ownedNFTs.keys
    }

    pub fun updateMetadata(id: UInt64, metadata: {String: String}) {
        self.metadataObjs[id] = metadata
    }

    pub fun getMetadata(id: UInt64): {String : String} {
        return self.metadataObjs[id]!
    }

    destroy() {
        destroy self.ownedNFTs
    }
  }

上面代码中包含了很多内容,不过我们将很快理解其中的含义。

首先,我们有一个名为onwedNFTs的变量,它负责跟踪一个用户持有的全部NFT。

接下来,名为metadataObjs的变量用来保存每个NFT的元数据映射,这个变量将通证id映射到关联的元数据,因此在设置之前我们需要先有通证id。

然后我们初始化变量。在Flow的资源中定义的变量需要进行初始化。

最后我们实现NFT集资源的全部函数。注意这些函数并非都向外部公开。别忘了之前我们在NFTReceiver资源接口中定义了任何人可以访问的函数。

我还想特别指出deposit函数。因为我们继承了默认的Flow NFT合约以便包含metadataObjs映射,我们也扩展默认的deposit函数来接收额外的metadata参数。为什么要这么做?我们需要确保只有通证的铸造人可以添加通证的元数据。为保持其私有化,我们将元数据的初始添加限制在铸造环节。

快要完成Cadence合约代码了,在NFT集资源下面,添加如下内容:

pub fun createEmptyCollection(): @Collection {
    return <- create Collection()
}

pub resource NFTMinter {
    pub var idCount: UInt64

    init() {
        self.idCount = 1
    }

    pub fun mintNFT(): @NFT {
        var newNFT <- create NFT(initID: self.idCount)

        self.idCount = self.idCount + 1 as UInt64

        return <-newNFT
    }
}

首先,我们定义一个函数负责创建空的NFT集合。这是初次访问我们合约的用户获得存储位置的实现代码。

之后,我们再创建一个资源,这很重要,否则将无法铸造通证。NFTMinter资源包含递增的idCount以确保我们的NFT的id不会重复。代码还定义了一个创建NFT的函数。

在NFTMinter资源下面,添加主合约初始化代码:

init() {
      self.account.save(<-self.createEmptyCollection(), to: /storage/NFTCollection)
      self.account.link<&{NFTReceiver}>(/public/NFTReceiver, target: /storage/NFTCollection)
      self.account.save(<-create NFTMinter(), to: /storage/NFTMinter)
}

这个初始化函数仅在合约部署时调用。它主要完成以下工作:

  1. 创建一个空的NFT集合,以便合约主可以铸造并持有NFT
  2. 集合资源发布到引用NFTReceiver接口的公共位置,这表明了NFTReceiver定义的方法可以被外部访问
  3. NFTMinter资源保存在合约创建者的账号存储中,这意味着只有合约创建者可以铸造通证

可以在这里查看完整的合约代码。

3、在Flow区块链部署Cadence合约

现在我们有了合约,在部署之前,最好使用Flow Playground进行测试。前往Flow Playground并打开侧栏的第一个账号,用我们的合约代码替换示例合约,然后点击Deploy。如果一切顺利,你应该在日志窗口看到类似下面这样的内容:

16:48:55 Deployment Deployed Contract To: 0x01

现在我们准备将合约部署到本地运行的仿真器上了。在命令行运行如下命令:

flow project start-emulator

仿真器运行并且flow.json配置正确的话,我们可以执行如下命令部署合约:

flow project deploy

如果一切顺利的话,应当可以看到下面这样的输出:

Deploying 1 contracts for accounts: emulator-account
PinataPartyContract -> 0xf8d6e0586b0a20c7

现在我们在Flow仿真器上有了一个激活的合约,接下来该铸造NFT了。

4、使用Cadence脚本铸造NFT

在下一个教程中,我们将创建一个app和用户界面来让整个铸造过程更加用户友好。不过在这个教程中,为了说明元数据与NFT在Flow上如何运作,我们将使用Cadence脚本和命令行来铸造NFT。

让我们在项目目录下创建一个新的目录transactions,然后在该目录中创建一个文件MintPinataParty.cdc。

为了编写交易,我们需要使用一个文件来引用提供给NFT的元数据。为此我们将通过Pinata上传文件到IPFS。点击这里上传你选好的视频文件。

上传文件后你会得到一个IPFS哈希(通常被称为内容标识符或CID)。拷贝下来这个哈希,因为我们在铸造过程中需要用到它。

现在,在你的MintPinataParty.cdc文件中,添加如下内容:

import PinataPartyContract from 0xf8d6e0586b0a20c7

transaction {
  let receiverRef: &{PinataPartyContract.NFTReceiver}
  let minterRef: &PinataPartyContract.NFTMinter

  prepare(acct: AuthAccount) {
      self.receiverRef = acct.getCapability<&{PinataPartyContract.NFTReceiver}>(/public/NFTReceiver)
          .borrow()
          ?? panic("Could not borrow receiver reference")        
      
      self.minterRef = acct.borrow<&PinataPartyContract.NFTMinter>(from: /storage/NFTMinter)
          ?? panic("could not borrow minter reference")
  }

  execute {
      let metadata : {String : String} = {
          "name": "The Big Swing",
          "swing_velocity": "29", 
          "swing_angle": "45", 
          "rating": "5",
          "uri": "ipfs://QmRZdc3mAMXpv6Akz9Ekp1y4vDSjazTx2dCQRkxVy1yUj6"
      }
      let newNFT <- self.minterRef.mintNFT()
  
      self.receiverRef.deposit(token: <-newNFT, metadata: metadata)

      log("NFT Minted and deposited to Account 2's Collection")
  }
}

这个交易很简单,让我们解释一下。

首先你会注意到开头的import语句。你可能还记得我们部署合约时收到一个账号,这就是我们需要引用的。因此将0xf8d6e0586b0a20c7替换为部署的账号地址。

接下来我们定义交易。这里的内容与我们期望执行的交易有关。

在我们的交易中做的第一件事,就是定义两个引用变量receiverRef和minterRef。在这个案例中我们同时是NFT的接收方和铸造方。这两个变量引用我们在合约中创建的资源。如果执行交易的人没有访问资源的权限,交易将失败。

接下来我们定义了一个prepare函数。这个函数输入账号信息并进行一些验证。我们尝试借用在NFTMinter和NFTReceiver上定义的能力,如果执行人没有权限,就会在这里失败。

最后是我们的execute函数。这个函数中我们为NFT构造元数据,铸造NFT,关联元数据,将NFT存入我们的账号。可以看到我创建了一个metadata变量,在其中添加了关于我们通证的信息。由于我们希望尽可能复制NBA Top Shot,在元数据中我还定义了一些统计信息,当然你可以输入任何有意义的信息。

你会注意到,在metadata中我也定义了一个uri属性,这指向保存我们资产文件的IPFS哈希。在这个示例中就是我们上传的视频,你可以将其替换为你得到的哈希。

出于一些考虑我们在哈希之前附加ipfs://前缀,这样在IPFS桌面客户端和浏览器扩展中就可以访问,也可以在Brave浏览器中直接访问,Brave提供了IPFS的原生支持。

我们调用mintNFT函数,它负责创建通证。接下来需要调用deposit函数以便将其放入账号。

最后是简单的日志输出。

现在我们基本上准备好发送交易、铸造NFT了。但是首先我们需要准备账号。在命令行进入项目根目录,创建一个新的私钥用于签名。

flow keys generate

上面的命令会生成公钥和私钥。记得保护好你的私钥!

我们需要私钥来签名交易,因此将其拷贝到flow.json文件中。我们还需要指定签名算法。最后看起来是这样:

"accounts": {
  "emulator-account": {
     "address": "YOUR ACCOUNT ADDRESS",
     "privateKey": "YOUR PRIVATE KEY",
     "chain": "flow-emulator",
     "sigAlgorithm": "ECDSA_P256",
     "hashAlgorithm": "SHA3_256"
  }
},

如果你要将项目放到github或其他远端git仓库,确保别包含你的私钥。你可以使用.gitignore避免将flow.json公开。即使我们只是使用本地仿真器,也要记得保护你的私钥。

现在发送交易,只需执行如下命令:

flow transactions send --code ./transactions/MintPinataParty.cdc --signer emulator-account

如果一切顺利,看起来应该是这样:

Getting information for account with address 0xf8d6e0586b0a20c7 ...
Submitting transaction with ID 4a79102747a450f65b6aab06a77161af196c3f7151b2400b3b3d09ade3b69823 ...
Successfully submitted transaction with ID 4a79102747a450f65b6aab06a77161af196c3f7151b2400b3b3d09ade3b69823

5、使用Cadence脚本验证NFT

现在要做的最后一件事,是验证通证并提取元数据。为此我们将编写一个简单的脚本并在命令行调用。

在项目根目录创建一个新的scripts文件夹,并在其中创建文件CheckTokenMetadata.cdc。 在文件中添加如下内容:

import PinataPartyContract from 0xf8d6e0586b0a20c7

pub fun main() : {String : String} {
    let nftOwner = getAccount(0xf8d6e0586b0a20c7)
    // log("NFT Owner")    
    let capability = nftOwner.getCapability<&{PinataPartyContract.NFTReceiver}>(/public/NFTReceiver)

    let receiverRef = capability.borrow()
        ?? panic("Could not borrow the receiver reference")

    return receiverRef.getMetadata(id: 1)
}

上面的脚本可以视为对以太坊智能合约的只读方法的调用,免费并且只是返回数据。

在我们的脚本中,从部署地址导入合约,然后定义个main函数,并在此函数内定义三个变量:

  • nftOwner: 持有NFt的账号
  • capability: 资源能力
  • receiverRef: 接收方

现在调用脚本:

flow scripts execute ./scripts/CheckTokenMetadata.cdc

应该可以看到如下类似内容:

{"name": "The Big Swing", "swing_velocity": "29", "swing_angle": "45", "rating": "5", "uri": "ipfs://QmRZdc3mAMXpv6Akz9Ekp1y4vDSjazTx2dCQRkxVy1yUj6"}

恭喜!你已经在Flow区块链成功部署Cadence合约、铸造NFT并将NFT关联的元数据保存在IPFS!


原文链接:Flow区块链NFT开发教程 — 汇智网

以上是关于区块链合约安全系列:如何认识及预防公链合约中的自毁攻击的主要内容,如果未能解决你的问题,请参考以下文章

学习区块链开发是学习go语言、hyper ledger fabric比较好、还是以太坊智能合约比较好或者公链开发?

区块链DAPP 开发入门 代码实现 场景应用

公链新宠Move合约如何快速入手

区块链及智能合约

Web3 开发系列教程—创建你的第一个智能合约什么是 Gas,它是如何使用的?

智能合约体系与功能服务的融合和革新?