Writing of Arbitrary Storage Address Attack

Defining Writing of Arbitrary Storage Address Attack

Writing of Arbitrary Storage Address Attack is where a malicious user overwrites any storage variable. This can be used by an attacker to overwrite variables such as the owner of the contract or a withdrawal limit and produce unwanted results from the contract. This attack relies on expected Solidity behaviors and is independent from any compiler or Ethereum Virtual Machine exploitations.

Understanding Solidity Memory Layout

To understand this attack we must look at the memory layout of solidity and how variables in contracts are stored. In solidity the storage for a contract is addressed by 256-bit pointers in a single memory space. An offset is used to locate and store different variables declared in the contract. The single storage layout is designed to prevent collision or overlap of the different memory addresses. In the simplest cases (ignoring variable packing) every variable occupies a 32-byte space in memory which is allocated based on the order or declaration. The allocations start at address 0 (32-bytes) and are incremented by 1 for each address. Dynamic variables such as arrays or maps cannot be stored sequentially in memory and thus hashing is used to solve this issue. In the case of a map the value mapped by the key (k) is stored at address keccak256(kp) where p is the memory space that would have been occupied by the map if it was a static type (reserved space). Keccak256() is a one-way cryptographic hashing function used in solidity which is believed to be cryptographically secure. If an attacker were able to find k where kp hashes to all 0 bytes then the first storage space could be overwritten. However, this is extremely difficult to do since keccak256 output range is extremely large and infeasible in a plausible amount of time. For dynamic arrays the length (uint256) is stored in the reserved slot and the data within the array is located sequentially at address keccak256(p).

Dynamic Data Structures

For most contracts user-provided data must go through keccak256 before having a change to affect storage locations, which the output cannot be modified. Dynamic arrays however can be exploited since they are stored sequentially after their hashed offset. If an attack can control the index into the dynamic array then they have control over the addresses within the bounds of the array. If the length variable underflows or overflows the value will wrap and without the proper checks all indices become valid between (2^256 - 1). Now the attacker has access to almost the entire storage address space and can overwrite the value at an address.


In most real-world cases the array manipulation would be more difficult to find and often wrapped up in the core features of the specific contract. When working with dynamic arrays within solidity, smart contracts backdoors could be introduced by accident or as a small mistake that can lead to serious exploitation's.


Before deploying the contract lets look at the OverwriteOwner() function. This is a function that would be created and called outside the contract but for the sake of the demonstration was included inside. For this contract we are attempting to overwrite the owner to a different address. We know our dynamic array is declared as the 4th variable and based on zero indexing starts at keccack256(0x3). To calculate that address you can go to the Remix Online IDE console and enter "web3.utils.sha3("0x3")". Now we know the starting address of the dynamic array we can use that to calculate the wrap around address to get to the owner variable. Using the formula (2^256 - ARRAY_START_ADDRESS + OVERWRITE_VARIABLE_ORDER).

Now we can deploying the Wallet contract on blockchain. We recommend using the Remix Online IDE with setup instruction found here. The Wallet contract constructor sets the owner and creates a dynamic array with a length of 0.

Before Pop()

After Pop()

Expanding the contract we can see all the functions available to call and views into the public variables. Upon viewing the length variable of the dynamic array we can see that length was initialized to 0. We can also see that owner has an address of "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4". Now to run the attack we first must call the Pop() method to underflow that dynamic array length. This will cause the array length to underflow and wrap to 2^256 - 1.

Before OverwriteOwner()

After OverwriteOwner()

Now we can call the OverwriteOwner() function which calculates the wrap around address of the owner variable and then call the Insert() function with the memory address of the owner variable and new owner value. In this example we are zeroing out the owner address. This attack could be used to change the owner of the contract and steal funds from the actual owner. In another scenario a withdraw limit could be changed and allow the attacker to withdraw the entire balance of the contract.

In the demonstration above the vulnerability was in the Pop() function. The length check of the array was looking for a value that was greater than or equal to 0. The check should have instead been looking for greater than 0. This check would prevent the array length from under flowing and thus the attack is not possible. To reiterate in most real-world cases the array manipulation would be more difficult to find and often wrapped up in the core features of the specific contract.

Solutions Available

Index prior to hashing: One potential solution is to apply the dynamic array’s index prior to hashing instead of after. This would produce the same effect as the map described above with a length variable. However with this solution would make the array non-sequential in memory which would create more CPU cache misses and create performance issues depending on array size.


Overflow/Underflow Error Handling: Whenever resizing a dynamic array ensures that checks are in place for an empty array or overflow of total memory space. If possible prevent users from directly modifying array lengths or place strict requirements on user input value.


Dynamic Memory: Consider if a dynamic data structure is required or if a static data structure can be used. Since all data structures share the same storage space it is imperative to make sure that one data structure cannot inadvertently overwrite the memory address of another data structure.

References

Chang, X., Zhu, J., & Zhao, S. (2020). Dynamic array double-access attack in Ethereum. 2020 IEEE 6th International Conference on Computer and Communications (ICCC). https://doi.org/10.1109/iccc51575.2020.9345014

Solidity. Solidity. (n.d.). Retrieved October 31, 2022, from https://docs.soliditylang.org/en/v0.8.17/index.html

Li, D., Sun, Y., Zhang, K., & Ding, Y. (2022). Multiple-layer security threats on the ethereum blockchain and their countermeasures. Security and Communication Networks, 2022 doi:https://doi.org/10.1155/2022/5307697

Tabora, V. (2022, May 29). Hashing functions in solidity using KECCAK256. Medium. Retrieved October 31, 2022, from https://medium.com/0xcode/hashing-functions-in-

solidity-using-keccak256-70779ea55bb0