[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
This commit is contained in:
Pete Batard 2023-06-13 17:57:34 +02:00
parent 090bfb9770
commit c4b1b23832
No known key found for this signature in database
GPG Key ID: 38E0CF5E69EDD671
5 changed files with 335 additions and 15 deletions

View File

@ -7,6 +7,7 @@
* Copyright © 2002-2015 Wei Dai & Igor Pavlov
* Copyright © 2015-2023 Pete Batard <pete@akeo.ie>
* Copyright © 2022 Jeffrey Walton <noloader@gmail.com>
* 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 = &regs->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(&regs->reg[j + 1], &regs->reg[j],
sizeof(*reg));
break;
}
/* new data overlapping registered region */
uprintf("%s: new region already part of another", __func__);
return FALSE;
}
reg = &regs->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] = &sections[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, &regs))
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

View File

@ -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"

View File

@ -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

View File

@ -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);

View File

@ -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"