twentyone 审计报告 我自己这次犯了一个重大错误。就是没有真正的理解题目意思。导致全是找的破环游戏规则(可以任意查看手牌的错误。其实是游戏这么设定是合理的)然后这次就不放我的报告的了。因为都是public惹得祸。让我以为这次的首飞就是只有权限的问题,那就看看正确的漏洞出处
计算玩家和庄家的手牌不一致 (高) 其实这个问题应该是最简单的。因为从下面代码就能显而易见
//playerHand if (cardValue == 0 || cardValue >= 10) { playerTotal += 10; } else { playerTotal += cardValue; } //dealerHand if (cardValue >= 10) { dealerTotal += 10; } else { dealerTotal += cardValue; }
就是没有发现cardValue在玩家和庄家的计算手牌处理方式就不以样,那么这个游戏的公平性就没有了
庄家无法提款,资金锁定 (高) 我感觉这是合约的功能实现问题了。就没有玩家如果输了。那么转账就该给庄家。但是合约并没有实现。所以就出现了资金锁定的问题 重点就是这个结束游戏的函数实现,部分代码如下:
if (playerWon) { payable(player).transfer(2 ether); // 将奖励转账给玩家 emit FeeWithdrawn(player, 2 ether); // 触发奖励提取事件 }
可以发现,只有给玩家转账,但是没有给庄家的转账
开始游戏是返回的是玩家的手牌(其实这一个漏洞其实我并不认可。)(高) 看他的报告。我认为这和漏洞是没有必要的,而且这个合约本来就很矛盾,没有初始化的金额。然后这个漏洞又是基于合约中由大量的金额才能实施,先来看看他是怎么说的
TwentyOne.sol 合约中的 startGame() 函数返回玩家的初始手牌值作为 uint256。这使得攻击者能够通过回滚不利的交易,确保只在获得有利手牌的情况下继续游戏,从而操控游戏玩法。攻击者可以利用一个定制的攻击合约,轻松地耗尽合约中的资金,而损失极小。 漏洞详情 根本原因 漏洞存在于 startGame() 函数中,该函数将玩家的初始手牌值作为返回值暴露。这种设计缺陷使得攻击者能够通过检查返回的值,选择性地回滚不利的交易。 利用过程 初始化: 攻击者部署并为恶意合约 AttackTwentyOne.sol 提供资金,恶意合约与 TwentyOne.sol 合约进行交互。 选择性执行: 攻击合约调用 startGame(),检查返回的手牌值(playerHand),如果该值低于预定义的阈值(例如 20),则回滚交易。 确保优势: 只有当攻击者的手牌值足够高时,交易才会继续,从而确保攻击者具有极高的获胜几率。 耗尽合约资金: 攻击者通过这种方式持续进行操作,直到目标合约的余额被耗尽。 function startGame() public payable returns (uint256) { require( address(this).balance >= 2 ether, "Not enough ether on contract to start game" ); address player = msg.sender; require(msg.value == 1 ether, "start game only with 1 ether"); initializeDeck(player); uint256 card1 = drawCard(player); uint256 card2 = drawCard(player); addCardForPlayer(player, card1); addCardForPlayer(player, card2); return playersHand(player); // 暴露游戏状态 }
个人认为,这个是游戏的开始才能调用的函数,而且还要初始化player,那么攻击者的调用就没有意义。而且攻击者要成功的调用这个startGame函数,查看玩家的手牌,那么还要发送一个1 ether,攻击者赢得游戏也才2 ether,攻击者还要使用1 ehter去开启游戏,所以这个是得不偿失的存在,虽然这个问题是存在的,但是我认为攻击者没有收益去这么做
平局判玩家输,不符合规定21点的游戏规则 (中) 这个就是纯游戏理解规则了,最开始我也了解了21点的游戏规则,但是后来我认为这个应该是合约这么设定的,就是由平局就是庄家胜,结果者居然是个漏洞,不可思议吗,这是漏洞代码
if (dealerHand > 21) { emit PlayerWonTheGame("Dealer went bust, players winning hand: ", playerHand); endGame(msg.sender, true); } else if (playerHand > dealerHand) { emit PlayerWonTheGame("Dealer's hand is lower, players winning hand: ", playerHand); endGame(msg.sender, true); } else { // 这个 else 块错误地将平局处理为玩家的失败 emit PlayerLostTheGame("Dealer's hand is higher, dealers winning hand: ", dealerHand); endGame(msg.sender, false); }
庄家的爆牌概率很高 (中) 概述 TwentyOne 合约中的庄家手牌随机逻辑导致庄家超出21点(爆牌)的概率较高(超过50%)。从长期来看,玩家将能够赢取合约中的所有ETH。 漏洞详情 庄家手牌的阈值(standThreshold)是基于一个随机数来决定的,该随机数通过对 block.timestamp、msg.sender 和 block.prevrandao 进行哈希处理后,对5取模,并加上17,得出庄家停止抽牌的阈值。 TwentyOne.sol solidity 复制代码 uint256 standThreshold = (uint256( keccak256( abi.encodePacked(block.timestamp, msg.sender, block.prevrandao) ) ) % 5) + 17; 从上面的代码可以看出,standThreshold 有5个可能的值:{17, 18, 19, 20, 21}。 庄家需要抽取卡片,直到手牌的点数达到该随机阈值。
个人认为这也是一个矛盾的点。审计开始的时候还说了用随机性来处理庄家的代码手牌,结果这样是个漏洞,确实导致了游戏的不公平进行
还有一些其他的低问题,就不说了