Blind Guy - K.L.M
Contrat
Bonne nouvelle (non), lorsque l’on lance le challenge et qu’on arrive sur la page, nous remarquons qu’aucun contrat nous est fourni…
Il ne nous reste plus qu’à sortir notre meilleur atout A.K.A le reverse.
Pour reverse un contrat, il nous faut son bytecode, on va donc le récupérer :
1
2
3
4
|
# $TAR = adresse du contrat cible, $RPC = URL du RPC
cast code $TAR -r $RPC
0x608060405234801561000f575f5ffd5b5060043610610055575f3560e01c806364d98f6e14610059578063799320bb14610077578063ec7eba0514610095578063ed74d933146100c5578063fe27c0f6146100e1575b5f5ffd5b6100616100ff565b60405161006e9190610446565b60405180910390f35b61007f610113565b60405161008c9190610446565b60405180910390f35b6100af60048036038101906100aa91906104bd565b610124565b6040516100bc9190610503565b60405180910390f35b6100df60048036038101906100da9190610546565b610141565b005b6100e96103c4565b6040516100f69190610503565b60405180910390f35b5f5f5f9054906101000a900460ff16905090565b5f5f9054906101000a900460ff1681565b6001602052805f5260405f205f915054906101000a900460ff1681565b5f5f9054906101000a900460ff161561018f576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610186906105cb565b60405180910390fd5b600a60ff1660015f3373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f9054906101000a900460ff1660ff1610610221576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161021890610633565b60405180910390fd5b5f6102753360015f3373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f9054906101000a900460ff166103c9565b90508015158215150361036a5760015f3373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f81819054906101000a900460ff16809291906102db9061067e565b91906101000a81548160ff021916908360ff16021790555050600a60ff1660015f3373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f9054906101000a900460ff1660ff16036103655760015f5f6101000a81548160ff0219169083151502179055505b6103c0565b5f60015f3373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020015f205f6101000a81548160ff021916908360ff1602179055505b5050565b600a81565b5f5f8383306040516020016103e09392919061071f565b6040516020818303038152906040528051906020012090505f6002825f6020811061040e5761040d61075b565b5b1a60f81b60f81c61041f91906107b5565b60ff161491505092915050565b5f8115159050919050565b6104408161042c565b82525050565b5f6020820190506104595f830184610437565b92915050565b5f5ffd5b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f61048c82610463565b9050919050565b61049c81610482565b81146104a6575f5ffd5b50565b5f813590506104b781610493565b92915050565b5f602082840312156104d2576104d161045f565b5b5f6104df848285016104a9565b91505092915050565b5f60ff82169050919050565b6104fd816104e8565b82525050565b5f6020820190506105165f8301846104f4565b92915050565b6105258161042c565b811461052f575f5ffd5b50565b5f813590506105408161051c565b92915050565b5f6020828403121561055b5761055a61045f565b5b5f61056884828501610532565b91505092915050565b5f82825260208201905092915050565b7f4368616c6c656e676520616c726561647920736f6c76656421000000000000005f82015250565b5f6105b5601983610571565b91506105c082610581565b602082019050919050565b5f6020820190508181035f8301526105e2816105a9565b9050919050565b7f596f75206861766520616c726561647920776f6e2100000000000000000000005f82015250565b5f61061d601583610571565b9150610628826105e9565b602082019050919050565b5f6020820190508181035f83015261064a81610611565b9050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f610688826104e8565b915060ff820361069b5761069a610651565b5b600182019050919050565b5f8160601b9050919050565b5f6106bc826106a6565b9050919050565b5f6106cd826106b2565b9050919050565b6106e56106e082610482565b6106c3565b82525050565b5f8160f81b9050919050565b5f610701826106eb565b9050919050565b610719610714826104e8565b6106f7565b82525050565b5f61072a82866106d4565b60148201915061073a8285610708565b60018201915061074a82846106d4565b601482019150819050949350505050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601260045260245ffd5b5f6107bf826104e8565b91506107ca836104e8565b9250826107da576107d9610788565b5b82820690509291505056fea264697066735822122053b44d351a34a3a7d641e2a86a1cf55fec17c764b9afb2693024d79310ef08d964736f6c634300081c0033
|
On se rend sur notre decompiler préféré, le mien est “Dedaub” : Lien Ici et on lui donne gentillement notre bytecode afin d’y voir plus clair (bonne blague).
On obtient un output de ce genre :
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
|
bool _solved; // STORAGE[0x0] bytes 0 to 0
mapping (address => uint8) owner; // STORAGE[0x1]
function fallback() public payable {
revert();
}
function isSolved() public payable {
return _solved;
}
function solved() public payable {
return _solved;
}
function 0xec7eba05(address varg0) public payable {
require(4 + (msg.data.length - 4) - 4 >= 32);
return owner[varg0];
}
function 0xed74d933(bool varg0) public payable {
require(4 + (msg.data.length - 4) - 4 >= 32);
require(!_solved, Error('Challenge already solved!'));
require(owner[msg.sender] < uint8(10), Error('You have already won!'));
require(0 < 32, Panic(50)); // access an out-of-bounds or negative index of bytesN array or slice
require(uint8(2), Panic(18)); // division by zero
if (varg0 - (uint8(uint8((byte(keccak256(msg.sender, owner[msg.sender], address(this)), 0x0)) << 248 >> 248) % uint8(2)) == 0)) {
owner[msg.sender] = 0;
} else {
require(owner[msg.sender] - uint8.max, Panic(17)); // arithmetic overflow or underflow
owner[msg.sender] = owner[msg.sender] + 1;
if (!(owner[msg.sender] - uint8(10))) {
_solved = 1;
}
}
}
function 0xfe27c0f6() public payable {
return uint8(10);
}
function __function_selector__( function_selector) public payable {
MEM[64] = 128;
require(!msg.value);
if (msg.data.length >= 4) {
if (0x64d98f6e == function_selector >> 224) {
isSolved();
} else if (0x799320bb == function_selector >> 224) {
solved();
} else if (0xec7eba05 == function_selector >> 224) {
0xec7eba05();
} else if (0xed74d933 == function_selector >> 224) {
0xed74d933();
} else if (0xfe27c0f6 == function_selector >> 224) {
0xfe27c0f6();
}
}
fallback();
}
|
Après un peu de lecture et de déduction, on rend ce code un peu plus propre et on identifie la partie qui nous intéresse :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
function 0xed74d933(bool varg0) public payable { //Fonction qui nous intéresse car elle manipule la variable "solved"
require(!_solved, Error('Challenge already solved!')); // Si déjà solve, normal
require(owner[msg.sender] < uint8(10), Error('You have already won!')); // Pareil
if (varg0 - (uint8(keccak256(msg.sender, owner[msg.sender], address(this))) % uint8(2)) == 0) { // Si notre argument (bool donc 0 ou 1) moins le résultat de : uint8(keccak256(MonAddresse, owner[msg.sender], AdresseContrat)) modulo 2 == 0 est de 1, soit ma valeur est différente de celle calculée, alors on remet à 0 le mapping.
owner[msg.sender] = 0;
}
//
else { //sinon, ajoute 1 au mapping
owner[msg.sender] = owner[msg.sender] + 1;
if (!(owner[msg.sender] - uint8(10))) { //si le mapping est = 10, alors solved = true et WIN WIN WIN
_solved = 1;
}
}
}
|
Etant donné qu’on se base ici sur des variables qui ne changent pas SAUF le mapping owner[msg.sender]
, on peut le faire de deux façons. Soit nous écrivons un contrat qui calcule automatiquement les conditions pour satisfaire à chaque appel de fonction les prérequis, soit nous le faisons à la main et dans ce cas, nous devons mémoriser le chemin au cas ou nous aurions un retour à 0.
Afin de faire un write-up propre, je vais créer un contrat qui va le faire automatiquement.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Attack {
address public target;
uint8 public steps = 0;
function setTarget(address _target) public {
target = _target;
steps = 0;
}
function solve() public {
require(target != address(0), "Target address is not set");
for (uint8 i = 0; i < 10; i++) {
bytes32 hash = keccak256(abi.encodePacked(address(this), steps, target));
bool choice = uint8(hash[0]) % 2 == 0;
steps++;
(bool success, ) = target.call(abi.encodeWithSelector(0xed74d933, choice));
require(success, "Call failed");
}
}
}
|
Solve
Une fois le contrat créé, on le déploie, et on appelle la fonction solve: $ATK = adresse du contrat attack, $TAR = adresse du contrat cible, $PK = clé privée, $RPC = URL du RPC
1
2
|
cast send $ATK "setTarget(address)" $TAR -r $RPC --private-key $PK
cast send $ATK "solve()" -r $RPC --private-key $PK
|
Win !
On récupère ensuite notre flag sur la plateforme.