storage(bank)
contract
1 | pragma solidity ^0.4.24; |
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 | struct SafeBox { |
Finding the Bug
After compiling the game contract in Remix (or other IDEs), several warnings popped out:
1 | browser/Bank.sol:32:13: Warning: Uninitialized storage pointer. Did you mean ‘<type> memory info’? |
1 | browser/Bank.sol:45:9: Warning: Uninitialized storage pointer. Did you mean ‘<type> memory 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 | ----------------------------------------------------- |
According to the structure of FailedAttempt
, its layout in the storage is:
1 | ----------------------------------------------------- |
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 | ----------------------------------------------------- |
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:
- Calculate
target = keccak256(keccak256(msg.sender||3)) + 2
. - Calculate
base = keccak256(2)
. - Calculate
idx = (target - base) // 2
. - If
(target - base) % 2 == 1
, thenidx += 2
, and do step 7 twice. This happens when thetriedPass
of the first element offailedLogs
does not overlap with thecallback
variable, so we choose the second element instead. For ease of problem-solving, we only consider that(target - base) % 2 == 0
. - If
(msg.sender << (12*8)) < idx
, then choose another player account, and restart from step 1. This happens when the overwritten length ofsafeboxes
is not large enough to overlap withfailedLogs
.(the safeboxes’ length should greater than idx, or it doesn’t overlap the failedLogs) - Call
deposit(0x000000000000000000000000)
with 1 ether. - Call
withdraw(0, triedPass)
. - Call
withdraw(idx, any)
, and theSendFlag
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 | contract cal{ |
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).