# #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**](https://immunefi.com/audit-competition/ethereum-protocol-attackathon)

* **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 inside`getData()` 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
```
