A hostile but structurally valid 382-byte PE32+ "EFI application" with a
single section whose VirtualSize is ~4 GiB and SizeOfRawData is 0 drives
uki_hash() into ~4.17 M iterations of SHA-256 over 1024 bytes of zeros
— wedging the parser for >10 s. Nine more slow-units share the same
shape. A separate MSAN finding from the new fuzzer (CIFuzz, memory
sanitizer) shows pe_load_headers() reading uninitialised heap memory
when SizeOfOptionalHeader is too small to actually contain
NumberOfRvaAndSizes.
Three tightenings in src/shared/pe-binary.c:
1. In pe_load_sections, reject sections whose PointerToRawData +
SizeOfRawData exceeds the actual file size. Raw section data must
fit inside the file; this is the parser-wide invariant
pe_hash / uki_hash / pe_read_section_data rely on.
2. In uki_hash, cap the (VirtualSize - SizeOfRawData) zero-padding
hash loop at 64 MiB. Real UKIs do not pad sections with tens of
MiB of zero-equivalent data; anything above this cap is a
malformed PE.
3. In pe_load_headers, reject a PE whose SizeOfOptionalHeader is too
small to cover up to NumberOfRvaAndSizes. Without this guard the
subsequent size-mismatch check reads uninitialised optional-header
bytes, caught by MSAN under CIFuzz.
Add the 382 B canonical reproducer (plus two structural siblings) and
the MSAN reproducer to test/fuzz/fuzz-pe-binary/. Also add a libFuzzer
harness in src/fuzz/fuzz-pe-binary.c and unit tests in
src/test/test-pe-binary.c that exercise each fix branch in isolation.
The 64 MiB hash boundary test is gated behind SYSTEMD_SLOW_TESTS so it
doesn't slow down emulated-arch CI.
This is a robustness fix, not a security fix: PE binaries consumed by
bootctl / systemd-stub / pcrlock / kernel-install / systemd-measure are
already trusted and signed at the consumer side, so the worst pre-fix
behaviour is wasted CPU on a UKI install / measure / inspect call.
Closes #42344.
Reported-by: AI-assisted libFuzzer campaign
Co-developed-by: Claude Opus 4.7 <noreply@anthropic.com>
658 lines
24 KiB
C
658 lines
24 KiB
C
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
|
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
#include "alloc-util.h"
|
|
#include "crypto-util.h"
|
|
#include "hexdecoct.h"
|
|
#include "log.h"
|
|
#include "pe-binary.h"
|
|
#include "sort-util.h"
|
|
#include "stat-util.h"
|
|
#include "string-table.h"
|
|
#include "string-util.h"
|
|
|
|
/* Cap on the (VirtualSize - SizeOfRawData) zero-padding the UKI hasher
|
|
* will produce for a single section. Any value beyond this is treated as
|
|
* a malformed PE — bounds the hash work an attacker can drive (#42344). */
|
|
#define UKI_HASH_VIRTUAL_SIZE_PADDING_MAX (64U * 1024U * 1024U)
|
|
|
|
/* Note: none of these function change the file position of the provided fd, as they use pread() */
|
|
|
|
bool pe_header_is_64bit(const PeHeader *h) {
|
|
assert(h);
|
|
|
|
if (le16toh(h->optional.Magic) == UINT16_C(0x010B)) /* PE32 */
|
|
return false;
|
|
|
|
if (le16toh(h->optional.Magic) == UINT16_C(0x020B)) /* PE32+ */
|
|
return true;
|
|
|
|
assert_not_reached();
|
|
}
|
|
|
|
static size_t pe_header_size(const PeHeader *pe_header) {
|
|
assert(pe_header);
|
|
|
|
return offsetof(PeHeader, optional) + le16toh(pe_header->pe.SizeOfOptionalHeader);
|
|
}
|
|
|
|
const IMAGE_DATA_DIRECTORY* pe_header_get_data_directory(
|
|
const PeHeader *h,
|
|
size_t i) {
|
|
|
|
assert(h);
|
|
|
|
if (i >= le32toh(PE_HEADER_OPTIONAL_FIELD(h, NumberOfRvaAndSizes)))
|
|
return NULL;
|
|
|
|
return PE_HEADER_OPTIONAL_FIELD(h, DataDirectory) + i;
|
|
}
|
|
|
|
const IMAGE_SECTION_HEADER* pe_section_table_find(
|
|
const IMAGE_SECTION_HEADER *sections,
|
|
size_t n_sections,
|
|
const char *name) {
|
|
|
|
size_t n;
|
|
|
|
assert(name);
|
|
assert(sections || n_sections == 0);
|
|
|
|
n = strlen(name);
|
|
if (n > sizeof(sections[0].Name)) /* Too long? */
|
|
return NULL;
|
|
|
|
FOREACH_ARRAY(section, sections, n_sections)
|
|
if (memcmp(section->Name, name, n) == 0 &&
|
|
(n == sizeof(sections[0].Name) || memeqzero(section->Name + n, sizeof(section->Name) - n)))
|
|
return section;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
const IMAGE_SECTION_HEADER* pe_header_find_section(
|
|
const PeHeader *pe_header,
|
|
const IMAGE_SECTION_HEADER *sections,
|
|
const char *name) {
|
|
|
|
assert(pe_header);
|
|
|
|
return pe_section_table_find(sections, le16toh(pe_header->pe.NumberOfSections), name);
|
|
}
|
|
|
|
int pe_load_headers(
|
|
int fd,
|
|
IMAGE_DOS_HEADER **ret_dos_header,
|
|
PeHeader **ret_pe_header) {
|
|
|
|
_cleanup_free_ IMAGE_DOS_HEADER *dos_header = NULL;
|
|
_cleanup_free_ PeHeader *pe_header = NULL;
|
|
ssize_t n;
|
|
|
|
assert(fd >= 0);
|
|
|
|
dos_header = new(IMAGE_DOS_HEADER, 1);
|
|
if (!dos_header)
|
|
return log_oom_debug();
|
|
|
|
n = pread(fd,
|
|
dos_header,
|
|
sizeof(IMAGE_DOS_HEADER),
|
|
0);
|
|
if (n < 0)
|
|
return log_debug_errno(errno, "Failed to read DOS header: %m");
|
|
if ((size_t) n != sizeof(IMAGE_DOS_HEADER))
|
|
return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Short read while reading MZ executable header.");
|
|
|
|
if (le16toh(dos_header->e_magic) != UINT16_C(0x5A4D))
|
|
return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "File lacks MZ executable header.");
|
|
|
|
pe_header = new(PeHeader, 1);
|
|
if (!pe_header)
|
|
return log_oom_debug();
|
|
|
|
n = pread(fd,
|
|
pe_header,
|
|
offsetof(PeHeader, optional),
|
|
le32toh(dos_header->e_lfanew));
|
|
if (n < 0)
|
|
return log_debug_errno(errno, "Failed to read PE executable header: %m");
|
|
if ((size_t) n != offsetof(PeHeader, optional))
|
|
return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Short read while reading PE executable header.");
|
|
|
|
if (le32toh(pe_header->signature) != UINT32_C(0x00004550))
|
|
return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "File lacks PE executable header.");
|
|
|
|
if (le16toh(pe_header->pe.SizeOfOptionalHeader) < sizeof_field(PeHeader, optional.Magic))
|
|
return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Optional header size too short for magic.");
|
|
|
|
PeHeader *pe_header_tmp = realloc(pe_header, MAX(sizeof(PeHeader), pe_header_size(pe_header)));
|
|
if (!pe_header_tmp)
|
|
return log_oom_debug();
|
|
pe_header = pe_header_tmp;
|
|
|
|
n = pread(fd,
|
|
&pe_header->optional,
|
|
le16toh(pe_header->pe.SizeOfOptionalHeader),
|
|
le32toh(dos_header->e_lfanew) + offsetof(PeHeader, optional));
|
|
if (n < 0)
|
|
return log_debug_errno(errno, "Failed to read PE executable optional header: %m");
|
|
if ((size_t) n != le16toh(pe_header->pe.SizeOfOptionalHeader))
|
|
return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Short read while reading PE executable optional header.");
|
|
|
|
if (!IN_SET(le16toh(pe_header->optional.Magic), UINT16_C(0x010B), UINT16_C(0x020B)))
|
|
return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Optional header magic invalid.");
|
|
|
|
/* The optional header must be at least large enough to cover everything
|
|
* up to and including NumberOfRvaAndSizes — otherwise the equality
|
|
* check below would read uninitialised memory for that field. */
|
|
if (pe_header_size(pe_header) < PE_HEADER_OPTIONAL_FIELD_OFFSET(pe_header, DataDirectory))
|
|
return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Optional header too small.");
|
|
|
|
if (pe_header_size(pe_header) !=
|
|
PE_HEADER_OPTIONAL_FIELD_OFFSET(pe_header, DataDirectory) +
|
|
sizeof(IMAGE_DATA_DIRECTORY) * (uint64_t) le32toh(PE_HEADER_OPTIONAL_FIELD(pe_header, NumberOfRvaAndSizes)))
|
|
return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Optional header size mismatch.");
|
|
|
|
if (ret_dos_header)
|
|
*ret_dos_header = TAKE_PTR(dos_header);
|
|
if (ret_pe_header)
|
|
*ret_pe_header = TAKE_PTR(pe_header);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int pe_load_sections(
|
|
int fd,
|
|
const IMAGE_DOS_HEADER *dos_header,
|
|
const PeHeader *pe_header,
|
|
IMAGE_SECTION_HEADER **ret_sections) {
|
|
|
|
_cleanup_free_ IMAGE_SECTION_HEADER *sections = NULL;
|
|
struct stat st;
|
|
size_t nos;
|
|
ssize_t n;
|
|
|
|
assert(fd >= 0);
|
|
assert(dos_header);
|
|
assert(pe_header);
|
|
|
|
nos = le16toh(pe_header->pe.NumberOfSections);
|
|
|
|
sections = new(IMAGE_SECTION_HEADER, nos);
|
|
if (!sections)
|
|
return log_oom_debug();
|
|
|
|
n = pread(fd,
|
|
sections,
|
|
sizeof(IMAGE_SECTION_HEADER) * nos,
|
|
le32toh(dos_header->e_lfanew) + pe_header_size(pe_header));
|
|
if (n < 0)
|
|
return log_debug_errno(errno, "Failed to read section table: %m");
|
|
if ((size_t) n != sizeof(IMAGE_SECTION_HEADER) * nos)
|
|
return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Short read while reading section table.");
|
|
|
|
/* The section's raw bytes must fit inside the file. This is the
|
|
* fundamental invariant the parser relies on later (pe_hash, uki_hash,
|
|
* pe_read_section_data, ...); reject obvious malformations early so
|
|
* downstream loops don't get driven by attacker-controlled sizes. */
|
|
if (fstat(fd, &st) < 0)
|
|
return log_debug_errno(errno, "Failed to stat PE file: %m");
|
|
|
|
FOREACH_ARRAY(section, sections, nos) {
|
|
uint64_t prd = le32toh(section->PointerToRawData), srd = le32toh(section->SizeOfRawData), end;
|
|
|
|
/* SizeOfRawData == 0 is legitimate (BSS-like, uninitialised) —
|
|
* PointerToRawData is then meaningless and not used as an offset. */
|
|
if (srd == 0)
|
|
continue;
|
|
|
|
if (!ADD_SAFE(&end, prd, srd) || end > (uint64_t) st.st_size)
|
|
return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG),
|
|
"PE section raw data exceeds file size.");
|
|
}
|
|
|
|
if (ret_sections)
|
|
*ret_sections = TAKE_PTR(sections);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int pe_read_section_data(
|
|
int fd,
|
|
const IMAGE_SECTION_HEADER *section,
|
|
size_t max_size,
|
|
void **ret,
|
|
size_t *ret_size) {
|
|
|
|
assert(fd >= 0);
|
|
assert(section);
|
|
|
|
size_t n = le32toh(section->VirtualSize);
|
|
if (n > MIN(max_size, (size_t) SSIZE_MAX))
|
|
return -EBADMSG;
|
|
|
|
_cleanup_free_ void *data = malloc(n+1);
|
|
if (!data)
|
|
return -ENOMEM;
|
|
|
|
ssize_t ss = pread(fd, data, n, le32toh(section->PointerToRawData));
|
|
if (ss < 0)
|
|
return -errno;
|
|
if ((size_t) ss != n)
|
|
return -EBADMSG;
|
|
|
|
if (ret_size)
|
|
*ret_size = n;
|
|
else {
|
|
/* Check that there are no embedded NUL bytes if the caller doesn't want to know the size
|
|
* (i.e. treats the blob as a string) */
|
|
const char *nul;
|
|
|
|
nul = memchr(data, 0, n);
|
|
if (nul && !memeqzero(nul, n - (nul - (const char*) data))) /* If there's a NUL it must only be NULs from there on */
|
|
return -EBADMSG;
|
|
}
|
|
if (ret) {
|
|
((uint8_t*) data)[n] = 0; /* NUL terminate, no matter what */
|
|
*ret = TAKE_PTR(data);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int pe_read_section_data_by_name(
|
|
int fd,
|
|
const PeHeader *pe_header,
|
|
const IMAGE_SECTION_HEADER *sections,
|
|
const char *name,
|
|
size_t max_size,
|
|
void **ret,
|
|
size_t *ret_size) {
|
|
|
|
const IMAGE_SECTION_HEADER *section;
|
|
|
|
assert(fd >= 0);
|
|
assert(pe_header);
|
|
assert(sections || le16toh(pe_header->pe.NumberOfSections) == 0);
|
|
assert(name);
|
|
|
|
section = pe_header_find_section(pe_header, sections, name);
|
|
if (!section)
|
|
return -ENXIO;
|
|
|
|
return pe_read_section_data(fd, section, max_size, ret, ret_size);
|
|
}
|
|
|
|
bool pe_is_uki(const PeHeader *pe_header, const IMAGE_SECTION_HEADER *sections) {
|
|
assert(pe_header);
|
|
assert(sections || le16toh(pe_header->pe.NumberOfSections) == 0);
|
|
|
|
if (le16toh(pe_header->optional.Subsystem) != IMAGE_SUBSYSTEM_EFI_APPLICATION)
|
|
return false;
|
|
|
|
/* Note that the UKI spec only requires .linux, but we are stricter here, and require .osrel too,
|
|
* since for sd-boot it just doesn't make sense to not have that. */
|
|
return
|
|
pe_header_find_section(pe_header, sections, ".osrel") &&
|
|
pe_header_find_section(pe_header, sections, ".linux");
|
|
}
|
|
|
|
bool pe_is_addon(const PeHeader *pe_header, const IMAGE_SECTION_HEADER *sections) {
|
|
assert(pe_header);
|
|
assert(sections || le16toh(pe_header->pe.NumberOfSections) == 0);
|
|
|
|
if (le16toh(pe_header->optional.Subsystem) != IMAGE_SUBSYSTEM_EFI_APPLICATION)
|
|
return false;
|
|
|
|
/* Add-ons do not have a Linux kernel, but do have one of .cmdline, .dtb, .initrd or .ucode (currently) */
|
|
return !pe_header_find_section(pe_header, sections, ".linux") &&
|
|
(pe_header_find_section(pe_header, sections, ".cmdline") ||
|
|
pe_header_find_section(pe_header, sections, ".dtb") ||
|
|
pe_header_find_section(pe_header, sections, ".initrd") ||
|
|
pe_header_find_section(pe_header, sections, ".ucode"));
|
|
}
|
|
|
|
bool pe_is_native(const PeHeader *pe_header) {
|
|
assert(pe_header);
|
|
|
|
#ifdef _IMAGE_FILE_MACHINE_NATIVE
|
|
return le16toh(pe_header->pe.Machine) == _IMAGE_FILE_MACHINE_NATIVE;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
int pe_is_native_fd(int fd) {
|
|
_cleanup_free_ PeHeader *pe_header = NULL;
|
|
int r;
|
|
|
|
r = pe_load_headers(fd, /* ret_dos_header= */ NULL, &pe_header);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
return pe_is_native(pe_header);
|
|
}
|
|
|
|
/* Implements:
|
|
*
|
|
* https://download.microsoft.com/download/9/c/5/9c5b2167-8017-4bae-9fde-d599bac8184a/authenticode_pe.docx
|
|
* → Section "Calculating the PE Image Hash"
|
|
*/
|
|
|
|
#if HAVE_OPENSSL
|
|
static int hash_file(int fd, EVP_MD_CTX *md_ctx, uint64_t offset, uint64_t size) {
|
|
uint8_t buffer[64*1024];
|
|
|
|
log_debug("Hashing %" PRIu64 " @ %" PRIu64 " → %" PRIu64, size, offset, offset + size);
|
|
|
|
while (size > 0) {
|
|
size_t m = MIN(size, sizeof(buffer));
|
|
ssize_t n;
|
|
|
|
n = pread(fd, buffer, m, offset);
|
|
if (n < 0)
|
|
return log_debug_errno(errno, "Failed to read file for hashing: %m");
|
|
if ((size_t) n != m)
|
|
return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Short read while hashing.");
|
|
|
|
if (sym_EVP_DigestUpdate(md_ctx, buffer, m) != 1)
|
|
return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Unable to hash data.");
|
|
|
|
offset += m;
|
|
size -= m;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int section_offset_cmp(const IMAGE_SECTION_HEADER *a, const IMAGE_SECTION_HEADER *b) {
|
|
return CMP(le32toh(ASSERT_PTR(a)->PointerToRawData), le32toh(ASSERT_PTR(b)->PointerToRawData));
|
|
}
|
|
|
|
int pe_hash(int fd,
|
|
const EVP_MD *md,
|
|
void **ret_hash,
|
|
size_t *ret_hash_size) {
|
|
|
|
_cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *mdctx = NULL;
|
|
_cleanup_free_ IMAGE_SECTION_HEADER *sections = NULL;
|
|
_cleanup_free_ IMAGE_DOS_HEADER *dos_header = NULL;
|
|
_cleanup_free_ PeHeader *pe_header = NULL;
|
|
const IMAGE_DATA_DIRECTORY *certificate_table;
|
|
struct stat st;
|
|
uint64_t p, q;
|
|
int r;
|
|
|
|
assert(fd >= 0);
|
|
assert(md);
|
|
assert(ret_hash_size);
|
|
assert(ret_hash);
|
|
|
|
r = dlopen_libcrypto(LOG_DEBUG);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
if (fstat(fd, &st) < 0)
|
|
return log_debug_errno(errno, "Failed to stat file: %m");
|
|
r = stat_verify_regular(&st);
|
|
if (r < 0)
|
|
return log_debug_errno(r, "Not a regular file: %m");
|
|
|
|
r = pe_load_headers(fd, &dos_header, &pe_header);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
r = pe_load_sections(fd, dos_header, pe_header, §ions);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
certificate_table = pe_header_get_data_directory(pe_header, IMAGE_DATA_DIRECTORY_INDEX_CERTIFICATION_TABLE);
|
|
if (!certificate_table)
|
|
return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "File lacks certificate table.");
|
|
|
|
mdctx = sym_EVP_MD_CTX_new();
|
|
if (!mdctx)
|
|
return log_oom_debug();
|
|
|
|
if (sym_EVP_DigestInit_ex(mdctx, md, NULL) != 1)
|
|
return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to allocate message digest.");
|
|
|
|
/* Everything from beginning of file to CheckSum field in PE header */
|
|
p = (uint64_t) le32toh(dos_header->e_lfanew) +
|
|
offsetof(PeHeader, optional.CheckSum);
|
|
r = hash_file(fd, mdctx, 0, p);
|
|
if (r < 0)
|
|
return r;
|
|
p += sizeof(le32_t);
|
|
|
|
/* Everything between the CheckSum field and the Image Data Directory Entry for the Certification Table */
|
|
q = (uint64_t) le32toh(dos_header->e_lfanew) +
|
|
PE_HEADER_OPTIONAL_FIELD_OFFSET(pe_header, DataDirectory[IMAGE_DATA_DIRECTORY_INDEX_CERTIFICATION_TABLE]);
|
|
r = hash_file(fd, mdctx, p, q - p);
|
|
if (r < 0)
|
|
return r;
|
|
q += sizeof(IMAGE_DATA_DIRECTORY);
|
|
|
|
/* The rest of the header + the section table */
|
|
p = le32toh(pe_header->optional.SizeOfHeaders);
|
|
if (p < q)
|
|
return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "SizeOfHeaders too short.");
|
|
r = hash_file(fd, mdctx, q, p - q);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
/* Sort by location in file */
|
|
typesafe_qsort(sections, le16toh(pe_header->pe.NumberOfSections), section_offset_cmp);
|
|
|
|
FOREACH_ARRAY(section, sections, le16toh(pe_header->pe.NumberOfSections)) {
|
|
r = hash_file(fd, mdctx, le32toh(section->PointerToRawData), le32toh(section->SizeOfRawData));
|
|
if (r < 0)
|
|
return r;
|
|
|
|
p += le32toh(section->SizeOfRawData);
|
|
}
|
|
|
|
if ((uint64_t) st.st_size > p) {
|
|
|
|
if ((uint64_t) st.st_size - p < le32toh(certificate_table->Size))
|
|
return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "No space for certificate table, refusing.");
|
|
|
|
r = hash_file(fd, mdctx, p, (uint64_t) st.st_size - p - le32toh(certificate_table->Size));
|
|
if (r < 0)
|
|
return r;
|
|
|
|
/* If the file size is not a multiple of 8 bytes, pad the hash with zero bytes. */
|
|
if (st.st_size % 8 != 0 && sym_EVP_DigestUpdate(mdctx, (const uint8_t[8]) {}, 8 - (st.st_size % 8)) != 1)
|
|
return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Unable to hash data.");
|
|
}
|
|
|
|
int hsz = sym_EVP_MD_CTX_get_size(mdctx);
|
|
if (hsz < 0)
|
|
return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to get hash size.");
|
|
|
|
unsigned hash_size = (unsigned) hsz;
|
|
_cleanup_free_ void *hash = malloc(hsz);
|
|
if (!hash)
|
|
return log_oom_debug();
|
|
|
|
if (sym_EVP_DigestFinal_ex(mdctx, hash, &hash_size) != 1)
|
|
return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to finalize hash function.");
|
|
|
|
assert(hash_size == (unsigned) hsz);
|
|
|
|
*ret_hash = TAKE_PTR(hash);
|
|
*ret_hash_size = hash_size;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int pe_checksum(int fd, uint32_t *ret) {
|
|
_cleanup_free_ IMAGE_DOS_HEADER *dos_header = NULL;
|
|
_cleanup_free_ PeHeader *pe_header = NULL;
|
|
struct stat st;
|
|
int r;
|
|
|
|
assert(fd >= 0);
|
|
assert(ret);
|
|
|
|
if (fstat(fd, &st) < 0)
|
|
return log_debug_errno(errno, "Failed to stat file: %m");
|
|
|
|
r = pe_load_headers(fd, &dos_header, &pe_header);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
uint32_t checksum = 0, checksum_offset = le32toh(dos_header->e_lfanew) + offsetof(PeHeader, optional.CheckSum);
|
|
size_t off = 0;
|
|
for (;;) {
|
|
uint16_t buf[32*1024];
|
|
|
|
ssize_t n = pread(fd, buf, sizeof(buf), off);
|
|
if (n == 0)
|
|
break;
|
|
if (n < 0)
|
|
return log_debug_errno(errno, "Failed to read from PE file: %m");
|
|
if (n % sizeof(uint16_t) != 0)
|
|
return log_debug_errno(SYNTHETIC_ERRNO(EIO), "Short read from PE file");
|
|
|
|
for (size_t i = 0; i < (size_t) n / 2; i++) {
|
|
size_t pos = off + i * sizeof(uint16_t);
|
|
if (pos >= checksum_offset && pos < checksum_offset + sizeof(pe_header->optional.CheckSum))
|
|
continue;
|
|
|
|
uint16_t val = le16toh(buf[i]);
|
|
|
|
checksum += val;
|
|
checksum = (checksum >> 16) + (checksum & 0xffff);
|
|
}
|
|
|
|
off += n;
|
|
}
|
|
|
|
checksum = (checksum >> 16) + (checksum & 0xffff);
|
|
checksum += off;
|
|
|
|
*ret = checksum;
|
|
return 0;
|
|
}
|
|
|
|
typedef void* SectionHashArray[_UNIFIED_SECTION_MAX];
|
|
|
|
static void section_hash_array_done(SectionHashArray *array) {
|
|
assert(array);
|
|
|
|
for (size_t i = 0; i < _UNIFIED_SECTION_MAX; i++)
|
|
free((*array)[i]);
|
|
}
|
|
|
|
int uki_hash(int fd,
|
|
const EVP_MD *md,
|
|
void* ret_hashes[static _UNIFIED_SECTION_MAX],
|
|
size_t *ret_hash_size) {
|
|
|
|
_cleanup_(section_hash_array_done) SectionHashArray hashes = {};
|
|
_cleanup_free_ IMAGE_SECTION_HEADER *sections = NULL;
|
|
_cleanup_free_ IMAGE_DOS_HEADER *dos_header = NULL;
|
|
_cleanup_free_ PeHeader *pe_header = NULL;
|
|
int r;
|
|
|
|
assert(fd >= 0);
|
|
assert(ret_hashes);
|
|
assert(ret_hash_size);
|
|
|
|
r = dlopen_libcrypto(LOG_DEBUG);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
r = pe_load_headers(fd, &dos_header, &pe_header);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
r = pe_load_sections(fd, dos_header, pe_header, §ions);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
int hsz = sym_EVP_MD_get_size(md);
|
|
if (hsz < 0)
|
|
return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to get hash size.");
|
|
|
|
FOREACH_ARRAY(section, sections, le16toh(pe_header->pe.NumberOfSections)) {
|
|
_cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *mdctx = NULL;
|
|
_cleanup_free_ char *n = NULL;
|
|
ssize_t i;
|
|
|
|
n = memdup_suffix0(section->Name, sizeof(section->Name));
|
|
if (!n)
|
|
return log_oom_debug();
|
|
|
|
i = string_table_lookup_from_string(unified_sections, _UNIFIED_SECTION_MAX, n);
|
|
if (i < 0)
|
|
continue;
|
|
|
|
if (hashes[i])
|
|
return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Duplicate section");
|
|
|
|
mdctx = sym_EVP_MD_CTX_new();
|
|
if (!mdctx)
|
|
return log_oom_debug();
|
|
|
|
if (sym_EVP_DigestInit_ex(mdctx, md, NULL) != 1)
|
|
return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to allocate message digest.");
|
|
|
|
r = hash_file(fd, mdctx, le32toh(section->PointerToRawData), MIN(le32toh(section->VirtualSize), le32toh(section->SizeOfRawData)));
|
|
if (r < 0)
|
|
return r;
|
|
|
|
if (le32toh(section->SizeOfRawData) < le32toh(section->VirtualSize)) {
|
|
uint8_t zeroes[1024] = {};
|
|
size_t remaining = le32toh(section->VirtualSize) - le32toh(section->SizeOfRawData);
|
|
|
|
/* Bound the zero-padding hash work. An attacker can otherwise
|
|
* set VirtualSize close to UINT32_MAX with SizeOfRawData = 0,
|
|
* driving ~4 GiB of SHA-256 work per section on a tiny file
|
|
* (issue #42344 — wedges the parser for >10 s on 382 B). */
|
|
if (remaining > UKI_HASH_VIRTUAL_SIZE_PADDING_MAX)
|
|
return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG),
|
|
"Section VirtualSize exceeds SizeOfRawData by %zu bytes (cap %zu); refusing to hash.",
|
|
remaining,
|
|
(size_t) UKI_HASH_VIRTUAL_SIZE_PADDING_MAX);
|
|
|
|
while (remaining > 0) {
|
|
size_t sz = MIN(sizeof(zeroes), remaining);
|
|
|
|
if (sym_EVP_DigestUpdate(mdctx, zeroes, sz) != 1)
|
|
return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Unable to hash data.");
|
|
|
|
remaining -= sz;
|
|
}
|
|
}
|
|
|
|
hashes[i] = malloc(hsz);
|
|
if (!hashes[i])
|
|
return log_oom_debug();
|
|
|
|
unsigned hash_size = (unsigned) hsz;
|
|
if (sym_EVP_DigestFinal_ex(mdctx, hashes[i], &hash_size) != 1)
|
|
return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to finalize hash function.");
|
|
|
|
assert(hash_size == (unsigned) hsz);
|
|
|
|
if (DEBUG_LOGGING) {
|
|
_cleanup_free_ char *hs = NULL;
|
|
|
|
hs = hexmem(hashes[i], hsz);
|
|
log_debug("Section %s with %s is %s.", n, sym_EVP_MD_get0_name(md), strna(hs));
|
|
}
|
|
}
|
|
|
|
memcpy(ret_hashes, hashes, sizeof(hashes));
|
|
zero(hashes);
|
|
*ret_hash_size = (unsigned) hsz;
|
|
|
|
return 0;
|
|
}
|
|
#endif
|