OKLink:2023年7月安全事件盘点
链上卫士:Paradigm CTF 2022 题目浅析—Vanity
题目背景
在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。