Skip to content
On this page

DeFi 的“隐形账单”:揭秘 Puzzle Wallet 的安全漏洞

想象一下,你热衷于 DeFi(去中心化金融),却被高昂的交易费用“劝退”?别担心,一群聪明的朋友想出了一个绝妙的主意:将多笔交易打包成一笔,大大降低成本。他们为此开发了一个名为 Puzzle Wallet 的智能合约。

为了应对潜在的bug,他们选择了 可升级合约 的设计。同时,为了确保只有自己人才能使用,他们设置了 白名单机制。在这个系统中,管理员(Admin) 拥有升级合约逻辑的权力,而 拥有者(Owner) 则负责管理允许使用的地址列表。

一切就绪,合约部署,朋友们欢呼雀跃,庆祝他们战胜了“邪恶的矿工”。然而,他们没有意识到,他们的“午餐钱”——也就是合约中的资产——正面临着前所未有的风险……

Puzzle Wallet 的“双重身份”与潜在危机

Puzzle Wallet 的核心在于其 代理模式(Proxy Pattern)。它并非直接执行合约逻辑,而是通过一个 PuzzleProxy 来转发。这个 PuzzleProxy 承担了两个重要角色:

  1. 可升级性入口: 它允许 管理员 调用 upgradeTo 函数,将合约逻辑指向新的实现合约。
  2. 权限管理: 它维护着 adminpendingAdmin 两个状态变量,用于管理管理员的变更。

而实际的钱包功能,则由 PuzzleWallet 合约实现,它拥有 ownermaxBalancewhitelistedbalances 等状态变量,并提供了 depositexecutemulticall 等操作。

关键点在于: PuzzleProxy 和 PuzzleWallet 合约在区块链上的存储是 共享的。这意味着,如果攻击者能够操纵 PuzzleProxy 中的状态,就有可能间接影响到 PuzzleWallet 的行为,甚至获得管理员权限。

漏洞扫描:delegatecallmsg.sender 的陷阱

题目描述中也给出了明确的提示:“理解 delegatecall 的工作原理,以及在执行 delegatecallmsg.sendermsg.value 的行为”。

delegatecall 是 Solidity 中一个非常强大的操作,它允许一个合约执行另一个合约的代码,并且 目标合约的代码以调用合约的上下文(包括 msg.sendermsg.value 和存储)来执行

正是这个特性,成为了本次攻击的突破口。

攻击路径:成为“白名单”上的“新管理员”

攻击者的目标是:Hijack this wallet to become the admin of the proxy(劫持这个钱包,成为代理的管理员)。

来看看攻击者是如何利用 delegatecall 和代理模式来实现这一目标的:

  1. 成为白名单用户: 攻击者需要先把自己添加到 PuzzleWallet 的白名单中,这样才能调用 depositmulticall 等受保护的函数。

  2. 利用 multicall 嵌套执行: PuzzleWallet 的 multicall 函数允许连续执行多个操作,并且允许 delegatecall 自身。攻击者巧妙地构造了一个 multicall 调用,其中包含了:

    • 一个 multicall 调用自身。
    • 一个 deposit 调用。

    这种嵌套调用看起来有些奇怪,但关键在于 multicall 函数内部的 (bool success,) = address(this).delegatecall(data[i]); 这一行。

  3. delegatecall 的妙用:multicall 执行到 delegatecall 自身时,它实际上是在执行 PuzzleWallet 合约的代码,但 以调用 multicall 的那个合约的 msg.sendermsg.value 来执行

    如果在 multicall 中,攻击者调用 target.deposit(),这会执行 PuzzleWalletdeposit 函数。但因为是通过 delegatecall 执行,msg.sender 实际上是调用 multicall 的那个合约,而不是 PuzzleWallet 的白名单用户。

  4. 绕过 onlyWhitelisted 限制: PuzzleWallet 的 deposit 函数要求 onlyWhitelisted。然而,当 multicall 通过 delegatecall 执行 deposit 时,msg.sender 变成了调用 multicall 的合约。如果攻击者事先将调用 multicall 的这个合约(即 Hack 合约)添加到了白名单,那么 PuzzleWallet.deposit 就会成功执行。

  5. 操纵 pendingAdmin 攻击者利用 PuzzleProxy 合约中的 proposeNewAdmin 函数,将自己的地址设为 pendingAdmin

  6. 获取管理员权限: 最关键的一步来了!通过 PuzzleWallet 合约的 multicall 函数,攻击者可以通过 delegatecall 执行 PuzzleProxy 合约的 approveNewAdmin 函数。由于 delegatecall 会继承调用者的 msg.sender,此时 approveNewAdmin 函数执行时,msg.sender 就是调用 multicall 的 Hack 合约

    approveNewAdmin 函数内部有一个 onlyAdmin 修饰器,以及 require(pendingAdmin == _expectedAdmin, ...) 的检查。

    攻击者在 constructor 中已经执行了 target.proposeNewAdmin(address(this)),所以 pendingAdmin 就是 Hack 合约的地址。当 delegatecall 调用 approveNewAdmin(address(this)) 时,msg.sender 是 Hack 合约,pendingAdmin 也是 Hack 合约,条件满足,Hack 合约就成功成为了新的 admin

  7. 设置 maxBalance 最后,攻击者还可以利用新获得的管理员权限,调用 setMaxBalance,并将 maxBalance 设置为目标地址(这里是 msg.sender,即合约部署者),为后续可能的操作铺平道路。

总结

Puzzle Wallet 的这次安全事件,生动地展示了智能合约开发中 存储冲突delegatecall 的潜在风险。通过精心设计的嵌套调用和对 delegatecall 工作机制的深刻理解,攻击者成功地绕过了白名单限制,篡改了合约的管理员权限,证明了即使是具有可升级性和访问控制的合约,也可能隐藏着意想不到的漏洞。

在这个 DeFi 的世界里,每一个细节都至关重要。安全审计和对 Solidity 语言特性的深入研究,是守护资产的关键。


Built with AiAda