Onchain Checkin

{FDE29822-0598-4034-9C58-D87D2B70BAFB}

这里是 program 的地址。explorer 上查询到如下数据:

{05EF0828-E2A2-4080-BE12-F33D91119DE7}

这两个有一个是测试数据:

{B51BE281-E1DB-4B59-8A30-685350640094}

另一个是对的

{AA9AA23E-78DF-4952-9480-D3B3D741F1DE}

但是这里只有两个

{2E625781-5176-4F84-A9D9-866A5CFEC58A}

另一个看源码这里:

{6EDDE0DC-E398-41A8-810F-148FADD6AFC4}

这里提到了 account3 的公钥。试了下 base58 也能解出来:

{28B2B2E2-6C2E-43B0-84F2-78193D8A4004}

拼一下:

SUCTF{Con9ra7s!YouHaveFound_7HE_KEeee3ey_P4rt_0f_Th3_F1ag.}

Onchain Magician

源码:

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.28;

contract MagicBox {
    struct Signature {
        uint8 v;
        bytes32 r;
        bytes32 s;
    }

    address magician;
    bytes32 alreadyUsedSignatureHash;
    bool isOpened;

    constructor() {}

    function isSolved() public view returns (bool) {
        return isOpened;
    }

    function getMessageHash(address _magician) public view returns (bytes32) {
        return keccak256(abi.encodePacked("I want to open the magic box", _magician, address(this), block.chainid));
    }

    function _getSignerAndSignatureHash(Signature memory _signature) internal view returns (address, bytes32) {
        address signer = ecrecover(getMessageHash(msg.sender), _signature.v, _signature.r, _signature.s);
        bytes32 signatureHash = keccak256(abi.encodePacked(_signature.v, _signature.r, _signature.s));
        return (signer, signatureHash);
    }

    function signIn(Signature memory signature) external {
        require(magician == address(0), "Magician already signed in");
        (address signer, bytes32 signatureHash) = _getSignerAndSignatureHash(signature);
        require(signer == msg.sender, "Invalid signature");
        magician = signer;
        alreadyUsedSignatureHash = signatureHash;
    }

    function openBox(Signature memory signature) external {
        require(magician == msg.sender, "Only magician can open the box");
        (address signer, bytes32 signatureHash) = _getSignerAndSignatureHash(signature);
        require(signer == msg.sender, "Invalid signature");
        require(signatureHash != alreadyUsedSignatureHash, "Signature already used");
        isOpened = true;
    }
}

分析:和其他的合约 ctf 一样,调用 openBox函数成功使得 isOpened为 ture 即可拿到 flag。

大致一看,这道题需要我们签署原始交易,获得 v, r, s 的值。

  • getMessageHash:该函数用于构造合约预期的 message 摘要
  • _getSignerAndSignatureHash:内部函数,用于还原签名的签署者,以及获得签名的哈希
  • signIn:传递签名(这里要求我们 msg.sender 和还原出来的签名地址相同,同时在此之前没有调用过该函数),设置 magician = signer
  • openBox:传递签名,想要调用成功,需要与上一次调用signIn的 signer 相同,同时签名的哈希不同

大致分析后,我们可以知道:每个人(signer)的交易哈希都只有一个,但是我们需要有两个不同的有效签名。这个实际上是以太坊的签名拓展性攻击漏洞。

关于该漏洞,详细流程可以查看我之前写的一篇文章:https://learnblockchain.cn/article/8281

简单来说:由于以太坊底层使用的是 Secp256K1 椭圆曲线,该椭圆曲线,对于一个签名,有两个有效的 s 值。所以,通过构造,我们得到另一个有效的 s 值,将这个 s 值作为调用openBox中传递即可。

对于计算 v、r、s,这里使用 foundry 的 sign cheatcode

完整 Poc:

import {Script, console2} from "forge-std/Script.sol";
import {MagicBox} from "../src/MagicBox.sol";

contract Attack is Script {
    function run() external {
        MagicBox target = MagicBox(vm.envAddress("target"));
        // secp256k1 曲线的阶 n
        uint256 n = uint256(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141);

        vm.startBroadcast();
        bytes32 MessageHash = target.getMessageHash(vm.envAddress("account"));
        vm.stopBroadcast();

        (uint8 v, bytes32 r, bytes32 s) = vm.sign(vm.envUint("key"), MessageHash);
        v = 27; // 27 还原出来的失败
        MagicBox.Signature memory signature1 = MagicBox.Signature(v, r, s);

        MagicBox.Signature memory signature2 = MagicBox.Signature(28, r, bytes32(n - uint256(s)));

        vm.startBroadcast();
        target.signIn(signature1);
        target.openBox(signature2);
        vm.stopBroadcast();
    }
}

这里需要注意的问题是:

以太坊的 v 值,有效值为 27 或 28,具体为哪个值,需要我们进行手动修改:

{FE7E8689-0932-4922-892A-A6BCC19EC358}

(第一次为 27 成功,第二次为 27 失败,修改为 28 后成功执行)

{78CD199F-A616-4181-A8CC-13682492F5BA}

之后广播即可:

{FF51B6ED-55D1-474F-9DDB-A6315A70856F}

flag:SUCTF{C0n9r4ts!Y0u’re_An_0ut5taNd1ng_OnchA1n_Ma9ic1an.}

{10271A05-923F-4BED-BD79-4D5FFBAFB0FB}