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 << "{";
367 std::cout << "}\n";
368 return 0;
369 }
370
371 f.seekg(0, std::ios::beg);
374 std::cout << "{";
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);
401 risk_score += 30;
402 risk = "MALFORMED";
403 reason_code = "ZIP_EOCD_INVALID";
404 detail = "invalid end of central directory signature";
405 } else {
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);
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
467 const uint32_t local_header_offset_32 =
read_u32_le(f);
468
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
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
496
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) {
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
516
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
528
529
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
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
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
584
585
586 const bool local_has_dd = (local_gp_flags & 0x08) != 0;
587 const bool cd_has_dd = (cd_gp_flags & 0x08) != 0;
588
589
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
600
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
611
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
623 risk_score += 5;
624 }
625
626
627
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
649
650
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
689
690
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
702
703
704
705
706 if (risk == "MALFORMED") {
707
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 << "{";
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)
constexpr uint32_t SIG_CENTRAL_DIR_HEADER
constexpr uint32_t SIG_LOCAL_FILE_HEADER
void print_json_bool(const char *key, bool value, bool comma=true)
uint16_t read_u16_le(std::ifstream &f)
constexpr uint32_t SIG_END_OF_CENTRAL_DIR
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)