Browse code

Adds detection and heuristic alert for zips with overlapping files, preventing extraction of non-recursive zip bombs.

Micah Snyder authored on 2019/07/13 10:09:45
Showing 2 changed files
... ...
@@ -5,14 +5,17 @@ Note: This file refers to the source tarball. Things described here may differ
5 5
 
6 6
 ## 0.101.3
7 7
 
8
-ClamAV 0.101.3 is a patch release...
8
+ClamAV 0.101.3 is a patch release to address a vulnerability to non-recursive
9
+zip bombs.
9 10
 
10
-- Fixes for the following vulnerabilities affecting 0.101.2 and prior:
11
-  -
11
+A Denial-of-Service (DoS) vulnerability may occur when scanning a zip bomb as a
12
+result of excessively long scan times. The issue is resolved by detecting the
13
+overlapping local file headers which characterize the non-recursive zip bomb
14
+described by David Fifield,
15
+[here](https://www.bamsoftware.com/hacks/zipbomb/).
12 16
 
13
-Additional thanks to the following community members for submitting bug reports:
14
-
15
--
17
+Thank you to Hanno Böck for reporting the issue as it relates to ClamAV,
18
+[here](https://bugzilla.clamav.net/show_bug.cgi?id=12356).
16 19
 
17 20
 ## 0.101.2
18 21
 
... ...
@@ -54,6 +54,8 @@
54 54
 #define UNZIP_PRIVATE
55 55
 #include "unzip.h"
56 56
 
57
+#define ZIP_MAX_NUM_OVERLAPPING_FILES 5
58
+
57 59
 #define ZIP_CRC32(r,c,b,l)			\
58 60
     do {					\
59 61
 	r = crc32(~c,b,l);			\
... ...
@@ -493,14 +495,14 @@ static inline int zdecrypt(const uint8_t *src, uint32_t csize, uint32_t usize, c
493 493
 	if (pass_zip)
494 494
 	    pass_zip = pass_zip->next;
495 495
 	else
496
-	    pass_any = pass_any->next;	    
496
+	    pass_any = pass_any->next;
497 497
     }
498 498
 
499 499
     cli_dbgmsg("cli_unzip: decrypt - skipping encrypted file, no valid passwords\n");
500 500
     return CL_SUCCESS;
501 501
 }
502 502
 
503
-static unsigned int lhdr(fmap_t *map, uint32_t loff,uint32_t zsize, unsigned int *fu, unsigned int fc, const uint8_t *ch, int *ret, cli_ctx *ctx, char *tmpd, int detect_encrypted, zip_cb zcb) {
503
+static unsigned int lhdr(fmap_t *map, uint32_t loff,uint32_t zsize, unsigned int *fu, unsigned int fc, const uint8_t *ch, int *ret, cli_ctx *ctx, char *tmpd, int detect_encrypted, zip_cb zcb, uint32_t *file_local_header_size, uint32_t* file_local_data_size) {
504 504
   const uint8_t *lh, *zip;
505 505
   char name[256];
506 506
   uint32_t csize, usize;
... ...
@@ -563,7 +565,7 @@ static unsigned int lhdr(fmap_t *map, uint32_t loff,uint32_t zsize, unsigned int
563 563
     }
564 564
     virus_found = 1;
565 565
   }
566
- 
566
+
567 567
   if(LH_flags & F_USEDD) {
568 568
     cli_dbgmsg("cli_unzip: lh - has data desc\n");
569 569
     if(!ch) {
... ...
@@ -581,6 +583,11 @@ static unsigned int lhdr(fmap_t *map, uint32_t loff,uint32_t zsize, unsigned int
581 581
   zip+=LH_elen;
582 582
   zsize-=LH_elen;
583 583
 
584
+  if (NULL != file_local_header_size)
585
+      *file_local_header_size = zip - lh;
586
+  if (NULL != file_local_data_size)
587
+      *file_local_data_size = csize;
588
+
584 589
   if (!csize) { /* FIXME: what's used for method0 files? csize or usize? Nothing in the specs, needs testing */
585 590
       cli_dbgmsg("cli_unzip: lh - skipping empty file\n");
586 591
   } else {
... ...
@@ -589,6 +596,7 @@ static unsigned int lhdr(fmap_t *map, uint32_t loff,uint32_t zsize, unsigned int
589 589
 	  fmap_unneed_off(map, loff, SIZEOF_LH);
590 590
 	  return 0;
591 591
       }
592
+
592 593
       if(LH_flags & F_ENCR) {
593 594
 	  if(fmap_need_ptr_once(map, zip, csize))
594 595
 	      *ret = zdecrypt(zip, csize, usize, lh, fu, ctx, tmpd, zcb);
... ...
@@ -624,12 +632,19 @@ static unsigned int lhdr(fmap_t *map, uint32_t loff,uint32_t zsize, unsigned int
624 624
   return zip-lh;
625 625
 }
626 626
 
627
-static unsigned int chdr(fmap_t *map, uint32_t coff, uint32_t zsize, unsigned int *fu, unsigned int fc, int *ret, cli_ctx *ctx, char *tmpd, struct zip_requests *requests) {
627
+static unsigned int chdr(fmap_t *map, uint32_t coff, uint32_t zsize, unsigned int *fu, unsigned int fc, int *ret, cli_ctx *ctx, char *tmpd, struct zip_requests *requests, uint32_t *file_local_offset, uint32_t *file_local_header_size, uint32_t *file_local_data_size) {
628 628
   char name[256];
629 629
   int last = 0;
630 630
   const uint8_t *ch;
631 631
   int virus_found = 0;
632 632
 
633
+  if (NULL != file_local_offset)
634
+      *file_local_offset = 0;
635
+  if (NULL != file_local_header_size)
636
+      *file_local_header_size = 0;
637
+  if (NULL != file_local_data_size)
638
+      *file_local_data_size = 0;
639
+
633 640
   if(!(ch = fmap_need_off(map, coff, SIZEOF_CH)) || CH_magic != 0x02014b50) {
634 641
       if(ch) fmap_unneed_ptr(map, ch, SIZEOF_CH);
635 642
       cli_dbgmsg("cli_unzip: ch - wrkcomplete\n");
... ...
@@ -674,7 +689,9 @@ static unsigned int chdr(fmap_t *map, uint32_t coff, uint32_t zsize, unsigned in
674 674
 
675 675
   if (!requests) {
676 676
       if(CH_off<zsize-SIZEOF_LH) {
677
-          lhdr(map, CH_off, zsize-CH_off, fu, fc, ch, ret, ctx, tmpd, 1, zip_scan_cb);
677
+          if (NULL != file_local_offset)
678
+              *file_local_offset = CH_off;
679
+          lhdr(map, CH_off, zsize-CH_off, fu, fc, ch, ret, ctx, tmpd, 1, zip_scan_cb, file_local_header_size, file_local_data_size);
678 680
       } else cli_dbgmsg("cli_unzip: ch - local hdr out of file\n");
679 681
   }
680 682
   else {
... ...
@@ -685,7 +702,7 @@ static unsigned int chdr(fmap_t *map, uint32_t coff, uint32_t zsize, unsigned in
685 685
           for (i = 0; i < requests->namecnt; ++i) {
686 686
               cli_dbgmsg("checking for %i: %s\n", i, requests->names[i]);
687 687
 
688
-              len = MIN(sizeof(name)-1, requests->namelens[i]);      
688
+              len = MIN(sizeof(name)-1, requests->namelens[i]);
689 689
               if (!strncmp(requests->names[i], name, len)) {
690 690
                   requests->match = 1;
691 691
                   requests->found = i;
... ...
@@ -712,6 +729,13 @@ int cli_unzip(cli_ctx *ctx) {
712 712
 #if HAVE_JSON
713 713
   int toval = 0;
714 714
 #endif
715
+  int bZipBombDetected                 = 0;
716
+  uint32_t cur_file_local_offset       = 0;
717
+  uint32_t cur_file_local_header_size  = 0;
718
+  uint32_t cur_file_local_data_size    = 0;
719
+  uint32_t prev_file_local_offset      = 0;
720
+  uint32_t prev_file_local_header_size = 0;
721
+  uint32_t prev_file_local_data_size   = 0;
715 722
 
716 723
   cli_dbgmsg("in cli_unzip\n");
717 724
   fsize = (uint32_t)map->len;
... ...
@@ -744,20 +768,48 @@ int cli_unzip(cli_ctx *ctx) {
744 744
   }
745 745
 
746 746
   if(coff) {
747
+      uint32_t nOverlappingFiles = 0;
748
+
747 749
       cli_dbgmsg("cli_unzip: central @%x\n", coff);
748
-      while((coff=chdr(map, coff, fsize, &fu, fc+1, &ret, ctx, tmpd, NULL))) {
750
+      while((coff=chdr(map, coff, fsize, &fu, fc+1, &ret, ctx, tmpd, NULL, &cur_file_local_offset, &cur_file_local_header_size, &cur_file_local_data_size))) {
749 751
 	  fc++;
750 752
 	  if (ctx->engine->maxfiles && fu>=ctx->engine->maxfiles) {
751 753
 	      cli_dbgmsg("cli_unzip: Files limit reached (max: %u)\n", ctx->engine->maxfiles);
752 754
 	      ret=CL_EMAXFILES;
753 755
 	  }
756
+    /*
757
+     * Detect overlapping files and zip bombs.
758
+     */
759
+    if ((((cur_file_local_offset > prev_file_local_offset) && (cur_file_local_offset < prev_file_local_offset + prev_file_local_header_size + prev_file_local_data_size)) ||
760
+         ((prev_file_local_offset > cur_file_local_offset) && (prev_file_local_offset < cur_file_local_offset + cur_file_local_header_size + cur_file_local_data_size))) &&
761
+        (cur_file_local_header_size + cur_file_local_data_size > 0)) {
762
+        /* Overlapping file detected */
763
+        nOverlappingFiles++;
764
+
765
+        cli_dbgmsg("cli_unzip: Overlapping files detected.\n");
766
+        cli_dbgmsg("    previous file end:  %u\n", prev_file_local_offset + prev_file_local_header_size + prev_file_local_data_size);
767
+        cli_dbgmsg("    current file start: %u\n", cur_file_local_offset);
768
+        if (ZIP_MAX_NUM_OVERLAPPING_FILES < nOverlappingFiles) {
769
+          if (SCAN_HEURISTICS) {
770
+              ret         = cli_append_virus(ctx, "Heuristics.Zip.OverlappingFiles");
771
+              virus_found = 1;
772
+          } else {
773
+              ret = CL_EFORMAT;
774
+          }
775
+          bZipBombDetected = 1;
776
+        }
777
+    }
778
+    prev_file_local_offset      = cur_file_local_offset;
779
+    prev_file_local_header_size = cur_file_local_header_size;
780
+    prev_file_local_data_size   = cur_file_local_data_size;
781
+
754 782
 #if HAVE_JSON
755 783
           if (cli_json_timeout_cycle_check(ctx, &toval) != CL_SUCCESS) {
756 784
               ret=CL_ETIMEOUT;
757 785
           }
758 786
 #endif
759 787
           if (ret != CL_CLEAN) {
760
-              if (ret == CL_VIRUS && SCAN_ALLMATCHES) {
788
+              if (ret == CL_VIRUS && SCAN_ALLMATCHES && !bZipBombDetected) {
761 789
                   ret = CL_CLEAN;
762 790
                   virus_found = 1;
763 791
               } else
... ...
@@ -769,7 +821,7 @@ int cli_unzip(cli_ctx *ctx) {
769 769
       ret = CL_VIRUS;
770 770
   if(fu<=(fc/4)) { /* FIXME: make up a sane ratio or remove the whole logic */
771 771
     fc = 0;
772
-    while (ret==CL_CLEAN && lhoff<fsize && (coff=lhdr(map, lhoff, fsize-lhoff, &fu, fc+1, NULL, &ret, ctx, tmpd, 1, zip_scan_cb))) {
772
+    while (ret==CL_CLEAN && lhoff<fsize && (coff=lhdr(map, lhoff, fsize-lhoff, &fu, fc+1, NULL, &ret, ctx, tmpd, 1, zip_scan_cb, NULL, NULL))) {
773 773
       fc++;
774 774
       lhoff+=coff;
775 775
       if (SCAN_ALLMATCHES && ret == CL_VIRUS) {
... ...
@@ -816,7 +868,7 @@ int unzip_single_internal(cli_ctx *ctx, off_t lhoffl, zip_cb zcb)
816 816
     return CL_CLEAN;
817 817
   }
818 818
 
819
-  lhdr(map, lhoffl, fsize, &fu, 0, NULL, &ret, ctx, NULL, 0, zcb);
819
+  lhdr(map, lhoffl, fsize, &fu, 0, NULL, &ret, ctx, NULL, 0, zcb, NULL, NULL);
820 820
 
821 821
   return ret;
822 822
 }
... ...
@@ -886,7 +938,7 @@ int unzip_search(cli_ctx *ctx, fmap_t *map, struct zip_requests *requests)
886 886
 
887 887
     if(coff) {
888 888
         cli_dbgmsg("unzip_search: central @%x\n", coff);
889
-        while(ret==CL_CLEAN && (coff=chdr(zmap, coff, fsize, NULL, fc+1, &ret, ctx, NULL, requests))) {
889
+        while(ret==CL_CLEAN && (coff=chdr(zmap, coff, fsize, NULL, fc+1, &ret, ctx, NULL, requests, NULL, NULL, NULL))) {
890 890
             if (requests->match) {
891 891
                 ret=CL_VIRUS;
892 892
             }