Files

409 lines
18 KiB
C

// Userspace PE32+ image loader. The kernel sees only generic process memory and execution state.
#include <ntlite/pe_loader.h>
#include <ntlite/syscall.h>
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <string.h>
#define PE_DOS_MAGIC 0x5A4Du
#define PE_SIGNATURE 0x00004550u
#define PE_MACHINE_AMD64 0x8664u
#define PE_FILE_RELOCS_STRIPPED 0x0001u
#define PE_OPTIONAL_MAGIC_PE32_PLUS 0x020Bu
#define PE_OPTIONAL_MIN_BYTES 112u
#define PE_SECTION_BYTES 40u
#define PE_DIRECTORY_IMPORT 1u
#define PE_DIRECTORY_RELOC 5u
#define PE_DIRECTORY_DELAY_IMPORT 13u
#define PE_ORDINAL_FLAG64 0x8000000000000000ull
#define PE_RELOC_DIR64 10u
#define PE_PAGE_SIZE 4096u
#define PE_SCN_MEM_EXECUTE 0x20000000u
#define PE_SCN_MEM_READ 0x40000000u
#define PE_SCN_MEM_WRITE 0x80000000u
typedef struct PE_VIEW {
const uint8_t* File;
size_t FileSize;
size_t OptionalOffset;
size_t SectionOffset;
uint16_t SectionCount;
uint16_t OptionalSize;
uint16_t FileCharacteristics;
uint32_t DirectoryCount;
uint64_t ImageBase;
uint32_t ImageSize;
uint32_t HeaderSize;
uint32_t EntryRva;
} PE_VIEW;
static bool PeRange(size_t offset, size_t length, size_t size) {
return offset <= size && length <= size - offset;
}
static uint16_t PeU16(const void* p) { uint16_t v; memcpy(&v, p, sizeof(v)); return v; }
static uint32_t PeU32(const void* p) { uint32_t v; memcpy(&v, p, sizeof(v)); return v; }
static uint64_t PeU64(const void* p) { uint64_t v; memcpy(&v, p, sizeof(v)); return v; }
static void PeW64(void* p, uint64_t v) { memcpy(p, &v, sizeof(v)); }
static bool PeImageRange(uint32_t rva, size_t length, size_t imageSize) {
return (size_t)rva <= imageSize && length <= imageSize - (size_t)rva;
}
static bool PeImageString(const uint8_t* image, size_t imageSize, uint32_t rva, size_t* lengthOut) {
if ((size_t)rva >= imageSize) return false;
for (size_t i = rva; i < imageSize; ++i) {
if (image[i] == 0) {
if (lengthOut) *lengthOut = i - rva;
return true;
}
}
return false;
}
static KSTATUS PeParse(const void* file, size_t fileSize, PE_VIEW* out) {
if (!file || !out || fileSize < 64u) return NTLSTATUS_FAILURE;
const uint8_t* b = (const uint8_t*)file;
if (PeU16(b) != PE_DOS_MAGIC) return NTLSTATUS_FAILURE;
size_t nt = PeU32(b + 0x3cu);
if (!PeRange(nt, 24u, fileSize) || PeU32(b + nt) != PE_SIGNATURE) return NTLSTATUS_FAILURE;
if (PeU16(b + nt + 4u) != PE_MACHINE_AMD64) return NTLSTATUS_NOT_SUPPORTED;
uint16_t sections = PeU16(b + nt + 6u);
uint16_t optionalSize = PeU16(b + nt + 20u);
uint16_t fileCharacteristics = PeU16(b + nt + 22u);
size_t optional = nt + 24u;
if (optionalSize < PE_OPTIONAL_MIN_BYTES || !PeRange(optional, optionalSize, fileSize)) return NTLSTATUS_FAILURE;
if (PeU16(b + optional) != PE_OPTIONAL_MAGIC_PE32_PLUS) return NTLSTATUS_NOT_SUPPORTED;
uint64_t imageBase = PeU64(b + optional + 24u);
uint32_t imageSize = PeU32(b + optional + 56u);
uint32_t headerSize = PeU32(b + optional + 60u);
uint32_t entryRva = PeU32(b + optional + 16u);
uint32_t directories = PeU32(b + optional + 108u);
uint32_t available = (uint32_t)((optionalSize - PE_OPTIONAL_MIN_BYTES) / 8u);
if (!imageBase || !imageSize || !headerSize || headerSize > imageSize || entryRva >= imageSize || directories > available)
return NTLSTATUS_FAILURE;
if (headerSize > fileSize) return NTLSTATUS_FAILURE;
if (directories > PE_DIRECTORY_DELAY_IMPORT) {
const uint8_t* delay = b + optional + PE_OPTIONAL_MIN_BYTES + PE_DIRECTORY_DELAY_IMPORT * 8u;
if (PeU32(delay) || PeU32(delay + 4u)) return NTLSTATUS_NOT_SUPPORTED;
}
size_t sectionOffset = optional + optionalSize;
size_t sectionBytes = (size_t)sections * PE_SECTION_BYTES;
if (sections && sectionBytes / PE_SECTION_BYTES != sections) return NTLSTATUS_FAILURE;
if (!PeRange(sectionOffset, sectionBytes, fileSize)) return NTLSTATUS_FAILURE;
for (uint16_t i = 0; i < sections; ++i) {
const uint8_t* sec = b + sectionOffset + (size_t)i * PE_SECTION_BYTES;
uint32_t va = PeU32(sec + 12u);
uint32_t rawSize = PeU32(sec + 16u);
uint32_t raw = PeU32(sec + 20u);
uint32_t virtualSize = PeU32(sec + 8u);
uint32_t span = virtualSize > rawSize ? virtualSize : rawSize;
if ((va & (PE_PAGE_SIZE - 1u)) != 0) return NTLSTATUS_NOT_SUPPORTED;
if (!PeImageRange(va, span, imageSize)) return NTLSTATUS_FAILURE;
if (rawSize && !PeRange(raw, rawSize, fileSize)) return NTLSTATUS_FAILURE;
}
memset(out, 0, sizeof(*out));
out->File = b;
out->FileSize = fileSize;
out->OptionalOffset = optional;
out->SectionOffset = sectionOffset;
out->SectionCount = sections;
out->OptionalSize = optionalSize;
out->FileCharacteristics = fileCharacteristics;
out->DirectoryCount = directories;
out->ImageBase = imageBase;
out->ImageSize = imageSize;
out->HeaderSize = headerSize;
out->EntryRva = entryRva;
return NTLSTATUS_SUCCESS;
}
static bool PeDirectory(const PE_VIEW* view, uint32_t index, uint32_t* rva, uint32_t* size) {
if (!view || index >= view->DirectoryCount) return false;
const uint8_t* d = view->File + view->OptionalOffset + PE_OPTIONAL_MIN_BYTES + (size_t)index * 8u;
if (rva) *rva = PeU32(d);
if (size) *size = PeU32(d + 4u);
return true;
}
static KSTATUS PeAllocCurrent(size_t bytes, void** out) {
if (!out || !bytes) return NTLSTATUS_INVALID_PARAMETER;
size_t pages = (bytes + PE_PAGE_SIZE - 1u) / PE_PAGE_SIZE;
SYS_ALLOC_VM_REQUEST req;
memset(&req, 0, sizeof(req));
req.Size = sizeof(req);
req.Protect = SYS_VM_READWRITE;
req.Flags = SYS_VM_FLAG_NONE;
req.PageCount = pages;
req.BaseOut = out;
return SysAllocVm(&req);
}
static KSTATUS PeReadFile(SYS_HANDLE file, void** bufferOut, size_t* sizeOut) {
if (!bufferOut || !sizeOut) return NTLSTATUS_INVALID_PARAMETER;
*bufferOut = 0; *sizeOut = 0;
uint64_t fileSize64 = 0;
KSTATUS s = SysGetFileSize(file, &fileSize64);
if (!KSUCCESS(s)) return s;
if (!fileSize64 || fileSize64 > (uint64_t)SIZE_MAX) return NTLSTATUS_BUFFER_TOO_SMALL;
size_t fileSize = (size_t)fileSize64;
void* buffer = 0;
s = PeAllocCurrent(fileSize, &buffer);
if (!KSUCCESS(s)) return s;
s = SysSeekFile(file, 0);
if (!KSUCCESS(s)) { SysFreeVm(buffer); return s; }
size_t got = 0;
SYS_FILE_IO_REQUEST read = SysMakeFileIoRequest(file, buffer, fileSize, &got);
s = SysReadFile(&read);
if (!KSUCCESS(s) || got != fileSize) {
SysFreeVm(buffer);
return KSUCCESS(s) ? NTLSTATUS_DEVICE_ERROR : s;
}
*bufferOut = buffer; *sizeOut = fileSize;
return NTLSTATUS_SUCCESS;
}
static KSTATUS PeRelocate(uint8_t* image, size_t imageSize, const PE_VIEW* view, uintptr_t actualBase) {
uintptr_t delta = actualBase - (uintptr_t)view->ImageBase;
if (!delta) return NTLSTATUS_SUCCESS;
uint32_t rva = 0, size = 0;
if (!PeDirectory(view, PE_DIRECTORY_RELOC, &rva, &size) || !rva || !size) {
// An image with no base fixups can still be position-independent. Only the
// COFF RELOCS_STRIPPED bit says missing relocation data makes rebasing invalid.
return (view->FileCharacteristics & PE_FILE_RELOCS_STRIPPED)
? NTLSTATUS_NOT_SUPPORTED
: NTLSTATUS_SUCCESS;
}
if (!PeImageRange(rva, size, imageSize)) return NTLSTATUS_FAILURE;
size_t offset = rva;
size_t end = (size_t)rva + size;
while (offset < end) {
if (end - offset < 8u) return NTLSTATUS_FAILURE;
uint32_t pageRva = PeU32(image + offset);
uint32_t blockSize = PeU32(image + offset + 4u);
if (blockSize < 8u || blockSize > end - offset || ((blockSize - 8u) & 1u)) return NTLSTATUS_FAILURE;
uint32_t count = (blockSize - 8u) / 2u;
for (uint32_t i = 0; i < count; ++i) {
uint16_t e = PeU16(image + offset + 8u + (size_t)i * 2u);
uint16_t type = e >> 12;
uint16_t pageOffset = e & 0x0fffu;
if (type == 0) continue;
if (type != PE_RELOC_DIR64) return NTLSTATUS_NOT_SUPPORTED;
uint32_t fixRva = pageRva + pageOffset;
if (fixRva < pageRva || !PeImageRange(fixRva, 8u, imageSize)) return NTLSTATUS_FAILURE;
PeW64(image + fixRva, PeU64(image + fixRva) + delta);
}
offset += blockSize;
}
return offset == end ? NTLSTATUS_SUCCESS : NTLSTATUS_FAILURE;
}
static KSTATUS PeBindImports(uint8_t* image, size_t imageSize, const PE_VIEW* view,
NTL_PE_IMPORT_RESOLVER resolver, void* context) {
uint32_t rva = 0, size = 0;
if (!PeDirectory(view, PE_DIRECTORY_IMPORT, &rva, &size) || !rva || !size)
return NTLSTATUS_SUCCESS;
if (!resolver) return NTLSTATUS_NOT_FOUND;
if (!PeImageRange(rva, size, imageSize)) return NTLSTATUS_FAILURE;
size_t offset = rva;
size_t end = (size_t)rva + size;
while (offset + 20u <= end) {
uint8_t* d = image + offset;
uint32_t originalThunk = PeU32(d + 0u);
uint32_t nameRva = PeU32(d + 12u);
uint32_t firstThunk = PeU32(d + 16u);
if (!originalThunk && !nameRva && !firstThunk) return NTLSTATUS_SUCCESS;
size_t moduleLen = 0;
if (!nameRva || !PeImageString(image, imageSize, nameRva, &moduleLen)) return NTLSTATUS_FAILURE;
uint32_t sourceThunk = originalThunk ? originalThunk : firstThunk;
if (!sourceThunk || !firstThunk) return NTLSTATUS_FAILURE;
for (uint32_t index = 0;; ++index) {
size_t sourceRva = (size_t)sourceThunk + (size_t)index * 8u;
size_t targetRva = (size_t)firstThunk + (size_t)index * 8u;
if (sourceRva + 8u > imageSize || targetRva + 8u > imageSize) return NTLSTATUS_FAILURE;
uint64_t thunk = PeU64(image + sourceRva);
if (!thunk) break;
if (thunk & PE_ORDINAL_FLAG64) return NTLSTATUS_NOT_SUPPORTED;
uint32_t hintNameRva = (uint32_t)thunk;
if (!PeImageRange(hintNameRva, 3u, imageSize)) return NTLSTATUS_FAILURE;
size_t symbolLen = 0;
if (!PeImageString(image, imageSize, hintNameRva + 2u, &symbolLen)) return NTLSTATUS_FAILURE;
uintptr_t address = 0;
KSTATUS s = resolver(context,
SysSvFromParts((const char*)image + nameRva, moduleLen),
SysSvFromParts((const char*)image + hintNameRva + 2u, symbolLen),
&address);
if (!KSUCCESS(s)) return s;
if (!address) return NTLSTATUS_NOT_FOUND;
PeW64(image + targetRva, address);
}
offset += 20u;
}
return NTLSTATUS_FAILURE;
}
static uint32_t PeSectionProtect(uint32_t characteristics) {
bool r = (characteristics & PE_SCN_MEM_READ) != 0;
bool w = (characteristics & PE_SCN_MEM_WRITE) != 0;
bool x = (characteristics & PE_SCN_MEM_EXECUTE) != 0;
if (x && w) return SYS_VM_EXECUTE_READWRITE;
if (x && r) return SYS_VM_EXECUTE_READ;
if (x) return SYS_VM_EXECUTE;
if (w) return SYS_VM_READWRITE;
if (r) return SYS_VM_READ;
return SYS_VM_NOACCESS;
}
static KSTATUS PeProtectRange(SYS_HANDLE process, void* base, size_t pages, uint32_t protect) {
if (process != SYS_HANDLE_INVALID)
return SysProtectProcessVm(process, base, pages, protect);
return SysProtectVm(base, pages, protect, (uint32_t*)0);
}
static KSTATUS PeProtectImage(SYS_HANDLE process, uint8_t* image, const PE_VIEW* view) {
size_t pages = ((size_t)view->ImageSize + PE_PAGE_SIZE - 1u) / PE_PAGE_SIZE;
KSTATUS s = PeProtectRange(process, image, pages, SYS_VM_READ);
if (!KSUCCESS(s)) return s;
for (uint16_t i = 0; i < view->SectionCount; ++i) {
const uint8_t* sec = view->File + view->SectionOffset + (size_t)i * PE_SECTION_BYTES;
uint32_t va = PeU32(sec + 12u);
uint32_t virtualSize = PeU32(sec + 8u);
uint32_t rawSize = PeU32(sec + 16u);
uint32_t span = virtualSize > rawSize ? virtualSize : rawSize;
if (!span) continue;
size_t sectionPages = ((size_t)span + PE_PAGE_SIZE - 1u) / PE_PAGE_SIZE;
s = PeProtectRange(process, image + va, sectionPages, PeSectionProtect(PeU32(sec + 36u)));
if (!KSUCCESS(s)) return s;
}
return NTLSTATUS_SUCCESS;
}
static KSTATUS PeAllocateImage(SYS_HANDLE process, size_t pages, void** baseOut) {
if (process == SYS_HANDLE_INVALID) {
SYS_ALLOC_VM_REQUEST alloc;
memset(&alloc, 0, sizeof(alloc));
alloc.Size = sizeof(alloc);
alloc.Protect = SYS_VM_READWRITE;
alloc.Flags = SYS_VM_FLAG_NONE;
alloc.PageCount = pages;
alloc.BaseOut = baseOut;
return SysAllocVm(&alloc);
}
SYS_ALLOC_PROCESS_VM_REQUEST alloc;
memset(&alloc, 0, sizeof(alloc));
alloc.Size = sizeof(alloc);
alloc.ProcessHandle = process;
alloc.Protect = SYS_VM_READWRITE;
alloc.Flags = SYS_VM_FLAG_NONE;
alloc.PageCount = pages;
alloc.BaseOut = baseOut;
return SysAllocProcessVm(&alloc);
}
KSTATUS NtlLoadPeImage(const NTL_PE_LOAD_REQUEST* request) {
if (!request || request->Size != sizeof(*request) || request->Flags != NTL_PE_LOAD_FLAG_NONE ||
request->FileHandle == SYS_HANDLE_INVALID || !request->ImageOut)
return NTLSTATUS_INVALID_PARAMETER;
void* file = 0;
size_t fileSize = 0;
KSTATUS s = PeReadFile(request->FileHandle, &file, &fileSize);
if (!KSUCCESS(s)) return s;
PE_VIEW view;
s = PeParse(file, fileSize, &view);
if (!KSUCCESS(s)) { SysFreeVm(file); return s; }
size_t pages = ((size_t)view.ImageSize + PE_PAGE_SIZE - 1u) / PE_PAGE_SIZE;
void* imageBase = 0;
s = PeAllocateImage(request->ProcessHandle, pages, &imageBase);
if (!KSUCCESS(s)) { SysFreeVm(file); return s; }
uint8_t* image = (uint8_t*)imageBase;
memset(image, 0, pages * PE_PAGE_SIZE);
memcpy(image, file, view.HeaderSize);
for (uint16_t i = 0; i < view.SectionCount; ++i) {
const uint8_t* sec = view.File + view.SectionOffset + (size_t)i * PE_SECTION_BYTES;
uint32_t va = PeU32(sec + 12u);
uint32_t rawSize = PeU32(sec + 16u);
uint32_t raw = PeU32(sec + 20u);
if (rawSize) memcpy(image + va, (const uint8_t*)file + raw, rawSize);
}
s = PeRelocate(image, view.ImageSize, &view, (uintptr_t)image);
if (KSUCCESS(s)) {
request->ImageOut->Base = (uintptr_t)image;
request->ImageOut->Size = view.ImageSize;
request->ImageOut->EntryPoint = (uintptr_t)image + view.EntryRva;
request->ImageOut->PreferredBase = view.ImageBase;
if (request->Prepared)
s = request->Prepared(request->PreparedContext, request->ImageOut);
}
if (KSUCCESS(s)) s = PeBindImports(image, view.ImageSize, &view, request->ResolveImport, request->ResolveContext);
if (KSUCCESS(s)) s = PeProtectImage(request->ProcessHandle, image, &view);
if (!KSUCCESS(s) && request->ProcessHandle == SYS_HANDLE_INVALID)
(void)SysFreeVm(image);
SysFreeVm(file);
return s;
}
KSTATUS NtlGetPeExport(uintptr_t imageBase, SYS_STRING_VIEW name, uintptr_t* addressOut) {
if (!imageBase || !addressOut || !SysSvIsValid(name) || name.Length == 0 ||
SysSvContainsByte(name, '\0'))
return NTLSTATUS_INVALID_PARAMETER;
*addressOut = 0;
const uint8_t* image = (const uint8_t*)imageBase;
if (PeU16(image) != PE_DOS_MAGIC) return NTLSTATUS_FAILURE;
uint32_t nt = PeU32(image + 0x3cu);
if (PeU32(image + nt) != PE_SIGNATURE || PeU16(image + nt + 4u) != PE_MACHINE_AMD64)
return NTLSTATUS_FAILURE;
const uint8_t* optional = image + nt + 24u;
if (PeU16(optional) != PE_OPTIONAL_MAGIC_PE32_PLUS) return NTLSTATUS_FAILURE;
uint32_t imageSize = PeU32(optional + 56u);
uint32_t directories = PeU32(optional + 108u);
if (directories < 1u) return NTLSTATUS_NOT_FOUND;
uint32_t exportRva = PeU32(optional + PE_OPTIONAL_MIN_BYTES);
uint32_t exportSize = PeU32(optional + PE_OPTIONAL_MIN_BYTES + 4u);
if (!exportRva || !exportSize || !PeImageRange(exportRva, exportSize, imageSize) ||
!PeImageRange(exportRva, 40u, imageSize))
return NTLSTATUS_NOT_FOUND;
const uint8_t* exp = image + exportRva;
uint32_t functionCount = PeU32(exp + 20u);
uint32_t nameCount = PeU32(exp + 24u);
uint32_t functionsRva = PeU32(exp + 28u);
uint32_t namesRva = PeU32(exp + 32u);
uint32_t ordinalsRva = PeU32(exp + 36u);
if (!PeImageRange(functionsRva, (size_t)functionCount * 4u, imageSize) ||
!PeImageRange(namesRva, (size_t)nameCount * 4u, imageSize) ||
!PeImageRange(ordinalsRva, (size_t)nameCount * 2u, imageSize))
return NTLSTATUS_FAILURE;
for (uint32_t i = 0; i < nameCount; ++i) {
uint32_t nameRva = PeU32(image + namesRva + (size_t)i * 4u);
size_t exportedLength = 0;
if (!PeImageString(image, imageSize, nameRva, &exportedLength)) return NTLSTATUS_FAILURE;
if (exportedLength != name.Length || memcmp(image + nameRva, name.Data, name.Length) != 0)
continue;
uint16_t ordinal = PeU16(image + ordinalsRva + (size_t)i * 2u);
if (ordinal >= functionCount) return NTLSTATUS_FAILURE;
uint32_t functionRva = PeU32(image + functionsRva + (size_t)ordinal * 4u);
if (!functionRva || !PeImageRange(functionRva, 1u, imageSize)) return NTLSTATUS_NOT_FOUND;
if (functionRva >= exportRva && functionRva < exportRva + exportSize)
return NTLSTATUS_NOT_SUPPORTED;
*addressOut = imageBase + functionRva;
return NTLSTATUS_SUCCESS;
}
return NTLSTATUS_NOT_FOUND;
}