/isotope

Created Mon, 20 Feb 2023 03:34:51 +0100
1366 Words

isotope

// isotope
// An advanced data mutation fuzzer
// © Jean Pereira <counterswarm.de>

const version = 0.6
const readme = `isotope v${version}
Usage: isotope [options]

Optional parameters:

  -d [data]        Input data
  -f [file]        Input file
  -r [directory]   Input sample
  -o [file]        Output file
  -n [count]       Sample count
  -c [count]       Max mutation count
  -m [module]      Module to load
  -s [seed]        Seed value
`

const arg = require('arg')

function randomChunks(num) {
  let result = [];
  let remaining = num;
  while (remaining > 0) {
    let chunk = Math.floor(Math.random() * remaining) + 1;
    result.push(chunk);
    remaining -= chunk;
  }
  return result;
}

let args

try {
  args = arg({
    '--help': Boolean,
    '-h': '--help',
    '--count': Number,
    '-c': '--count',
    '--data': String,
    '-d': '--data',
    '--file': String,
    '-f': '--file',
    '--output': String,
    '-o': '--output',
    '--num': Number,
    '-n': '--num',
    '--random': String,
    '-r': '--random',
    '--seed': Number,
    '-s': '--seed'
  })
} catch(e){
  displayHelp()
}

const required = []

checkRequiredArgs(args, required)

function a(name) {
  return args[`--${name}`]
}

function displayHelp() {
  console.log(readme)
  process.exit(1)
}

if(a('help')) {
  displayHelp()
}

function checkRequiredArgs(args, requiredArgs) {
  required.forEach(arg => {
    if (!args[arg]) {
      displayHelp()
    }
  })
}

const crypto = require('crypto')
const fs = require('fs')
const net = require('net')
const glob = require('glob')
const path = require('path')
const minimatch = require('minimatch')

let useSeed = false
let seedValue = 0

if(a('seed')) {
  useSeed = true
  seedValue = parseInt(a('seed'))
}

const runServer = false

const mutationMethods = [
  'fbi', 'fby', 'dby',
  'dbi', 'rvb', 'rpb',
  'trb', 'dub', 'rrb',
  'spb', 'oby', 'rby',
  'mat', 'cpb'
]

function byteArray(int) {
  let result = []
  while (int > 0) {
    let chunk = Math.floor(Math.random() * int) + 1
    result.push(chunk)
    int -= chunk
  }
  return result
}

function generateByteMatrix(buf) {
  if(Math.floor(Math.random() * 2) > 0) {
    return byteArray(buf.length)
  }
  else {
    return [1, 4, 8, 16, 24, 32, 64, 72, 144, 576]
  }
}

function fbi(buf) { // bitflip: reverse the bits of a random byte
  buf = Buffer.from(buf)
  randPos = Math.floor(Math.random() * buf.length)
  buf[randPos] = parseInt(buf[randPos].toString().split('').reverse().join(''))
  return buf
}

function fby(buf) {
  buf = Buffer.from(buf)
  const randPos = Math.floor(Math.random() * buf.length)
  const flipPos = Math.floor(Math.random() * buf.length)
  const bufferCopy = buf[randPos]
  buf[randPos] = buf[flipPos]
  buf[flipPos] = bufferCopy
  return buf
}

function rby(buf) {
  buf = Buffer.from(buf)
  const randPos = Math.floor(Math.random() * buf.length)
  buf[randPos] = crypto.randomBytes(1)[0]
  return buf
}

function mat(buf) {
  buf = Buffer.from(buf)
  const randPos = Math.floor(Math.random() * buf.length)
  const randPos2 = Math.floor(Math.random() * buf.length)

  const mathOps = [
    '%', '/', '-',
    '+', '|', '*'
  ]
  const mathVars = [
    'buf[randPos]',
    'buf[randPos+1]',
    'buf[randPos-1]',
    'buf[randPos2]',
    'buf[randPos]',
    'crypto.randomBytes(1)[0]',
    '0xFF',
    '0x90',
    '0x0A',
    '0x00'
  ]
  if(Math.floor(Math.random() * 200 ) > 0) {
    eval(`buf[randPos] = buf[randPos] ${mathOps[Math.floor(Math.random() * mathOps.length)]} ${mathVars[Math.floor(Math.random() * mathVars.length)]}`)
  } else {
    eval(`buf[randPos] = ${mathVars[Math.floor(Math.random() * mathVars.length)]}`)
  }
  return buf
}

function dby(buf) {
  const byteMatrix = generateByteMatrix(buf)
  const randPos = Math.floor(Math.random() * buf.length)
  const byteCount = byteMatrix[Math.floor(Math.random() * byteMatrix.length)]
  const maxPos = buf.length
  const writePos = Math.floor(Math.random() * maxPos)

  newBuf = Buffer.concat([
    buf.slice(0, writePos),
    fby(Buffer.from(buf.slice(writePos, writePos + byteCount))),
    buf.slice(writePos + byteCount, buf.length)
  ])

  buf = newBuf

  return buf
}

function dbi(buf) {
  const byteMatrix = generateByteMatrix(buf)
  const randPos = Math.floor(Math.random() * buf.length)
  const byteCount = byteMatrix[Math.floor(Math.random() * byteMatrix.length)]
  const maxPos = buf.length
  const writePos = Math.floor(Math.random() * maxPos)

  newBuf = Buffer.concat([
    buf.slice(0, writePos),
    fbi(Buffer.from(buf.slice(writePos, writePos + byteCount))),
    buf.slice(writePos + byteCount, buf.length)
  ])

  buf = newBuf

  return buf
}

function oby(buf) {
  const byteMatrix = generateByteMatrix(buf)
  const randPos = Math.floor(Math.random() * buf.length)
  const byteCount = byteMatrix[Math.floor(Math.random() * byteMatrix.length)]
  const maxPos = buf.length
  const writePos = Math.floor(Math.random() * maxPos)

  newBuf = Buffer.concat([
    buf.slice(0, writePos),
    crypto.randomBytes(byteCount),
    buf.slice(writePos + byteCount, buf.length)
  ])

  buf = newBuf

  return buf
}

function rvb(buf) {
  const byteMatrix = generateByteMatrix(buf)
  const randPos = Math.floor(Math.random() * buf.length)
  const byteCount = byteMatrix[Math.floor(Math.random() * byteMatrix.length)]
  const maxPos = buf.length
  const writePos = Math.floor(Math.random() * maxPos)

  newBuf = Buffer.concat([
    buf.slice(0, writePos),
    Buffer.from(buf.slice(writePos, writePos + byteCount).toString().split('').reverse().join('')),
    buf.slice(writePos + byteCount, buf.length)
  ])

  buf = newBuf

  return buf
}

function rpb(buf) {
  const byteMatrix = generateByteMatrix(buf)
  const randPos = Math.floor(Math.random() * buf.length)
  const byteCount = byteMatrix[Math.floor(Math.random() * byteMatrix.length)]
  const maxPos = buf.length - byteCount
  const writePos = Math.floor(Math.random() * maxPos)

  if ((writePos + byteCount) < maxPos) {
    newBuf = Buffer.concat([
      buf.slice(0, writePos),
      Buffer.from(String.fromCharCode(buf[writePos]).repeat(byteCount)),
      buf.slice(writePos + byteCount, buf.length)
    ])
    if(Buffer.compare(newBuf, buf)) {
      buf = newBuf
    } else {
      buf = rpb(buf)
    }
  } else {
    buf = rpb(buf)
  }
  return buf
}

function cpb(buf) {
  const byteMatrix = generateByteMatrix(buf)
  const randPos = Math.floor(Math.random() * buf.length)
  const byteCount = byteMatrix[Math.floor(Math.random() * byteMatrix.length)]
  const maxPos = buf.length
  const writePos = Math.floor(Math.random() * maxPos)

  if (writePos < maxPos) {
    newBuf = Buffer.concat([
      buf.slice(0, writePos),
      Buffer.from(buf.slice(writePos, writePos + byteCount).toString().repeat(Math.floor(Math.random() * 100))),
      buf.slice(writePos + byteCount, buf.length)
    ])
    if(Buffer.compare(newBuf, buf)) {
      buf = newBuf
    } else {
      buf = trb(buf)
    }
  } else {
    buf = trb(buf)
  }
  return buf
}

function trb(buf) {
  const byteMatrix = generateByteMatrix(buf)
  const randPos = Math.floor(Math.random() * buf.length)
  const byteCount = byteMatrix[Math.floor(Math.random() * byteMatrix.length)]
  const maxPos = buf.length
  const writePos = Math.floor(Math.random() * maxPos)

  if (writePos < maxPos) {
    newBuf = Buffer.concat([
      buf.slice(0, writePos),
      buf.slice(writePos + byteCount, buf.length)
    ])
    if(Buffer.compare(newBuf, buf)) {
      buf = newBuf
    } else {
      buf = trb(buf)
    }
  } else {
    buf = trb(buf)
  }
  return buf
}

function dub(buf) {
  const byteMatrix = generateByteMatrix(buf)
  const randPos = Math.floor(Math.random() * buf.length)
  const byteCount = byteMatrix[Math.floor(Math.random() * byteMatrix.length)]
  const maxPos = buf.length - byteCount
  const writePos = Math.floor(Math.random() * maxPos)

  if ((writePos + byteCount) < maxPos) {
    newBuf = Buffer.concat([
      buf.slice(0, writePos),
      Buffer.from(String.fromCharCode(buf[writePos]).repeat(byteCount)),
      buf.slice(writePos, buf.length)
    ])
    if(Buffer.compare(newBuf, buf)) {
      buf = newBuf
    } else {
      buf = dub(buf)
    }
  } else {
    buf = dub(buf)
  }
  return buf
}

function rrb(buf) {
  const byteMatrix = generateByteMatrix(buf)
  const randPos = Math.floor(Math.random() * buf.length)
  const byteCount = byteMatrix[Math.floor(Math.random() * byteMatrix.length)]
  const maxPos = buf.length - byteCount
  const writePos = Math.floor(Math.random() * maxPos)

  if ((writePos + byteCount) < maxPos) {
    newBuf = Buffer.concat([
      buf.slice(0, writePos),
      Buffer.from(String.fromCharCode(buf[writePos + byteCount]).repeat(byteCount)),
      buf.slice(writePos + byteCount, buf.length)
    ])
    if(Buffer.compare(newBuf, buf)) {
      buf = newBuf
    } else {
      buf = rrb(buf)
    }
  } else {
    buf = rrb(buf)
  }
  return buf
}

function spb(buf) {
  const byteMatrix = generateByteMatrix(buf)
  const randPos = Math.floor(Math.random() * buf.length)
  const byteCount = byteMatrix[Math.floor(Math.random() * byteMatrix.length)]
  const maxPos = buf.length - byteCount
  const writePos = Math.floor(Math.random() * maxPos)

  if ((writePos + byteCount) < maxPos) {
    newBuf = Buffer.concat([
      buf.slice(0, writePos),
      Buffer.from(String.fromCharCode(buf[writePos + byteCount]).repeat(byteCount)),
      buf.slice(writePos, buf.length)
    ])
    if(Buffer.compare(newBuf, buf)) {
      buf = newBuf
    } else {
      buf = spb(buf)
    }
  } else {
    buf = spb(buf)
  }
  return buf
}

function dirGlob(pattern) {
  const directory = path.dirname(pattern)
  const basename = path.basename(pattern)
  return fs.readdirSync(directory).filter(file => minimatch(file, basename)).map(f => `${directory}/${f}`)
}

function calculateHashValue(seed) {
  const hash = crypto.createHash('sha256')
  hash.update(seed.toString())
  const bytes = hash.digest()
  let random = 0
  for (let i = 0; i < bytes.length; i++) {
    random = random * 256 + bytes[i]
  }
  return random
}

function secureRandom(int, seed=false) {
  if(!useSeed) {
    return Math.floor(crypto.randomBytes(1)[0] / 256 * int)
  } else {
    seed = seedValue
  }
  random = calculateHashValue(seed)
  return Math.floor(random / (256 ** 32) * int)
}

function srandProb(arr) {
  arr = arr.map(x => Array(x[1]).fill(x[0])).flat();
  return arr[Math.floor(Math.random() * arr.length)];
}

function randomArray(arr, seed=false) {
  if(!seed) {
    const randomIndex = crypto.randomBytes(1).readUInt8(0) % arr.length
    return arr[randomIndex]
  }
  random = calculateHashValue(seed)
  const randomIndex = random % arr.length
  return arr[randomIndex]
}

function mutateBuffer(buf, recursive=false) {
  const processMutations = (secureRandom(maxMutations-1)+1)

  if(!recursive) {
    if(a('output')) {
      if(a('num') && a('output').match('%n')) { // add fallback method: filebla.txt-1, filebla.txt-2 ...
        for(let i = 0; i < parseInt(a('num')); i++) {
          let fileName = a('output').replace('%n', String(i))
          console.log(`Writing file: ${fileName}`)
          fs.writeFileSync(fileName, mutateBuffer(buf, true))
        }
      } else {
        fs.writeFileSync(a('output'), mutateBuffer(buf, true))
      }
    } else {
      return mutateBuffer(buf, true)
    }
  } else {
    for(let i=0; i < processMutations; i++) {
      if(useSeed) {
        mutationType = randomArray(mutationMethods, (seedValue + i))
      } else {
        mutationType = randomArray(mutationMethods, false)
      }
      eval(`buf = ${mutationType}(buf)`)
    }
  }

  return buf
}

let buf
let logData = false

if (a('file') || a('random')) {
  if(a('random')) {
    buf = fs.readFileSync(randomArray(dirGlob(a('random')), false))
  } else {
    buf = fs.readFileSync(a('file'))
  }
} else {
  if(a('data')) {
    buf = Buffer.from(a('data'), 'utf-8')
  } else {
    buf = Buffer.from(crypto.randomBytes(secureRandom(10000)+1), 'utf-8')
  }
  logData = true
}

let maxMutations = srandProb([
  [1,50],  [5,50],  [10,50], [15,50], [20,30],
  [25,30], [35,30], [45,30], [50,20], [100,10],
  [secureRandom(buf.length),1],
  [buf.length,1]])

if(a('count')) {
  maxMutations = parseInt(a('count'))
}

try {
  buf = mutateBuffer(buf)
} catch(e) {
}

logData ? console.log(buf.toString()) : false