Guía de implementación para libnexaparse — NexaCore Systems | Parser TLV de mensajes financieros (ISO 8583 / FIX)
LLVMFuzzerTestOneInput que LibAFL invoca con datos generados. La compilación usa el wrapper libafl_cxx para instrumentar automáticamente con SanCov + ASan.// NexaFuzzer — harness para libnexaparse TLV parser // Build: $CXX -DNO_MAIN -g -O2 harness.cc .libs/libnexaparse.a -o fuzz #include <stdint.h> #include <stddef.h> #include <string.h> #include "nexaparse/tlv_parser.h" #include "nexaparse/fix_decoder.h" // Tokens del formato TLV / ISO 8583 para dictionary fuzzing static const uint8_t NEXAPARSE_MAGIC[] = {0x69, 0x73, 0x6f, 0x38}; static const uint8_t TLV_HEADER[] = {0x30, 0x82, 0x00, 0x00}; extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { if (size < 4) return 0; // demasiado corto // === TEST 1: Parser TLV genérico === tlv_context_t ctx = {}; int rc = nexa_tlv_parse(&ctx, data, size); if (rc == NEXA_OK) { // Iterar campos parseados para ejercer más rutas tlv_field_t field; while (nexa_tlv_next(&ctx, &field) == NEXA_OK) { nexa_tlv_get_value(&field, NULL, 0); } } nexa_tlv_free(&ctx); // === TEST 2: Decoder FIX protocol === if (size > 8) { fix_message_t msg = {}; if (nexa_fix_decode(&msg, (const char*)data, size) == NEXA_OK) { nexa_fix_validate(&msg); nexa_fix_free(&msg); } } return 0; }
use libafl::prelude::*; use libafl_bolts::prelude::*; use libafl_targets::{ libfuzzer_test_one_input, std_edges_map_observer }; /// Mutador personalizado para mensajes TLV /// Conoce la estructura: [Tag:2][Len:2][Value:Len] pub struct TlvMutator; impl<S> Mutator<BytesInput, S> for TlvMutator where S: HasRand, { fn mutate( &mut self, state: &mut S, input: &mut BytesInput, ) -> Result<MutationResult, Error> { let buf = input.bytes_mut(); if buf.len() < 4 { return Ok(MutationResult::Skipped); } let r = state.rand_mut(); match r.below(5) { 0 => { // Overflow length field buf[2] = 0xFF; buf[3] = 0xFF; } 1 => { // Nested TLV injection let nested = vec![0x30, 0x82, 0x01, 0x00]; buf.splice(4..4, nested); } 2 => { // Zero-length value with non-zero tag buf[2] = 0x00; buf[3] = 0x00; } 3 => { // Mutate tag bytes (unknown tags) buf[0] ^= 0x80; } _ => { // Random byte flip in value let i = r.below(buf.len() as u64) as usize; buf[i] ^= r.below(256) as u8; } } Ok(MutationResult::Mutated) } }
#[no_mangle] pub extern "C" fn libafl_main() { let mut run_client = |state: Option<_>, mut mgr, _core_id| { // Observers let edges_obs = HitcountsMapObserver::new( unsafe { std_edges_map_observer("edges") } ).track_indices(); let time_obs = TimeObserver::new("time"); let bt_obs = BacktraceObserver::owned( "bt", HarnessType::InProcess ); // Feedback: cobertura + tiempo let mut feedback = feedback_or!( MaxMapFeedback::new(&edges_obs), TimeFeedback::new(&time_obs) ); // Objetivo: crash único por backtrace let mut objective = feedback_and!( feedback_or_fast!( CrashFeedback::new(), TimeoutFeedback::new() ), NewHashFeedback::new(&bt_obs) ); // Estado con corpus en disco let mut state = state.unwrap_or_else(|| { StdState::new( StdRand::new(), InMemoryOnDiskCorpus::new( "corpus/" ).unwrap(), OnDiskCorpus::new( "crashes/" ).unwrap(), &mut feedback, &mut objective, ).unwrap() }); // Mutadores: TLV custom + havoc let mutator = StdScheduledMutator::new( havoc_mutations() .merge(tokens_mutations()) .merge(tuple_list!(TlvMutator)) ); let mut harness = |input: &BytesInput| { let buf = input.target_bytes().as_slice(); libfuzzer_test_one_input(buf); ExitKind::Ok }; // Lanzar con 32 cores Launcher::builder() .run_client(&mut run_client) .cores(&Cores::from_cmdline("0-31").unwrap()) .build() .launch() .unwrap(); }; }
#!/usr/bin/env bash # Build NexaFuzzer — NexaCore libnexaparse set -euo pipefail # ── 1. Entorno ────────────────────────────── export LLVM_CONFIG=llvm-config-17 export CC=clang-17 export CXX=clang++-17 export RUSTFLAGS="-C linker=/usr/bin/clang-17" # ── 2. Compilar LibAFL wrapper ─────────────── echo "[*] Building LibAFL compiler wrapper..." cd nexa_fuzzer cargo build --release cd .. FUZZER_CC=./nexa_fuzzer/target/release/libafl_cc FUZZER_CXX=./nexa_fuzzer/target/release/libafl_cxx # ── 3. Build libnexaparse (estática) ───────── echo "[*] Building libnexaparse static lib..." cd libnexaparse-1.4.2 CC=$FUZZER_CC CXX=$FUZZER_CXX \ ./configure --enable-shared=no \ --disable-tests make -j$(nproc) cd .. # ── 4. Compile harness ─────────────────────── echo "[*] Compiling harness..." $FUZZER_CXX -DNO_MAIN -g -O2 \ -I libnexaparse-1.4.2/include \ harness/nexaparse_fuzz.cc \ libnexaparse-1.4.2/.libs/libnexaparse.a \ -o nexafuzz echo "[+] Build OK → ./nexafuzz"
#!/usr/bin/env bash # Campaña de 72h — 32 cores — NexaCore set -euo pipefail CORES="0-31" INPUT="seeds/" OUTPUT="corpus/" CRASHES="crashes/" DICT="dicts/tlv_iso8583.dict" TIMEOUT=2000 # ms mkdir -p $OUTPUT $CRASHES # Seeds: mensajes TLV válidos de prueba cp testdata/iso8583_sample_*.bin $INPUT/ echo "[*] Starting NexaFuzzer — 32 cores, 72h..." echo "[*] Coverage target: >85% edges" echo "[*] Dictionary: $DICT" echo ./nexafuzz \ --cores $CORES \ --input $INPUT \ --output $OUTPUT \ --crashes $CRASHES \ --timeout $TIMEOUT \ -x $DICT \ --tui 1 \ & FUZZER_PID=$! echo "[+] Fuzzer PID: $FUZZER_PID" # Monitorizar crashrate watch -n 60 "ls crashes/ | wc -l"
# Tokens ISO 8583 / TLV para guided fuzzing "\x30\x82" # TLV sequence header "\x02\x01" # INTEGER tag + len=1 "\x04\x00" # OCTET STRING empty "\xff\xff" # Max length overflow "\x69\x73\x6f\x38" # "iso8" magic "ISOAMT" "TXNREF" "CARDNO" "F001" # FIX field 1 "35=\x01" # FIX tag 35 separator
| ID | Severidad | Tipo | Función | Offset | CVE / Estado | T+ |
|---|---|---|---|---|---|---|
| NXC-001 | ● CRÍTICO | heap-overflow (write) | nexa_tlv_parse_field() | +0x3c | CVE-2025-NXC-001 | 0h 47min |
| NXC-002 | ● CRÍTICO | use-after-free | nexa_tlv_free() + iter | +0xa8 | CVE-2025-NXC-002 | 3h 12min |
| NXC-003 | ● CRÍTICO | heap-overflow (read) | nexa_fix_decode_tag() | +0x11f | CVE-2025-NXC-003 | 8h 55min |
| NXC-004 | ● CRÍTICO | stack overflow (recursivo) | parse_nested_tlv() | +0x6b | CVE-2025-NXC-004 | 14h 02min |
| NXC-005 | ● ALTO | use-after-free | nexa_tlv_next() | +0x58 | En análisis | 19h 30min |
| NXC-006 | ● ALTO | null deref | iso8583_field_lookup() | +0x22 | En análisis | 24h 18min |
| NXC-007 | ● ALTO | integer overflow → OOB | tlv_length_decode() | +0x0f | En análisis | 31h 44min |
| NXC-008 | ● MEDIO | stack overflow | fix_checksum_verify() | +0x197 | — | 36h 11min |
| NXC-009 | ● MEDIO | use-after-free | nexa_ctx_reset() | +0x84 | — | 41h 50min |
| NXC-010 | ● MEDIO | null deref | tlv_context_validate() | +0x4d | — | 58h 22min |
| NXC-011 | ● MEDIO | assertion failure | fix_msg_field_count() | +0x3 | — | 67h 05min |
nexa_tlv_parse_field() el campo length del TLV no se valida contra el tamaño del buffer antes de la copia. Añadir guard: if (field.length > remaining) return NEXA_ERR_TRUNCATED; antes de cualquier acceso a datos.nexa_tlv_free() libera el buffer interno pero el iterador mantiene puntero. Solución: nullificar todos los punteros internos tras free, y añadir flag freed que el iterador compruebe antes de desreferenciar.nexa_fix_decode_tag() asume que el separador SOH (\x01) siempre existe antes del fin del buffer. Añadir bounds check en el bucle de búsqueda de separador con límite explícito.parse_nested_tlv() es recursiva sin límite de profundidad. Un mensaje con 1000 niveles de anidamiento agota el stack. Añadir parámetro depth y retornar error si depth > 32.crypto_utils.c (68%) y tls_wrapper.c (29%) es baja. Crear harnesses específicos con inputs que ejerzan las rutas de cifrado y el handshake TLS para descubrir vulnerabilidades en esas áreas.LLVMPasses::AutoTokens en el wrapper para extraer automáticamente constantes mágicas y comparaciones del binario. Mejora guiado de mutaciones sin esfuerzo manual.cargo-fuzz con backend LibAFL para integración con libfuzzer existente.