package fuzzer import ( "encoding/binary" "math/rand" ) // mutateFunc — одна стратегия мутации: берёт данные, возвращает новую // (не модифицирует исходный срез — воркеры работают конкурентно с общим // корпусом, in-place мутация была бы гонкой данных). type mutateFunc func(rng *rand.Rand, data []byte) []byte var mutators = []mutateFunc{ mutateBitFlip, mutateByteFlip, mutateArith, mutateInteresting, mutateBlockDelete, mutateBlockDuplicate, } func mutateBitFlip(rng *rand.Rand, data []byte) []byte { if len(data) == 0 { return data } out := append([]byte(nil), data...) idx := rng.Intn(len(out)) bit := rng.Intn(8) out[idx] ^= 1 << bit return out } func mutateByteFlip(rng *rand.Rand, data []byte) []byte { if len(data) == 0 { return data } out := append([]byte(nil), data...) out[rng.Intn(len(out))] = byte(rng.Intn(256)) return out } // interestingBytes/16/32 — классический AFL-набор "граничных" значений: // нули, минус единица, границы знакового/беззнакового диапазона. Такие // значения статистически чаще ломают проверки длины/индексов, чем // случайный байт. var ( interestingBytes = []byte{0x00, 0x01, 0x7f, 0x80, 0xff} interesting16 = []int16{0, 1, -1, 0x7fff, -0x8000, 0xff, 0x100} interesting32 = []int32{0, 1, -1, 0x7fffffff, -0x80000000, 0xffff, 0x10000} ) func mutateInteresting(rng *rand.Rand, data []byte) []byte { if len(data) == 0 { return data } out := append([]byte(nil), data...) switch rng.Intn(3) { case 0: out[rng.Intn(len(out))] = interestingBytes[rng.Intn(len(interestingBytes))] case 1: if len(out) >= 2 { idx := rng.Intn(len(out) - 1) v := interesting16[rng.Intn(len(interesting16))] binary.LittleEndian.PutUint16(out[idx:], uint16(v)) } default: if len(out) >= 4 { idx := rng.Intn(len(out) - 3) v := interesting32[rng.Intn(len(interesting32))] binary.LittleEndian.PutUint32(out[idx:], uint32(v)) } } return out } // mutateArith — прибавляет/отнимает небольшое значение (1..35, тот же // диапазон, что ARITH_MAX в AFL) от случайного байта. Ловит off-by-one // и похожие арифметические баги, которые чистый bit-flip обычно не находит. func mutateArith(rng *rand.Rand, data []byte) []byte { if len(data) == 0 { return data } out := append([]byte(nil), data...) idx := rng.Intn(len(out)) delta := byte(rng.Intn(35) + 1) if rng.Intn(2) == 0 { out[idx] += delta } else { out[idx] -= delta } return out } func mutateBlockDelete(rng *rand.Rand, data []byte) []byte { if len(data) < 2 { return data } start := rng.Intn(len(data)) length := 1 + rng.Intn(len(data)-start) out := append([]byte(nil), data[:start]...) out = append(out, data[start+length:]...) return out } func mutateBlockDuplicate(rng *rand.Rand, data []byte) []byte { if len(data) == 0 { return data } start := rng.Intn(len(data)) length := 1 + rng.Intn(len(data)-start) block := data[start : start+length] insertAt := rng.Intn(len(data) + 1) out := append([]byte(nil), data[:insertAt]...) out = append(out, block...) out = append(out, data[insertAt:]...) return out } // splice скрещивает два входа в случайной точке — классическая техника // "склеить два интересных сэмпла и посмотреть, что получится". func splice(rng *rand.Rand, a, b []byte) []byte { if len(a) == 0 || len(b) == 0 { return append([]byte(nil), a...) } n := len(a) if len(b) < n { n = len(b) } cut := rng.Intn(n) out := append([]byte(nil), a[:cut]...) out = append(out, b[cut:]...) return out } // havoc — стадия из классического AFL, которая на практике находит // больше всего крашей: несколько случайных мутаций подряд за один проход, // иногда вперемешку со splice по другому сэмплу из корпуса. func havoc(rng *rand.Rand, data []byte, corpus [][]byte) []byte { out := append([]byte(nil), data...) rounds := 1 + rng.Intn(8) for i := 0; i < rounds; i++ { if len(corpus) > 1 && rng.Intn(4) == 0 { out = splice(rng, out, corpus[rng.Intn(len(corpus))]) continue } out = mutators[rng.Intn(len(mutators))](rng, out) } return out }