Browse code

Update PE parsing code related to Authenticode verification The following changes were made - The code to calculate the authenticode hash was not properly accounting for the case where a PE had sections that either overlapped with each other or overlapped with the PE header. One common case for this is UPX-packed binaries, where the first section with data on disk starts at offset 0x400, which overlaps with the specified PE header by 0xC00 bytes. - The code didn't wrap accesses to fields in the Security DataDirectory with EC32(), so it seems likely that authenticode parsing always encountered issues on big endian systems. I think I fixed all of the accesses in cli_checkfp_pe, but there might still be issues here. I'll test this further. - We parse the authenticode data header to better ensure that it's PCKS7 we are trying to parse, and not one of the other types - cli_checkfp_pe should now finish faster in the case where there is no authenticode data and we don't want to compute the section hashes. - Fixed a potential memory leak in one cli_checkfp_pe failure case

Andrew authored on 2018/08/28 11:53:23
Showing 2 changed files
... ...
@@ -5405,6 +5405,7 @@ int cli_checkfp_pe(cli_ctx *ctx, uint8_t *authsha1, stats_section_t *hashes, uin
5405 5405
     struct pe_image_data_dir *dirs;
5406 5406
     fmap_t *map = *ctx->fmap;
5407 5407
     void *hashctx=NULL;
5408
+    struct pe_certificate_hdr cert_hdr;
5408 5409
 
5409 5410
     if (flags & CL_CHECKFP_PE_FLAG_STATS)
5410 5411
         if (!(hashes))
... ...
@@ -5439,8 +5440,14 @@ int cli_checkfp_pe(cli_ctx *ctx, uint8_t *authsha1, stats_section_t *hashes, uin
5439 5439
     if(nsections < 1 || nsections > 96)
5440 5440
         return CL_EFORMAT;
5441 5441
 
5442
-    if(EC16(file_hdr.SizeOfOptionalHeader) < sizeof(struct pe_image_optional_hdr32))
5442
+    // TODO the pe_image_optional_hdr32 structure includes space for all 16
5443
+    // data directories, but these might not all exist in a given binary.
5444
+    // We need to check NumberOfRvaAndSizes instead, and allow through any
5445
+    // with at least 5 (the security DataDirectory)
5446
+    if(EC16(file_hdr.SizeOfOptionalHeader) < sizeof(struct pe_image_optional_hdr32)) {
5447
+        cli_dbgmsg("cli_checkfp_pe: SizeOfOptionalHeader < less than the size expected (%lu)\n", sizeof(struct pe_image_optional_hdr32));
5443 5448
         return CL_EFORMAT;
5449
+    }
5444 5450
 
5445 5451
     at = e_lfanew + sizeof(struct pe_image_file_hdr);
5446 5452
     if(fmap_readn(map, &optional_hdr32, at, sizeof(struct pe_image_optional_hdr32)) != sizeof(struct pe_image_optional_hdr32))
... ...
@@ -5475,6 +5482,17 @@ int cli_checkfp_pe(cli_ctx *ctx, uint8_t *authsha1, stats_section_t *hashes, uin
5475 5475
         dirs = optional_hdr64.DataDirectory;
5476 5476
     }
5477 5477
 
5478
+    // As an optimization, check the security DataDirectory here and if
5479
+    // it's less than 8-bytes (and we aren't relying on this code to compute
5480
+    // the section hashes), bail out
5481
+    if (EC32(dirs[4].Size) < 8) {
5482
+        if (flags & CL_CHECKFP_PE_FLAG_STATS) {
5483
+            /* If stats is enabled, continue parsing the sample */
5484
+            flags ^= CL_CHECKFP_PE_FLAG_AUTHENTICODE;
5485
+        } else {
5486
+            return CL_VIRUS;
5487
+        }
5488
+    }
5478 5489
     fsize = map->len;
5479 5490
 
5480 5491
     valign = (pe_plus)?EC32(optional_hdr64.SectionAlignment):EC32(optional_hdr32.SectionAlignment);
... ...
@@ -5490,12 +5508,17 @@ int cli_checkfp_pe(cli_ctx *ctx, uint8_t *authsha1, stats_section_t *hashes, uin
5490 5490
     if(!exe_sections)
5491 5491
         return CL_EMEM;
5492 5492
 
5493
+    // TODO I'm not sure why this is necessary since the specification says
5494
+    // that PointerToRawData is expected to be a multiple of the file
5495
+    // alignment.  Should we report this is as a PE with an error?
5493 5496
     for(i = 0; falign!=0x200 && i<nsections; i++) {
5494 5497
         /* file alignment fallback mode - blah */
5495 5498
         if (falign && section_hdr[i].SizeOfRawData && EC32(section_hdr[i].PointerToRawData)%falign && !(EC32(section_hdr[i].PointerToRawData)%0x200))
5496 5499
             falign = 0x200;
5497 5500
     }
5498 5501
 
5502
+    // TODO Why is this needed?  hdr_size should already be rounded up
5503
+    // to a multiple of the file alignment.
5499 5504
     hdr_size = PESALIGN(hdr_size, falign); /* Aligned headers virtual size */
5500 5505
 
5501 5506
     if (flags & CL_CHECKFP_PE_FLAG_STATS) {
... ...
@@ -5507,23 +5530,32 @@ int cli_checkfp_pe(cli_ctx *ctx, uint8_t *authsha1, stats_section_t *hashes, uin
5507 5507
         }
5508 5508
     }
5509 5509
 
5510
+    // TODO Why do we fix up these alignments?  This shouldn't be needed?
5510 5511
     for(i = 0; i < nsections; i++) {
5511 5512
         exe_sections[i].rva = PEALIGN(EC32(section_hdr[i].VirtualAddress), valign);
5512 5513
         exe_sections[i].vsz = PESALIGN(EC32(section_hdr[i].VirtualSize), valign);
5513 5514
         exe_sections[i].raw = PEALIGN(EC32(section_hdr[i].PointerToRawData), falign);
5514 5515
         exe_sections[i].rsz = PESALIGN(EC32(section_hdr[i].SizeOfRawData), falign);
5515 5516
 
5517
+        // TODO exe_sections[i].ursz is not assigned to (will always be 0)
5518
+        // Figure out what this is meant to do and ensure that happens
5516 5519
         if (!exe_sections[i].vsz && exe_sections[i].rsz)
5517 5520
             exe_sections[i].vsz=PESALIGN(exe_sections[i].ursz, valign);
5518 5521
 
5519
-        if (exe_sections[i].rsz && fsize>exe_sections[i].raw && !CLI_ISCONTAINED(0, (uint32_t) fsize, exe_sections[i].raw, exe_sections[i].rsz))
5520
-            exe_sections[i].rsz = fsize - exe_sections[i].raw;
5522
+        if (exe_sections[i].rsz && fsize>exe_sections[i].raw && !CLI_ISCONTAINED(0, (uint32_t) fsize, exe_sections[i].raw, exe_sections[i].rsz)) {
5523
+            cli_dbgmsg("cli_checkfp_pe: encountered section not fully contained within the file\n");
5524
+            free(exe_sections);
5525
+            return CL_EFORMAT;
5526
+        }
5521 5527
 
5522 5528
         if (exe_sections[i].rsz && exe_sections[i].raw >= fsize) {
5529
+            cli_dbgmsg("cli_checkfp_pe: encountered section that doesn't exist within the file\n");
5523 5530
             free(exe_sections);
5524 5531
             return CL_EFORMAT;
5525 5532
         }
5526 5533
 
5534
+        // TODO These checks aren't needed because the u vars are never assigned (always 0)
5535
+        // Figure out what this is meant to do and ensure that happens
5527 5536
         if (exe_sections[i].urva>>31 || exe_sections[i].uvsz>>31 || (exe_sections[i].rsz && exe_sections[i].uraw>>31) || exe_sections[i].ursz>>31) {
5528 5537
             free(exe_sections);
5529 5538
             return CL_EFORMAT;
... ...
@@ -5539,16 +5571,33 @@ int cli_checkfp_pe(cli_ctx *ctx, uint8_t *authsha1, stats_section_t *hashes, uin
5539 5539
 
5540 5540
     if (flags & CL_CHECKFP_PE_FLAG_AUTHENTICODE) {
5541 5541
         /* Check to see if we have a security section. */
5542
-        if(!cli_hm_have_size(ctx->engine->hm_fp, CLI_HASH_SHA1, 2) && dirs[4].Size < 8) {
5542
+        if(!cli_hm_have_size(ctx->engine->hm_fp, CLI_HASH_SHA1, 2) && EC32(dirs[4].Size) < 8) {
5543 5543
             if (flags & CL_CHECKFP_PE_FLAG_STATS) {
5544 5544
                 /* If stats is enabled, continue parsing the sample */
5545 5545
                 flags ^= CL_CHECKFP_PE_FLAG_AUTHENTICODE;
5546 5546
             } else {
5547
+                free(exe_sections);
5547 5548
                 if (hashctx)
5548 5549
                     cl_hash_destroy(hashctx);
5549 5550
                 return CL_BREAK;
5550 5551
             }
5551 5552
         }
5553
+
5554
+
5555
+        // Verify that we have all the bytes we expect in the authenticode sig
5556
+        // and that the certificate table is the last thing in the file
5557
+        // (according to the MS13-098 bulletin, this is a requirement)
5558
+        if (fsize != EC32(dirs[4].Size) + EC32(dirs[4].VirtualAddress)) {
5559
+            if (flags & CL_CHECKFP_PE_FLAG_STATS) {
5560
+                flags ^= CL_CHECKFP_PE_FLAG_AUTHENTICODE;
5561
+            } else {
5562
+                cli_dbgmsg("cli_checkfp_pe: expected authenticode data at the end of the file\n");
5563
+                free(exe_sections);
5564
+                if (hashctx)
5565
+                    cl_hash_destroy(hashctx);
5566
+                return CL_EFORMAT;
5567
+            }
5568
+        }
5552 5569
     }
5553 5570
 
5554 5571
 #define hash_chunk(where, size, isStatAble, section) \
... ...
@@ -5614,33 +5663,8 @@ int cli_checkfp_pe(cli_ctx *ctx, uint8_t *authsha1, stats_section_t *hashes, uin
5614 5614
             continue;
5615 5615
 
5616 5616
         hash_chunk(exe_sections[i].raw, exe_sections[i].rsz, 1, i);
5617
-        if (flags & CL_CHECKFP_PE_FLAG_AUTHENTICODE)
5618
-            at += exe_sections[i].rsz;
5619 5617
     }
5620 5618
 
5621
-    while (flags & CL_CHECKFP_PE_FLAG_AUTHENTICODE) {
5622
-        if((size_t)at < fsize) {
5623
-            hlen = fsize - at;
5624
-            if(dirs[4].Size > hlen) {
5625
-                if (flags & CL_CHECKFP_PE_FLAG_STATS) {
5626
-                    flags ^= CL_CHECKFP_PE_FLAG_AUTHENTICODE;
5627
-                    break;
5628
-                } else {
5629
-                    free(exe_sections);
5630
-                    if (hashctx)
5631
-                        cl_hash_destroy(hashctx);
5632
-                    return CL_EFORMAT;
5633
-                }
5634
-            }
5635
-
5636
-            hlen -= dirs[4].Size;
5637
-            hash_chunk(at, hlen, 0, 0);
5638
-            at += hlen;
5639
-        }
5640
-
5641
-        break;
5642
-    } while (0);
5643
-
5644 5619
     free(exe_sections);
5645 5620
 
5646 5621
     if (flags & CL_CHECKFP_PE_FLAG_AUTHENTICODE && hashctx) {
... ...
@@ -5653,13 +5677,30 @@ int cli_checkfp_pe(cli_ctx *ctx, uint8_t *authsha1, stats_section_t *hashes, uin
5653 5653
             cli_dbgmsg("Authenticode: %s\n", shatxt);
5654 5654
         }
5655 5655
 
5656
-        hlen = dirs[4].Size;
5657
-        if(hlen < 8)
5656
+        hlen = EC32(dirs[4].Size);
5657
+
5658
+        if(fmap_readn(map, &cert_hdr, EC32(dirs[4].VirtualAddress), sizeof(cert_hdr)) != sizeof(cert_hdr))
5659
+            return CL_EFORMAT;
5660
+
5661
+        if (EC16(cert_hdr.revision) != WIN_CERT_REV_2) {
5662
+            cli_dbgmsg("cli_checkfp_pe: unsupported authenticode data revision\n");
5658 5663
             return CL_VIRUS;
5664
+        }
5665
+
5666
+        if (EC16(cert_hdr.type) != WIN_CERT_TYPE_PKCS7) {
5667
+            cli_dbgmsg("cli_checkfp_pe: unsupported authenticode data type\n");
5668
+            return CL_VIRUS;
5669
+        }
5670
+
5671
+        if (EC32(cert_hdr.length) != hlen) {
5672
+            cli_dbgmsg("cli_checkfp_pe: unexpected authenticode data length\n");
5673
+            return CL_VIRUS;
5674
+        }
5659 5675
 
5660
-        hlen -= 8;
5676
+        at = EC32(dirs[4].VirtualAddress) + sizeof(cert_hdr);
5677
+        hlen -= sizeof(cert_hdr);
5661 5678
 
5662
-        return asn1_check_mscat((struct cl_engine *)(ctx->engine), map, at + 8, hlen, authsha1);
5679
+        return asn1_check_mscat((struct cl_engine *)(ctx->engine), map, at, hlen, authsha1);
5663 5680
     } else {
5664 5681
         if (hashctx)
5665 5682
             cl_hash_destroy(hashctx);
... ...
@@ -144,6 +144,17 @@ struct pe_image_section_hdr {
144 144
     uint32_t Characteristics;
145 145
 };
146 146
 
147
+#define WIN_CERT_REV_2 0x0200
148
+#define WIN_CERT_TYPE_PKCS7 0x0002
149
+
150
+/** PE authenticode data header
151
+  \group_pe */
152
+struct pe_certificate_hdr {
153
+    uint32_t length; /** length of the certificate data, including the header */
154
+    uint16_t revision;
155
+    uint16_t type;
156
+};
157
+
147 158
 /** Data for the bytecode PE hook
148 159
   \group_pe */
149 160
 struct cli_pe_hook_data {