Appearance
揭秘 EllipticToken:一次惊心动魄的签名漏洞攻防战
在数字资产的汪洋大海中,ERC20 代币是不可或缺的一员。而 Bob 创造的 EllipticToken ($ETK),则以其创新的椭圆曲线签名兑换系统,为代币的发行和管理带来了新的可能性。然而,正如标题所言,即便是精妙的设计,也可能因为“懒惰”而埋下隐患。本次,我们将深入剖析 EllipticToken 的核心机制,寻找 Bob 留下的“优化”破绽,并上演一场精彩的代币“盗取”戏码。
EllipticToken 的魅力:签名兑换与授权
EllipticToken 的核心在于其 off-chain(链下)的代币券(voucher)和 permit(授权)系统。Bob 可以在链下生成代币券,然后通过椭圆曲线签名来验证这些代币券的有效性,并在链上兑换 $ETK 代币。此外,它还引入了基于椭圆曲线签名的 permit 机制,允许代币持有者授权他人进行代币操作。
ECDSA 算法的“优化”:危险的捷径
ECDSA(Elliptic Curve Digital Signature Algorithm)是一种广泛应用的数字签名算法,其安全性依赖于一系列严格的数学步骤。然而,Bob 在实现这一算法时,却“优化”了一些步骤,这正是我们寻找的突破口。正如题目所提示的:“Look for any missing step in the Elliptic Curve Digital Signature Algorithm (ECDSA). Good luck. Elliptic curves do not forgive domain confusion.”(寻找 ECDSA 算法中任何缺失的步骤。祝你好运。椭圆曲线不会原谅域混乱。)
目标:夺回 Alice 的 $ETK
我们的目标是潜入 EllipticToken 的世界,找到 Bob 留下的漏洞,并从 Alice(0xA11CE84AcB91Ac59B0A4E2945C9157eF3Ab17D4e)手中“夺回”她刚刚兑换到的 $ETK 代币。这将是一场关于理解算法、洞察细节的挑战。
漏洞初探:redeemVoucher 函数的陷阱
在 redeemVoucher 函数中,用户通过提供 amount、receiver、salt、ownerSignature 和 receiverSignature 来兑换代币。
- Owner 验证:
ECDSA.recover(voucherHash, ownerSignature) == owner()确保了签名是由合约的 owner 发出的。 - Receiver 验证:
ECDSA.recover(voucherHash, receiverSignature) == receiver确保了接收者本人也同意了此次兑换。
看上去一切正常,但 ECDSA 的验证过程中,数据的“域”(domain)至关重要。
permit 函数的“优化”:致命的疏忽
而 permit 函数,更是 Bob 留下“优化”痕迹的重灾区:
permitHash = keccak256(abi.encode(amount)): 这里的permitHash仅仅基于amount进行哈希。usedHashes[permitHash]和usedHashes[bytes32(amount)]: 这里尝试防止amount被重复使用。tokenOwner = ECDSA.recover(bytes32(amount), tokenOwnerSignature): 这是关键! 这里使用bytes32(amount)作为permitHash来恢复tokenOwner的地址。这意味着,tokenOwnerSignature实际上是对amount的签名,而非通常意义上对一个完整的交易哈希的签名。permitAcceptHash = keccak256(abi.encodePacked(tokenOwner, spender, amount)): 这是spender签名的哈希。usedHashes[permitHash] = true;: 仅将基于amount的permitHash标记为已使用。
漏洞分析:签名域的混乱
最明显的漏洞在于 permit 函数中,ownerSignature(这里指 tokenOwnerSignature)实际上是对 amount 这个值的签名,而不是对一个包含了所有关键信息(如 tokenOwner、spender、amount)的完整哈希值的签名。 ECDSA 的正确使用是签名一个唯一标识交易的哈希值。Bob 为了“优化”,直接使用 amount 这个值作为签名消息,这导致了签名域的混乱。
更严重的是,permitHash 的生成方式 keccak256(abi.encode(amount)) 并没有包含 tokenOwner 的地址,而 ECDSA.recover 的结果 tokenOwner 又是后续验证所依赖的。
利用漏洞:上演“夺回”大戏
聪明的攻击者(也就是我们)可以利用这个签名机制的缺陷。
- 制造 Alice 的签名: Alice 想要将她的代币授权给我们的攻击者(Player)。Alice 只需要使用她的私钥,对我们想要她授权的
amount进行签名。由于 contract 的permit函数在验证tokenOwnerSignature时,只是恢复ECDSA.recover(bytes32(amount), tokenOwnerSignature),所以 Alice 只要提供一个针对amount的有效签名,这个签名就可以被 contract 识别为 Alice 的签名。 - 制造 Spender 的签名: 攻击者(Player)则需要对
keccak256(abi.encodePacked(Alice_address, Player_address, amount))进行签名,作为spenderSignature。 - 调用
permit函数: 攻击者使用构造好的amount、Alice 的地址、Alice 对amount的签名 (ownerSignature) 以及攻击者对permitAcceptHash的签名 (spenderSignature) 来调用permit函数。 _approve成功: 由于permit函数的验证逻辑存在缺陷,它会错误地认为 Alice 授权了攻击者,并调用_approve(tokenOwner, spender, amount),将amount的代币授权给攻击者。transferFrom夺回代币: 拥有了授权后,攻击者就可以调用transferFrom(ALICE, player, EllipticToken(ELLIPTICTOKEN_INST).balanceOf(ALICE)),将 Alice 名下所有的 $ETK 代币转给自己。
MyScript.sol 的精妙演绎
MyScript.sol 正是利用了上述漏洞的实际操作脚本。
- 它首先定义了
ALICE_new_sig,其中amount被精心构造。 - 然后,它使用
vm.sign为permitAcceptHash生成了sig,这里的playerpk代表了攻击者的私钥,ALICE是代币持有者,player是攻击者自己。 - 最后,通过
vm.startBroadcast和vm.stopBroadcast模拟了攻击者的链上操作,依次调用EllipticToken(ELLIPTICTOKEN_INST).permit来获取授权,然后调用transferFrom来转移代币。
总结:安全无小事,严谨是关键
EllipticToken 的故事,再次提醒我们,在区块链安全领域,每一个细节都至关重要。ECDSA 算法的严谨性不容一丝一毫的“优化”和妥协。Bob 的“懒惰”导致了签名域的混乱,最终让 Alice 的 $ETK 代币陷入危险。在这个充满挑战的 CTF 题目中,我们不仅学习了 ECDSA 的工作原理,更深刻体会到了代码审计和漏洞挖掘的乐趣与价值。