Altcoin7 is the ultimate cryptocurrency forum.

Discussions are fun when we are part of a community.
Login Free Registration

Get 10 Altcoin7 Points just by registering on this forums.

The 0x vulnerability, explained

#1
On Friday July 12th, 0x shut down their v2 Exchange because a flaw in the signature verification routine meant that a signature of
Code:
0x04
was treated as a valid signature for all non-smart-contract accounts. This blog post explains how this is possible.
Background
0x is, when grossly oversimplified, a platform which allows users to trade with other users. If Alice wants to buy 1000 ZRX for 1 ETH, Alice can submit an order through the 0x protocol. If Bob then decides that he wants to take Alice's order, he can use 0x's Exchange contract to securely perform the exchange.
In order for the Exchange to make sure that Alice really did make the offer that Bob claims she's making, Bob needs to submit Alice's signature with the order data. This signature is the only thing preventing Bob from claiming that Alice is offering 1000000 ZRX for 1 wei, so it's imperative that signatures can't be forged.
0x supports several types of signatures. EIP712 and EthSign signatures are based on ECDSA signatures and are cryptographically secure, while Wallet and Validator signatures query an address for the validity of the provided signature. The Wallet signature in particular will query the sender address, and is intended to allow multi-signature wallets to make trades.
The Code
Let's take a look at how 0x verifies a Wallet signature.
Code:
function isValidWalletSignature(
   bytes32 hash,
   address walletAddress,
   bytes signature
)
   internal
   view
   returns (bool isValid)
{
   bytes memory calldata = abi.encodeWithSelector(
       IWallet(walletAddress).isValidSignature.selector,
       hash,
       signature
   );
   assembly {
       let cdStart := add(calldata, 32)
       let success := staticcall(
           gas,              // forward all gas
           walletAddress,    // address of Wallet contract
           cdStart,          // pointer to start of input
           mload(calldata),  // length of input
           cdStart,          // write output over input
           32                // output size is 32 bytes
       )

       switch success
       case 0 {
           // Revert with `Error("WALLET_ERROR")`
           /* snip */
           revert(0, 100)
       }
       case 1 {
           // Signature is valid if call did not revert and returned true
           isValid := mload(cdStart)
       }
   }
   return isValid;
}
Source
If we ignore the fact that this was written in inline assembly, it's fairly straightforward. First, lines 10-14 construct the ABI-encoded data which will be sent to the wallet. Lines 17-24 perform the call to the wallet. Finally, lines 26-34 check whether the call succeeded, and load the returned boolean.
The Problem
While the code to validate a Wallet signature was simple enough, it was written without knowledge of at least one of these two subtleties of the EVM:
1. Executing instructions outside the code is equivalent to executing STOP instructions
In most modern computers, executing undefined instructions means that your computer will execute garbage until the program crashes. However, the EVM is special because if execution happens to go outside the code of the smart contract, it's implicitly treated as a
Code:
STOP
instruction.
[Image: image-1.png]Section 9.4
A side effect of this is that accounts with no code can still be executed - they'll just immediately halt.
2. While the CALL family of instructions allow specifying where the output should be copied, the output area is not cleared beforehand
Take a look at this excerpt from the formal definition of the
Code:
CALL
instruction, where
Code:
µ[5]
is where in memory the return data should be copied,
Code:
µ[6]
is the length of the return data to be copied, and
Code:
o
is the data returned by the
Code:
CALL
instruction.
[Image: image.png]Appendix H
This states that only
Code:
n
bytes will be copied from the returned data to main memory, where
Code:
n
is the minimum of the number of bytes expected and the number of bytes returned. This implies that if less bytes are returned than expected, only the number of bytes returned will be copied to memory.
Going back to the validation routine, the authors instructed the EVM to overwrite the input data with the returned data, likely to save gas. Under normal operation, given a hash of
Code:
0xAA...AA
and a signature of
Code:
0x1CFF...FF
, the memory before and after a call might look something like this.
[Image: image-2.png]Before[Image: image-3.png]After
However, if there was no data returned by the call, then the memory after the call would look like this:
[Image: image-2.png]After
In other words, the memory is unchanged. Now, when the code on line 33 loads the first 32 bytes into a boolean variable, the nonzero value is coerced into
Code:
true
. Then, this
Code:
true
is returned from the function, indicating that "yes, the signature provided is fine" when it clearly isn't.
As for why the magic signature of
Code:
0x04
is always considered valid, it's because
Code:
0x04
is the ID number for a Wallet type signature, and the signature type is the last byte in the signature array.
Code:
// Allowed signature types.
enum SignatureType {
   Illegal,         // 0x00, default value
   Invalid,         // 0x01
   EIP712,          // 0x02
   EthSign,         // 0x03
   Wallet,          // 0x04
   Validator,       // 0x05
   PreSigned,       // 0x06
   NSignatureTypes  // 0x07, number of signature types. Always leave at end.
}
Source
Code:
function isValidSignature(
   bytes32 hash,
   address signerAddress,
   bytes memory signature
)
   public
   view
   returns (bool isValid)
{
   /* snip */

   // Pop last byte off of signature byte array.
   uint8 signatureTypeRaw = uint8(signature.popLastByte());
   
   /* snip */

   SignatureType signatureType = SignatureType(signatureTypeRaw);
   
   /* snip */
   if (signatureType == SignatureType.Wallet) {
       isValid = isValidWalletSignature(
           hash,
           signerAddress,
           signature
       );
       return isValid;
   }
   /* snip */
}
Source
The Solution
To their credit, 0x triaged and fixed this vulnerability in a couple of hours. The relevant commit can be viewed here, but there's really only two sections of interest.
The first change requires that the wallet address contains some code. This behavior matches what the Solidity compiler inserts before performing a function call.
Code:
if iszero(extcodesize(walletAddress)) {
   // Revert with `Error("WALLET_ERROR")`
   /* snip */
   revert(0, 100)
}
The second change requires the return data to be exactly 32 bytes long. This is also what the Solidity compiler inserts after performing a function call.
Code:
if iszero(eq(returndatasize(), 32)) {
   // Revert with `Error("WALLET_ERROR")`
   /* snip */
   revert(0, 100)
}
Conclusion
This isn't the first time that an EVM subtlety has bitten a smart contract developer, and it won't be the last. However, it's usually through incidents like this that we all learn about a new dangerous pattern to watch out for. Hopefully through this incident, future developers and auditors will be able to catch similar problems before they make it to mainnet.

Source : https://samczsun.com/the-0x-vulnerability-explained/
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  How torrents work - Explained Bushido 1 629 05-01-2019, 04:27 AM
Last Post: Mugen



Users browsing this thread: 1 Guest(s)