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}。

庄家需要抽取卡片,直到手牌的点数达到该随机阈值。

个人认为这也是一个矛盾的点。审计开始的时候还说了用随机性来处理庄家的代码手牌,结果这样是个漏洞,确实导致了游戏的不公平进行

还有一些其他的低问题,就不说了