抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

DoubleEntryPoint

1. 题目要求

  • 1.1 此级别具有CryptoVault特殊功能,sweepToken功能。这是用于检索卡在合约中的代币的常用函数。操作CryptoVault使用underlying无法清除的令牌,因为它是 . 的重要核心逻辑组件CryptoVault。可以清除任何其他令牌。

    底层代币是合约定义中实施的 DET 代币的一个实例DoubleEntryPoint,并CryptoVault持有 100 个单位。此外,CryptoVault还拥有 100 个LegacyToken LGT

    在此级别中,您应该找出错误所在CryptoVault并防止它被耗尽令牌。

    该合约具有Forta合约功能,任何用户都可以注册自己的detection bot合约。Forta 是一个去中心化的、基于社区的监控网络,用于尽快检测 DeFi、NFT、治理、桥梁和其他 Web3 系统上的威胁和异常。你的工作是实现一个detection bot并在合约中注册它Forta。机器人的实施将需要发出正确的警报,以防止潜在的攻击或漏洞利用。

    可能有帮助的事情:

    • 双入口点如何为代币合约工作?
  • 1.2 题目代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;

    import "openzeppelin-contracts-08/access/Ownable.sol";
    import "openzeppelin-contracts-08/token/ERC20/ERC20.sol";

    interface DelegateERC20 {
    function delegateTransfer(address to, uint256 value, address origSender) external returns (bool);
    }

    interface IDetectionBot {
    function handleTransaction(address user, bytes calldata msgData) external;
    }

    interface IForta {
    function setDetectionBot(address detectionBotAddress) external;
    function notify(address user, bytes calldata msgData) external;
    function raiseAlert(address user) external;
    }

    contract Forta is IForta {
    mapping(address => IDetectionBot) public usersDetectionBots;
    mapping(address => uint256) public botRaisedAlerts;

    function setDetectionBot(address detectionBotAddress) external override {
    usersDetectionBots[msg.sender] = IDetectionBot(detectionBotAddress);
    }

    function notify(address user, bytes calldata msgData) external override {
    if(address(usersDetectionBots[user]) == address(0)) return;
    try usersDetectionBots[user].handleTransaction(user, msgData) {
    return;
    } catch {}
    }

    function raiseAlert(address user) external override {
    if(address(usersDetectionBots[user]) != msg.sender) return;
    botRaisedAlerts[msg.sender] += 1;
    }
    }

    contract CryptoVault {
    address public sweptTokensRecipient;
    IERC20 public underlying;

    constructor(address recipient) {
    sweptTokensRecipient = recipient;
    }

    function setUnderlying(address latestToken) public {
    require(address(underlying) == address(0), "Already set");
    underlying = IERC20(latestToken);
    }

    /*
    ...
    */

    function sweepToken(IERC20 token) public {
    require(token != underlying, "Can't transfer underlying token");
    token.transfer(sweptTokensRecipient, token.balanceOf(address(this)));
    }
    }

    contract LegacyToken is ERC20("LegacyToken", "LGT"), Ownable {
    DelegateERC20 public delegate;

    function mint(address to, uint256 amount) public onlyOwner {
    _mint(to, amount);
    }

    function delegateToNewContract(DelegateERC20 newContract) public onlyOwner {
    delegate = newContract;
    }

    function transfer(address to, uint256 value) public override returns (bool) {
    if (address(delegate) == address(0)) {
    return super.transfer(to, value);
    } else {
    return delegate.delegateTransfer(to, value, msg.sender);
    }
    }
    }

    contract DoubleEntryPoint is ERC20("DoubleEntryPointToken", "DET"), DelegateERC20, Ownable {
    address public cryptoVault;
    address public player;
    address public delegatedFrom;
    Forta public forta;

    constructor(address legacyToken, address vaultAddress, address fortaAddress, address playerAddress) {
    delegatedFrom = legacyToken;
    forta = Forta(fortaAddress);
    player = playerAddress;
    cryptoVault = vaultAddress;
    _mint(cryptoVault, 100 ether);
    }

    modifier onlyDelegateFrom() {
    require(msg.sender == delegatedFrom, "Not legacy contract");
    _;
    }

    modifier fortaNotify() {
    address detectionBot = address(forta.usersDetectionBots(player));

    // Cache old number of bot alerts
    uint256 previousValue = forta.botRaisedAlerts(detectionBot);

    // Notify Forta
    forta.notify(player, msg.data);

    // Continue execution
    _;

    // Check if alarms have been raised
    if(forta.botRaisedAlerts(detectionBot) > previousValue) revert("Alert has been triggered, reverting");
    }

    function delegateTransfer(
    address to,
    uint256 value,
    address origSender
    ) public override onlyDelegateFrom fortaNotify returns (bool) {
    _transfer(origSender, to, value);
    return true;
    }
    }

2. 分析

  • 这个挑战似乎是 OpenZeppelin 和 Forta 之间的合资企业,一个实时安全和操作监控。据我所知,试图向您解释您应该如何集成 Forta 系统来监控您的合同是一个挑战。让我们看看进展如何。

    从挑战的描述(说实话不清楚)我们有两个令牌:LegacyToken顾名思义是一个已“弃用”的令牌(这在现实生活中发生过吗?)支持一个名为DoubleEntryPoint.

    我们还有一个称为 Vault 的库CryptoVault,它具有一些功能(与挑战范围无关),并提供一种称为实用程序的方法,允许sweepToken(IERC20 token)任何人“扫描”(转移)到sweptTokensRecipient(部署时定义的地址)已发送的令牌不小心去了金库。该函数中唯一的检查是您不能扫除underlyingVault 的令牌。

    在部署时,我们从这个配置开始:

    • CryptoVault持有100 个 DET ( DoubleEntryToken)
    • CryptoVault持有100 LGT ( LegacyToken)

    我们的目标是创建一个Forta DetectionBot来监控合约并防止外部攻击者耗尽本CryptoVault不应耗尽的代币。

    让我们回顾一下每个合同,看看我们是否能找到一些攻击媒介。

    LegacyToken.sol

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    contract LegacyToken is ERC20("LegacyToken", "LGT"), Ownable {
    DelegateERC20 public delegate;

    function mint(address to, uint256 amount) public onlyOwner {
    _mint(to, amount);
    }

    function delegateToNewContract(DelegateERC20 newContract) public onlyOwner {
    delegate = newContract;
    }

    function transfer(address to, uint256 value) public override returns (bool) {
    if (address(delegate) == address(0)) {
    return super.transfer(to, value);
    } else {
    return delegate.delegateTransfer(to, value, msg.sender);
    }
    }
    }

    它是一个ERC20继承自的令牌Ownable。合约owner的 可以通过调用mint新代币和更新变量的值。delegate``delegateToNewContract

    奇怪的部分是在transfer覆盖了标准提供的默认函数的函数中ERC20

    如果没有定义委托(address(delegate) == address(0)),则合约使用标准的默认逻辑ERC20;否则执行return delegate.delegateTransfer(to, value, msg.sender)

    在这种情况下,delegateDoubleEntryPoint合同本身。这是什么意思?当您在现实中执行转移时,LegacyToken它正在转发要执行的操作DoubleEntryPoint.delegateTransfer。让我们切换到另一个令牌代码,看看发生了什么

    DoubleEntryPoint.sol

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    contract DoubleEntryPoint is ERC20("DoubleEntryPointToken", "DET"), DelegateERC20, Ownable {
    address public cryptoVault;
    address public player;
    address public delegatedFrom;
    Forta public forta;

    constructor(
    address legacyToken,
    address vaultAddress,
    address fortaAddress,
    address playerAddress
    ) public {
    delegatedFrom = legacyToken;
    forta = Forta(fortaAddress);
    player = playerAddress;
    cryptoVault = vaultAddress;
    _mint(cryptoVault, 100 ether);
    }

    modifier onlyDelegateFrom() {
    require(msg.sender == delegatedFrom, "Not legacy contract");
    _;
    }

    modifier fortaNotify() {
    address detectionBot = address(forta.usersDetectionBots(player));

    // Cache old number of bot alerts
    uint256 previousValue = forta.botRaisedAlerts(detectionBot);

    // Notify Forta
    forta.notify(player, msg.data);

    // Continue execution
    _;

    // Check if alarms have been raised
    if (forta.botRaisedAlerts(detectionBot) > previousValue) revert("Alert has been triggered, reverting");
    }

    function delegateTransfer(
    address to,
    uint256 value,
    address origSender
    ) public override onlyDelegateFrom fortaNotify returns (bool) {
    _transfer(origSender, to, value);
    return true;
    }
    }

    合约是ERC20继承自 和 的DelegateERC20普通代币OwnableDelegateERC20是强制合约实现tokenfunction delegateTransfer(address to, uint256 value, address origSender)需要的功能的接口LegacyToken

    有时constructor,设置一些状态变量并将100令牌铸造到CryptoVault.

    在进入delegateTransfer函数之前,让我们回顾一下fortaNotify函数修饰符

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    modifier fortaNotify() {
    address detectionBot = address(forta.usersDetectionBots(player));

    // Cache old number of bot alerts
    uint256 previousValue = forta.botRaisedAlerts(detectionBot);

    // Notify Forta
    forta.notify(player, msg.data);

    // Continue execution
    _;

    // Check if alarms have been raised
    if (forta.botRaisedAlerts(detectionBot) > previousValue) revert("Alert has been triggered, reverting");
    }

    这个修改器的作用是触发 Forta 检测系统实现的一些逻辑。它在本地存储执行代码函数之前引发的警报数量,并将该数字与执行调用函数修饰符的函数主体之后引发的警报数量进行比较。

    如果警报数量增加,交易将恢复并显示消息"Alert has been triggered, reverting"

    让我们回顾一下调用LegacyToken“遗留”时令牌也使用的重要功能。LegacyToken.transfer

    1
    2
    3
    4
    5
    6
    7
    8
    function delegateTransfer(
    address to,
    uint256 value,
    address origSender
    ) public override onlyDelegateFrom fortaNotify returns (bool) {
    _transfer(origSender, to, value);
    return true;
    }

    如果查看函数修饰符列表,您会看到

    • onlyDelegateFrom只允许delegateFrom调用此函数。在这种情况下,只LegacyToken允许合约调用此函数,否则将允许任何人调用_transfer(即低级 ERC20 传输)来自origSender
    • fortaNotify是一个特殊的功能修饰符,可以触发一些特定的 Forta 逻辑,就像我们之前看到的那样

    函数本身很简单,就是调用函数的ERC20内部实现_transfer。请记住,_transfer只检查 thattoorigSenderare notaddress(0)以及origSender有足够的令牌可以转移到to(它还检查不足/溢出条件)但它不检查 that origSenderismsg.sender或消费者是否有足够的津贴。这就是为什么我们有修饰符onlyDelegateFrom

    CryptoVault.sol

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    contract CryptoVault {
    address public sweptTokensRecipient;
    IERC20 public underlying;

    constructor(address recipient) public {
    sweptTokensRecipient = recipient;
    }

    function setUnderlying(address latestToken) public {
    require(address(underlying) == address(0), "Already set");
    underlying = IERC20(latestToken);
    }

    /*
    ...
    */

    function sweepToken(IERC20 token) public {
    require(token != underlying, "Can't transfer underlying token");
    token.transfer(sweptTokensRecipient, token.balanceOf(address(this)));
    }
    }

    合约应实现普通加密 Vault 系统的逻辑。对于挑战的范围而言,这部分逻辑并不有趣。

    由于任何金库也CryptoVault有一个基础令牌,在这种情况下是DoubleEntryPoint.

    任何人都可以调用的函数sweepToken允许金库将任意token(指定为输入参数)的整个金库余额转移到sweptTokensRecipient. 收件人应该是安全的,因为它是由合同的部署者及时初始化的constructor

    从代码中可以看出,唯一完成的检查是防止 Vault 转移令牌underlying

    通过部署 Forta DetectionBot 找到漏洞并阻止它

    通过结合我们收集到的所有信息,您是否发现了我们可以利用的漏洞?回顾一下我们现有的知识:

    • CryptoVaultunderlying令牌是DoubleEntryPoint。该合约提供了一个sweepToken在 Vault 中转移代币的方法,但它阻止了清除DoubleEntryPoint代币(因为它是underlying
    • DoubleEntryPointtoken 是一个 ERC20 令牌,它实现了一个delegateTransfer只能由LegacyToken令牌调用的自定义函数,并且由 Forta 通过执行函数修饰符来监控fortaNotify。该函数允许委托人将一定数量的代币从origSpender任意接收者转移
    • LegacyToken是已“弃用”的 ERC20 令牌。当transfer(address to, uint256 value)函数被调用时DoubleEntryPoint,(令牌的“新版本”)delegate.delegateTransfer(to, value, msg.sender)被调用

    问题在哪里?因为LegacyToken.transfer是“镜像”,DoubleEntryPoint.transfer这意味着当您要求尝试转移 1 个时,LegacyToken实际上您转移的是 1 个DoubleEntryPoint代币(为了能够做到这一点,您的余额中必须同时拥有这两个代币)

    包含CryptoVault两个令牌中的 100 个,但sweepToken仅阻止underlying DoubleEntryPoint.

    但是通过了解其工作原理,我们可以通过调用LegacyToken轻松扫除所有令牌。DoubleEntryPoint``CryptoVault.sweep(address(legacyTokenContract))

    现在我们知道如何利用它,我们如何利用 Forta 集成来防止利用并恢复交易?我们可以构建一个扩展 Forta 的合约IDetectionBot并将其插入DoubleEntryPoint. 通过这样做,我们应该能够在 VaultsweepToken触发LegacyToken.transfer将触发DoubleEntryPoint.delegateTransfer将触发(在执行函数代码之前)函数fortaNotify修饰符时防止利用。是的,我知道执行链很深,但请耐心等待,我们明白了!

    合约IDetectionBot接口只有一个函数签名function handleTransaction(address user, bytes calldata msgData) external;,可以通过DoubleEntryPoint.delegateTransfer这些参数直接调用forta.notify(player, msg.data)

    只有在这两个条件都为真时,我们才会在内部DetectionBot发出警报:

    • 原始发件人(正在呼叫的人DoubleEntryPoint.delegateTransfer)是CryptoVault
    • 调用函数的签名(的前 4 个字节calldata)等于delegateTransfer签名

    让我们origSender从中提取值msgData(请记住,在本例中,该参数值等于msg.data)。如果您查看“特殊变量和函数”部分下的块和交易属性msg.data的 Solidity 文档,您会看到这是一种代表完整 calldata 的bytes calldata数据类型。这是什么意思?在这些字节中,您将同时拥有函数选择器(4 个字节)和函数有效负载。

    要提取参数,我们可以简单地使用abi.decode这样的(address to, uint256 value, address origSender) = abi.decode(msgData[4:], (address, uint256, address));。一个重要的注意事项:我们假设在这些字节中,这些特定类型的三个值以这些特定顺序排列。我们正在做一个非常艰难的假设。这就是为什么我们需要将此信息与函数签名与强制执行这些类型和顺序要求的函数签名相匹配这一事实结合起来delegateTransfer

    1
    msgData`第二部分非常简单,我们只需通过合并前 4 个字节来重建调用签名,`bytes memory callSig = abi.encodePacked(msgData[0], msgData[1], msgData[2], msgData[3]);`并将其与我们知道的正确签名进行比较`delegateTransfer`→`abi.encodeWithSignature("delegateTransfer(address,uint256,address)")

    解决方案代码

    让我们看看检测的整个代码DetectionBot

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    contract DetectionBot is IDetectionBot {
    address private monitoredSource;
    bytes private monitoredSig;

    constructor(address _monitoredSource, bytes memory _monitoredSig) public {
    monitoredSource = _monitoredSource;
    monitoredSig = _monitoredSig;
    }

    function handleTransaction(address user, bytes calldata msgData) external override {
    (address to, uint256 value, address origSender) = abi.decode(msgData[4:], (address, uint256, address));

    bytes memory callSig = abi.encodePacked(msgData[0], msgData[1], msgData[2], msgData[3]);

    if (origSender == monitoredSource && keccak256(callSig) == keccak256(monitoredSig)) {
    IForta(msg.sender).raiseAlert(user);
    }
    }
    }

    在构造函数内部,第一个参数将是我们要监视的源,在本例中是地址,CryptoVault第二个参数是我们打算监视的函数的签名,在本例中是abi.encodeWithSignature("delegateTransfer(address,uint256,address)")

    现在我们只需要部署传递正确参数的机器人并将机器人插入 Forta 系统并解决挑战。我们走吧!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function exploitLevel() internal override {
    vm.startPrank(player, player);

    // Create and deploy the `DetectionBot` with the correct constructor paramter
    // The first one is the source we want to monitor
    // The second one is the signature of the function we want to match
    DetectionBot bot = new DetectionBot(
    level.cryptoVault(),
    abi.encodeWithSignature("delegateTransfer(address,uint256,address)")
    );

    // add the bot to the Forta network detection system that monitor the `DoubleEntryPoint` contract
    level.forta().setDetectionBot(address(bot));

    vm.stopPrank();
    }

3. 解题

评论



政策 · 统计 | 本站使用 Volantis 主题设计