学院 安全深度 文章

链上卫士:Paradigm CTF 2022 题目浅析—Vanity

2022.09.30 0xc730

题目背景

在Paradigm CTF 2022中有着这样一道DeFi金融赛题:

题目提示 “Just think of the gas savings!” 看起来没有什么意义。

题目分析

1)首先看下Setup.sol,要求调用Challenge合约的bestScore函数,且其返回值需要不小于16。

//Setup.sol
pragma solidity 0.7.6;

import "./Challenge.sol";

contract Setup {
    Challenge public immutable challenge;

    constructor() {
        challenge = new Challenge();
    }

    function isSolved() external view returns (bool) {
        return challenge.bestScore() >= 16;
    }
}

2)接下来看下Challenge.sol,有两个external的solve函数,最后都会调用private的solve函数,从这个private的solve函数逻辑来看,将输入的address转化为20个bytes的数组,判断每个字节等于0x00的个数,并记录最大值到bestScore变量。因此,解题的目标是找到一个地址,要求其中包含0x00的字节数目不小于16个。

详细分析下两个external的solve函数,第一个直接将msg.sender作为参数调用private的solve函数,如需满足题目要求,需要msg.sender为包含不少于16个字节0x00的地址,这个基本上是行不通的。第二个solve函数会调用SignatureChecker.isValidSignatureNow(signer, MAGIC, signature),通过该函数检测后,会以signer为参数调用private的solve函数。

// Challenge.sol

pragma solidity 0.7.6;

import "./SignatureChecker.sol";

contract Challenge {
    bytes32 private immutable MAGIC = keccak256(abi.encodePacked("CHALLENGE_MAGIC"));

    uint public bestScore;

    function solve() external {
        solve(msg.sender);
    }

    function solve(address signer, bytes memory signature) external {
        require(SignatureChecker.isValidSignatureNow(signer, MAGIC, signature), "Challenge/invalidSignature");

        solve(signer);
    }

    function solve(address who) private {
        uint score = 0;

        for (uint i = 0; i < 20; i++) if (bytes20(who)[i] == 0) score++;

        if (score > bestScore) bestScore = score;
    }
}

3)继续分析SignatureChecker.sol文件,在isValidSignatureNow函数中,会首先通过ecrecover进行签名校验,如果想校验成功,相当于使用signer地址,对hash进行签名,得到signature,还是需要找到包含至少16个0x00字节的地址。再看第二部分检测,是对signer进行staticcall,要求返回值前四个字节与

isValidSignatureNow函数selector相同。

到这一步,解题思路比较明确了,即signer地址使用内置合约地址,然后要求该内置合约的返回值满足前四个字节与isValidSignatureNow函数selector相同即可。

//SignatureChecker.sol
library SignatureChecker {
    function isValidSignatureNow(
        address signer,
        bytes32 hash,
        bytes memory signature
    ) internal view returns (bool) {
        (address recovered, ECDSA.RecoverError error) = ECDSA.tryRecover(hash, signature);
        if (error == ECDSA.RecoverError.NoError && recovered == signer) {
            return true;
        }

        (bool success, bytes memory result) = signer.staticcall(
            abi.encodeWithSelector(IERC1271.isValidSignature.selector, hash, signature)
        );
        return (success && result.length == 32 && abi.decode(result, (bytes4)) == IERC1271.isValidSignature.selector);
    }
}

4)查看备选的内置合约,相关链接:https://www.evm.codes/precompiled

内置合约0x02,和0x03都满足要求,即对输入data进行哈希,得到返回值。0x02合约对应的SHA256更为常见些,我们选取0x02合约作为signer地址,即0x0000000000000000000000000000000000000002

5)再整理下整体解题思路,准备下各个环节用到的参数。

首先SignatureChecker.isValidSignatureNow(signer, MAGIC, signature)使用到对MAGIC为

keccak256(abi.encodePacked(“CHALLENGE_MAGIC”))结果,使用cast工具,得到哈希值为:0x19bb34e293bba96bf0caeea54cdd3d2dad7fdf44cbea855173fa84534fcfb528

cast keccak "CHALLENGE_MAGIC"
0x19bb34e293bba96bf0caeea54cdd3d2dad7fdf44cbea855173fa84534fcfb528

在计算下IERC1271.isValidSignature.selector的结果,由如下代码

interface IERC1271 {
    function isValidSignature(bytes32 hash, bytes memory signature) external view returns (bytes4 magicValue);
}

可使用cast工具得到函数selector结果为:0x1626ba7e

cast sig "isValidSignature(bytes32,bytes)" 
0x1626ba7e

为得到signer.staticcall的参数,我们使用cast abi-encode对hash和signature进行编码,此处signature我们假设是长度是32字节,先临时使用全零值占位,得到结果如下:

cast abi-encode "func(bytes32,bytes)" 0x19bb34e293bba96bf0caeea54cdd3d2dad7fdf44cbea855173fa84534fcfb528 0000000000000000000000000000000000000000000000000000000000000000
0x19bb34e293bba96bf0caeea54cdd3d2dad7fdf44cbea855173fa84534fcfb528000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000

对上面abi-encode结果进行分析如下

0x
19bb34e293bba96bf0caeea54cdd3d2dad7fdf44cbea855173fa84534fcfb528 //0x00 hash参数,为bytes32,
0000000000000000000000000000000000000000000000000000000000000040 //0x20 signature参数的起始地址,值为0x40
0000000000000000000000000000000000000000000000000000000000000020 //0x40 signature参数的长度,值为0x20
0000000000000000000000000000000000000000000000000000000000000000 //0x60 signature的值,此处使用全零值占位

此时可以得到signer.staticcall的参数为isValidSignature函数selector,加上abi-encode结果,即

0x1626ba7e19bb34e293bba96bf0caeea54cdd3d2dad7fdf44cbea855173fa84534fcfb528000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000

我们需要将32字节长度的signature的数值从0开始向上遍历,计算其sha256哈希结果,结果前四个字节等于0x1626ba7e时,我们就找到了满足要求的signature参数。

解题步骤

按题目分析思路编写代码,查找满足要求的signature参数,我们使用JavaScript代码实现。代码使用除signature部分作为baseStr,然后对32字节的signature从0开始递增遍历,并将结果拼接到base字符串,然后进行哈希计算,并取前4个字节进行判断,满足要求则停止循环。

const crypto=require('crypto');
  
function decimalToHex(d, padding) {
    var hex = Number(d).toString(16);
    padding = typeof (padding) === "undefined" || padding === null ? padding = 2 : padding;

    while (hex.length < padding) {
        hex = "0" + hex;
    }
    return hex;
}
  
var baseStr = "1626ba7e19bb34e293bba96bf0caeea54cdd3d2dad7fdf44cbea855173fa84534fcfb52800000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000020"

var max = 2**32;
for(i=0; i< max; i++) {
    var obj=crypto.createHash('sha256');
    var nonceStr = decimalToHex(i, 64);
    var str = baseStr + nonceStr;
    var buf = Buffer.from(str, "hex")
    obj.update(buf)

    var res = obj.digest('hex');

    if (res.substr(0, 8) == '1626ba7e') {
        console.log('find', i, nonceStr);
        break;
    }

    if (i % 1000000 == 0) {
        console.log(i, nonceStr);
    }
}

程序成功找到满足要求的signature后log输出如下:

find 3341776893 00000000000000000000000000000000000000000000000000000000c72f77fd

将signer:0x0000000000000000000000000000000000000002

和signature:0x00000000000000000000000000000000000000000000000000000000c72f77fd

作为参数调用Challenge的solve函数,执行成功后,调用Setup合约的isSolved,返回值为true。

免责声明:OKLink学院仅提供信息参考,不构成任何投资建议。

相关推荐

security-insight