Skip to content
On this page

揭秘 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 函数中,用户通过提供 amountreceiversaltownerSignaturereceiverSignature 来兑换代币。

  • Owner 验证: ECDSA.recover(voucherHash, ownerSignature) == owner() 确保了签名是由合约的 owner 发出的。
  • Receiver 验证: ECDSA.recover(voucherHash, receiverSignature) == receiver 确保了接收者本人也同意了此次兑换。

看上去一切正常,但 ECDSA 的验证过程中,数据的“域”(domain)至关重要。

permit 函数的“优化”:致命的疏忽

permit 函数,更是 Bob 留下“优化”痕迹的重灾区:

  1. permitHash = keccak256(abi.encode(amount)) 这里的 permitHash 仅仅基于 amount 进行哈希。
  2. usedHashes[permitHash]usedHashes[bytes32(amount)] 这里尝试防止 amount 被重复使用。
  3. tokenOwner = ECDSA.recover(bytes32(amount), tokenOwnerSignature) 这是关键! 这里使用 bytes32(amount) 作为 permitHash 来恢复 tokenOwner 的地址。这意味着,tokenOwnerSignature 实际上是对 amount 的签名,而非通常意义上对一个完整的交易哈希的签名。
  4. permitAcceptHash = keccak256(abi.encodePacked(tokenOwner, spender, amount)) 这是 spender 签名的哈希。
  5. usedHashes[permitHash] = true; 仅将基于 amountpermitHash 标记为已使用。

漏洞分析:签名域的混乱

最明显的漏洞在于 permit 函数中,ownerSignature(这里指 tokenOwnerSignature)实际上是对 amount 这个值的签名,而不是对一个包含了所有关键信息(如 tokenOwnerspenderamount)的完整哈希值的签名。 ECDSA 的正确使用是签名一个唯一标识交易的哈希值。Bob 为了“优化”,直接使用 amount 这个值作为签名消息,这导致了签名域的混乱。

更严重的是,permitHash 的生成方式 keccak256(abi.encode(amount)) 并没有包含 tokenOwner 的地址,而 ECDSA.recover 的结果 tokenOwner 又是后续验证所依赖的。

利用漏洞:上演“夺回”大戏

聪明的攻击者(也就是我们)可以利用这个签名机制的缺陷。

  1. 制造 Alice 的签名: Alice 想要将她的代币授权给我们的攻击者(Player)。Alice 只需要使用她的私钥,对我们想要她授权的 amount 进行签名。由于 contract 的 permit 函数在验证 tokenOwnerSignature 时,只是恢复 ECDSA.recover(bytes32(amount), tokenOwnerSignature),所以 Alice 只要提供一个针对 amount 的有效签名,这个签名就可以被 contract 识别为 Alice 的签名。
  2. 制造 Spender 的签名: 攻击者(Player)则需要对 keccak256(abi.encodePacked(Alice_address, Player_address, amount)) 进行签名,作为 spenderSignature
  3. 调用 permit 函数: 攻击者使用构造好的 amount、Alice 的地址、Alice 对 amount 的签名 (ownerSignature) 以及攻击者对 permitAcceptHash 的签名 (spenderSignature) 来调用 permit 函数。
  4. _approve 成功: 由于 permit 函数的验证逻辑存在缺陷,它会错误地认为 Alice 授权了攻击者,并调用 _approve(tokenOwner, spender, amount),将 amount 的代币授权给攻击者。
  5. transferFrom 夺回代币: 拥有了授权后,攻击者就可以调用 transferFrom(ALICE, player, EllipticToken(ELLIPTICTOKEN_INST).balanceOf(ALICE)),将 Alice 名下所有的 $ETK 代币转给自己。

MyScript.sol 的精妙演绎

MyScript.sol 正是利用了上述漏洞的实际操作脚本。

  • 它首先定义了 ALICE_new_sig,其中 amount 被精心构造。
  • 然后,它使用 vm.signpermitAcceptHash 生成了 sig,这里的 playerpk 代表了攻击者的私钥,ALICE 是代币持有者,player 是攻击者自己。
  • 最后,通过 vm.startBroadcastvm.stopBroadcast 模拟了攻击者的链上操作,依次调用 EllipticToken(ELLIPTICTOKEN_INST).permit 来获取授权,然后调用 transferFrom 来转移代币。

总结:安全无小事,严谨是关键

EllipticToken 的故事,再次提醒我们,在区块链安全领域,每一个细节都至关重要。ECDSA 算法的严谨性不容一丝一毫的“优化”和妥协。Bob 的“懒惰”导致了签名域的混乱,最终让 Alice 的 $ETK 代币陷入危险。在这个充满挑战的 CTF 题目中,我们不仅学习了 ECDSA 的工作原理,更深刻体会到了代码审计和漏洞挖掘的乐趣与价值。

Built with AiAda