Skip to content
API Documentation
BastionGuard™
archive_worker.cpp File Reference

Go to the source code of this file.

Classes

struct  anonymous_namespace{archive_worker.cpp}::ZipEntryInfo

Namespaces

namespace  anonymous_namespace{archive_worker.cpp}

Functions

uint16_t anonymous_namespace{archive_worker.cpp}::read_u16_le (std::ifstream &f)
uint32_t anonymous_namespace{archive_worker.cpp}::read_u32_le (std::ifstream &f)
bool anonymous_namespace{archive_worker.cpp}::read_exact (std::ifstream &f, char *buf, std::size_t len)
std::string anonymous_namespace{archive_worker.cpp}::read_string (std::ifstream &f, std::size_t len)
bool anonymous_namespace{archive_worker.cpp}::contains_dotdot_path (const std::string &name)
bool anonymous_namespace{archive_worker.cpp}::is_absolute_or_weird_path (const std::string &name)
bool anonymous_namespace{archive_worker.cpp}::looks_like_nested_archive_name (const std::string &name)
void anonymous_namespace{archive_worker.cpp}::print_json_bool (const char *key, bool value, bool comma=true)
void anonymous_namespace{archive_worker.cpp}::print_json_int (const char *key, int value, bool comma=true)
void anonymous_namespace{archive_worker.cpp}::print_json_str (const char *key, const std::string &value, bool comma=true)
bool anonymous_namespace{archive_worker.cpp}::find_eocd (std::ifstream &f, std::uint64_t file_size, std::uint64_t &eocd_offset)
bool anonymous_namespace{archive_worker.cpp}::parse_zip64_extra (const std::vector< unsigned char > &extra, bool uncomp_is_sentinel, bool comp_is_sentinel, bool offset_is_sentinel, std::uint64_t *out_uncomp, std::uint64_t *out_comp, std::uint64_t *out_offset)
bool anonymous_namespace{archive_worker.cpp}::read_local_entry (std::ifstream &f, std::uint64_t file_size, std::uint64_t local_offset, uint16_t &method, uint32_t &compressed_size, uint32_t &uncompressed_size, uint16_t &gp_flags_out, std::string &filename, std::uint64_t &data_offset, std::uint64_t &data_end, std::uint64_t &zip64_comp_size, std::uint64_t &zip64_uncomp_size, bool &has_zip64_extra)
int main (int argc, char **argv)

Variables

constexpr uint32_t anonymous_namespace{archive_worker.cpp}::SIG_LOCAL_FILE_HEADER = 0x04034b50
constexpr uint32_t anonymous_namespace{archive_worker.cpp}::SIG_CENTRAL_DIR_HEADER = 0x02014b50
constexpr uint32_t anonymous_namespace{archive_worker.cpp}::SIG_END_OF_CENTRAL_DIR = 0x06054b50

Function Documentation

◆ main()

int main ( int argc,
char ** argv )

Definition at line 327 of file archive_worker.cpp.

327 {
328 if (argc != 2) {
329 std::cerr << "usage: archive_worker <archive-path>\n";
330 return 2;
331 }
332
333 const fs::path path(argv[1]);
334
335 bool is_zip = false;
336 bool stored_size_mismatch = false;
337 bool header_mismatch = false;
338 bool invalid_offset = false;
339 bool overlapping_entries = false;
340 bool nested_archive = false;
341
342 int risk_score = 0;
343 std::string risk = "CLEAN";
344 std::string reason_code;
345 std::string detail;
346
347 std::ifstream f(path, std::ios::binary);
348 if (!f) {
349 std::cerr << "cannot open file\n";
350 return 3;
351 }
352
353 f.seekg(0, std::ios::end);
354 const std::uint64_t file_size = static_cast<std::uint64_t>(f.tellg());
355 if (file_size < 4) {
356 std::cout << "{";
357 print_json_bool("is_zip", false);
358 print_json_bool("stored_size_mismatch", false);
359 print_json_bool("header_mismatch", false);
360 print_json_bool("invalid_offset", false);
361 print_json_bool("overlapping_entries", false);
362 print_json_bool("nested_archive", false);
363 print_json_int("risk_score", 0);
364 print_json_str("risk", "CLEAN");
365 print_json_str("reason_code", "");
366 print_json_str("detail", "", false);
367 std::cout << "}\n";
368 return 0;
369 }
370
371 f.seekg(0, std::ios::beg);
372 const uint32_t first_sig = read_u32_le(f);
373 if (first_sig != SIG_LOCAL_FILE_HEADER) {
374 std::cout << "{";
375 print_json_bool("is_zip", false);
376 print_json_bool("stored_size_mismatch", false);
377 print_json_bool("header_mismatch", false);
378 print_json_bool("invalid_offset", false);
379 print_json_bool("overlapping_entries", false);
380 print_json_bool("nested_archive", false);
381 print_json_int("risk_score", 0);
382 print_json_str("risk", "CLEAN");
383 print_json_str("reason_code", "");
384 print_json_str("detail", "", false);
385 std::cout << "}\n";
386 return 0;
387 }
388
389 is_zip = true;
390
391 std::uint64_t eocd_offset = 0;
392 if (!find_eocd(f, file_size, eocd_offset)) {
393 risk_score += 30;
394 risk = "MALFORMED";
395 reason_code = "ZIP_EOCD_NOT_FOUND";
396 detail = "end of central directory not found";
397 } else {
398 f.seekg(static_cast<std::streamoff>(eocd_offset), std::ios::beg);
399 const uint32_t eocd_sig = read_u32_le(f);
400 if (eocd_sig != SIG_END_OF_CENTRAL_DIR) {
401 risk_score += 30;
402 risk = "MALFORMED";
403 reason_code = "ZIP_EOCD_INVALID";
404 detail = "invalid end of central directory signature";
405 } else {
406 (void)read_u16_le(f); // disk number
407 (void)read_u16_le(f); // cd start disk
408 const uint16_t entries_this_disk = read_u16_le(f);
409 const uint16_t total_entries = read_u16_le(f);
410 const uint32_t cd_size = read_u32_le(f);
411 const uint32_t cd_offset = read_u32_le(f);
412 (void)read_u16_le(f); // comment len
413
414 if (entries_this_disk != total_entries) {
415 header_mismatch = true;
416 risk_score += 15;
417 }
418
419 if (static_cast<std::uint64_t>(cd_offset) + cd_size > file_size) {
420 invalid_offset = true;
421 risk_score += 45;
422 reason_code = "ZIP_INVALID_OFFSET";
423 detail = "central directory outside file";
424 } else {
425 std::vector<ZipEntryInfo> entries;
426 entries.reserve(total_entries);
427
428 std::uint64_t pos = cd_offset;
429 for (uint16_t i = 0; i < total_entries; ++i) {
430 if (pos + 46 > file_size) {
431 invalid_offset = true;
432 risk_score += 45;
433 if (reason_code.empty()) {
434 reason_code = "ZIP_INVALID_OFFSET";
435 detail = "central directory header outside file";
436 }
437 break;
438 }
439
440 f.seekg(static_cast<std::streamoff>(pos), std::ios::beg);
441 const uint32_t cd_sig = read_u32_le(f);
442 if (cd_sig != SIG_CENTRAL_DIR_HEADER) {
443 header_mismatch = true;
444 risk_score += 35;
445 if (reason_code.empty()) {
446 reason_code = "ZIP_HEADER_MISMATCH";
447 detail = "central directory signature mismatch";
448 }
449 break;
450 }
451
452 (void)read_u16_le(f); // version made by
453 (void)read_u16_le(f); // version needed
454 const uint16_t cd_gp_flags = read_u16_le(f);
455 const uint16_t cd_method = read_u16_le(f);
456 (void)read_u16_le(f); // time
457 (void)read_u16_le(f); // date
458 (void)read_u32_le(f); // crc32
459 const uint32_t cd_comp_size = read_u32_le(f);
460 const uint32_t cd_uncomp_size = read_u32_le(f);
461 const uint16_t name_len = read_u16_le(f);
462 const uint16_t extra_len = read_u16_le(f);
463 const uint16_t comment_len = read_u16_le(f);
464 (void)read_u16_le(f); // disk start
465 (void)read_u16_le(f); // int attrs
466 (void)read_u32_le(f); // ext attrs
467 const uint32_t local_header_offset_32 = read_u32_le(f);
468
469 std::string cd_name = read_string(f, name_len);
470 if (cd_name.size() != name_len) {
471 invalid_offset = true;
472 risk_score += 45;
473 if (reason_code.empty()) {
474 reason_code = "ZIP_INVALID_OFFSET";
475 detail = "cannot read central directory filename";
476 }
477 break;
478 }
479
480 // Leggi extra field della central directory (può contenere ZIP64 ext)
481 std::vector<unsigned char> cd_extra(extra_len);
482 if (extra_len > 0) {
483 f.read(reinterpret_cast<char*>(cd_extra.data()), extra_len);
484 if (static_cast<std::size_t>(f.gcount()) != extra_len) {
485 invalid_offset = true;
486 risk_score += 45;
487 if (reason_code.empty()) {
488 reason_code = "ZIP_INVALID_OFFSET";
489 detail = "cannot read central directory extra field";
490 }
491 break;
492 }
493 }
494
495 // ZIP64: se comp/uncomp/offset valgono 0xFFFFFFFF, il vero valore
496 // sta nell'extra field 0x0001.
497 const bool cd_comp_is_sentinel = (cd_comp_size == 0xFFFFFFFFu);
498 const bool cd_uncomp_is_sentinel = (cd_uncomp_size == 0xFFFFFFFFu);
499 const bool cd_off_is_sentinel = (local_header_offset_32 == 0xFFFFFFFFu);
500
501 std::uint64_t cd_comp_size_64 = cd_comp_size;
502 std::uint64_t cd_uncomp_size_64 = cd_uncomp_size;
503 std::uint64_t local_header_offset = local_header_offset_32;
504 bool cd_has_zip64_extra = false;
505 if (cd_comp_is_sentinel || cd_uncomp_is_sentinel || cd_off_is_sentinel) {
506 cd_has_zip64_extra = parse_zip64_extra(
507 cd_extra,
508 cd_uncomp_is_sentinel,
509 cd_comp_is_sentinel,
510 cd_off_is_sentinel,
511 cd_uncomp_is_sentinel ? &cd_uncomp_size_64 : nullptr,
512 cd_comp_is_sentinel ? &cd_comp_size_64 : nullptr,
513 cd_off_is_sentinel ? &local_header_offset : nullptr);
514
515 // Se il local header offset era sentinel ma non troviamo l'extra
516 // field, il file è davvero malformato.
517 if (cd_off_is_sentinel && !cd_has_zip64_extra) {
518 invalid_offset = true;
519 risk_score += 45;
520 if (reason_code.empty()) {
521 reason_code = "ZIP_INVALID_OFFSET";
522 detail = "missing ZIP64 extra field for local header offset";
523 }
524 }
525 }
526
527 if (contains_dotdot_path(cd_name) || is_absolute_or_weird_path(cd_name)) {
528 // Segnale FORTE: un file legittimo non ha mai "../" o path
529 // assoluti nel nome. Zip-slip classico.
530 header_mismatch = true;
531 risk_score += 40;
532 if (reason_code.empty()) {
533 reason_code = "ZIP_PATH_TRAVERSAL_NAME";
534 detail = "suspicious path inside archive";
535 }
536 }
537
538 if (looks_like_nested_archive_name(cd_name)) {
539 nested_archive = true;
540 }
541
542 if (local_header_offset >= file_size) {
543 invalid_offset = true;
544 risk_score += 45;
545 if (reason_code.empty()) {
546 reason_code = "ZIP_INVALID_OFFSET";
547 detail = "local header offset outside file";
548 }
549 } else {
550 uint16_t local_method = 0;
551 uint32_t local_comp_size = 0;
552 uint32_t local_uncomp_size = 0;
553 uint16_t local_gp_flags = 0;
554 std::string local_name;
555 std::uint64_t data_offset = 0;
556 std::uint64_t data_end = 0;
557 std::uint64_t local_zip64_comp = 0;
558 std::uint64_t local_zip64_uncomp = 0;
559 bool local_has_zip64_extra = false;
560
561 if (!read_local_entry(
562 f,
563 file_size,
564 local_header_offset,
565 local_method,
566 local_comp_size,
567 local_uncomp_size,
568 local_gp_flags,
569 local_name,
570 data_offset,
571 data_end,
572 local_zip64_comp,
573 local_zip64_uncomp,
574 local_has_zip64_extra))
575 {
576 invalid_offset = true;
577 risk_score += 45;
578 if (reason_code.empty()) {
579 reason_code = "ZIP_INVALID_OFFSET";
580 detail = "invalid local header";
581 }
582 } else {
583 // Data descriptor (bit 3): per lo standard, i size nel
584 // local header possono essere 0 e i valori reali sono nel
585 // data descriptor dopo i dati. Non è un indizio di tampering.
586 const bool local_has_dd = (local_gp_flags & 0x08) != 0;
587 const bool cd_has_dd = (cd_gp_flags & 0x08) != 0;
588
589 // Calcoliamo size effettive (ZIP64-aware) per il confronto.
590 std::uint64_t eff_local_comp = local_comp_size;
591 std::uint64_t eff_local_uncomp = local_uncomp_size;
592 if (local_comp_size == 0xFFFFFFFFu && local_has_zip64_extra) {
593 eff_local_comp = local_zip64_comp;
594 }
595 if (local_uncomp_size == 0xFFFFFFFFu && local_has_zip64_extra) {
596 eff_local_uncomp = local_zip64_uncomp;
597 }
598
599 // Le size possono essere legittimamente 0 nel local header
600 // quando c'è data descriptor: saltiamo il confronto.
601 const bool skip_size_check = local_has_dd &&
602 local_comp_size == 0 &&
603 local_uncomp_size == 0;
604
605 bool name_mismatch = (local_name != cd_name);
606 bool method_mismatch = (local_method != cd_method);
607 bool size_mismatch = !skip_size_check &&
608 (eff_local_comp != cd_comp_size_64 ||
609 eff_local_uncomp != cd_uncomp_size_64);
610 // Anche il flag data-descriptor dovrebbe combaciare; se no
611 // è un segnale debole, ma non "vero" mismatch semantico.
612 bool dd_flag_mismatch = (local_has_dd != cd_has_dd);
613
614 if (name_mismatch || method_mismatch || size_mismatch) {
615 header_mismatch = true;
616 risk_score += 35;
617 if (reason_code.empty()) {
618 reason_code = "ZIP_HEADER_MISMATCH";
619 detail = "local header and central directory differ";
620 }
621 } else if (dd_flag_mismatch) {
622 // Segnale debole: flag non combacia ma dati sono coerenti.
623 risk_score += 5;
624 }
625
626 // STORED-size-mismatch: un file non compresso DEVE avere
627 // compressed_size == uncompressed_size. Skip se sentinel o dd.
628 if (cd_method == 0 &&
629 !cd_comp_is_sentinel &&
630 !cd_uncomp_is_sentinel &&
631 !cd_has_dd &&
632 cd_comp_size != cd_uncomp_size)
633 {
634 stored_size_mismatch = true;
635 risk_score += 40;
636 reason_code = "ZIP_STORED_SIZE_MISMATCH";
637 detail = "STORED entry but compressed and uncompressed sizes differ";
638 }
639
640 ZipEntryInfo entry;
641 entry.name = cd_name;
642 entry.method = cd_method;
643 entry.compressed_size = cd_comp_size;
644 entry.uncompressed_size = cd_uncomp_size;
645 entry.local_header_offset =
646 static_cast<uint32_t>(local_header_offset & 0xFFFFFFFFu);
647 entry.data_offset = data_offset;
648 // Usa size ZIP64-corretta; con data descriptor non
649 // conosciamo la dimensione esatta lato local header,
650 // quindi usiamo la central directory (affidabile).
651 entry.data_end = data_offset + cd_comp_size_64;
652 entries.push_back(std::move(entry));
653 }
654 }
655
656 pos += 46ULL + name_len + extra_len + comment_len;
657 if (pos > file_size) {
658 invalid_offset = true;
659 risk_score += 45;
660 if (reason_code.empty()) {
661 reason_code = "ZIP_INVALID_OFFSET";
662 detail = "central directory entry overflow";
663 }
664 break;
665 }
666 }
667
668 if (!entries.empty()) {
669 std::sort(entries.begin(), entries.end(),
670 [](const ZipEntryInfo& a, const ZipEntryInfo& b) {
671 return a.data_offset < b.data_offset;
672 });
673
674 for (std::size_t i = 1; i < entries.size(); ++i) {
675 if (entries[i].data_offset < entries[i - 1].data_end) {
676 overlapping_entries = true;
677 risk_score += 50;
678 if (reason_code.empty()) {
679 reason_code = "ZIP_OVERLAPPING_ENTRIES";
680 detail = "overlapping archive entries detected";
681 }
682 break;
683 }
684 }
685 }
686
687 if (nested_archive) {
688 // Segnale molto debole: JAR, AAB, installer, archivi di backup,
689 // bundle di distribuzione spesso contengono archivi annidati.
690 // Da solo non deve scattare SUSPICIOUS.
691 risk_score += 5;
692 if (reason_code.empty()) {
693 reason_code = "ZIP_NESTED_ARCHIVE";
694 detail = "nested archive detected";
695 }
696 }
697 }
698 }
699 }
700
701 // Soglie:
702 // - risk >= 60 → EVASIVE (più segnali forti, o uno forte + correlati)
703 // - risk >= 30 → SUSPICIOUS (un segnale forte tipo header_mismatch o stored_size_mismatch)
704 // - altrimenti → CLEAN (anche con segnali molto deboli come un nested archive isolato)
705 // Se nel processo è già stato impostato "MALFORMED" (EOCD non trovato), preserviamolo.
706 if (risk == "MALFORMED") {
707 // no-op: preserva MALFORMED
708 } else if (risk_score >= 60) {
709 risk = "EVASIVE";
710 } else if (risk_score >= 30) {
711 risk = "SUSPICIOUS";
712 } else {
713 risk = "CLEAN";
714 }
715
716 std::cout << "{";
717 print_json_bool("is_zip", is_zip);
718 print_json_bool("stored_size_mismatch", stored_size_mismatch);
719 print_json_bool("header_mismatch", header_mismatch);
720 print_json_bool("invalid_offset", invalid_offset);
721 print_json_bool("overlapping_entries", overlapping_entries);
722 print_json_bool("nested_archive", nested_archive);
723 print_json_int("risk_score", risk_score);
724 print_json_str("risk", risk);
725 print_json_str("reason_code", reason_code);
726 print_json_str("detail", detail, false);
727 std::cout << "}\n";
728
729 return 0;
730}
bool read_local_entry(std::ifstream &f, std::uint64_t file_size, std::uint64_t local_offset, uint16_t &method, uint32_t &compressed_size, uint32_t &uncompressed_size, uint16_t &gp_flags_out, std::string &filename, std::uint64_t &data_offset, std::uint64_t &data_end, std::uint64_t &zip64_comp_size, std::uint64_t &zip64_uncomp_size, bool &has_zip64_extra)
void print_json_str(const char *key, const std::string &value, bool comma=true)
void print_json_int(const char *key, int value, bool comma=true)
bool parse_zip64_extra(const std::vector< unsigned char > &extra, bool uncomp_is_sentinel, bool comp_is_sentinel, bool offset_is_sentinel, std::uint64_t *out_uncomp, std::uint64_t *out_comp, std::uint64_t *out_offset)
bool contains_dotdot_path(const std::string &name)
bool looks_like_nested_archive_name(const std::string &name)
std::string read_string(std::ifstream &f, std::size_t len)
void print_json_bool(const char *key, bool value, bool comma=true)
uint16_t read_u16_le(std::ifstream &f)
bool is_absolute_or_weird_path(const std::string &name)
bool find_eocd(std::ifstream &f, std::uint64_t file_size, std::uint64_t &eocd_offset)
uint32_t read_u32_le(std::ifstream &f)

References anonymous_namespace{archive_worker.cpp}::contains_dotdot_path(), anonymous_namespace{archive_worker.cpp}::find_eocd(), anonymous_namespace{archive_worker.cpp}::is_absolute_or_weird_path(), anonymous_namespace{archive_worker.cpp}::looks_like_nested_archive_name(), main(), anonymous_namespace{archive_worker.cpp}::parse_zip64_extra(), anonymous_namespace{archive_worker.cpp}::print_json_bool(), anonymous_namespace{archive_worker.cpp}::print_json_int(), anonymous_namespace{archive_worker.cpp}::print_json_str(), anonymous_namespace{archive_worker.cpp}::read_local_entry(), anonymous_namespace{archive_worker.cpp}::read_string(), anonymous_namespace{archive_worker.cpp}::read_u16_le(), anonymous_namespace{archive_worker.cpp}::read_u32_le(), anonymous_namespace{archive_worker.cpp}::SIG_CENTRAL_DIR_HEADER, anonymous_namespace{archive_worker.cpp}::SIG_END_OF_CENTRAL_DIR, and anonymous_namespace{archive_worker.cpp}::SIG_LOCAL_FILE_HEADER.