From c4b1b2383240aac1a05d22c5742207e78a79e52d Mon Sep 17 00:00:00 2001 From: Pete Batard Date: Tue, 13 Jun 2023 17:57:34 +0200 Subject: [PATCH] [hash] add PE256 hash functions * Mostly copied from U-Boot's https://github.com/u-boot/u-boot/blob/master/lib/efi_loader/efi_image_loader.c --- src/hash.c | 319 +++++++++++++++++++++++++++++++++++++++++++++++++- src/license.h | 12 +- src/rufus.c | 8 +- src/rufus.h | 1 + src/rufus.rc | 10 +- 5 files changed, 335 insertions(+), 15 deletions(-) diff --git a/src/hash.c b/src/hash.c index 363bcb99..0dbb34c6 100644 --- a/src/hash.c +++ b/src/hash.c @@ -7,6 +7,7 @@ * Copyright © 2002-2015 Wei Dai & Igor Pavlov * Copyright © 2015-2023 Pete Batard * Copyright © 2022 Jeffrey Walton + * Copyright © 2016 Alexander Graf * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,6 +30,8 @@ * * SHA-512 modified from LibTomCrypt - Public Domain * + * PE256 modified from u-boot's efi_image_loader.c - GPLv2.0+ + * * CPU accelerated SHA code taken from SHA-Intrinsics - Public Domain * * MD5 code from various public domain sources sharing the following @@ -1469,7 +1472,7 @@ hash_init_t *hash_init[HASH_MAX] = { md5_init, sha1_init , sha256_init, sha512_i hash_write_t *hash_write[HASH_MAX] = { md5_write, sha1_write , sha256_write, sha512_write }; hash_final_t *hash_final[HASH_MAX] = { md5_final, sha1_final , sha256_final, sha512_final }; -// Compute an individual hash without threading or buffering, for a single file +/* Compute an individual hash without threading or buffering, for a single file */ BOOL HashFile(const unsigned type, const char* path, uint8_t* hash) { BOOL r = FALSE; @@ -1511,6 +1514,318 @@ out: return r; } +/* A part of an image, used for hashing */ +struct image_region { + const uint8_t* data; + uint32_t size; +}; + +/** + * struct efi_image_regions - A list of memory regions + * + * @max: Maximum number of regions + * @num: Number of regions + * @reg: Array of regions + */ +struct efi_image_regions { + int max; + int num; + struct image_region reg[]; +}; + +/** + * efi_image_region_add() - add an entry of region + * @regs: Pointer to array of regions + * @start: Start address of region (included) + * @end: End address of region (excluded) + * @nocheck: Flag against overlapped regions + * + * Take one entry of region \[@start, @end\[ and insert it into the list. + * + * * If @nocheck is false, the list will be sorted ascending by address. + * Overlapping entries will not be allowed. + * + * * If @nocheck is true, the list will be sorted ascending by sequence + * of adding the entries. Overlapping is allowed. + * + * Return: TRUE on success, FALSE on error + */ +BOOL efi_image_region_add(struct efi_image_regions* regs, + const void* start, const void* end, int nocheck) +{ + struct image_region* reg; + int i, j; + + if (regs->num >= regs->max) { + uprintf("%s: no more room for regions", __func__); + return FALSE; + } + + if (end < start) + return FALSE; + + for (i = 0; i < regs->num; i++) { + reg = ®s->reg[i]; + if (nocheck) + continue; + + /* new data after registered region */ + if ((uint8_t*)start >= reg->data + reg->size) + continue; + + /* new data preceding registered region */ + if ((uint8_t*)end <= reg->data) { + for (j = regs->num - 1; j >= i; j--) + memcpy(®s->reg[j + 1], ®s->reg[j], + sizeof(*reg)); + break; + } + + /* new data overlapping registered region */ + uprintf("%s: new region already part of another", __func__); + return FALSE; + } + + reg = ®s->reg[i]; + reg->data = start; + reg->size = (uint32_t)((uintptr_t)end - (uintptr_t)start); + regs->num++; + + return TRUE; +} + +/** + * cmp_pe_section() - compare virtual addresses of two PE image sections + * @arg1: Pointer to pointer to first section header + * @arg2: Pointer to pointer to second section header + * + * Compare the virtual addresses of two sections of an portable executable. + * The arguments are defined as const void * to allow usage with qsort(). + * + * Return: -1 if the virtual address of arg1 is less than that of arg2, + * 0 if the virtual addresses are equal, 1 if the virtual address + * of arg1 is greater than that of arg2. + */ +static int cmp_pe_section(const void* arg1, const void* arg2) +{ + const IMAGE_SECTION_HEADER* section1, * section2; + + section1 = *((const IMAGE_SECTION_HEADER**)arg1); + section2 = *((const IMAGE_SECTION_HEADER**)arg2); + + if (section1->VirtualAddress < section2->VirtualAddress) + return -1; + else if (section1->VirtualAddress == section2->VirtualAddress) + return 0; + else + return 1; +} + +/** + * efi_image_parse() - parse a PE image + * @efi: Pointer to image + * @len: Size of @efi + * @regp: Pointer to a list of regions + * + * Parse image binary in PE32(+) format, assuming that sanity of PE image + * has been checked by a caller. + * + * Return: TRUE on success, FALSE on error + */ +BOOL efi_image_parse(uint8_t* efi, size_t len, struct efi_image_regions** regp) +{ + struct efi_image_regions* regs; + IMAGE_DOS_HEADER* dos; + IMAGE_NT_HEADERS32* nt; + IMAGE_SECTION_HEADER *sections, **sorted; + int num_regions, num_sections, i; + DWORD ctidx = IMAGE_DIRECTORY_ENTRY_SECURITY; + uint32_t align, size, authsz; + size_t bytes_hashed; + + dos = (void*)efi; + nt = (void*)(efi + dos->e_lfanew); + + /* + * Count maximum number of regions to be digested. + * We don't have to have an exact number here. + * See efi_image_region_add()'s in parsing below. + */ + num_regions = 3; /* for header */ + num_regions += nt->FileHeader.NumberOfSections; + num_regions++; /* for extra */ + + regs = calloc(sizeof(*regs) + sizeof(struct image_region) * num_regions, 1); + if (!regs) + goto err; + regs->max = num_regions; + + /* + * Collect data regions for hash calculation + * 1. File headers + */ + if (nt->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) { + IMAGE_NT_HEADERS64* nt64 = (void*)nt; + IMAGE_OPTIONAL_HEADER64* opt = &nt64->OptionalHeader; + + /* Skip CheckSum */ + efi_image_region_add(regs, efi, &opt->CheckSum, 0); + if (nt64->OptionalHeader.NumberOfRvaAndSizes <= ctidx) { + efi_image_region_add(regs, + &opt->Subsystem, + efi + opt->SizeOfHeaders, 0); + } else { + /* Skip Certificates Table */ + efi_image_region_add(regs, + &opt->Subsystem, + &opt->DataDirectory[ctidx], 0); + efi_image_region_add(regs, + &opt->DataDirectory[ctidx] + 1, + efi + opt->SizeOfHeaders, 0); + + authsz = opt->DataDirectory[ctidx].Size; + } + + bytes_hashed = opt->SizeOfHeaders; + align = opt->FileAlignment; + } else if (nt->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) { + IMAGE_OPTIONAL_HEADER32* opt = &nt->OptionalHeader; + + /* Skip CheckSum */ + efi_image_region_add(regs, efi, &opt->CheckSum, 0); + if (nt->OptionalHeader.NumberOfRvaAndSizes <= ctidx) { + efi_image_region_add(regs, + &opt->Subsystem, + efi + opt->SizeOfHeaders, 0); + } else { + /* Skip Certificates Table */ + efi_image_region_add(regs, &opt->Subsystem, + &opt->DataDirectory[ctidx], 0); + efi_image_region_add(regs, + &opt->DataDirectory[ctidx] + 1, + efi + opt->SizeOfHeaders, 0); + + authsz = opt->DataDirectory[ctidx].Size; + } + + bytes_hashed = opt->SizeOfHeaders; + align = opt->FileAlignment; + } else { + uprintf("%s: Invalid optional header magic %x", __func__, + nt->OptionalHeader.Magic); + goto err; + } + + /* 2. Sections */ + num_sections = nt->FileHeader.NumberOfSections; + sections = (void*)((uint8_t*)&nt->OptionalHeader + + nt->FileHeader.SizeOfOptionalHeader); + sorted = calloc(sizeof(IMAGE_SECTION_HEADER*), num_sections); + if (!sorted) { + uprintf("%s: Out of memory", __func__); + goto err; + } + + /* + * Make sure the section list is in ascending order. + */ + for (i = 0; i < num_sections; i++) + sorted[i] = §ions[i]; + qsort(sorted, num_sections, sizeof(sorted[0]), cmp_pe_section); + + for (i = 0; i < num_sections; i++) { + if (!sorted[i]->SizeOfRawData) + continue; + + size = (sorted[i]->SizeOfRawData + align - 1) & ~(align - 1); + efi_image_region_add(regs, efi + sorted[i]->PointerToRawData, + efi + sorted[i]->PointerToRawData + size, 0); + //uprintf("section[%d](%s): raw: 0x%x-0x%x, virt: %x-%x", + // i, sorted[i]->Name, + // sorted[i]->PointerToRawData, + // sorted[i]->PointerToRawData + size, + // sorted[i]->VirtualAddress, + // sorted[i]->VirtualAddress + // + sorted[i]->Misc.VirtualSize); + + bytes_hashed += size; + } + free(sorted); + + /* 3. Extra data excluding Certificates Table */ + if (bytes_hashed + authsz < len) { + //uprintf("extra data for hash: %zu", + // len - (bytes_hashed + authsz)); + efi_image_region_add(regs, efi + bytes_hashed, + efi + len - authsz, 0); + } + + *regp = regs; + return TRUE; + +err: + free(regs); + return FALSE; +} + +/* + * Compute the PE256 (a.k.a. Applocker SHA256) hash of a single EFI executable. + * This is a SHA-256 hash applied to only specific parts of a PE binary. + * See https://security.stackexchange.com/a/199627/270178. + * Oh, and you'd think that Windows's ImageGetDigestStream() API could be used + * for some part of this but you'd be very, very wrong since the PE sections it + * feeds to the hash function does include the PE header checksum field... + */ +BOOL PE256File(const char* path, uint8_t* hash) +{ + BOOL r = FALSE; + HASH_CONTEXT hash_ctx = { {0} }; + int i; + uint32_t rb; + uint8_t* buf = NULL; + struct __stat64 stat64 = { 0 }; + struct efi_image_regions* regs = NULL; + + if ((path == NULL) || (hash == NULL)) + goto out; + + /* Filter anything that would be out of place as a EFI bootloader */ + if (_stat64U(path, &stat64) != 0) { + uprintf("Could not open '%s", path); + goto out; + } + if ((stat64.st_size < 1 * KB) || (stat64.st_size > 64 * MB)) { + uprintf("'%s' is either too small or too large for PE-256", path); + goto out; + } + + /* Read the executable into a memory buffer */ + rb = read_file(path, &buf); + if (rb < 1 * KB) + goto out; + + /* Isolate the PE sections to hash */ + if (!efi_image_parse(buf, rb, ®s)) + goto out; + + /* Hash the relevant PE data */ + sha256_init(&hash_ctx); + for (i = 0; i < regs->num; i++) + sha256_write(&hash_ctx, regs->reg[i].data, regs->reg[i].size); + sha256_final(&hash_ctx); + + memcpy(hash, hash_ctx.buf, SHA256_HASHSIZE); + r = TRUE; + +out: + free(regs); + free(buf); + return r; +} + +/* + * Compute the hash of a single buffer. + */ BOOL HashBuffer(const unsigned type, const uint8_t* buf, const size_t len, uint8_t* hash) { BOOL r = FALSE; @@ -1603,7 +1918,7 @@ INT_PTR CALLBACK HashCallback(HWND hDlg, UINT message, WPARAM wParam, LPARAM lPa return (INT_PTR)FALSE; } -// Individual thread that computes one of MD5, SHA1, SHA256 or SHA512 in parallel +/* Individual thread that computes one of MD5, SHA1, SHA256 or SHA512 in parallel */ DWORD WINAPI IndividualHashThread(void* param) { HASH_CONTEXT hash_ctx = { {0} }; // There's a memset in hash_init, but static analyzers still bug us diff --git a/src/license.h b/src/license.h index 47462ed1..884f8882 100644 --- a/src/license.h +++ b/src/license.h @@ -114,22 +114,26 @@ const char* additional_copyrights = "https://kolibrios.org/\\line\n" "GNU General Public License (GPL) v2 or later\\line\n" "\\line\n" -"MD5 hash by Ron Rivest, Colin Plumb et al.\\line\n" +"MD5 digest functions by Ron Rivest, Colin Plumb et al.\\line\n" "Public Domain\\line\n" "\\line\n" -"SHA-1 digest functions from GnuPG:\\line\n" +"SHA1 digest functions from GnuPG:\\line\n" "https://www.gnupg.org/\\line\n" "GNU General Public License (GPL) v3 or later\\line\n" "\\line\n" -"SHA-256 digest functions 7-zip by Igor Pavlov and Crypto++ by Wei Dai:\\line\n" +"SHA256 digest functions from 7-zip by Igor Pavlov and Crypto++ by Wei Dai:\\line\n" "https://7-zip.org/\\line\n" "https://github.com/weidai11/cryptopp/\\line\n" "Public Domain\\line\n" "\\line\n" -"SHA-512 digest functions from libtomcrypt:\\line\n" +"SHA512 digest functions from libtomcrypt:\\line\n" "https://github.com/libtom/libtomcrypt\\line\n" "Public Domain\\line\n" "\\line\n" +"PE256 digest functions from U-Boot:\\line\n" +"https://github.com/u-boot/u-boot\\line\n" +"GNU General Public License (GPL) v2 or later\\line\n" +"\\line\n" "About and License dialogs inspired by WinSCP by Martin Prikryl\\line\n" "https://winscp.net/\\line\n" "GNU General Public License (GPL) v3 or later\\line\n" diff --git a/src/rufus.c b/src/rufus.c index ebd12dbe..6d7cea26 100755 --- a/src/rufus.c +++ b/src/rufus.c @@ -3859,10 +3859,10 @@ extern int TestHashes(void); // Ctrl-T => Alternate Test mode that doesn't require a full rebuild if ((ctrl_without_focus || ((GetKeyState(VK_CONTROL) & 0x8000) && (msg.message == WM_KEYDOWN))) && (msg.wParam == 'T')) { -// uint8_t sum[32] = { 0 }; -// PE256("D:\\Incoming\\bootx64.efi", sum); -// DumpBufferHex(sum, 32); - TestHashes(); + uint8_t sum[32] = { 0 }; + PE256File("C:\\Projects\\rufus\\bootx64.efi", sum); + DumpBufferHex(sum, 32); +// TestHashes(); continue; } #endif diff --git a/src/rufus.h b/src/rufus.h index 0717b1d7..5e1d4955 100644 --- a/src/rufus.h +++ b/src/rufus.h @@ -698,6 +698,7 @@ extern HANDLE CreateFileWithTimeout(LPCSTR lpFileName, DWORD dwDesiredAccess, DW HANDLE hTemplateFile, DWORD dwTimeOut); extern BOOL SetThreadAffinity(DWORD_PTR* thread_affinity, size_t num_threads); extern BOOL HashFile(const unsigned type, const char* path, uint8_t* sum); +extern BOOL PE256File(const char* path, uint8_t* hash); extern BOOL HashBuffer(const unsigned type, const uint8_t* buf, const size_t len, uint8_t* sum); extern BOOL IsFileInDB(const char* path); extern BOOL IsBufferInDB(const unsigned char* buf, const size_t len); diff --git a/src/rufus.rc b/src/rufus.rc index e434bd14..45f19d23 100644 --- a/src/rufus.rc +++ b/src/rufus.rc @@ -33,7 +33,7 @@ LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL IDD_DIALOG DIALOGEX 12, 12, 232, 326 STYLE DS_SETFONT | DS_MODALFRAME | DS_CENTER | WS_MINIMIZEBOX | WS_POPUP | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_ACCEPTFILES -CAPTION "Rufus 4.2.2048" +CAPTION "Rufus 4.2.2049" FONT 9, "Segoe UI Symbol", 400, 0, 0x0 BEGIN LTEXT "Drive Properties",IDS_DRIVE_PROPERTIES_TXT,8,6,53,12,NOT WS_GROUP @@ -392,8 +392,8 @@ END // VS_VERSION_INFO VERSIONINFO - FILEVERSION 4,2,2048,0 - PRODUCTVERSION 4,2,2048,0 + FILEVERSION 4,2,2049,0 + PRODUCTVERSION 4,2,2049,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -411,13 +411,13 @@ BEGIN VALUE "Comments", "https://rufus.ie" VALUE "CompanyName", "Akeo Consulting" VALUE "FileDescription", "Rufus" - VALUE "FileVersion", "4.2.2048" + VALUE "FileVersion", "4.2.2049" VALUE "InternalName", "Rufus" VALUE "LegalCopyright", "© 2011-2023 Pete Batard (GPL v3)" VALUE "LegalTrademarks", "https://www.gnu.org/licenses/gpl-3.0.html" VALUE "OriginalFilename", "rufus-4.2.exe" VALUE "ProductName", "Rufus" - VALUE "ProductVersion", "4.2.2048" + VALUE "ProductVersion", "4.2.2049" END END BLOCK "VarFileInfo"