#37113 [BC-Low] https://github.com/erigontech/erigon ), though it does not seem to be exploitable at

Submitted on Nov 25th 2024 at 19:13:18 UTC by @CertiK for Attackathon | Ethereum Protocol

  • Report ID: #37113

  • Report Type: Blockchain/DLT

  • Report severity: Low

  • Target: https://github.com/ledgerwatch/erigon

  • Impacts:

    • (Specifications) A bug in specifications with no direct impact on client implementations

Description

Brief/Intro

The utility function getData() mishandles an overflow case, which would lead to an out-of-range panic.

Vulnerability Details

The utility function getData() is utilized to take a subslice of a byte slice specified with the start position and size.

The implementation of getData() proceeds as follows:

  • Get the length of the input data;

  • Reset the start position start as the length of the input if start is larger than length;

  • Add the start with size to get end;

  • Reset the end as the length of the input if end is larger than length.

Besides the same result, function getDataAndAdjustedBounds() also returns the start and end - start.

https://github.com/erigontech/erigon/blob/v2.60.10/core/vm/common.go#L55

// getData returns a slice from the data based on the start and size and pads
// up to size with zero's. This function is overflow safe.
func getData(data []byte, start uint64, size uint64) []byte {
	length := uint64(len(data))
	if start > length {
		start = length
	}
	end := start + size
	if end > length {
		end = length
	}
	return common.RightPadBytes(data[start:end], int(size))
}

The issue lies in the step 3 that the addition of two uint64 integers, end := start + size could lead to overflow. The overflow would result in out-of-range panic when taking the subslice data[start:end]. In particular, the size is extremely large, and start is less than the length of the input.

The two utility functions are called in multiple places with non-constant start and size, including the ModExp precompile contract and opcode CODECOPY and EXTCODECOPY.

The ModExp precompile contract serves as a precompile contract to compute the modular exponentiation in big integers (i.e., base ** exponent mod module), outlined in the https://eips.ethereum.org/EIPS/eip-198, https://eips.ethereum.org/EIPS/eip-2565.

The entry point of all precompile contracts is the function RunPrecompiledContract() that invokes function RequiredGas() to compute the required gas to run the precompile contract and Run() to perform the actual execution.

https://github.com/erigontech/erigon/blob/v2.60.10/core/vm/contracts.go#L212

func RunPrecompiledContract(p PrecompiledContract, input []byte, suppliedGas uint64,
) (ret []byte, remainingGas uint64, err error) {
	gasCost := p.RequiredGas(input)
	if suppliedGas < gasCost {
		return nil, 0, ErrOutOfGas
	}
	suppliedGas -= gasCost
	output, err := p.Run(input)
	return output, suppliedGas, err
}

In the Run() of ModExp precompile contract, the function getData() is used to get the values of the base, exponent and module based on the specified byte length of base, exponent and module.

The input of the ModExp precompile contract Run() is a byte slice :

  • baseLen is the first 32 bytes of the input (position 0 ~ 32 );

  • expLen is the second 32 bytes of the input (position 32 ~ 64) ;

  • modLen is the third 32 bytes of the input (position 64 ~ 96);

  • base is the value stored in the position starting at 96 and with size baseLen;

  • exponent is the value stored in the position starting at 96+baseLen and with size expLen;

  • module is the value stored in the position starting at 96+baseLen+expLen and with size modLen;

https://github.com/erigontech/erigon/blob/v2.60.10/core/vm/contracts.go#L428

func (c *bigModExp) Run(input []byte) ([]byte, error) {
	var (
		baseLen = new(big.Int).SetBytes(getData(input, 0, 32)).Uint64()
		expLen  = new(big.Int).SetBytes(getData(input, 32, 32)).Uint64()
		modLen  = new(big.Int).SetBytes(getData(input, 64, 32)).Uint64()
	)
	if len(input) > 96 {
		input = input[96:]
	} else {
		input = input[:0]
	}
	// Handle a special case when both the base and mod length is zero
	if baseLen == 0 && modLen == 0 {
		return []byte{}, nil
	}
	// Retrieve the operands and execute the exponentiation
	var (
		base = new(big.Int).SetBytes(getData(input, 0, baseLen))
		exp  = new(big.Int).SetBytes(getData(input, baseLen, expLen))
		mod  = new(big.Int).SetBytes(getData(input, baseLen+expLen, modLen))
		v    []byte
	)
	switch {
	case mod.BitLen() == 0:
		// Modulo 0 is undefined, return zero
		return common.LeftPadBytes([]byte{}, int(modLen)), nil
	case base.Cmp(libcommon.Big1) == 0:
		//If base == 1, then we can just return base % mod (if mod >= 1, which it is)
		v = base.Mod(base, mod).Bytes()
	//case mod.Bit(0) == 0:
	//	// Modulo is even
	//	v = math.FastExp(base, exp, mod).Bytes()
	default:
		// Modulo is odd
		v = base.Exp(base, exp, mod).Bytes()
	}
	return common.LeftPadBytes(v, int(modLen)), nil
}

In the above invocations, the out-of-range panic insidegetData() could be triggered if baseLen+expLen is overflowed, particularly, baseLen is small but expLen is extremely large.

However, the gas cost of this execution is max of uint64, which makes the attack impossible. Similarly, the situation is applied in the opcodes CODECOPY and EXTCODECOPY with massive gas cost due to large memory expansion.

Impact Details

Given the current gas constraints, the issue does not appear to be exploitable at this time. However, as utility functions, improper usage or potential future adaptations could lead to unexpected results.

References

  • https://github.com/erigontech/erigon

  • https://eips.ethereum.org/EIPS/eip-198

  • https://eips.ethereum.org/EIPS/eip-2565

Proof of Concept

Proof of Concept

The PoC section provides a unit test with a crafted input to trigger the out-of-range panic inside the function getData() .

  • Set the input of RunPrecompiledContract() as follows. The parse of input shows that baseLen is 1 and expLen is max uint64:

"0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000ffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000ee"
  • Specify the gas as max uint64, the test script is as follows:

package vm


import (
   "bytes"
   "encoding/hex"
   "encoding/json"
   "fmt"
   "math"
   "os"
   "testing"
   "time"


   libcommon "github.com/ledgerwatch/erigon-lib/common"


   "github.com/ledgerwatch/erigon/common"
)


func TestModExpPrecompile(t *testing.T) {
   modExpContract := PrecompiledContractsCancun[libcommon.Address([]byte{0x05})]


   hexString := "0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000ffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000ee"


   input, err := hex.DecodeString(hexString)
   if err != nil {
      fmt.Printf("Error decoding hex: %v\n", err)
   }


   maxGas := uint64(math.MaxUint64)


   output, suppliedGas, err := RunPrecompiledContract(modExpContract, input, maxGas)
   if err != nil {
      fmt.Printf("ModExp precompile contract error: %v\n", err)
   } else {
      fmt.Printf("Output is: %x, supplied gas is: %d\n", output, suppliedGas)
   }
}
  • Run the unit test to check it triggers the out-of-range panic:

=== RUN   TestModExpPrecompile
--- FAIL: TestModExpPrecompile (0.00s)
panic: runtime error: cannot convert slice with length 1 to array or pointer to array with length 20 [recovered]
	panic: runtime error: cannot convert slice with length 1 to array or pointer to array with length 20

goroutine 25 [running]:
testing.tRunner.func1.2({0x101f5a460, 0xc000245848})
	/usr/local/go/src/testing/testing.go:1631 +0x24a
testing.tRunner.func1()
	/usr/local/go/src/testing/testing.go:1634 +0x377
panic({0x101f5a460?, 0xc000245848?})
	/usr/local/go/src/runtime/panic.go:770 +0x132
github.com/ledgerwatch/erigon/core/vm.TestModExpPrecompile(0xc00013c1a0?)
	/Users/xxx/immunefi/erigon/core/vm/contracts_test.go:35 +0x17
testing.tRunner(0xc00013c1a0, 0x101ff0298)
	/usr/local/go/src/testing/testing.go:1689 +0xfb
created by testing.(*T).Run in goroutine 1
	/usr/local/go/src/testing/testing.go:1742 +0x390


Process finished with the exit code 1

Was this helpful?