Appearance
DeFi 的“隐形账单”:揭秘 Puzzle Wallet 的安全漏洞
想象一下,你热衷于 DeFi(去中心化金融),却被高昂的交易费用“劝退”?别担心,一群聪明的朋友想出了一个绝妙的主意:将多笔交易打包成一笔,大大降低成本。他们为此开发了一个名为 Puzzle Wallet 的智能合约。
为了应对潜在的bug,他们选择了 可升级合约 的设计。同时,为了确保只有自己人才能使用,他们设置了 白名单机制。在这个系统中,管理员(Admin) 拥有升级合约逻辑的权力,而 拥有者(Owner) 则负责管理允许使用的地址列表。
一切就绪,合约部署,朋友们欢呼雀跃,庆祝他们战胜了“邪恶的矿工”。然而,他们没有意识到,他们的“午餐钱”——也就是合约中的资产——正面临着前所未有的风险……
Puzzle Wallet 的“双重身份”与潜在危机
Puzzle Wallet 的核心在于其 代理模式(Proxy Pattern)。它并非直接执行合约逻辑,而是通过一个 PuzzleProxy 来转发。这个 PuzzleProxy 承担了两个重要角色:
- 可升级性入口: 它允许 管理员 调用
upgradeTo函数,将合约逻辑指向新的实现合约。 - 权限管理: 它维护着
admin和pendingAdmin两个状态变量,用于管理管理员的变更。
而实际的钱包功能,则由 PuzzleWallet 合约实现,它拥有 owner、maxBalance、whitelisted 和 balances 等状态变量,并提供了 deposit、execute 和 multicall 等操作。
关键点在于: PuzzleProxy 和 PuzzleWallet 合约在区块链上的存储是 共享的。这意味着,如果攻击者能够操纵 PuzzleProxy 中的状态,就有可能间接影响到 PuzzleWallet 的行为,甚至获得管理员权限。
漏洞扫描:delegatecall 与 msg.sender 的陷阱
题目描述中也给出了明确的提示:“理解 delegatecall 的工作原理,以及在执行 delegatecall 时 msg.sender 和 msg.value 的行为”。
delegatecall 是 Solidity 中一个非常强大的操作,它允许一个合约执行另一个合约的代码,并且 目标合约的代码以调用合约的上下文(包括 msg.sender、msg.value 和存储)来执行。
正是这个特性,成为了本次攻击的突破口。
攻击路径:成为“白名单”上的“新管理员”
攻击者的目标是:Hijack this wallet to become the admin of the proxy(劫持这个钱包,成为代理的管理员)。
来看看攻击者是如何利用 delegatecall 和代理模式来实现这一目标的:
成为白名单用户: 攻击者需要先把自己添加到 PuzzleWallet 的白名单中,这样才能调用
deposit和multicall等受保护的函数。利用
multicall嵌套执行: PuzzleWallet 的multicall函数允许连续执行多个操作,并且允许delegatecall自身。攻击者巧妙地构造了一个multicall调用,其中包含了:- 一个
multicall调用自身。 - 一个
deposit调用。
这种嵌套调用看起来有些奇怪,但关键在于
multicall函数内部的(bool success,) = address(this).delegatecall(data[i]);这一行。- 一个
delegatecall的妙用: 当multicall执行到delegatecall自身时,它实际上是在执行PuzzleWallet合约的代码,但 以调用multicall的那个合约的msg.sender和msg.value来执行。如果在
multicall中,攻击者调用target.deposit(),这会执行PuzzleWallet的deposit函数。但因为是通过delegatecall执行,msg.sender实际上是调用multicall的那个合约,而不是 PuzzleWallet 的白名单用户。绕过
onlyWhitelisted限制: PuzzleWallet 的deposit函数要求onlyWhitelisted。然而,当multicall通过delegatecall执行deposit时,msg.sender变成了调用multicall的合约。如果攻击者事先将调用multicall的这个合约(即 Hack 合约)添加到了白名单,那么PuzzleWallet.deposit就会成功执行。操纵
pendingAdmin: 攻击者利用PuzzleProxy合约中的proposeNewAdmin函数,将自己的地址设为pendingAdmin。获取管理员权限: 最关键的一步来了!通过
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!设置
maxBalance: 最后,攻击者还可以利用新获得的管理员权限,调用setMaxBalance,并将maxBalance设置为目标地址(这里是msg.sender,即合约部署者),为后续可能的操作铺平道路。
总结
Puzzle Wallet 的这次安全事件,生动地展示了智能合约开发中 存储冲突 和 delegatecall 的潜在风险。通过精心设计的嵌套调用和对 delegatecall 工作机制的深刻理解,攻击者成功地绕过了白名单限制,篡改了合约的管理员权限,证明了即使是具有可升级性和访问控制的合约,也可能隐藏着意想不到的漏洞。
在这个 DeFi 的世界里,每一个细节都至关重要。安全审计和对 Solidity 语言特性的深入研究,是守护资产的关键。