cosmopolitan/tool/build/pecheck.c
2023-08-11 04:39:19 -07:00

331 lines
15 KiB
C

/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
│vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi│
╞══════════════════════════════════════════════════════════════════════════════╡
│ Copyright 2023 Justine Alexandra Roberts Tunney │
│ │
│ Permission to use, copy, modify, and/or distribute this software for │
│ any purpose with or without fee is hereby granted, provided that the │
│ above copyright notice and this permission notice appear in all copies. │
│ │
│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL │
│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED │
│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE │
│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL │
│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR │
│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER │
│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │
│ PERFORMANCE OF THIS SOFTWARE. │
╚─────────────────────────────────────────────────────────────────────────────*/
#include "libc/calls/calls.h"
#include "libc/intrin/bits.h"
#include "libc/intrin/kprintf.h"
#include "libc/limits.h"
#include "libc/nt/struct/imageimportbyname.internal.h"
#include "libc/nt/struct/imageimportdescriptor.internal.h"
#include "libc/nt/struct/imagentheaders.internal.h"
#include "libc/nt/struct/imageoptionalheader.internal.h"
#include "libc/nt/struct/imagesectionheader.internal.h"
#include "libc/runtime/runtime.h"
#include "libc/stdckdint.h"
#include "libc/stdio/stdio.h"
#include "libc/str/str.h"
#include "libc/sysv/consts/map.h"
#include "libc/sysv/consts/o.h"
#include "libc/sysv/consts/prot.h"
/**
* @fileoverview Linter for PE static executable files.
*
* Generating PE files from scratch is tricky. There's numerous things
* that can go wrong, and operating systems don't explain what's wrong
* when they refuse to run a program. This program can help illuminate
* any issues with your generated binaries, with better error messages
*/
struct Exe {
char *map;
size_t size;
const char *path;
struct NtImageNtHeaders *pe;
struct NtImageSectionHeader *sections;
uint32_t section_count;
};
static wontreturn void Die(const char *thing, const char *reason) {
tinyprint(2, thing, ": ", reason, "\n", NULL);
exit(1);
}
static wontreturn void DieSys(const char *thing) {
perror(thing);
exit(1);
}
static void LogPeSections(FILE *f, struct NtImageSectionHeader *p, size_t n) {
size_t i;
fprintf(f, "Name Offset RelativeVirtAddr FileSiz MemSiz Flg\n");
for (i = 0; i < n; ++i) {
fprintf(f, "%-8.8s 0x%06lx 0x%016lx 0x%06lx 0x%06lx %c%c%c\n", p[i].Name,
p[i].PointerToRawData, p[i].VirtualAddress, p[i].SizeOfRawData,
p[i].Misc.VirtualSize,
p[i].Characteristics & kNtPeSectionMemRead ? 'R' : ' ',
p[i].Characteristics & kNtPeSectionMemWrite ? 'W' : ' ',
p[i].Characteristics & kNtPeSectionMemExecute ? 'E' : ' ');
}
}
// resolves relative virtual address
//
// this is a trivial process when an executable has been loaded properly
// i.e. a separate mmap() call was made for each individual section; but
// we've only mapped the executable file itself into memory; thus, we'll
// need to remap a virtual address into a file offset to get the pointer
//
// returns pointer to image data, or null on error
static void *GetRva(struct Exe *exe, uint32_t rva, uint32_t size) {
int i;
for (i = 0; i < exe->section_count; ++i) {
if (exe->sections[i].VirtualAddress <= rva &&
rva < exe->sections[i].VirtualAddress +
exe->sections[i].Misc.VirtualSize) {
if (rva + size <=
exe->sections[i].VirtualAddress + exe->sections[i].Misc.VirtualSize) {
return exe->map + exe->sections[i].PointerToRawData +
(rva - exe->sections[i].VirtualAddress);
} else {
break;
}
}
}
return 0;
}
static bool HasControlCodes(const char *s) {
int c;
while ((c = *s++)) {
if (isascii(c) && iscntrl(c)) {
return true;
}
}
return false;
}
static void CheckPeImportByName(struct Exe *exe, uint32_t rva) {
struct NtImageImportByName *hintname;
if (rva & 1)
Die(exe->path, "PE IMAGE_IMPORT_BY_NAME (hint name) structures must "
"be 2-byte aligned");
if (!(hintname = GetRva(exe, rva, sizeof(struct NtImageImportByName))))
Die(exe->path, "PE import table RVA entry didn't reslove");
if (!*hintname->Name)
Die(exe->path, "PE imported function name is empty string");
if (HasControlCodes(hintname->Name))
Die(exe->path, "PE imported function name contains ascii control codes");
}
static void CheckPe(const char *path, char *map, size_t size) {
int pagesz = 4096;
// sanity check mz header
if (size < 64) //
Die(path, "Image too small for MZ header");
if (READ16LE(map) != ('M' | 'Z' << 8))
Die(path, "Image doesn't start with MZ");
uint32_t pe_offset;
if ((pe_offset = READ32LE(map + 60)) >= size)
Die(path, "PE header offset points past end of image");
if (pe_offset & 7)
Die(path, "PE header offset must possess an 8-byte alignment");
if (pe_offset + sizeof(struct NtImageNtHeaders) > size)
Die(path, "PE mandatory headers overlap end of image");
struct NtImageNtHeaders *pe = (struct NtImageNtHeaders *)(map + pe_offset);
if ((pe_offset + sizeof(struct NtImageFileHeader) + 4 +
pe->FileHeader.SizeOfOptionalHeader) > size)
Die(path, "PE optional header size overlaps end of image");
// sanity check pe header
if (pe->Signature != ('P' | 'E' << 8))
Die(path, "PE Signature must be 0x00004550");
if (!(pe->FileHeader.Characteristics & kNtPeFileExecutableImage))
Die(path, "PE Characteristics must have executable bit set");
if (pe->FileHeader.Characteristics & kNtPeFileDll)
Die(path, "PE Characteristics can't have DLL bit set");
if (pe->FileHeader.NumberOfSections < 1)
Die(path, "PE NumberOfSections >= 1 must be the case");
if (pe->OptionalHeader.Magic != kNtPe64bit)
Die(path, "PE OptionalHeader Magic must be 0x020b");
if (pe->OptionalHeader.FileAlignment < 512)
Die(path, "PE FileAlignment must be at least 512");
if (pe->OptionalHeader.FileAlignment > 65536)
Die(path, "PE FileAlignment can't exceed 65536");
if (pe->OptionalHeader.FileAlignment & (pe->OptionalHeader.FileAlignment - 1))
Die(path, "PE FileAlignment must be a two power");
if (pe->OptionalHeader.SectionAlignment &
(pe->OptionalHeader.SectionAlignment - 1))
Die(path, "PE SectionAlignment must be a two power");
if (pe->OptionalHeader.SectionAlignment < pe->OptionalHeader.FileAlignment)
Die(path, "PE SectionAlignment >= FileAlignment must be the case");
if (pe->OptionalHeader.SectionAlignment < pagesz &&
pe->OptionalHeader.SectionAlignment != pe->OptionalHeader.FileAlignment)
Die(path, "PE SectionAlignment must equal FileAlignment if it's less than "
"the microprocessor architecture's page size");
if (pe->OptionalHeader.ImageBase & 65535)
Die(path, "PE ImageBase must be multiple of 65536");
if (pe->OptionalHeader.ImageBase > INT_MAX &&
!(pe->FileHeader.Characteristics & kNtImageFileLargeAddressAware))
Die(path, "PE FileHeader.Characteristics needs "
"IMAGE_FILE_LARGE_ADDRESS_AWARE if ImageBase > INT_MAX");
// validate the size of the pe optional headers
int len;
if (ckd_mul(&len, pe->OptionalHeader.NumberOfRvaAndSizes,
sizeof(struct NtImageDataDirectory)) ||
ckd_add(&len, len, sizeof(struct NtImageOptionalHeader)))
Die(path, "encountered overflow computing PE SizeOfOptionalHeader");
if (pe->FileHeader.SizeOfOptionalHeader != len)
Die(path, "PE SizeOfOptionalHeader had incorrect value");
if (len > size || (char *)&pe->OptionalHeader + len > map + size)
Die(path, "PE OptionalHeader overflows image");
// perform even more pe validation
if (pe->OptionalHeader.SizeOfImage &
(pe->OptionalHeader.SectionAlignment - 1))
Die(path, "PE SizeOfImage must be multiple of SectionAlignment");
if (pe->OptionalHeader.SizeOfHeaders & (pe->OptionalHeader.FileAlignment - 1))
Die(path, "PE SizeOfHeaders must be multiple of FileAlignment");
if (pe->OptionalHeader.SizeOfHeaders > pe->OptionalHeader.AddressOfEntryPoint)
Die(path, "PE SizeOfHeaders <= AddressOfEntryPoint must be the case");
if (pe->OptionalHeader.SizeOfHeaders >= pe->OptionalHeader.SizeOfImage)
Die(path, "PE SizeOfHeaders < SizeOfImage must be the case");
if (pe->OptionalHeader.SizeOfStackCommit >> 32)
Die(path, "PE SizeOfStackReserve can't exceed 4GB");
if (pe->OptionalHeader.SizeOfStackReserve >> 32)
Die(path, "PE SizeOfStackReserve can't exceed 4GB");
if (pe->OptionalHeader.SizeOfHeapCommit >> 32)
Die(path, "PE SizeOfHeapReserve can't exceed 4GB");
if (pe->OptionalHeader.SizeOfHeapReserve >> 32)
Die(path, "PE SizeOfHeapReserve can't exceed 4GB");
// check pe section headers
struct NtImageSectionHeader *sections =
(struct NtImageSectionHeader *)((char *)&pe->OptionalHeader +
pe->FileHeader.SizeOfOptionalHeader);
for (int i = 0; i < pe->FileHeader.NumberOfSections; ++i) {
if (sections[i].SizeOfRawData & (pe->OptionalHeader.FileAlignment - 1))
Die(path, "PE SizeOfRawData should be multiple of FileAlignment");
if (sections[i].PointerToRawData & (pe->OptionalHeader.FileAlignment - 1))
Die(path, "PE PointerToRawData must be multiple of FileAlignment");
if (map + sections[i].PointerToRawData >= map + size)
Die(path, "PE PointerToRawData points outside image");
if (map + sections[i].PointerToRawData + sections[i].SizeOfRawData >
map + size)
Die(path, "PE SizeOfRawData overlaps end of image");
if (!sections[i].VirtualAddress)
Die(path, "PE VirtualAddress shouldn't be zero");
if (sections[i].VirtualAddress & (pe->OptionalHeader.SectionAlignment - 1))
Die(path, "PE VirtualAddress must be multiple of SectionAlignment");
if ((sections[i].Characteristics &
(kNtPeSectionCntCode | kNtPeSectionCntInitializedData |
kNtPeSectionCntUninitializedData)) ==
kNtPeSectionCntUninitializedData) {
if (sections[i].SizeOfRawData)
Die(path, "PE SizeOfRawData should be zero for pure BSS section");
if (sections[i].SizeOfRawData)
Die(path, "PE PointerToRawData should be zero for pure BSS section");
}
if (!i) {
if (sections[i].VirtualAddress !=
((pe->OptionalHeader.SizeOfHeaders +
(pe->OptionalHeader.SectionAlignment - 1)) &
-pe->OptionalHeader.SectionAlignment))
Die(path, "PE VirtualAddress of first section must be SizeOfHeaders "
"rounded up to SectionAlignment");
} else {
if (sections[i].VirtualAddress !=
sections[i - 1].VirtualAddress +
((sections[i - 1].Misc.VirtualSize +
(pe->OptionalHeader.SectionAlignment - 1)) &
-pe->OptionalHeader.SectionAlignment))
Die(path, "PE sections must be in ascending order and the virtual "
"memory they define must be adjacent after VirtualSize is "
"rounded up to the SectionAlignment");
}
}
// create an object for our portable executable
struct Exe exe[1] = {{
.pe = pe,
.path = path,
.map = map,
.size = size,
.sections = sections,
.section_count = pe->FileHeader.NumberOfSections,
}};
// validate dll imports
struct NtImageDataDirectory *ddImports =
exe->pe->OptionalHeader.DataDirectory + kNtImageDirectoryEntryImport;
if (exe->pe->OptionalHeader.NumberOfRvaAndSizes >= 2 && ddImports->Size) {
if (ddImports->Size % sizeof(struct NtImageImportDescriptor) != 0)
Die(exe->path, "PE Imports data directory entry Size should be a "
"multiple of sizeof(IMAGE_IMPORT_DESCRIPTOR)");
if (ddImports->VirtualAddress & 3)
Die(exe->path, "PE IMAGE_IMPORT_DESCRIPTOR table must be 4-byte aligned");
struct NtImageImportDescriptor *idt;
if (!(idt = GetRva(exe, ddImports->VirtualAddress, ddImports->Size)))
Die(exe->path, "couldn't resolve VirtualAddress/Size RVA of PE Import "
"Directory Table to within a defined PE section");
if (idt->ImportLookupTable >= exe->size)
Die(exe->path, "Import Directory Table VirtualAddress/Size RVA resolved "
"to dense unrelated binary content");
for (int i = 0; idt->ImportLookupTable; ++i, ++idt) {
char *dllname;
if (!(dllname = GetRva(exe, idt->DllNameRva, 2)))
Die(exe->path, "PE DllNameRva doesn't resolve to a PE section");
if (!*dllname)
Die(exe->path, "PE import DllNameRva pointed to empty string");
if (HasControlCodes(dllname))
Die(exe->path, "PE import DllNameRva contained ascii control codes");
if (idt->ImportLookupTable & 7)
Die(exe->path, "PE ImportLookupTable must be 8-byte aligned");
if (idt->ImportAddressTable & 7)
Die(exe->path, "PE ImportAddressTable must be 8-byte aligned");
uint64_t *ilt, *iat;
if (!(ilt = GetRva(exe, idt->ImportLookupTable, 8)))
Die(exe->path, "PE ImportLookupTable RVA didn't resolve to a section");
if (!(iat = GetRva(exe, idt->ImportAddressTable, 8)))
Die(exe->path, "PE ImportAddressTable RVA didn't resolve to a section");
for (int j = 0;; ++j, ++ilt, ++iat) {
if (*ilt != *iat) {
kprintf("i=%d j=%d ilt=%#x iat=%#x\n", i, j, *ilt, *iat);
Die(exe->path, "PE ImportLookupTable and ImportAddressTable should "
"have identical content");
}
if (!*ilt) break;
CheckPeImportByName(exe, *ilt);
}
}
}
}
int main(int argc, char *argv[]) {
int i, fd;
void *map;
ssize_t size;
const char *path;
#ifndef NDEBUG
ShowCrashReports();
#endif
for (i = 1; i < argc; ++i) {
path = argv[i];
if ((fd = open(path, O_RDONLY)) == -1) DieSys(path);
if ((size = lseek(fd, 0, SEEK_END)) == -1) DieSys(path);
map = mmap(0, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
if (map == MAP_FAILED) DieSys(path);
CheckPe(path, map, size);
if (munmap(map, size)) DieSys(path);
if (close(fd)) DieSys(path);
}
}