Submitted on Dec 26th 2024 at 08:23:45 UTC by @gln for
Report ID: #38146
Report Type: Blockchain/DLT
Report severity: Medium
Target: https://github.com/status-im/nimbus-eth2
Impacts:
Direct loss of funds
Shutdown of greater than or equal to 10% or equal to but less than 33% of network processing nodes without brute force actions, but does not shut down the network
Description
Brief/Intro
Nimbus-eth2 libp2p incorrectly parses protobuf messages. As a result it will lead to denial of service issue.
Vulnerability Details
First we need to see how Nim converts uint64 to int type.
Consider the following simple nim program:
var bsize:uint64 = 0x80000000_00000000'u64
echo "casting..."
var offset:int = int(bsize)
echo "offset = ", offset
If you compile and run it, you will receive the exception:
casting...
../nim-2.2.0/lib/system/fatal.nim(53) sysFatal
Error: unhandled exception: value out of range [RangeDefect]
Error: execution of an external program failed:
So, If the value of uint64 is larger than 0x7fffffff_ffffffff, fatal RangeDefect exception will be thrown and program will stop.
In gossipsub protocol RPC messages are encoded by using protobuf.
In case of nimbus-eth2 it is handled by custom protobuf library - miniprotobuf.nim
Let's look at the code https://github.com/vacp2p/nim-libp2p/blob/8855bce0854ecf4adad7a0556bb2b2d2f98e0e20/libp2p/varint.nim#L106
proc getUVarint*[T: PB | LP](
vtype: typedesc[T],
pbytes: openArray[byte],
outlen: var int,
outval: var SomeUVarint,
): VarintResult[void] =
outlen = 0
outval = type(outval)(0)
let parsed = type(outval).fromBytes(pbytes, Leb128)
if parsed.len == 0:
return err(VarintError.Incomplete)
if parsed.len < 0:
return err(VarintError.Overflow)
when vtype is LP and sizeof(outval) == 8:
if parsed.val >= 0x8000_0000_0000_0000'u64:
return err(VarintError.Overflow)
if vsizeof(parsed.val) != parsed.len:
return err(VarintError.Overlong)
(outval, outlen) = parsed
ok()
If vtype is PB, there are no checks for parsed.val, it can be arbitrary large value
Now we need to see how protobuf parser is being used https://github.com/vacp2p/nim-libp2p/blob/8855bce0854ecf4adad7a0556bb2b2d2f98e0e20/libp2p/protocols/pubsub/rpc/protobuf.nim#L331
Let's look at the actual parser https://github.com/vacp2p/nim-libp2p/blob/8855bce0854ecf4adad7a0556bb2b2d2f98e0e20/libp2p/protobuf/minprotobuf.nim#L344
proc skipValue(data: var ProtoBuffer, header: ProtoHeader): ProtoResult[void] =
case header.wire
...
of ProtoFieldKind.Length:
var length = 0
var bsize = 0'u64
2) if PB.getUVarint(data.toOpenArray(), length, bsize).isOk():
data.offset += length
3) if bsize <= uint64(data.maxSize):
4) if data.isEnough(int(bsize)):
data.offset += int(bsize)
ok()
else:
err(ProtoError.MessageIncomplete)
else:
err(ProtoError.MessageTooBig)
else:
err(ProtoError.VarintDecode)
Note that maxSize is equal to uint.high
Varint is fetched from incoming stream
Even if bsize is larger than 0x7fffffff_ffffffff, the check will pass because data.maxSize is equal to 0xffffffff_ffffffff
Nim throws fatal exception when trying to convert bsize to 'int' type
Impact Details
Basically, attacker will be able to crash nimbus-eth2 nodes remotely with a single packet.