moon-boofuzz

    Deterministic protocol fuzzing core with native TCP/UDP execution and portable JSONL replay

    fuzzing
    protocol
    boofuzz
    testing
    Download zip
    Author
    Version
    0.1.0
    License
    GPL-2.0-only
    Last updated
    yesterday
    Downloads
    10

    #Tested API example

    以下示例由 moon test 执行。根包测试使用 @moon_boofuzz 别名;在自己的项目中,将包导入为所用别名即可。包已发布到 mooncakes,可在自己的项目中 moon add GuoXBQ-Q/moon-boofuzz 引入;直接运行本仓库示例仍不需要额外安装。

    #命名请求与惰性变异

    CompiledRequest 用于新协议模型。正常渲染和变异枚举分别调用 render() 与 cases();用 next() 逐个获取载荷,避免先建立完整用例数组。

    ///|
    test "named request quick start" {
    let request = @moon_boofuzz.CompiledRequest::compile(
    @moon_boofuzz.Block::new("packet", [
    Leaf("prefix", @moon_boofuzz.Field::simple(b"PING ", [])),
    Leaf("value", @moon_boofuzz.Field::simple(b"ok", [b"", b"\x00\xff"])),
    ]),
    )
    assert_eq(request.render(), b"PING ok")
    let cases = request.cases(limit=1)
    assert_eq(cases.next().map(case => case.payload), Some(b"PING "))
    assert_eq(cases.next(), None)
    assert_eq(cases.state(), Limited)
    let resumed = request.cases(start=cases.position())
    assert_eq(resumed.next().map(case => case.payload), Some(b"PING \x00\xff"))
    }

    字段路径包含请求名,例如 packet.value。会话图、网络执行及回调的使用分别见 docs/SESSION.md、docs/RUNNER.md 和 docs/MONITORS.md。

    #Simple and Group

    Field::simple(default, candidates) preserves every explicit candidate. Field::group(values, default_value=...) selects the first value by default, then removes only the first matching default from mutation candidates. Both snapshot input arrays; fuzzable=false yields zero cases. Indices are zero based and out-of-range access returns None. Use these fields as named Leaf nodes in CompiledRequest; the legacy flat Request remains available.

    ///|
    test "group field" {
    let field = @moon_boofuzz.Field::group([b"GET", b"POST", b"GET"])
    assert_eq(field.default_value(), b"GET")
    assert_eq(field.num_mutations(), 2)
    assert_eq(field.mutation(0), Some(b"POST"))
    assert_eq(field.mutation(1), Some(b"GET"))
    }

    ///|
    test "minimal byte request" {
    let request = @moon_boofuzz.Request::new([
    @moon_boofuzz.Static(b"PING "),
    @moon_boofuzz.Choice(b"ok", [b"", b"long"]),
    ])
    assert_eq(request.render(), b"PING ok")
    assert_eq(request.mutations(), [b"PING ", b"PING long"])
    }

    ModelError

    pub suberror ModelError {
    Invalid(String)
    Limit(String)
    } derive(
    Debug
    )

    Block

    pub struct Block {
    name : String
    children : Array[Node]
    condition : Condition?
    alignment : (Int, Bytes)?
    group : String?
    }

    Block::aligned

    fn Block::aligned(self : Block, modulus : Int, pattern? : Bytes) -> Block raise ModelError

    Upstream intentionally appends a full modulus even when already aligned.

    Block::new

    fn Block::new(name : String, children : Array[Node]) -> Block

    Block::when

    fn Block::when(self : Block, condition : Condition) -> Block

    Block::with_group

    fn Block::with_group(self : Block, target : String) -> Block

    Link the block to a Group field; its candidates multiply with the block's own mutation cases like upstream Block(group=...) (boofuzz/blocks/block.py mutations, 518c139). The target is a full field path.

    CaseSeq

    pub enum CaseSeq {
    Plain(Int)
    Sequence(Array[CaseSeq])
    Product(Int, CaseSeq)
    }

    Lazy case sequence tree. Upstream enumerates a block as its plain child mutations followed by group products replaying that same sequence once per group candidate (boofuzz/blocks/block.py mutations, 518c139).

    CaseStream

    pub struct CaseStream {
    request : CompiledRequest
    limit : Int
    skip : Int
    ordinal : Int
    emitted : Int
    state : EnumerationState
    stack : Array[StreamFrame]
    combinatorial : Bool
    units : Array[StreamUnit]
    max_depth : Int?
    depth : Int
    depth_emitted : Int
    frames : Array[CombFrame]
    vars : Map[String, Bytes]?
    }

    CaseStream::next

    fn CaseStream::next(self : CaseStream) -> MutationCase? raise ModelError

    CaseStream::position

    fn CaseStream::position(self : CaseStream) -> Int

    The next raw candidate position, including oversized skipped candidates.

    CaseStream::remaining_skip

    fn CaseStream::remaining_skip(self : CaseStream) -> Int

    Cases not yet consumed from the start offset; nonzero after a stream exhausts before reaching it.

    CaseStream::state

    CaseStream::stop

    fn CaseStream::stop(self : CaseStream) -> Unit

    ChecksumAlgorithm

    pub(all) enum ChecksumAlgorithm {
    Crc32
    Crc32c
    Adler32
    Md5
    Sha1
    } derive(Eq,
    Debug
    )

    Checksum algorithms supported by checksum nodes. MD5/SHA-1 render with upstream's 32-bit word swap when the node endianness is big (boofuzz/blocks/checksum.py:171-189, 518c139).

    ChecksumAlgorithm::equal

    ChecksumAlgorithm::not_equal

    fn ChecksumAlgorithm::not_equal(x : ChecksumAlgorithm, y : ChecksumAlgorithm) -> Bool

    ChecksumSpec

    pub struct ChecksumSpec {
    target : String
    endian : Endian
    mutations : Array[Bytes]
    algorithm : ChecksumAlgorithm
    }

    CombFrame

    pub struct CombFrame {
    unit_index : Int
    incoming : Map[Int, Bool]
    acc : Map[Int, Bool]
    parts : Array[(Int, Int, Bool)]
    }

    CompiledRequest

    pub struct CompiledRequest {
    name : String
    entries : Array[Entry]
    paths : Map[String, Int]
    max_bytes : Int
    seq : CaseSeq
    }

    CompiledRequest::cases

    fn CompiledRequest::cases(self : CompiledRequest, start? : Int, limit? : Int, vars? : Map[String, Bytes]?) -> CaseStream raise ModelError

    CompiledRequest::cases_with_variables

    fn CompiledRequest::cases_with_variables(self : CompiledRequest, vars : Map[String, Bytes]?, start? : Int, limit? : Int) -> CaseStream raise ModelError

    Enumerate combinatorial cases: depth 1..max_depth (indefinite when max_depth is absent, stopping at the first depth without a valid case), combining that many units of the base sequence, deduplicated exactly like upstream. Group products take part as whole units.

    CompiledRequest::combinatorial_cases

    fn CompiledRequest::combinatorial_cases(self : CompiledRequest, start? : Int, limit? : Int, max_depth? : Int) -> CaseStream raise ModelError

    Enumerate combinatorial cases: depth 1..max_depth (indefinite when max_depth is absent, stopping at the first depth without a valid case), combining that many units of the base sequence, deduplicated exactly like upstream. Group products take part as whole units.

    CompiledRequest::combinatorial_cases_with_variables

    fn CompiledRequest::combinatorial_cases_with_variables(self : CompiledRequest, vars : Map[String, Bytes]?, start? : Int, limit? : Int, max_depth? : Int) -> CaseStream raise ModelError

    CompiledRequest::compile

    fn CompiledRequest::compile(root : Block, max_bytes? : Int) -> CompiledRequest raise ModelError

    Validate and snapshot a named protocol tree. Limits count encoded wire bytes.

    CompiledRequest::count_at

    fn CompiledRequest::count_at(self : CompiledRequest, path : String) -> Int raise ModelError

    Number of sequential cases contributed by the entry at path (the entry's own candidate count; group product replays are excluded).

    CompiledRequest::from_request

    fn CompiledRequest::from_request(name : String, request : Request, max_bytes? : Int) -> CompiledRequest raise ModelError

    Legacy flat fields receive stable names field0, field1, ... under name.

    CompiledRequest::name

    fn CompiledRequest::name(self : CompiledRequest) -> String

    CompiledRequest::parent_path

    fn CompiledRequest::parent_path(self : CompiledRequest, path : String) -> String? raise ModelError

    CompiledRequest::paths

    fn CompiledRequest::paths(self : CompiledRequest) -> Array[String]

    CompiledRequest::raw_mutation_count

    fn CompiledRequest::raw_mutation_count(self : CompiledRequest) -> Int64

    Case positions of the sequential stream, including positions inside group product replays.

    CompiledRequest::raw_prefix_before

    fn CompiledRequest::raw_prefix_before(self : CompiledRequest, path : String) -> Int64 raise ModelError

    Raw sequential case count emitted before the entry at path renders its first plain case, walking the same tree the stream enumerates: unlike count_at this includes group product replays of preceding grouped blocks, so the result is a real stream position. Returns -1 when the entry emits no plain cases at all (disabled or non-mutating entry).

    CompiledRequest::render

    fn CompiledRequest::render(self : CompiledRequest) -> Bytes raise ModelError

    CompiledRequest::render_case

    fn CompiledRequest::render_case(self : CompiledRequest, parts : Array[(String, Int)], vars : Map[String, Bytes]?) -> Bytes raise ModelError

    Render a mutation case (parts from MutationCase::parts) with session variables, so edge-callback variable writes are visible at send time.

    CompiledRequest::render_path

    fn CompiledRequest::render_path(self : CompiledRequest, path : String) -> Bytes raise ModelError

    CompiledRequest::render_with

    fn CompiledRequest::render_with(self : CompiledRequest, vars : Map[String, Bytes]?) -> Bytes raise ModelError

    Render with session variables; inside a running case dynamic fields resolve strictly, matching upstream node.render(mutation_context).

    Condition

    pub(all) enum Condition {
    Equal(String, Bytes)
    NotEqual(String, Bytes)
    OneOf(String, Array[Bytes])
    NotOneOf(String, Array[Bytes])
    Greater(String, Bytes)
    GreaterEqual(String, Bytes)
    Less(String, Bytes)
    LessEqual(String, Bytes)
    }

    Block visibility conditions, following boofuzz/blocks/block.py _do_dependencies_allow_render at 518c13904fc32e7f2cc88c9dec934e509062953e. The ordering variants keep upstream's operand order: upstream evaluates dep_value OP dependent_value (the configured threshold on the left), so Greater renders when the current field value is LESS than the configured bytes, not greater. Preserved for definition parity.

    Endian

    pub(all) enum Endian {
    Little
    Big
    } derive(Eq,
    Debug
    )

    Endian::equal

    fn Endian::equal(Endian, Endian) -> Bool

    Endian::not_equal

    fn Endian::not_equal(x : Endian, y : Endian) -> Bool

    Endian::to_repr

    Entry

    type Entry

    EnumerationState

    pub(all) enum EnumerationState {
    Running
    Exhausted
    Limited
    Stopped
    } derive(Eq,
    Debug
    )

    EnumerationState::equal

    EnumerationState::not_equal

    fn EnumerationState::not_equal(x : EnumerationState, y : EnumerationState) -> Bool

    Field

    pub struct Field {
    value : Bytes
    count : Int
    candidate : (Int) -> Bytes
    candidate_length : (Int) -> Int64
    }

    An immutable field definition with indexed, on-demand mutation generation.

    Field::binary

    fn Field::binary(value : Bytes, size? : Int, max_len? : Int, padding? : Bytes, fuzzable? : Bool, fuzz_values? : Array[Bytes]) -> Field raise ModelError

    Binary mutation candidates are materialized individually on indexed access. Size/max_len adjust mutations only, matching upstream; default stays verbatim. Padding is restricted to exactly one byte.

    Field::default_value

    fn Field::default_value(self : Field) -> Bytes

    Field::delimiter

    fn Field::delimiter(value : String, fuzzable? : Bool, fuzz_values? : Array[Bytes]) -> Field

    Field::float

    fn Field::float(default_value? : Double, s_format? : String, f_min? : Double, f_max? : Double, max_mutations? : Int, seed? : Int64, encode_as_ieee_754? : Bool, endian? : Endian, fuzzable? : Bool, fuzz_values? : Array[Bytes]) -> Field raise ModelError

    Float candidates: the first mutation is the default value formatted with s_format, the rest draw random.uniform(f_min, f_max) from the seeded MT19937 stream; adjacent duplicates are dropped exactly like upstream, where the uniform draw is consumed before the dedup check so the remaining sequence matches upstream's generator. The candidate count is the number of formatted values actually produced, whereas upstream num_mutations keeps claiming max_mutations.

    Field::from_lines

    fn Field::from_lines(default_value? : Bytes, lines? : Array[Bytes], max_len? : Int, fuzzable? : Bool, fuzz_values? : Array[Bytes]) -> Field raise ModelError

    Cycle through bad values, one per provided line. Empty lines are dropped like upstream's filter(None, ...); max_len > 0 keeps only shorter lines. Upstream collapses the filtered library through a Python set ONLY when at least one line exceeds max_len (from_file.py:43-47), so the port applies dedup under the same condition — keeping first-occurrence order where the set would scramble it (documented deviation).

    Field::group

    fn Field::group(values : Array[Bytes], default_value? : Bytes, fuzzable? : Bool, fuzz_values? : Array[Bytes]) -> Field raise ModelError

    Group removes only the first occurrence of its default from candidates.

    Field::integer

    fn Field::integer(value : UInt64, width? : Int, endian? : Endian, fuzzable? : Bool, fuzz_values? : Array[Bytes]) -> Field raise ModelError

    Unsigned binary integers with upstream's sorted, deduplicated +/-10 boundaries. Width is in bits. ASCII, full_range and custom max_num are not supported.

    Field::mutation

    fn Field::mutation(self : Field, index : Int) -> Bytes?

    Field::num_mutations

    fn Field::num_mutations(self : Field) -> Int

    Field::random_data

    fn Field::random_data(default_value? : Bytes, min_length? : Int, max_length? : Int, max_mutations? : Int, step? : Int, fuzzable? : Bool, fuzz_values? : Array[Bytes]) -> Field raise ModelError

    Random data candidates from upstream's deterministic Random(0) stream. Step mode fixes each length at min_length + i*step without consuming the generator; otherwise each candidate draws one randint for its length and one randint(0, 255) per byte. Candidate identity is stable, so the upstream count quirk (mutations loop over get_num_mutations, which also counts fuzz_values) is intentionally not reproduced: the random sequence has exactly max_mutations entries and user fuzz_values are appended after.

    Field::simple

    fn Field::simple(value : Bytes, values : Array[Bytes], fuzzable? : Bool, fuzz_values? : Array[Bytes]) -> Field

    Field::text

    fn Field::text(value : String, fuzzable? : Bool, fuzz_values? : Array[Bytes]) -> Field

    UTF-8 bad-string library, variable repeats and deterministic long strings. Only dynamic size is supported. Enforce wire-byte limits at request rendering.

    MutationCase

    pub(all) struct MutationCase {
    id : String
    request_name : String
    field_path : String
    mutation_index : Int
    ordinal : Int
    payload : Bytes
    extra : Array[(String, Int)]
    } derive(Eq,
    Debug
    )

    MutationCase::equal

    MutationCase::not_equal

    fn MutationCase::not_equal(x : MutationCase, y : MutationCase) -> Bool

    MutationCase::parts

    fn MutationCase::parts(self : MutationCase) -> Array[(String, Int)]

    All mutated (path, mutation index) pairs of this case, outermost first.

    Node

    pub(all) enum Node {
    Leaf(String, Field)
    Nested(Block)
    Repeat(String, String, Int, Int, Int, String?)
    Sized(String, SizeSpec)
    Checksummed(String, ChecksumSpec)
    Mirrored(String, String)
    Dynamic(String, String, Field)
    }

    Named protocol items. Names are one path segment and cannot contain dots.

    Node::checksum

    fn Node::checksum(name : String, target : String, endian? : Endian, mutations? : Array[UInt], fuzzable? : Bool, algorithm? : ChecksumAlgorithm) -> Node raise ModelError

    Node::dynamic

    fn Node::dynamic(name : String, variable : String, field : Field) -> Node

    Bind a field's default value to a session variable. Inside a test case the variable must exist (upstream raises on a missing ProtocolSession variable); outside, the field's own value renders. Mutations derive from the fallback value, matching upstream's mutation seeding.

    Node::mirror

    fn Node::mirror(name : String, target : String) -> Node

    Node::repeat

    fn Node::repeat(name : String, target : String, min? : Int, max? : Int, step? : Int, variable? : String) -> Node

    Repeat appends copies of a preceding named block; normal output is empty.

    Node::size

    fn Node::size(name : String, target : String, length? : Int, endian? : Endian, offset? : Int, inclusive? : Bool, ascii? : Bool, mutations? : Array[UInt64], fuzzable? : Bool) -> Node raise ModelError

    Primitive

    pub(all) enum Primitive {
    Static(Bytes)
    Choice(Bytes, Array[Bytes])
    } derive(
    Debug
    )

    Explicit byte fields. Automatic boundary mutations are planned.

    Primitive::default_value

    fn Primitive::default_value(self : Primitive) -> Bytes

    Return the normal wire value.

    Primitive::mutations

    fn Primitive::mutations(self : Primitive) -> Array[Bytes]

    Explicit alternatives in caller order; duplicates are preserved.

    Request

    pub struct Request {
    fields : Array[Primitive]
    }

    A flat request. Nested blocks and computed fields are planned.

    Request::mutations

    fn Request::mutations(self : Request) -> Array[Bytes]

    Generate one-field-at-a-time cases in field and alternative order. Materializes all cases; intended for small explicit mutation sets.

    Request::new

    fn Request::new(fields : Array[Primitive]) -> Request

    Snapshot caller-owned field definitions.

    Request::render

    fn Request::render(self : Request) -> Bytes

    Render the normal request without text decoding.

    SessionGraph

    pub struct SessionGraph {
    requests : Array[CompiledRequest]
    edges : Array[Array[Int]]
    names : Map[String, Int]
    edge_callbacks : Map[(Int, Int), (StepContext) -> Bytes?]
    }

    SessionGraph::add

    fn SessionGraph::add(self : SessionGraph, request : CompiledRequest) -> Unit raise ModelError

    SessionGraph::connect

    fn SessionGraph::connect(self : SessionGraph, from : String, to : String, callback? : (StepContext) -> Bytes?) -> Unit raise ModelError

    SessionGraph::new

    SessionGraph::paths

    fn SessionGraph::paths(self : SessionGraph, targets? : Array[String], max_paths? : Int) -> Array[SessionPath] raise ModelError

    Roots follow node insertion order; outgoing edges follow connection order.

    SessionPath

    pub struct SessionPath {
    requests : Array[CompiledRequest]
    transitions : Array[(StepContext) -> Bytes??]
    }

    SessionPath::cases

    fn SessionPath::cases(self : SessionPath, start? : Int, limit? : Int, variables? : Map[String, Bytes]?) -> CaseStream raise ModelError

    Only the terminal request mutates; prefixes are rendered from their defaults.

    SessionPath::cases_with_variables

    fn SessionPath::cases_with_variables(self : SessionPath, vars : Map[String, Bytes]?, start? : Int, limit? : Int) -> CaseStream raise ModelError

    SessionPath::combinatorial_cases_with_variables

    fn SessionPath::combinatorial_cases_with_variables(self : SessionPath, vars : Map[String, Bytes]?, start? : Int, limit? : Int, max_depth? : Int) -> CaseStream raise ModelError

    SessionPath::names

    fn SessionPath::names(self : SessionPath) -> Array[String]

    SessionPath::prefix

    fn SessionPath::prefix(self : SessionPath) -> Array[Bytes] raise ModelError

    SessionPath::requests

    fn SessionPath::requests(self : SessionPath) -> Array[CompiledRequest]

    SessionPath::target

    SizeSpec

    pub struct SizeSpec {
    target : String
    length : Int
    endian : Endian
    offset : Int
    inclusive : Bool
    ascii : Bool
    mutations : Array[Bytes]
    }

    StepContext

    pub(all) struct StepContext {
    variables : Map[String, Bytes]
    received : Array[Bytes]
    request : String
    }

    Context handed to an edge callback: the mutable session-variable map for the running case, bytes received from earlier steps, and the destination request name. Mirrors upstream's ProtocolSession (protocol_session.py, 518c139); callbacks write variables and may return replacement data.

    StreamFrame

    pub struct StreamFrame {
    seq : CaseSeq
    child : Int
    pass : Int
    walking : Bool
    }

    One stack frame of the lazy case-sequence walker. Sequence frames track the next child, plain frames the next mutation index, product frames the current group pass.

    StreamUnit

    pub struct StreamUnit {
    parts : Array[(Int, Int, Bool)]
    child : Int
    }

    A pre-enumerated unit of the base case sequence for combinatorial enumeration. is_group marks parts contributed by a product's group, which upstream adds without consulting the skip set. child is the availability-checked leaf entry.

    TextCandidate

    type TextCandidate derive(Eq)

    TextCandidate::equal

    TextCandidate::not_equal

    fn TextCandidate::not_equal(x : TextCandidate, y : TextCandidate) -> Bool

    adler32

    fn adler32(bytes : Bytes) -> UInt

    Adler-32 (RFC 1950): two 16-bit sums modulo 65521, matching zlib.

    crc32

    fn crc32(bytes : Bytes) -> UInt

    Reflected IEEE CRC32, initial/final XOR 0xffffffff, polynomial 0xedb88320.

    crc32c

    fn crc32c(bytes : Bytes) -> UInt

    CRC-32C (Castagnoli, poly 0x82F63B78 reflected), initial/final XOR 0xffffffff — the algorithm behind upstream's optional crc32c dependency.

    ipv4_checksum

    fn ipv4_checksum(header : Bytes) -> UInt

    IPv4 header checksum over the header bytes with the checksum field zero; the result is already complemented (0xb861 for the RFC 1071 example).

    md5

    fn md5(message : Bytes) -> Bytes

    MD5 digest (16 bytes) of the input.

    sha1

    fn sha1(message : Bytes) -> Bytes

    SHA-1 digest (20 bytes) of the input.

    udp_checksum

    fn udp_checksum(source : Bytes, destination : Bytes, udp_bytes : Bytes) -> UInt

    UDP checksum over the pseudo-header plus the UDP header and payload. source/destination are the 4-byte IPv4 addresses of the pseudo header; length is the UDP length field (header + payload). The complemented sum is computed over the same layout as upstream helpers.udp_checksum; an all-zero result is transmitted as 0xffff.