@bcoders.gr/evm-disassembler
Version:
A comprehensive EVM bytecode disassembler and analyzer with support for multiple EVM versions
388 lines (337 loc) • 12.6 kB
JavaScript
/**
* Comprehensive bytecode analysis
* @module analyzer
*/
const { decodeBytecode, validateBytecode, extractPushValues, extractAddresses } = require('./decoder');
const { detectMetadata } = require('./metadata');
const { analyzeStackDepth, analyzeControlFlow, analyzeStackPatterns, calculateComplexity } = require('./stack-analyzer');
const { detectFunctions, analyzeFunctionBodies, detectContractPatterns, compareFunctionPatterns } = require('./function-detector');
const { DANGEROUS_OPCODES } = require('./constants');
/**
* Perform comprehensive analysis of EVM bytecode
* @param {string} bytecode - Hex string of bytecode
* @param {Object} options - Analysis options
* @returns {Object} Complete analysis results
*/
function analyzeBytecode(bytecode, options = {}) {
const {
evmVersion = 'latest',
includeMetadata = true,
stopAtMetadata = true,
performStackAnalysis = true,
performFunctionAnalysis = true,
performSecurityAnalysis = true
} = options;
// Validate bytecode
const validation = validateBytecode(bytecode);
if (!validation.valid) {
return {
error: validation.error,
valid: false
};
}
// Decode instructions
const instructions = decodeBytecode(bytecode, {
evmVersion,
includeMetadata,
stopAtMetadata
});
const analysis = {
valid: true,
bytecodeSize: validation.size,
instructionCount: instructions.length,
instructions
};
// Metadata analysis
if (includeMetadata) {
const metadata = detectMetadata(bytecode);
if (metadata) {
analysis.metadata = metadata;
}
}
// Stack analysis
if (performStackAnalysis) {
const stackAnalysis = analyzeStackDepth(instructions);
const controlFlow = analyzeControlFlow(instructions);
const patterns = analyzeStackPatterns(instructions);
const complexity = calculateComplexity(instructions, stackAnalysis, controlFlow);
analysis.stack = stackAnalysis;
analysis.controlFlow = controlFlow;
analysis.patterns = patterns;
analysis.complexity = complexity;
}
// Function analysis
if (performFunctionAnalysis) {
const functions = detectFunctions(instructions);
const functionBodies = analyzeFunctionBodies(instructions, functions.functions);
const contractPatterns = detectContractPatterns(functions.functions);
const patternComparisons = compareFunctionPatterns(functions.functions);
analysis.functions = {
...functions,
...functionBodies,
contractPatterns,
patternComparisons
};
}
// Security analysis
if (performSecurityAnalysis) {
analysis.security = performSecurityAnalysisInternal(instructions, analysis);
}
// Extract additional information
analysis.pushValues = extractPushValues(instructions);
analysis.addresses = extractAddresses(instructions);
return analysis;
}
/**
* Perform security-focused analysis
* @param {Array} instructions - Decoded instructions
* @param {Object} analysis - Current analysis results
* @returns {Object} Security analysis results
*/
function performSecurityAnalysisInternal(instructions, analysis) {
const security = {
dangerousOpcodes: [],
potentialVulnerabilities: [],
gasIntensive: [],
externalCalls: [],
selfdestructs: [],
delegatecalls: []
};
// Scan for dangerous opcodes
for (let i = 0; i < instructions.length; i++) {
const inst = instructions[i];
if (DANGEROUS_OPCODES.includes(inst.opcode)) {
security.dangerousOpcodes.push({
pc: inst.pc,
opcode: inst.opcode,
context: getInstructionContext(instructions, i)
});
}
// Specific dangerous patterns
if (inst.opcode === 'SELFDESTRUCT') {
security.selfdestructs.push({
pc: inst.pc,
reachable: inst.reachable !== false
});
if (inst.reachable !== false) {
security.potentialVulnerabilities.push({
type: 'SELFDESTRUCT',
severity: 'HIGH',
pc: inst.pc,
description: 'Contract contains reachable SELFDESTRUCT'
});
}
}
if (inst.opcode === 'DELEGATECALL') {
security.delegatecalls.push({
pc: inst.pc,
context: getInstructionContext(instructions, i)
});
// Check if target is dynamic
if (i > 0 && !instructions[i - 1].opcode.startsWith('PUSH')) {
security.potentialVulnerabilities.push({
type: 'DYNAMIC_DELEGATECALL',
severity: 'HIGH',
pc: inst.pc,
description: 'DELEGATECALL with dynamic target address'
});
}
}
// External calls
if (['CALL', 'CALLCODE', 'DELEGATECALL', 'STATICCALL'].includes(inst.opcode)) {
security.externalCalls.push({
pc: inst.pc,
type: inst.opcode
});
}
// Gas intensive operations
if (inst.opcode === 'EXP' || inst.opcode === 'SHA3') {
security.gasIntensive.push({
pc: inst.pc,
opcode: inst.opcode
});
}
}
// Check for reentrancy patterns
checkReentrancyPatterns(instructions, security);
// Check for integer overflow patterns
checkOverflowPatterns(instructions, security);
// Check for access control patterns
checkAccessControlPatterns(instructions, analysis, security);
// Calculate security score
security.score = calculateSecurityScore(security);
return security;
}
/**
* Get context around an instruction
* @param {Array} instructions - All instructions
* @param {number} index - Target instruction index
* @param {number} contextSize - Number of instructions before/after
* @returns {Object} Context information
*/
function getInstructionContext(instructions, index, contextSize = 3) {
const before = [];
const after = [];
// Get instructions before
for (let i = Math.max(0, index - contextSize); i < index; i++) {
before.push({
pc: instructions[i].pc,
opcode: instructions[i].opcode
});
}
// Get instructions after
for (let i = index + 1; i < Math.min(instructions.length, index + contextSize + 1); i++) {
after.push({
pc: instructions[i].pc,
opcode: instructions[i].opcode
});
}
return { before, after };
}
/**
* Check for reentrancy vulnerability patterns
* @param {Array} instructions - Decoded instructions
* @param {Object} security - Security analysis object
*/
function checkReentrancyPatterns(instructions, security) {
// Look for pattern: external call followed by state change
for (let i = 0; i < instructions.length - 5; i++) {
const inst = instructions[i];
if (['CALL', 'CALLCODE'].includes(inst.opcode)) {
// Look for SSTORE after the call
for (let j = i + 1; j < Math.min(i + 20, instructions.length); j++) {
if (instructions[j].opcode === 'SSTORE') {
security.potentialVulnerabilities.push({
type: 'POTENTIAL_REENTRANCY',
severity: 'MEDIUM',
pc: inst.pc,
description: 'External call followed by state change (potential reentrancy)'
});
break;
}
}
}
}
}
/**
* Check for integer overflow patterns
* @param {Array} instructions - Decoded instructions
* @param {Object} security - Security analysis object
*/
function checkOverflowPatterns(instructions, security) {
// Look for arithmetic operations without safety checks
for (let i = 0; i < instructions.length; i++) {
const inst = instructions[i];
if (['ADD', 'MUL'].includes(inst.opcode)) {
// Check if there's an overflow check nearby
let hasCheck = false;
// Look for LT/GT comparisons after arithmetic
for (let j = i + 1; j < Math.min(i + 5, instructions.length); j++) {
if (['LT', 'GT', 'SLT', 'SGT'].includes(instructions[j].opcode)) {
hasCheck = true;
break;
}
}
if (!hasCheck) {
security.potentialVulnerabilities.push({
type: 'UNCHECKED_ARITHMETIC',
severity: 'LOW',
pc: inst.pc,
description: `Unchecked ${inst.opcode} operation (potential overflow)`
});
}
}
}
}
/**
* Check for access control patterns
* @param {Array} instructions - Decoded instructions
* @param {Object} analysis - Analysis results
* @param {Object} security - Security analysis object
*/
function checkAccessControlPatterns(instructions, analysis, security) {
// Check if contract has owner-like patterns
const hasOwnerFunction = analysis.functions &&
analysis.functions.functions.some(f => f.signature === 'owner()');
// Look for CALLER checks before sensitive operations
let callerChecks = 0;
for (let i = 0; i < instructions.length; i++) {
if (instructions[i].opcode === 'CALLER') {
// Check if followed by EQ comparison
for (let j = i + 1; j < Math.min(i + 5, instructions.length); j++) {
if (instructions[j].opcode === 'EQ') {
callerChecks++;
break;
}
}
}
}
if (hasOwnerFunction && callerChecks === 0) {
security.potentialVulnerabilities.push({
type: 'MISSING_ACCESS_CONTROL',
severity: 'MEDIUM',
description: 'Contract has owner function but no caller checks detected'
});
}
}
/**
* Calculate overall security score
* @param {Object} security - Security analysis results
* @returns {number} Security score (0-100)
*/
function calculateSecurityScore(security) {
let score = 100;
// Deduct points for vulnerabilities
for (const vuln of security.potentialVulnerabilities) {
switch (vuln.severity) {
case 'HIGH':
score -= 20;
break;
case 'MEDIUM':
score -= 10;
break;
case 'LOW':
score -= 5;
break;
}
}
// Deduct for dangerous opcodes
score -= security.dangerousOpcodes.length * 2;
// Ensure score doesn't go below 0
return Math.max(0, score);
}
/**
* Generate analysis summary
* @param {Object} analysis - Complete analysis results
* @returns {Object} Summary object
*/
function generateSummary(analysis) {
const summary = {
bytecodeSize: analysis.bytecodeSize,
instructionCount: analysis.instructionCount,
hasMetadata: !!analysis.metadata,
compiler: analysis.metadata?.compiler || 'Unknown',
compilerVersion: analysis.metadata?.version || 'Unknown'
};
if (analysis.functions) {
summary.functionCount = analysis.functions.totalFunctions;
summary.knownFunctions = analysis.functions.knownFunctions;
summary.detectedStandards = analysis.functions.contractPatterns.detectedStandards;
}
if (analysis.complexity) {
summary.cyclomaticComplexity = analysis.complexity.cyclomaticComplexity;
summary.maxStackDepth = analysis.complexity.maxStackDepth;
}
if (analysis.security) {
summary.securityScore = analysis.security.score;
summary.vulnerabilityCount = analysis.security.potentialVulnerabilities.length;
summary.highSeverityIssues = analysis.security.potentialVulnerabilities
.filter(v => v.severity === 'HIGH').length;
}
return summary;
}
module.exports = {
analyzeBytecode,
performSecurityAnalysisInternal,
generateSummary
};