diff --git a/.gitignore b/.gitignore index a8f241fb91..bcb8c782cb 100644 --- a/.gitignore +++ b/.gitignore @@ -266,3 +266,4 @@ launchSettings.json # Benchmarks **/BenchmarkDotNet.Artifacts/ +/fuzzers/Neo.VM.Fuzzer/fuzzer-output/ diff --git a/fuzzers/Neo.VM.Fuzzer/Documentation/DOSDetection.md b/fuzzers/Neo.VM.Fuzzer/Documentation/DOSDetection.md new file mode 100644 index 0000000000..ac55fc9159 --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/Documentation/DOSDetection.md @@ -0,0 +1,176 @@ +# DOS Detection in Neo VM Fuzzer + +## Overview + +This document describes the Denial of Service (DOS) detection capabilities implemented in the Neo VM Fuzzer. The fuzzer is designed to identify potential DOS vectors in Neo VM scripts that could be exploited to cause resource exhaustion or performance degradation in the Neo blockchain. + +DOS attacks in a virtual machine context typically exploit inefficient execution paths that consume excessive resources. In the Neo VM, these can manifest as: + +1. **Computational DOS**: Scripts that execute an excessive number of operations +2. **Memory DOS**: Scripts that consume excessive memory through stack manipulation +3. **Infinite Loops**: Scripts that create execution paths that never terminate +4. **Exponential Complexity**: Scripts that trigger exponential growth in resource usage + +## Detection Mechanisms + +The DOS detection system analyzes script execution metrics to identify potential DOS vectors based on the following criteria: + +1. **High Instruction Count**: Scripts that execute an excessive number of instructions may indicate a DOS vector. The current threshold is set to 100 instructions. + +2. **Excessive Stack Depth**: Scripts that create deep stacks may cause memory exhaustion. The current threshold is set to 5 stack items. + +3. **Slow Opcodes**: Scripts that repeatedly execute opcodes with high average execution times may indicate a DOS vector. The current thresholds are: + - Average execution time > 0.05ms + - Executed more than 2 times + +4. **Long Execution Time**: Scripts that take a long time to execute may indicate a DOS vector. The current threshold is set to 10ms. + +5. **Potential Infinite Loops**: Scripts that contain patterns indicative of infinite loops are flagged as potential DOS vectors. + +## Detection Strategy + +The enhanced fuzzer: + +1. Tracks detailed execution metrics: + - Instruction count per opcode + - Stack depth throughout execution + - Memory allocations + - Execution time per opcode + - Branch decision patterns + +2. Identifies suspicious patterns: + - Operations with execution time significantly above average + - Scripts with high instruction-to-progress ratios + - Repeated state patterns indicating potential infinite loops + - Exponential growth in resource consumption + +3. Scores and ranks scripts by their "DOS potential" + +## Implementation + +The DOS detection system is implemented in the following components: + +1. **DOSDetector**: The main class responsible for analyzing execution metrics and detecting potential DOS vectors. + - Located in `Utils/DOSDetector.cs` + - Configurable thresholds for different detection mechanisms + - Calculates a DOS score based on multiple factors + - Tracks execution time per opcode + - Monitors stack depth and memory usage + - Identifies potential infinite loops by tracking instruction pointer frequencies + - Provides detailed analysis results with recommendations + + Key methods: + - `OnStep`: Handles step events from the execution engine to track metrics + - `AnalyzeExecution`: Analyzes collected metrics to calculate a DOS score + - `GenerateReport`: Creates a detailed report of the analysis results + +2. **VMRunner**: Integrates with the DOSDetector to analyze script execution. + - Located in `Runners/VMRunner.cs` + - Performs DOS analysis for both successful and crashed script executions + - Collects execution metrics such as instruction count, stack depth, and execution time + - Saves potential DOS vectors to the corpus + + Key methods: + - `Execute`: Executes a script and performs DOS analysis + - `SaveDOSVector`: Saves a potential DOS vector to the corpus + +3. **InstrumentedExecutionEngine**: Tracks detailed execution metrics used for DOS detection. + - Located in `Runners/InstrumentedExecutionEngine.cs` + - Monitors instruction execution, stack operations, and execution time + - Provides hooks for the DOSDetector to monitor execution + - Records opcode execution times and frequencies + + Key methods: + - `Execute`: Executes a script with detailed instrumentation + - `OnStep`: Fires when an instruction is executed, providing metrics to subscribers + +## DOS Score Calculation + +The DOS score is calculated based on multiple factors, with each factor contributing a portion to the overall score: + +- **Instruction Count**: Up to 0.5 points based on the number of instructions executed +- **Stack Depth**: Up to 0.3 points based on the maximum stack depth +- **Slow Opcodes**: Up to 0.3 points based on the presence of slow opcodes +- **Execution Time**: Up to 0.5 points based on the total execution time +- **Potential Infinite Loops**: Up to 0.5 points based on loop detection + +A script is considered a potential DOS vector if its DOS score exceeds the configured threshold (default: 0.1). + +## Workflow Integration + +The DOS detection is integrated into the fuzzing workflow: + +1. The fuzzer executes a script with the instrumented execution engine +2. The DOSDetector analyzes the execution metrics +3. If the DOS score exceeds the threshold, the script is flagged as a potential DOS vector +4. The fuzzer saves the potential DOS vector to the corpus +5. The fuzzer includes the DOS vector in the fuzzing results + +### DOS Vector Analysis File Format + +Each DOS vector analysis file contains: + +``` +DOS Vector Analysis: dos-20250326-055929-0_80-High_instruction_count__3411;_Excessive_stack_depth__2048-7b56e5b8 +Timestamp: 3/26/2025 5:59:29AM +DOS Score: 0.80 +Detection Reason: High instruction count: 3411; Excessive stack depth: 2048 + +Metrics: + TotalInstructions: 3411 + MaxStackDepth: 2048 + UniqueOpcodes: 0 + TotalExecutionTimeMs: 3.7773 + InstructionScore: 0.5 + LoopScore: 0 + StackScore: 0.3 + +Recommendations: + - Consider adding instruction count limits to prevent excessive execution + - Consider adding stack depth limits to prevent stack overflow attacks +``` + +## Test Scripts + +The following test scripts are provided to verify the DOS detection functionality: + +1. **minimal_dos_vector.neo**: A simple script that triggers DOS detection based on execution time +2. **stack_depth_dos.neo**: A script that focuses on excessive stack depth +3. **comprehensive_dos_vector.neo**: A script that combines multiple DOS vectors +4. **final_dos_test.neo**: A comprehensive test script that triggers multiple DOS detection mechanisms + +## Recent Enhancements + +1. **DOS Detection for Crashed Scripts**: Modified the VMRunner to perform DOS analysis even when scripts crash with exceptions, ensuring we can detect potential DOS vectors in all scripts. + +2. **Adjusted Detection Thresholds**: Lowered the thresholds to make the detection more sensitive: + - Reduced instruction count threshold from 5000 to 100 + - Reduced stack depth threshold from 50 to 5 + - Reduced slow opcode threshold from 0.2ms to 0.05ms + - Reduced execution time threshold from 500ms to 10ms + +3. **Enhanced Logging**: Added detailed logging about the DOS detection process to help debug and understand why scripts are or aren't being detected as DOS vectors. + +## Future Improvements + +1. **Improved Loop Detection**: Enhance the detection of potential infinite loops by analyzing execution patterns. +2. **Dynamic Thresholds**: Implement dynamic thresholds based on the average execution metrics of the corpus. +3. **Opcode-Specific Analysis**: Implement more detailed analysis of specific opcodes known to be resource-intensive. +4. **Memory Usage Analysis**: Add detection for scripts that consume excessive memory. +5. **Machine Learning-Based Detection**: Implement anomaly detection for more precise identification of DOS vectors. +6. **Automatic Test Case Generation**: Generate test cases that specifically target DOS vulnerabilities. +7. **Formal Verification Integration**: Integrate with formal verification tools to prove absence of certain DOS vectors. +8. **Automatic Mitigation Recommendations**: Generate mitigation recommendations based on detected patterns. + +## Usage + +To enable DOS detection when running the fuzzer, use the following command-line options: + +```bash +dotnet run -- --detect-dos --dos-threshold 0.1 --track-opcodes --track-memory +``` + +- `--detect-dos`: Enables DOS detection +- `--dos-threshold`: Sets the threshold for DOS detection (default: 0.1) +- `--track-opcodes`: Enables tracking of opcode execution times (required for DOS detection) +- `--track-memory`: Enables tracking of memory usage (recommended for DOS detection) diff --git a/fuzzers/Neo.VM.Fuzzer/Documentation/EXTENDING.md b/fuzzers/Neo.VM.Fuzzer/Documentation/EXTENDING.md new file mode 100644 index 0000000000..2490aa6737 --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/Documentation/EXTENDING.md @@ -0,0 +1,577 @@ +# Extending the Neo VM Fuzzer + +This document provides guidance on how to extend and customize the Neo VM Fuzzer for specific testing needs. + +## Architecture Overview + +Before extending the fuzzer, familiarize yourself with the architecture described in [FUZZER_ARCHITECTURE.md](FUZZER_ARCHITECTURE.md). The fuzzer is designed with modularity in mind, making it straightforward to extend or replace individual components. + +## Extension Points + +### 1. Script Generation + +The `ScriptGenerator` class is responsible for creating random Neo VM scripts. You can extend it to: + +- Add new script generation strategies +- Implement domain-specific script patterns +- Create targeted scripts for specific VM features + +#### Example: Adding a New Script Generation Strategy + +```csharp +public class EnhancedScriptGenerator : ScriptGenerator +{ + public EnhancedScriptGenerator(Random random) : base(random) + { + } + + // Add a new method for generating scripts with specific patterns + public byte[] GenerateStackHeavyScript(int maxInstructions = 100) + { + var scriptBuilder = new ScriptBuilder(); + + // Generate a script that heavily exercises stack operations + for (int i = 0; i < maxInstructions; i++) + { + // Add stack-related operations + if (_random.Next(3) == 0) + { + scriptBuilder.Emit(OpCode.PUSH1); + } + else if (_random.Next(3) == 1) + { + scriptBuilder.Emit(OpCode.DUP); + } + else + { + scriptBuilder.Emit(OpCode.SWAP); + } + } + + scriptBuilder.Emit(OpCode.RET); + return scriptBuilder.ToArray(); + } +} +``` + +### 2. VM Execution + +The `VMRunner` class handles script execution in the Neo VM. You can extend it to: + +- Add new instrumentation for specific metrics +- Implement alternative execution strategies +- Add specialized error detection + +#### Example: Enhanced Coverage Tracking + +```csharp +public class EnhancedVMRunner : VMRunner +{ + public EnhancedVMRunner(int timeoutMs = 5000) : base(timeoutMs) + { + } + + // Override to add enhanced coverage tracking + protected override void InstrumentEngine(ExecutionEngine engine, VMExecutionResult result) + { + base.InstrumentEngine(engine, result); + + // Add additional instrumentation + engine.OnStep += (sender, e) => + { + // Track additional metrics + var context = engine.CurrentContext; + if (context != null) + { + // Track branch decisions + if (e.OpCode == OpCode.JMPIF || e.OpCode == OpCode.JMPIFNOT) + { + bool condition = context.EvaluationStack.Peek().GetBoolean(); + result.Coverage.Add($"Branch:{e.InstructionPointer}:{condition}"); + } + + // Track stack depth patterns + result.Coverage.Add($"StackPattern:{context.EvaluationStack.Count % 10}"); + } + }; + } +} +``` + +### 3. Corpus Management + +The `CorpusManager` class manages the collection of test scripts. You can extend it to: + +- Implement different corpus selection strategies +- Add script normalization or minimization +- Implement corpus distillation + +#### Example: Script Minimization + +```csharp +public class EnhancedCorpusManager : CorpusManager +{ + public EnhancedCorpusManager(string outputDir, string? corpusDir = null) + : base(outputDir, corpusDir) + { + } + + // Add script minimization capability + public byte[] MinimizeScript(byte[] script, Func testFunction) + { + byte[] minimized = script.ToArray(); + bool changed; + + do + { + changed = false; + + // Try removing chunks of the script + for (int chunkSize = 16; chunkSize >= 1; chunkSize /= 2) + { + for (int i = 0; i < minimized.Length - chunkSize; i++) + { + byte[] candidate = new byte[minimized.Length - chunkSize]; + Buffer.BlockCopy(minimized, 0, candidate, 0, i); + Buffer.BlockCopy(minimized, i + chunkSize, candidate, i, minimized.Length - i - chunkSize); + + if (testFunction(candidate)) + { + minimized = candidate; + changed = true; + break; + } + } + + if (changed) break; + } + } while (changed); + + return minimized; + } +} +``` + +### 4. Coverage Analysis + +The `CoverageTracker` class monitors code coverage. You can extend it to: + +- Implement different coverage metrics +- Add visualization capabilities +- Implement differential coverage analysis + +#### Example: Branch Coverage + +```csharp +public class BranchCoverageTracker : CoverageTracker +{ + private readonly HashSet _branchCoverage = new HashSet(); + + // Track branch coverage specifically + public bool HasNewBranchCoverage(HashSet coverage) + { + bool hasNewBranches = false; + + foreach (var point in coverage) + { + if (point.StartsWith("Branch:") && _branchCoverage.Add(point)) + { + hasNewBranches = true; + } + } + + return hasNewBranches; + } + + // Generate branch coverage report + public string GetBranchCoverageReport() + { + var report = new System.Text.StringBuilder(); + + var branches = _branchCoverage + .Where(c => c.StartsWith("Branch:")) + .Select(c => c.Substring(7)) + .ToList(); + + report.AppendLine($"Total Branch Coverage: {branches.Count} branches"); + + // Group by instruction pointer + var branchGroups = branches + .Select(b => b.Split(':')) + .GroupBy(parts => parts[0]) + .ToList(); + + foreach (var group in branchGroups) + { + var trueCount = group.Count(parts => parts[1] == "True"); + var falseCount = group.Count(parts => parts[1] == "False"); + + report.AppendLine($"Branch at {group.Key}: True={trueCount}, False={falseCount}"); + } + + return report.ToString(); + } +} +``` + +## Adding New Features + +### 1. Custom Mutation Strategies + +You can implement custom mutation strategies to target specific aspects of the Neo VM: + +```csharp +public static class MutationStrategies +{ + // Mutate jump targets specifically + public static byte[] MutateJumpTargets(byte[] script, Random random) + { + byte[] result = script.ToArray(); + + // Find jump instructions + for (int i = 0; i < result.Length - 1; i++) + { + OpCode opcode = (OpCode)result[i]; + + if (opcode == OpCode.JMP || opcode == OpCode.JMPIF || + opcode == OpCode.JMPIFNOT || opcode == OpCode.CALL) + { + // Mutate the jump offset + result[i + 1] = (byte)random.Next(256); + } + } + + return result; + } + + // Focus on arithmetic operations + public static byte[] MutateArithmetic(byte[] script, Random random) + { + byte[] result = script.ToArray(); + + // Define arithmetic opcodes + OpCode[] arithmeticOpcodes = new[] + { + OpCode.ADD, OpCode.SUB, OpCode.MUL, OpCode.DIV, OpCode.MOD, + OpCode.SHL, OpCode.SHR, OpCode.BOOLAND, OpCode.BOOLOR, + OpCode.NUMEQUAL, OpCode.NUMNOTEQUAL, OpCode.LT, OpCode.GT + }; + + // Replace some opcodes with arithmetic ones + for (int i = 0; i < result.Length; i++) + { + if (random.Next(10) == 0) + { + result[i] = (byte)arithmeticOpcodes[random.Next(arithmeticOpcodes.Length)]; + } + } + + return result; + } +} +``` + +### 2. Smart Contract-Specific Fuzzing + +For fuzzing Neo smart contracts, you can add contract-specific components: + +```csharp +public class SmartContractFuzzer +{ + private readonly ScriptGenerator _generator; + private readonly VMRunner _runner; + + public SmartContractFuzzer(ScriptGenerator generator, VMRunner runner) + { + _generator = generator; + _runner = runner; + } + + // Generate a script that calls a specific smart contract method + public byte[] GenerateContractMethodCall(byte[] contractScript, string method, int paramCount) + { + var scriptBuilder = new ScriptBuilder(); + + // Generate random parameters + for (int i = 0; i < paramCount; i++) + { + // Add random parameter based on type + switch (i % 4) + { + case 0: // Integer + scriptBuilder.Emit(OpCode.PUSHINT32); + scriptBuilder.Emit(BitConverter.GetBytes(new Random().Next())); + break; + case 1: // Boolean + scriptBuilder.Emit(new Random().Next(2) == 0 ? OpCode.PUSHF : OpCode.PUSHT); + break; + case 2: // String + byte[] strBytes = Encoding.UTF8.GetBytes($"param{i}"); + scriptBuilder.Emit(OpCode.PUSHDATA1); + scriptBuilder.Emit((byte)strBytes.Length); + foreach (byte b in strBytes) + scriptBuilder.Emit(b); + break; + case 3: // Array + scriptBuilder.Emit(OpCode.NEWARRAY0); + break; + } + } + + // Add method name + byte[] methodBytes = Encoding.UTF8.GetBytes(method); + scriptBuilder.Emit(OpCode.PUSHDATA1); + scriptBuilder.Emit((byte)methodBytes.Length); + foreach (byte b in methodBytes) + scriptBuilder.Emit(b); + + // Add parameter count and call + scriptBuilder.Emit(OpCode.PUSHINT8); + scriptBuilder.Emit((byte)paramCount); + + // Append contract script + foreach (byte b in contractScript) + scriptBuilder.Emit(b); + + scriptBuilder.Emit(OpCode.RET); + + return scriptBuilder.ToArray(); + } +} +``` + +### 3. Differential Fuzzing + +To compare execution between different VM versions: + +```csharp +public class DifferentialFuzzer +{ + private readonly ScriptGenerator _generator; + + public DifferentialFuzzer(ScriptGenerator generator) + { + _generator = generator; + } + + // Compare execution between two VM versions + public bool CompareExecution(byte[] script, string vmVersion1, string vmVersion2) + { + // Load the first VM version + var engine1 = LoadVMVersion(vmVersion1); + var result1 = ExecuteScript(engine1, script); + + // Load the second VM version + var engine2 = LoadVMVersion(vmVersion2); + var result2 = ExecuteScript(engine2, script); + + // Compare results + return CompareResults(result1, result2); + } + + private ExecutionEngine LoadVMVersion(string version) + { + // Implementation would load a specific VM version + // This is a placeholder + return new ExecutionEngine(); + } + + private VMExecutionResult ExecuteScript(ExecutionEngine engine, byte[] script) + { + var result = new VMExecutionResult(); + + try + { + engine.LoadScript(script); + result.State = engine.Execute(); + + // Capture result stack + if (engine.ResultStack != null) + { + foreach (var item in engine.ResultStack) + { + // Store result for comparison + } + } + } + catch (Exception ex) + { + result.Crashed = true; + result.ExceptionMessage = ex.Message; + } + + return result; + } + + private bool CompareResults(VMExecutionResult result1, VMExecutionResult result2) + { + // Compare VM states + if (result1.State != result2.State) + return false; + + // Compare crash status + if (result1.Crashed != result2.Crashed) + return false; + + // Compare exception messages if crashed + if (result1.Crashed && result1.ExceptionMessage != result2.ExceptionMessage) + return false; + + // Compare result stacks + // Implementation would compare stack items + + return true; + } +} +``` + +## Integration with CI/CD + +To integrate the fuzzer with continuous integration: + +1. Create a dedicated fuzzing project: + +```csharp +public class ContinuousFuzzer +{ + private readonly string _outputDir; + private readonly string _corpusDir; + + public ContinuousFuzzer(string outputDir, string corpusDir) + { + _outputDir = outputDir; + _corpusDir = corpusDir; + } + + public int Run(int iterations, int timeoutMinutes) + { + // Set up components + var random = new Random(); + var scriptGenerator = new ScriptGenerator(random); + var vmRunner = new VMRunner(5000); + var corpusManager = new CorpusManager(_outputDir, _corpusDir); + var coverageTracker = new CoverageTracker(); + + // Load corpus + corpusManager.LoadCorpus(); + + // Set timeout + var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(timeoutMinutes)); + + int crashCount = 0; + + try + { + // Main fuzzing loop + for (int i = 0; i < iterations && !cancellationTokenSource.Token.IsCancellationRequested; i++) + { + // Generate script + byte[] script = random.Next(10) < 3 + ? corpusManager.GetRandomScript() + : scriptGenerator.GenerateRandomScript(); + + // Execute script + var result = vmRunner.RunScript(script); + + // Handle result + if (result.Crashed) + { + crashCount++; + corpusManager.SaveCrash(script, result.ExceptionMessage); + } + else if (coverageTracker.HasNewCoverage(result.Coverage)) + { + corpusManager.SaveInteresting(script); + } + } + } + catch (OperationCanceledException) + { + // Timeout reached + } + + // Save coverage report + coverageTracker.SaveCoverageReport(Path.Combine(_outputDir, "coverage_report.txt")); + + return crashCount; + } +} +``` + +2. Create a CI script that runs the fuzzer and reports results. + +## Performance Optimization + +To optimize fuzzer performance: + +1. Implement parallel fuzzing: + +```csharp +public class ParallelFuzzer +{ + public void RunParallel(int iterations, int threadCount) + { + // Shared components + var coverageTracker = new CoverageTracker(); + var corpusManager = new CorpusManager("output", "corpus"); + + // Load corpus + corpusManager.LoadCorpus(); + + // Create a thread-safe queue for interesting scripts + var interestingScripts = new ConcurrentQueue(); + + // Run fuzzing in parallel + Parallel.For(0, threadCount, threadId => + { + // Thread-local components + var random = new Random(threadId); + var scriptGenerator = new ScriptGenerator(random); + var vmRunner = new VMRunner(5000); + + for (int i = 0; i < iterations / threadCount; i++) + { + // Generate script + byte[] script; + if (random.Next(10) < 3 && corpusManager.CorpusSize > 0) + { + script = corpusManager.GetRandomScript(); + script = scriptGenerator.MutateScript(script); + } + else + { + script = scriptGenerator.GenerateRandomScript(); + } + + // Execute script + var result = vmRunner.RunScript(script); + + // Handle result + if (result.Crashed) + { + lock (corpusManager) + { + corpusManager.SaveCrash(script, result.ExceptionMessage); + } + } + else if (coverageTracker.HasNewCoverage(result.Coverage)) + { + interestingScripts.Enqueue(script); + } + } + }); + + // Process interesting scripts + foreach (var script in interestingScripts) + { + corpusManager.SaveInteresting(script); + } + } +} +``` + +## Conclusion + +The Neo VM Fuzzer is designed to be extensible and adaptable to various testing needs. By following the patterns and examples in this document, you can customize the fuzzer to focus on specific aspects of the Neo VM or to integrate with your development workflow. + +For further assistance or to contribute improvements to the fuzzer, please submit issues or pull requests to the Neo project repository. diff --git a/fuzzers/Neo.VM.Fuzzer/Documentation/EventArgs.md b/fuzzers/Neo.VM.Fuzzer/Documentation/EventArgs.md new file mode 100644 index 0000000000..17bfd7ee12 --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/Documentation/EventArgs.md @@ -0,0 +1,45 @@ +# Event Arguments + +This document describes the event argument classes used in the Neo VM Fuzzer for tracking execution events. + +## StepEventArgs + +The `StepEventArgs` class provides information about each step of execution in the Neo VM. It is used to track instruction execution and gather metrics for fuzzing analysis. + +### Properties + +- **OpCode**: The operation code being executed +- **InstructionPointer**: The current instruction pointer position +- **StackSize**: The current size of the evaluation stack + +### Usage + +```csharp +// Example of handling a step event +engine.OnStep += (sender, e) => { + Console.WriteLine($"Executing opcode: {e.OpCode} at position {e.InstructionPointer}"); + Console.WriteLine($"Current stack size: {e.StackSize}"); +}; +``` + +## FaultEventArgs + +The `FaultEventArgs` class provides information about faults that occur during script execution. It is used to track and analyze errors that occur during fuzzing. + +### Properties + +- **ExceptionType**: The type of exception that occurred +- **ExceptionMessage**: The detailed message of the exception +- **InstructionPointer**: The instruction pointer position where the fault occurred + +### Usage + +```csharp +// Example of handling a fault event +engine.OnFault += (sender, e) => { + Console.WriteLine($"Fault occurred at position {e.InstructionPointer}"); + Console.WriteLine($"Exception: {e.ExceptionType} - {e.ExceptionMessage}"); +}; +``` + +These event argument classes are essential for tracking execution flow and identifying interesting behaviors during fuzzing runs. They enable detailed analysis of script execution and help identify potential vulnerabilities or bugs in the Neo VM. diff --git a/fuzzers/Neo.VM.Fuzzer/Documentation/FUZZER_ARCHITECTURE.md b/fuzzers/Neo.VM.Fuzzer/Documentation/FUZZER_ARCHITECTURE.md new file mode 100644 index 0000000000..a9bd0c4d1e --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/Documentation/FUZZER_ARCHITECTURE.md @@ -0,0 +1,126 @@ +# Neo VM Fuzzer Architecture + +## Overview + +The Neo VM Fuzzer is a specialized tool designed to test the robustness and security of the Neo Virtual Machine (Neo VM) through automated fuzzing techniques. This document describes the architecture, components, and workflow of the fuzzing engine. + +## Purpose + +The primary goals of the Neo VM Fuzzer are: + +1. **Identify bugs and vulnerabilities** in the Neo VM implementation through automated testing +2. **Improve test coverage** by exercising code paths that might not be covered by traditional unit tests +3. **Ensure stability** of the Neo VM when processing unexpected or malformed inputs +4. **Validate error handling** mechanisms in the Neo VM + +## Architecture + +The Neo VM Fuzzer follows a modular architecture with the following key components: + +``` +Neo.VM.Fuzzer/ +├── Program.cs # Entry point and main fuzzing loop +├── Generators/ # Script generation components +│ └── ScriptGenerator.cs # Generates random but valid Neo VM scripts +├── Runners/ # VM execution components +│ └── VMRunner.cs # Executes scripts in the Neo VM with instrumentation +├── Utils/ # Utility components +│ ├── CorpusManager.cs # Manages the corpus of test scripts +│ └── CoverageTracker.cs # Tracks code coverage during fuzzing +└── Documentation/ # Documentation files + ├── FUZZER_ARCHITECTURE.md # This architecture document + ├── USAGE.md # Usage instructions + └── EXTENDING.md # Guide for extending the fuzzer +``` + +## Components + +### Script Generator + +The `ScriptGenerator` component is responsible for creating random but valid Neo VM scripts. It: + +- Generates scripts with various opcodes and operands +- Ensures structural validity of the generated scripts +- Provides mutation capabilities to evolve existing scripts + +The generator understands the Neo VM instruction set and ensures that generated scripts follow the correct format, including proper operand sizes and structure. + +### VM Runner + +The `VMRunner` component executes the generated scripts in the Neo VM and collects execution data. It: + +- Runs scripts with timeout protection +- Captures exceptions and error states +- Instruments the VM to collect coverage information +- Tracks execution metrics like time and memory usage + +This component uses a custom instrumented version of the Neo VM's `ExecutionEngine` to gather detailed information about script execution. + +### Corpus Manager + +The `CorpusManager` component maintains a collection of interesting scripts for fuzzing. It: + +- Saves scripts that cause crashes or exceptions +- Maintains a corpus of scripts that achieve unique code coverage +- Loads existing scripts from a corpus directory +- Provides access to scripts for mutation and evolution + +### Coverage Tracker + +The `CoverageTracker` component monitors which parts of the Neo VM code are exercised during fuzzing. It: + +- Tracks unique code paths and execution patterns +- Identifies scripts that discover new coverage +- Generates coverage reports for analysis +- Helps guide the fuzzing process toward unexplored code + +## Workflow + +The Neo VM Fuzzer follows this general workflow: + +1. **Initialization**: + - Parse command-line arguments + - Set up the fuzzing environment + - Load any existing corpus of scripts + +2. **Generation Phase**: + - Generate a new random script or + - Select and mutate an existing script from the corpus + +3. **Execution Phase**: + - Execute the script in the Neo VM with instrumentation + - Monitor for crashes, exceptions, or timeouts + - Collect coverage information + +4. **Analysis Phase**: + - Determine if the script found new coverage + - Save interesting scripts to the corpus + - Record any crashes or exceptions + +5. **Reporting**: + - Generate summary statistics + - Save coverage reports + - Document any issues found + +This process repeats for a specified number of iterations or until manually stopped. + +## Integration with Neo VM + +The fuzzer integrates with the Neo VM through direct references to the `Neo.VM` library. It uses the public API of the VM to: + +- Create and load scripts +- Execute scripts in the VM +- Access execution context and state +- Monitor for exceptions and errors + +The fuzzer also uses reflection and custom instrumentation to gather detailed information about the internal state of the VM during execution. + +## Future Enhancements + +Potential future enhancements to the Neo VM Fuzzer include: + +1. **Guided Fuzzing**: Using evolutionary algorithms to guide script generation toward specific code paths +2. **Differential Fuzzing**: Comparing execution results between different versions of the Neo VM +3. **Smart Contract Fuzzing**: Extending the fuzzer to test Neo smart contracts +4. **Integration with CI/CD**: Automating fuzzing as part of the continuous integration pipeline +5. **Advanced Mutation Strategies**: Implementing more sophisticated script mutation techniques diff --git a/fuzzers/Neo.VM.Fuzzer/Documentation/USAGE.md b/fuzzers/Neo.VM.Fuzzer/Documentation/USAGE.md new file mode 100644 index 0000000000..c1379ec72d --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/Documentation/USAGE.md @@ -0,0 +1,141 @@ +# Neo VM Fuzzer Usage Guide + +This document provides instructions for using the Neo VM Fuzzer to test the Neo Virtual Machine. + +## Prerequisites + +- .NET 7.0 SDK or later +- Neo VM source code + +## Building the Fuzzer + +1. Clone the Neo repository if you haven't already: + ``` + git clone https://github.com/neo-project/neo.git + cd neo + ``` + +2. Build the fuzzer: + ``` + dotnet build src/Neo.VM.Fuzzer + ``` + +## Basic Usage + +Run the fuzzer with default settings: + +``` +dotnet run --project src/Neo.VM.Fuzzer +``` + +This will: +- Run 1000 fuzzing iterations +- Use a random seed +- Save results to the `fuzzer-output` directory +- Use a 5-second timeout for each script execution + +## Command Line Options + +The fuzzer supports the following command line options: + +| Option | Description | Default | +|--------|-------------|---------| +| `-i, --iterations` | Number of fuzzing iterations to run | 1000 | +| `-s, --seed` | Random seed for reproducible fuzzing | Random | +| `-o, --output` | Output directory for crash reports and interesting scripts | `fuzzer-output` | +| `-t, --timeout` | Timeout in milliseconds for each VM execution | 5000 | +| `-m, --mutation-rate` | Rate of mutation for script evolution (0.0-1.0) | 0.1 | +| `-c, --corpus` | Directory with initial corpus of scripts | None | + +### Examples + +Run 10,000 iterations with a specific seed: +``` +dotnet run --project src/Neo.VM.Fuzzer -- -i 10000 -s 12345 +``` + +Use a custom output directory and longer timeout: +``` +dotnet run --project src/Neo.VM.Fuzzer -- -o my-fuzzer-results -t 10000 +``` + +Use an existing corpus of scripts: +``` +dotnet run --project src/Neo.VM.Fuzzer -- -c path/to/corpus +``` + +## Output Directory Structure + +The fuzzer creates the following structure in the output directory: + +``` +output-dir/ +├── crashes/ # Scripts that caused crashes +│ ├── crash_*.bin # Binary script files +│ └── crash_*.txt # Crash information +├── interesting/ # Scripts that found new coverage +│ └── interesting_*.bin +└── coverage_report.txt # Final coverage report +``` + +## Analyzing Results + +### Crash Analysis + +When the fuzzer finds a script that crashes the VM, it saves: +1. The script itself as a binary file (`.bin`) +2. A text file with the same name containing the exception details (`.txt`) + +To reproduce a crash: +``` +dotnet run --project src/Neo.VM.Fuzzer -- -c output-dir/crashes -i 1 +``` + +### Coverage Analysis + +The fuzzer generates a coverage report showing: +- Total coverage points reached +- OpCode coverage statistics +- Most and least frequently executed code paths + +This information can help identify areas of the VM that need more testing. + +## Continuous Fuzzing + +For long-running fuzzing sessions, you can: + +1. Start with an initial run: + ``` + dotnet run --project src/Neo.VM.Fuzzer -- -o initial-run + ``` + +2. Use the results as a corpus for subsequent runs: + ``` + dotnet run --project src/Neo.VM.Fuzzer -- -c initial-run/interesting -o next-run + ``` + +3. Repeat this process to iteratively improve coverage. + +## Debugging Crashes + +When investigating a crash: + +1. Examine the crash text file to understand the exception +2. Use the binary script file with a debugger: + ```csharp + var script = System.IO.File.ReadAllBytes("path/to/crash.bin"); + var engine = new ExecutionEngine(); + engine.LoadScript(script); + // Set breakpoints as needed + engine.Execute(); + ``` + +## Performance Considerations + +- The fuzzer can be resource-intensive, especially with large numbers of iterations +- Consider using a higher timeout for complex scripts +- For very large fuzzing campaigns, run on a dedicated machine + +## Extending the Fuzzer + +See [EXTENDING.md](EXTENDING.md) for information on customizing and extending the fuzzer for specific testing needs. diff --git a/fuzzers/Neo.VM.Fuzzer/Documentation/VMRunner.md b/fuzzers/Neo.VM.Fuzzer/Documentation/VMRunner.md new file mode 100644 index 0000000000..a681e2ce69 --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/Documentation/VMRunner.md @@ -0,0 +1,96 @@ +# VMRunner + +The `VMRunner` class is responsible for executing Neo VM scripts and tracking execution results during fuzzing. It provides comprehensive functionality for running scripts, monitoring their execution, and collecting metrics that help assess the effectiveness of the fuzzing process. + +## Overview + +The VMRunner serves as the execution engine for the Neo VM Fuzzer, providing the following capabilities: + +1. Executing Neo VM scripts with configurable timeout settings +2. Tracking code coverage during script execution +3. Monitoring execution metrics such as stack size and invocation depth +4. Detecting crashes and timeouts +5. Recording detailed execution results for analysis + +## Class Structure + +The VMRunner is implemented in the `Neo.VM.Fuzzer.Runners` namespace and provides two primary execution methods: + +1. `RunScript(byte[] script)` - Executes a script and returns a `VMExecutionResult` +2. `Execute(Script script, CancellationToken cancellationToken)` - Executes a script and returns an `ExecutionResult` + +## Key Features + +### Coverage Tracking + +The VMRunner tracks which instructions are executed during script execution, allowing the fuzzer to identify scripts that explore new paths through the Neo VM. This is essential for guided fuzzing, where the goal is to maximize code coverage. + +```csharp +// Example of checking for new coverage +bool foundNewCoverage = vmRunner.FoundNewCoverage(script, result); +if (foundNewCoverage) { + // Save this script as interesting +} +``` + +### Timeout Handling + +Scripts that run indefinitely can stall the fuzzing process. The VMRunner implements timeout detection to terminate long-running scripts: + +```csharp +// Configure timeout in constructor +var vmRunner = new VMRunner(timeoutMs: 5000); +``` + +### Detailed Execution Results + +The VMRunner provides comprehensive information about each script execution through the `VMExecutionResult` and `ExecutionResult` classes, including: + +- Execution state (HALT, FAULT, etc.) +- Execution time +- Stack information +- Exception details (if any) +- Coverage information + +## Integration with Other Components + +The VMRunner works closely with other components of the Neo VM Fuzzer: + +1. **ScriptGenerator**: Provides scripts for the VMRunner to execute +2. **MutationEngine**: Evolves scripts based on execution results +3. **CorpusManager**: Stores interesting scripts identified by the VMRunner +4. **FuzzingResults**: Aggregates execution metrics from multiple runs + +## Usage Example + +```csharp +// Initialize components +var vmRunner = new VMRunner(timeoutMs: 5000, verbose: true); +var scriptGenerator = new ScriptGenerator(random); + +// Generate and execute a script +byte[] script = scriptGenerator.GenerateRandomScript(); +var result = vmRunner.RunScript(script); + +// Process the result +if (result.Crashed) { + Console.WriteLine($"Script crashed with exception: {result.ExceptionType}"); +} else if (result.TimedOut) { + Console.WriteLine("Script execution timed out"); +} else { + Console.WriteLine($"Script executed successfully in {result.ExecutionTimeMs}ms"); +} +``` + +## Implementation Details + +The VMRunner uses two different approaches for executing scripts: + +1. **InstrumentedExecutionEngine**: A custom execution engine that tracks coverage and execution details +2. **Event-based tracking**: Using the `InstructionPointerChanged` event to monitor execution + +This dual approach provides flexibility in how scripts are executed and monitored, allowing for different types of analysis. + +## Performance Considerations + +The VMRunner is designed to be efficient, but tracking coverage and execution metrics does introduce some overhead. In performance-critical scenarios, the verbose mode can be disabled to reduce logging overhead. diff --git a/fuzzers/Neo.VM.Fuzzer/Generators/MutationEngine.cs b/fuzzers/Neo.VM.Fuzzer/Generators/MutationEngine.cs new file mode 100644 index 0000000000..6dfdd31b7c --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/Generators/MutationEngine.cs @@ -0,0 +1,341 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// MutationEngine.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Neo.VM.Fuzzer.Generators +{ + /// + /// Provides various mutation strategies for evolving scripts during fuzzing + /// + public class MutationEngine + { + private readonly Random _random; + private readonly double _mutationRate; + + // Safe opcodes that can be used for mutation + private readonly OpCode[] _safeOpcodes = new OpCode[] + { + OpCode.PUSH0, OpCode.PUSH1, OpCode.PUSH2, OpCode.PUSH3, + OpCode.PUSH4, OpCode.PUSH5, OpCode.PUSH6, OpCode.PUSH7, + OpCode.PUSH8, OpCode.PUSH9, OpCode.PUSH10, OpCode.PUSH11, + OpCode.PUSH12, OpCode.PUSH13, OpCode.PUSH14, OpCode.PUSH15, + OpCode.PUSH16, OpCode.PUSHM1, OpCode.PUSHNULL, OpCode.NOP, + OpCode.ADD, OpCode.SUB, OpCode.MUL, OpCode.DIV, OpCode.MOD, + OpCode.POW, OpCode.AND, OpCode.OR, OpCode.XOR, OpCode.NOT, + OpCode.INC, OpCode.DEC, OpCode.SIGN, OpCode.ABS, OpCode.BOOLAND, + OpCode.BOOLOR, OpCode.NUMEQUAL, OpCode.NUMNOTEQUAL, OpCode.LT, + OpCode.LE, OpCode.GT, OpCode.GE, OpCode.MIN, OpCode.MAX, + OpCode.WITHIN, OpCode.DUP, OpCode.OVER, OpCode.PICK, OpCode.SWAP, + OpCode.ROT, OpCode.ROLL, OpCode.DROP, OpCode.NIP, OpCode.TUCK, + OpCode.DEPTH + }; + + /// + /// Initializes a new instance of the MutationEngine class + /// + /// Random number generator + /// Rate of mutation (0.0-1.0) + public MutationEngine(Random random, double mutationRate = 0.1) + { + _random = random ?? throw new ArgumentNullException(nameof(random)); + _mutationRate = Math.Clamp(mutationRate, 0.0, 1.0); + } + + /// + /// Mutates a script using a randomly selected mutation strategy + /// + /// The original script to mutate + /// A mutated version of the script + public byte[] MutateScript(byte[] script) + { + if (script == null || script.Length == 0) + { + // Can't mutate an empty script + return new byte[] { (byte)OpCode.RET }; + } + + // Make a copy of the original script + byte[] mutatedScript = script.ToArray(); + + // Apply multiple mutations based on mutation rate + int mutationCount = Math.Max(1, (int)(script.Length * _mutationRate)); + + for (int i = 0; i < mutationCount; i++) + { + // Choose a mutation strategy + int strategy = _random.Next(6); + + switch (strategy) + { + case 0: + mutatedScript = BitFlipMutation(mutatedScript); + break; + case 1: + mutatedScript = ByteReplaceMutation(mutatedScript); + break; + case 2: + mutatedScript = ByteInsertMutation(mutatedScript); + break; + case 3: + mutatedScript = ByteDeleteMutation(mutatedScript); + break; + case 4: + mutatedScript = ByteSwapMutation(mutatedScript); + break; + case 5: + mutatedScript = OpcodeReplaceMutation(mutatedScript); + break; + } + } + + // Ensure the script ends with RET + if (mutatedScript.Length == 0 || mutatedScript[mutatedScript.Length - 1] != (byte)OpCode.RET) + { + Array.Resize(ref mutatedScript, mutatedScript.Length + 1); + mutatedScript[mutatedScript.Length - 1] = (byte)OpCode.RET; + } + + return mutatedScript; + } + + /// + /// Performs a bit flip mutation on a random bit in the script + /// + private byte[] BitFlipMutation(byte[] script) + { + if (script.Length == 0) + return script; + + byte[] result = script.ToArray(); + int position = _random.Next(result.Length); + int bit = _random.Next(8); + + // Flip the bit + result[position] ^= (byte)(1 << bit); + + return result; + } + + /// + /// Replaces a random byte in the script with a new random byte + /// + private byte[] ByteReplaceMutation(byte[] script) + { + if (script.Length == 0) + return script; + + byte[] result = script.ToArray(); + int position = _random.Next(result.Length); + + // Replace with a random byte + result[position] = (byte)_random.Next(256); + + return result; + } + + /// + /// Inserts a random byte at a random position in the script + /// + private byte[] ByteInsertMutation(byte[] script) + { + byte[] result = new byte[script.Length + 1]; + int position = _random.Next(script.Length + 1); + + // Copy bytes before the insertion point + if (position > 0) + { + Buffer.BlockCopy(script, 0, result, 0, position); + } + + // Insert the new byte + result[position] = (byte)_random.Next(256); + + // Copy bytes after the insertion point + if (position < script.Length) + { + Buffer.BlockCopy(script, position, result, position + 1, script.Length - position); + } + + return result; + } + + /// + /// Deletes a random byte from the script + /// + private byte[] ByteDeleteMutation(byte[] script) + { + if (script.Length <= 1) + return script; + + byte[] result = new byte[script.Length - 1]; + int position = _random.Next(script.Length); + + // Copy bytes before the deletion point + if (position > 0) + { + Buffer.BlockCopy(script, 0, result, 0, position); + } + + // Copy bytes after the deletion point + if (position < script.Length - 1) + { + Buffer.BlockCopy(script, position + 1, result, position, script.Length - position - 1); + } + + return result; + } + + /// + /// Swaps two random bytes in the script + /// + private byte[] ByteSwapMutation(byte[] script) + { + if (script.Length <= 1) + return script; + + byte[] result = script.ToArray(); + int position1 = _random.Next(result.Length); + int position2 = _random.Next(result.Length); + + // Ensure the positions are different + while (position1 == position2 && result.Length > 1) + { + position2 = _random.Next(result.Length); + } + + // Swap the bytes + byte temp = result[position1]; + result[position1] = result[position2]; + result[position2] = temp; + + return result; + } + + /// + /// Replaces a random opcode in the script with another safe opcode + /// + private byte[] OpcodeReplaceMutation(byte[] script) + { + if (script.Length == 0) + return script; + + byte[] result = script.ToArray(); + int position = _random.Next(result.Length); + + // Replace with a random safe opcode + result[position] = (byte)_safeOpcodes[_random.Next(_safeOpcodes.Length)]; + + return result; + } + + /// + /// Performs crossover between two scripts to create a new script + /// + /// The first parent script + /// The second parent script + /// A new script created by combining parts of both parent scripts + public byte[] CrossoverScripts(byte[] script1, byte[] script2) + { + if (script1 == null || script1.Length == 0) + return script2?.ToArray() ?? new byte[] { (byte)OpCode.RET }; + + if (script2 == null || script2.Length == 0) + return script1.ToArray(); + + // Choose a crossover strategy + int strategy = _random.Next(3); + + switch (strategy) + { + case 0: + return SinglePointCrossover(script1, script2); + case 1: + return TwoPointCrossover(script1, script2); + default: + return UniformCrossover(script1, script2); + } + } + + /// + /// Performs single-point crossover between two scripts + /// + private byte[] SinglePointCrossover(byte[] script1, byte[] script2) + { + int point1 = _random.Next(script1.Length); + int point2 = _random.Next(script2.Length); + + byte[] result = new byte[point1 + (script2.Length - point2)]; + + // Copy first part from script1 + Buffer.BlockCopy(script1, 0, result, 0, point1); + + // Copy second part from script2 + Buffer.BlockCopy(script2, point2, result, point1, script2.Length - point2); + + return result; + } + + /// + /// Performs two-point crossover between two scripts + /// + private byte[] TwoPointCrossover(byte[] script1, byte[] script2) + { + // Ensure points are in order + int start1 = _random.Next(script1.Length); + int end1 = _random.Next(start1, script1.Length); + + int start2 = _random.Next(script2.Length); + int end2 = _random.Next(start2, script2.Length); + + int middleLength = end2 - start2; + byte[] result = new byte[start1 + middleLength + (script1.Length - end1)]; + + // Copy first part from script1 + Buffer.BlockCopy(script1, 0, result, 0, start1); + + // Copy middle part from script2 + Buffer.BlockCopy(script2, start2, result, start1, middleLength); + + // Copy last part from script1 + Buffer.BlockCopy(script1, end1, result, start1 + middleLength, script1.Length - end1); + + return result; + } + + /// + /// Performs uniform crossover between two scripts + /// + private byte[] UniformCrossover(byte[] script1, byte[] script2) + { + int length = Math.Max(script1.Length, script2.Length); + byte[] result = new byte[length]; + + for (int i = 0; i < length; i++) + { + if (_random.Next(2) == 0) + { + // Take from script1 if possible + result[i] = i < script1.Length ? script1[i] : (byte)OpCode.NOP; + } + else + { + // Take from script2 if possible + result[i] = i < script2.Length ? script2[i] : (byte)OpCode.NOP; + } + } + + return result; + } + } +} diff --git a/fuzzers/Neo.VM.Fuzzer/Generators/ScriptGenerator.cs b/fuzzers/Neo.VM.Fuzzer/Generators/ScriptGenerator.cs new file mode 100644 index 0000000000..a43503d8ce --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/Generators/ScriptGenerator.cs @@ -0,0 +1,485 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ScriptGenerator.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; + +namespace Neo.VM.Fuzzer.Generators +{ + /// + /// Generates random but valid Neo VM scripts for fuzzing + /// + public class ScriptGenerator + { + private readonly Random _random; + + // Group opcodes by category for more structured generation + private static readonly OpCode[] _arithmeticOpcodes = new[] + { + OpCode.ADD, OpCode.SUB, OpCode.MUL, OpCode.DIV, OpCode.MOD, OpCode.POW, + OpCode.SHL, OpCode.SHR, OpCode.NOT, OpCode.BOOLAND, OpCode.BOOLOR, + OpCode.NUMEQUAL, OpCode.NUMNOTEQUAL, OpCode.LT, OpCode.LE, OpCode.GT, OpCode.GE, + OpCode.MIN, OpCode.MAX, OpCode.WITHIN + }; + + private static readonly OpCode[] _stackOpcodes = new[] + { + OpCode.DUP, OpCode.SWAP, OpCode.TUCK, OpCode.OVER, OpCode.ROT, + OpCode.DEPTH, OpCode.DROP, OpCode.NIP, OpCode.PICK, OpCode.ROLL + }; + + private static readonly OpCode[] _arrayOpcodes = new[] + { + OpCode.NEWARRAY, OpCode.NEWARRAY0, OpCode.NEWSTRUCT, OpCode.NEWSTRUCT0, + OpCode.APPEND, OpCode.REMOVE, OpCode.HASKEY, + OpCode.KEYS, OpCode.VALUES, OpCode.PACK, OpCode.UNPACK, + OpCode.PICKITEM, OpCode.SETITEM, OpCode.SIZE + }; + + private static readonly OpCode[] _controlFlowOpcodes = new[] + { + OpCode.JMP, OpCode.JMPIF, OpCode.JMPIFNOT, OpCode.CALL, + OpCode.RET, OpCode.SYSCALL + }; + + private static readonly OpCode[] _constantOpcodes = new[] + { + OpCode.PUSH0, OpCode.PUSHM1, OpCode.PUSH1, OpCode.PUSH2, OpCode.PUSH3, + OpCode.PUSH4, OpCode.PUSH5, OpCode.PUSH6, OpCode.PUSH7, OpCode.PUSH8, + OpCode.PUSH9, OpCode.PUSH10, OpCode.PUSH11, OpCode.PUSH12, OpCode.PUSH13, + OpCode.PUSH14, OpCode.PUSH15, OpCode.PUSH16, OpCode.PUSHDATA1, OpCode.PUSHDATA2, + OpCode.PUSHDATA4, OpCode.PUSHINT8, OpCode.PUSHINT16, OpCode.PUSHINT32, OpCode.PUSHINT64, + OpCode.PUSHINT128, OpCode.PUSHINT256, OpCode.PUSHA, OpCode.PUSHNULL, + OpCode.PUSHF, OpCode.PUSHT + }; + + private static readonly OpCode[] _cryptoOpcodes = new[] + { + OpCode.EQUAL + }; + + private static readonly OpCode[] _conversionOpcodes = new[] + { + OpCode.CONVERT + }; + + /// + /// Initializes a new instance of the ScriptGenerator class + /// + /// Random number generator + public ScriptGenerator(Random random) + { + _random = random ?? throw new ArgumentNullException(nameof(random)); + } + + /// + /// Generates a random Neo VM script + /// + /// Maximum number of instructions in the script + /// A byte array containing the generated script + public byte[] GenerateRandomScript(int maxInstructions = 100) + { + // Ensure we don't generate too large scripts + maxInstructions = Math.Min(maxInstructions, 1000); + + var scriptBuilder = new ScriptBuilder(); + + // Choose a script generation strategy + int strategy = _random.Next(4); + + switch (strategy) + { + case 0: + GenerateArithmeticHeavyScript(scriptBuilder, maxInstructions); + break; + case 1: + GenerateStackHeavyScript(scriptBuilder, maxInstructions); + break; + case 2: + GenerateArrayHeavyScript(scriptBuilder, maxInstructions); + break; + default: + GenerateRandomMixedScript(scriptBuilder, maxInstructions); + break; + } + + // Always end with RET to ensure script terminates + scriptBuilder.Emit(OpCode.RET); + + return scriptBuilder.ToArray(); + } + + /// + /// Generates a script focused on arithmetic operations + /// + private void GenerateArithmeticHeavyScript(ScriptBuilder scriptBuilder, int maxInstructions) + { + // First push some values onto the stack + for (int i = 0; i < _random.Next(5, 10); i++) + { + EmitRandomPush(scriptBuilder); + } + + // Then perform arithmetic operations + int operations = Math.Min(maxInstructions - 10, _random.Next(10, maxInstructions)); + + for (int i = 0; i < operations; i++) + { + if (_random.Next(5) == 0) + { + // Occasionally push more values to keep the stack from depleting + EmitRandomPush(scriptBuilder); + } + else + { + // Emit an arithmetic operation + scriptBuilder.Emit(_arithmeticOpcodes[_random.Next(_arithmeticOpcodes.Length)]); + } + } + } + + /// + /// Generates a script focused on stack manipulation + /// + private void GenerateStackHeavyScript(ScriptBuilder scriptBuilder, int maxInstructions) + { + // First push some values onto the stack + for (int i = 0; i < _random.Next(5, 15); i++) + { + EmitRandomPush(scriptBuilder); + } + + // Then perform stack operations + int operations = Math.Min(maxInstructions - 15, _random.Next(10, maxInstructions)); + + for (int i = 0; i < operations; i++) + { + if (_random.Next(5) == 0) + { + // Occasionally push more values + EmitRandomPush(scriptBuilder); + } + else + { + // Emit a stack operation + OpCode opcode = _stackOpcodes[_random.Next(_stackOpcodes.Length)]; + + // For PICK and ROLL, we need to push an index first + if (opcode == OpCode.PICK || opcode == OpCode.ROLL) + { + // Push a small index to avoid stack underflow + EmitSmallIntPush(scriptBuilder, _random.Next(5)); + } + + scriptBuilder.Emit(opcode); + } + } + } + + /// + /// Generates a script focused on array and struct operations + /// + private void GenerateArrayHeavyScript(ScriptBuilder scriptBuilder, int maxInstructions) + { + // First create some arrays + for (int i = 0; i < _random.Next(1, 3); i++) + { + // Push some items for the array + int arraySize = _random.Next(1, 5); + for (int j = 0; j < arraySize; j++) + { + EmitRandomPush(scriptBuilder); + } + + // Create the array + EmitSmallIntPush(scriptBuilder, arraySize); + scriptBuilder.Emit(OpCode.PACK); + } + + // Then perform array operations + int operations = Math.Min(maxInstructions - 15, _random.Next(10, maxInstructions)); + + for (int i = 0; i < operations; i++) + { + int op = _random.Next(10); + + if (op < 2) + { + // Create a new array occasionally + if (_random.Next(2) == 0) + { + scriptBuilder.Emit(OpCode.NEWARRAY0); + } + else + { + scriptBuilder.Emit(OpCode.NEWSTRUCT0); + } + } + else if (op < 5) + { + // Push a value and an index for array operations + EmitRandomPush(scriptBuilder); + EmitSmallIntPush(scriptBuilder, _random.Next(3)); + + // Array operation + scriptBuilder.Emit(_arrayOpcodes[_random.Next(_arrayOpcodes.Length)]); + } + else + { + // Other array operations + scriptBuilder.Emit(_arrayOpcodes[_random.Next(_arrayOpcodes.Length)]); + } + } + } + + /// + /// Generates a script with a random mix of operations + /// + private void GenerateRandomMixedScript(ScriptBuilder scriptBuilder, int maxInstructions) + { + // Push some initial values + for (int i = 0; i < _random.Next(3, 8); i++) + { + EmitRandomPush(scriptBuilder); + } + + // Generate random operations + int operations = Math.Min(maxInstructions - 8, _random.Next(10, maxInstructions)); + + for (int i = 0; i < operations; i++) + { + int category = _random.Next(7); + + switch (category) + { + case 0: + // Arithmetic + scriptBuilder.Emit(_arithmeticOpcodes[_random.Next(_arithmeticOpcodes.Length)]); + break; + case 1: + // Stack + scriptBuilder.Emit(_stackOpcodes[_random.Next(_stackOpcodes.Length)]); + break; + case 2: + // Array + scriptBuilder.Emit(_arrayOpcodes[_random.Next(_arrayOpcodes.Length)]); + break; + case 3: + // Control flow - be careful with these + if (_random.Next(5) == 0) // Only 20% chance to avoid too many jumps + { + EmitSafeControlFlow(scriptBuilder); + } + else + { + EmitRandomPush(scriptBuilder); + } + break; + case 4: + // Constants + EmitRandomPush(scriptBuilder); + break; + case 5: + // Crypto + scriptBuilder.Emit(_cryptoOpcodes[_random.Next(_cryptoOpcodes.Length)]); + break; + case 6: + // Conversion + scriptBuilder.Emit(_conversionOpcodes[_random.Next(_conversionOpcodes.Length)]); + break; + } + + // Occasionally push more values to keep the stack from depleting + if (_random.Next(5) == 0) + { + EmitRandomPush(scriptBuilder); + } + } + } + + /// + /// Emits a safe control flow instruction (avoiding infinite loops) + /// + private void EmitSafeControlFlow(ScriptBuilder scriptBuilder) + { + // For safety, we'll only use forward jumps with small offsets + OpCode opcode = _controlFlowOpcodes[_random.Next(3)]; // Only JMP, JMPIF, JMPIFNOT + + // Push a condition for conditional jumps + if (opcode == OpCode.JMPIF || opcode == OpCode.JMPIFNOT) + { + EmitRandomPush(scriptBuilder); + } + + // Emit the control flow instruction with a small forward offset + scriptBuilder.Emit(opcode); + scriptBuilder.Emit((byte)_random.Next(1, 10)); // Small forward jump + } + + /// + /// Emits a random push operation + /// + private void EmitRandomPush(ScriptBuilder scriptBuilder) + { + int pushType = _random.Next(10); + + switch (pushType) + { + case 0: + // Push small constant (0-16) + scriptBuilder.Emit((OpCode)(_random.Next(17) + (byte)OpCode.PUSH0)); + break; + case 1: + // Push -1 + scriptBuilder.Emit(OpCode.PUSHM1); + break; + case 2: + // Push true/false + scriptBuilder.Emit(_random.Next(2) == 0 ? OpCode.PUSHF : OpCode.PUSHT); + break; + case 3: + // Push null + scriptBuilder.Emit(OpCode.PUSHNULL); + break; + case 4: + // Push small int + EmitSmallIntPush(scriptBuilder, _random.Next(100)); + break; + case 5: + // Push int8 + scriptBuilder.Emit(OpCode.PUSHINT8); + scriptBuilder.Emit((byte)_random.Next(256)); + break; + case 6: + // Push int16 + scriptBuilder.Emit(OpCode.PUSHINT16); + short int16Value = (short)_random.Next(short.MinValue, short.MaxValue); + scriptBuilder.Emit(BitConverter.GetBytes(int16Value)); + break; + case 7: + // Push int32 + scriptBuilder.Emit(OpCode.PUSHINT32); + int int32Value = _random.Next(); + scriptBuilder.Emit(BitConverter.GetBytes(int32Value)); + break; + case 8: + // Push small data + EmitSmallData(scriptBuilder); + break; + default: + // Push medium data + EmitMediumData(scriptBuilder); + break; + } + } + + /// + /// Emits a small integer push + /// + private void EmitSmallIntPush(ScriptBuilder scriptBuilder, int value) + { + if (value == -1) + { + scriptBuilder.Emit(OpCode.PUSHM1); + } + else if (value >= 0 && value <= 16) + { + scriptBuilder.Emit((OpCode)((byte)OpCode.PUSH0 + value)); + } + else + { + scriptBuilder.Emit(OpCode.PUSHINT8); + scriptBuilder.Emit((byte)value); + } + } + + /// + /// Emits a small data push + /// + private void EmitSmallData(ScriptBuilder scriptBuilder) + { + int length = _random.Next(1, 10); + byte[] data = new byte[length]; + _random.NextBytes(data); + + scriptBuilder.Emit(OpCode.PUSHDATA1); + scriptBuilder.Emit((byte)length); + foreach (byte b in data) + { + scriptBuilder.Emit(b); + } + } + + /// + /// Emits a medium-sized data push + /// + private void EmitMediumData(ScriptBuilder scriptBuilder) + { + int length = _random.Next(10, 50); + byte[] data = new byte[length]; + _random.NextBytes(data); + + scriptBuilder.Emit(OpCode.PUSHDATA1); + scriptBuilder.Emit((byte)length); + foreach (byte b in data) + { + scriptBuilder.Emit(b); + } + } + } + + /// + /// Helper class for building Neo VM scripts + /// + public class ScriptBuilder + { + private readonly List _script = new List(); + + /// + /// Emits an opcode to the script + /// + /// The opcode to emit + public void Emit(OpCode opcode) + { + _script.Add((byte)opcode); + } + + /// + /// Emits a byte to the script + /// + /// The byte to emit + public void Emit(byte b) + { + _script.Add(b); + } + + /// + /// Emits a byte array to the script + /// + /// The byte array to emit + public void Emit(byte[] bytes) + { + _script.AddRange(bytes); + } + + /// + /// Converts the script to a byte array + /// + /// A byte array containing the script + public byte[] ToArray() + { + return _script.ToArray(); + } + } +} diff --git a/fuzzers/Neo.VM.Fuzzer/Neo.VM.Fuzzer.csproj b/fuzzers/Neo.VM.Fuzzer/Neo.VM.Fuzzer.csproj new file mode 100644 index 0000000000..b45c926008 --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/Neo.VM.Fuzzer.csproj @@ -0,0 +1,24 @@ + + + + Exe + net7.0 + enable + enable + Neo.VM.Fuzzer + + + + + + + + + + + + + + + + diff --git a/fuzzers/Neo.VM.Fuzzer/Program.cs b/fuzzers/Neo.VM.Fuzzer/Program.cs new file mode 100644 index 0000000000..44ad956321 --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/Program.cs @@ -0,0 +1,274 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// Program.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using CommandLine; +using Neo.VM.Fuzzer.Generators; +using Neo.VM.Fuzzer.Runners; +using Neo.VM.Fuzzer.Utils; +using System; +using System.IO; +using System.Threading; + +namespace Neo.VM.Fuzzer +{ + /// + /// Main entry point for the Neo VM Fuzzer + /// + public class Program + { + /// + /// Command line options for the fuzzer + /// + public class Options + { + [Option('i', "iterations", Default = 1000, HelpText = "Number of fuzzing iterations to run")] + public int Iterations { get; set; } + + [Option('s', "seed", HelpText = "Random seed for reproducible fuzzing")] + public int? Seed { get; set; } + + [Option('o', "output", Default = "fuzzer-output", HelpText = "Output directory for crash reports and interesting scripts")] + public string OutputDir { get; set; } = "fuzzer-output"; + + [Option('t', "timeout", Default = 5000, HelpText = "Timeout in milliseconds for each VM execution")] + public int TimeoutMs { get; set; } + + [Option('m', "mutation-rate", Default = 0.1, HelpText = "Rate of mutation for script evolution (0.0-1.0)")] + public double MutationRate { get; set; } + + [Option('c', "corpus", HelpText = "Directory with initial corpus of scripts")] + public string? CorpusDir { get; set; } + + [Option('v', "verbose", Default = false, HelpText = "Enable verbose output")] + public bool Verbose { get; set; } + + [Option('r', "report-interval", Default = 100, HelpText = "Interval for progress reporting")] + public int ReportInterval { get; set; } + + [Option("detect-dos", Default = false, HelpText = "Enable detection of potential DOS vectors")] + public bool DetectDOS { get; set; } + + [Option("dos-threshold", Default = 0.8, HelpText = "Threshold for flagging potential DOS vectors (0.0-1.0)")] + public double DOSThreshold { get; set; } + + [Option("track-memory", Default = false, HelpText = "Enable detailed memory tracking for DOS detection")] + public bool TrackMemory { get; set; } + + [Option("track-opcodes", Default = true, HelpText = "Track execution time per opcode for DOS detection")] + public bool TrackOpcodes { get; set; } + } + + /// + /// Main entry point + /// + public static void Main(string[] args) + { + Parser.Default.ParseArguments(args) + .WithParsed(RunFuzzer) + .WithNotParsed(errors => + { + Console.WriteLine("Failed to parse command line arguments:"); + foreach (var error in errors) + { + Console.WriteLine($" {error}"); + } + }); + } + + /// + /// Runs the fuzzer with the specified options + /// + private static void RunFuzzer(Options options) + { + Console.WriteLine("=== Neo VM Fuzzer ==="); + + // Initialize random with seed if provided + Random random = options.Seed.HasValue + ? new Random(options.Seed.Value) + : new Random(); + + if (options.Seed.HasValue) + { + Console.WriteLine($"Using seed: {options.Seed.Value}"); + } + else + { + Console.WriteLine("Using random seed"); + } + + // Create output directory + Directory.CreateDirectory(options.OutputDir); + Console.WriteLine($"Output directory: {options.OutputDir}"); + + // Initialize components + var scriptGenerator = new ScriptGenerator(random); + var mutationEngine = new MutationEngine(random, options.MutationRate); + var vmRunner = new VMRunner( + options.TimeoutMs, + detectDOS: options.DetectDOS, + dosThreshold: options.DOSThreshold, + trackMemory: options.TrackMemory, + trackOpcodes: options.TrackOpcodes); + var corpusManager = new CorpusManager(options.OutputDir, options.CorpusDir); + var fuzzingResults = new FuzzingResults(options.OutputDir); + var coverageTracker = new CoverageTracker(); + + // Load initial corpus if available + corpusManager.LoadCorpus(); + Console.WriteLine($"Initial corpus size: {corpusManager.CorpusSize}"); + + Console.WriteLine($"Starting fuzzing with {options.Iterations} iterations..."); + Console.WriteLine(); + + int crashCount = 0; + int timeoutCount = 0; + int newCoverageCount = 0; + + // Main fuzzing loop + for (int i = 0; i < options.Iterations; i++) + { + // Report progress periodically + if (i % options.ReportInterval == 0 && i > 0) + { + ReportProgress(i, options.Iterations, crashCount, timeoutCount, newCoverageCount, coverageTracker.CoverageCount); + } + + // Generate or mutate a script + byte[] scriptBytes; + + if (corpusManager.CorpusSize > 0 && random.NextDouble() < 0.7) // 70% chance to use corpus when available + { + // Use and mutate a script from the corpus + scriptBytes = corpusManager.GetRandomScript(); + scriptBytes = mutationEngine.MutateScript(scriptBytes); + } + else + { + // Generate a new random script + scriptBytes = scriptGenerator.GenerateRandomScript(); + } + + // Execute the script + var executionResult = vmRunner.Execute(scriptBytes); + + // Check if it's a potential DOS vector + if (options.DetectDOS && executionResult.DOSAnalysis?.IsPotentialDOSVector == true) + { + corpusManager.SaveDOSVector(scriptBytes, executionResult.DOSAnalysis); + Console.WriteLine($"Found potential DOS vector! Score: {executionResult.DOSAnalysis.DOSScore:F2}, Reason: {executionResult.DOSAnalysis.DetectionReason}"); + } + else if (options.DetectDOS && executionResult.DOSAnalysis != null) + { + // Add detailed logging about why it wasn't detected as a DOS vector + Console.WriteLine($"Script analyzed but not flagged as DOS vector. Score: {executionResult.DOSAnalysis.DOSScore:F2}, Threshold: {options.DOSThreshold:F2}"); + Console.WriteLine($"Metrics: Instructions={executionResult.DOSAnalysis.Metrics["TotalInstructions"]}, MaxStackDepth={executionResult.DOSAnalysis.Metrics["MaxStackDepth"]}, ExecutionTime={executionResult.DOSAnalysis.Metrics["TotalExecutionTimeMs"]}ms"); + + if (executionResult.Crashed) + { + Console.WriteLine($"Script crashed with {executionResult.ExceptionType} but DOS analysis was still performed"); + } + } + else if (options.DetectDOS) + { + Console.WriteLine("DOS analysis was not performed or returned null"); + } + + // Check if it crashed + if (executionResult.Crashed) + { + corpusManager.SaveCrash(scriptBytes, executionResult.ExceptionType); + Console.WriteLine($"Found crash! Exception: {executionResult.ExceptionType}"); + } + + // Check if it found new coverage + bool foundNewCoverage = false; + foreach (var point in executionResult.Coverage) + { + if (coverageTracker.AddCoveragePoint(point)) + { + foundNewCoverage = true; + } + } + + if (foundNewCoverage) + { + corpusManager.SaveInteresting(scriptBytes); + Console.WriteLine("Found new coverage!"); + } + + // Record result for statistics + fuzzingResults.RecordResult( + executionResult.ExecutionTimeMs, + executionResult.Crashed, + executionResult.TimedOut, + foundNewCoverage, + executionResult.ExceptionType, + executionResult.Coverage?.Select(c => ParseOpCode(c))?.Where(op => op != OpCode.NOP), + executionResult.DOSAnalysis + ); + + // Occasionally reset the VM to avoid memory issues + if (i % 1000 == 999) + { + GC.Collect(); + Thread.Sleep(100); + } + } + + // Report final results + ReportProgress(options.Iterations, options.Iterations, crashCount, timeoutCount, newCoverageCount, coverageTracker.CoverageCount); + + // Save results + string resultsFile = $"fuzzing_results_{DateTime.Now:yyyyMMdd_HHmmss}.txt"; + fuzzingResults.SaveResults(resultsFile); + + // Save histogram + string histogramFile = $"execution_time_histogram_{DateTime.Now:yyyyMMdd_HHmmss}.csv"; + fuzzingResults.SaveExecutionTimeHistogram(histogramFile); + + Console.WriteLine($"\nResults saved to {Path.Combine(options.OutputDir, resultsFile)}"); + Console.WriteLine($"Execution time histogram saved to {Path.Combine(options.OutputDir, histogramFile)}"); + Console.WriteLine("\nFuzzing completed!"); + } + + /// + /// Reports progress during fuzzing + /// + private static void ReportProgress(int current, int total, int crashes, int timeouts, int newCoverage, int totalCoverage) + { + double progress = (double)current / total * 100; + Console.WriteLine($"Progress: {current}/{total} ({progress:F2}%)"); + Console.WriteLine($"Crashes: {crashes} | Timeouts: {timeouts} | New Coverage: {newCoverage}"); + Console.WriteLine($"Total Coverage: {totalCoverage} points"); + Console.WriteLine(); + } + + /// + /// Parses an opcode from a coverage string + /// + private static OpCode ParseOpCode(string coveragePoint) + { + if (string.IsNullOrEmpty(coveragePoint) || !coveragePoint.StartsWith("OpCode:")) + { + return OpCode.NOP; + } + + string opCodeStr = coveragePoint.Substring(7); // Remove "OpCode:" prefix + + if (Enum.TryParse(opCodeStr, out var opCode)) + { + return opCode; + } + + return OpCode.NOP; + } + } +} diff --git a/fuzzers/Neo.VM.Fuzzer/Properties/AssemblyInfo.cs b/fuzzers/Neo.VM.Fuzzer/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..7333fc53ff --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/Properties/AssemblyInfo.cs @@ -0,0 +1,14 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// AssemblyInfo.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Neo.VM.Tests")] diff --git a/fuzzers/Neo.VM.Fuzzer/README.md b/fuzzers/Neo.VM.Fuzzer/README.md new file mode 100644 index 0000000000..2d67b928cc --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/README.md @@ -0,0 +1,135 @@ +# Neo VM Fuzzer + +A fuzzing engine for the Neo Virtual Machine (Neo VM) that generates random but valid scripts, executes them, and tracks any crashes or unexpected behaviors to enhance the robustness of the VM. + +## Overview + +The Neo VM Fuzzer is designed to: + +1. Generate random but valid Neo VM scripts +2. Execute these scripts in the Neo VM +3. Track code coverage and execution metrics +4. Identify crashes, exceptions, and unexpected behaviors +5. Detect potential Denial of Service (DOS) vectors +6. Evolve scripts through mutation to find more issues + +This tool helps ensure the Neo VM is robust and secure by systematically testing its behavior with a wide range of inputs. + +## Project Location + +The Neo VM Fuzzer is located in the `fuzzers` directory of the Neo project, separate from the main solution. This organization reflects its specialized purpose as a fuzzing tool for the Neo VM, which can be run independently from the main Neo project. + +## Project Structure + +``` +fuzzers/Neo.VM.Fuzzer/ +├── Program.cs # Entry point and main fuzzing loop +├── Generators/ # Script generation components +│ ├── ScriptGenerator.cs # Generates random but valid Neo VM scripts +│ └── MutationEngine.cs # Evolves scripts through mutation +├── Runners/ # VM execution components +│ ├── VMRunner.cs # Executes scripts in the Neo VM +│ └── InstrumentedExecutionEngine.cs # Custom engine for tracking execution +├── Utils/ # Utility components +│ ├── CorpusManager.cs # Manages the corpus of test scripts +│ ├── CoverageTracker.cs # Tracks code coverage during fuzzing +│ ├── DOSDetector.cs # Detects potential DOS vectors +│ └── FuzzingResults.cs # Tracks and analyzes fuzzing results +└── Documentation/ # Documentation files + ├── FUZZER_ARCHITECTURE.md # Architecture document + ├── USAGE.md # Usage instructions + ├── DOSDetection.md # DOS detection documentation + └── EXTENDING.md # Guide for extending the fuzzer +``` + +## Getting Started + +### Prerequisites + +- .NET 9.0 SDK or later +- Neo VM source code + +### Building the Fuzzer + +```bash +dotnet build fuzzers/Neo.VM.Fuzzer +``` + +### Running the Fuzzer + +Basic usage: + +```bash +dotnet run --project fuzzers/Neo.VM.Fuzzer +``` + +With options: + +```bash +dotnet run --project fuzzers/Neo.VM.Fuzzer -- -i 10000 -s 12345 -o fuzzer-output -t 10000 +``` + +## Command Line Options + +| Option | Description | Default | +|--------|-------------|---------| +| `-i, --iterations` | Number of fuzzing iterations to run | 1000 | +| `-s, --seed` | Random seed for reproducible fuzzing | Random | +| `-o, --output` | Output directory for crash reports and interesting scripts | `fuzzer-output` | +| `-t, --timeout` | Timeout in milliseconds for each VM execution | 5000 | +| `-m, --mutation-rate` | Rate of mutation for script evolution (0.0-1.0) | 0.1 | +| `-c, --corpus` | Directory with initial corpus of scripts | None | +| `-v, --verbose` | Enable verbose output | false | +| `-r, --report-interval` | Interval for progress reporting | 100 | +| `--detect-dos` | Enable detection of potential DOS vectors | false | +| `--dos-threshold` | Threshold for flagging potential DOS vectors (0.0-1.0) | 0.8 | +| `--track-memory` | Enable detailed memory tracking for DOS detection | false | +| `--track-opcodes` | Track execution time per opcode for DOS detection | true | + +## Documentation + +For more detailed information, see the documentation files: + +- [Fuzzer Architecture](Documentation/FUZZER_ARCHITECTURE.md) - Overview of the fuzzer's design and components +- [Usage Guide](Documentation/USAGE.md) - Detailed instructions for using the fuzzer +- [DOS Detection](Documentation/DOSDetection.md) - Information on detecting potential DOS vectors +- [Extension Guide](Documentation/EXTENDING.md) - Information on customizing and extending the fuzzer + +## Features + +- **Random Script Generation**: Creates valid Neo VM scripts with various opcodes and operands +- **Instrumented Execution**: Tracks code coverage and execution details +- **Crash Detection**: Identifies scripts that cause exceptions or unexpected behavior +- **DOS Detection**: Identifies scripts that could lead to denial of service conditions +- **Coverage-Guided Fuzzing**: Focuses on scripts that explore new code paths +- **Mutation Engine**: Evolves scripts to find more issues over time +- **Corpus Management**: Maintains a collection of interesting scripts for further testing +- **Detailed Reporting**: Provides comprehensive analysis of fuzzing results + +## Example Usage + +### Basic Fuzzing + +```bash +dotnet run --project fuzzers/Neo.VM.Fuzzer +``` + +### Fuzzing with DOS Detection + +```bash +dotnet run --project fuzzers/Neo.VM.Fuzzer -- --detect-dos --dos-threshold 0.7 --track-memory +``` + +### Reproducible Fuzzing with Verbose Output + +```bash +dotnet run --project fuzzers/Neo.VM.Fuzzer -- -i 5000 -s 12345 -v --detect-dos +``` + +## Contributing + +Contributions to the Neo VM Fuzzer are welcome. Please follow the standard Neo project contribution guidelines. + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. diff --git a/fuzzers/Neo.VM.Fuzzer/Runners/FaultEventArgs.cs b/fuzzers/Neo.VM.Fuzzer/Runners/FaultEventArgs.cs new file mode 100644 index 0000000000..aeab4429b9 --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/Runners/FaultEventArgs.cs @@ -0,0 +1,47 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// FaultEventArgs.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +namespace Neo.VM.Fuzzer.Runners +{ + /// + /// Provides data for the fault event in the execution engine + /// + public class FaultEventArgs : EventArgs + { + /// + /// Gets the type of exception that occurred + /// + public string ExceptionType { get; } + + /// + /// Gets the detailed message of the exception + /// + public string ExceptionMessage { get; } + + /// + /// Gets the instruction pointer position where the fault occurred + /// + public int InstructionPointer { get; } + + /// + /// Initializes a new instance of the FaultEventArgs class + /// + /// The type of exception that occurred + /// The detailed message of the exception + /// The instruction pointer position where the fault occurred + public FaultEventArgs(string exceptionType, string exceptionMessage, int instructionPointer) + { + ExceptionType = exceptionType; + ExceptionMessage = exceptionMessage; + InstructionPointer = instructionPointer; + } + } +} diff --git a/fuzzers/Neo.VM.Fuzzer/Runners/InstrumentedExecutionEngine.cs b/fuzzers/Neo.VM.Fuzzer/Runners/InstrumentedExecutionEngine.cs new file mode 100644 index 0000000000..81574e5af1 --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/Runners/InstrumentedExecutionEngine.cs @@ -0,0 +1,252 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// InstrumentedExecutionEngine.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Neo.VM.Fuzzer.Runners +{ + /// + /// An instrumented execution engine that tracks code coverage and execution details + /// + public class InstrumentedExecutionEngine : ExecutionEngine + { + private readonly Stopwatch _stopwatch = new Stopwatch(); + private readonly HashSet _coverage = new HashSet(); + private readonly Dictionary _opcodeExecutionCounts = new Dictionary(); + private readonly Dictionary> _opcodeExecutionTimes = new Dictionary>(); + private readonly Dictionary _instructionPointerCounts = new Dictionary(); + private readonly Stopwatch _opcodeStopwatch = new Stopwatch(); + private OpCode _currentOpcode; + + /// + /// Event that is raised when a step is executed + /// + public event EventHandler? OnStepEvent; + + /// + /// Event that is raised when a fault occurs + /// + public event EventHandler? OnFaultEvent; + + /// + /// Event that is raised when execution is completed + /// + public event EventHandler? OnExecutionCompleted; + + /// + /// Gets the collection of coverage points tracked during execution + /// + public HashSet Coverage => _coverage; + + /// + /// Gets the execution time in milliseconds + /// + public long ExecutionTimeMs => _stopwatch.ElapsedMilliseconds; + + /// + /// Gets the maximum stack size reached during execution + /// + public int MaxStackSize { get; private set; } = 0; + + /// + /// Gets the maximum invocation stack depth reached during execution + /// + public int MaxInvocationDepth { get; private set; } = 0; + + /// + /// Gets the number of instructions executed + /// + public int InstructionsExecuted { get; private set; } = 0; + + /// + /// Gets the dictionary of instruction execution counts + /// + public IReadOnlyDictionary InstructionExecutionCount => _instructionPointerCounts; + + /// + /// Gets the dictionary of opcode execution times in ticks + /// + public IReadOnlyDictionary> OpcodeExecutionTimes => _opcodeExecutionTimes; + + /// + /// Initializes a new instance of the InstrumentedExecutionEngine class + /// + public InstrumentedExecutionEngine() : base() + { + // In the current Neo VM version, we need to override methods instead of using events + // Event handlers will be called from overridden methods + } + + /// + /// Executes the loaded script and tracks execution metrics + /// + /// The final VM state + public new VMState Execute() + { + _stopwatch.Restart(); + MaxStackSize = 0; + MaxInvocationDepth = 0; + _coverage.Clear(); + _opcodeExecutionCounts.Clear(); + _opcodeExecutionTimes.Clear(); + _instructionPointerCounts.Clear(); + + try + { + var result = base.Execute(); + + // Raise the OnExecutionCompleted event + OnExecutionCompleted?.Invoke(this, EventArgs.Empty); + + return result; + } + finally + { + _stopwatch.Stop(); + } + } + + /// + /// Handles the step event to track execution details + /// + protected override void PreExecuteInstruction(Instruction instruction) + { + base.PreExecuteInstruction(instruction); + + // Start timing the opcode execution + _opcodeStopwatch.Restart(); + + // Save the current opcode for timing + _currentOpcode = instruction.OpCode; + + // Track instruction pointer coverage + int ip = CurrentContext.InstructionPointer; + if (CurrentContext?.Script != null) + { + string key = $"{CurrentContext.Script.GetHashCode()}:{ip}"; + _coverage.Add(key); + } + + // Track instruction execution count + if (!_instructionPointerCounts.ContainsKey(ip)) + { + _instructionPointerCounts[ip] = 1; + } + else + { + _instructionPointerCounts[ip]++; + } + + // Increment the instruction counter + InstructionsExecuted++; + + // Track specific patterns + TrackSpecificPatterns(instruction.OpCode); + + // Raise the OnStep event + if (CurrentContext != null) + { + OnStepEvent?.Invoke(this, new StepEventArgs(instruction.OpCode, ip, CurrentContext.EvaluationStack.Count)); + } + } + + /// + /// Handles post-execution of an instruction + /// + protected override void PostExecuteInstruction(Instruction instruction) + { + base.PostExecuteInstruction(instruction); + + // Stop timing the opcode execution + _opcodeStopwatch.Stop(); + + // Track opcode execution time + if (!_opcodeExecutionTimes.ContainsKey(_currentOpcode)) + { + _opcodeExecutionTimes[_currentOpcode] = new List(); + } + _opcodeExecutionTimes[_currentOpcode].Add(_opcodeStopwatch.ElapsedTicks); + + // Track opcode execution count + if (!_opcodeExecutionCounts.ContainsKey(_currentOpcode)) + { + _opcodeExecutionCounts[_currentOpcode] = 1; + } + else + { + _opcodeExecutionCounts[_currentOpcode]++; + } + } + + /// + /// Handles faults during execution + /// + protected override void OnFault(Exception ex) + { + base.OnFault(ex); + + // Raise the OnFaultEvent + int instructionPointer = CurrentContext?.InstructionPointer ?? 0; + OnFaultEvent?.Invoke(this, new FaultEventArgs(ex.GetType().Name, ex.Message, instructionPointer)); + } + + /// + /// Tracks specific patterns of opcodes that might be interesting for fuzzing + /// + /// The current opcode + private void TrackSpecificPatterns(OpCode opcode) + { + // Track arithmetic operations + if (opcode >= OpCode.ADD && opcode <= OpCode.MOD) + { + _coverage.Add("Pattern:Arithmetic"); + } + + // Track stack operations + if (opcode >= OpCode.DUP && opcode <= OpCode.DEPTH) + { + _coverage.Add("Pattern:StackOp"); + } + + // Track control flow operations + if (opcode >= OpCode.JMP && opcode <= OpCode.ENDTRY) + { + _coverage.Add("Pattern:ControlFlow"); + } + + // Track array/struct operations + if ((opcode >= OpCode.NEWARRAY && opcode <= OpCode.NEWSTRUCT) || + (opcode >= OpCode.APPEND && opcode <= OpCode.SETITEM)) + { + _coverage.Add("Pattern:ArrayStruct"); + } + + // Track crypto operations - removed SHA1 and CHECKMULTISIG as they don't exist in current Neo VM + if (opcode == OpCode.EQUAL) + { + _coverage.Add("Pattern:Crypto"); + } + + // Track potentially expensive operations for DOS detection + if (opcode == OpCode.APPEND || + opcode == OpCode.SETITEM || + opcode == OpCode.NEWARRAY || + opcode == OpCode.NEWSTRUCT || + opcode == OpCode.UNPACK) + { + _coverage.Add("Pattern:ExpensiveOperation"); + } + } + } +} diff --git a/fuzzers/Neo.VM.Fuzzer/Runners/StackItem.cs b/fuzzers/Neo.VM.Fuzzer/Runners/StackItem.cs new file mode 100644 index 0000000000..8c1b776a66 --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/Runners/StackItem.cs @@ -0,0 +1,50 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// StackItem.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; + +namespace Neo.VM.Fuzzer.Runners +{ + /// + /// Represents an item on the VM execution stack + /// + public class StackItem + { + /// + /// Gets the type of the stack item + /// + public string Type { get; } + + /// + /// Gets the string representation of the stack item value + /// + public string Value { get; } + + /// + /// Initializes a new instance of the StackItem class + /// + /// The type of the stack item + /// The string representation of the value + public StackItem(string type, string value) + { + Type = type ?? throw new ArgumentNullException(nameof(type)); + Value = value ?? throw new ArgumentNullException(nameof(value)); + } + + /// + /// Returns a string representation of the stack item + /// + public override string ToString() + { + return $"{Type}: {Value}"; + } + } +} diff --git a/fuzzers/Neo.VM.Fuzzer/Runners/StepEventArgs.cs b/fuzzers/Neo.VM.Fuzzer/Runners/StepEventArgs.cs new file mode 100644 index 0000000000..2894115a73 --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/Runners/StepEventArgs.cs @@ -0,0 +1,47 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// StepEventArgs.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +namespace Neo.VM.Fuzzer.Runners +{ + /// + /// Provides data for the step event in the execution engine + /// + public class StepEventArgs : EventArgs + { + /// + /// Gets the operation code being executed + /// + public OpCode OpCode { get; } + + /// + /// Gets the current instruction pointer position + /// + public int InstructionPointer { get; } + + /// + /// Gets the current size of the evaluation stack + /// + public int StackSize { get; } + + /// + /// Initializes a new instance of the StepEventArgs class + /// + /// The operation code being executed + /// The current instruction pointer position + /// The current size of the evaluation stack + public StepEventArgs(OpCode opCode, int instructionPointer, int stackSize) + { + OpCode = opCode; + InstructionPointer = instructionPointer; + StackSize = stackSize; + } + } +} diff --git a/fuzzers/Neo.VM.Fuzzer/Runners/VMRunner.cs b/fuzzers/Neo.VM.Fuzzer/Runners/VMRunner.cs new file mode 100644 index 0000000000..145ea11162 --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/Runners/VMRunner.cs @@ -0,0 +1,276 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// VMRunner.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.VM; +using Neo.VM.Fuzzer.Utils; +using Neo.VM.Types; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Neo.VM.Fuzzer.Runners +{ + /// + /// Executes Neo VM scripts and tracks execution results + /// + public class VMRunner + { + private readonly HashSet _coverage = new(); + private readonly int _timeoutMs; + private readonly bool _detectDOS; + private readonly double _dosThreshold; + private readonly bool _trackMemory; + private readonly bool _trackOpcodes; + private readonly DOSDetector? _dosDetector; + + /// + /// Creates a new VM runner + /// + /// Timeout in milliseconds for script execution + /// Whether to detect potential DOS vectors + /// Threshold for flagging potential DOS vectors (0.0-1.0) + /// Whether to track memory usage + /// Whether to track execution time per opcode + public VMRunner(int timeoutMs = 5000, bool detectDOS = false, double dosThreshold = 0.8, bool trackMemory = false, bool trackOpcodes = true) + { + _timeoutMs = timeoutMs; + _detectDOS = detectDOS; + _dosThreshold = dosThreshold; + _trackMemory = trackMemory; + _trackOpcodes = trackOpcodes; + + if (_detectDOS) + { + _dosDetector = new DOSDetector(_dosThreshold, _trackMemory, _trackOpcodes); + } + } + + /// + /// Executes a script and returns the execution result + /// + /// The script bytes to execute + /// An execution result with details about the execution + public ExecutionResult Execute(byte[] script) + { + var result = new ExecutionResult(); + _coverage.Clear(); + + if (_detectDOS) + { + _dosDetector?.Reset(); + } + + using var cts = new CancellationTokenSource(); + cts.CancelAfter(_timeoutMs); + + try + { + var task = Task.Run(() => + { + try + { + using var engine = new InstrumentedExecutionEngine(); + InstrumentEngine(engine, result); + + engine.LoadScript(script); + engine.Execute(); + + result.State = engine.State; + result.FinalStack = engine.ResultStack.ToArray(); + result.Success = engine.State == VMState.HALT; + } + catch (Exception ex) + { + result.Exception = ex; + result.ExceptionType = ex.GetType().Name; + result.ExceptionMessage = ex.Message; + result.Crashed = true; + result.Success = false; + } + }, cts.Token); + + if (!task.Wait(_timeoutMs)) + { + result.TimedOut = true; + result.Success = false; + } + } + catch (OperationCanceledException) + { + result.TimedOut = true; + result.Success = false; + } + catch (Exception ex) + { + result.Exception = ex; + result.ExceptionType = ex.GetType().Name; + result.ExceptionMessage = ex.Message; + result.Crashed = true; + result.Success = false; + } + + return result; + } + + /// + /// Instruments the execution engine with event handlers and trackers + /// + private void InstrumentEngine(InstrumentedExecutionEngine engine, ExecutionResult result) + { + // Track execution time + var stopwatch = Stopwatch.StartNew(); + + // Set up DOS detection if enabled + if (_detectDOS && _dosDetector != null) + { + engine.OnStepEvent += _dosDetector.OnStep; + engine.OnFaultEvent += _dosDetector.OnFault; + } + + // Track coverage + engine.OnStepEvent += (sender, e) => + { + _coverage.Add($"Opcode:{e.OpCode}"); + }; + + // Record execution time when finished + AppDomain.CurrentDomain.ProcessExit += (sender, e) => + { + stopwatch.Stop(); + result.ExecutionTimeMs = stopwatch.Elapsed.TotalMilliseconds; + }; + + // Handle normal completion + engine.OnExecutionCompleted += (sender, e) => + { + stopwatch.Stop(); + result.ExecutionTimeMs = stopwatch.Elapsed.TotalMilliseconds; + result.Coverage = engine.Coverage.ToList(); + + // Perform DOS analysis if enabled + if (_detectDOS) + { + int totalInstructions = engine.InstructionsExecuted; + var opcodeExecutionTimes = engine.OpcodeExecutionTimes; + result.DOSAnalysis = _dosDetector?.Analyze( + totalInstructions, + opcodeExecutionTimes, + result.ExecutionTimeMs); + } + }; + + // Handle faults + engine.OnFaultEvent += (sender, e) => + { + stopwatch.Stop(); + result.ExecutionTimeMs = stopwatch.Elapsed.TotalMilliseconds; + result.ExceptionType = e.ExceptionType; + result.Crashed = true; + result.Coverage = engine.Coverage.ToList(); + + // Perform DOS analysis if enabled (even for crashed scripts) + if (_detectDOS) + { + int totalInstructions = engine.InstructionsExecuted; + var opcodeExecutionTimes = engine.OpcodeExecutionTimes; + result.DOSAnalysis = _dosDetector?.Analyze( + totalInstructions, + opcodeExecutionTimes, + result.ExecutionTimeMs); + } + }; + } + + /// + /// Checks if executing a script found new coverage + /// + public bool FoundNewCoverage(byte[] script, ExecutionResult result) + { + if (result.TimedOut || !result.Success) return false; + + var previousCoverage = new HashSet(_coverage); + Execute(script); + return _coverage.Except(previousCoverage).Any(); + } + + /// + /// Gets the current coverage + /// + public HashSet GetCoverage() + { + return new HashSet(_coverage); + } + } + + /// + /// Result of executing a script + /// + public class ExecutionResult + { + /// + /// Gets or sets the final VM state + /// + public VMState State { get; set; } + + /// + /// Gets or sets whether the execution was successful + /// + public bool Success { get; set; } + + /// + /// Gets or sets whether the execution timed out + /// + public bool TimedOut { get; set; } + + /// + /// Gets or sets whether the execution crashed + /// + public bool Crashed { get; set; } + + /// + /// Gets or sets the execution time in milliseconds + /// + public double ExecutionTimeMs { get; set; } + + /// + /// Gets or sets the exception that occurred during execution + /// + public Exception? Exception { get; set; } + + /// + /// Gets or sets the type of exception that occurred + /// + public string? ExceptionType { get; set; } + + /// + /// Gets or sets the exception message + /// + public string? ExceptionMessage { get; set; } + + /// + /// Gets or sets the final stack contents + /// + public Neo.VM.Types.StackItem[]? FinalStack { get; set; } = System.Array.Empty(); + + /// + /// Gets or sets the coverage information + /// + public List Coverage { get; set; } = new List(); + + /// + /// Gets or sets the DOS analysis result + /// + public DOSDetector.DOSAnalysisResult? DOSAnalysis { get; set; } + } +} diff --git a/fuzzers/Neo.VM.Fuzzer/Utils/ConfigurationManager.cs b/fuzzers/Neo.VM.Fuzzer/Utils/ConfigurationManager.cs new file mode 100644 index 0000000000..ac08e01345 --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/Utils/ConfigurationManager.cs @@ -0,0 +1,289 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// ConfigurationManager.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; +using System.IO; +using System.Text.Json; + +namespace Neo.VM.Fuzzer.Utils +{ + /// + /// Manages the configuration settings for the Neo VM Fuzzer + /// + public class ConfigurationManager + { + private static ConfigurationManager? _instance; + + /// + /// Gets the singleton instance of the ConfigurationManager + /// + public static ConfigurationManager Instance => _instance ??= new ConfigurationManager(); + + /// + /// Gets or sets the number of fuzzing iterations to run + /// + public int Iterations { get; set; } = 1000; + + /// + /// Gets or sets the random seed for reproducibility + /// + public int Seed { get; set; } = new Random().Next(); + + /// + /// Gets or sets the output directory for fuzzing results + /// + public string OutputDirectory { get; set; } = Path.Combine(Directory.GetCurrentDirectory(), "fuzzer_output"); + + /// + /// Gets or sets the directory containing initial corpus scripts + /// + public string? CorpusDirectory { get; set; } + + /// + /// Gets or sets the timeout in milliseconds for script execution + /// + public int TimeoutMs { get; set; } = 1000; + + /// + /// Gets or sets the mutation rate (0.0 to 1.0) + /// + public double MutationRate { get; set; } = 0.1; + + /// + /// Gets or sets whether to output verbose information + /// + public bool Verbose { get; set; } = false; + + /// + /// Gets or sets whether to use guided fuzzing + /// + public bool GuidedFuzzing { get; set; } = true; + + /// + /// Gets or sets the maximum script size in bytes + /// + public int MaxScriptSize { get; set; } = 1024; + + /// + /// Gets or sets the minimum script size in bytes + /// + public int MinScriptSize { get; set; } = 10; + + /// + /// Gets or sets the probability of generating arithmetic-heavy scripts + /// + public double ArithmeticProbability { get; set; } = 0.3; + + /// + /// Gets or sets the probability of generating stack-heavy scripts + /// + public double StackProbability { get; set; } = 0.3; + + /// + /// Gets or sets the probability of generating array-heavy scripts + /// + public double ArrayProbability { get; set; } = 0.2; + + /// + /// Gets or sets the probability of generating random scripts + /// + public double RandomProbability { get; set; } = 0.2; + + /// + /// Gets or sets the probability of using crossover mutation + /// + public double CrossoverProbability { get; set; } = 0.2; + + /// + /// Gets or sets the maximum number of scripts to keep in the corpus + /// + public int MaxCorpusSize { get; set; } = 1000; + + /// + /// Gets or sets whether to save all crashes + /// + public bool SaveAllCrashes { get; set; } = true; + + /// + /// Gets or sets whether to save all interesting scripts + /// + public bool SaveAllInteresting { get; set; } = true; + + /// + /// Gets or sets the interval for saving progress reports + /// + public int ProgressReportInterval { get; set; } = 100; + + /// + /// Private constructor to enforce singleton pattern + /// + private ConfigurationManager() + { + } + + /// + /// Loads configuration from a JSON file + /// + /// Path to the configuration file + /// True if configuration was loaded successfully, false otherwise + public bool LoadFromFile(string filePath) + { + try + { + if (!File.Exists(filePath)) + { + return false; + } + + string json = File.ReadAllText(filePath); + var config = JsonSerializer.Deserialize(json); + + if (config == null) + { + return false; + } + + // Copy properties from loaded config to this instance + Iterations = config.Iterations; + Seed = config.Seed; + OutputDirectory = config.OutputDirectory; + CorpusDirectory = config.CorpusDirectory; + TimeoutMs = config.TimeoutMs; + MutationRate = config.MutationRate; + Verbose = config.Verbose; + GuidedFuzzing = config.GuidedFuzzing; + MaxScriptSize = config.MaxScriptSize; + MinScriptSize = config.MinScriptSize; + ArithmeticProbability = config.ArithmeticProbability; + StackProbability = config.StackProbability; + ArrayProbability = config.ArrayProbability; + RandomProbability = config.RandomProbability; + CrossoverProbability = config.CrossoverProbability; + MaxCorpusSize = config.MaxCorpusSize; + SaveAllCrashes = config.SaveAllCrashes; + SaveAllInteresting = config.SaveAllInteresting; + ProgressReportInterval = config.ProgressReportInterval; + + return true; + } + catch (Exception ex) + { + Console.WriteLine($"Error loading configuration: {ex.Message}"); + return false; + } + } + + /// + /// Saves the current configuration to a JSON file + /// + /// Path to save the configuration file + /// True if configuration was saved successfully, false otherwise + public bool SaveToFile(string filePath) + { + try + { + string json = JsonSerializer.Serialize(this, new JsonSerializerOptions + { + WriteIndented = true + }); + + File.WriteAllText(filePath, json); + return true; + } + catch (Exception ex) + { + Console.WriteLine($"Error saving configuration: {ex.Message}"); + return false; + } + } + + /// + /// Creates a default configuration file if one doesn't exist + /// + /// Path to the configuration file + /// True if configuration was created successfully, false otherwise + public static bool CreateDefaultConfigurationFile(string filePath) + { + try + { + if (File.Exists(filePath)) + { + return true; + } + + return Instance.SaveToFile(filePath); + } + catch (Exception ex) + { + Console.WriteLine($"Error creating default configuration: {ex.Message}"); + return false; + } + } + + /// + /// Validates the current configuration settings + /// + /// True if configuration is valid, false otherwise + public bool Validate() + { + // Ensure output directory exists or can be created + try + { + Directory.CreateDirectory(OutputDirectory); + } + catch + { + Console.WriteLine($"Error: Cannot create output directory: {OutputDirectory}"); + return false; + } + + // Validate corpus directory if specified + if (!string.IsNullOrEmpty(CorpusDirectory) && !Directory.Exists(CorpusDirectory)) + { + Console.WriteLine($"Warning: Corpus directory does not exist: {CorpusDirectory}"); + } + + // Validate numeric ranges + if (Iterations <= 0) + { + Console.WriteLine("Error: Iterations must be greater than 0"); + return false; + } + + if (TimeoutMs <= 0) + { + Console.WriteLine("Error: Timeout must be greater than 0"); + return false; + } + + if (MutationRate < 0.0 || MutationRate > 1.0) + { + Console.WriteLine("Error: Mutation rate must be between 0.0 and 1.0"); + return false; + } + + if (MaxScriptSize <= 0 || MinScriptSize <= 0 || MinScriptSize > MaxScriptSize) + { + Console.WriteLine("Error: Invalid script size range"); + return false; + } + + // Validate probability distributions + double totalProbability = ArithmeticProbability + StackProbability + ArrayProbability + RandomProbability; + if (Math.Abs(totalProbability - 1.0) > 0.001) + { + Console.WriteLine($"Warning: Script type probabilities do not sum to 1.0 (sum: {totalProbability:F3})"); + } + + return true; + } + } +} diff --git a/fuzzers/Neo.VM.Fuzzer/Utils/CorpusManager.cs b/fuzzers/Neo.VM.Fuzzer/Utils/CorpusManager.cs new file mode 100644 index 0000000000..3f86ebbcff --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/Utils/CorpusManager.cs @@ -0,0 +1,199 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// CorpusManager.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; + +namespace Neo.VM.Fuzzer.Utils +{ + /// + /// Manages the corpus of scripts for fuzzing, including saving and loading interesting scripts + /// + public class CorpusManager + { + private readonly string _outputDir; + private readonly string _crashesDir; + private readonly string _corpusDir; + private readonly string _dosVectorsDir; + private readonly HashSet _knownCorpusHashes = new(); + private readonly HashSet _knownCrashHashes = new(); + private readonly HashSet _knownDOSVectorHashes = new(); + private readonly List _corpus = new List(); + private readonly Random _random = new Random(); + + /// + /// Gets the number of scripts in the corpus + /// + public int CorpusSize => _corpus.Count; + + /// + /// Creates a new corpus manager + /// + /// Directory to save results to + /// Optional directory with initial corpus of scripts + public CorpusManager(string outputDir, string? initialCorpusDir = null) + { + _outputDir = outputDir; + _corpusDir = Path.Combine(outputDir, "corpus"); + _crashesDir = Path.Combine(outputDir, "crashes"); + _dosVectorsDir = Path.Combine(outputDir, "dos-vectors"); + + // Create output directories if they don't exist + Directory.CreateDirectory(_outputDir); + Directory.CreateDirectory(_corpusDir); + Directory.CreateDirectory(_crashesDir); + Directory.CreateDirectory(_dosVectorsDir); + } + + /// + /// Loads the initial corpus from the corpus directory + /// + public void LoadCorpus() + { + if (string.IsNullOrEmpty(_corpusDir) || !Directory.Exists(_corpusDir)) + { + return; + } + + foreach (var file in Directory.GetFiles(_corpusDir, "*.bin")) + { + try + { + byte[] script = File.ReadAllBytes(file); + _corpus.Add(script); + } + catch (Exception ex) + { + Console.WriteLine($"Error loading corpus file {file}: {ex.Message}"); + } + } + } + + /// + /// Gets a random script from the corpus + /// + /// A random script from the corpus + public byte[] GetRandomScript() + { + if (_corpus.Count == 0) + { + throw new InvalidOperationException("Corpus is empty"); + } + + return _corpus[_random.Next(_corpus.Count)]; + } + + /// + /// Saves a script that caused a crash + /// + /// The script that caused the crash + /// The exception message + public void SaveCrash(byte[] script, string? exceptionMessage) + { + string crashDir = _crashesDir; + string filename = $"crash_{DateTime.Now:yyyyMMdd_HHmmss}_{ComputeHash(script)}.bin"; + string path = Path.Combine(crashDir, filename); + + File.WriteAllBytes(path, script); + + // Save metadata + if (!string.IsNullOrEmpty(exceptionMessage)) + { + string metadataPath = Path.ChangeExtension(path, ".txt"); + File.WriteAllText(metadataPath, exceptionMessage); + } + } + + /// + /// Saves an interesting script that found new coverage + /// + /// The interesting script + public void SaveInteresting(byte[] script) + { + // Add to corpus + _corpus.Add(script); + + // Save to disk + string interestingDir = _corpusDir; + string filename = $"interesting_{DateTime.Now:yyyyMMdd_HHmmss}_{ComputeHash(script)}.bin"; + string path = Path.Combine(interestingDir, filename); + + File.WriteAllBytes(path, script); + } + + /// + /// Saves a script that is identified as a potential DOS vector + /// + /// The script bytes + /// The DOS analysis result + /// True if the script was saved, false if it was already known + public bool SaveDOSVector(byte[] script, DOSDetector.DOSAnalysisResult dosAnalysis) + { + // Generate a hash of the script to check if we've seen it before + string hash = ComputeHash(script); + + // Skip if we've already seen this DOS vector + if (_knownDOSVectorHashes.Contains(hash)) + { + return false; + } + + // Add to known DOS vectors + _knownDOSVectorHashes.Add(hash); + + // Generate a unique filename + string timestamp = DateTime.Now.ToString("yyyyMMdd-HHmmss"); + string scoreStr = dosAnalysis.DOSScore.ToString("F2").Replace(".", "_"); + string reason = dosAnalysis.DetectionReason.Replace(" ", "_").Replace(":", "_"); + string filename = $"dos-{timestamp}-{scoreStr}-{reason}-{hash.Substring(0, 8)}"; + + // Save the script + string scriptPath = Path.Combine(_dosVectorsDir, $"{filename}.bin"); + File.WriteAllBytes(scriptPath, script); + + // Save the analysis details + string analysisPath = Path.Combine(_dosVectorsDir, $"{filename}.txt"); + using var writer = new StreamWriter(analysisPath); + + writer.WriteLine($"DOS Vector Analysis: {filename}"); + writer.WriteLine($"Timestamp: {DateTime.Now}"); + writer.WriteLine($"DOS Score: {dosAnalysis.DOSScore:F2}"); + writer.WriteLine($"Detection Reason: {dosAnalysis.DetectionReason}"); + writer.WriteLine(); + + writer.WriteLine("Metrics:"); + foreach (var metric in dosAnalysis.Metrics) + { + writer.WriteLine($" {metric.Key}: {metric.Value}"); + } + + writer.WriteLine(); + writer.WriteLine("Recommendations:"); + foreach (var recommendation in dosAnalysis.Recommendations) + { + writer.WriteLine($" - {recommendation}"); + } + + return true; + } + + private string ComputeHash(byte[] data) + { + using var sha256 = SHA256.Create(); + var hashBytes = sha256.ComputeHash(data); + return BitConverter.ToString(hashBytes).Replace("-", "").ToLower(); + } + } +} diff --git a/fuzzers/Neo.VM.Fuzzer/Utils/CoverageTracker.cs b/fuzzers/Neo.VM.Fuzzer/Utils/CoverageTracker.cs new file mode 100644 index 0000000000..822acc0a30 --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/Utils/CoverageTracker.cs @@ -0,0 +1,164 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// CoverageTracker.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Neo.VM.Fuzzer.Utils +{ + /// + /// Tracks code coverage during fuzzing to identify interesting scripts + /// + public class CoverageTracker + { + private readonly HashSet _globalCoverage = new HashSet(); + private readonly Dictionary _coverageFrequency = new Dictionary(); + + /// + /// Gets the total number of unique coverage points seen + /// + public int TotalCoverage => _globalCoverage.Count; + + /// + /// Gets the number of unique coverage points seen + /// + public int CoverageCount => _globalCoverage.Count; + + /// + /// Checks if the provided coverage contains any new coverage points + /// + /// The coverage from a script execution + /// True if new coverage was found, false otherwise + public bool HasNewCoverage(HashSet coverage) + { + if (coverage == null || coverage.Count == 0) + { + return false; + } + + bool hasNewCoverage = false; + + // Check for new coverage points + foreach (var point in coverage) + { + if (_globalCoverage.Add(point)) + { + hasNewCoverage = true; + } + + // Update frequency + if (!_coverageFrequency.TryGetValue(point, out int count)) + { + _coverageFrequency[point] = 1; + } + else + { + _coverageFrequency[point] = count + 1; + } + } + + return hasNewCoverage; + } + + /// + /// Adds a single coverage point and checks if it's new + /// + /// The coverage point to add + /// True if this is a new coverage point, false otherwise + public bool AddCoveragePoint(string point) + { + if (string.IsNullOrEmpty(point)) + { + return false; + } + + bool isNew = _globalCoverage.Add(point); + + // Update frequency + if (!_coverageFrequency.TryGetValue(point, out int count)) + { + _coverageFrequency[point] = 1; + } + else + { + _coverageFrequency[point] = count + 1; + } + + return isNew; + } + + /// + /// Gets a report of the current coverage statistics + /// + /// A string containing coverage statistics + public string GetCoverageReport() + { + var report = new System.Text.StringBuilder(); + + report.AppendLine($"Total Coverage Points: {TotalCoverage}"); + + // Report OpCode coverage + var opcodeCoverage = _globalCoverage + .Where(c => c.StartsWith("OpCode:")) + .Select(c => c.Substring(7)) + .ToList(); + + report.AppendLine($"OpCode Coverage: {opcodeCoverage.Count} unique opcodes"); + + // Top 10 most frequent coverage points + var topFrequent = _coverageFrequency + .OrderByDescending(kv => kv.Value) + .Take(10) + .ToList(); + + report.AppendLine("\nTop 10 Most Frequent Coverage Points:"); + foreach (var kv in topFrequent) + { + report.AppendLine($" {kv.Key}: {kv.Value} times"); + } + + // Top 10 least frequent coverage points + var leastFrequent = _coverageFrequency + .OrderBy(kv => kv.Value) + .Take(10) + .ToList(); + + report.AppendLine("\nTop 10 Least Frequent Coverage Points:"); + foreach (var kv in leastFrequent) + { + report.AppendLine($" {kv.Key}: {kv.Value} times"); + } + + return report.ToString(); + } + + /// + /// Saves the coverage report to a file + /// + /// The path to save the report to + public void SaveCoverageReport(string filePath) + { + string report = GetCoverageReport(); + System.IO.File.WriteAllText(filePath, report); + + // Also save raw coverage data for further analysis + string rawDataPath = System.IO.Path.ChangeExtension(filePath, ".csv"); + using var writer = new System.IO.StreamWriter(rawDataPath); + + writer.WriteLine("CoveragePoint,Frequency"); + foreach (var kv in _coverageFrequency.OrderByDescending(kv => kv.Value)) + { + writer.WriteLine($"{kv.Key},{kv.Value}"); + } + } + } +} diff --git a/fuzzers/Neo.VM.Fuzzer/Utils/DOSDetector.cs b/fuzzers/Neo.VM.Fuzzer/Utils/DOSDetector.cs new file mode 100644 index 0000000000..2424062034 --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/Utils/DOSDetector.cs @@ -0,0 +1,308 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// DOSDetector.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.VM; +using Neo.VM.Fuzzer.Runners; +using Neo.VM.Types; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Neo.VM.Fuzzer.Utils +{ + /// + /// Detects potential Denial of Service (DOS) vectors in Neo VM scripts + /// + public class DOSDetector + { + private readonly Dictionary> _opcodeExecutionTimes = new(); + private readonly Dictionary _opcodeExecutionCounts = new(); + private readonly List _stackDepthSamples = new(); + private readonly List _stateSnapshots = new(); + private readonly Stopwatch _stopwatch = new(); + private int _totalInstructions = 0; + private int _maxStackDepth = 0; + private long _totalExecutionTime = 0; + private readonly double _dosThreshold; + private readonly bool _trackMemory; + private readonly bool _trackOpcodes; + + /// + /// Represents a snapshot of the VM state at a point in time + /// + private class StateSnapshot + { + public OpCode CurrentOpCode { get; set; } + public int InstructionPointer { get; set; } + public int StackDepth { get; set; } + public long Timestamp { get; set; } + + public override bool Equals(object? obj) + { + if (obj is not StateSnapshot other) return false; + return CurrentOpCode == other.CurrentOpCode && + InstructionPointer == other.InstructionPointer && + StackDepth == other.StackDepth; + } + + public override int GetHashCode() + { + return HashCode.Combine(CurrentOpCode, InstructionPointer, StackDepth); + } + } + + /// + /// Results of DOS detection analysis + /// + public class DOSAnalysisResult + { + public bool IsPotentialDOSVector { get; set; } + public double DOSScore { get; set; } + public string DetectionReason { get; set; } = string.Empty; + public Dictionary Metrics { get; set; } = new(); + public List Recommendations { get; set; } = new(); + } + + /// + /// Creates a new DOS detector + /// + /// Threshold for flagging potential DOS vectors (0.0-1.0) + /// Whether to track memory usage + /// Whether to track execution time per opcode + public DOSDetector(double dosThreshold = 0.8, bool trackMemory = false, bool trackOpcodes = true) + { + _dosThreshold = dosThreshold; + _trackMemory = trackMemory; + _trackOpcodes = trackOpcodes; + _stopwatch.Start(); // Start the stopwatch immediately to track execution time + } + + /// + /// Handles the OnStep event from the execution engine + /// + public void OnStep(object sender, StepEventArgs e) + { + if (sender is not ExecutionEngine engine) return; + + _totalInstructions++; + + // Track stack depth + int stackDepth = e.StackSize; + _stackDepthSamples.Add(stackDepth); + + if (stackDepth > _maxStackDepth) + { + _maxStackDepth = stackDepth; + } + + // Take a snapshot of the current state + _stateSnapshots.Add(new StateSnapshot + { + CurrentOpCode = e.OpCode, + InstructionPointer = e.InstructionPointer, + StackDepth = stackDepth, + Timestamp = _stopwatch.ElapsedTicks + }); + } + + /// + /// Handles exceptions during execution + /// + public void OnFault(object sender, FaultEventArgs e) + { + // Record the exception for analysis + _stopwatch.Stop(); + + // Add the exception to state snapshots for analysis + _stateSnapshots.Add(new StateSnapshot + { + CurrentOpCode = OpCode.RET, + InstructionPointer = e.InstructionPointer, + StackDepth = 0, + Timestamp = _stopwatch.ElapsedTicks + }); + } + + /// + /// Resets the detector for a new execution + /// + public void Reset() + { + _opcodeExecutionTimes.Clear(); + _opcodeExecutionCounts.Clear(); + _stackDepthSamples.Clear(); + _stateSnapshots.Clear(); + _totalInstructions = 0; + _maxStackDepth = 0; + _totalExecutionTime = 0; + _stopwatch.Reset(); + _stopwatch.Start(); // Restart the stopwatch for the new execution + } + + /// + /// Analyzes the execution metrics to detect potential DOS vectors + /// + /// Total number of instructions executed + /// Dictionary of opcode execution times + /// Total execution time in milliseconds + /// Analysis result with DOS detection information + public DOSAnalysisResult Analyze( + int instructionCount, + IReadOnlyDictionary> opcodeExecutionTimes, + double totalExecutionTimeMs) + { + var result = new DOSAnalysisResult + { + IsPotentialDOSVector = false, + DOSScore = 0.0, + DetectionReason = string.Empty, + Metrics = new Dictionary + { + ["TotalInstructions"] = _totalInstructions, + ["MaxStackDepth"] = _maxStackDepth, + ["UniqueOpcodes"] = _opcodeExecutionCounts.Count, + ["TotalExecutionTimeMs"] = totalExecutionTimeMs + }, + Recommendations = new List() + }; + + double score = 0.0; + List detectionReasons = new(); + + // Check for high instruction count - LOWERED THRESHOLD FROM 5000 to 100 + if (_totalInstructions > 100) + { + double instructionScore = Math.Min(0.5, _totalInstructions / 1000.0); + score += instructionScore; + detectionReasons.Add($"High instruction count: {_totalInstructions}"); + result.Metrics["InstructionScore"] = instructionScore; + result.Recommendations.Add("Consider adding instruction count limits to prevent excessive execution"); + } + + // Check for potential infinite loops + var loopScore = DetectPotentialInfiniteLoops(result, detectionReasons); + score += loopScore; + result.Metrics["LoopScore"] = loopScore; + + // Check for excessive stack usage - LOWERED THRESHOLD FROM 50 to 5 + if (_maxStackDepth > 5) + { + double stackScore = Math.Min(0.3, _maxStackDepth / 50.0); + score += stackScore; + detectionReasons.Add($"Excessive stack depth: {_maxStackDepth}"); + result.Metrics["StackScore"] = stackScore; + result.Recommendations.Add("Consider adding stack depth limits to prevent stack overflow attacks"); + } + + // Check for slow opcodes - LOWERED THRESHOLD FROM 0.2ms to 0.05ms and from 5 executions to 2 + if (opcodeExecutionTimes != null && opcodeExecutionTimes.Count > 0) + { + var slowOpcodes = new Dictionary(); + double totalOpTime = 0; + + foreach (var kvp in opcodeExecutionTimes) + { + if (kvp.Value.Count == 0) continue; + + double avgTime = kvp.Value.Average(); + totalOpTime += avgTime * _opcodeExecutionCounts.GetValueOrDefault(kvp.Key, 0); + + // Identify particularly slow opcodes + if (avgTime > 0.05 && _opcodeExecutionCounts.GetValueOrDefault(kvp.Key, 0) > 2) + { + slowOpcodes[kvp.Key] = avgTime; + } + } + + if (slowOpcodes.Count > 0) + { + double opcodeScore = Math.Min(0.4, slowOpcodes.Count / 5.0); + score += opcodeScore; + + var topSlowOpcodes = slowOpcodes + .OrderByDescending(kvp => kvp.Value * _opcodeExecutionCounts.GetValueOrDefault(kvp.Key, 0)) + .Take(3) + .ToList(); + + detectionReasons.Add($"Slow opcodes: {string.Join(", ", topSlowOpcodes.Select(kvp => kvp.Key))}"); + result.Metrics["SlowOpcodes"] = topSlowOpcodes.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value); + result.Metrics["OpcodeScore"] = opcodeScore; + + result.Recommendations.Add("Optimize usage of slow opcodes or consider adding execution time limits"); + } + } + + // Check for long execution time - LOWERED THRESHOLD FROM 500ms to 10ms + if (totalExecutionTimeMs > 10) + { + double timeScore = Math.Min(0.5, totalExecutionTimeMs / 100.0); + score += timeScore; + detectionReasons.Add($"Long execution time: {totalExecutionTimeMs:F2}ms"); + result.Metrics["TimeScore"] = timeScore; + result.Recommendations.Add("Consider adding execution time limits to prevent long-running scripts"); + } + + // Normalize score to 0.0-1.0 range + result.DOSScore = Math.Min(1.0, score); + + // Determine if this is a potential DOS vector + result.IsPotentialDOSVector = result.DOSScore >= _dosThreshold; + + // Set detection reason + if (detectionReasons.Count > 0) + { + result.DetectionReason = string.Join("; ", detectionReasons); + } + + return result; + } + + private double DetectPotentialInfiniteLoops(DOSAnalysisResult result, List detectionReasons) + { + // Look for repeated state patterns - LOWERED THRESHOLD FROM 10 to 5 repetitions and from 30% to 20% ratio + var repeatedStates = _stateSnapshots + .GroupBy(s => new { s.CurrentOpCode, s.InstructionPointer, s.StackDepth }) + .Where(g => g.Count() > 5) // More than 5 identical states + .OrderByDescending(g => g.Count()) + .ToList(); + + if (repeatedStates.Any()) + { + var topRepeatedState = repeatedStates.First(); + double repetitionRatio = (double)topRepeatedState.Count() / _totalInstructions; + + if (repetitionRatio > 0.2) // More than 20% of instructions are the same state + { + detectionReasons.Add($"Potential infinite loop at IP: {topRepeatedState.Key.InstructionPointer}"); + + // Add loop information to metrics + result.Metrics["PotentialLoops"] = repeatedStates + .Take(3) + .Select(g => new + { + InstructionPointer = g.Key.InstructionPointer, + OpCode = g.Key.CurrentOpCode, + RepetitionCount = g.Count(), + RepetitionRatio = (double)g.Count() / _totalInstructions + }) + .ToList(); + + result.Recommendations.Add("Check for potential infinite loops and add proper termination conditions"); + + return Math.Min(0.6, repetitionRatio); + } + } + + return 0.0; + } + } +} diff --git a/fuzzers/Neo.VM.Fuzzer/Utils/FuzzingResults.cs b/fuzzers/Neo.VM.Fuzzer/Utils/FuzzingResults.cs new file mode 100644 index 0000000000..cf648af0bc --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/Utils/FuzzingResults.cs @@ -0,0 +1,362 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// FuzzingResults.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; + +namespace Neo.VM.Fuzzer.Utils +{ + /// + /// Tracks and analyzes the results of fuzzing runs + /// + public class FuzzingResults + { + private readonly string _outputDirectory; + private readonly ConcurrentDictionary _exceptionCounts = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _opcodeFrequency = new ConcurrentDictionary(); + private readonly List _executionTimes = new List(); + private readonly object _lockObject = new object(); + + private int _totalExecutions; + private int _crashCount; + private int _timeoutCount; + private int _newCoverageCount; + private int _dosVectorCount; + private double _totalExecutionTime; + private double _maxExecutionTime; + private double _minExecutionTime = double.MaxValue; + private readonly ConcurrentDictionary _dosReasonCounts = new ConcurrentDictionary(); + private readonly List _dosScores = new List(); + + /// + /// Gets the total number of script executions + /// + public int TotalExecutions => _totalExecutions; + + /// + /// Gets the number of crashes detected + /// + public int CrashCount => _crashCount; + + /// + /// Gets the number of timeouts detected + /// + public int TimeoutCount => _timeoutCount; + + /// + /// Gets the number of scripts that found new coverage + /// + public int NewCoverageCount => _newCoverageCount; + + /// + /// Gets the number of potential DOS vectors detected + /// + public int DOSVectorCount => _dosVectorCount; + + /// + /// Gets the average execution time in milliseconds + /// + public double AverageExecutionTimeMs => _totalExecutions > 0 ? _totalExecutionTime / _totalExecutions : 0; + + /// + /// Gets the maximum execution time in milliseconds + /// + public double MaxExecutionTimeMs => _maxExecutionTime; + + /// + /// Gets the minimum execution time in milliseconds + /// + public double MinExecutionTimeMs => _minExecutionTime == double.MaxValue ? 0 : _minExecutionTime; + + /// + /// Gets the average DOS score + /// + public double AverageDOSScore => _dosScores.Count > 0 ? _dosScores.Average() : 0; + + /// + /// Initializes a new instance of the FuzzingResults class + /// + /// Directory to save results to + public FuzzingResults(string outputDirectory) + { + _outputDirectory = outputDirectory ?? throw new ArgumentNullException(nameof(outputDirectory)); + + // Ensure output directory exists + Directory.CreateDirectory(_outputDirectory); + } + + /// + /// Records the result of a fuzzing run + /// + /// Execution time in milliseconds + /// Whether the execution crashed + /// Whether the execution timed out + /// Whether the execution found new coverage + /// Type of exception if crashed + /// List of opcodes executed during the run + /// DOS analysis result if available + public void RecordResult( + double executionTimeMs, + bool crashed, + bool timedOut, + bool foundNewCoverage, + string? exceptionType = null, + IEnumerable? executedOpcodes = null, + DOSDetector.DOSAnalysisResult? dosAnalysis = null) + { + lock (_lockObject) + { + _totalExecutions++; + _totalExecutionTime += executionTimeMs; + + if (executionTimeMs > _maxExecutionTime) + { + _maxExecutionTime = executionTimeMs; + } + + if (executionTimeMs < _minExecutionTime) + { + _minExecutionTime = executionTimeMs; + } + + _executionTimes.Add(executionTimeMs); + + if (crashed) + { + _crashCount++; + + // Record exception type + if (!string.IsNullOrEmpty(exceptionType)) + { + _exceptionCounts.AddOrUpdate( + exceptionType, + 1, + (_, count) => count + 1); + } + } + + if (timedOut) + { + _timeoutCount++; + } + + if (foundNewCoverage) + { + _newCoverageCount++; + } + + // Record DOS vector information + if (dosAnalysis != null && dosAnalysis.IsPotentialDOSVector) + { + _dosVectorCount++; + _dosScores.Add(dosAnalysis.DOSScore); + + // Record DOS reason + if (!string.IsNullOrEmpty(dosAnalysis.DetectionReason)) + { + _dosReasonCounts.AddOrUpdate( + dosAnalysis.DetectionReason, + 1, + (_, count) => count + 1); + } + } + + // Record opcode frequencies + if (executedOpcodes != null) + { + foreach (var opcode in executedOpcodes) + { + _opcodeFrequency.AddOrUpdate( + opcode, + 1, + (_, count) => count + 1); + } + } + } + } + + /// + /// Saves the fuzzing results to a file + /// + /// Name of the file to save to + public void SaveResults(string filename) + { + string filePath = Path.Combine(_outputDirectory, filename); + + using (var writer = new StreamWriter(filePath, false, Encoding.UTF8)) + { + writer.WriteLine("=== Neo VM Fuzzer Results ==="); + writer.WriteLine(); + + writer.WriteLine("General Statistics:"); + writer.WriteLine($"Total Executions: {_totalExecutions}"); + writer.WriteLine($"Crashes: {_crashCount} ({PercentOf(_crashCount, _totalExecutions):F2}%)"); + writer.WriteLine($"Timeouts: {_timeoutCount} ({PercentOf(_timeoutCount, _totalExecutions):F2}%)"); + writer.WriteLine($"New Coverage: {_newCoverageCount} ({PercentOf(_newCoverageCount, _totalExecutions):F2}%)"); + writer.WriteLine($"DOS Vectors: {_dosVectorCount} ({PercentOf(_dosVectorCount, _totalExecutions):F2}%)"); + writer.WriteLine(); + + writer.WriteLine("Execution Time Statistics:"); + writer.WriteLine($"Average Execution Time: {AverageExecutionTimeMs:F2} ms"); + writer.WriteLine($"Maximum Execution Time: {MaxExecutionTimeMs:F2} ms"); + writer.WriteLine($"Minimum Execution Time: {MinExecutionTimeMs:F2} ms"); + + if (_executionTimes.Count > 0) + { + var sortedTimes = _executionTimes.OrderBy(t => t).ToList(); + double median = sortedTimes.Count % 2 == 0 + ? (sortedTimes[sortedTimes.Count / 2 - 1] + sortedTimes[sortedTimes.Count / 2]) / 2 + : sortedTimes[sortedTimes.Count / 2]; + + writer.WriteLine($"Median Execution Time: {median:F2} ms"); + + // Calculate percentiles + int p90Index = (int)Math.Ceiling(sortedTimes.Count * 0.9) - 1; + int p95Index = (int)Math.Ceiling(sortedTimes.Count * 0.95) - 1; + int p99Index = (int)Math.Ceiling(sortedTimes.Count * 0.99) - 1; + + writer.WriteLine($"90th Percentile: {sortedTimes[p90Index]:F2} ms"); + writer.WriteLine($"95th Percentile: {sortedTimes[p95Index]:F2} ms"); + writer.WriteLine($"99th Percentile: {sortedTimes[p99Index]:F2} ms"); + } + + writer.WriteLine(); + + // Exception statistics + if (_exceptionCounts.Count > 0) + { + writer.WriteLine("Exception Statistics:"); + foreach (var exception in _exceptionCounts.OrderByDescending(e => e.Value)) + { + writer.WriteLine($"{exception.Key}: {exception.Value} ({PercentOf(exception.Value, _crashCount):F2}%)"); + } + writer.WriteLine(); + } + + // DOS vector statistics + if (_dosReasonCounts.Count > 0) + { + writer.WriteLine("DOS Vector Statistics:"); + writer.WriteLine($"Total DOS Vectors: {_dosVectorCount}"); + writer.WriteLine($"Average DOS Score: {AverageDOSScore:F2}"); + + if (_dosScores.Count > 0) + { + var sortedScores = _dosScores.OrderBy(s => s).ToList(); + double median = sortedScores.Count % 2 == 0 + ? (sortedScores[sortedScores.Count / 2 - 1] + sortedScores[sortedScores.Count / 2]) / 2 + : sortedScores[sortedScores.Count / 2]; + + writer.WriteLine($"Median DOS Score: {median:F2}"); + writer.WriteLine($"Maximum DOS Score: {sortedScores.Max():F2}"); + writer.WriteLine($"Minimum DOS Score: {sortedScores.Min():F2}"); + } + + writer.WriteLine(); + writer.WriteLine("DOS Reasons:"); + foreach (var kvp in _dosReasonCounts.OrderByDescending(x => x.Value)) + { + writer.WriteLine($"{kvp.Key}: {kvp.Value} ({PercentOf(kvp.Value, _dosVectorCount):F2}%)"); + } + } + + // Opcode frequency + if (_opcodeFrequency.Count > 0) + { + writer.WriteLine("Opcode Frequency (Top 20):"); + foreach (var opcode in _opcodeFrequency.OrderByDescending(o => o.Value).Take(20)) + { + writer.WriteLine($"{opcode.Key}: {opcode.Value} ({PercentOf(opcode.Value, _totalExecutions):F2}%)"); + } + writer.WriteLine(); + } + + writer.WriteLine("=== End of Report ==="); + } + } + + /// + /// Calculates the percentage of a value relative to a total + /// + private static double PercentOf(int value, int total) + { + return total > 0 ? (double)value / total * 100 : 0; + } + + /// + /// Generates a histogram of execution times + /// + /// Number of buckets in the histogram + /// A dictionary mapping time ranges to counts + public Dictionary GenerateExecutionTimeHistogram(int bucketCount = 10) + { + var histogram = new Dictionary(); + + if (_executionTimes.Count == 0) + { + return histogram; + } + + double min = _executionTimes.Min(); + double max = _executionTimes.Max(); + double bucketSize = (max - min) / bucketCount; + + // Initialize buckets + for (int i = 0; i < bucketCount; i++) + { + double bucketStart = min + i * bucketSize; + double bucketEnd = bucketStart + bucketSize; + string bucketLabel = $"{bucketStart:F2}-{bucketEnd:F2}"; + histogram[bucketLabel] = 0; + } + + // Fill buckets + foreach (var time in _executionTimes) + { + int bucketIndex = Math.Min(bucketCount - 1, (int)((time - min) / bucketSize)); + double bucketStart = min + bucketIndex * bucketSize; + double bucketEnd = bucketStart + bucketSize; + string bucketLabel = $"{bucketStart:F2}-{bucketEnd:F2}"; + histogram[bucketLabel]++; + } + + return histogram; + } + + /// + /// Saves a detailed histogram of execution times to a file + /// + /// Name of the file to save to + /// Number of buckets in the histogram + public void SaveExecutionTimeHistogram(string filename, int bucketCount = 20) + { + string filePath = Path.Combine(_outputDirectory, filename); + var histogram = GenerateExecutionTimeHistogram(bucketCount); + + using (var writer = new StreamWriter(filePath, false, Encoding.UTF8)) + { + writer.WriteLine("=== Execution Time Histogram ==="); + writer.WriteLine("Time Range (ms),Count,Percentage"); + + foreach (var bucket in histogram.OrderBy(b => double.Parse(b.Key.Split('-')[0]))) + { + double percentage = PercentOf(bucket.Value, _totalExecutions); + writer.WriteLine($"{bucket.Key},{bucket.Value},{percentage:F2}%"); + } + } + } + } +} diff --git a/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/aggressive_dos.neo b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/aggressive_dos.neo new file mode 100644 index 0000000000..7cdfd2c774 --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/aggressive_dos.neo @@ -0,0 +1,67 @@ +// Aggressive DOS vector test script +// This script is designed to trigger DOS detection by performing +// computationally expensive operations with high instruction count +// and excessive stack usage + +// Initialize loop counter with a high value +0x02 0x58 0x02 // PUSHINT16 with value 600 (0x0258) + +// Main loop start +// Label: LOOP_START +0x4A // DUP - duplicate counter + +// Check if counter is 0 +0x10 // PUSH0 +0xA0 // NUMEQUAL + +// If counter is 0, jump to end +0x24 // JMPIF +0x3A // Jump to END (offset from current position) + +// Create a large stack (potential stack overflow) +0x11 // PUSH1 +0x12 // PUSH2 +0x13 // PUSH3 +0x14 // PUSH4 +0x15 // PUSH5 +0x16 // PUSH6 +0x17 // PUSH7 +0x18 // PUSH8 +0x19 // PUSH9 +0x1A // PUSH10 + +// Perform expensive operations on the stack +// Repeated multiplication, division, and modulo operations +0xA0 // MUL (1*2=2) +0xA0 // MUL (2*3=6) +0xA0 // MUL (6*4=24) +0xA0 // MUL (24*5=120) +0xA0 // MUL (120*6=720) +0xA0 // MUL (720*7=5040) +0xA0 // MUL (5040*8=40320) +0xA0 // MUL (40320*9=362880) +0xA0 // MUL (362880*10=3628800) + +// Perform expensive power operations +0x11 // PUSH1 +0x12 // PUSH2 +0xA3 // POW (1^2) +0x13 // PUSH3 +0xA3 // POW (1^3) +0x14 // PUSH4 +0xA3 // POW (1^4) + +// Clear the stack +0x49 // CLEAR + +// Decrement counter +0x4A // DUP - duplicate counter +0x9D // DEC - decrement by 1 + +// Jump back to loop start +0x22 // JMP +0xC7 // Jump back to LOOP_START (negative offset) + +// Label: END +0x11 // PUSH1 (success value) +0x40 // RET diff --git a/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/basic_loop.neo b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/basic_loop.neo new file mode 100644 index 0000000000..fb1858755b --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/basic_loop.neo @@ -0,0 +1,28 @@ +// Basic Loop Test Script +// This script creates a simple loop that should execute without exceptions + +// Push initial value onto stack (loop counter) +0x01 0x88 0x13 // PUSHINT16 with value 5000 (0x1388) + +// Start of loop +// Label: LOOP_START +0x4A // DUP - duplicate counter + +// Check if counter is 0 +0x10 // PUSH0 +0xA0 // NUMEQUAL + +// If counter is 0, jump to end +0x24 // JMPIF +0x07 // Jump to END (offset from current position) + +// Decrement counter +0x9D // DEC - decrement by 1 + +// Jump back to loop start +0x22 // JMP +0xF7 // Jump back to LOOP_START (negative offset) + +// Label: END +0x11 // PUSH1 (success value) +0x40 // RET diff --git a/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/combined_dos_vector.neo b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/combined_dos_vector.neo new file mode 100644 index 0000000000..e91ef2e9eb --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/combined_dos_vector.neo @@ -0,0 +1,82 @@ +// Combined DOS Vector Test Script +// This script is designed to trigger multiple DOS detection mechanisms: +// 1. High instruction count (>5000) +// 2. Excessive stack depth (>50) +// 3. Potential infinite loop patterns +// 4. Long execution time (>500ms) + +// Push initial counter for main loop (10000 iterations) +0x01 0x10 0x27 // PUSHINT16 with value 10000 (0x2710) + +// Start of main loop +// Label: MAIN_LOOP +0x4A // DUP - duplicate counter + +// Check if counter is 0 +0x10 // PUSH0 +0xA0 // NUMEQUAL + +// If counter is 0, jump to end +0x24 // JMPIF +0x3A // Jump to END (offset from current position) + +// Push another counter for nested loop (100 iterations) +0x01 0x64 0x00 // PUSHINT16 with value 100 (0x0064) + +// Start of nested loop +// Label: NESTED_LOOP +0x4A // DUP - duplicate counter + +// Check if counter is 0 +0x10 // PUSH0 +0xA0 // NUMEQUAL + +// If counter is 0, jump to end of nested loop +0x24 // JMPIF +0x1A // Jump to NESTED_LOOP_END (offset from current position) + +// Build up stack to exceed stack depth threshold +0x11 // PUSH1 +0x11 // PUSH1 +0x11 // PUSH1 +0x11 // PUSH1 +0x11 // PUSH1 +0x11 // PUSH1 +0x11 // PUSH1 +0x11 // PUSH1 +0x11 // PUSH1 +0x11 // PUSH1 + +// Perform some expensive operations +0x01 0x0A 0x00 // PUSHINT16 with value 10 (0x000A) +0x02 // PUSHBYTES1 (1 byte) +0x0A // 10 in decimal +0xAA // MUL (multiply) +0xAA // MUL (multiply) +0xAA // MUL (multiply) + +// Clear most of the stack to avoid overflow +0x49 // CLEAR - clear the stack + +// Decrement nested loop counter +0x4A // DUP - duplicate counter +0x9D // DEC - decrement by 1 + +// Jump back to nested loop start +0x22 // JMP +0xE5 // Jump back to NESTED_LOOP (negative offset) + +// Label: NESTED_LOOP_END +0x49 // CLEAR - clear the stack + +// Decrement main loop counter +0x4A // DUP - duplicate counter +0x9D // DEC - decrement by 1 + +// Jump back to main loop start +0x22 // JMP +0xC5 // Jump back to MAIN_LOOP (negative offset) + +// Label: END +0x11 // PUSH1 (success value) +0x40 // RET diff --git a/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/comprehensive_dos_vector.neo b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/comprehensive_dos_vector.neo new file mode 100644 index 0000000000..48e08752a2 --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/comprehensive_dos_vector.neo @@ -0,0 +1,32 @@ +// Comprehensive DOS Vector Test Script +// This script is designed to trigger multiple DOS detection mechanisms: +// 1. High instruction count +// 2. Excessive stack depth +// 3. Long execution time + +// Push multiple values onto the stack to exceed our stack depth threshold +0x08 // PUSHT - push true +0x08 // PUSHT - push true +0x08 // PUSHT - push true +0x08 // PUSHT - push true +0x08 // PUSHT - push true +0x08 // PUSHT - push true +0x08 // PUSHT - push true +0x08 // PUSHT - push true +0x08 // PUSHT - push true +0x08 // PUSHT - push true + +// Perform some simple operations to increase instruction count +0x09 // PUSHF - push false +0xA0 // NUMEQUAL - compare values +0x09 // PUSHF - push false +0xA0 // NUMEQUAL - compare values +0x09 // PUSHF - push false +0xA0 // NUMEQUAL - compare values +0x09 // PUSHF - push false +0xA0 // NUMEQUAL - compare values +0x09 // PUSHF - push false +0xA0 // NUMEQUAL - compare values + +// Return successfully +0x40 // RET diff --git a/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/excessive_stack_depth.neo b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/excessive_stack_depth.neo new file mode 100644 index 0000000000..c4526d26c4 --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/excessive_stack_depth.neo @@ -0,0 +1,33 @@ +// Excessive stack depth DOS test script +// This script is designed to exceed the stack depth threshold of 100 +// by pushing many values onto the stack without popping them + +// Push loop counter (150 iterations to exceed stack depth of 100) +0x01 0x96 0x00 // PUSHINT16 with value 150 (0x0096) + +// Loop start +// Label: LOOP_START +0x4A // DUP - duplicate counter + +// Check if counter is 0 +0x10 // PUSH0 +0xA0 // NUMEQUAL + +// If counter is 0, jump to end +0x24 // JMPIF +0x0F // Jump to END (offset from current position) + +// Push a value onto the stack (without popping) +0x11 // PUSH1 + +// Decrement counter +0x4A // DUP - duplicate counter +0x9D // DEC - decrement by 1 + +// Jump back to loop start +0x22 // JMP +0xF2 // Jump back to LOOP_START (negative offset) + +// Label: END +0x11 // PUSH1 (success value) +0x40 // RET diff --git a/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/expensive_operations.neo b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/expensive_operations.neo new file mode 100644 index 0000000000..b6263f1050 --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/expensive_operations.neo @@ -0,0 +1,38 @@ +// Expensive operations script +// This script performs computationally expensive operations in a loop +// OpCodes: PUSH1, DUP, MUL, DIV, MOD, JMP + +// Initialize counter and value +0x15 // PUSH5 (loop counter = 50) +0x11 // PUSH1 (initial value) + +// Loop start (position 2) +0x4A // DUP (duplicate counter) +0x10 // PUSH0 +0xA0 // NUMEQUAL + +// If counter is 0, jump to end +0x24 // JMPIF +0x18 // Jump to position 24 (offset from current position) + +// Expensive operations +0x4A // DUP (duplicate value) +0x4A // DUP (duplicate value again) +0x4A // DUP (duplicate value again) +0xA0 // MUL (multiply) +0xA1 // DIV (divide) +0xA2 // MOD (modulo) +0xA0 // MUL (multiply again) +0xA1 // DIV (divide again) +0xA2 // MOD (modulo again) + +// Decrement counter +0x9D // DEC + +// Jump back to loop start +0x22 // JMP +0xE9 // Jump to position 2 (negative offset from current position) + +// End of script +0x11 // PUSH1 (success value) +0x40 // RET diff --git a/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/extreme_instruction_count.neo b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/extreme_instruction_count.neo new file mode 100644 index 0000000000..e651693e32 --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/extreme_instruction_count.neo @@ -0,0 +1,34 @@ +// Extreme instruction count DOS test script +// This script is designed to exceed the instruction count threshold +// by creating a very large loop with minimal operations + +// Push loop counter (50,000 iterations) +0x01 0x50 0xC3 // PUSHINT16 with value 50000 (0xC350) + +// Loop start +// Label: LOOP_START +0x4A // DUP - duplicate counter + +// Check if counter is 0 +0x10 // PUSH0 +0xA0 // NUMEQUAL + +// If counter is 0, jump to end +0x24 // JMPIF +0x0F // Jump to END (offset from current position) + +// Simple operation to increment instruction count +0x11 // PUSH1 +0x49 // CLEAR - clear the stack to avoid stack overflow + +// Decrement counter +0x4A // DUP - duplicate counter +0x9D // DEC - decrement by 1 + +// Jump back to loop start +0x22 // JMP +0xF1 // Jump back to LOOP_START (negative offset) + +// Label: END +0x11 // PUSH1 (success value) +0x40 // RET diff --git a/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/final_dos_test.neo b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/final_dos_test.neo new file mode 100644 index 0000000000..690ca867f0 --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/final_dos_test.neo @@ -0,0 +1,47 @@ +// Final Comprehensive DOS Test Script +// This script is designed to trigger multiple DOS detection mechanisms +// based on our analysis of successful DOS vectors from fuzzing + +// PART 1: Excessive Stack Depth +// Push multiple values onto the stack to exceed our stack depth threshold +// Most common DOS vector trigger was stack depth > 5 +0x08 // PUSHT - push true +0x08 // PUSHT - push true +0x08 // PUSHT - push true +0x08 // PUSHT - push true +0x08 // PUSHT - push true +0x08 // PUSHT - push true +0x08 // PUSHT - push true +0x08 // PUSHT - push true +0x08 // PUSHT - push true +0x08 // PUSHT - push true + +// PART 2: High Instruction Count +// Perform simple operations to increase instruction count +// We need >100 instructions to trigger this detection +0x09 // PUSHF - push false +0xA0 // NUMEQUAL - compare values +0x09 // PUSHF - push false +0xA0 // NUMEQUAL - compare values +0x09 // PUSHF - push false +0xA0 // NUMEQUAL - compare values +0x09 // PUSHF - push false +0xA0 // NUMEQUAL - compare values +0x09 // PUSHF - push false +0xA0 // NUMEQUAL - compare values + +// PART 3: Long Execution Time +// Add a delay by performing redundant operations +// We need >10ms execution time to trigger this detection +0x08 // PUSHT - push true +0x09 // PUSHF - push false +0xA0 // NUMEQUAL - compare values +0x08 // PUSHT - push true +0x09 // PUSHF - push false +0xA0 // NUMEQUAL - compare values +0x08 // PUSHT - push true +0x09 // PUSHF - push false +0xA0 // NUMEQUAL - compare values + +// Return successfully +0x40 // RET diff --git a/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/high_instruction_count.neo b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/high_instruction_count.neo new file mode 100644 index 0000000000..e5ef127564 --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/high_instruction_count.neo @@ -0,0 +1,33 @@ +// High instruction count DOS test script +// This script is designed to exceed the 10,000 instruction threshold +// by creating a large loop with many operations + +// Push loop counter (15,000 iterations) +0x01 0xB8 0x3A // PUSHINT16 with value 15000 (0x3AB8) + +// Loop start +// Label: LOOP_START +0x4A // DUP - duplicate counter + +// Check if counter is 0 +0x10 // PUSH0 +0xA0 // NUMEQUAL + +// If counter is 0, jump to end +0x24 // JMPIF +0x0F // Jump to END (offset from current position) + +// Simple operation to increment instruction count +0x11 // PUSH1 + +// Decrement counter +0x4A // DUP - duplicate counter +0x9D // DEC - decrement by 1 + +// Jump back to loop start +0x22 // JMP +0xF2 // Jump back to LOOP_START (negative offset) + +// Label: END +0x11 // PUSH1 (success value) +0x40 // RET diff --git a/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/infinite_loop.neo b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/infinite_loop.neo new file mode 100644 index 0000000000..1edf6196a6 --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/infinite_loop.neo @@ -0,0 +1,32 @@ +// Potential infinite loop script +// This script creates a loop that executes many times +// OpCodes: PUSH1, JMP, RET + +// Push 1000 onto the stack (loop counter) +0x01 0xE8 0x03 // PUSHINT16 (0x01) with value 1000 (0x03E8) + +// Loop start (at position 3) +0x4A // DUP - duplicate counter + +// Check if counter is 0 +0x10 // PUSH0 +0xA0 // NUMEQUAL + +// If counter is 0, jump to end +0x24 // JMPIF +0x15 // Jump to position 21 (offset from current position) + +// Decrement counter +0x4A // DUP +0x9D // DEC + +// Store counter back +0x4A // DUP (keep a copy) + +// Jump back to loop start +0x22 // JMP +0xEA // Jump to position 3 (negative offset from current position) + +// End of script +0x11 // PUSH1 (success value) +0x40 // RET diff --git a/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/infinite_loop_detector.neo b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/infinite_loop_detector.neo new file mode 100644 index 0000000000..7aa27c743f --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/infinite_loop_detector.neo @@ -0,0 +1,58 @@ +// Infinite loop detection test script +// This script is designed to trigger the infinite loop detection +// by repeating the same state more than 10 times with >30% repetition ratio + +// Initialize counter for outer loop (small to ensure high repetition ratio) +0x14 // PUSH4 (outer loop counter = 4) + +// Outer loop start +// Label: OUTER_LOOP +0x4A // DUP - duplicate outer counter + +// Check if outer counter is 0 +0x10 // PUSH0 +0xA0 // NUMEQUAL + +// If outer counter is 0, jump to end +0x24 // JMPIF +0x1A // Jump to END (offset from current position) + +// Initialize counter for inner loop (large to create many repetitions) +0x01 0x64 0x00 // PUSHINT16 with value 100 (0x0064) + +// Inner loop start +// Label: INNER_LOOP +0x4A // DUP - duplicate inner counter + +// Check if inner counter is 0 +0x10 // PUSH0 +0xA0 // NUMEQUAL + +// If inner counter is 0, jump to inner end +0x24 // JMPIF +0x0F // Jump to INNER_END (offset from current position) + +// Simple operation that will be repeated many times +0x11 // PUSH1 +0x49 // CLEAR - clear the stack + +// Decrement inner counter +0x4A // DUP - duplicate inner counter +0x9D // DEC - decrement by 1 + +// Jump back to inner loop start +0x22 // JMP +0xF2 // Jump back to INNER_LOOP (negative offset) + +// Label: INNER_END +// Decrement outer counter +0x4A // DUP - duplicate outer counter +0x9D // DEC - decrement by 1 + +// Jump back to outer loop start +0x22 // JMP +0xE7 // Jump back to OUTER_LOOP (negative offset) + +// Label: END +0x11 // PUSH1 (success value) +0x40 // RET diff --git a/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/minimal_dos_vector.neo b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/minimal_dos_vector.neo new file mode 100644 index 0000000000..78486efd1f --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/minimal_dos_vector.neo @@ -0,0 +1,23 @@ +// Minimal DOS Vector Test Script +// This script is designed to be as simple as possible while still triggering +// our updated DOS detection thresholds + +// Push a few values onto the stack to trigger stack depth threshold +0x08 // PUSHT - push true +0x08 // PUSHT - push true +0x08 // PUSHT - push true +0x08 // PUSHT - push true +0x08 // PUSHT - push true +0x08 // PUSHT - push true + +// Perform some simple operations to increase instruction count +0x09 // PUSHF - push false +0xA0 // NUMEQUAL - compare values +0x09 // PUSHF - push false +0xA0 // NUMEQUAL - compare values +0x09 // PUSHF - push false +0xA0 // NUMEQUAL - compare values + +// Return successfully +0x08 // PUSHT - push true (success value) +0x40 // RET diff --git a/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/recursive_operations.neo b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/recursive_operations.neo new file mode 100644 index 0000000000..150607b3b0 --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/recursive_operations.neo @@ -0,0 +1,29 @@ +// Recursive operations script +// This script creates a pattern that could lead to excessive stack usage +// OpCodes: PUSH1, DUP, ADD, CALL, RET + +// Initialize counter +0x11 // PUSH1 (initial value) + +// Start of recursive function (position 1) +0x4A // DUP (duplicate value on stack) +0x4A // DUP (duplicate again) + +// Check if counter is high enough +0x14 // PUSH4 (threshold) +0xB4 // GE (greater than or equal) + +// If counter is high enough, jump to end +0x24 // JMPIF +0x10 // Jump to position 16 (offset from current position) + +// Increment counter +0x11 // PUSH1 +0x9E // ADD + +// Call recursive function +0x34 // CALL +0xEF // Call position 1 (negative offset from current position) + +// End of recursive function +0x40 // RET diff --git a/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/robust_dos_test.neo b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/robust_dos_test.neo new file mode 100644 index 0000000000..f992a06646 --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/robust_dos_test.neo @@ -0,0 +1,29 @@ +// Robust DOS Test Script +// This script is designed to be robust against crashes while still triggering DOS detection +// It focuses on high instruction count and potential infinite loop detection + +// Push a small value onto the stack (to avoid stack overflow) +0x01 0x88 0x13 // PUSHINT16 with value 5000 (0x1388) + +// Start of main loop +// Label: MAIN_LOOP +0x4A // DUP - duplicate counter + +// Check if counter is 0 +0x10 // PUSH0 +0xA0 // NUMEQUAL + +// If counter is 0, jump to end +0x24 // JMPIF +0x06 // Jump to END (offset from current position) + +// Decrement counter +0x9D // DEC - decrement by 1 + +// Jump back to main loop start +0x22 // JMP +0xF8 // Jump back to MAIN_LOOP (negative offset) + +// Label: END +0x11 // PUSH1 (success value) +0x40 // RET diff --git a/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/simple_dos_vector.neo b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/simple_dos_vector.neo new file mode 100644 index 0000000000..bc7e1e798e --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/simple_dos_vector.neo @@ -0,0 +1,30 @@ +// Simple DOS Vector Test Script +// This script is designed to execute a large number of simple instructions +// to trigger the high instruction count DOS detection threshold (>5000) + +// Push loop counter (6000 iterations to exceed the 5000 threshold) +0x01 0x70 0x17 // PUSHINT16 with value 6000 (0x1770) + +// Loop start +// Label: LOOP_START +0x4A // DUP - duplicate counter + +// Check if counter is 0 +0x10 // PUSH0 +0xA0 // NUMEQUAL + +// If counter is 0, jump to end +0x24 // JMPIF +0x09 // Jump to END (offset from current position) + +// Decrement counter +0x4A // DUP - duplicate counter +0x9D // DEC - decrement by 1 + +// Jump back to loop start +0x22 // JMP +0xF5 // Jump back to LOOP_START (negative offset) + +// Label: END +0x11 // PUSH1 (success value) +0x40 // RET diff --git a/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/slow_opcodes.neo b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/slow_opcodes.neo new file mode 100644 index 0000000000..03621a5f54 --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/slow_opcodes.neo @@ -0,0 +1,47 @@ +// Slow opcodes DOS test script +// This script is designed to trigger slow opcodes detection +// by using computationally expensive operations repeatedly + +// Push loop counter (500 iterations) +0x01 0xF4 0x01 // PUSHINT16 with value 500 (0x01F4) + +// Loop start +// Label: LOOP_START +0x4A // DUP - duplicate counter + +// Check if counter is 0 +0x10 // PUSH0 +0xA0 // NUMEQUAL + +// If counter is 0, jump to end +0x24 // JMPIF +0x22 // Jump to END (offset from current position) + +// Expensive operations that are likely to be slow +// Push large numbers for calculations +0x01 0xFF 0xFF // PUSHINT16 with large value 0xFFFF +0x01 0xFF 0xFF // PUSHINT16 with large value 0xFFFF + +// Perform expensive power operation (a^b) +0xA3 // POW + +// More expensive operations +0x01 0xFF 0xFF // PUSHINT16 with large value 0xFFFF +0x01 0xFE 0xFF // PUSHINT16 with large value 0xFFFE +0xA0 // MUL (multiply large numbers) +0x01 0x01 0x00 // PUSHINT16 with value 0x0001 +0xA1 // DIV (division is typically slow) +0x01 0xFF 0x00 // PUSHINT16 with value 0x00FF +0xA2 // MOD (modulo is typically slow) + +// Decrement counter +0x4A // DUP - duplicate counter +0x9D // DEC - decrement by 1 + +// Jump back to loop start +0x22 // JMP +0xDF // Jump back to LOOP_START (negative offset) + +// Label: END +0x11 // PUSH1 (success value) +0x40 // RET diff --git a/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/stack_depth_dos.neo b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/stack_depth_dos.neo new file mode 100644 index 0000000000..d4e7b859ce --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/stack_depth_dos.neo @@ -0,0 +1,17 @@ +// Stack Depth DOS Vector Test Script +// This script is designed to trigger the stack depth DOS detection threshold + +// Push multiple values onto the stack to exceed our stack depth threshold +0x08 // PUSHT - push true +0x08 // PUSHT - push true +0x08 // PUSHT - push true +0x08 // PUSHT - push true +0x08 // PUSHT - push true +0x08 // PUSHT - push true +0x08 // PUSHT - push true +0x08 // PUSHT - push true +0x08 // PUSHT - push true +0x08 // PUSHT - push true + +// Return successfully +0x40 // RET diff --git a/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/valid_dos_vector.neo b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/valid_dos_vector.neo new file mode 100644 index 0000000000..3ed04d08bc --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/valid_dos_vector.neo @@ -0,0 +1,40 @@ +// Valid DOS Vector Test Script +// This script is designed to execute a large number of instructions +// without causing exceptions, to trigger the high instruction count threshold (>5000) + +// Push constants for our loop +0x03 // PUSHINT64 +0x88 0x13 0x00 0x00 0x00 0x00 0x00 0x00 // Value 5000 (0x1388) as 8 bytes + +// Start of loop +// Label: LOOP_START +0x4A // DUP - duplicate counter + +// Check if counter is 0 +0x03 // PUSHINT64 +0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 // Value 0 as 8 bytes +0xA0 // NUMEQUAL + +// If counter is 0, jump to end +0x24 // JMPIF +0x13 // Jump to END (offset from current position) + +// Simple operations that won't cause exceptions +0x08 // PUSHT - push true +0x09 // PUSHF - push false +0xA0 // NUMEQUAL - compare values +0x49 // CLEAR - clear the stack + +// Decrement counter +0x4A // DUP - duplicate counter +0x03 // PUSHINT64 +0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 // Value 1 as 8 bytes +0xA1 // SUB - subtract 1 + +// Jump back to loop start +0x22 // JMP +0xEB // Jump back to LOOP_START (negative offset) + +// Label: END +0x08 // PUSHT - push true (success value) +0x40 // RET diff --git a/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/valid_high_instruction.neo b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/valid_high_instruction.neo new file mode 100644 index 0000000000..950fc98015 --- /dev/null +++ b/fuzzers/Neo.VM.Fuzzer/dos-test-scripts/valid_high_instruction.neo @@ -0,0 +1,37 @@ +// Valid High Instruction Count DOS Test Script +// This script is designed to execute a large number of instructions +// without causing exceptions, to trigger DOS detection + +// Push loop counter (10000 iterations) +0x01 0x10 0x27 // PUSHINT16 with value 10000 (0x2710) + +// Loop start +// Label: LOOP_START +0x4A // DUP - duplicate counter + +// Check if counter is 0 +0x10 // PUSH0 +0xA0 // NUMEQUAL + +// If counter is 0, jump to end +0x24 // JMPIF +0x0F // Jump to END (offset from current position) + +// Simple operations that won't cause exceptions +0x11 // PUSH1 +0x11 // PUSH1 +0xAA // MUL - multiply +0x49 // CLEAR - clear the stack + +// Decrement counter +0x4A // DUP - duplicate counter +0x11 // PUSH1 +0xA1 // SUB - subtract 1 + +// Jump back to loop start +0x22 // JMP +0xF1 // Jump back to LOOP_START (negative offset) + +// Label: END +0x11 // PUSH1 (success value) +0x40 // RET diff --git a/neo.sln b/neo.sln index 54e97affc4..b98d0b45de 100644 --- a/neo.sln +++ b/neo.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.2.32516.85 MinimumVisualStudioVersion = 10.0.40219.1 @@ -88,6 +88,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Extensions.Benchmarks", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Json.Benchmarks", "benchmarks\Neo.Json.Benchmarks\Neo.Json.Benchmarks.csproj", "{5F984D2B-793F-4683-B53A-80050E6E0286}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "fuzzers", "fuzzers", "{07F23ABD-80E4-4465-A05C-02BE4391DC43}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.VM.Fuzzer", "fuzzers\Neo.VM.Fuzzer\Neo.VM.Fuzzer.csproj", "{D64D992A-12C8-48AF-96C6-3F04A6E401C5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -246,6 +250,10 @@ Global {72997EAB-9B0C-4BC8-B797-955C219C2C97}.Debug|Any CPU.Build.0 = Debug|Any CPU {72997EAB-9B0C-4BC8-B797-955C219C2C97}.Release|Any CPU.ActiveCfg = Release|Any CPU {72997EAB-9B0C-4BC8-B797-955C219C2C97}.Release|Any CPU.Build.0 = Release|Any CPU + {D64D992A-12C8-48AF-96C6-3F04A6E401C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D64D992A-12C8-48AF-96C6-3F04A6E401C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D64D992A-12C8-48AF-96C6-3F04A6E401C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D64D992A-12C8-48AF-96C6-3F04A6E401C5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -290,6 +298,7 @@ Global {B6CB2559-10F9-41AC-8D58-364BFEF9688B} = {C25EB0B0-0CAC-4CC1-8F36-F9229EFB99EC} {5F984D2B-793F-4683-B53A-80050E6E0286} = {C25EB0B0-0CAC-4CC1-8F36-F9229EFB99EC} {72997EAB-9B0C-4BC8-B797-955C219C2C97} = {7F257712-D033-47FF-B439-9D4320D06599} + {D64D992A-12C8-48AF-96C6-3F04A6E401C5} = {07F23ABD-80E4-4465-A05C-02BE4391DC43} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BCBA19D9-F868-4C6D-8061-A2B91E06E3EC}