这篇文章咱水一篇去年碰到的 2 个 NFT 的漏洞,首先还是先上代码
function mintAllow(uint256 _tokenAmount, string memory name, bytes memory signature) public payable {
uint256 s = totalSupply();
require(check(name, signature) == msg.sender, "Signature Invalid"); //server side signature
require(OGStatus, "OG sale is not active");
require(_tokenAmount > 0, "Mint more than 0");
require(_tokenAmount <= maxMintPerTxOG, "Mint less");
require(s + _tokenAmount <= maxSupply, "Mint less");
require(msg.value >= priceOG * _tokenAmount, "ETH input is wrong");
require(allowedMintCountOG(msg.sender) >= _tokenAmount, "You minted too many");
require(tx.origin == msg.sender);
for (uint256 i = 0; i < _tokenAmount; ++i) {
_safeMint(msg.sender, s + i, "");
}
delete s;
updateMintCountOG(msg.sender, _tokenAmount);
}
function check(string memory name, bytes memory signature) public view returns(address) {
return _verify(name, signature);
}
function _verify(string memory name, bytes memory signature) internal view returns(address) {
bytes32 digest = _hash(name);
return ECDSA.recover(digest, signature);
}
function allowedMintCountOG(address minter) public view returns(uint256) {
return maxMintPerWalletOG - mintCountMapOG[minter];
}
function updateMintCountOG(address minter, uint256 count) private {
mintCountMapOG[minter] += count;
}
项目就不说了,全部代码也不贴了,mintAllow 是 mint 入口,这是一个白名单机制的 mint,来看这个 mint 会有什么问题。
他的主要验证白名单的机制是 require(check(name, signature) == msg.sender, "Signature Invalid"); 但是我们查看 check 函数后发现这个 signature 其实是 msg.sender 自己的签名,用户可以随便去签,完全没有限制作用。并且这里的签名还是用的 eth_sign 这是个很危险的操作,用户不应该去签任何 eth_sign 的签名。还有没有其它问题。其实是有的,updateMintCountOG 更新用户已经 mint 的数量放到了函数最后,这会导致可能的重入攻击,万幸是了 require(tx.origin == msg.sender); 禁止掉了使用合约进入 mint。
那么这么一段代码会有三个问题
1.链下不应该用 eth_sign 签名,链下签名应该用 personal_sign
2.ECDSA.recover 用法错误,正确用法是使用由开发者控制的私钥进行签名。
3.交换代码顺序,先更新变量,再交互 mint
这个项目最后结果是找到了项目方,关闭了 mint 通过其它方式来进行发售,结局很好。
address private _signerAddress;
function presaleBuy(bytes32 hash, bytes memory sig, uint256 qty, string memory nonce) external payable nonReentrant {
require(presaleLive, "presale not live");
require(matchAddresSigner(hash, sig), "no direct mint");
require(qty <= 5, "no more than 5");
require(hashTransaction(msg.sender, qty, nonce) == hash, "hash check failed");
require(totalSupply() + qty <= maxPresale, "presale out of stock");
require(pricePerToken * qty == msg.value, "exact amount needed");
require(!_usedNonces[nonce], "nonce already used");
_usedNonces[nonce] = true;
for (uint256 i = 0; i < qty; i++) {
_safeMint(msg.sender, totalSupply() + 1);
}
}
function hashTransaction(address sender, uint256 qty, string memory nonce) private pure returns(bytes32) {
bytes32 hash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", keccak256(abi.encodePacked(sender, qty, nonce))));
return hash;
}
function matchAddresSigner(bytes32 hash, bytes memory signature) private view returns(bool) {
return _signerAddress == hash.recover(signature);
}
同样具体项目也不说,入口为 presaleBuy,初年像是 ECDSA.recover 的正确使用方法,但是 ECDSA.recover 在这种场景不是单独使用的,需要配合相应的验证,不然会导致重放攻击,这个里面就没有进行相应的验证 hashTransaction 像是在进行验证,但是没有起作用,实际变成了浪费 gas 的代码。
这段代码有 2个问题
1.matchAddresSigner 没有检查 msg.sender
2.matchAddresSigner 没有相关的配合检查代码,导致了重放
这个项目的结局是 mint 的很快,有些很少数量被重放利用,损失不大。
mirror 的编辑器挺难用的,不能直接让编辑 markdown code 吗。
