The key to exploiting this compiler vulnerability was creating an overlap in memory between the passed amounts array and the MintVars struct.
The problem in the compiler is that it doesn't check whether the array size in bytes would even fit into memory in the first place.
Therefore, we can have it overflow - the memory will "wrap around" back to the beginning, allowing it to overlap with other objects in memory.
Actually passing such a large array as calldata is not practically possible, but we don't have to do that: We can simply specify a large array length within the ABI encoding in the calldata even though the calldata sent is not large enough to hold any of it.
(ABI-encoded arrays' first 32-bytes slot specifies the array length, each 32-byte slot after that contains the actual array data - or in this case, none, although the size claims otherwise)
But it had me wondering: Why doesn't calldatacopy() consume all of the gas in the attempt to copy all those zero-items from calldata?
I wanted to take a look at the bytecode to see what actually happens. Problem is: For this rather old Solidity version there's no good way to do this - and none of the usual decompilers could handle it either. So I crammed out my good-old, half-backed decompiler attempt and it kinda worked. (see above)
So, when we pass an array with an item length of 3618502788666131106986593281521497120414687020801267626233049500247285301248 (as specified in the solution) the compiler attempted to calculate the size in bytes of the array's contents by multiplying the number with 32. A calculation that will overflow and result in an array size of exactly 0 bytes! (var8)
Solidity adds 0x20 (32 bytes for the array length slot) to this and adds it to the free memory pointer. So, for this huge array it actually only ended up reserving enough memory space to store the length.
It then continues to store the large array length to memory and finally uses calldatacopy() to copy the actual array contents from calldata to memory - which is basically a No-Op because the overflown array size is 0.
And this answers my question: It doesn't run out of gas, because the array contents that it actually ends up copying is nothing.
The issue becomes exploitable thanks to the fact that the MintVars v variable reserves more memory for itself after the array. Basically the first array's item is at the same place as the first element of the MintVars struct (and so on). That means, as you write into MintVars, you are as well writing into the array.
In hindsight it seems so simple, but it took a while to wrap my head around it (nearly overflowed!)
Thanks to the
@hexens guys for nerdsniping me (participate in their
@TheSecureum RACE tomorrow for more fun) and thanks to
@hrkrshnn and
@jonataspvt for their patience 😄
⚔ Vulnerabilities Visualized ⚔
Paradigm CTF Edition - "Swap" (2021)
The notorious "Swap" was unsolved for 2 MONTHS until
@HRitzdorf and
@a_permenev solved it.
In the spirit of
@paradigm hosting
@paradigm_ctf this weekend:
2 whole minutes of complex vuln visualization 👀 👇