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
This commit is contained in:
Andrew 2018-08-27 22:53:23 -04:00 committed by Micah Snyder
parent 0a2492de87
commit 18a813afb6
2 changed files with 87 additions and 35 deletions

View file

@ -5405,6 +5405,7 @@ int cli_checkfp_pe(cli_ctx *ctx, uint8_t *authsha1, stats_section_t *hashes, uin
struct pe_image_data_dir *dirs;
fmap_t *map = *ctx->fmap;
void *hashctx=NULL;
struct pe_certificate_hdr cert_hdr;
if (flags & CL_CHECKFP_PE_FLAG_STATS)
if (!(hashes))
@ -5439,8 +5440,14 @@ int cli_checkfp_pe(cli_ctx *ctx, uint8_t *authsha1, stats_section_t *hashes, uin
if(nsections < 1 || nsections > 96)
return CL_EFORMAT;
if(EC16(file_hdr.SizeOfOptionalHeader) < sizeof(struct pe_image_optional_hdr32))
// TODO the pe_image_optional_hdr32 structure includes space for all 16
// data directories, but these might not all exist in a given binary.
// We need to check NumberOfRvaAndSizes instead, and allow through any
// with at least 5 (the security DataDirectory)
if(EC16(file_hdr.SizeOfOptionalHeader) < sizeof(struct pe_image_optional_hdr32)) {
cli_dbgmsg("cli_checkfp_pe: SizeOfOptionalHeader < less than the size expected (%lu)\n", sizeof(struct pe_image_optional_hdr32));
return CL_EFORMAT;
}
at = e_lfanew + sizeof(struct pe_image_file_hdr);
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
dirs = optional_hdr64.DataDirectory;
}
// As an optimization, check the security DataDirectory here and if
// it's less than 8-bytes (and we aren't relying on this code to compute
// the section hashes), bail out
if (EC32(dirs[4].Size) < 8) {
if (flags & CL_CHECKFP_PE_FLAG_STATS) {
/* If stats is enabled, continue parsing the sample */
flags ^= CL_CHECKFP_PE_FLAG_AUTHENTICODE;
} else {
return CL_VIRUS;
}
}
fsize = map->len;
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
if(!exe_sections)
return CL_EMEM;
// TODO I'm not sure why this is necessary since the specification says
// that PointerToRawData is expected to be a multiple of the file
// alignment. Should we report this is as a PE with an error?
for(i = 0; falign!=0x200 && i<nsections; i++) {
/* file alignment fallback mode - blah */
if (falign && section_hdr[i].SizeOfRawData && EC32(section_hdr[i].PointerToRawData)%falign && !(EC32(section_hdr[i].PointerToRawData)%0x200))
falign = 0x200;
}
// TODO Why is this needed? hdr_size should already be rounded up
// to a multiple of the file alignment.
hdr_size = PESALIGN(hdr_size, falign); /* Aligned headers virtual size */
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
}
}
// TODO Why do we fix up these alignments? This shouldn't be needed?
for(i = 0; i < nsections; i++) {
exe_sections[i].rva = PEALIGN(EC32(section_hdr[i].VirtualAddress), valign);
exe_sections[i].vsz = PESALIGN(EC32(section_hdr[i].VirtualSize), valign);
exe_sections[i].raw = PEALIGN(EC32(section_hdr[i].PointerToRawData), falign);
exe_sections[i].rsz = PESALIGN(EC32(section_hdr[i].SizeOfRawData), falign);
// TODO exe_sections[i].ursz is not assigned to (will always be 0)
// Figure out what this is meant to do and ensure that happens
if (!exe_sections[i].vsz && exe_sections[i].rsz)
exe_sections[i].vsz=PESALIGN(exe_sections[i].ursz, valign);
if (exe_sections[i].rsz && fsize>exe_sections[i].raw && !CLI_ISCONTAINED(0, (uint32_t) fsize, exe_sections[i].raw, exe_sections[i].rsz))
exe_sections[i].rsz = fsize - exe_sections[i].raw;
if (exe_sections[i].rsz && exe_sections[i].raw >= fsize) {
if (exe_sections[i].rsz && fsize>exe_sections[i].raw && !CLI_ISCONTAINED(0, (uint32_t) fsize, exe_sections[i].raw, exe_sections[i].rsz)) {
cli_dbgmsg("cli_checkfp_pe: encountered section not fully contained within the file\n");
free(exe_sections);
return CL_EFORMAT;
}
if (exe_sections[i].rsz && exe_sections[i].raw >= fsize) {
cli_dbgmsg("cli_checkfp_pe: encountered section that doesn't exist within the file\n");
free(exe_sections);
return CL_EFORMAT;
}
// TODO These checks aren't needed because the u vars are never assigned (always 0)
// Figure out what this is meant to do and ensure that happens
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) {
free(exe_sections);
return CL_EFORMAT;
@ -5539,16 +5571,33 @@ int cli_checkfp_pe(cli_ctx *ctx, uint8_t *authsha1, stats_section_t *hashes, uin
if (flags & CL_CHECKFP_PE_FLAG_AUTHENTICODE) {
/* Check to see if we have a security section. */
if(!cli_hm_have_size(ctx->engine->hm_fp, CLI_HASH_SHA1, 2) && dirs[4].Size < 8) {
if(!cli_hm_have_size(ctx->engine->hm_fp, CLI_HASH_SHA1, 2) && EC32(dirs[4].Size) < 8) {
if (flags & CL_CHECKFP_PE_FLAG_STATS) {
/* If stats is enabled, continue parsing the sample */
flags ^= CL_CHECKFP_PE_FLAG_AUTHENTICODE;
} else {
free(exe_sections);
if (hashctx)
cl_hash_destroy(hashctx);
return CL_BREAK;
}
}
// Verify that we have all the bytes we expect in the authenticode sig
// and that the certificate table is the last thing in the file
// (according to the MS13-098 bulletin, this is a requirement)
if (fsize != EC32(dirs[4].Size) + EC32(dirs[4].VirtualAddress)) {
if (flags & CL_CHECKFP_PE_FLAG_STATS) {
flags ^= CL_CHECKFP_PE_FLAG_AUTHENTICODE;
} else {
cli_dbgmsg("cli_checkfp_pe: expected authenticode data at the end of the file\n");
free(exe_sections);
if (hashctx)
cl_hash_destroy(hashctx);
return CL_EFORMAT;
}
}
}
#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
continue;
hash_chunk(exe_sections[i].raw, exe_sections[i].rsz, 1, i);
if (flags & CL_CHECKFP_PE_FLAG_AUTHENTICODE)
at += exe_sections[i].rsz;
}
while (flags & CL_CHECKFP_PE_FLAG_AUTHENTICODE) {
if((size_t)at < fsize) {
hlen = fsize - at;
if(dirs[4].Size > hlen) {
if (flags & CL_CHECKFP_PE_FLAG_STATS) {
flags ^= CL_CHECKFP_PE_FLAG_AUTHENTICODE;
break;
} else {
free(exe_sections);
if (hashctx)
cl_hash_destroy(hashctx);
return CL_EFORMAT;
}
}
hlen -= dirs[4].Size;
hash_chunk(at, hlen, 0, 0);
at += hlen;
}
break;
} while (0);
free(exe_sections);
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
cli_dbgmsg("Authenticode: %s\n", shatxt);
}
hlen = dirs[4].Size;
if(hlen < 8)
hlen = EC32(dirs[4].Size);
if(fmap_readn(map, &cert_hdr, EC32(dirs[4].VirtualAddress), sizeof(cert_hdr)) != sizeof(cert_hdr))
return CL_EFORMAT;
if (EC16(cert_hdr.revision) != WIN_CERT_REV_2) {
cli_dbgmsg("cli_checkfp_pe: unsupported authenticode data revision\n");
return CL_VIRUS;
}
hlen -= 8;
if (EC16(cert_hdr.type) != WIN_CERT_TYPE_PKCS7) {
cli_dbgmsg("cli_checkfp_pe: unsupported authenticode data type\n");
return CL_VIRUS;
}
return asn1_check_mscat((struct cl_engine *)(ctx->engine), map, at + 8, hlen, authsha1);
if (EC32(cert_hdr.length) != hlen) {
cli_dbgmsg("cli_checkfp_pe: unexpected authenticode data length\n");
return CL_VIRUS;
}
at = EC32(dirs[4].VirtualAddress) + sizeof(cert_hdr);
hlen -= sizeof(cert_hdr);
return asn1_check_mscat((struct cl_engine *)(ctx->engine), map, at, hlen, authsha1);
} else {
if (hashctx)
cl_hash_destroy(hashctx);

View file

@ -144,6 +144,17 @@ struct pe_image_section_hdr {
uint32_t Characteristics;
};
#define WIN_CERT_REV_2 0x0200
#define WIN_CERT_TYPE_PKCS7 0x0002
/** PE authenticode data header
\group_pe */
struct pe_certificate_hdr {
uint32_t length; /** length of the certificate data, including the header */
uint16_t revision;
uint16_t type;
};
/** Data for the bytecode PE hook
\group_pe */
struct cli_pe_hook_data {