surku
Version:
A mutation-based general purpose fuzzer.
340 lines (320 loc) • 10.6 kB
JavaScript
const fs = require("fs");
const path = require("path");
const Surku = function (user_config) {
this.m = require("./mersenne-twister.js");
const mutators = require("./mutators.js");
this.mutators = mutators;
const self = this;
// eslint-disable-next-line no-control-regex
const stringCheckRegExp = /[\u0000-\u0005]+/;
// eslint-disable-next-line no-control-regex
const isNonNullRegExp = /[^\u0000-\u0005]+/;
const config = {
maxMutations: 20,
minMutations: 1,
chunkSize: 3000,
useOnly: undefined,
seed: undefined,
verbose: undefined,
};
this.config = config;
if (user_config !== undefined) {
for (const key in user_config) this.config[key] = user_config[key];
}
if (
this.config.useOnly !== undefined &&
this.config.useOnly instanceof Array
) {
mutators.useOnlyMutators(this.config.useOnly);
}
this.ra = function (array) {
if (Array.isArray(array)) return array[this.rint(array.length)];
console.error("ra: called with non-array");
console.error(array);
return false;
};
this.rint = function (max) {
if (max || max === 0) {
const rintOutput = Math.floor(this.r.genrand_real1() * max);
if (Number.isNaN(rintOutput))
console.error(
`rint: called with non-number ${max} results to ${rintOutput}`
);
return rintOutput;
}
console.error(`rint: called with ${max}`);
return false;
};
this.wrint = function (max) {
if (max === undefined) max = 2000;
const num = this.r.genrand_real1() * 3;
const div = this.r.genrand_real1() + Number.MIN_VALUE; // Avoid divide by zero.
return (Math.floor(num / div) ? Math.floor(num / div) : 1) % max;
};
const seedBase = this.m.newMersenneTwister(self.config.seed);
this.seedBase = seedBase;
this.generateTestCase = function (input) {
this.config = self.config;
this.storage = self.storage;
this.debugPrint = function (message, level) {
if (
self.config.hasOwnProperty("verbose") &&
self.config.verbose >= level
) {
process.stderr.write(message);
}
};
this.ra = self.ra;
this.rint = self.rint;
this.r = self.m.newMersenneTwister(seedBase.genrand_int31());
return mutate.call(this, input);
};
//
// Hold kind of linked list of stored data
//
/*
storage:{
where:[
[key1,key2],
[[key1value1,key1value2],[key2value2]]
]
}
*/
this.storage = {
lastChunks: [],
maxKeys: 100,
maxValues: 30,
maxChunks: 10,
valueStorages: {},
storeKeyValuePair(keyValueArray, where) {
// Check if valueStorage name was provided
const key = keyValueArray[0];
const value = keyValueArray[1];
let storageObject;
// if(process.memoryUsage().heapUsed<700*1024*1024){
if (where === undefined) where = "defaultStorage";
// If valueStorage named with value of where exists use it, else create empty one.
if (this.valueStorages.hasOwnProperty(where))
storageObject = this.valueStorages[where];
else {
this.valueStorages[where] = [[], []];
storageObject = this.valueStorages[where];
}
// Check that keyValueArray is an Array with length 2 so that storage will stay in sync.
if (keyValueArray instanceof Array && keyValueArray.length == 2) {
// Look from valueStorage[where] if key exists, if it does save into keys index on values array.
// Check that size of storage is not exceeded from unshift return value.
const index = storageObject[0].indexOf(key);
if (index !== -1) {
if (storageObject[1][index].unshift(value) > this.maxValues) {
storageObject[1][index].pop();
}
} else {
if (storageObject[0].unshift(key) > this.maxKeys) {
storageObject[0].pop();
storageObject[1].pop();
}
storageObject[1].unshift([value]);
}
} else {
console.log(
`Invalid input to storeKeyValue. Must be Array [key,value] got ${keyValueArray}`
);
}
/* }else{
console.log('Memory consumption high. Skipping saves to storage.')
} */
},
getValueForKey(key, where) {
let storageObject;
if (where === undefined) where = "defaultStorage";
if (this.valueStorages.hasOwnProperty(where))
storageObject = this.valueStorages[where];
else return false;
const index = storageObject[0].indexOf(key);
if (index !== -1) return self.ra(storageObject[1][index]);
return false;
},
pushNewChunk(chunk) {
if (this.lastChunks.unshift({ data: chunk }) > this.maxChunks) {
this.lastChunks.pop();
}
},
getChunk() {
return self.ra(this.lastChunks);
},
};
function splitToChunks(input) {
if (input.length > config.chunkSize) {
const inputLength = input.length;
let index = 0;
let count = 0;
const tmp = [];
for (; index < inputLength - config.chunkSize; count++)
tmp[count] = input.slice(
index,
(index += Math.floor(Math.random() * 1000) + 1)
);
tmp[count] = input.slice(index, inputLength);
return tmp;
}
return [input];
}
//
// mutate(input)
// input: Data to be mutated
//
// Selects amount of mutations to be done based on config.maxMutations and config.minMutations
// Takes data chunk size of config.chunkSize from random location inside the input data and applies mutators.
//
function mutate(input) {
let string = true;
if (input instanceof Buffer) {
string = false;
input = input.toString("binary");
} else if (!(input instanceof String || typeof input === "string")) {
console.log("Wrong input format. Must be String or Buffer!");
return input;
}
input = splitToChunks(input);
// console.log('Chunks: '+input.length)
if (input.length !== 0) {
let mutations;
if (this.config.maxMutations === this.config.minMutations)
mutations = this.config.maxMutations;
else if (this.config.maxMutations !== undefined)
mutations =
this.wrint(this.config.maxMutations - this.config.minMutations) -
1 +
this.config.minMutations;
else
mutations = this.config.minMutations
? this.config.minMutations + this.wrint(100)
: this.wrint(100);
let index;
let mutator;
let chunk;
let isString;
let result = false;
this.debugPrint(`Start: ${process.memoryUsage().heapUsed}\n`, 1);
while (mutations--) {
this.debugPrint(`Round: ${process.memoryUsage().heapUsed}\n`, 1);
index = 0;
chunk = "";
if (input.length > 1) {
let tries = 10;
while (tries--) {
index = this.rint(input.length);
if (isNonNullRegExp.test(input[index])) break;
}
let length = 0;
let chunks = 0;
while (length < config.chunkSize) {
length += input[index + chunks].length;
chunks++;
if (index + chunks >= input.length) {
break;
}
}
chunk = input.splice(index, chunks).join("");
} else {
chunk = input.splice(0, 1).join("");
}
isString = !stringCheckRegExp.test(chunk);
let tryCount = 10;
result = false;
if (chunk.length > 0) {
if (this.storage.lastChunks.length === 0 || !this.rint(5))
this.storage.pushNewChunk(chunk);
while (tryCount--) {
mutator = this.ra(mutators.mutators);
this.debugPrint(`Mutator: ${mutator.mutatorFunction.name} - `, 1);
if (
!mutator.stringOnly ||
(mutator.stringOnly && isString) ||
!this.rint(3)
) {
result = mutator.mutatorFunction.call(this, chunk, isString);
if (result === false) this.debugPrint("Fail\n", 1);
} else {
this.debugPrint("Fail\n", 1);
result = false;
}
if (result !== false) break;
}
} else if (input.length === 0 || input[0].length === 0) {
break;
}
if (result !== false) {
this.debugPrint("Success\n", 1);
Array.prototype.splice.apply(
input,
[index, 0].concat(splitToChunks(result))
);
}
}
this.debugPrint(`End: ${process.memoryUsage().heapUsed}\n`, 1);
input = input.join("");
if (string) return input;
return Buffer.from(input, "binary");
}
this.debugPrint("Mutate Error: Zero-sized input.");
return input;
}
return this;
};
//
// Commandline interface wrapper.
//
// TODO: Investigate if file could be read as a Stream and mutated with just a single chunk in memory.
if (require.main === module) {
const config = require("./cmd.js");
let samples;
const debugPrint = function (message, level) {
if (config.hasOwnProperty("verbose") && config.verbose >= level) {
process.stderr.write(message);
}
};
debugPrint("Initializing Surku with config:\n", 5);
if (config.verbose >= 5) console.log(config);
const S = new Surku(config);
if (config.inputPath) samples = fs.readdirSync(config.inputPath);
else if (config.inputFile) {
config.inputPath = "";
samples = config.inputFile;
}
const sampleSelectorRandom = S.m.newMersenneTwister(
S.seedBase.genrand_int31()
);
let output = {};
let fileName = "";
if (config.outputName !== undefined) {
fileName = config.outputName.split("%n");
}
for (let x = 0; x < config.count; x++) {
const random = sampleSelectorRandom.genrand_real1();
const index = Math.floor(random * samples.length);
let sample = samples[index];
if (config.inputPath) sample = path.join(config.inputPath, sample);
debugPrint(`Input file: ${sample}\n`, 5);
if (fs.statSync(sample).isDirectory()) {
x--;
samples.splice(index, 1);
if (samples.length === 0) {
console.log("Input folder doesn't contain any files");
process.exit(2);
}
} else {
output = S.generateTestCase(fs.readFileSync(sample));
if (fileName === "") console.log(output.toString());
else {
debugPrint(`Output file: ${fileName.join(x)}\n`);
debugPrint(`Output file size: ${output.length}\n`);
fs.writeFileSync(fileName.join(x), output);
}
}
}
} else {
module.exports = Surku;
}